package console import ( "context" "net/http" "strings" "testing" ) // edgesFor writes a fleet's edges files and returns the fixture's put helper // shape, so each door test states its fleet in one place. func putEdges(t *testing.T, fx *consoleFixture, files map[string]string) { t.Helper() for name, body := range files { if _, err := fx.rel.Put(context.Background(), "control/edges/"+name+".json", []byte(body), ""); err != nil { t.Fatal(err) } } } // Only a SOCKET-BACKED INSTANCE needs an armed activator on the target box. // A static route is served from bucket bytes by every armed edge with no // process at all, so refusing one for want of a hibernation bucket refuses a // site that would have served perfectly. The gate ran before the record's // kind was even read, so it applied to every kind alike. func TestCreateDoorLetsStaticIntoANonHostingRegion(t *testing.T) { fx, token := instanceFixture(t, "code-inst-statictonohost") m := titoManifest() m.Regions = nil m.Pool = "" enableInstances(t, fx, "titogo", m) // uk is reachable and explicitly cannot host; ie can. putEdges(t, fx, map[string]string{ "uk-box": `{"v":1,"box":"uk-box","eip":"1.2.3.4","placement":"uk","hibernation_configured":false,"hosts":{}}`, "ie-box": `{"v":1,"box":"ie-box","eip":"5.6.7.8","placement":"ie","data_bucket":"carlos-ie","hibernation_configured":true,"hosts":{}}`, }) u := instancesURL(fx, "bab", "titogo") // A static site into uk: allowed. It needs an edge, not an activator. code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"brochure.go.tito.io","region":"uk","kind":"static"}`) if code != http.StatusCreated { t.Fatalf("create static into a reachable non-hosting region = %d %s, want 201 — a static route runs no process", code, out) } // An instance into the same region: refused, and the refusal says why // and lists somewhere that would work. code, _, out = rawReq(t, http.MethodPost, u, token, `{"host":"app.go.tito.io","region":"uk"}`) if code != http.StatusBadRequest { t.Fatalf("create instance into a non-hosting region = %d %s, want 400", code, out) } if !strings.Contains(out, "cannot host") || !strings.Contains(out, "ie") { t.Errorf("refusal %q should say the region cannot host and name one that can", out) } } // An addr-backed service proxies to a process the platform never spawns, so // it needs the region to exist, not to host. func TestCreateDoorLetsAddrBackedServiceIntoANonHostingRegion(t *testing.T) { fx, token := instanceFixture(t, "code-inst-svctonohost") letStateUpstreams(t, fx, "alice@keymail.dev") m := titoManifest() m.Regions = nil m.Pool = "" enableInstances(t, fx, "titogo", m) putEdges(t, fx, map[string]string{ "uk-box": `{"v":1,"box":"uk-box","eip":"1.2.3.4","placement":"uk","hibernation_configured":false,"hosts":{}}`, }) code, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token, `{"host":"svc.go.tito.io","region":"uk","kind":"service","addrs":["10.0.0.4:8080"]}`) if code != http.StatusCreated { t.Fatalf("create addr-backed service into a non-hosting region = %d %s, want 201", code, out) } } // An UNREACHABLE region is refused for every kind — a box nothing can dial // homes nothing, static included. func TestCreateDoorRefusesEveryKindIntoAnUnreachableRegion(t *testing.T) { fx, token := instanceFixture(t, "code-inst-unreachable") m := titoManifest() m.Regions = nil m.Pool = "" enableInstances(t, fx, "titogo", m) putEdges(t, fx, map[string]string{ "se-box": `{"v":1,"box":"se-box","eip":"","placement":"se","hosts":{}}`, "ie-box": `{"v":1,"box":"ie-box","eip":"5.6.7.8","placement":"ie","data_bucket":"carlos-ie","hibernation_configured":true,"hosts":{}}`, }) code, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token, `{"host":"brochure.go.tito.io","region":"se","kind":"static"}`) if code != http.StatusBadRequest || !strings.Contains(out, "not armed") { t.Fatalf("create static into an unreachable region = %d %s, want 400 not-armed", code, out) } } // The move door's bar is the target's published STAGING bucket, and it is // unconditional. internal/host's moveTargets drops any target whose edges // file carries no data_bucket, and runMoveRound then answers "publishes no // data bucket yet — waiting" forever, so a door that admitted the move would // wedge a LIVE instance's residence on a lane that does not exist. func TestMoveDoorRequiresAStagingBucket(t *testing.T) { fx, token := instanceFixture(t, "code-inst-movestaging") m := titoManifest() m.Regions = nil m.Pool = "" enableInstances(t, fx, "titogo", m) // uk is an old writer: reachable, eligible to HOST (unknown is not no), // but it publishes no bucket coordinates, so it can take no move. putEdges(t, fx, map[string]string{ "uk-box": `{"v":1,"box":"uk-box","eip":"1.2.3.4","placement":"uk","hosts":{}}`, "ie-box": `{"v":1,"box":"ie-box","eip":"5.6.7.8","placement":"ie","data_bucket":"carlos-ie","hibernation_configured":true,"hosts":{}}`, }) u := instancesURL(fx, "bab", "titogo") // uk IS a legal create target — unknown hosting stays eligible. This // instance is created in ie, though, so the move below is a genuine // cross-region request and cannot be refused by the same-region conflict // instead of by the bar under test. if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"mover.go.tito.io","region":"ie"}`); code != http.StatusCreated { t.Fatalf("create into ie = %d %s, want 201", code, out) } if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"stayer.go.tito.io","region":"uk"}`); code != http.StatusCreated { t.Fatalf("create into an unknown-hosting region = %d %s, want 201 — unknown is not no", code, out) } // uk is NOT a legal move target: it publishes no staging bucket. code, _, out := rawReq(t, http.MethodPost, u+"/mover.go.tito.io/move", token, `{"region":"uk"}`) if code != http.StatusBadRequest { t.Fatalf("move to a region with no staging bucket = %d %s, want 400", code, out) } if !strings.Contains(out, "staging bucket") { t.Errorf("refusal %q should name the missing staging bucket — the worker's own reason", out) } if !strings.Contains(out, "ie") { t.Errorf("refusal %q should list the regions that can take a move", out) } }