package console import ( "context" "encoding/json" "errors" "fmt" "net/http" "regexp" "strings" "testing" "time" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) type fillRec struct { provision.Record RegionFrom string `json:"region_from"` RegionName string `json:"region_name"` } // fillFixture: titogo with an unrestricted shared-pool manifest, a stored // app region, a named home, the deployment's secrets key, and two // eligible boxes. fx.st.db is the Store's own *sql.DB (same package). func fillFixture(t *testing.T, appRegion string) (*consoleFixture, string, string) { t.Helper() fx, token := instanceFixture(t, "code-fill-"+appRegion) fx.srv.cfg.HomeRegion, fx.srv.cfg.SecretsPub = "ie", testKeyPub m := titoManifest() m.Pool, m.Regions = "", nil enableInstances(t, fx, "titogo", m) if _, err := fx.st.db.Exec(`UPDATE apps SET region = ?, region_source = 'chosen' WHERE app = 'titogo'`, appRegion); err != nil { t.Fatal(err) } setEdges(t, fx, map[string]string{"ie-box": homeBox, "uk-box": ukEligible}) return fx, token, instancesURL(fx, "bab", "titogo") } func create(t *testing.T, u, token, body string) (int, fillRec, string) { t.Helper() status, _, out := rawReq(t, http.MethodPost, u, token, body) var rec fillRec json.Unmarshal([]byte(out), &rec) return status, rec, out } func mustCreate(t *testing.T, u, token, body string) fillRec { t.Helper() status, rec, out := create(t, u, token, body) if status != http.StatusCreated { t.Fatalf("create %s = %d %s, want 201", body, status, out) } return rec } func TestFillFromTheAppRegion(t *testing.T) { fx, token, u := fillFixture(t, "uk") letStateUpstreams(t, fx, "alice@keymail.dev") // one case below is addr-backed if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`); rec.Region != "uk" || rec.RegionFrom != "app" || rec.RegionName != ukPlace { t.Fatalf("fill = %+v", rec) } if rec := mustCreate(t, u, token, `{"host":"b.go.tito.io","region":"ie"}`); rec.Region != "" || rec.RegionFrom != "request" { t.Fatalf("explicit home = %+v", rec) } for _, body := range []string{ `{"host":"c.go.tito.io","kind":"static"}`, `{"host":"d.go.tito.io","kind":"service","addrs":["10.0.0.4:8080"]}`, } { if rec := mustCreate(t, u, token, body); rec.Region != "" { t.Errorf("%s was filled: %+v", body, rec) } } status, _, out := create(t, u, token, `{"host":"e.go.tito.io","role":"replica"}`) if status != http.StatusBadRequest || !strings.Contains(out, "role replica needs a placement") { t.Errorf("replica without a region = %d %s, want the existing refusal", status, out) } } func TestFillWithAHomeAppRegionChangesNothing(t *testing.T) { _, token, u := fillFixture(t, "") if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`); rec.Region != "" || rec.RegionFrom != "" { t.Fatalf("home app = %+v", rec) } } // Precedence with a NON-HOME app default in play: pool and fleet win. func TestFillPrecedence(t *testing.T) { fx, token, u := fillFixture(t, "uk") m := titoManifest() m.Pool, m.Regions = "tito", nil enableInstances(t, fx, "titogo", m) if rec := mustCreate(t, u, token, `{"host":"p.go.tito.io"}`); rec.Region != "" || rec.RegionFrom != "" { t.Fatalf("pooled = %+v, want no fill", rec) } m.Pool = "" enableInstances(t, fx, "titogo", m) if err := fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme"); err != nil { t.Fatal(err) } if rec := mustCreate(t, u, token, `{"host":"f.go.tito.io"}`); rec.Region != "fleet/acme" || rec.RegionFrom != "fleet" { t.Fatalf("fleet = %+v", rec) } } func TestFillStoreErrorIsC20(t *testing.T) { fx, token, u := fillFixture(t, "uk") fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") } status, _, out := create(t, u, token, `{"host":"a.go.tito.io"}`) if status != http.StatusBadGateway || !strings.Contains(out, msgAppRegionUnreadable) { t.Fatalf("store error = %d %s, want 502 C20", status, out) } } // codex R2-2: dormancy keeps explicit requests, never app-derived ones. // Stage B Task 10 review M5: with no edges file at all but boxes still // reporting fresh, the evidence is incomplete (ruling (A)), so the // app-derived request is "could not confirm" (C28), not "absent"; once no // box reports either, it is C16 absent. func TestAppDerivedNeedsPositiveEligibility(t *testing.T) { fx, token, u := fillFixture(t, "uk") for _, k := range []string{"control/edges/uk-box.json", "control/edges/ie-box.json"} { if err := fx.rel.Delete(t.Context(), k); err != nil { t.Fatal(err) } } mustCreate(t, u, token, `{"host":"x.go.tito.io","region":"uk"}`) status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`) if status != http.StatusServiceUnavailable || !strings.Contains(out, msgAppRegionUnconfirmed("uk")) { t.Fatalf("app-derived uk with fresh boxes and no edges files = %d %s, want C28 unconfirmed", status, out) } keys, err := fx.rel.List(t.Context(), "status/boxes/") if err != nil { t.Fatal(err) } for _, k := range keys { if err := fx.rel.Delete(t.Context(), k); err != nil { t.Fatal(err) } } status, _, out = create(t, u, token, `{"host":"z.go.tito.io"}`) if status != http.StatusBadRequest || !strings.Contains(out, reasonText("absent")) { t.Fatalf("app-derived uk under dormancy = %d %s, want C16 absent", status, out) } } // F1: the box is retired but its edges object remains, as it does in // production (nothing deletes it). Its status snapshot has gone stale, and // that alone must take uk out of automatic placement: C16 absent, the same // refusal as the deleted-object case above. func TestAppDerivedNeedsALiveBox(t *testing.T) { fx, token, u := fillFixture(t, "uk") setBoxSnapshot(t, fx.rel, "uk-box", time.Now().Add(-time.Hour)) status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`) if status != http.StatusBadRequest || !strings.Contains(out, reasonText("absent")) { t.Fatalf("app-derived uk with a stale box snapshot = %d %s, want C16 absent", status, out) } } // Partial reads (re-review r1): with a keyless sibling listed beside the // keyed uk box, a failed edges GET or a failed status GET on that sibling // is incomplete evidence. An app-derived uk create is refused C28 (could // not confirm, try again or pass a region), never created through the box // that was read. An explicit --region uk create and a home app's create // keep their behaviour under both failures: the explicit gates read the // files alone and the home is exempt. func TestAppDerivedNeedsCompleteEvidence(t *testing.T) { failures := map[string]string{ "edges GET": "control/edges/uk-box-2.json", "status GET": "status/boxes/uk-box-2.json", } for name, key := range failures { fx, token, u := fillFixture(t, "uk") setEdges(t, fx, map[string]string{"uk-box-2": ukKeylessSibling}) fx.srv.cfg.Releases = failingGet{Store: fx.rel, key: key} status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`) if status != http.StatusServiceUnavailable || !strings.Contains(out, msgAppRegionUnconfirmed("uk")) { t.Errorf("%s failure, app-derived uk = %d %s, want 503 C28", name, status, out) } if rec := mustCreate(t, u, token, `{"host":"x.go.tito.io","region":"uk"}`); rec.Region != "uk" { t.Errorf("%s failure, explicit uk = %+v, want created in uk as before", name, rec) } } for name, key := range failures { fx, token, u := fillFixture(t, "") setEdges(t, fx, map[string]string{"uk-box-2": ukKeylessSibling}) fx.srv.cfg.Releases = failingGet{Store: fx.rel, key: key} if rec := mustCreate(t, u, token, `{"host":"h.go.tito.io"}`); rec.Region != "" { t.Errorf("%s failure, home app = %+v, want created at the home", name, rec) } } } // Unmatched boxes, ruling (A): a fresh status snapshot for a box with no // edges file is incomplete evidence at the door too, so an app-derived uk // create is C28, while an explicit --region uk create and a home app's // create keep their behaviour. func TestAppDerivedNeedsEveryFreshBoxToPublishEdges(t *testing.T) { fx, token, u := fillFixture(t, "uk") setBoxSnapshot(t, fx.rel, "uk-box-2", time.Now()) status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`) if status != http.StatusServiceUnavailable || !strings.Contains(out, msgAppRegionUnconfirmed("uk")) { t.Errorf("fresh status-only box, app-derived uk = %d %s, want 503 C28", status, out) } if rec := mustCreate(t, u, token, `{"host":"x.go.tito.io","region":"uk"}`); rec.Region != "uk" { t.Errorf("explicit uk = %+v, want created in uk as before", rec) } fx, token, u = fillFixture(t, "") setBoxSnapshot(t, fx.rel, "uk-box-2", time.Now()) if rec := mustCreate(t, u, token, `{"host":"h.go.tito.io"}`); rec.Region != "" { t.Errorf("home app = %+v, want created at the home", rec) } } func TestAppDerivedEdgesReadFailureIs503(t *testing.T) { fx, token, u := fillFixture(t, "uk") fx.srv.cfg.Releases = failingList{fx.rel} status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`) if status != http.StatusServiceUnavailable || !strings.Contains(out, msgAppRegionUnconfirmed("uk")) { t.Fatalf("edges failure = %d %s, want 503 C28", status, out) } } func TestAppDerivedRefusals(t *testing.T) { fx, token, u := fillFixture(t, "uk") m := titoManifest() m.Pool, m.Regions = "", []string{"au"} enableInstances(t, fx, "titogo", m) // au publishes no box, so nothing the allow-list names can pass: C17b. if status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`); status != http.StatusBadRequest || !strings.Contains(out, msgAppRegionNotAllowed("uk", nil)) { t.Fatalf("allow-list = %d %s, want C17b", status, out) } m.Regions = nil enableInstances(t, fx, "titogo", m) if status, _, out := create(t, u, token, `{"host":"z.go.tito.io","replicas":["uk"]}`); status != http.StatusBadRequest || !strings.Contains(out, msgReplicaOverlap("uk")) { t.Fatalf("replica overlap = %d %s, want C26", status, out) } setEdges(t, fx, map[string]string{"uk-box": ukNoKey}) if status, _, out := create(t, u, token, `{"host":"k.go.tito.io"}`); status != http.StatusBadRequest || !strings.Contains(out, reasonText("no-secrets-key")) { t.Fatalf("app-derived keyless uk = %d %s, want C16 no-secrets-key", status, out) } } // R9's explicit half: refused only when a current writer PROVES the // region keyless, and only for what needs config on the box. func TestExplicitCreateIntoAKeylessRegion(t *testing.T) { fx, token, u := fillFixture(t, "") letStateUpstreams(t, fx, "alice@keymail.dev") // one case below is addr-backed setEdges(t, fx, map[string]string{"uk-box": ukNoKey}) if status, _, out := create(t, u, token, `{"host":"a.go.tito.io","region":"uk"}`); status != http.StatusBadRequest || !strings.Contains(out, msgNoSecretsKey("uk")) { t.Fatalf("keyless instance = %d %s, want C31", status, out) } if status, _, out := create(t, u, token, `{"host":"r.go.tito.io","region":"uk","role":"replica"}`); status != http.StatusBadRequest || !strings.Contains(out, msgNoSecretsKey("uk")) { t.Fatalf("keyless replica = %d %s, want C31", status, out) } mustCreate(t, u, token, `{"host":"s.go.tito.io","region":"uk","kind":"static"}`) // An addr-backed service proxies to a process the platform never starts, // so it needs no config on the box either (codex plan-R2 7). mustCreate(t, u, token, `{"host":"v.go.tito.io","region":"uk","kind":"service","addrs":["10.0.0.4:8080"]}`) setEdges(t, fx, map[string]string{"uk-box": ukOldWriter}) mustCreate(t, u, token, `{"host":"o.go.tito.io","region":"uk"}`) // unknown keeps today's behaviour setEdges(t, fx, map[string]string{"uk-box": ukNoKey}) fx.srv.cfg.SecretsPub = "" mustCreate(t, u, token, `{"host":"n.go.tito.io","region":"uk"}`) // the console cannot judge } // edgesListFails fails only the listing of control/edges/, so the rest of the // create door (records, leftovers) runs as it does in production. type edgesListFails struct{ release.Store } func (e edgesListFails) List(ctx context.Context, prefix string) ([]string, error) { if strings.HasPrefix(prefix, edgesFilePrefix) { return nil, errors.New("bucket unreachable") } return e.Store.List(ctx, prefix) } // PF10: an explicit request keeps today's behaviour when the edges cannot be // read. R9 refuses only on proof, so unknown data never refuses it. func TestExplicitCreateWithTheEdgesListingDownIsNotRefused(t *testing.T) { fx, token, u := fillFixture(t, "") fx.srv.cfg.Releases = edgesListFails{fx.rel} if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io","region":"uk"}`); rec.Region != "uk" || rec.RegionFrom != "request" { t.Fatalf("explicit uk with the edges down = %+v", rec) } if rec := mustCreate(t, u, token, `{"host":"r.go.tito.io","region":"uk","role":"replica"}`); rec.Role != "replica" || rec.Region != "uk" { t.Fatalf("explicit replica with the edges down = %+v", rec) } } func TestFillRunsWithTheSwitchOff(t *testing.T) { fx, token, u := fillFixture(t, "uk") fx.srv.cfg.AppRegions = false if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`); rec.Region != "uk" { t.Fatalf("switch off = %+v, want the stored uk", rec) } } // codex plan-R3 6: a successful replica create names its place too, so the // CLI prints C14 for it as for any other placement. func TestReplicaCreateNamesItsPlace(t *testing.T) { _, token, u := fillFixture(t, "") rec := mustCreate(t, u, token, `{"host":"r.go.tito.io","region":"uk","role":"replica"}`) if rec.Role != "replica" || rec.Region != "uk" || rec.RegionName != ukPlace { t.Fatalf("replica = %+v, want role replica in uk named %q", rec, ukPlace) } } // persistedRecord is the record's bytes as the bucket holds them, with the // create time blanked (the one field a golden cannot pin). func persistedRecord(t *testing.T, fx *consoleFixture, host string) string { t.Helper() raw, _, err := fx.rel.Get(t.Context(), provision.RecordKey("bab", "titogo", host)) if err != nil { t.Fatal(err) } return regexp.MustCompile(`"created_at":"[^"]*"`).ReplaceAllString(string(raw), `"created_at":"T"`) } // codex plan-R3 5, spec ยง16: the persisted record, byte for byte. A legacy // home create writes exactly what the unmodified door wrote (the golden was // taken from the base tree), and an app-defaulted non-home create differs // only by its region: no steering stamp, nothing else (R8). func TestPersistedRecordsMatchTheirGoldens(t *testing.T) { 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"}` 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"}` for _, c := range []struct{ appRegion, want string }{{"", legacy}, {"uk", appUK}} { fx, token, u := fillFixture(t, c.appRegion) rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`) if rec.Steering != "" { t.Fatalf("app region %q: the response carries a steering stamp %q", c.appRegion, rec.Steering) } if got := persistedRecord(t, fx, "a.go.tito.io"); got != c.want { t.Fatalf("app region %q: stored\n%s\nwant\n%s", c.appRegion, got, c.want) } } } // codex plan-R4 2: C16's and C17's lists name exactly the regions an // explicit create of this same instance would pass: the allow-list, the // armed gate and R9 (not automatic eligibility), never the region refused. // Each advertised region is then submitted, and must be accepted. func TestAppRegionRefusalsAdvertiseOnlyWhatPasses(t *testing.T) { auEligible := edgeJSON("au-box", "9.9.9.9", "ap-southeast-2", "au", eligibleBox) for _, c := range []struct { name string ukBox string regions []string want func(choices []string) string choices []string }{ // uk is the app's region, keyless; the home is outside the allow-list. {"C16", ukNoKey, []string{"uk", "au", "nz"}, func(ch []string) string { return msgAppRegionUnavailable("uk", "no-secrets-key", ch) }, []string{"au"}}, // uk is eligible but outside the allow-list; "ie" there is the // home's NAME, which the door reads as "" and so refuses. {"C17", ukEligible, []string{"au", "nz", "ie"}, func(ch []string) string { return msgAppRegionNotAllowed("uk", ch) }, []string{"au"}}, } { fx, token, u := fillFixture(t, "uk") setEdges(t, fx, map[string]string{"uk-box": c.ukBox, "au-box": auEligible, "nz-box": nzNoHosting}) m := titoManifest() m.Pool, m.Regions = "", c.regions enableInstances(t, fx, "titogo", m) status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`) if want := c.want(c.choices); status != http.StatusBadRequest || !strings.Contains(out, want) { t.Errorf("%s = %d %s, want %q", c.name, status, out, want) } for i, code := range c.choices { mustCreate(t, u, token, fmt.Sprintf(`{"host":"alt%d.go.tito.io","region":%q}`, i, code)) } } } // PF8: with an unnamed home the empty code is no choice a caller could pass, // so it is never listed. The home joins a list only when CONSOLE_HOME_REGION // names it, and then under that name. func TestAppRegionRefusalListsSeedTheHomeOnlyWhenNamed(t *testing.T) { auEligible := edgeJSON("au-box", "9.9.9.9", "ap-southeast-2", "au", eligibleBox) for _, home := range []string{"", "ie"} { fx, token, u := fillFixture(t, "uk") fx.srv.cfg.HomeRegion = home setEdges(t, fx, map[string]string{"uk-box": ukNoKey, "au-box": auEligible}) want := []string{"au"} if home != "" { want = []string{"au", "ie"} } status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`) if w := msgAppRegionUnavailable("uk", "no-secrets-key", want); status != http.StatusBadRequest || !strings.Contains(out, w) { t.Fatalf("home %q: = %d %s, want %q", home, status, out, w) } for i, code := range want { mustCreate(t, u, token, fmt.Sprintf(`{"host":"alt%d.go.tito.io","region":%q}`, i, code)) } } }