diff --git a/internal/notify/templates/templates_test.go b/internal/notify/templates/templates_test.go
index d447a3d68..0ff301ada 100644
--- a/internal/notify/templates/templates_test.go
+++ b/internal/notify/templates/templates_test.go
@@ -5,8 +5,13 @@ import (
"path/filepath"
"strings"
"testing"
+ "time"
)
+// previewTime is a fixed timestamp so deletion previews/tests are
+// deterministic instead of depending on the wall clock.
+var previewTime = time.Date(2026, time.July, 1, 9, 0, 0, 0, time.UTC)
+
// ─── Base template (shared chrome) ───────────────────────────────
func TestBaseTemplate_Structure(t *testing.T) {
@@ -269,6 +274,25 @@ func TestPreview(t *testing.T) {
{"reset-password.html", func() (string, error) {
return GenerateResetPasswordHTML("", "https://app.warmbly.com/reset?token=abc123def456")
}},
+ {"trial-expired.html", func() (string, error) { return GenerateTrialExpiredHTML() }},
+ {"invitation.html", func() (string, error) {
+ return GenerateInvitationHTML("Jane Doe", "Acme Inc", AppURL+"/invite?token=abc123")
+ }},
+ {"notification.html", func() (string, error) {
+ return GenerateNotificationHTML("New sign-in to your account", "Signed in from Chrome on macOS (London, GB).", AppURL+"/app/settings/security", "")
+ }},
+ {"deletion-org-scheduled.html", func() (string, error) {
+ return GenerateOrgDeletionScheduledHTML("Acme Inc", previewTime, 30, AppURL+"/organization/settings/danger-zone")
+ }},
+ {"deletion-user-scheduled.html", func() (string, error) {
+ return GenerateUserDeletionScheduledHTML("Jane", previewTime, 30, AppURL+"/account/danger-zone")
+ }},
+ {"deletion-reminder.html", func() (string, error) {
+ return GenerateDeletionReminderHTML("Acme Inc", previewTime, AppURL+"/organization/settings/danger-zone")
+ }},
+ {"deletion-completed.html", func() (string, error) {
+ return GenerateDeletionCompletedHTML(previewTime, previewTime)
+ }},
}
for _, tmpl := range templates {
@@ -286,6 +310,143 @@ func TestPreview(t *testing.T) {
t.Logf("Open all: open %s/*.html", dir)
}
+// ─── Trial expired ──────────────────────────────────────────────
+
+func TestGenerateTrialExpiredHTML(t *testing.T) {
+ html, err := GenerateTrialExpiredHTML()
+ if err != nil {
+ t.Fatalf("GenerateTrialExpiredHTML returned error: %v", err)
+ }
+ checks := []string{
+ "", // shared base shell
+ "#f5f6f8", // branded cream wrapper
+ "Your free trial has ended",
+ "Choose a plan",
+ AppURL + "/settings/billing", // billing CTA href
+ "
Your Warmbly trial has ended",
+ }
+ for _, s := range checks {
+ if !strings.Contains(html, s) {
+ t.Errorf("trial expired: expected HTML to contain %q", s)
+ }
+ }
+}
+
+// ─── Invitation ─────────────────────────────────────────────────
+
+func TestGenerateInvitationHTML(t *testing.T) {
+ url := AppURL + "/invite?token=abc123"
+ html, err := GenerateInvitationHTML("Jane Doe", "Acme Inc", url)
+ if err != nil {
+ t.Fatalf("GenerateInvitationHTML returned error: %v", err)
+ }
+ checks := []string{
+ "#f5f6f8",
+ "Jane Doe",
+ "Acme Inc",
+ "Accept invitation",
+ url,
+ "expires in 7 days",
+ }
+ for _, s := range checks {
+ if !strings.Contains(html, s) {
+ t.Errorf("invitation: expected HTML to contain %q", s)
+ }
+ }
+}
+
+func TestGenerateInvitationHTML_EscapesNames(t *testing.T) {
+ html, err := GenerateInvitationHTML("", "Acme & Co", AppURL+"/invite?token=x")
+ if err != nil {
+ t.Fatalf("GenerateInvitationHTML returned error: %v", err)
+ }
+ if strings.Contains(html, "") {
+ t.Error("invitation: inviter name must be HTML-escaped")
+ }
+ if !strings.Contains(html, "Acme & Co") {
+ t.Error("invitation: org name ampersand must be HTML-escaped")
+ }
+}
+
+// ─── Notification ───────────────────────────────────────────────
+
+func TestGenerateNotificationHTML_WithCTA(t *testing.T) {
+ html, err := GenerateNotificationHTML("New sign-in", "Signed in from Chrome.", AppURL+"/app/settings/security", "")
+ if err != nil {
+ t.Fatalf("GenerateNotificationHTML returned error: %v", err)
+ }
+ checks := []string{
+ "#f5f6f8",
+ "New sign-in",
+ "Signed in from Chrome.",
+ "Open in Warmbly", // default CTA label
+ AppURL + "/app/settings/security",
+ "New sign-in",
+ }
+ for _, s := range checks {
+ if !strings.Contains(html, s) {
+ t.Errorf("notification: expected HTML to contain %q", s)
+ }
+ }
+}
+
+func TestGenerateNotificationHTML_NoCTA(t *testing.T) {
+ html, err := GenerateNotificationHTML("Heads up", "Something happened.", "", "")
+ if err != nil {
+ t.Fatalf("GenerateNotificationHTML returned error: %v", err)
+ }
+ if strings.Contains(html, "Open in Warmbly") {
+ t.Error("notification: no button should render when ctaURL is empty")
+ }
+}
+
+// ─── Deletion (danger zone) ─────────────────────────────────────
+
+func TestGenerateDeletionEmails(t *testing.T) {
+ cancel := AppURL + "/account/danger-zone"
+ cases := []struct {
+ name string
+ gen func() (string, error)
+ wants []string
+ notWant string
+ }{
+ {
+ name: "org scheduled",
+ gen: func() (string, error) { return GenerateOrgDeletionScheduledHTML("Acme Inc", previewTime, 30, cancel) },
+ wants: []string{"Organization scheduled for deletion", "Acme Inc", "Cancel deletion", "Will be deleted on", "01 July 2026", cancel},
+ },
+ {
+ name: "user scheduled",
+ gen: func() (string, error) { return GenerateUserDeletionScheduledHTML("Jane", previewTime, 30, cancel) },
+ wants: []string{"Account scheduled for deletion", "Hi Jane", "Cancel deletion", "30 days"},
+ },
+ {
+ name: "reminder",
+ gen: func() (string, error) { return GenerateDeletionReminderHTML("Acme Inc", previewTime, cancel) },
+ wants: []string{"Deletion in", "Acme Inc", "Cancel deletion"},
+ },
+ {
+ name: "completed",
+ gen: func() (string, error) { return GenerateDeletionCompletedHTML(previewTime, previewTime) },
+ wants: []string{"Deletion completed", "Executed at:"},
+ },
+ }
+ for _, c := range cases {
+ html, err := c.gen()
+ if err != nil {
+ t.Fatalf("%s: returned error: %v", c.name, err)
+ }
+ if !strings.Contains(html, "") {
+ t.Errorf("%s: expected shared base shell", c.name)
+ }
+ for _, w := range c.wants {
+ if !strings.Contains(html, w) {
+ t.Errorf("%s: expected HTML to contain %q", c.name, w)
+ }
+ }
+ }
+}
+
// ─── Constants ──────────────────────────────────────────────────
func TestBusinessConstants(t *testing.T) {