persona-community-5/services/persona-api/internal/email/renderer_test.go
jordan f5f3229364
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Add components: service/persona-api, worker/media-worker, app-react/creator-ui
2026-02-24 07:40:04 +00:00

203 lines
5.4 KiB
Go

package email_test
import (
"strings"
"testing"
emailpkg "git.threesix.ai/jordan/persona-community-5/pkg/email"
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/email"
)
var testBrand = emailpkg.BrandConfig{
AppName: "Test App",
AppURL: "https://example.com",
SupportEmail: "support@example.com",
PrimaryColor: "#6366f1",
}
func newTestRenderer(t *testing.T) *emailpkg.Renderer {
t.Helper()
r, err := emailpkg.NewRendererFromFS(email.TemplateFS, "templates", testBrand)
if err != nil {
t.Fatalf("NewRendererFromFS: %v", err)
}
return r
}
func TestRendererLoads(t *testing.T) {
r := newTestRenderer(t)
purposes := r.Purposes()
want := []string{"email_verify", "login_otp", "magic_link", "password_reset", "welcome"}
if len(purposes) != len(want) {
t.Fatalf("expected %d purposes, got %d: %v", len(want), len(purposes), purposes)
}
for i, p := range want {
if purposes[i] != p {
t.Errorf("purpose[%d]: want %q, got %q", i, p, purposes[i])
}
}
}
func TestRenderLoginOTP(t *testing.T) {
r := newTestRenderer(t)
out, err := r.Render("login_otp", emailpkg.EmailContext{
Code: "482916",
ExpiresIn: 10,
Purpose: "sign in",
})
if err != nil {
t.Fatalf("Render login_otp: %v", err)
}
if out.Subject == "" {
t.Error("Subject is empty")
}
if !strings.Contains(out.Subject, "Test App") {
t.Errorf("Subject %q does not contain app name", out.Subject)
}
if !strings.Contains(out.HTML, "482916") {
t.Error("HTML does not contain OTP code")
}
if !strings.Contains(out.HTML, "code-box") {
t.Error("HTML does not contain code-box element")
}
if out.PlainText == "" {
t.Error("PlainText is empty")
}
if !strings.Contains(out.PlainText, "482916") {
t.Error("PlainText does not contain OTP code")
}
if out.Preheader == "" {
t.Error("Preheader is empty")
}
}
func TestRenderMagicLink(t *testing.T) {
r := newTestRenderer(t)
out, err := r.Render("magic_link", emailpkg.EmailContext{
ActionURL: "https://example.com/auth/verify?token=abc123",
ButtonText: "Sign In \u2192",
ExpiresIn: 15,
})
if err != nil {
t.Fatalf("Render magic_link: %v", err)
}
if !strings.Contains(out.HTML, "Sign In") {
t.Error("HTML does not contain button text")
}
if !strings.Contains(out.HTML, "auth/verify") {
t.Error("HTML does not contain action URL")
}
if out.PlainText == "" {
t.Error("PlainText is empty")
}
}
func TestRenderPasswordReset(t *testing.T) {
r := newTestRenderer(t)
out, err := r.Render("password_reset", emailpkg.EmailContext{
ActionURL: "https://example.com/auth/reset?token=xyz789",
ButtonText: "Reset Password \u2192",
ExpiresIn: 60,
})
if err != nil {
t.Fatalf("Render password_reset: %v", err)
}
if !strings.Contains(out.HTML, "Reset Password") {
t.Error("HTML does not contain button text")
}
if !strings.Contains(out.Subject, "Reset") {
t.Errorf("Subject %q does not mention reset", out.Subject)
}
}
func TestRenderVerifyEmail(t *testing.T) {
r := newTestRenderer(t)
out, err := r.Render("email_verify", emailpkg.EmailContext{
Code: "738201",
ExpiresIn: 30,
Purpose: "verify your email",
})
if err != nil {
t.Fatalf("Render email_verify: %v", err)
}
if !strings.Contains(out.HTML, "738201") {
t.Error("HTML does not contain verification code")
}
if !strings.Contains(out.HTML, "code-box") {
t.Error("HTML does not contain code-box element")
}
}
func TestRenderWelcome(t *testing.T) {
r := newTestRenderer(t)
out, err := r.Render("welcome", emailpkg.EmailContext{
ActionURL: "https://example.com/dashboard",
ButtonText: "Get Started \u2192",
Name: "Jordan",
})
if err != nil {
t.Fatalf("Render welcome: %v", err)
}
if !strings.Contains(out.HTML, "Jordan") {
t.Error("HTML does not contain user name")
}
if !strings.Contains(out.HTML, "Welcome") {
t.Error("HTML does not contain welcome heading")
}
}
func TestBrandColorInjection(t *testing.T) {
r := newTestRenderer(t)
out, err := r.Render("login_otp", emailpkg.EmailContext{
Code: "123456",
ExpiresIn: 10,
})
if err != nil {
t.Fatalf("Render: %v", err)
}
// Brand primary color should appear in the inlined styles.
if !strings.Contains(out.HTML, "#6366f1") {
t.Error("HTML does not contain brand color #6366f1")
}
}
func TestUnknownPurposeReturnsError(t *testing.T) {
r := newTestRenderer(t)
_, err := r.Render("nonexistent_type", emailpkg.EmailContext{})
if err == nil {
t.Error("expected error for unknown purpose, got nil")
}
}
func TestAllTemplatesHaveSubjectAndPreheader(t *testing.T) {
r := newTestRenderer(t)
contexts := map[string]emailpkg.EmailContext{
"login_otp": {Code: "111111", ExpiresIn: 10},
"magic_link": {ActionURL: "https://example.com/auth", ButtonText: "Sign In", ExpiresIn: 15},
"password_reset": {ActionURL: "https://example.com/reset", ButtonText: "Reset", ExpiresIn: 60},
"email_verify": {Code: "222222", ExpiresIn: 30},
"welcome": {ActionURL: "https://example.com", ButtonText: "Get Started", Name: "Alex"},
}
for _, purpose := range r.Purposes() {
ctx := contexts[purpose]
out, err := r.Render(purpose, ctx)
if err != nil {
t.Errorf("%s: render error: %v", purpose, err)
continue
}
if out.Subject == "" {
t.Errorf("%s: Subject is empty", purpose)
}
if out.Preheader == "" {
t.Errorf("%s: Preheader is empty", purpose)
}
if out.HTML == "" {
t.Errorf("%s: HTML is empty", purpose)
}
if out.PlainText == "" {
t.Errorf("%s: PlainText is empty", purpose)
}
}
}