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