| 1 | package mail |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ecdh" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/appconfig" |
| 11 | "github.com/carlosframework/platform/internal/dnsrec" |
| 12 | "github.com/carlosframework/platform/internal/release" |
| 13 | ) |
| 14 | |
| 15 | // enableFixture seeds a declared app with one VERIFIED domain — the state |
| 16 | // Enable expects to find. |
| 17 | func enableFixture(t *testing.T) (context.Context, release.Store, *MemProvisioner, *ecdh.PublicKey, time.Time) { |
| 18 | t.Helper() |
| 19 | ctx := context.Background() |
| 20 | s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") |
| 21 | now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) |
| 22 | |
| 23 | _, pubB64, err := appconfig.GenerateKey() |
| 24 | if err != nil { |
| 25 | t.Fatalf("GenerateKey: %v", err) |
| 26 | } |
| 27 | pub, err := appconfig.ParsePublicKey(pubB64, "test") |
| 28 | if err != nil { |
| 29 | t.Fatalf("ParsePublicKey: %v", err) |
| 30 | } |
| 31 | |
| 32 | if _, err := Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", |
| 33 | "noreply@harbourmail.bag.oncarlos.com", "owner@example.test", now); err != nil { |
| 34 | t.Fatalf("Declare: %v", err) |
| 35 | } |
| 36 | if _, _, err := EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(), |
| 37 | "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { |
| 38 | t.Fatalf("EnsureDomain: %v", err) |
| 39 | } |
| 40 | p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com", |
| 41 | Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) |
| 42 | if _, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now); err != nil { |
| 43 | t.Fatalf("VerifyDomain: %v", err) |
| 44 | } |
| 45 | return ctx, s, p, pub, now |
| 46 | } |
| 47 | |
| 48 | // The v1 criterion in one test: the five variables harbourmail already |
| 49 | // reads arrive, with the right values, and the app changes nothing. |
| 50 | func TestEnableDeliversHarbourmailsEnv(t *testing.T) { |
| 51 | ctx, s, p, pub, now := enableFixture(t) |
| 52 | rec, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 53 | if err != nil { |
| 54 | t.Fatalf("Enable: %v", err) |
| 55 | } |
| 56 | if rec.Provisioned == nil || rec.Provisioned.KeyID == "" { |
| 57 | t.Fatalf("nothing provisioned: %+v", rec) |
| 58 | } |
| 59 | man, err := appconfig.Get(ctx, s, "bag", "harbourmail") |
| 60 | if err != nil { |
| 61 | t.Fatalf("appconfig.Get: %v", err) |
| 62 | } |
| 63 | if got := man.Vars["HARBOURMAIL_SMTP_HOST"].Value; got != "email-smtp.eu-west-1.amazonaws.com" { |
| 64 | t.Fatalf("HOST = %q", got) |
| 65 | } |
| 66 | if got := man.Vars["HARBOURMAIL_SMTP_PORT"].Value; got != "587" { |
| 67 | t.Fatalf("PORT = %q", got) |
| 68 | } |
| 69 | if got := man.Vars["HARBOURMAIL_SMTP_FROM"].Value; got != "noreply@harbourmail.bag.oncarlos.com" { |
| 70 | t.Fatalf("FROM = %q", got) |
| 71 | } |
| 72 | // The username is the access key id and is NOT a secret — it is |
| 73 | // delivered as a plain env var, unsealed, precisely so it can be |
| 74 | // compared against rec.Provisioned.KeyID as a staleness check. The |
| 75 | // password IS the secret and must arrive sealed. |
| 76 | if got := man.Vars["HARBOURMAIL_SMTP_USER"]; got.Kind != "env" || got.Value != rec.Provisioned.KeyID { |
| 77 | t.Fatalf("USER = %+v, want Kind=env Value=%q", got, rec.Provisioned.KeyID) |
| 78 | } |
| 79 | if man.Vars["HARBOURMAIL_SMTP_PASS"].Kind != "secret" { |
| 80 | t.Fatal("PASS was not delivered as a sealed secret") |
| 81 | } |
| 82 | if man.Vars["HARBOURMAIL_SMTP_PASS"].Envelope == "" { |
| 83 | t.Fatal("PASS has no envelope") |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | // The delivered password is the DERIVED one, not the raw IAM secret. This |
| 88 | // is the test that catches delivering the secret access key by mistake — |
| 89 | // which would authenticate against nothing and leak a real AWS credential |
| 90 | // into an app's environment. |
| 91 | // |
| 92 | // This opens the actual delivered envelope rather than just re-deriving the |
| 93 | // expected value from the fake's plaintext: a version that only checked |
| 94 | // "the derivation function returns something different from its input" |
| 95 | // would pass even if Enable delivered the raw secret, because it never |
| 96 | // looks at what was actually sealed and sent. |
| 97 | func TestEnableDeliversTheDerivedPasswordNotTheSecret(t *testing.T) { |
| 98 | ctx, s, p, _, now := enableFixture(t) |
| 99 | privB64, pubB64, err := appconfig.GenerateKey() |
| 100 | if err != nil { |
| 101 | t.Fatalf("GenerateKey: %v", err) |
| 102 | } |
| 103 | priv, err := appconfig.ParsePrivateKey(privB64, "test") |
| 104 | if err != nil { |
| 105 | t.Fatalf("ParsePrivateKey: %v", err) |
| 106 | } |
| 107 | ownPub, err := appconfig.ParsePublicKey(pubB64, "test") |
| 108 | if err != nil { |
| 109 | t.Fatalf("ParsePublicKey: %v", err) |
| 110 | } |
| 111 | |
| 112 | rec, err := Enable(ctx, s, p, ownPub, testDeployment(), "bag", "harbourmail", now) |
| 113 | if err != nil { |
| 114 | t.Fatalf("Enable: %v", err) |
| 115 | } |
| 116 | raw := p.Users[UserName("bag", "harbourmail")].Keys[rec.Provisioned.KeyID].Secret |
| 117 | want := SMTPPassword(raw, "eu-west-1") |
| 118 | if want == raw { |
| 119 | t.Fatal("test is not meaningful — derivation returned the input") |
| 120 | } |
| 121 | |
| 122 | man, err := appconfig.Get(ctx, s, "bag", "harbourmail") |
| 123 | if err != nil { |
| 124 | t.Fatalf("appconfig.Get: %v", err) |
| 125 | } |
| 126 | env := man.Vars["HARBOURMAIL_SMTP_PASS"].Envelope |
| 127 | if env == "" { |
| 128 | t.Fatal("PASS has no envelope") |
| 129 | } |
| 130 | plain, err := appconfig.Open(priv, appconfig.Binding{Account: "bag", App: "harbourmail"}, env) |
| 131 | if err != nil { |
| 132 | t.Fatalf("Open: %v", err) |
| 133 | } |
| 134 | if string(plain) != want { |
| 135 | t.Fatalf("delivered password = %q, want the derived password %q", plain, want) |
| 136 | } |
| 137 | if string(plain) == raw { |
| 138 | t.Fatal("Enable delivered the raw IAM secret instead of the derived SMTP password") |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | // A healthy provisioned app makes a re-run a no-op: no fresh key, no |
| 143 | // rewrite. Minting on every pass would burn IAM's two-key ceiling and |
| 144 | // churn every instance's config. |
| 145 | // TestEnsureDomainRefreshesPoliciesWhenItBindsAConfigSet is the trap that |
| 146 | // shipped in v0.12.0 and that #268's own advice walks a member straight into. |
| 147 | // |
| 148 | // EnsureDomain binds the identity to a configuration set. EnsureUser — the |
| 149 | // only place a credential's policy is written — runs in Enable and |
| 150 | // CreateStandalone, NOT here. So for an app enabled BEFORE a config set |
| 151 | // existed (every app on a deployment that arms SNS later), `carlos email |
| 152 | // domains add` bound the set and left the policy naming only the identity, |
| 153 | // which SES answers with a 554 on every send. That is the #271 shape |
| 154 | // arriving through the repair path instead of the provisioning one, and it |
| 155 | // is strictly worse than the drift it was meant to fix: silent-no-events |
| 156 | // becomes silent-no-sending. |
| 157 | // |
| 158 | // The fix is that binding and granting happen together. This test pins the |
| 159 | // pairing rather than either half. |
| 160 | func TestEnsureDomainRefreshesPoliciesWhenItBindsAConfigSet(t *testing.T) { |
| 161 | ctx := context.Background() |
| 162 | s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") |
| 163 | now := time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC) |
| 164 | |
| 165 | _, pubB64, err := appconfig.GenerateKey() |
| 166 | if err != nil { |
| 167 | t.Fatalf("GenerateKey: %v", err) |
| 168 | } |
| 169 | pub, err := appconfig.ParsePublicKey(pubB64, "test") |
| 170 | if err != nil { |
| 171 | t.Fatalf("ParsePublicKey: %v", err) |
| 172 | } |
| 173 | |
| 174 | // A deployment with NO SNS topic: the domain verifies and the app is |
| 175 | // enabled with no configuration set anywhere — the ordinary shape |
| 176 | // before event ingest is armed. |
| 177 | noSNS := Deployment{Region: "eu-west-1", PlatformSuffix: "oncarlos.com"} |
| 178 | if _, err := Declare(ctx, s, "bag", "seapointish", "SEAPOINTISH_SMTP", |
| 179 | "noreply@seapointish.bag.oncarlos.com", "paul@rslw.com", now); err != nil { |
| 180 | t.Fatalf("Declare: %v", err) |
| 181 | } |
| 182 | if _, _, err := EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), noSNS, |
| 183 | "bag", "seapointish", "seapointish.bag.oncarlos.com", now); err != nil { |
| 184 | t.Fatalf("EnsureDomain: %v", err) |
| 185 | } |
| 186 | p.SetIdentityStatus("eu-west-1", "seapointish.bag.oncarlos.com", |
| 187 | Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) |
| 188 | if _, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "seapointish.bag.oncarlos.com", "eu-west-1", now); err != nil { |
| 189 | t.Fatalf("VerifyDomain: %v", err) |
| 190 | } |
| 191 | if _, err := Enable(ctx, s, p, pub, noSNS, "bag", "seapointish", now); err != nil { |
| 192 | t.Fatalf("Enable: %v", err) |
| 193 | } |
| 194 | if _, err := CreateStandalone(ctx, s, p, noSNS, "bag", "seapointish", "smoke", "paul@rslw.com", now); err != nil { |
| 195 | t.Fatalf("CreateStandalone: %v", err) |
| 196 | } |
| 197 | |
| 198 | // Now the deployment arms SNS and the member follows the advice the |
| 199 | // status warning prints. |
| 200 | withSNS := testDeployment() |
| 201 | if _, _, err := EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), withSNS, |
| 202 | "bag", "seapointish", "seapointish.bag.oncarlos.com", now); err != nil { |
| 203 | t.Fatalf("EnsureDomain (with SNS): %v", err) |
| 204 | } |
| 205 | |
| 206 | dom, _, err := GetDomain(ctx, s, "bag", "seapointish.bag.oncarlos.com") |
| 207 | if err != nil { |
| 208 | t.Fatalf("GetDomain: %v", err) |
| 209 | } |
| 210 | want := dom.VerifiedConfigSetARNs() |
| 211 | if len(want) == 0 { |
| 212 | t.Fatal("no configuration set was bound, so this test proves nothing") |
| 213 | } |
| 214 | for _, u := range []string{UserName("bag", "seapointish"), StandaloneUserName("bag", "seapointish", "smoke")} { |
| 215 | mu, ok := p.Users[u] |
| 216 | if !ok { |
| 217 | t.Fatalf("user %s does not exist", u) |
| 218 | } |
| 219 | for _, arn := range want { |
| 220 | if !strings.Contains(mu.Policy, arn) { |
| 221 | t.Errorf("%s: policy does not grant %s after the set was bound\n"+ |
| 222 | "binding without refreshing the policy is a 554 on every send — worse than the drift it repairs\npolicy = %s", |
| 223 | u, arn, mu.Policy) |
| 224 | } |
| 225 | } |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | // TestEnableGrantsTheConfigSetToTheDeliveredUser is the WALK half of issue |
| 230 | // #271, and it exists because mutation testing showed the policy-level tests |
| 231 | // could not see this: deleting the line in Enable that gathers config-set |
| 232 | // ARNs left every test in this package green while reproducing the exact |
| 233 | // outage — an identity bound to a configuration set, and a delivered |
| 234 | // credential whose policy does not mention it, which SES refuses with a 554. |
| 235 | // |
| 236 | // The assertion is deliberately taken from the RECORD rather than from a |
| 237 | // hardcoded name, and it fails when the record contributes none, so it |
| 238 | // cannot pass vacuously the day EnsureDomain stops binding a set. |
| 239 | func TestEnableGrantsTheConfigSetToTheDeliveredUser(t *testing.T) { |
| 240 | ctx, s, p, pub, now := enableFixture(t) |
| 241 | if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != nil { |
| 242 | t.Fatalf("Enable: %v", err) |
| 243 | } |
| 244 | dom, _, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com") |
| 245 | if err != nil { |
| 246 | t.Fatalf("GetDomain: %v", err) |
| 247 | } |
| 248 | want := dom.VerifiedConfigSetARNs() |
| 249 | if len(want) == 0 { |
| 250 | t.Fatal("the fixture bound no configuration set, so this test proves nothing — testDeployment() must carry an SNS topic") |
| 251 | } |
| 252 | policy := p.Users[UserName("bag", "harbourmail")].Policy |
| 253 | for _, arn := range want { |
| 254 | if !strings.Contains(policy, arn) { |
| 255 | t.Errorf("delivered user's policy does not grant %s\npolicy = %s\n"+ |
| 256 | "SES authorises a configuration set as its own resource: without this every send is a 554", arn, policy) |
| 257 | } |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | // TestCreateStandaloneGrantsTheConfigSetToo: the standalone credential is a |
| 262 | // SEPARATE IAM user with its own policy, minted by its own walk, so it can |
| 263 | // regress independently of the delivered one. |
| 264 | func TestCreateStandaloneGrantsTheConfigSetToo(t *testing.T) { |
| 265 | ctx, s, p, pub, now := enableFixture(t) |
| 266 | if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != nil { |
| 267 | t.Fatalf("Enable: %v", err) |
| 268 | } |
| 269 | if _, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "harbourmail", "smoke", "owner@example.test", now); err != nil { |
| 270 | t.Fatalf("CreateStandalone: %v", err) |
| 271 | } |
| 272 | dom, _, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com") |
| 273 | if err != nil { |
| 274 | t.Fatalf("GetDomain: %v", err) |
| 275 | } |
| 276 | want := dom.VerifiedConfigSetARNs() |
| 277 | if len(want) == 0 { |
| 278 | t.Fatal("the fixture bound no configuration set, so this test proves nothing") |
| 279 | } |
| 280 | policy := p.Users[StandaloneUserName("bag", "harbourmail", "smoke")].Policy |
| 281 | for _, arn := range want { |
| 282 | if !strings.Contains(policy, arn) { |
| 283 | t.Errorf("standalone user's policy does not grant %s\npolicy = %s", arn, policy) |
| 284 | } |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | func TestEnableIsIdempotent(t *testing.T) { |
| 289 | ctx, s, p, pub, now := enableFixture(t) |
| 290 | first, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 291 | if err != nil { |
| 292 | t.Fatalf("first Enable: %v", err) |
| 293 | } |
| 294 | second, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 295 | if err != nil { |
| 296 | t.Fatalf("second Enable: %v", err) |
| 297 | } |
| 298 | if first.Provisioned.KeyID != second.Provisioned.KeyID { |
| 299 | t.Fatal("a re-run minted a fresh key") |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | // Enable must refuse an app with no VERIFIED domain. A credential with no |
| 304 | // identity is a principal that can send nothing while looking provisioned. |
| 305 | func TestEnableRefusesWithoutAVerifiedDomain(t *testing.T) { |
| 306 | ctx := context.Background() |
| 307 | s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") |
| 308 | now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) |
| 309 | _, pubB64, _ := appconfig.GenerateKey() |
| 310 | pub, _ := appconfig.ParsePublicKey(pubB64, "test") |
| 311 | |
| 312 | Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", "n@x.com", "owner@example.test", now) |
| 313 | EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(), |
| 314 | "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now) // left pending |
| 315 | if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err == nil { |
| 316 | t.Fatal("Enable succeeded with only a pending domain") |
| 317 | } |
| 318 | } |
| 319 | |
| 320 | func TestEnableRefusesAnUndeclaredApp(t *testing.T) { |
| 321 | ctx := context.Background() |
| 322 | s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") |
| 323 | now := time.Now().UTC() |
| 324 | _, pubB64, _ := appconfig.GenerateKey() |
| 325 | pub, _ := appconfig.ParsePublicKey(pubB64, "test") |
| 326 | if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != ErrNotDeclared { |
| 327 | t.Fatalf("Enable on an undeclared app = %v, want ErrNotDeclared", err) |
| 328 | } |
| 329 | } |
| 330 | |
| 331 | // The From address must be at a granted domain. Otherwise the policy's |
| 332 | // ses:FromAddress condition refuses every send at runtime — a failure that |
| 333 | // surfaces as "email silently does not work" rather than as a refusal here. |
| 334 | // |
| 335 | // Declare itself accepts any From (a member may declare before any domain |
| 336 | // is verified); Enable, which knows the granted domains, is what refuses |
| 337 | // one outside every verified domain. |
| 338 | func TestDeclareAcceptsAnyFromButEnableRefusesAnUngrantedOne(t *testing.T) { |
| 339 | ctx, s, p, pub, now := enableFixture(t) |
| 340 | if _, err := Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", |
| 341 | "noreply@somewhere-else.com", "owner@example.test", now); err != nil { |
| 342 | t.Fatalf("Declare itself may accept any From: %v", err) |
| 343 | } |
| 344 | // but Enable, which knows the granted domains, must refuse it |
| 345 | if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err == nil { |
| 346 | t.Fatal("Enable accepted a From address outside every granted domain") |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | // A pause must survive a re-walk. Task 13 adds the pause verb |
| 351 | // (Provisioned.Active = false); nothing writes it yet, but Enable must |
| 352 | // already carry an existing Active value forward rather than hardcoding |
| 353 | // true, or the pause — whose whole purpose is stopping one tenant's bounce |
| 354 | // rate from putting the shared SES account under review — would be undone |
| 355 | // silently the next time delivery needed repair. |
| 356 | func TestEnableCarriesAPausedActiveForwardThroughARewalk(t *testing.T) { |
| 357 | ctx, s, p, pub, now := enableFixture(t) |
| 358 | if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != nil { |
| 359 | t.Fatalf("first Enable: %v", err) |
| 360 | } |
| 361 | |
| 362 | // Simulate a pause directly on the record — the way the pause verb |
| 363 | // (Task 13) will, once it exists. |
| 364 | got, etag, err := Get(ctx, s, "bag", "harbourmail") |
| 365 | if err != nil { |
| 366 | t.Fatalf("Get: %v", err) |
| 367 | } |
| 368 | got.Provisioned.Active = false |
| 369 | got.Provisioned.PauseKind = PauseKindCap |
| 370 | if _, err := Put(ctx, s, "bag", "harbourmail", got, etag); err != nil { |
| 371 | t.Fatalf("Put: %v", err) |
| 372 | } |
| 373 | |
| 374 | // Remove a delivered var so the idempotence short-circuit cannot fire — |
| 375 | // this forces Enable to actually re-walk rather than return early. |
| 376 | if _, err := appconfig.Unset(ctx, s, "bag", "harbourmail", []string{"HARBOURMAIL_SMTP_HOST"}); err != nil { |
| 377 | t.Fatalf("Unset: %v", err) |
| 378 | } |
| 379 | |
| 380 | second, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 381 | if err != nil { |
| 382 | t.Fatalf("re-walk Enable: %v", err) |
| 383 | } |
| 384 | if second.Provisioned.Active { |
| 385 | t.Fatal("a re-walk silently resumed a paused credential") |
| 386 | } |
| 387 | // R1: PauseKind must survive the rewrite alongside Active — dropping it |
| 388 | // turns a CAP pause into a kind-less one, which nothing auto-resumes. |
| 389 | if second.Provisioned.PauseKind != PauseKindCap { |
| 390 | t.Fatalf("PauseKind = %q, want %q to survive the re-walk", second.Provisioned.PauseKind, PauseKindCap) |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | // If the record ever names a key that is not what was actually delivered |
| 395 | // — e.g. a failed final Put after a successful mint+deliver, or hand |
| 396 | // edited bucket data — EnvDelivered must catch it via the _USER value |
| 397 | // comparison, and Enable must re-walk rather than short-circuit on a |
| 398 | // disagreement that would otherwise be permanent. |
| 399 | func TestEnvDeliveredAndEnableCatchAStaleRecordedKeyID(t *testing.T) { |
| 400 | ctx, s, p, pub, now := enableFixture(t) |
| 401 | first, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 402 | if err != nil { |
| 403 | t.Fatalf("first Enable: %v", err) |
| 404 | } |
| 405 | deliveredKeyID := first.Provisioned.KeyID |
| 406 | |
| 407 | got, etag, err := Get(ctx, s, "bag", "harbourmail") |
| 408 | if err != nil { |
| 409 | t.Fatalf("Get: %v", err) |
| 410 | } |
| 411 | got.Provisioned.KeyID = "AKIASTALE00000000" |
| 412 | if _, err := Put(ctx, s, "bag", "harbourmail", got, etag); err != nil { |
| 413 | t.Fatalf("Put: %v", err) |
| 414 | } |
| 415 | |
| 416 | missing, err := EnvDelivered(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", "AKIASTALE00000000", "") |
| 417 | if err != nil { |
| 418 | t.Fatalf("EnvDelivered: %v", err) |
| 419 | } |
| 420 | if !containsString(missing, "HARBOURMAIL_SMTP_USER") { |
| 421 | t.Fatalf("missing = %v, want HARBOURMAIL_SMTP_USER reported", missing) |
| 422 | } |
| 423 | |
| 424 | second, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 425 | if err != nil { |
| 426 | t.Fatalf("second Enable: %v", err) |
| 427 | } |
| 428 | if second.Provisioned.KeyID == "AKIASTALE00000000" { |
| 429 | t.Fatal("Enable short-circuited on a stale recorded key id instead of re-walking") |
| 430 | } |
| 431 | if second.Provisioned.KeyID == deliveredKeyID { |
| 432 | t.Fatal("re-walk should mint a fresh key, not silently reuse the originally delivered one") |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | // C2 (review follow-up): the idempotence short-circuit used to return |
| 437 | // BEFORE EnsureUser — the only place the delivered credential's policy is |
| 438 | // written — so a second domain added and verified after the first enable |
| 439 | // never reached the policy on a plain re-`enable`. Spec §4 states the |
| 440 | // opposite explicitly: "attaching a custom domain adds an ARN and rewrites |
| 441 | // the policy without rotating a credential." |
| 442 | func TestEnableRewritesThePolicyWhenASecondDomainIsAdded(t *testing.T) { |
| 443 | ctx, s, p, pub, now := enableFixture(t) |
| 444 | first, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 445 | if err != nil { |
| 446 | t.Fatalf("first Enable: %v", err) |
| 447 | } |
| 448 | firstKeyID := first.Provisioned.KeyID |
| 449 | |
| 450 | second := "second.bag.oncarlos.com" |
| 451 | if _, _, err := EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(), |
| 452 | "bag", "harbourmail", second, now); err != nil { |
| 453 | t.Fatalf("EnsureDomain second: %v", err) |
| 454 | } |
| 455 | p.SetIdentityStatus("eu-west-1", second, Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) |
| 456 | if _, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", second, "eu-west-1", now); err != nil { |
| 457 | t.Fatalf("VerifyDomain second: %v", err) |
| 458 | } |
| 459 | |
| 460 | rewalked, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 461 | if err != nil { |
| 462 | t.Fatalf("re-enable: %v", err) |
| 463 | } |
| 464 | if rewalked.Provisioned.KeyID != firstKeyID { |
| 465 | t.Fatal("re-enable minted a fresh key — the short-circuit must still govern the expensive half") |
| 466 | } |
| 467 | policy := p.Users[UserName("bag", "harbourmail")].Policy |
| 468 | for _, want := range []string{ |
| 469 | "harbourmail.bag.oncarlos.com", second, "*@harbourmail.bag.oncarlos.com", "*@" + second, |
| 470 | } { |
| 471 | if !strings.Contains(policy, want) { |
| 472 | t.Fatalf("policy after re-enable missing %q:\n%s", want, policy) |
| 473 | } |
| 474 | } |
| 475 | } |
| 476 | |
| 477 | // The symmetric shrink case: a domain that WAS granted and verified stops |
| 478 | // being verified (e.g. SES reports its DKIM/MAIL FROM failed), narrowing |
| 479 | // grantedDomains on the NEXT Enable call. The surviving policy must not go |
| 480 | // on naming the departed ARN and From pattern — the same defect C2 fixes, |
| 481 | // approached from the other direction. |
| 482 | func TestEnableRewritesThePolicyWhenADomainStopsVerifying(t *testing.T) { |
| 483 | ctx, s, p, pub, now := enableFixture(t) |
| 484 | second := "second.bag.oncarlos.com" |
| 485 | if _, _, err := EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(), |
| 486 | "bag", "harbourmail", second, now); err != nil { |
| 487 | t.Fatalf("EnsureDomain second: %v", err) |
| 488 | } |
| 489 | p.SetIdentityStatus("eu-west-1", second, Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) |
| 490 | if _, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", second, "eu-west-1", now); err != nil { |
| 491 | t.Fatalf("VerifyDomain second: %v", err) |
| 492 | } |
| 493 | |
| 494 | first, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 495 | if err != nil { |
| 496 | t.Fatalf("first Enable: %v", err) |
| 497 | } |
| 498 | policy := p.Users[UserName("bag", "harbourmail")].Policy |
| 499 | if !strings.Contains(policy, second) { |
| 500 | t.Fatalf("policy before the regression missing %q:\n%s", second, policy) |
| 501 | } |
| 502 | |
| 503 | // second regresses: SES now reports it FAILED. |
| 504 | p.SetIdentityStatus("eu-west-1", second, Status{DKIM: "FAILED", MailFrom: "FAILED", Sending: "PENDING"}) |
| 505 | if _, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", second, "eu-west-1", now); err != nil { |
| 506 | t.Fatalf("VerifyDomain regression: %v", err) |
| 507 | } |
| 508 | |
| 509 | rewalked, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 510 | if err != nil { |
| 511 | t.Fatalf("re-enable after regression: %v", err) |
| 512 | } |
| 513 | if rewalked.Provisioned.KeyID != first.Provisioned.KeyID { |
| 514 | t.Fatal("re-enable minted a fresh key — the short-circuit must still govern the expensive half") |
| 515 | } |
| 516 | policy = p.Users[UserName("bag", "harbourmail")].Policy |
| 517 | if strings.Contains(policy, second) { |
| 518 | t.Fatalf("policy after the regression still names the departed domain %q:\n%s", second, policy) |
| 519 | } |
| 520 | if !strings.Contains(policy, "harbourmail.bag.oncarlos.com") { |
| 521 | t.Fatalf("policy after the regression lost the SURVIVING domain too:\n%s", policy) |
| 522 | } |
| 523 | } |
| 524 | |
| 525 | // I1 (review follow-up): the exact failure class the _USER staleness check |
| 526 | // was added to catch, applied to the OTHER unsealed variable it was never |
| 527 | // extended to. Declare freely overwrites Declared.From at any time (only |
| 528 | // the env prefix is locked once delivered); a re-declare followed by a |
| 529 | // re-enable must re-deliver the new From, not leave the stale one standing |
| 530 | // forever because nothing ever compared _FROM's VALUE. |
| 531 | func TestEnableRedeliversAChangedFromAddress(t *testing.T) { |
| 532 | ctx, s, p, pub, now := enableFixture(t) |
| 533 | first, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 534 | if err != nil { |
| 535 | t.Fatalf("first Enable: %v", err) |
| 536 | } |
| 537 | |
| 538 | newFrom := "admin@harbourmail.bag.oncarlos.com" |
| 539 | if _, err := Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", newFrom, "owner@example.test", now); err != nil { |
| 540 | t.Fatalf("re-declare: %v", err) |
| 541 | } |
| 542 | |
| 543 | missing, err := EnvDelivered(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", first.Provisioned.KeyID, newFrom) |
| 544 | if err != nil { |
| 545 | t.Fatalf("EnvDelivered: %v", err) |
| 546 | } |
| 547 | if !containsString(missing, "HARBOURMAIL_SMTP_FROM") { |
| 548 | t.Fatalf("missing = %v, want HARBOURMAIL_SMTP_FROM reported after a re-declare changed it", missing) |
| 549 | } |
| 550 | |
| 551 | second, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) |
| 552 | if err != nil { |
| 553 | t.Fatalf("re-enable: %v", err) |
| 554 | } |
| 555 | if second.Declared.From != newFrom { |
| 556 | t.Fatalf("record's Declared.From = %q, want %q", second.Declared.From, newFrom) |
| 557 | } |
| 558 | man, err := appconfig.Get(ctx, s, "bag", "harbourmail") |
| 559 | if err != nil { |
| 560 | t.Fatalf("appconfig.Get: %v", err) |
| 561 | } |
| 562 | fromVar, ok := man.Vars["HARBOURMAIL_SMTP_FROM"] |
| 563 | if !ok || fromVar.Value != newFrom { |
| 564 | t.Fatalf("delivered HARBOURMAIL_SMTP_FROM = %+v, want value %q", fromVar, newFrom) |
| 565 | } |
| 566 | } |
| 567 | |