carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/hostops"
12 "github.com/carlosframework/platform/internal/mail"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16func TestClaimFlagOffRevokeIgnoresCancelledContext(t *testing.T) {
17 for _, byValue := range []bool{false, true} {
18 t.Run(map[bool]string{false: "id", true: "value"}[byValue], func(t *testing.T) {
19 st := openTestStore(t)
20 raw := mintUserToken(t, st, "owner@example.com", time.Now())
21 ctx, cancel := context.WithCancel(t.Context())
22 cancel()
23 var err error
24 if byValue {
25 err = st.RevokeCLITokenValueContext(ctx, raw)
26 } else {
27 err = st.RevokeCLITokenContext(ctx, "owner@example.com", hashToken(raw))
28 }
29 if err != nil {
30 t.Fatalf("flag-off revoke: %v", err)
31 }
32 rows, err := st.CLITokens("owner@example.com")
33 if err != nil || len(rows) != 0 {
34 t.Fatalf("token still live: %v %v", rows, err)
35 }
36 if byValue {
37 err = st.RevokeCLITokenValueContext(ctx, raw)
38 if err != nil {
39 t.Fatal(err)
40 }
41 } else {
42 err = st.RevokeCLITokenContext(ctx, "owner@example.com", hashToken(raw))
43 if !errors.Is(err, errNoSuchCLIToken) {
44 t.Fatalf("missing token: %v", err)
45 }
46 }
47 })
48 }
49}
50
51func TestClaimServiceCredentialLookupError(t *testing.T) {
52 for _, gated := range []bool{false, true} {
53 t.Run(map[bool]string{false: "off", true: "on"}[gated], func(t *testing.T) {
54 st := openTestStore(t)
55 if gated {
56 st = newClaimCase(t).st
57 }
58 st.Close()
59 _, want := st.ServicePrincipals(OpsAccountID)
60 if want == nil {
61 t.Fatal("closed DB lookup succeeded")
62 }
63 called := false
64 err := (&Server{store: st}).serviceCredentialWrite(t.Context(), &Account{ID: OpsAccountID}, serviceHandle{name: "test"}, "test", func(*Store) error { called = true; return nil })
65 if gated {
66 if called || err == nil || err.Error() != want.Error() {
67 t.Fatalf("lookup error swallowed: called=%v err=%v want=%v", called, err, want)
68 }
69 } else if !called || err != nil {
70 t.Fatalf("flag-off changed: called=%v err=%v", called, err)
71 }
72 })
73 }
74}
75
76type ambiguousDisassociate struct {
77 mail.Provisioner
78 err error
79 disassociations, deletes int
80}
81
82func (p *ambiguousDisassociate) DisassociateTenantResource(ctx context.Context, tenant, resource string) error {
83 p.disassociations++
84 if err := p.Provisioner.DisassociateTenantResource(ctx, tenant, resource); err != nil {
85 return err
86 }
87 return p.err
88}
89func (p *ambiguousDisassociate) DeleteIdentity(ctx context.Context, region, domain string) error {
90 p.deletes++
91 return p.Provisioner.DeleteIdentity(ctx, region, domain)
92}
93
94func TestClaimMailDiscardedBackendErrorRetainsBothLocks(t *testing.T) {
95 cx := newClaimCase(t)
96 ctx := t.Context()
97 const domain = "remove.example.com"
98 d := cx.srv.cfg.MailDeployment
99 if _, _, err := mail.EnsureDomain(ctx, cx.srv.claimMailStore(), cx.srv.cfg.MailProvisioner, cx.srv.cfg.DNSWriter, d, "bab", "titogo", domain, time.Now()); err != nil {
100 t.Fatal(err)
101 }
102 uncertain := errors.New("lost disassociation response")
103 p := &ambiguousDisassociate{Provisioner: cx.srv.cfg.MailProvisioner, err: uncertain}
104 _, err := mail.RemoveDomain(ctx, cx.srv.claimMailStore(), p, cx.srv.cfg.DNSWriter, d, "bab", "titogo", domain)
105 if p.disassociations == 0 || p.deletes == 0 {
106 t.Fatalf("did not reach failing and successful backend calls: %+v err=%v", p, err)
107 }
108 if rec, _, e := mail.GetDomain(ctx, cx.rel, "bab", domain); e != nil || rec != nil {
109 t.Fatalf("later removal did not succeed: %v %v", rec, e)
110 }
111 a, e := hostops.ReadCanonicalAdmission(ctx, cx.rel, "bab", "titogo")
112 if e != nil {
113 t.Fatal(e)
114 }
115 if a.Value.Op == nil {
116 t.Error("app admission released after ambiguous disassociation")
117 }
118 held := false
119 for _, effect := range a.Value.Effects {
120 if effect.State == "uncertain" {
121 held = true
122 }
123 }
124 if !held {
125 t.Errorf("no uncertain effect: %+v", a.Value.Effects)
126 }
127 raw, _, e := cx.rel.Get(ctx, hostops.MailGateKey)
128 if e != nil {
129 t.Fatal(e)
130 }
131 var gate struct {
132 State string `json:"state"`
133 }
134 if e = json.Unmarshal(raw, &gate); e != nil {
135 t.Fatal(e)
136 }
137 if gate.State != "held" {
138 t.Errorf("mail gate released: %s", raw)
139 }
140}
141
142func TestClaimPurgeMailRecordRefusesBeforeEffects(t *testing.T) {
143 for _, tc := range []struct{ name, key, body string }{
144 {"sender schema", "email/senders/app.json", `{"schema":2,"id":"app","scope":"app","source":"domain","provisioned":{"iam_user":"carlos-mail-bab-titogo"}}`},
145 {"sender identity", "email/senders/app.json", `{"schema":1,"id":"other","scope":"app","source":"domain","provisioned":{"iam_user":"carlos-mail-bab-titogo"}}`},
146 {"legacy schema", "email.json", `{"schema":2,"app":"titogo","provisioned":{"iam_user":"carlos-mail-bab-titogo"}}`},
147 {"legacy identity", "email.json", `{"schema":1,"app":"other","provisioned":{"iam_user":"carlos-mail-bab-titogo"}}`},
148 } {
149 for _, stage := range []string{"readiness", "request", "teardown", "mail teardown"} {
150 t.Run(tc.name+"/"+stage, func(t *testing.T) {
151 cx := newClaimCase(t)
152 cx.purgeWorld(t)
153 p := cx.srv.cfg.ClaimPurge
154 ctx := t.Context()
155 const id = "strict-mail-purge"
156 appID := cx.appIDs[claimAppRef{Account: "bab", App: "titogo"}]
157 var h *hostops.OperationHandle
158 if stage == "teardown" {
159 got, err := p.Request(ctx, "bab", "titogo", id, "owner", claimFxOwner)
160 if got != id || err != nil && !errors.Is(err, hostops.ErrPending) {
161 t.Fatalf("request: %s %v", got, err)
162 }
163 var err2 error
164 h, err2 = p.Authority.Control.ResumeExclusive(ctx, "bab", "titogo", appID, id, hostops.OpPurging, p.Owner)
165 if err2 != nil {
166 t.Fatal(err2)
167 }
168 j, _, err := hostops.ReadPurgeJournal(ctx, cx.rel, "bab", "titogo", id, appID)
169 if err != nil {
170 t.Fatal(err)
171 }
172 j.Phase = "claimed" // Stage fixture: host barriers are tested separately.
173 key, _ := hostops.PurgeKey("bab", "titogo")
174 putMailJSON(t, cx.rel, key, j)
175 }
176 key := release.AppPrefix("bab", "titogo") + tc.key
177 if _, err := cx.rel.Put(ctx, key, []byte(tc.body), ""); err != nil {
178 t.Fatal(err)
179 }
180 legacy, _, err := mail.Get(ctx, cx.rel, "bab", "titogo")
181 if err != nil || legacy == nil || legacy.Schema != mail.Schema || legacy.App != "titogo" {
182 t.Fatalf("flag-off conversion changed: %+v %v", legacy, err)
183 }
184 mark := cx.rec.mark()
185 if stage == "mail teardown" {
186 err = mail.Teardown(ctx, cx.srv.claimMailStore(), cx.srv.cfg.MailProvisioner, cx.srv.cfg.DNSWriter, cx.srv.cfg.MailDeployment, "bab", "titogo")
187 } else if stage == "readiness" {
188 err = p.mailReady(ctx, "bab", "titogo")
189 } else if stage == "request" {
190 _, err = p.Request(ctx, "bab", "titogo", id, "owner", claimFxOwner)
191 journal, _ := hostops.PurgeKey("bab", "titogo")
192 if _, _, e := cx.rel.Get(ctx, journal); !errors.Is(e, os.ErrNotExist) {
193 t.Errorf("invalid inventory reserved purge: %v", e)
194 }
195 } else {
196 err = p.stage(ctx, h, "teardown", func(ctx context.Context) error {
197 scope := mail.ClaimMailScope{Account: "bab", App: "titogo", AppID: appID, Operation: id}
198 return mail.WithClaimMailResource(mail.WithClaimMailScope(ctx, scope), cx.srv.claimMailStore(), func(held context.Context) error {
199 return p.teardown(held, h, "bab", "titogo", id, appID)
200 })
201 })
202 }
203 if err == nil || errors.Is(err, hostops.ErrPending) {
204 t.Errorf("invalid inventory was not refused: %v", err)
205 }
206 for _, e := range cx.rec.since(mark) {
207 if !e.Control && (e.Unscoped || len(e.Scopes) > 0) {
208 t.Errorf("effect before validation: %v", e)
209 }
210 }
211 if _, _, e := cx.rel.Get(ctx, key); e != nil {
212 t.Errorf("invalid estate record removed: %v", e)
213 }
214 })
215 }
216 }
217}
218