package api
import (
"context"
"net/http"
"testing"
"time"
"craftdesign.group/quarry/internal/ops"
)
type said struct {
user, kind, room string
ref uint64
text string
}
// chatRig is the forum rig with a room to talk into. lou may chat on
// quarry; mallory may not, and neither is an admin — replying by mail
// is an ordinary account's business.
func chatRig(t *testing.T) (*ReplyReceiver, *fakeReplyStore, *[]said) {
t.Helper()
var log []said
store := &fakeReplyStore{secret: "s3cret"}
h := &ReplyReceiver{
Store: store,
CapsFor: func(account, repo string) CapSet {
if account == "lou" {
return capsOf("C3")
}
return capsOf("")
},
Say: func(ctx context.Context, kind, room string, ref uint64, text string) (uint64, error) {
log = append(log, said{ops.User(ctx), kind, room, ref, text})
return 42, nil
},
Now: func() time.Time { return time.Unix(1_800_000_000, 0) },
}
return h, store, &log
}
// The chat transaction ids were minted from the day chat had mentions
// and nothing ever read them back: a reply to one answered "no thread
// behind transaction", so the mail was accepted, understood and
// dropped. Both rooms now land.
func TestAMailedReplyLandsInTheRoomItAnswers(t *testing.T) {
for _, c := range []struct {
name, txn, kind, room string
ref uint64
}{
{"a repository's room", "mention:chat:quarry:1234:lou", "chat", "quarry", 1234},
{"a team's room", "mention:team:grove:99:lou", "team", "grove", 99},
} {
h, store, log := chatRig(t)
w := deliver(h, "s3cret", 1_800_000_000, replyEvent(c.txn, "lou", " On it.\n"))
if w.Code != http.StatusCreated {
t.Errorf("FAIL: %s: status %d: %s", c.name, w.Code, w.Body)
continue
}
if len(*log) != 1 {
t.Errorf("FAIL: %s: %d lines, want 1", c.name, len(*log))
continue
}
got := (*log)[0]
if got.user != "lou" || got.kind != c.kind || got.room != c.room ||
got.ref != c.ref || got.text != "On it." {
t.Errorf("FAIL: %s: said %+v", c.name, got)
} else {
t.Logf("ok: %s — %s reply as %s into %s, answering %d", c.name, got.kind, got.user, got.room, got.ref)
}
if store.seen["<[email protected]>"] == "" {
t.Errorf("FAIL: %s: outcome not recorded", c.name)
}
}
}
// Every gate the room has, the mail meets too. Replying by email is a
// different DOOR, never a different set of rules.
func TestAMailedChatReplyMeetsTheRoomsGates(t *testing.T) {
// Somebody else's notification is not yours to answer.
h, _, log := chatRig(t)
w := deliver(h, "s3cret", 1_800_000_000, replyEvent("mention:chat:quarry:7:lou", "mallory", "hello"))
if w.Code != http.StatusForbidden || len(*log) != 0 {
t.Errorf("FAIL: mallory answered lou's notification (%d, %d lines)", w.Code, len(*log))
} else {
t.Log("ok: a reply from the wrong account is refused")
}
// No chat capability on that repository, no line.
h2, _, log2 := chatRig(t)
w2 := deliver(h2, "s3cret", 1_800_000_000, replyEvent("mention:chat:quarry:7:mallory", "mallory", "hello"))
if w2.Code != http.StatusForbidden || len(*log2) != 0 {
t.Errorf("FAIL: mallory chatted without C (%d, %d lines)", w2.Code, len(*log2))
} else {
t.Log("ok: the room's capability is checked")
}
// An empty reply — everything quoted stripped — is not a message.
h3, _, log3 := chatRig(t)
w3 := deliver(h3, "s3cret", 1_800_000_000, replyEvent("mention:chat:quarry:7:lou", "lou", " \n "))
if w3.Code < 400 || len(*log3) != 0 {
t.Errorf("FAIL: an empty reply became a line (%d)", w3.Code)
} else {
t.Log("ok: an empty reply is refused")
}
}
// The id is parsed from the RIGHT: who is always last, a seq always
// sits before it, and a room is the field whose grammar we do not own.
func TestChatTransactionIdsAreReadBack(t *testing.T) {
for _, c := range []struct {
txn, kind, room string
ref uint64
who string
ok bool
}{
{"mention:chat:quarry:1234:lou", "chat", "quarry", 1234, "lou", true},
{"mention:team:grove:99:ann", "team", "grove", 99, "ann", true},
{"mention:chat:my:repo:7:lou", "chat", "my:repo", 7, "lou", true},
{"mention:chat:quarry:notaseq:lou", "", "", 0, "", false},
{"mention:chat:quarry:lou", "", "", 0, "", false},
{"landing:abc:lou", "", "", 0, "", false},
} {
got, ok := replyTarget(c.txn)
if ok != c.ok {
t.Errorf("FAIL: %q read=%v want=%v", c.txn, ok, c.ok)
continue
}
if !ok {
t.Logf("ok: %q is not a room reply", c.txn)
continue
}
if got.Kind != c.kind || got.Room != c.room || got.Ref != c.ref || got.Who != c.who {
t.Errorf("FAIL: %q -> %+v", c.txn, got)
} else {
t.Logf("ok: %q -> %s %s #%d for %s", c.txn, got.Kind, got.Room, got.Ref, got.Who)
}
}
// The forum shapes still read as they did.
if got, ok := replyTarget("forum-reply:abc123:lou"); !ok || got.Kind != "forum" || got.Hash != "abc123" || got.Who != "lou" {
t.Errorf("FAIL: the forum shape broke: %+v %v", got, ok)
} else {
t.Log("ok: the forum shapes are unchanged")
}
if got, ok := replyTarget("mention:forum:abc123:lou"); !ok || got.Kind != "forum" || got.Hash != "abc123" {
t.Errorf("FAIL: the forum mention shape broke: %+v %v", got, ok)
} else {
t.Log("ok: the forum mention shape is unchanged")
}
}