package api
import (
"path/filepath"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// mentionRig is teamChatRig with a notify plane attached, so what a
// line in a room TELLS somebody can be asserted rather than assumed.
func mentionRig(t *testing.T) (*ops.Registry, *fakeNotify) {
t.Helper()
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
for _, who := range []string{"owen", "mia", "outsider"} {
if err := db.AddAccount(who, "a-long-enough-password", false); err != nil {
t.Fatal(err)
}
}
n := &fakeNotify{}
repos := NewRepos(map[string]*Repo{
"orchard": {Code: "aaaa000000000000000000000000000000000001"}})
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: repos, Teams: db,
Chat: &fakeChat{}, Notify: n, Accounts: acctAdapter{db},
CapsFor: func(a, repo string) CapSet { return db.EffectiveCaps(a, repo) }}); err != nil {
t.Fatal(err)
}
if _, err := r.Invoke(asUser("owen"), "team.create", teamCreateIn{Name: "grove"}); err != nil {
t.Fatal(err)
}
if _, err := r.Invoke(asUser("owen"), "team.member",
teamMemberIn{Name: "grove", Account: "mia"}); err != nil {
t.Fatal(err)
}
return r, n
}
// mentionsOf waits for want mentions and then a moment longer, so a
// test asserting "none" or "exactly two" is not really asserting
// "none YET". fireNotify is best-effort and runs off the op's own
// goroutine, so a bare read races it in both directions.
func mentionsOf(t *testing.T, n *fakeNotify, want int) []NotifyEvent {
t.Helper()
read := func() []NotifyEvent {
var out []NotifyEvent
for _, e := range n.snapshot() {
if e.Workflow == "quarry-mention" {
out = append(out, e)
}
}
return out
}
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if len(read()) >= want {
break
}
time.Sleep(20 * time.Millisecond)
}
// A late extra would make "exactly one" a lie, so give one more
// beat before answering.
time.Sleep(100 * time.Millisecond)
return read()
}
// Naming somebody in a team's room tells them. It told nobody: a room
// is a place people are addressed in, and this one fired no mentions
// at all while the same words in a repository's channel fired both a
// record and a notification.
func TestNamingSomebodyInATeamRoomTellsThem(t *testing.T) {
r, n := mentionRig(t)
if _, err := r.Invoke(asUser("owen"), "team.chat.post",
teamChatPostIn{Name: "grove", Text: "standup in ten @mia"}); err != nil {
t.Fatal(err)
}
got := mentionsOf(t, n, 1)
if len(got) != 1 {
t.Fatalf("FAIL: %d mentions, want 1", len(got))
}
if got[0].To != "mia" {
t.Errorf("FAIL: told %q", got[0].To)
}
// The link points at the room the line is in, not at a repository
// the line has nothing to do with.
link, _ := got[0].Payload["link"].(string)
if link != "/t/grove/chat" {
t.Errorf("FAIL: link = %q", link)
}
}
// Somebody who cannot open the room is not mentionable in it: naming
// them would send them to a page that refuses them.
func TestAnOutsiderIsNotMentionableInARoomTheyCannotOpen(t *testing.T) {
r, n := mentionRig(t)
if _, err := r.Invoke(asUser("owen"), "team.chat.post",
teamChatPostIn{Name: "grove", Text: "hello @outsider"}); err != nil {
t.Fatal(err)
}
if got := mentionsOf(t, n, 0); len(got) != 0 {
t.Errorf("FAIL: an outsider was told: %+v", got)
}
}
// Naming yourself is not being told about yourself.
func TestNamingYourselfTellsNobody(t *testing.T) {
r, n := mentionRig(t)
if _, err := r.Invoke(asUser("mia"), "team.chat.post",
teamChatPostIn{Name: "grove", Text: "note to self @mia"}); err != nil {
t.Fatal(err)
}
if got := mentionsOf(t, n, 0); len(got) != 0 {
t.Errorf("FAIL: mia was told about her own line: %+v", got)
}
}
// Two different lines naming one person are two notifications: the key
// is address-based on the sequence, so a replay collapses and a second
// line does not.
func TestTwoLinesNamingOnePersonAreTwoTellings(t *testing.T) {
r, n := mentionRig(t)
for _, text := range []string{"first @mia", "second @mia"} {
if _, err := r.Invoke(asUser("owen"), "team.chat.post",
teamChatPostIn{Name: "grove", Text: text}); err != nil {
t.Fatal(err)
}
}
got := mentionsOf(t, n, 2)
if len(got) != 2 {
t.Fatalf("FAIL: %d mentions, want 2", len(got))
}
if got[0].TxnID == got[1].TxnID {
t.Errorf("FAIL: two lines share an idempotency key: %s", got[0].TxnID)
}
if !strings.Contains(got[0].TxnID, "grove") {
t.Errorf("FAIL: the key does not name the room: %s", got[0].TxnID)
}
}