Skip to content
quarry Artifact [beb441fb0f]
Login

Artifact [beb441fb0f]

Artifact beb441fb0f0383bfbf6c9a8afcab90b6ab3fe0ae7d5acf235020cdfe12169e01:


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)
	}
}