package services import ( "context" "strings" "testing" ) // TestGetActiveEmbeddedFallback covers the no-DB path. Without a sqlx.DB, // every key/lang pair returns the embedded default with IsDefault=true. func TestGetActiveEmbeddedFallback(t *testing.T) { svc := NewEmailTemplateService(nil) for _, key := range CanonicalEmailTemplateKeys { for _, lang := range EmailTemplateLanguages { row, err := svc.GetActive(context.Background(), key, lang) if err != nil { t.Errorf("GetActive(%s, %s): %v", key, lang, err) continue } if !row.IsDefault { t.Errorf("GetActive(%s, %s): IsDefault=false on no-DB service", key, lang) } if row.Body == "" { t.Errorf("GetActive(%s, %s): empty body", key, lang) } if key == EmailTemplateKeyBase { if row.Subject != "" { t.Errorf("base subject expected empty, got %q", row.Subject) } } else if row.Subject == "" { t.Errorf("GetActive(%s, %s): empty subject for non-base key", key, lang) } } } } // TestGetActiveUnknownKey ensures unknown keys are 404-shaped. func TestGetActiveUnknownKey(t *testing.T) { svc := NewEmailTemplateService(nil) if _, err := svc.GetActive(context.Background(), "nope", "de"); err != ErrTemplateUnknownKey { t.Errorf("expected ErrTemplateUnknownKey, got %v", err) } } // TestGetActiveUnknownLang ensures unknown languages are 400-shaped. func TestGetActiveUnknownLang(t *testing.T) { svc := NewEmailTemplateService(nil) if _, err := svc.GetActive(context.Background(), EmailTemplateKeyInvitation, "fr"); err != ErrTemplateUnknownLang { t.Errorf("expected ErrTemplateUnknownLang, got %v", err) } } // TestSaveRequiresStore checks that mutations against a no-DB service fail // closed — no silent acceptance, no in-memory drift. func TestSaveRequiresStore(t *testing.T) { svc := NewEmailTemplateService(nil) _, err := svc.Save(context.Background(), SaveInput{ Key: EmailTemplateKeyInvitation, Lang: "de", Subject: "x", Body: `{{define "content"}}

x

{{end}}`, }) if err != ErrTemplateStoreUnavailable { t.Errorf("expected ErrTemplateStoreUnavailable, got %v", err) } } // TestValidateTemplate checks every code path of the validation function. // Save and the preview endpoint both lean on this — a drift here breaks both. func TestValidateTemplate(t *testing.T) { cases := []struct { name string key, subj string body string wantErr error wantSubsErr string // substring required in the wrapped error }{ { "invitation valid", EmailTemplateKeyInvitation, "[Paliad] {{.InviterName}} invites you", `{{define "content"}}

{{.InviterName}}

{{end}}`, nil, "", }, { "invitation missing content block", EmailTemplateKeyInvitation, "x", `

oops, no define

`, ErrTemplateMissingContent, "", }, { "invitation bad body syntax", EmailTemplateKeyInvitation, "x", `{{define "content"}}

{{.InviterName}{{end}}`, nil, "syntax", }, { "invitation bad subject syntax", EmailTemplateKeyInvitation, `[Paliad] {{.InviterName`, `{{define "content"}}

x

{{end}}`, nil, "syntax", }, { "base valid", EmailTemplateKeyBase, "", `{{block "content" .}}{{end}}`, nil, "", }, { "base missing block call", EmailTemplateKeyBase, "", `no inner`, ErrTemplateMissingBaseBlock, "", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { err := ValidateTemplate(tc.key, tc.subj, tc.body) if tc.wantErr != nil { if err != tc.wantErr { t.Errorf("got %v, want %v", err, tc.wantErr) } return } if tc.wantSubsErr != "" { if err == nil || !strings.Contains(err.Error(), tc.wantSubsErr) { t.Errorf("got %v, want substring %q", err, tc.wantSubsErr) } return } if err != nil { t.Errorf("expected no error, got %v", err) } }) } } // TestEmailTemplateVariablesShape ensures every canonical key has a non-empty // variable contract — the editor sidebar would render an empty box otherwise. func TestEmailTemplateVariablesShape(t *testing.T) { for _, key := range CanonicalEmailTemplateKeys { vars := EmailTemplateVariables(key) if len(vars) == 0 { t.Errorf("key %s: no variables registered", key) } for _, v := range vars { if v.Name == "" || v.Type == "" || v.Description == "" { t.Errorf("key %s: variable %+v has empty field", key, v) } } } }