| 1 | package mail |
| 2 | |
| 3 | import ( |
| 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 | |
| 13 | const consoleMailDomain = "console-mail.bab.oncarlos.com" |
| 14 | |
| 15 | // consoleMailFixture ensures the console's own sending domain the way the |
| 16 | // console does: ops account, referrer ConsoleMailApp, a platform name. |
| 17 | func consoleMailFixture(t *testing.T) (context.Context, release.Store, *MemProvisioner, *dnsrec.MemWriter) { |
| 18 | t.Helper() |
| 19 | ctx := context.Background() |
| 20 | s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("shared-aws-account"), dnsrec.NewMemWriter() |
| 21 | now := time.Date(2026, 10, 2, 12, 0, 0, 0, time.UTC) |
| 22 | if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), release.OpsAccount, ConsoleMailApp, consoleMailDomain, now); err != nil { |
| 23 | t.Fatalf("EnsureDomain: %v", err) |
| 24 | } |
| 25 | return ctx, s, p, w |
| 26 | } |
| 27 | |
| 28 | func assertConsoleMailIntact(t *testing.T, ctx context.Context, s release.Store, p *MemProvisioner) { |
| 29 | t.Helper() |
| 30 | rec, _, err := GetDomain(ctx, s, release.OpsAccount, consoleMailDomain) |
| 31 | if err != nil || rec == nil { |
| 32 | t.Fatalf("the console's domain record is gone: %+v (err %v)", rec, err) |
| 33 | } |
| 34 | if len(rec.Apps) != 1 || rec.Apps[0] != ConsoleMailApp { |
| 35 | t.Fatalf("referrers = %v, want [%s]", rec.Apps, ConsoleMailApp) |
| 36 | } |
| 37 | if _, ok := p.Identities[identityKey("eu-west-1", consoleMailDomain)]; !ok { |
| 38 | t.Fatal("the console's SES identity was deleted") |
| 39 | } |
| 40 | if claim, err := getDomainClaim(ctx, s, consoleMailDomain); err != nil || claim == nil { |
| 41 | t.Fatalf("the console's domain claim was released: %+v (err %v)", claim, err) |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | // An app that lands on the reserved name in the ops account (a move, a |
| 46 | // migrate, a row from before the reservation) is still an app, and deleting |
| 47 | // it runs Teardown. That must not take the console's own sending domain |
| 48 | // with it. |
| 49 | func TestTeardownKeepsTheConsoleMailDomain(t *testing.T) { |
| 50 | ctx, s, p, w := consoleMailFixture(t) |
| 51 | if err := Teardown(ctx, s, p, w, testDeployment(), release.OpsAccount, ConsoleMailApp); err != nil { |
| 52 | t.Fatalf("Teardown: %v", err) |
| 53 | } |
| 54 | assertConsoleMailIntact(t, ctx, s, p) |
| 55 | } |
| 56 | |
| 57 | // The same name in any other account is an ordinary app: its Teardown |
| 58 | // releases its domains as usual. |
| 59 | func TestTeardownOfConsoleMailNameElsewhereIsOrdinary(t *testing.T) { |
| 60 | ctx := context.Background() |
| 61 | s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("shared-aws-account"), dnsrec.NewMemWriter() |
| 62 | now := time.Date(2026, 10, 2, 12, 0, 0, 0, time.UTC) |
| 63 | if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), "aaa", ConsoleMailApp, "console-mail.aaa.oncarlos.com", now); err != nil { |
| 64 | t.Fatalf("EnsureDomain: %v", err) |
| 65 | } |
| 66 | if err := Teardown(ctx, s, p, w, testDeployment(), "aaa", ConsoleMailApp); err != nil { |
| 67 | t.Fatalf("Teardown: %v", err) |
| 68 | } |
| 69 | if rec, _, err := GetDomain(ctx, s, "aaa", "console-mail.aaa.oncarlos.com"); err != nil || rec != nil { |
| 70 | t.Fatalf("an ordinary app's domain survived its teardown: %+v (err %v)", rec, err) |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | func TestRemoveDomainRefusesTheConsoleMailDomain(t *testing.T) { |
| 75 | ctx, s, p, w := consoleMailFixture(t) |
| 76 | out, err := RemoveDomain(ctx, s, p, w, testDeployment(), release.OpsAccount, ConsoleMailApp, consoleMailDomain) |
| 77 | if !errors.Is(err, ErrConsoleMailReserved) { |
| 78 | t.Fatalf("RemoveDomain = %+v, %v; want ErrConsoleMailReserved", out, err) |
| 79 | } |
| 80 | assertConsoleMailIntact(t, ctx, s, p) |
| 81 | } |
| 82 | |
| 83 | // A claim with no record (an ensure that failed after claiming) is released |
| 84 | // by RemoveDomain for any app in the account. The console's claim is not: |
| 85 | // the console re-runs its own ensure and picks it back up. |
| 86 | func TestRemoveDomainRefusesTheConsoleMailOrphanClaim(t *testing.T) { |
| 87 | ctx, s, p, w := consoleMailFixture(t) |
| 88 | if err := s.Delete(ctx, DomainKey(release.OpsAccount, consoleMailDomain)); err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | if _, err := RemoveDomain(ctx, s, p, w, testDeployment(), release.OpsAccount, "blog", consoleMailDomain); !errors.Is(err, ErrConsoleMailReserved) { |
| 92 | t.Fatalf("RemoveDomain(orphan) = %v, want ErrConsoleMailReserved", err) |
| 93 | } |
| 94 | if claim, err := getDomainClaim(ctx, s, consoleMailDomain); err != nil || claim == nil { |
| 95 | t.Fatalf("the console's orphan claim was released: %+v (err %v)", claim, err) |
| 96 | } |
| 97 | if _, ok := p.Identities[identityKey("eu-west-1", consoleMailDomain)]; !ok { |
| 98 | t.Fatal("the console's SES identity was deleted") |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | // Teardown's orphan-claim sweep is scoped to the app that made the claim, |
| 103 | // so it too must skip the console's. |
| 104 | func TestTeardownKeepsTheConsoleMailOrphanClaim(t *testing.T) { |
| 105 | ctx, s, p, w := consoleMailFixture(t) |
| 106 | if err := s.Delete(ctx, DomainKey(release.OpsAccount, consoleMailDomain)); err != nil { |
| 107 | t.Fatal(err) |
| 108 | } |
| 109 | if err := Teardown(ctx, s, p, w, testDeployment(), release.OpsAccount, ConsoleMailApp); err != nil { |
| 110 | t.Fatalf("Teardown: %v", err) |
| 111 | } |
| 112 | if claim, err := getDomainClaim(ctx, s, consoleMailDomain); err != nil || claim == nil { |
| 113 | t.Fatalf("the console's orphan claim was swept: %+v (err %v)", claim, err) |
| 114 | } |
| 115 | if _, ok := p.Identities[identityKey("eu-west-1", consoleMailDomain)]; !ok { |
| 116 | t.Fatal("the console's SES identity was deleted") |
| 117 | } |
| 118 | } |
| 119 | |