Skip to content
quarry Artifact [1e1bad1f0c]
Login

Artifact [1e1bad1f0c]

Artifact 1e1bad1f0cd8b3bf02fbbb5fd91a5d78d81d0610cb859cc327d1166a53772a7f:


package api

import (
	"context"
	"sort"
	"sync"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

// memChatNotify is chat_notify in memory, keyed repo\x00account.
type memChatNotify struct {
	mu    sync.Mutex
	modes map[string]string
}

func (m *memChatNotify) SetChatNotify(repo, account, mode string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.modes == nil {
		m.modes = map[string]string{}
	}
	if mode == oltp.ChatMentions {
		delete(m.modes, repo+"\x00"+account)
	} else {
		m.modes[repo+"\x00"+account] = mode
	}
	return nil
}

func (m *memChatNotify) ChatNotify(repo, account string) string {
	m.mu.Lock()
	defer m.mu.Unlock()
	if mode, ok := m.modes[repo+"\x00"+account]; ok {
		return mode
	}
	return oltp.ChatMentions
}

func (m *memChatNotify) ChatNotifyEvery(repo string) ([]string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var out []string
	for k, mode := range m.modes {
		if mode == oltp.ChatEvery && len(k) > len(repo) && k[:len(repo)+1] == repo+"\x00" {
			out = append(out, k[len(repo)+1:])
		}
	}
	sort.Strings(out)
	return out, nil
}

// A channel's bell decides who hears a message. Following every message
// tells a follower about each one, but not its author, not somebody it
// named (the mention already did), and not somebody who has lost the
// chat letter since following. Off silences even a mention.
func TestAChatChannelsBellDecidesWhoHears(t *testing.T) {
	fn := &fakeNotify{}
	store := &memChatNotify{}
	repos := testRepos(wikiIndex(t))
	mustRepo(t, repos, "test").Code = "c1"
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: repos, Chat: &fakeChat{}, Notify: fn, ChatNotify: store,
		CapsFor: func(a, _ string) CapSet {
			if a == "eve" {
				return fakeCaps("o") // can read code, not chat
			}
			return fakeCaps(anyCapLetters)
		}}); err != nil {
		t.Fatal(err)
	}
	set := func(who, mode string) {
		t.Helper()
		ctx := ops.WithUser(context.Background(), who)
		if _, err := r.Invoke(ctx, "chat.notify.set", chatNotifySetIn{Repo: "test", Mode: mode}); err != nil {
			t.Fatal(err)
		}
	}
	set("dan", "all") // follows everything
	set("eve", "all") // followed, then lost the chat letter
	set("gus", "all") // follows, and is named: hears the mention only
	set("lou", "all") // the author: never told of their own message
	set("fay", "off") // off: not even the mention
	if got, _ := r.Invoke(ops.WithUser(context.Background(), "fay"), "chat.notify", chatNotifyIn{Repo: "test"}); got.(chatNotifyOut).Mode != "off" {
		t.Fatalf("FAIL: chat.notify for fay = %+v", got)
	}

	if _, err := r.Invoke(ops.WithUser(context.Background(), "lou"), "chat.post",
		chatPostIn{Repo: "test", Text: "release-233 is up, @fay @gus"}); err != nil {
		t.Fatal(err)
	}
	var evs []NotifyEvent
	for range 50 {
		if evs = fn.snapshot(); len(evs) >= 2 {
			break
		}
		time.Sleep(20 * time.Millisecond)
	}
	time.Sleep(60 * time.Millisecond) // anything extra would have landed by now
	got := map[string]string{}
	for _, e := range fn.snapshot() {
		got[e.To] += e.Workflow + " "
	}
	want := map[string]string{"dan": "quarry-chat ", "gus": "quarry-mention "}
	if len(got) != len(want) || got["dan"] != want["dan"] || got["gus"] != want["gus"] {
		t.Fatalf("FAIL: told %v, want %v", got, want)
	}
	t.Log("ok: a follower hears every message; the author, the named, the off and the unentitled do not hear it twice or at all")

	set("dan", "mentions")
	if list, _ := store.ChatNotifyEvery("test"); len(list) != 3 || list[0] != "eve" {
		t.Fatalf("FAIL: after dan went back to mentions, followers = %v", list)
	}
	t.Log("ok: going back to mentions stops following")
}

// A message a bridge imported from an upstream fossil never went
// through chat.post, so nobody following the channel heard of it: the
// libfossil channel, set to "every message", sent nothing. ChatArrived
// gives a bridged line the same notifications, and its "fossil:<login>"
// author is not sent a copy of their own line.
func TestABridgedMessageReachesFollowers(t *testing.T) {
	fn := &fakeNotify{}
	store := &memChatNotify{}
	_ = store.SetChatNotify("test", "dan", "all")
	_ = store.SetChatNotify("test", "lou", "all")
	d := Deps{Notify: fn, ChatNotify: store, CapsFor: func(string, string) CapSet { return fakeCaps(anyCapLetters) }}
	d.ChatArrived("test", "pushed the sqlext-functions branch", "fossil:lou", 42)
	var evs []NotifyEvent
	for range 50 {
		if evs = fn.snapshot(); len(evs) >= 1 {
			break
		}
		time.Sleep(20 * time.Millisecond)
	}
	time.Sleep(50 * time.Millisecond)
	evs = fn.snapshot()
	if len(evs) != 1 || evs[0].Workflow != "quarry-chat" || evs[0].To != "dan" {
		t.Fatalf("FAIL: bridged message notified %+v, want one quarry-chat to dan", evs)
	}
	t.Log("ok: the follower heard it; the author did not")
}