package mail import ( "os" "path/filepath" "strings" "testing" "time" ) // cases renders every message with realistic data. Each entry's key is the // template name, optionally with a "/variant" suffix for a message whose // branches are worth seeing separately. func cases() map[string]any { s := Sender{PublicURL: "https://vantage-hq.hostxtra.co.uk"} now := time.Now().UTC() day := 24 * time.Hour return map[string]any{ "verification": s.verificationData("sam@example.com", "3f9c2a7d1e"), "invite": s.inviteData("sam@example.com", "Northwind Ops", "8a1b2c3d4e"), "license": licenseData("northwind-prod", "eyJ2IjoxLCJpbnN0YW5jZSI6Im5vcnRod2luZC1wcm9kIiwidGllciI6ImZyZWUiLCJleHAiOjE3OTk5OTk5OTl9.MEUCIQDk3v1rX0sX7kQ2m9WQ1o0q8pWm6lq3vRjz8yN0yX4bGQIgYk2Fh7s"), "instanceready": instanceReadyData("northwind-prod", "https://northwind-prod.vantage.hostxtra.co.uk", now.Add(30*day)), "instanceready/nologin": instanceReadyData("northwind-prod", "", now.Add(30*day)), "renewed": renewedData("northwind-prod", now.Add(30*day)), "expiring": expiringData("northwind-prod", s.PublicURL, now.Add(7*day)), "expired": expiredData("northwind-prod", s.PublicURL, now.Add(14*day)), "expired/noreaper": expiredData("northwind-prod", s.PublicURL, time.Time{}), "deletionwarning": deletionWarningData("northwind-prod", s.PublicURL, now.Add(7*day), 7), "deletionwarning/1day": deletionWarningData("northwind-prod", s.PublicURL, now.Add(day), 1), "cancelled": s.billingData("northwind-prod"), "pastdue": s.billingData("northwind-prod"), "monitoralert": MonitorEvent{MonitorName: "api.northwind.io", Type: "http", OldStatus: "up", NewStatus: "down", Message: "GET /healthz returned 503 Service Unavailable after 2.4s", Time: now, Down: true}, "monitoralert/recovered": MonitorEvent{MonitorName: "api.northwind.io", Type: "http", OldStatus: "down", NewStatus: "up", Message: "200 OK in 182ms", Time: now}, "contact": struct { Enquiry Received string }{Enquiry{Name: "Priya Shah", Email: "priya@acme.io", Servers: "50-100", Topic: "Sales", Message: "Hi,\n\nWe run about 80 Linux boxes across two sites and are looking at Vantage for patching and uptime.\nCould we get a demo next week?"}, now.Format(time.RFC1123)}, "vuln_digest": VulnDigest{InstanceName: "northwind-prod", Count: 14, TopSeverity: "critical", Summary: "14 new findings across 3 servers, 2 of them critical.", Rows: []VulnDigestRow{ {CVEID: "CVE-2026-31337", Severity: "critical", PackageName: "openssl", ServerName: "web-01", FixedIn: "3.0.15-1"}, {CVEID: "CVE-2026-29001", Severity: "critical", PackageName: "sudo", ServerName: "db-02", FixedIn: "1.9.16p2"}, {CVEID: "GHSA-4xq2-9r7m-p3wv", Severity: "high", PackageName: "golang.org/x/net", ServerName: "web-01"}, {CVEID: "CVE-2026-20488", Severity: "medium", PackageName: "curl", ServerName: "worker-03", FixedIn: "8.9.1"}, {CVEID: "CVE-2025-48112", Severity: "low", PackageName: "less", ServerName: "worker-03"}, }, More: 9, DBAge: "2 days"}, "announcement": announcementData{ Announcement: sampleAnnouncement(), BodyHTML: "

Workflows can now run on a schedule.

", }, "accountlocked": sampleNotice(), "disputereminder": sampleNotice(), "accountterminated": sampleNotice(), "accountrestored": sampleNotice(), } } // TestRenderAll renders every template and fails if one exists that no case // covers. The templates are parsed in init(), so without this test a mistyped // field is a boot-time panic in three services. // // Set MAIL_PREVIEW_DIR to also write each rendering to disk for a look. func TestRenderAll(t *testing.T) { covered := map[string]bool{} dir := os.Getenv("MAIL_PREVIEW_DIR") for key, data := range cases() { name, _, _ := strings.Cut(key, "/") covered[name] = true m, err := render(name, data, "https://vantage-hq.hostxtra.co.uk") if err != nil { t.Fatalf("%s: %v", key, err) } if m.Subject == "" { t.Errorf("%s: empty subject", key) } for part, body := range map[string]string{"subject": m.Subject, "text": m.Text, "html": m.HTML} { if strings.Contains(body, "") { t.Errorf("%s: %s part has ", key, part) } if strings.Contains(body, "—") { t.Errorf("%s: %s part has an em dash", key, part) } } if dir != "" { base := filepath.Join(dir, strings.ReplaceAll(key, "/", "--")) _ = os.WriteFile(base+".html", []byte(m.HTML), 0o644) _ = os.WriteFile(base+".txt", []byte(m.Text), 0o644) _ = os.WriteFile(base+".subject", []byte(m.Subject), 0o644) } } for name := range sets { if !covered[name] { t.Errorf("template %q has no render case", name) } } } // TestRenderNoPortal checks the footer degrades when the sender has no // PublicURL, as a monitor notification channel does not. func TestRenderNoPortal(t *testing.T) { m, err := render("monitoralert", MonitorEvent{MonitorName: "m", Type: "tcp", Time: time.Now(), Down: true}, "") if err != nil { t.Fatal(err) } if strings.Contains(m.HTML, "/billing") { t.Error("footer links rendered without a portal URL") } }