Skip to content
quarry Artifact [894a09caa3]
Login

Artifact [894a09caa3]

Artifact 894a09caa31dff4751a2408763f0c0ef54ca73a21f32ed41226855066d714425:


package api

import (
	"go/ast"
	"go/parser"
	"go/token"
	"io/fs"
	"path/filepath"
	"sort"
	"strings"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/notify"
)

// Every account email names its kind, and the kind is in the table as the
// application's: the origin is decided once, in mailkinds.go, and not
// inferred from a template. A new accountMail without a classified Kind
// fails here, and ships as text only until it is classified.
func TestEveryAccountMailIsClassified(t *testing.T) {
	fset := token.NewFileSet()
	found := 0
	filepath.WalkDir(".", 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 {
			cl, ok := n.(*ast.CompositeLit)
			if !ok {
				return true
			}
			if id, ok := cl.Type.(*ast.Ident); !ok || id.Name != "accountMail" {
				return true
			}
			found++
			var kind string
			for _, el := range cl.Elts {
				if kv, ok := el.(*ast.KeyValueExpr); ok {
					if k, ok := kv.Key.(*ast.Ident); ok && k.Name == "Kind" {
						if v, ok := kv.Value.(*ast.Ident); ok {
							kind = v.Name
						}
					}
				}
			}
			if kind == "" {
				t.Errorf("%s: an accountMail with no Kind", fset.Position(cl.Pos()))
			}
			return true
		})
		return nil
	})
	if found != 10 {
		t.Errorf("%d accountMail literals, want the ten call sites (the team kinds share teamNotice)", found)
	}
	for _, k := range []string{kindInvite, kindReset, kindPasswordChanged, kindEmailCode, kindEmailChanged, kindEmailSet, kindEmailProposed, kindInviteRedeemed, kindAccessChanged} {
		if mailKinds[k].From != fromApp || mailKinds[k].Reason == "" {
			t.Errorf("%s is not classified as the application's with a reason", k)
		}
	}
	for _, k := range []string{kindTeamAdded, kindTeamRemoved, kindTeamRoleChanged} {
		if mailKinds[k].From != fromTeam || mailKinds[k].Reason == "" {
			t.Errorf("%s is not classified as a team's with a reason", k)
		}
	}
	if got := (Deps{Origin: "https://f.example"}).card(accountMail{Subject: "s", Body: "<p>x</p>"}); got != "" {
		t.Error("an unclassified email got a card")
	}
}

// Every email the forge sends is in the table: each notify workflow with an
// email step, each kind the repository cards draw, and each account kind.
func TestEveryEmailKindIsInTheTable(t *testing.T) {
	for _, def := range notify.Workflows() {
		for _, st := range def.Steps {
			if st.Type == "email" {
				if k, ok := mailKinds[def.TriggerIdentifier]; !ok || k.From != fromRepo {
					t.Errorf("workflow %s has an email step and is not a repository kind in the table", def.TriggerIdentifier)
				}
			}
		}
	}
	for wf := range mailCards {
		if mailKinds[wf].From != fromRepo {
			t.Errorf("%s draws a repository card and the table does not say repo", wf)
		}
	}
	if mailKindFor("quarry-mention", "team chat") != "quarry-mention.team" || mailKinds["quarry-mention.team"].From != fromTeam ||
		mailKindFor("quarry-mention", "chat") != "quarry-mention" {
		t.Error("a mention in a team's chat is not the team's")
	}
	for k, v := range mailKinds {
		if v.Reason == "" || (v.From != fromApp && v.From != fromRepo && v.From != fromTeam) {
			t.Errorf("%s: incomplete entry", k)
		}
	}
}

// The classification decides the header: an application email carries the
// wordmark in its header, a repository email never does.
func TestHeaderFollowsTheClassification(t *testing.T) {
	prev := AssetVersion
	AssetVersion = nil
	t.Cleanup(func() { AssetVersion = prev })
	d := Deps{Origin: "https://forge.example"}
	var kinds []string
	for k := range mailKinds {
		kinds = append(kinds, k)
	}
	sort.Strings(kinds)
	for _, k := range kinds {
		var doc, name string
		switch {
		case mailKinds[k].From == fromApp && strings.HasPrefix(k, "account."):
			doc = d.card(accountMail{Kind: k, Subject: "s", Body: "<p>x</p>"})
		case mailKinds[k].From == fromTeam && strings.HasPrefix(k, "team."):
			name = "crew"
			doc = d.card(accountMail{Kind: k, Team: name, Subject: "s", Body: "<p>x</p>", Open: "Open crew", URL: "https://forge.example/t/crew"})
		case mailKinds[k].From == fromRepo || mailKinds[k].From == fromTeam:
			name = "alpha"
			surface := "chat"
			if mailKinds[k].From == fromTeam {
				name, surface = "crew", "team chat"
			}
			p := map[string]any{"repo": name, "from": "eve", "text": "hi", "title": "T", "summary": "eve proposed landing 3 x",
				"surface": surface, "link": "https://forge.example/x"}
			e := d.withMailHTML(NotifyEvent{Workflow: strings.TrimSuffix(k, ".team"), Payload: p}, time.Now())
			var ok bool
			if doc, ok = e.Payload["html"].(string); !ok && k != "quarry-activity" {
				t.Errorf("%s: no card", k)
			}
			if k == "quarry-activity" {
				continue // drawn by notify's template, from its own tests
			}
		default:
			continue // the probe: held to the application's header by its own test
		}
		head := doc[strings.Index(doc, "<body"):strings.Index(doc, `<div class="q-text" style="font-size:15px;`)]
		has := strings.Contains(head, ">QUARRY<") && strings.Contains(head, `class="q-wm-b"`)
		if has != (mailKinds[k].From == fromApp) {
			t.Errorf("%s (%s): header wordmark = %v", k, mailKinds[k].From, has)
		}
		// A repository's or a team's header names it, in its own case.
		if mailKinds[k].From != fromApp && !strings.Contains(head, ">"+name+"<") {
			t.Errorf("%s (%s): the header does not name %q", k, mailKinds[k].From, name)
		}
		if !strings.Contains(doc[strings.Index(doc, `class="q-rule q-muted"`):], ">QUARRY<") {
			t.Errorf("%s: the footer lost the wordmark", k)
		}
	}
}