feat(API): user invitations and emails
This commit is contained in:
1 parent
7aae704d0a
commit
acdef18b37
17 files changed
+1687
-7
No files matched your search
@@ -0,0 +1,114 @@
|
||||
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: "<b>x</b>"}
|
||||
|
||||
_, body, err := repo.Render("invitation", data)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
if strings.Contains(body, "<b>x</b>") || !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 or user emails", 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", "marta@renew.example"} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Errorf("list leaks %q", forbidden)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user