package api
import (
"regexp"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/oltp"
)
// stubTeamRoster answers TeamMembers only; the embedded nil interface
// panics on anything else, so a test cannot lean on it by accident.
type stubTeamRoster struct {
TeamStore
members []string
}
func (s stubTeamRoster) TeamMembers(string) ([]oltp.TeamMember, error) {
out := make([]oltp.TeamMember, len(s.members))
for i, m := range s.members {
out[i] = oltp.TeamMember{Account: m}
}
return out, nil
}
// stubForumSubs is the three ForumStore calls forumFanout makes.
type stubForumSubs struct{ ForumStore }
func (stubForumSubs) AutoSubscribe(_, _, _ string) error { return nil }
func (stubForumSubs) ForumSubscribers(_, _ string) ([]string, error) { return nil, nil }
func (stubForumSubs) RecordMention(_, _, _, _ string) error { return nil }
// mentionLinks fires each of the three mention paths once at dan and
// returns the payload link the notify seam saw, keyed by path.
func mentionLinks(t *testing.T, origin string) map[string]string {
t.Helper()
paths := map[string]func(d Deps){
"chat": func(d Deps) { d.chatMentionAt("alpha", "hi @dan", "eve", 7) },
"team": func(d Deps) { d.teamMentionAt("crew", "hi @dan", "eve", 7) },
"forum": func(d Deps) {
forumFanout(d, "alpha", "abcdef0123456789abcdef", "hash0123456789ab", "eve", "t", "hi @dan", "")
},
}
got := map[string]string{}
for name, fire := range paths {
fn := &fakeNotify{}
d := Deps{Origin: origin, Notify: fn, Forum: stubForumSubs{},
Teams: stubTeamRoster{members: []string{"dan", "eve"}}}
fire(d)
evs := waitNotify(fn, 1)
if len(evs) != 1 {
t.Fatalf("%s: %d events, want 1", name, len(evs))
}
got[name], _ = evs[0].Payload["link"].(string)
}
return got
}
func TestMentionLinksAreAbsoluteWhenOriginSet(t *testing.T) {
const origin = "https://forge.example"
for name, link := range mentionLinks(t, origin) {
if !strings.HasPrefix(link, origin+"/") {
t.Errorf("%s: link %q does not start with the origin", name, link)
}
}
}
func TestMentionLinksStayRelativeWithoutOrigin(t *testing.T) {
for name, link := range mentionLinks(t, "") {
if !strings.HasPrefix(link, "/") {
t.Errorf("%s: link %q should stay site-relative", name, link)
}
}
}
func TestFireNotifyLeavesAbsoluteAndMissingLinksAlone(t *testing.T) {
fn := &fakeNotify{}
d := Deps{Origin: "https://forge.example", Notify: fn}
const abs = "https://other.example/x"
d.fireNotify(NotifyEvent{Workflow: "w", To: "a", Payload: map[string]any{"link": abs}})
d.fireNotify(NotifyEvent{Workflow: "w", To: "b", Payload: map[string]any{"repo": "r"}})
evs := waitNotify(fn, 2)
if len(evs) != 2 {
t.Fatalf("%d events, want 2", len(evs))
}
for _, e := range evs {
switch e.To {
case "a":
if e.Payload["link"] != abs {
t.Errorf("absolute link rewritten to %v", e.Payload["link"])
}
case "b":
if _, ok := e.Payload["link"]; ok {
t.Errorf("a link appeared from nowhere: %v", e.Payload["link"])
}
}
}
}
// The caller's payload map is shared with the attention publish; the
// rewrite must not reach back into it.
func TestFireNotifyDoesNotMutateCallerPayload(t *testing.T) {
fn := &fakeNotify{}
d := Deps{Origin: "https://forge.example", Notify: fn}
p := map[string]any{"link": "/r/a/chat/1"}
d.fireNotify(NotifyEvent{Workflow: "w", To: "a", Payload: p})
waitNotify(fn, 1)
if p["link"] != "/r/a/chat/1" {
t.Fatalf("caller payload mutated: %v", p["link"])
}
}
func TestMailHTMLRidesMentionsAndReplies(t *testing.T) {
const text = "> quoted\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\n```\ncode\n```\n\n @dan"
for name, fire := range map[string]func(Deps){
"chat": func(d Deps) { d.chatMentionAt("alpha", text, "eve", 7) },
"forum": func(d Deps) {
forumFanout(d, "alpha", "abcdef0123456789abcdef", "hash0123456789ab", "eve", "t", text, "")
},
} {
fn := &fakeNotify{}
d := Deps{Origin: "https://forge.example", Notify: fn, Forum: stubForumSubs{}}
fire(d)
evs := waitNotify(fn, 1)
if len(evs) != 1 {
t.Fatalf("%s: %d events", name, len(evs))
}
p := evs[0].Payload
if p["text"] != text {
t.Errorf("%s: text changed", name)
}
h, _ := p["html"].(string)
if strings.Contains(h, "<img") {
t.Errorf("%s: html has an image", name)
}
for _, want := range []string{`class="q-card"`, "<blockquote", "<pre", "reply to this email", "prefers-color-scheme: dark",
`href="https://cdn.example/x.png"`, `href="` + p["link"].(string) + `"`} {
if !strings.Contains(h, want) {
t.Errorf("%s: html lacks %s", name, want)
}
}
}
}
func TestMailHTMLNeedsAnOriginAndAMailWorkflow(t *testing.T) {
fn := &fakeNotify{}
d := Deps{Notify: fn}
d.chatMentionAt("alpha", "hi @dan", "eve", 7)
if _, has := waitNotify(fn, 1)[0].Payload["html"]; has {
t.Fatal("html built with no origin to make links absolute")
}
e := Deps{Origin: "https://forge.example"}.withMailHTML(NotifyEvent{Workflow: "quarry-ops",
Payload: map[string]any{"text": "x", "link": "https://forge.example/a"}}, time.Now())
if _, has := e.Payload["html"]; has {
t.Fatal("html on a workflow with no card")
}
}
func TestMailHTMLCardPerWorkflow(t *testing.T) {
d := Deps{Origin: "https://forge.example"}
for wf, tc := range map[string]struct {
payload map[string]any
reply bool
}{
"quarry-mention": {map[string]any{"repo": "alpha", "from": "eve", "text": "hi", "surface": "chat", "ref": "message #7", "link": "https://forge.example/r/alpha/chat/7"}, true},
"quarry-forum-reply": {map[string]any{"repo": "alpha", "from": "eve", "title": "T", "text": "hi", "surface": "forum", "link": "https://forge.example/r/alpha/forum/x"}, true},
"quarry-landing": {map[string]any{"repo": "alpha", "from": "eve", "summary": "eve proposed #3", "surface": "landing", "link": "https://forge.example/r/alpha/landing/x"}, true},
} {
e := d.withMailHTML(NotifyEvent{Workflow: wf, Payload: tc.payload}, time.Now())
h, _ := e.Payload["html"].(string)
if strings.Count(h, `class="q-card"`) != 1 || !strings.Contains(h, `class="q-btn" href="`+tc.payload["link"].(string)+`"`) {
t.Errorf("%s: no single card with its button", wf)
}
if strings.Contains(h, "reply to this email") != tc.reply {
t.Errorf("%s: reply hint != %v", wf, tc.reply)
}
for _, m := range regexp.MustCompile(`(?:src|href)="([^"]*)"`).FindAllStringSubmatch(h, -1) {
if !strings.HasPrefix(m[1], "https://") {
t.Errorf("%s: non-absolute %q", wf, m[1])
}
}
if n := strings.Count(h, "<link "); n != 1 {
t.Errorf("%s: %d <link> tags", wf, n)
}
}
}
func TestMailHTMLActivityCarriesTheForge(t *testing.T) {
e := Deps{Origin: "https://forge.example/"}.withMailHTML(NotifyEvent{Workflow: "quarry-activity",
Payload: map[string]any{"repo": "alpha"}}, time.Now())
if e.Payload["origin"] != "https://forge.example" || e.Payload["host"] != "forge.example" {
t.Fatalf("origin/host: %v %v", e.Payload["origin"], e.Payload["host"])
}
}