package test import ( "RenewCMS/internal/infrastructure/config" "RenewCMS/internal/infrastructure/mailer" "RenewCMS/internal/infrastructure/useCases" "bytes" "net/http" "os" "strings" "testing" "time" ) func TestInvitationsQaMailer(t *testing.T) { repo := mailer.NewMailRepository(config.SmtpSettings{}) t.Run("The invitation renders with the inviter, the address and the accept link", func(t *testing.T) { data := useCases.MailInviteData{Host: invitePublicURL, VerificationCode: "abc-123", Email: "lea@example.com", InvitedBy: "Ines Duarte"} subject, body, err := repo.Render("invitation", data) if err != nil { t.Fatalf("unexpected error %v", err) } if subject != "Ines Duarte invited you to RenewCMS" { t.Errorf("unexpected subject %q", subject) } for _, want := range []string{invitePublicURL + "/invite/accept?c=abc-123", "lea@example.com", "Ines Duarte"} { if !strings.Contains(body, want) { t.Errorf("body misses %q", want) } } }) t.Run("The inviter name is HTML-escaped in the body", func(t *testing.T) { data := useCases.MailInviteData{Host: invitePublicURL, VerificationCode: "abc-123", Email: "lea@example.com", InvitedBy: "x"} _, body, err := repo.Render("invitation", data) if err != nil { t.Fatalf("unexpected error %v", err) } if strings.Contains(body, "x") || !strings.Contains(body, "<b>x</b>") { t.Errorf("inviter name was not escaped") } }) t.Run("Other subjects are unchanged", func(t *testing.T) { reset, _, err := repo.Render("password-reset", useCases.MailPasswordResetData{Host: invitePublicURL, VerificationCode: "x", Email: "a@b.example"}) if err != nil || reset != "Reset your RenewCMS password" { t.Errorf("unexpected reset subject %q, %v", reset, err) } verification, _, err := repo.Render("verification", useCases.MailValidationData{Host: invitePublicURL, VerificationCode: "x"}) if err != nil || verification != "Verify your RenewCMS account" { t.Errorf("unexpected verification subject %q, %v", verification, err) } }) t.Run("The shipped template is byte-identical to the design export", func(t *testing.T) { shipped, err := os.ReadFile("../internal/infrastructure/mailer/templates/invitation.html") if err != nil { t.Fatal(err) } design, err := os.ReadFile("../docs/design/source/emails/invitation.html") if err != nil { t.Fatal(err) } if !bytes.Equal(shipped, design) { t.Errorf("template differs from docs/design/source/emails/invitation.html") } }) } func TestInvitationsQaList(t *testing.T) { t.Run("Invite status follows its age", func(t *testing.T) { f := newInviteFixture(t, 100) now := time.Now() f.invites.seed(inviteAt("old@renew.example", now.Add(-72*time.Hour-time.Minute), now.Add(-time.Minute))) f.invites.seed(inviteAt("fresh@renew.example", now.Add(-48*time.Hour), now.Add(24*time.Hour))) acceptedAt := now accepted := inviteAt("done@renew.example", now.Add(-time.Hour), now.Add(71*time.Hour)) accepted.AcceptedAt = &acceptedAt f.invites.seed(accepted) rec := f.list(t) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d", rec.Code) } statuses := map[string]string{} for _, item := range decodeList(t, rec.Body.Bytes()) { if decodedString(t, item, "kind") == "invite" { statuses[decodedString(t, item, "email")] = decodedString(t, item, "status") } } if len(statuses) != 2 || statuses["old@renew.example"] != "expired" || statuses["fresh@renew.example"] != "pending" { t.Errorf("unexpected statuses %v", statuses) } }) t.Run("The list never carries secrets", func(t *testing.T) { f := newInviteFixture(t, 100) f.invites.seed(pendingInvite("sam@renew.example")) body := f.list(t).Body.String() for _, forbidden := range []string{"\"code\"", "hashed_code", "verification_code", "password"} { if strings.Contains(body, forbidden) { t.Errorf("list leaks %q", forbidden) } } }) }