116 lines
5.1 KiB
Go
116 lines
5.1 KiB
Go
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: "<p>Workflows can now run <strong>on a schedule</strong>.</p>",
|
|
},
|
|
"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, "<no value>") {
|
|
t.Errorf("%s: %s part has <no value>", 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")
|
|
}
|
|
}
|