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 "errors"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/dnsrec"
10 "github.com/carlosframework/platform/internal/release"
11)
12
13func testDeployment() Deployment {
14 return Deployment{Region: "eu-west-1", PlatformSuffix: "oncarlos.com",
15 SNSTopicARN: "arn:aws:sns:eu-west-1:1:carlos-mail"}
16}
17
18func TestDomainKindSplitsOnWhoOwnsTheZone(t *testing.T) {
19 d := testDeployment()
20 if got := DomainKindFor(d, "harbourmail.bag.oncarlos.com"); got != KindPlatform {
21 t.Fatalf("kind = %q, want platform", got)
22 }
23 if got := DomainKindFor(d, "harbourmail.com"); got != KindCustom {
24 t.Fatalf("kind = %q, want custom", got)
25 }
26 // A lookalike must NOT be treated as ours — suffix matching has to be
27 // label-aware, or "evil-oncarlos.com" would have CARLOS try to write
28 // DNS into a zone it does not own.
29 if got := DomainKindFor(d, "evil-oncarlos.com"); got != KindCustom {
30 t.Fatalf("kind for a lookalike = %q, want custom", got)
31 }
32}
33
34// A platform domain is published for the member: CARLOS owns the zone, so
35// the walk writes every record itself and the member does nothing.
36func TestEnsureDomainPublishesPlatformDNS(t *testing.T) {
37 ctx := context.Background()
38 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
39 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
40
41 rec, pending, err := EnsureDomain(ctx, s, p, w, testDeployment(),
42 "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now)
43 if err != nil {
44 t.Fatalf("EnsureDomain: %v", err)
45 }
46 if rec.Kind != KindPlatform {
47 t.Fatalf("kind = %q, want platform", rec.Kind)
48 }
49 if len(pending) != 0 {
50 t.Fatalf("a platform domain must leave nothing for the member to publish, got %d", len(pending))
51 }
52 // 3 DKIM + MX + SPF + DMARC
53 if len(w.Records) != 6 {
54 t.Fatalf("published %d records, want 6: %+v", len(w.Records), w.Records)
55 }
56 st := rec.Regions["eu-west-1"]
57 if st.Status != StatusPending {
58 t.Fatalf("status = %q, want pending — DNS is written, SES has not converged", st.Status)
59 }
60 if st.MailFrom != "mail-eu-west-1.harbourmail.bag.oncarlos.com" {
61 t.Fatalf("MailFrom = %q", st.MailFrom)
62 }
63}
64
65// A custom domain is EMITTED for the customer: CARLOS cannot write their
66// zone, so the same record set comes back for them to publish and nothing
67// is written.
68func TestEnsureDomainEmitsCustomRecordsAndWritesNothing(t *testing.T) {
69 ctx := context.Background()
70 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
71 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
72
73 rec, pending, err := EnsureDomain(ctx, s, p, w, testDeployment(),
74 "bag", "harbourmail", "harbourmail.com", now)
75 if err != nil {
76 t.Fatalf("EnsureDomain: %v", err)
77 }
78 if rec.Kind != KindCustom {
79 t.Fatalf("kind = %q, want custom", rec.Kind)
80 }
81 if len(pending) != 6 {
82 t.Fatalf("emitted %d records for the customer, want 6", len(pending))
83 }
84 if len(w.Records) != 0 {
85 t.Fatal("wrote into a zone CARLOS does not own")
86 }
87}
88
89// Re-running is a no-op that returns the SAME tokens. This is the crash
90// repair path: a walk that half-failed is fixed by running it again, and
91// fresh tokens would orphan the DNS the first run published.
92func TestEnsureDomainIsIdempotent(t *testing.T) {
93 ctx := context.Background()
94 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
95 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
96 d := testDeployment()
97
98 first, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now)
99 if err != nil {
100 t.Fatalf("first: %v", err)
101 }
102 second, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now)
103 if err != nil {
104 t.Fatalf("second: %v", err)
105 }
106 f, sec := first.Regions["eu-west-1"], second.Regions["eu-west-1"]
107 if f.DKIMTokens[0] != sec.DKIMTokens[0] || f.IdentityARN != sec.IdentityARN {
108 t.Fatal("re-running changed the identity — published DNS would be orphaned")
109 }
110 if len(w.Records) != 6 {
111 t.Fatalf("re-running duplicated records: %d", len(w.Records))
112 }
113}
114
115// A second app on one domain joins the apps set rather than re-creating the
116// identity. This is the whole reason the record is account-scoped.
117func TestEnsureDomainAddsASecondAppWithoutRecreating(t *testing.T) {
118 ctx := context.Background()
119 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
120 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
121 d := testDeployment()
122
123 first, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "shared.com", now)
124 if err != nil {
125 t.Fatalf("first app: %v", err)
126 }
127 rec, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "marketing", "shared.com", now)
128 if err != nil {
129 t.Fatalf("second app: %v", err)
130 }
131 if len(rec.Apps) != 2 {
132 t.Fatalf("apps = %v, want both", rec.Apps)
133 }
134 if len(p.Identities) != 1 {
135 t.Fatalf("identities = %d, want 1 — a second app must join, not re-create", len(p.Identities))
136 }
137 if first.Regions["eu-west-1"].IdentityARN != rec.Regions["eu-west-1"].IdentityARN {
138 t.Fatal("identity ARN changed between apps — the identity was re-created")
139 }
140}
141
142// A half-failure after the identity exists but before DNS is published must
143// be repaired by a re-run, not left stuck.
144func TestEnsureDomainRepairsAHalfFailedPublish(t *testing.T) {
145 ctx := context.Background()
146 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
147 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
148 d := testDeployment()
149
150 w.FailNext["EnsureRecord"] = errors.New("route53 down")
151 if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err == nil {
152 t.Fatal("EnsureDomain succeeded despite a DNS write failure")
153 }
154 // A failed publish must leave NO record — the record must never claim
155 // DNS that was not actually written. This is what pins the ordering:
156 // create-identity, then publish, then record, in that order.
157 if rec, _, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com"); err != nil || rec != nil {
158 t.Fatalf("a failed publish left a record claiming DNS that is not there: %+v", rec)
159 }
160 if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil {
161 t.Fatalf("re-run did not repair: %v", err)
162 }
163 if len(w.Records) != 6 {
164 t.Fatalf("after repair, published %d records, want 6", len(w.Records))
165 }
166}
167
168// A region already verified must not be knocked back to pending by a
169// re-run — only a freshly-ensured region starts pending. StatusFailed is
170// deliberately NOT preserved this way (a re-run is a fresh attempt), so
171// this test targets verified specifically.
172func TestEnsureDomainPreservesVerifiedStatus(t *testing.T) {
173 ctx := context.Background()
174 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
175 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
176 d := testDeployment()
177
178 if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil {
179 t.Fatalf("first: %v", err)
180 }
181
182 // Simulate SES having converged: mark the region verified directly on
183 // the persisted record, the way a status-check walk would.
184 got, etag, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com")
185 if err != nil {
186 t.Fatalf("GetDomain: %v", err)
187 }
188 got.Regions["eu-west-1"].Status = StatusVerified
189 if _, err := PutDomain(ctx, s, "bag", got, etag); err != nil {
190 t.Fatalf("PutDomain: %v", err)
191 }
192
193 second, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now)
194 if err != nil {
195 t.Fatalf("second: %v", err)
196 }
197 if got := second.Regions["eu-west-1"].Status; got != StatusVerified {
198 t.Fatalf("status = %q, want verified — a re-run must not knock back a converged region", got)
199 }
200}
201
202// A deployment with no SNS topic must not record a configuration set name
203// for a set that was never created — a later send against a phantom set
204// fails with "Configuration set does not exist" on every attempt.
205func TestEnsureDomainLeavesConfigSetEmptyWithoutSNS(t *testing.T) {
206 ctx := context.Background()
207 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
208 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
209 d := testDeployment()
210 d.SNSTopicARN = ""
211
212 rec, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now)
213 if err != nil {
214 t.Fatalf("EnsureDomain: %v", err)
215 }
216 if got := rec.Regions["eu-west-1"].ConfigSet; got != "" {
217 t.Fatalf("ConfigSet = %q, want empty — no SNS topic means no config set was created", got)
218 }
219}
220
221// C1 (review follow-up): recording a config set NAME in the domain record
222// is not the same as SES actually emitting events for that identity — SES
223// only does that for a send carrying a configuration set, and nothing
224// before this task ever bound the identity to one. A domain ensured
225// against a deployment WITH an SNS topic must have its identity's default
226// configuration set stamped AT THE PROVISIONER, not just recorded in the
227// bucket.
228func TestEnsureDomainWithSNSStampsTheIdentitysConfigSet(t *testing.T) {
229 ctx := context.Background()
230 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
231 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
232 d := testDeployment() // carries an SNSTopicARN
233
234 if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil {
235 t.Fatalf("EnsureDomain: %v", err)
236 }
237 id, ok := p.Identities[identityKey(d.Region, "harbourmail.bag.oncarlos.com")]
238 if !ok {
239 t.Fatal("no identity recorded at the provisioner")
240 }
241 if want := ConfigSetName("bag"); id.ConfigSet != want {
242 t.Fatalf("identity ConfigSet = %q, want %q — SetIdentityConfigSet was never called, so SES "+
243 "would never emit an event for this identity and the daily cap/abuse pause would never fire",
244 id.ConfigSet, want)
245 }
246}
247
248// The symmetric SNS-less case: without a configuration set to bind to,
249// SetIdentityConfigSet must not be called at all — calling it with an
250// empty name would either error at a real backend or silently bind the
251// identity to nothing.
252func TestEnsureDomainWithoutSNSStampsNoIdentityConfigSet(t *testing.T) {
253 ctx := context.Background()
254 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
255 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
256 d := testDeployment()
257 d.SNSTopicARN = ""
258
259 if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil {
260 t.Fatalf("EnsureDomain: %v", err)
261 }
262 id, ok := p.Identities[identityKey(d.Region, "harbourmail.bag.oncarlos.com")]
263 if !ok {
264 t.Fatal("no identity recorded at the provisioner")
265 }
266 if id.ConfigSet != "" {
267 t.Fatalf("identity ConfigSet = %q, want empty — no SNS topic means no config set to bind to", id.ConfigSet)
268 }
269}
270
271// acct reaches EnsureTenant — a real backend call — before anything checks
272// it, unless validation gates the walk before that call. app is persisted
273// into the record and later turned into an IAM principal name by other
274// walks, so it must be validated too.
275func TestEnsureDomainRefusesInvalidAccount(t *testing.T) {
276 ctx := context.Background()
277 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
278 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
279
280 if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), "../evil", "harbourmail", "harbourmail.bag.oncarlos.com", now); err == nil {
281 t.Fatal("EnsureDomain accepted an invalid account")
282 }
283 if p.Tenants["../evil"] || p.Tenants[TenantName("../evil")] {
284 t.Fatal("an invalid account reached EnsureTenant before validation")
285 }
286}
287
288// EnsureDomain must create the tenant under its PREFIXED name, never the
289// bare account sqid — the SES tenant namespace is shared with other
290// products in the same AWS account (TenantName's doc comment in policy.go
291// has the live collision this guards against), so a bare sqid landing in
292// p.Tenants would be the exact defect this closes.
293func TestEnsureDomainCreatesPrefixedTenant(t *testing.T) {
294 ctx := context.Background()
295 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
296 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
297
298 if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(),
299 "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil {
300 t.Fatalf("EnsureDomain: %v", err)
301 }
302 if !p.Tenants["carlos-bag"] {
303 t.Fatal("EnsureDomain did not create the prefixed tenant carlos-bag")
304 }
305 if p.Tenants["bag"] {
306 t.Fatal("EnsureDomain created a bare-sqid tenant — this collides with another product's namespace")
307 }
308}
309
310func TestEnsureDomainRefusesInvalidApp(t *testing.T) {
311 ctx := context.Background()
312 s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter()
313 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
314
315 if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), "bag", "../evil", "harbourmail.bag.oncarlos.com", now); err == nil {
316 t.Fatal("EnsureDomain accepted an invalid app")
317 }
318}
319
320// failAfterN wraps a Writer and fails the (n+1)th EnsureRecord, so a
321// PARTIAL publish can be exercised — MemWriter.FailNext is one-shot on
322// the first call, which is the least realistic crash point for this walk
323// (three published, then a crash, is the realistic one). After the one
324// failure it behaves like the wrapped Writer for good, matching how a
325// transient DNS-provider failure would look on a retry.
326type failAfterN struct {
327 dnsrec.Writer
328 n int
329 err error
330 failed bool
331}
332
333func (f *failAfterN) EnsureRecord(ctx context.Context, r dnsrec.Record) error {
334 if !f.failed && f.n <= 0 {
335 f.failed = true
336 return f.err
337 }
338 if !f.failed {
339 f.n--
340 }
341 return f.Writer.EnsureRecord(ctx, r)
342}
343
344// A crash after SOME records are published but before all six is the
345// realistic failure mode this walk must survive. A re-run must converge to
346// the full set with no duplicates, and the half-published attempt must not
347// have persisted a record claiming DNS that is not fully there.
348func TestEnsureDomainRepairsAPartialPublish(t *testing.T) {
349 ctx := context.Background()
350 s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
351 inner := dnsrec.NewMemWriter()
352 w := &failAfterN{Writer: inner, n: 3, err: errors.New("route53 down mid-publish")}
353 now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
354 d := testDeployment()
355
356 if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err == nil {
357 t.Fatal("EnsureDomain succeeded despite a mid-publish failure")
358 }
359 if len(inner.Records) != 3 {
360 t.Fatalf("published %d records before the failure, want 3", len(inner.Records))
361 }
362 if rec, _, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com"); err != nil || rec != nil {
363 t.Fatalf("a partial publish left a record claiming DNS that is not fully there: %+v", rec)
364 }
365
366 if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil {
367 t.Fatalf("re-run did not repair: %v", err)
368 }
369 if len(inner.Records) != 6 {
370 t.Fatalf("after repair, published %d records, want 6 with no duplicates", len(inner.Records))
371 }
372}
373
374// Step 2 associates each account's EXISTING tenant with its identities and
375// its configuration set (design §7, "Associations are wired in step 2, not
376// here"). Associating a resource does not modify it and changes no
377// behaviour on its own: no send carries X-SES-TENANT yet. It is done now so
378// that step 3 can rely on it, and because SES validates the association on
379// every tenant-attributed send.
380func TestEnsureDomainAssociatesTheIdentityAndTheSetWithTheAccountsTenant(t *testing.T) {
381 ctx := context.Background()
382 s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
383 now := time.Date(2026, 9, 15, 12, 0, 0, 0, time.UTC)
384 if _, err := Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP",
385 "noreply@harbourmail.bag.oncarlos.com", "owner@example.test", now); err != nil {
386 t.Fatal(err)
387 }
388 rec, _, err := EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(),
389 "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now)
390 if err != nil {
391 t.Fatal(err)
392 }
393 tenant := TenantName("bag")
394 identity := rec.Regions["eu-west-1"].IdentityARN
395 set, ok := ConfigSetARN(identity, ConfigSetName("bag"))
396 if !ok {
397 t.Fatalf("no configuration-set ARN composed from %q", identity)
398 }
399 for _, arn := range []string{identity, set} {
400 if !p.TenantResources[tenant][arn] {
401 t.Errorf("%s is not associated with %s: %v", arn, tenant, p.TenantResources[tenant])
402 }
403 }
404 // A re-run associates the same pair again without error: every verb on
405 // this seam is idempotent, which is what makes the walk re-runnable.
406 if _, _, err := EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(),
407 "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil {
408 t.Fatalf("re-run: %v", err)
409 }
410
411 // An identity cannot be deleted while an association exists (design §3),
412 // so teardown removes the association first. MemProvisioner models that
413 // refusal, because without it the first real teardown would fail.
414 if err := Teardown(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(), "bag", "harbourmail"); err != nil {
415 t.Fatalf("Teardown: %v", err)
416 }
417 if p.TenantResources[tenant][identity] {
418 t.Error("the identity association survived teardown, so the identity could never be deleted")
419 }
420 // The configuration set is per ACCOUNT and outlives any one app, so its
421 // association is never removed by an app's teardown.
422 if !p.TenantResources[tenant][set] {
423 t.Error("an app's teardown removed the account's configuration-set association")
424 }
425}
426