package api
import (
"context"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strconv"
"strings"
"testing"
)
// Every email the forge itself sends goes through sendMail, which renders
// the card. A new call to Mail.Send anywhere else would ship a message
// without it, so this walks the sources and fails on one, the way the
// purge test catches a new repo-keyed table. (Notify's email steps are
// held to the card by notify's own test.)
func TestMailSendHasOneCallSite(t *testing.T) {
var sites []string
fset := token.NewFileSet()
for _, root := range []string{"../../internal", "../../cmd"} {
filepath.WalkDir(root, func(path string, e fs.DirEntry, err error) error {
if err != nil || e.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
f, perr := parser.ParseFile(fset, path, nil, 0)
if perr != nil {
t.Fatal(perr)
}
ast.Inspect(f, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Send" {
return true
}
if recv, ok := sel.X.(*ast.SelectorExpr); ok && recv.Sel.Name == "Mail" {
sites = append(sites, filepath.Base(path)+":"+fset.Position(call.Pos()).String())
}
return true
})
return nil
})
}
if len(sites) != 1 || !strings.HasPrefix(sites[0], "transactional.go:") {
t.Fatalf("Mail.Send must be called only from sendMail (transactional.go); found %v", sites)
}
}
func TestSendMailSendsTheApplicationsCard(t *testing.T) {
mail := &fakeMail{}
d := Deps{Origin: "https://forge.example", Mail: mail}
if err := d.sendMail(context.Background(), "[email protected]", accountMail{Kind: kindInvite, Subject: "s", Text: "plain", Body: "<p>x</p>", Open: "Go", URL: "https://forge.example/x"}); err != nil {
t.Fatal(err)
}
if len(mail.sent) != 1 {
t.Fatalf("%d sent", len(mail.sent))
}
h := mail.sent[0].HTML
for _, want := range []string{`class="q-card"`, `src="https://forge.example/apple-touch-icon.png"`, ">QUARRY<", `class="q-btn"`} {
if !strings.Contains(h, want) {
t.Errorf("card lacks %s", want)
}
}
if strings.Contains(h, ">quarry<") || strings.Contains(h, "lockup") || strings.Contains(h, "wordmark.png") {
t.Error("lowercase quarry in an application email's header")
}
}
// Every image address in a message carries its content hash, and a changed
// image gets a changed address: Gmail's proxy caches by URL.
func TestEmailImagesCarryTheirContentHash(t *testing.T) {
prev := AssetVersion
t.Cleanup(func() { AssetVersion = prev })
d := Deps{Origin: "https://forge.example"}
card := func() string {
return d.card(accountMail{Kind: kindInvite, Subject: "s", Body: "<p>x</p>", Open: "Go", URL: "https://forge.example/x"})
}
AssetVersion = func(path string) string { return "v1-" + strconv.Itoa(len(path)) }
one := card()
for _, want := range []string{
`src="https://forge.example/apple-touch-icon.png?v=v1-21"`,
`src="https://forge.example/badge.png?v=v1-10"`,
`src="https://forge.example/badge-sm.png?v=v1-13"`,
} {
if !strings.Contains(one, want) {
t.Errorf("card lacks %s", want)
}
}
AssetVersion = func(path string) string { return "v2-" + strconv.Itoa(len(path)) }
if two := card(); two == one || strings.Contains(two, "v1-") {
t.Error("changed bytes did not change the addresses")
}
// Icons in a rendered message, and the landing headline's, are versioned too.
if got := d.mailLink("/icons/ticket-open.png"); got != "https://forge.example/icons/ticket-open.png?v=v2-22" {
t.Errorf("icon link %q", got)
}
if got := d.mailLink("/r/a/ticket/1"); got != "https://forge.example/r/a/ticket/1" {
t.Errorf("an ordinary link was versioned: %q", got)
}
head := string(d.landingContext(map[string]any{"summary": "eve accepted landing 3 x", "from": "eve", "number": 3, "state": "accepted"}))
if !strings.Contains(head, "/icons/landing-accepted.png?v=v2-27") {
t.Errorf("landing headline icon unversioned: %s", head)
}
// Absent a version (no web package linked) the address is the plain one.
AssetVersion = nil
if got := d.asset("/badge.png"); got != "https://forge.example/badge.png" {
t.Errorf("%q", got)
}
}