package mail import ( "context" "errors" "reflect" "sync" "testing" ) type fakeClaimAdmitter struct { mu sync.Mutex calls [][]ClaimMailApp holds []ClaimMailHold released int refuse error } func (a *fakeClaimAdmitter) AdmitClaimMailApps(ctx context.Context, hold ClaimMailHold, apps []ClaimMailApp) (ClaimMailAdmission, error) { a.mu.Lock() defer a.mu.Unlock() a.calls = append(a.calls, append([]ClaimMailApp(nil), apps...)) a.holds = append(a.holds, hold) if a.refuse != nil { return nil, a.refuse } return fixtureAdmission{ctx: ctx, released: &a.released, mu: &a.mu}, nil } func TestClaimMailRequiresProductionAdmitter(t *testing.T) { f := newClaimDomainFixture(t, "alpha") if _, err := NewClaimMailStore(f.log, f.mg, nil, claimTestOwner); !errors.Is(err, ErrClaimMailPending) { t.Fatalf("constructor accepted a nil admitter: %v", err) } f.marked = ClaimMailStore{} // zero value: no gate, no admitter rec, etag, err := GetDomain(f.ctx, f.base, claimTestAccount, claimTestDomain) if err != nil { t.Fatal(err) } before, tag := f.domainRaw() f.reset() if _, err = PutDomain(f.ctx, f.marked, claimTestAccount, rec, etag); !errors.Is(err, ErrClaimMailPending) { t.Fatalf("missing provider: %v", err) } f.requireUntouched(before, tag) if g := f.gate(); g.Revision != 0 { t.Fatalf("gate used without an admitter: %+v", &g) } } func TestClaimMailAdmitterReceivesSortedDedupedUnionOnce(t *testing.T) { f := newClaimDomainFixture(t, "alpha", "beta") a := &fakeClaimAdmitter{} f.useAdmitter(a) rec, etag, err := GetDomain(f.ctx, f.base, claimTestAccount, claimTestDomain) if err != nil { t.Fatal(err) } rec.Apps = []string{"gamma", "beta", "alpha", "beta"} // reordered, duplicated, one app added if _, err = PutDomain(f.ctx, f.marked, claimTestAccount, rec, etag); err != nil { t.Fatalf("PutDomain: %v", err) } want := []ClaimMailApp{{claimTestAccount, "alpha"}, {claimTestAccount, "beta"}, {claimTestAccount, "gamma"}} if len(a.calls) != 1 || !reflect.DeepEqual(a.calls[0], want) { t.Fatalf("admitter calls=%v want one call %v", a.calls, want) } if a.holds[0].Gate == "" || a.holds[0].Owner != claimTestOwner { t.Fatalf("hold=%+v", a.holds[0]) } if a.released != 1 { t.Fatalf("released=%d", a.released) } if n := f.log.admissionGets(); n != 0 { t.Fatalf("fallback read admissions %d times although a hook is installed", n) } f.requireGateFree() } func TestClaimMailAdmitterRefusalLeavesNothingBehind(t *testing.T) { f := newClaimDomainFixture(t, "alpha", "beta") a := &fakeClaimAdmitter{refuse: ErrClaimMailPending} f.useAdmitter(a) rec, etag, err := GetDomain(f.ctx, f.base, claimTestAccount, claimTestDomain) if err != nil { t.Fatal(err) } beforeRaw, beforeEtag := f.domainRaw() f.reset() rec.Apps = []string{"alpha"} if _, err = PutDomain(f.ctx, f.marked, claimTestAccount, rec, etag); !errors.Is(err, ErrClaimMailPending) { t.Fatalf("PutDomain: %v", err) } if a.released != 0 { t.Fatalf("released=%d after a refused admission", a.released) } f.requireUntouched(beforeRaw, beforeEtag) f.requireGateFree() } // A rebinding VerifyDomain runs a nested PutDomain and a nested RefreshPolicies. Neither // may call the hook or read admissions: the outer call preflighted the whole set. func TestClaimMailVerifyDomainNestedCallsReuseThePreflightedSet(t *testing.T) { f := newClaimDomainFixture(t, "alpha", "beta") a := &fakeClaimAdmitter{} f.useAdmitter(a) if err := f.mem.SetIdentityConfigSet(f.ctx, "eu-west-1", claimTestDomain, ""); err != nil { t.Fatal(err) } f.reset() rec, err := VerifyDomain(f.ctx, f.marked, f.prov, testDeployment(), claimTestAccount, claimTestDomain, "eu-west-1", f.now) if err != nil { t.Fatalf("VerifyDomain: %v", err) } if f.prov.hits["SetIdentityConfigSet"] != 1 || f.log.putsTo(DomainKey(claimTestAccount, claimTestDomain)) != 1 { t.Fatalf("rebind path not exercised: %v", f.prov.hits) } if rec.Regions["eu-west-1"].Status != StatusVerified { t.Fatalf("status=%q", rec.Regions["eu-west-1"].Status) } if len(a.calls) != 1 || a.released != 1 { t.Fatalf("admitter calls=%d released=%d, want 1 and 1", len(a.calls), a.released) } if n := f.log.admissionGets(); n != 0 { t.Fatalf("nested calls read admissions %d times", n) } f.requireGateFree() } func TestClaimMailNestedCallNeverWidensOrRereadsAdmission(t *testing.T) { outer := func(f *claimDomainFixture, inner func(ctx context.Context) error) error { return WithClaimMailResource(f.ctx, f.marked, func(held context.Context) error { return claimMailAdmitted(held, f.marked, claimTestAccount, claimMailPlan{Apps: []string{"alpha"}}, inner) }) } t.Run("covered nested call ignores a purge that starts after preflight", func(t *testing.T) { f := newClaimDomainFixture(t, "alpha") rec, etag, err := GetDomain(f.ctx, f.base, claimTestAccount, claimTestDomain) if err != nil { t.Fatal(err) } err = outer(f, func(inner context.Context) error { f.setAdmission("beta", "purging") f.log.reset() rec.Apps = []string{"alpha"} if _, err := PutDomain(inner, f.marked, claimTestAccount, rec, etag); err != nil { return err } if n := f.log.admissionGets(); n != 0 { t.Errorf("nested call read admissions %d times", n) } return nil }) if err != nil { t.Fatalf("outer: %v", err) } f.requireGateFree() }) t.Run("widening nested call fails closed without reading admissions", func(t *testing.T) { f := newClaimDomainFixture(t, "alpha") rec, etag, err := GetDomain(f.ctx, f.base, claimTestAccount, claimTestDomain) if err != nil { t.Fatal(err) } err = outer(f, func(inner context.Context) error { rec.Apps = []string{"alpha"} newEtag, err := PutDomain(inner, f.marked, claimTestAccount, rec, etag) if err != nil { return err } // first effect, marks the gate dirty _, widenedEtag := f.domainRaw() if widenedEtag != newEtag { t.Errorf("etag mismatch %s %s", widenedEtag, newEtag) } f.log.reset() rec.Apps = []string{"alpha", "beta"} _, err = PutDomain(inner, f.marked, claimTestAccount, rec, newEtag) if !errors.Is(err, ErrClaimMailPending) { t.Errorf("widening PutDomain: %v", err) } if n := f.log.admissionGets(); n != 0 { t.Errorf("widening call read admissions %d times", n) } if n := f.log.putsTo(DomainKey(claimTestAccount, claimTestDomain)); n != 0 { t.Errorf("widening call wrote %d times", n) } return err }) if !errors.Is(err, ErrClaimMailPending) { t.Fatalf("outer: %v", err) } // After an effect the gate is retained for reconciliation. Real wrappers never reach // this state because their plans union domain membership before the first effect. if g := f.gate(); g.State != "held" { t.Fatalf("gate=%+v, want held", &g) } }) }