carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package mail
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/dnsrec"
10 "github.com/carlosframework/platform/internal/release"
11)
12
13func seedDomain(t *testing.T, s release.Store, p *MemProvisioner) time.Time {
14 t.Helper()
15 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
16 if _, _, err := EnsureDomain(context.Background(), s, p, dnsrec.NewMemWriter(),
17 testDeployment(), "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil {
18 t.Fatalf("seed: %v", err)
19 }
20 return now
21}
22
23// All three axes must converge. An identity verified for SENDING whose
24// MAIL FROM has not converged still sends — from the wrong envelope domain,
25// failing DMARC alignment — so partial success must NOT read as verified.
26func TestVerifyRequiresEveryAxis(t *testing.T) {
27 ctx := context.Background()
28 s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
29 now := seedDomain(t, s, p)
30
31 p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com",
32 Status{DKIM: "SUCCESS", MailFrom: "PENDING", Sending: "SUCCESS"})
33 rec, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now)
34 if err != nil {
35 t.Fatalf("VerifyDomain: %v", err)
36 }
37 if rec.Regions["eu-west-1"].Status != StatusPending {
38 t.Fatal("a domain with an unconverged MAIL FROM was marked verified")
39 }
40
41 p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com",
42 Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"})
43 rec, err = VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now)
44 if err != nil {
45 t.Fatalf("VerifyDomain: %v", err)
46 }
47 if rec.Regions["eu-west-1"].Status != StatusVerified {
48 t.Fatalf("status = %q, want verified", rec.Regions["eu-west-1"].Status)
49 }
50}
51
52// SES reports a permanent DKIM failure as FAILED. That is a terminal state
53// the member must see, not a pending one they wait on forever.
54func TestVerifyRecordsAPermanentFailure(t *testing.T) {
55 ctx := context.Background()
56 s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
57 now := seedDomain(t, s, p)
58
59 p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com",
60 Status{DKIM: "FAILED", MailFrom: "PENDING", Sending: "PENDING"})
61 rec, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now)
62 if err != nil {
63 t.Fatalf("VerifyDomain: %v", err)
64 }
65 if rec.Regions["eu-west-1"].Status != StatusFailed {
66 t.Fatalf("status = %q, want failed", rec.Regions["eu-west-1"].Status)
67 }
68}
69
70// A domain that recovers must be able to leave the failed state — SES can
71// report FAILED and later succeed once DNS is fixed.
72func TestVerifyCanRecoverFromFailed(t *testing.T) {
73 ctx := context.Background()
74 s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
75 now := seedDomain(t, s, p)
76
77 p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com",
78 Status{DKIM: "FAILED", MailFrom: "PENDING", Sending: "PENDING"})
79 VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now)
80 p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com",
81 Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"})
82 rec, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now)
83 if err != nil {
84 t.Fatalf("VerifyDomain: %v", err)
85 }
86 if rec.Regions["eu-west-1"].Status != StatusVerified {
87 t.Fatal("a recovered domain stayed failed")
88 }
89}
90
91// A domain must not stay verified once SES regresses — status is
92// recomputed from scratch every call, never latched on a prior verified
93// reading. Without this test, a VerifyDomain that short-circuits on an
94// already-verified region ("nothing to do, it converged") would pass every
95// other test in this file while silently leaving a de-verified domain
96// reading verified forever.
97func TestVerifyLeavesVerifiedWhenSESRegresses(t *testing.T) {
98 ctx := context.Background()
99 s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
100 now := seedDomain(t, s, p)
101
102 p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com",
103 Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"})
104 rec, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now)
105 if err != nil {
106 t.Fatalf("VerifyDomain: %v", err)
107 }
108 if rec.Regions["eu-west-1"].Status != StatusVerified {
109 t.Fatalf("status = %q, want verified", rec.Regions["eu-west-1"].Status)
110 }
111
112 p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com",
113 Status{DKIM: "FAILED", MailFrom: "SUCCESS", Sending: "SUCCESS"})
114 rec, err = VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now)
115 if err != nil {
116 t.Fatalf("VerifyDomain: %v", err)
117 }
118 if rec.Regions["eu-west-1"].Status != StatusFailed {
119 t.Fatalf("status = %q, want failed — a previously verified domain must not stay latched verified once SES regresses", rec.Regions["eu-west-1"].Status)
120 }
121}
122
123func TestVerifyRefusesAnUnknownRegion(t *testing.T) {
124 ctx := context.Background()
125 s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
126 now := seedDomain(t, s, p)
127 if _, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "ap-south-1", now); err == nil {
128 t.Fatal("verifying a region the domain has no identity in returned success")
129 }
130}
131
132// ---- config-set reconciliation (issue #272, ruling 1) ----------------------
133
134// TestVerifyDomainRebindsADriftedConfigSet: the record and SES can disagree,
135// and when they do the tooling goes quiet about a domain that emits nothing.
136// Reconciliation lands HERE because IdentityStatus already asks SES about
137// this identity on every verify — the drift check is free, where doing it on
138// every status read would put an API call on a path that answers from the
139// bucket alone and would fail whenever AWS is unreachable.
140func TestVerifyDomainRebindsADriftedConfigSet(t *testing.T) {
141 ctx, s, p, _, now := enableFixture(t)
142 d := testDeployment()
143
144 // Something unbinds the set out of band — exactly what an operator
145 // reaching for the AWS console does.
146 if err := p.SetIdentityConfigSet(ctx, d.Region, "harbourmail.bag.oncarlos.com", ""); err != nil {
147 t.Fatalf("unbind: %v", err)
148 }
149 got, err := p.IdentityStatus(ctx, d.Region, "harbourmail.bag.oncarlos.com")
150 if err != nil || got.ConfigSet != "" {
151 t.Fatalf("precondition: identity still reports config set %q (err %v)", got.ConfigSet, err)
152 }
153
154 if _, err := VerifyDomain(ctx, s, p, d, "bag", "harbourmail.bag.oncarlos.com", d.Region, now); err != nil {
155 t.Fatalf("VerifyDomain: %v", err)
156 }
157
158 got, err = p.IdentityStatus(ctx, d.Region, "harbourmail.bag.oncarlos.com")
159 if err != nil {
160 t.Fatalf("IdentityStatus: %v", err)
161 }
162 if want := ConfigSetName("bag"); got.ConfigSet != want {
163 t.Errorf("config set after verify = %q, want %q — verify saw the drift and left it",
164 got.ConfigSet, want)
165 }
166}
167
168// TestVerifyDomainUnbindsWhenTheDeploymentHasNoTopic is the reverse
169// direction, and it matters as much: a deployment that turns event ingest
170// OFF must not leave identities bound to a configuration set whose
171// destination no longer exists, because a send through a deleted set fails.
172func TestVerifyDomainUnbindsWhenTheDeploymentHasNoTopic(t *testing.T) {
173 ctx, s, p, _, now := enableFixture(t)
174 withSNS := testDeployment()
175 noSNS := Deployment{Region: withSNS.Region, PlatformSuffix: withSNS.PlatformSuffix}
176
177 if got, _ := p.IdentityStatus(ctx, withSNS.Region, "harbourmail.bag.oncarlos.com"); got.ConfigSet == "" {
178 t.Fatal("precondition: the fixture never bound a config set")
179 }
180 if _, err := VerifyDomain(ctx, s, p, noSNS, "bag", "harbourmail.bag.oncarlos.com", noSNS.Region, now); err != nil {
181 t.Fatalf("VerifyDomain: %v", err)
182 }
183 got, err := p.IdentityStatus(ctx, noSNS.Region, "harbourmail.bag.oncarlos.com")
184 if err != nil {
185 t.Fatalf("IdentityStatus: %v", err)
186 }
187 if got.ConfigSet != "" {
188 t.Errorf("config set = %q, want empty — a deployment with no topic must not leave identities bound", got.ConfigSet)
189 }
190}
191
192// TestVerifyDomainRefreshesPoliciesWhenItRebinds is the wiring half. A
193// repair that fixes SES and the record but not the IAM policy converts
194// silent-no-events into a 554 on every send — strictly worse than the drift
195// it repaired. Same lesson as #279, arriving through the other door.
196func TestVerifyDomainRefreshesPoliciesWhenItRebinds(t *testing.T) {
197 ctx, s, p, pub, now := enableFixture(t)
198 d := testDeployment()
199 if _, err := Enable(ctx, s, p, pub, d, "bag", "harbourmail", now); err != nil {
200 t.Fatalf("Enable: %v", err)
201 }
202 // Strip the config-set ARN back out of the policy, as it would be on an
203 // app enabled before the set existed.
204 dom, _, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com")
205 if err != nil {
206 t.Fatalf("GetDomain: %v", err)
207 }
208 if _, err := p.EnsureUser(ctx, UserName("bag", "harbourmail"), UserPath,
209 dom.VerifiedARNs(), nil, []string{"harbourmail.bag.oncarlos.com"}); err != nil {
210 t.Fatalf("EnsureUser: %v", err)
211 }
212 if err := p.SetIdentityConfigSet(ctx, d.Region, "harbourmail.bag.oncarlos.com", ""); err != nil {
213 t.Fatalf("unbind: %v", err)
214 }
215
216 if _, err := VerifyDomain(ctx, s, p, d, "bag", "harbourmail.bag.oncarlos.com", d.Region, now); err != nil {
217 t.Fatalf("VerifyDomain: %v", err)
218 }
219
220 dom, _, err = GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com")
221 if err != nil {
222 t.Fatalf("GetDomain: %v", err)
223 }
224 want := dom.VerifiedConfigSetARNs()
225 if len(want) == 0 {
226 t.Fatal("nothing was bound, so this test proves nothing")
227 }
228 policy := p.Users[UserName("bag", "harbourmail")].Policy
229 for _, arn := range want {
230 if !strings.Contains(policy, arn) {
231 t.Errorf("policy does not grant %s after the rebind — repairing the binding without the policy is a 554\npolicy = %s", arn, policy)
232 }
233 }
234}
235