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 "fmt"
8 "net/http"
9 "regexp"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/provision"
15 "github.com/carlosframework/platform/internal/release"
16)
17
18type fillRec struct {
19 provision.Record
20 RegionFrom string `json:"region_from"`
21 RegionName string `json:"region_name"`
22}
23
24// fillFixture: titogo with an unrestricted shared-pool manifest, a stored
25// app region, a named home, the deployment's secrets key, and two
26// eligible boxes. fx.st.db is the Store's own *sql.DB (same package).
27func fillFixture(t *testing.T, appRegion string) (*consoleFixture, string, string) {
28 t.Helper()
29 fx, token := instanceFixture(t, "code-fill-"+appRegion)
30 fx.srv.cfg.HomeRegion, fx.srv.cfg.SecretsPub = "ie", testKeyPub
31 m := titoManifest()
32 m.Pool, m.Regions = "", nil
33 enableInstances(t, fx, "titogo", m)
34 if _, err := fx.st.db.Exec(`UPDATE apps SET region = ?, region_source = 'chosen' WHERE app = 'titogo'`, appRegion); err != nil {
35 t.Fatal(err)
36 }
37 setEdges(t, fx, map[string]string{"ie-box": homeBox, "uk-box": ukEligible})
38 return fx, token, instancesURL(fx, "bab", "titogo")
39}
40
41func create(t *testing.T, u, token, body string) (int, fillRec, string) {
42 t.Helper()
43 status, _, out := rawReq(t, http.MethodPost, u, token, body)
44 var rec fillRec
45 json.Unmarshal([]byte(out), &rec)
46 return status, rec, out
47}
48
49func mustCreate(t *testing.T, u, token, body string) fillRec {
50 t.Helper()
51 status, rec, out := create(t, u, token, body)
52 if status != http.StatusCreated {
53 t.Fatalf("create %s = %d %s, want 201", body, status, out)
54 }
55 return rec
56}
57
58func TestFillFromTheAppRegion(t *testing.T) {
59 fx, token, u := fillFixture(t, "uk")
60 letStateUpstreams(t, fx, "alice@keymail.dev") // one case below is addr-backed
61 if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`); rec.Region != "uk" || rec.RegionFrom != "app" || rec.RegionName != ukPlace {
62 t.Fatalf("fill = %+v", rec)
63 }
64 if rec := mustCreate(t, u, token, `{"host":"b.go.tito.io","region":"ie"}`); rec.Region != "" || rec.RegionFrom != "request" {
65 t.Fatalf("explicit home = %+v", rec)
66 }
67 for _, body := range []string{
68 `{"host":"c.go.tito.io","kind":"static"}`,
69 `{"host":"d.go.tito.io","kind":"service","addrs":["10.0.0.4:8080"]}`,
70 } {
71 if rec := mustCreate(t, u, token, body); rec.Region != "" {
72 t.Errorf("%s was filled: %+v", body, rec)
73 }
74 }
75 status, _, out := create(t, u, token, `{"host":"e.go.tito.io","role":"replica"}`)
76 if status != http.StatusBadRequest || !strings.Contains(out, "role replica needs a placement") {
77 t.Errorf("replica without a region = %d %s, want the existing refusal", status, out)
78 }
79}
80
81func TestFillWithAHomeAppRegionChangesNothing(t *testing.T) {
82 _, token, u := fillFixture(t, "")
83 if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`); rec.Region != "" || rec.RegionFrom != "" {
84 t.Fatalf("home app = %+v", rec)
85 }
86}
87
88// Precedence with a NON-HOME app default in play: pool and fleet win.
89func TestFillPrecedence(t *testing.T) {
90 fx, token, u := fillFixture(t, "uk")
91 m := titoManifest()
92 m.Pool, m.Regions = "tito", nil
93 enableInstances(t, fx, "titogo", m)
94 if rec := mustCreate(t, u, token, `{"host":"p.go.tito.io"}`); rec.Region != "" || rec.RegionFrom != "" {
95 t.Fatalf("pooled = %+v, want no fill", rec)
96 }
97 m.Pool = ""
98 enableInstances(t, fx, "titogo", m)
99 if err := fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme"); err != nil {
100 t.Fatal(err)
101 }
102 if rec := mustCreate(t, u, token, `{"host":"f.go.tito.io"}`); rec.Region != "fleet/acme" || rec.RegionFrom != "fleet" {
103 t.Fatalf("fleet = %+v", rec)
104 }
105}
106
107func TestFillStoreErrorIsC20(t *testing.T) {
108 fx, token, u := fillFixture(t, "uk")
109 fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") }
110 status, _, out := create(t, u, token, `{"host":"a.go.tito.io"}`)
111 if status != http.StatusBadGateway || !strings.Contains(out, msgAppRegionUnreadable) {
112 t.Fatalf("store error = %d %s, want 502 C20", status, out)
113 }
114}
115
116// codex R2-2: dormancy keeps explicit requests, never app-derived ones.
117// Stage B Task 10 review M5: with no edges file at all but boxes still
118// reporting fresh, the evidence is incomplete (ruling (A)), so the
119// app-derived request is "could not confirm" (C28), not "absent"; once no
120// box reports either, it is C16 absent.
121func TestAppDerivedNeedsPositiveEligibility(t *testing.T) {
122 fx, token, u := fillFixture(t, "uk")
123 for _, k := range []string{"control/edges/uk-box.json", "control/edges/ie-box.json"} {
124 if err := fx.rel.Delete(t.Context(), k); err != nil {
125 t.Fatal(err)
126 }
127 }
128 mustCreate(t, u, token, `{"host":"x.go.tito.io","region":"uk"}`)
129 status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
130 if status != http.StatusServiceUnavailable || !strings.Contains(out, msgAppRegionUnconfirmed("uk")) {
131 t.Fatalf("app-derived uk with fresh boxes and no edges files = %d %s, want C28 unconfirmed", status, out)
132 }
133 keys, err := fx.rel.List(t.Context(), "status/boxes/")
134 if err != nil {
135 t.Fatal(err)
136 }
137 for _, k := range keys {
138 if err := fx.rel.Delete(t.Context(), k); err != nil {
139 t.Fatal(err)
140 }
141 }
142 status, _, out = create(t, u, token, `{"host":"z.go.tito.io"}`)
143 if status != http.StatusBadRequest || !strings.Contains(out, reasonText("absent")) {
144 t.Fatalf("app-derived uk under dormancy = %d %s, want C16 absent", status, out)
145 }
146}
147
148// F1: the box is retired but its edges object remains, as it does in
149// production (nothing deletes it). Its status snapshot has gone stale, and
150// that alone must take uk out of automatic placement: C16 absent, the same
151// refusal as the deleted-object case above.
152func TestAppDerivedNeedsALiveBox(t *testing.T) {
153 fx, token, u := fillFixture(t, "uk")
154 setBoxSnapshot(t, fx.rel, "uk-box", time.Now().Add(-time.Hour))
155 status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
156 if status != http.StatusBadRequest || !strings.Contains(out, reasonText("absent")) {
157 t.Fatalf("app-derived uk with a stale box snapshot = %d %s, want C16 absent", status, out)
158 }
159}
160
161// Partial reads (re-review r1): with a keyless sibling listed beside the
162// keyed uk box, a failed edges GET or a failed status GET on that sibling
163// is incomplete evidence. An app-derived uk create is refused C28 (could
164// not confirm, try again or pass a region), never created through the box
165// that was read. An explicit --region uk create and a home app's create
166// keep their behaviour under both failures: the explicit gates read the
167// files alone and the home is exempt.
168func TestAppDerivedNeedsCompleteEvidence(t *testing.T) {
169 failures := map[string]string{
170 "edges GET": "control/edges/uk-box-2.json",
171 "status GET": "status/boxes/uk-box-2.json",
172 }
173 for name, key := range failures {
174 fx, token, u := fillFixture(t, "uk")
175 setEdges(t, fx, map[string]string{"uk-box-2": ukKeylessSibling})
176 fx.srv.cfg.Releases = failingGet{Store: fx.rel, key: key}
177 status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
178 if status != http.StatusServiceUnavailable || !strings.Contains(out, msgAppRegionUnconfirmed("uk")) {
179 t.Errorf("%s failure, app-derived uk = %d %s, want 503 C28", name, status, out)
180 }
181 if rec := mustCreate(t, u, token, `{"host":"x.go.tito.io","region":"uk"}`); rec.Region != "uk" {
182 t.Errorf("%s failure, explicit uk = %+v, want created in uk as before", name, rec)
183 }
184 }
185 for name, key := range failures {
186 fx, token, u := fillFixture(t, "")
187 setEdges(t, fx, map[string]string{"uk-box-2": ukKeylessSibling})
188 fx.srv.cfg.Releases = failingGet{Store: fx.rel, key: key}
189 if rec := mustCreate(t, u, token, `{"host":"h.go.tito.io"}`); rec.Region != "" {
190 t.Errorf("%s failure, home app = %+v, want created at the home", name, rec)
191 }
192 }
193}
194
195// Unmatched boxes, ruling (A): a fresh status snapshot for a box with no
196// edges file is incomplete evidence at the door too, so an app-derived uk
197// create is C28, while an explicit --region uk create and a home app's
198// create keep their behaviour.
199func TestAppDerivedNeedsEveryFreshBoxToPublishEdges(t *testing.T) {
200 fx, token, u := fillFixture(t, "uk")
201 setBoxSnapshot(t, fx.rel, "uk-box-2", time.Now())
202 status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
203 if status != http.StatusServiceUnavailable || !strings.Contains(out, msgAppRegionUnconfirmed("uk")) {
204 t.Errorf("fresh status-only box, app-derived uk = %d %s, want 503 C28", status, out)
205 }
206 if rec := mustCreate(t, u, token, `{"host":"x.go.tito.io","region":"uk"}`); rec.Region != "uk" {
207 t.Errorf("explicit uk = %+v, want created in uk as before", rec)
208 }
209 fx, token, u = fillFixture(t, "")
210 setBoxSnapshot(t, fx.rel, "uk-box-2", time.Now())
211 if rec := mustCreate(t, u, token, `{"host":"h.go.tito.io"}`); rec.Region != "" {
212 t.Errorf("home app = %+v, want created at the home", rec)
213 }
214}
215
216func TestAppDerivedEdgesReadFailureIs503(t *testing.T) {
217 fx, token, u := fillFixture(t, "uk")
218 fx.srv.cfg.Releases = failingList{fx.rel}
219 status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
220 if status != http.StatusServiceUnavailable || !strings.Contains(out, msgAppRegionUnconfirmed("uk")) {
221 t.Fatalf("edges failure = %d %s, want 503 C28", status, out)
222 }
223}
224
225func TestAppDerivedRefusals(t *testing.T) {
226 fx, token, u := fillFixture(t, "uk")
227 m := titoManifest()
228 m.Pool, m.Regions = "", []string{"au"}
229 enableInstances(t, fx, "titogo", m)
230 // au publishes no box, so nothing the allow-list names can pass: C17b.
231 if status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`); status != http.StatusBadRequest ||
232 !strings.Contains(out, msgAppRegionNotAllowed("uk", nil)) {
233 t.Fatalf("allow-list = %d %s, want C17b", status, out)
234 }
235 m.Regions = nil
236 enableInstances(t, fx, "titogo", m)
237 if status, _, out := create(t, u, token, `{"host":"z.go.tito.io","replicas":["uk"]}`); status != http.StatusBadRequest ||
238 !strings.Contains(out, msgReplicaOverlap("uk")) {
239 t.Fatalf("replica overlap = %d %s, want C26", status, out)
240 }
241 setEdges(t, fx, map[string]string{"uk-box": ukNoKey})
242 if status, _, out := create(t, u, token, `{"host":"k.go.tito.io"}`); status != http.StatusBadRequest ||
243 !strings.Contains(out, reasonText("no-secrets-key")) {
244 t.Fatalf("app-derived keyless uk = %d %s, want C16 no-secrets-key", status, out)
245 }
246}
247
248// R9's explicit half: refused only when a current writer PROVES the
249// region keyless, and only for what needs config on the box.
250func TestExplicitCreateIntoAKeylessRegion(t *testing.T) {
251 fx, token, u := fillFixture(t, "")
252 letStateUpstreams(t, fx, "alice@keymail.dev") // one case below is addr-backed
253 setEdges(t, fx, map[string]string{"uk-box": ukNoKey})
254 if status, _, out := create(t, u, token, `{"host":"a.go.tito.io","region":"uk"}`); status != http.StatusBadRequest || !strings.Contains(out, msgNoSecretsKey("uk")) {
255 t.Fatalf("keyless instance = %d %s, want C31", status, out)
256 }
257 if status, _, out := create(t, u, token, `{"host":"r.go.tito.io","region":"uk","role":"replica"}`); status != http.StatusBadRequest || !strings.Contains(out, msgNoSecretsKey("uk")) {
258 t.Fatalf("keyless replica = %d %s, want C31", status, out)
259 }
260 mustCreate(t, u, token, `{"host":"s.go.tito.io","region":"uk","kind":"static"}`)
261 // An addr-backed service proxies to a process the platform never starts,
262 // so it needs no config on the box either (codex plan-R2 7).
263 mustCreate(t, u, token, `{"host":"v.go.tito.io","region":"uk","kind":"service","addrs":["10.0.0.4:8080"]}`)
264 setEdges(t, fx, map[string]string{"uk-box": ukOldWriter})
265 mustCreate(t, u, token, `{"host":"o.go.tito.io","region":"uk"}`) // unknown keeps today's behaviour
266 setEdges(t, fx, map[string]string{"uk-box": ukNoKey})
267 fx.srv.cfg.SecretsPub = ""
268 mustCreate(t, u, token, `{"host":"n.go.tito.io","region":"uk"}`) // the console cannot judge
269}
270
271// edgesListFails fails only the listing of control/edges/, so the rest of the
272// create door (records, leftovers) runs as it does in production.
273type edgesListFails struct{ release.Store }
274
275func (e edgesListFails) List(ctx context.Context, prefix string) ([]string, error) {
276 if strings.HasPrefix(prefix, edgesFilePrefix) {
277 return nil, errors.New("bucket unreachable")
278 }
279 return e.Store.List(ctx, prefix)
280}
281
282// PF10: an explicit request keeps today's behaviour when the edges cannot be
283// read. R9 refuses only on proof, so unknown data never refuses it.
284func TestExplicitCreateWithTheEdgesListingDownIsNotRefused(t *testing.T) {
285 fx, token, u := fillFixture(t, "")
286 fx.srv.cfg.Releases = edgesListFails{fx.rel}
287 if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io","region":"uk"}`); rec.Region != "uk" || rec.RegionFrom != "request" {
288 t.Fatalf("explicit uk with the edges down = %+v", rec)
289 }
290 if rec := mustCreate(t, u, token, `{"host":"r.go.tito.io","region":"uk","role":"replica"}`); rec.Role != "replica" || rec.Region != "uk" {
291 t.Fatalf("explicit replica with the edges down = %+v", rec)
292 }
293}
294
295func TestFillRunsWithTheSwitchOff(t *testing.T) {
296 fx, token, u := fillFixture(t, "uk")
297 fx.srv.cfg.AppRegions = false
298 if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`); rec.Region != "uk" {
299 t.Fatalf("switch off = %+v, want the stored uk", rec)
300 }
301}
302
303// codex plan-R3 6: a successful replica create names its place too, so the
304// CLI prints C14 for it as for any other placement.
305func TestReplicaCreateNamesItsPlace(t *testing.T) {
306 _, token, u := fillFixture(t, "")
307 rec := mustCreate(t, u, token, `{"host":"r.go.tito.io","region":"uk","role":"replica"}`)
308 if rec.Role != "replica" || rec.Region != "uk" || rec.RegionName != ukPlace {
309 t.Fatalf("replica = %+v, want role replica in uk named %q", rec, ukPlace)
310 }
311}
312
313// persistedRecord is the record's bytes as the bucket holds them, with the
314// create time blanked (the one field a golden cannot pin).
315func persistedRecord(t *testing.T, fx *consoleFixture, host string) string {
316 t.Helper()
317 raw, _, err := fx.rel.Get(t.Context(), provision.RecordKey("bab", "titogo", host))
318 if err != nil {
319 t.Fatal(err)
320 }
321 return regexp.MustCompile(`"created_at":"[^"]*"`).ReplaceAllString(string(raw), `"created_at":"T"`)
322}
323
324// codex plan-R3 5, spec §16: the persisted record, byte for byte. A legacy
325// home create writes exactly what the unmodified door wrote (the golden was
326// taken from the base tree), and an app-defaulted non-home create differs
327// only by its region: no steering stamp, nothing else (R8).
328func TestPersistedRecordsMatchTheirGoldens(t *testing.T) {
329 const legacy = `{"schema":1,"host":"a.go.tito.io","slug":"a","app":"titogo","account":"bab","channel":"stable","created_at":"T","created_by":"alice@keymail.dev"}`
330 const appUK = `{"schema":1,"host":"a.go.tito.io","slug":"a","app":"titogo","account":"bab","region":"uk","channel":"stable","created_at":"T","created_by":"alice@keymail.dev"}`
331 for _, c := range []struct{ appRegion, want string }{{"", legacy}, {"uk", appUK}} {
332 fx, token, u := fillFixture(t, c.appRegion)
333 rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`)
334 if rec.Steering != "" {
335 t.Fatalf("app region %q: the response carries a steering stamp %q", c.appRegion, rec.Steering)
336 }
337 if got := persistedRecord(t, fx, "a.go.tito.io"); got != c.want {
338 t.Fatalf("app region %q: stored\n%s\nwant\n%s", c.appRegion, got, c.want)
339 }
340 }
341}
342
343// codex plan-R4 2: C16's and C17's lists name exactly the regions an
344// explicit create of this same instance would pass: the allow-list, the
345// armed gate and R9 (not automatic eligibility), never the region refused.
346// Each advertised region is then submitted, and must be accepted.
347func TestAppRegionRefusalsAdvertiseOnlyWhatPasses(t *testing.T) {
348 auEligible := edgeJSON("au-box", "9.9.9.9", "ap-southeast-2", "au", eligibleBox)
349 for _, c := range []struct {
350 name string
351 ukBox string
352 regions []string
353 want func(choices []string) string
354 choices []string
355 }{
356 // uk is the app's region, keyless; the home is outside the allow-list.
357 {"C16", ukNoKey, []string{"uk", "au", "nz"}, func(ch []string) string { return msgAppRegionUnavailable("uk", "no-secrets-key", ch) }, []string{"au"}},
358 // uk is eligible but outside the allow-list; "ie" there is the
359 // home's NAME, which the door reads as "" and so refuses.
360 {"C17", ukEligible, []string{"au", "nz", "ie"}, func(ch []string) string { return msgAppRegionNotAllowed("uk", ch) }, []string{"au"}},
361 } {
362 fx, token, u := fillFixture(t, "uk")
363 setEdges(t, fx, map[string]string{"uk-box": c.ukBox, "au-box": auEligible, "nz-box": nzNoHosting})
364 m := titoManifest()
365 m.Pool, m.Regions = "", c.regions
366 enableInstances(t, fx, "titogo", m)
367 status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
368 if want := c.want(c.choices); status != http.StatusBadRequest || !strings.Contains(out, want) {
369 t.Errorf("%s = %d %s, want %q", c.name, status, out, want)
370 }
371 for i, code := range c.choices {
372 mustCreate(t, u, token, fmt.Sprintf(`{"host":"alt%d.go.tito.io","region":%q}`, i, code))
373 }
374 }
375}
376
377// PF8: with an unnamed home the empty code is no choice a caller could pass,
378// so it is never listed. The home joins a list only when CONSOLE_HOME_REGION
379// names it, and then under that name.
380func TestAppRegionRefusalListsSeedTheHomeOnlyWhenNamed(t *testing.T) {
381 auEligible := edgeJSON("au-box", "9.9.9.9", "ap-southeast-2", "au", eligibleBox)
382 for _, home := range []string{"", "ie"} {
383 fx, token, u := fillFixture(t, "uk")
384 fx.srv.cfg.HomeRegion = home
385 setEdges(t, fx, map[string]string{"uk-box": ukNoKey, "au-box": auEligible})
386 want := []string{"au"}
387 if home != "" {
388 want = []string{"au", "ie"}
389 }
390 status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
391 if w := msgAppRegionUnavailable("uk", "no-secrets-key", want); status != http.StatusBadRequest || !strings.Contains(out, w) {
392 t.Fatalf("home %q: = %d %s, want %q", home, status, out, w)
393 }
394 for i, code := range want {
395 mustCreate(t, u, token, fmt.Sprintf(`{"host":"alt%d.go.tito.io","region":%q}`, i, code))
396 }
397 }
398}
399