| 1 | package console |
| 2 | |
| 3 | import ( |
| 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 | |
| 18 | type 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). |
| 27 | func 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 | |
| 41 | func 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 | |
| 49 | func 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 | |
| 58 | func 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 | |
| 81 | func 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. |
| 89 | func 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 | |
| 107 | func 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. |
| 121 | func 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. |
| 152 | func 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. |
| 168 | func 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. |
| 199 | func 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 | |
| 216 | func 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 | |
| 225 | func 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. |
| 250 | func 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. |
| 273 | type edgesListFails struct{ release.Store } |
| 274 | |
| 275 | func (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. |
| 284 | func 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 | |
| 295 | func 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. |
| 305 | func 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). |
| 315 | func 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). |
| 328 | func 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. |
| 347 | func 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. |
| 380 | func 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 | |