148 lines
3.9 KiB
Go
148 lines
3.9 KiB
Go
package mail
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRenderLoginCode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
subject, body := renderLoginCode("123456", 10*time.Minute)
|
|
if !strings.Contains(subject, "123456") {
|
|
t.Fatalf("subject must include code, got %q", subject)
|
|
}
|
|
if !strings.Contains(body, "123456") {
|
|
t.Fatalf("body must include code, got %q", body)
|
|
}
|
|
if !strings.Contains(body, "10 minutes") {
|
|
t.Fatalf("body must include human-readable TTL, got %q", body)
|
|
}
|
|
}
|
|
|
|
func TestRenderLoginCode_RoundsTTL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := map[string]struct {
|
|
ttl time.Duration
|
|
expect string
|
|
}{
|
|
"sub-minute": {ttl: 30 * time.Second, expect: "1 minutes"},
|
|
"exact": {ttl: 10 * time.Minute, expect: "10 minutes"},
|
|
"with secs": {ttl: 5*time.Minute + 29*time.Second, expect: "5 minutes"},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
_, body := renderLoginCode("000000", tc.ttl)
|
|
if !strings.Contains(body, tc.expect) {
|
|
t.Fatalf("body missing %q for ttl=%s, got %q", tc.expect, tc.ttl, body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormaliseRecipient(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := map[string]struct {
|
|
input string
|
|
want string
|
|
err bool
|
|
}{
|
|
"plain": {input: "alice@example.test", want: "alice@example.test"},
|
|
"trims": {input: " bob@example.test ", want: "bob@example.test"},
|
|
"display-stripped": {input: "Alice <alice@example.test>", want: "alice@example.test"},
|
|
"empty": {input: "", err: true},
|
|
"whitespace": {input: " ", err: true},
|
|
"malformed": {input: "not-an-email", err: true},
|
|
"with-spaces": {input: "ali ce@example.test", err: true},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, err := normaliseRecipient(tc.input)
|
|
if tc.err {
|
|
if err == nil {
|
|
t.Fatalf("expected error, got %q", got)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != tc.want {
|
|
t.Fatalf("got %q want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTemplateRendererLoginCode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
render := templateRenderers[TemplateLoginCode]
|
|
if render == nil {
|
|
t.Fatal("TemplateLoginCode renderer must be registered")
|
|
}
|
|
subject, body, err := render(map[string]any{"code": "654321", "ttl": 7 * time.Minute})
|
|
if err != nil {
|
|
t.Fatalf("render: %v", err)
|
|
}
|
|
if !strings.Contains(subject, "654321") || !strings.Contains(body, "654321") {
|
|
t.Fatalf("subject=%q body=%q must mention code", subject, body)
|
|
}
|
|
if _, _, err := render(map[string]any{"ttl": 7 * time.Minute}); err == nil {
|
|
t.Fatal("missing code must error")
|
|
}
|
|
}
|
|
|
|
func TestNextBackoffMonotonicAndCapped(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Sample many runs per attempt so jitter does not flake the
|
|
// invariant: median of attempt N is below median of attempt N+1
|
|
// up to the cap.
|
|
prev := time.Duration(0)
|
|
for n := 1; n <= 12; n++ {
|
|
var sum time.Duration
|
|
runs := 32
|
|
for range runs {
|
|
sum += nextBackoff(n)
|
|
}
|
|
avg := sum / time.Duration(runs)
|
|
if avg > backoffMax+backoffMax/4 { // generous upper bound
|
|
t.Fatalf("attempt %d avg %s exceeds capped budget", n, avg)
|
|
}
|
|
if avg < backoffBase/2 {
|
|
t.Fatalf("attempt %d avg %s below base/2", n, avg)
|
|
}
|
|
if n > 1 && avg < prev/2 {
|
|
t.Fatalf("backoff decreased dramatically between attempts %d and %d (%s vs %s)", n-1, n, prev, avg)
|
|
}
|
|
prev = avg
|
|
}
|
|
}
|
|
|
|
func TestIsPermanent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if IsPermanent(nil) {
|
|
t.Fatal("nil must not be permanent")
|
|
}
|
|
transient := &SendError{Err: errSentinel("transient")}
|
|
if IsPermanent(transient) {
|
|
t.Fatal("default SendError must not be permanent")
|
|
}
|
|
permanent := &SendError{Err: errSentinel("permanent"), Permanent: true}
|
|
if !IsPermanent(permanent) {
|
|
t.Fatal("Permanent=true must report true")
|
|
}
|
|
}
|
|
|
|
// errSentinel is a tiny sentinel error helper used only in tests.
|
|
type errSentinel string
|
|
|
|
func (e errSentinel) Error() string { return string(e) }
|