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 "sync"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/hostops"
10 "github.com/carlosframework/platform/internal/release"
11)
12
13type fixGate struct {
14 mu sync.Mutex
15 held bool
16 releases int
17}
18
19func (g *fixGate) AcquireClaimMailGate(ctx context.Context, _ ClaimMailOwner, _ ClaimMailScope) (context.Context, string, error) {
20 g.mu.Lock()
21 defer g.mu.Unlock()
22 if g.held {
23 return nil, "", ErrClaimMailPending
24 }
25 g.held = true
26 return ctx, "gate", nil
27}
28func (g *fixGate) CheckClaimMailGate(context.Context) error { return nil }
29func (g *fixGate) ReleaseClaimMailGate(context.Context) error {
30 g.mu.Lock()
31 defer g.mu.Unlock()
32 g.held = false
33 g.releases++
34 return nil
35}
36
37type fixAdmitter struct {
38 mu sync.Mutex
39 settled [][]hostops.MailAttempt
40}
41
42type fixAdmission struct {
43 ctx context.Context
44 f *fixAdmitter
45}
46
47func (f *fixAdmitter) AdmitClaimMailApps(ctx context.Context, _ ClaimMailHold, _ []ClaimMailApp) (ClaimMailAdmission, error) {
48 return fixAdmission{ctx: ctx, f: f}, nil
49}
50func (a fixAdmission) Context() context.Context { return a.ctx }
51func (a fixAdmission) Settle(_ context.Context, _ error, attempts []hostops.MailAttempt) error {
52 a.f.mu.Lock()
53 defer a.f.mu.Unlock()
54 a.f.settled = append(a.f.settled, attempts)
55 return nil
56}
57func (a fixAdmission) Release(context.Context) error { return nil }
58
59// Review P2-9: an earlier admission's effect under the same gate does not
60// make a later admission that failed before any effect look issued, so the
61// gate is released instead of wedged.
62func TestClaimMailAttemptsArePerAdmission(t *testing.T) {
63 ctx := t.Context()
64 gate, admitter := &fixGate{}, &fixAdmitter{}
65 s, err := NewClaimMailStore(release.NewDirStore(t.TempDir()), gate, admitter, ClaimMailOwner{Console: "c", Enrollment: "e", Controller: "k"})
66 if err != nil {
67 t.Fatal(err)
68 }
69 plan := claimMailPlan{Apps: []string{"demo"}}
70 refused := errors.New("validation failed before any call")
71 err = WithClaimMailResource(ctx, s, func(held context.Context) error {
72 if err := claimMailAdmitted(held, s, "bab", plan, func(c context.Context) error {
73 _, err := s.Put(c, "accounts/bab/email/state.json", []byte(`{}`), "")
74 return err
75 }); err != nil {
76 return err
77 }
78 return claimMailAdmitted(held, s, "bab", plan, func(context.Context) error { return refused })
79 })
80 if !errors.Is(err, refused) {
81 t.Fatalf("result: %v", err)
82 }
83 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 {
84 t.Fatalf("attempts leaked across admissions: %+v", admitter.settled)
85 }
86 if gate.held || gate.releases != 1 {
87 t.Fatalf("gate wedged by an earlier admission's effect: held=%v releases=%d", gate.held, gate.releases)
88 }
89}
90
91// A failure after this admission's own attempt still retains the gate.
92func TestClaimMailFailureAfterAttemptRetainsGate(t *testing.T) {
93 ctx := t.Context()
94 gate, admitter := &fixGate{}, &fixAdmitter{}
95 s, err := NewClaimMailStore(release.NewDirStore(t.TempDir()), gate, admitter, ClaimMailOwner{Console: "c", Enrollment: "e", Controller: "k"})
96 if err != nil {
97 t.Fatal(err)
98 }
99 boom := errors.New("ses timeout")
100 err = WithClaimMailResource(ctx, s, func(held context.Context) error {
101 return claimMailAdmitted(held, s, "bab", claimMailPlan{Apps: []string{"demo"}}, func(c context.Context) error {
102 if _, err := s.Put(c, "accounts/bab/email/state.json", []byte(`{}`), ""); err != nil {
103 return err
104 }
105 return boom
106 })
107 })
108 if !errors.Is(err, boom) {
109 t.Fatalf("result: %v", err)
110 }
111 if !gate.held || gate.releases != 0 {
112 t.Fatal("gate released after an uncertain effect")
113 }
114}
115