Skip to content
quarry Artifact [b804b2d96e]
Login

Artifact [b804b2d96e]

Artifact b804b2d96ee4a74e9595890c37c38eb967306c7d4b3289a891abc97cc782d8fc:


package api

import (
	"context"
	"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![x](https://cdn.example/x.png) @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 src="https://cdn`) {
			t.Errorf("%s: a message image was kept as 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)
		}
	}
}

// A landing's card carries its title as a heading and its message
// rendered like a mention's, and a hint only where the surface has one.
func TestMailHTMLLandingCarriesTitleAndMessage(t *testing.T) {
	d := Deps{Origin: "https://forge.example"}
	e := d.withMailHTML(NotifyEvent{Workflow: "quarry-landing", Payload: map[string]any{
		"repo": "alpha", "from": "eve", "summary": "eve proposed !3 fix \u2192 trunk", "surface": "landing",
		"title": "Fix the \u201cthing\u201d", "message": "Body with **bold** and a \u2014 dash.\n\n- one\n- two",
		"ref": "landed as 0123456789ab", "link": "https://forge.example/r/alpha/landing/x"}}, time.Now())
	h, _ := e.Payload["html"].(string)
	for _, want := range []string{"Fix the \u201cthing\u201d", "<strong", "\u2014 dash", "<li", "landed as 0123456789ab", "!3"} {
		if !strings.Contains(h, want) {
			t.Errorf("card lacks %q", want)
		}
	}
	e = d.withMailHTML(NotifyEvent{Workflow: "quarry-landing", Payload: map[string]any{
		"repo": "alpha", "from": "ci", "summary": "p failed", "surface": "ci", "link": "https://forge.example/r/alpha/ci/run/1"}}, time.Now())
	if h, _ := e.Payload["html"].(string); strings.Contains(h, "reply to this email") {
		t.Error("reply hint on a surface with no place for the answer")
	}
}

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

type stubAvatars struct{ has map[string]bool }

func (s stubAvatars) SetAvatar(oltp.Avatar) error { return nil }
func (s stubAvatars) AvatarOf(k oltp.AvatarKind, n string) (oltp.Avatar, bool, error) {
	return oltp.Avatar{Kind: k, Name: n}, s.has[string(k)+"/"+n], nil
}
func (s stubAvatars) AvatarETags(oltp.AvatarKind) (map[string]string, error) { return nil, nil }
func (s stubAvatars) ClearAvatar(oltp.AvatarKind, string) error              { return nil }

func TestMailHeaderNamesItsSource(t *testing.T) {
	d := Deps{Origin: "https://forge.example", Repos: NewRepos(map[string]*Repo{"alpha": {}, "beta": {}, "secret": {}}),
		CanRead: func(_ context.Context, r string) bool { return r != "secret" },
		Avatars: stubAvatars{has: map[string]bool{"repo/alpha": true, "repo/secret": true, "team/crew": true}}}
	card := func(repo, surface string) string {
		e := d.withMailHTML(NotifyEvent{Workflow: "quarry-mention", Payload: map[string]any{
			"repo": repo, "from": "eve", "text": "hi", "surface": surface, "link": "https://forge.example/x"}}, time.Now())
		return e.Payload["html"].(string)
	}
	// An uploaded avatar: one image at the stable address, nothing per recipient.
	h := card("alpha", "chat")
	if !strings.Contains(h, `<img src="https://forge.example/r/alpha/avatar"`) || strings.Count(h, "<img") != 2 { // the avatar and the footer logo
		t.Errorf("repo with an avatar: %s", h)
	}
	if !strings.Contains(card("crew", "team chat"), `<img src="https://forge.example/t/crew/avatar"`) {
		t.Error("team avatar not at /t/crew/avatar")
	}
	// No avatar, or one an unauthenticated client could not fetch: the table identicon.
	for _, repo := range []string{"beta", "secret"} {
		if h := card(repo, "chat"); strings.Count(h, "<img") != 1 || !strings.Contains(h, `class="q-idnbg"`) {
			t.Errorf("%s: expected the identicon table", repo)
		}
	}
	// The forge's own repository wears the forge's logo at the one public
	// address, unless an avatar has been uploaded; every card's footer has it.
	const logo = `<img src="https://forge.example/apple-touch-icon.png"`
	own := Deps{Origin: "https://forge.example", Repos: NewRepos(map[string]*Repo{"quarry": {}}),
		CanRead: func(context.Context, string) bool { return true }}
	ownCard := func(d Deps) string {
		return d.withMailHTML(NotifyEvent{Workflow: "quarry-mention", Payload: map[string]any{
			"repo": "quarry", "from": "eve", "text": "hi", "surface": "chat", "link": "https://forge.example/x"}}, time.Now()).Payload["html"].(string)
	}
	if h := ownCard(own); strings.Count(h, logo) != 2 || strings.Contains(h, `class="q-idnbg"`) {
		t.Errorf("own repo: want the logo in header and footer, no identicon: %d", strings.Count(h, logo))
	}
	own.Avatars = stubAvatars{has: map[string]bool{"repo/quarry": true}}
	if h := ownCard(own); strings.Count(h, logo) != 1 || !strings.Contains(h, "/r/quarry/avatar") {
		t.Error("an uploaded avatar should replace the logo in the header")
	}
	if h := card("beta", "chat"); strings.Count(h, logo) != 1 {
		t.Errorf("another repo: logo only in the footer")
	}
	e := d.withMailHTML(NotifyEvent{Workflow: "quarry-activity", Payload: map[string]any{"repo": "alpha"}}, time.Now())
	if m, _ := e.Payload["mark"].(string); !strings.Contains(m, "/r/alpha/avatar") {
		t.Errorf("activity mark: %q", m)
	}
}

func TestAccountCardPageIsNeutral(t *testing.T) {
	h := Deps{Origin: "https://forge.example"}.accountCard("s", "<p>x</p>", "Go", "https://forge.example/x")
	if !strings.Contains(h, "background:#f4f4f4;") || strings.Contains(h, "f3ecda") {
		t.Error("account card page wash")
	}
}