package mail import ( "context" "errors" "sync" "testing" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/release" ) type fixGate struct { mu sync.Mutex held bool releases int } func (g *fixGate) AcquireClaimMailGate(ctx context.Context, _ ClaimMailOwner, _ ClaimMailScope) (context.Context, string, error) { g.mu.Lock() defer g.mu.Unlock() if g.held { return nil, "", ErrClaimMailPending } g.held = true return ctx, "gate", nil } func (g *fixGate) CheckClaimMailGate(context.Context) error { return nil } func (g *fixGate) ReleaseClaimMailGate(context.Context) error { g.mu.Lock() defer g.mu.Unlock() g.held = false g.releases++ return nil } type fixAdmitter struct { mu sync.Mutex settled [][]hostops.MailAttempt } type fixAdmission struct { ctx context.Context f *fixAdmitter } func (f *fixAdmitter) AdmitClaimMailApps(ctx context.Context, _ ClaimMailHold, _ []ClaimMailApp) (ClaimMailAdmission, error) { return fixAdmission{ctx: ctx, f: f}, nil } func (a fixAdmission) Context() context.Context { return a.ctx } func (a fixAdmission) Settle(_ context.Context, _ error, attempts []hostops.MailAttempt) error { a.f.mu.Lock() defer a.f.mu.Unlock() a.f.settled = append(a.f.settled, attempts) return nil } func (a fixAdmission) Release(context.Context) error { return nil } // Review P2-9: an earlier admission's effect under the same gate does not // make a later admission that failed before any effect look issued, so the // gate is released instead of wedged. func TestClaimMailAttemptsArePerAdmission(t *testing.T) { ctx := t.Context() gate, admitter := &fixGate{}, &fixAdmitter{} s, err := NewClaimMailStore(release.NewDirStore(t.TempDir()), gate, admitter, ClaimMailOwner{Console: "c", Enrollment: "e", Controller: "k"}) if err != nil { t.Fatal(err) } plan := claimMailPlan{Apps: []string{"demo"}} refused := errors.New("validation failed before any call") err = WithClaimMailResource(ctx, s, func(held context.Context) error { if err := claimMailAdmitted(held, s, "bab", plan, func(c context.Context) error { _, err := s.Put(c, "accounts/bab/email/state.json", []byte(`{}`), "") return err }); err != nil { return err } return claimMailAdmitted(held, s, "bab", plan, func(context.Context) error { return refused }) }) if !errors.Is(err, refused) { t.Fatalf("result: %v", err) } if len(admitter.settled) != 2 || len(admitter.settled[0]) != 1 || admitter.settled[0][0].Key != "accounts/bab/email/state.json" || len(admitter.settled[1]) != 0 { t.Fatalf("attempts leaked across admissions: %+v", admitter.settled) } if gate.held || gate.releases != 1 { t.Fatalf("gate wedged by an earlier admission's effect: held=%v releases=%d", gate.held, gate.releases) } } // A failure after this admission's own attempt still retains the gate. func TestClaimMailFailureAfterAttemptRetainsGate(t *testing.T) { ctx := t.Context() gate, admitter := &fixGate{}, &fixAdmitter{} s, err := NewClaimMailStore(release.NewDirStore(t.TempDir()), gate, admitter, ClaimMailOwner{Console: "c", Enrollment: "e", Controller: "k"}) if err != nil { t.Fatal(err) } boom := errors.New("ses timeout") err = WithClaimMailResource(ctx, s, func(held context.Context) error { return claimMailAdmitted(held, s, "bab", claimMailPlan{Apps: []string{"demo"}}, func(c context.Context) error { if _, err := s.Put(c, "accounts/bab/email/state.json", []byte(`{}`), ""); err != nil { return err } return boom }) }) if !errors.Is(err, boom) { t.Fatalf("result: %v", err) } if !gate.held || gate.releases != 0 { t.Fatal("gate released after an uncertain effect") } }