| new file mode 100644 |
| index 000000000..affd566c4 |
| --- /dev/null |
| +++ b/internal/console/regionfill_test.go |
| @@ -0,0 +1,310 @@ |
| +package console |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| + "net/http" |
| + "regexp" |
| + "strings" |
| + "testing" |
| + |
| + "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) { |
| + _, token, u := fillFixture(t, "uk") |
| + 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. |
| +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.StatusBadRequest || !strings.Contains(out, reasonText("absent")) { |
| + t.Fatalf("app-derived uk under dormancy = %d %s, want C16 absent", status, out) |
| + } |
| +} |
| + |
| +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, "") |
| + 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)) |
| + } |
| + } |
| +} |