203 lines
5.4 KiB
Go
203 lines
5.4 KiB
Go
package email_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
emailpkg "git.threesix.ai/jordan/persona-community-2/pkg/email"
|
|
"git.threesix.ai/jordan/persona-community-2/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)
|
|
}
|
|
}
|
|
}
|