package api
import (
"context"
"strings"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
type toldInbox struct{ rows map[string][]AttentionEvent }
func (i *toldInbox) Attend(account string, e AttentionEvent) (uint64, error) {
if i.rows == nil {
i.rows = map[string][]AttentionEvent{}
}
i.rows[account] = append(i.rows[account], e)
return uint64(len(i.rows[account])), nil
}
func (i *toldInbox) TailAttention(string, uint64) (<-chan AttentionEvent, func()) {
return nil, func() {}
}
func (i *toldInbox) AttentionPending(string, uint64) (int, error) { return 0, nil }
func (i *toldInbox) AttentionTip(string) (uint64, error) { return 0, nil }
func (i *toldInbox) RecentAttention(string, int) ([]AttentionEvent, error) {
return nil, nil
}
func (i *toldInbox) said(account string) string {
var b strings.Builder
for _, e := range i.rows[account] {
b.WriteString(e.Summary + "\n")
}
return b.String()
}
// A repository offer was recorded, readable by both sides, and told to
// nobody. The only way to learn one was waiting was to open the team's
// settings page — the one place somebody with nothing pending has no
// reason to visit (ticket 6d176a8c).
//
// Three tellings, because an offer is a question and it has two answers.
func TestARepositoryOfferTellsBothSides(t *testing.T) {
rig := func() (*ops.Registry, *oltp.DB, *toldInbox) {
db, err := oltp.Open(t.TempDir() + "/o.db")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
for _, who := range []string{"lou", "ann"} {
if err := db.AddAccount(who, "a-long-enough-password", false); err != nil {
t.Fatal(err)
}
}
inbox := &toldInbox{}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t)),
Teams: db, Accounts: acctAdapter{db}, Attention: inbox,
CapsFor: func(a, repo string) CapSet { return db.EffectiveCaps(a, repo) }}); err != nil {
t.Fatal(err)
}
return r, db, inbox
}
lou := ops.WithUser(context.Background(), "lou") // team owner
ann := ops.WithUser(context.Background(), "ann") // repo admin, not a member
// OFFERED: the owner who must decide is told.
r, db, inbox := rig()
if _, err := r.Invoke(lou, "team.create", teamCreateIn{Name: "core"}); err != nil {
t.Fatal(err)
}
if err := db.SetCaps("ann", "test", "a"); err != nil {
t.Fatal(err)
}
if _, err := r.Invoke(ann, "team.repo.add", teamRepoIn{Name: "core", Repo: "test"}); err != nil {
t.Fatal(err)
}
if got := inbox.said("lou"); !strings.Contains(got, "ann offered test to core") {
t.Errorf("FAIL: the owner was not told an offer is waiting: %q", got)
} else {
t.Log("ok: the owner who must decide is told")
}
if inbox.said("ann") != "" {
t.Errorf("FAIL: the offerer was told about their own action: %q", inbox.said("ann"))
} else {
t.Log("ok: nobody is told what they just did themselves")
}
// ACCEPTED: the offerer gets their answer.
if _, err := r.Invoke(lou, "team.repo.offer.accept", teamRepoIn{Name: "core", Repo: "test"}); err != nil {
t.Fatal(err)
}
if got := inbox.said("ann"); !strings.Contains(got, "lou accepted test into core") {
t.Errorf("FAIL: the offerer was not told it was accepted: %q", got)
} else {
t.Log("ok: an accepted offer answers the person who asked")
}
// DECLINED: the same question, answered the other way.
r2, db2, inbox2 := rig()
if _, err := r2.Invoke(lou, "team.create", teamCreateIn{Name: "core"}); err != nil {
t.Fatal(err)
}
if err := db2.SetCaps("ann", "test", "a"); err != nil {
t.Fatal(err)
}
if _, err := r2.Invoke(ann, "team.repo.add", teamRepoIn{Name: "core", Repo: "test"}); err != nil {
t.Fatal(err)
}
if _, err := r2.Invoke(lou, "team.repo.offer.decline", teamRepoIn{Name: "core", Repo: "test"}); err != nil {
t.Fatal(err)
}
if got := inbox2.said("ann"); !strings.Contains(got, "lou declined test for core") {
t.Errorf("FAIL: the offerer was not told it was declined: %q", got)
} else {
t.Log("ok: a declined offer answers too, rather than going quiet")
}
}