package console import ( "context" "net/http" "net/url" "strings" "testing" "time" "github.com/carlosframework/platform/internal/registry" ) // A name whose platform apex a sibling already holds is not free. The // instance door refuses a sibling's apex once the sibling exists; this is the // other order — the instance first, while the name was free, and the app after // it — which would leave the new app with no URL of its own for ever. func TestANameWhoseApexASiblingHoldsIsNotFree(t *testing.T) { fx, member := instanceFixture(t, "code-apex-claim") fx.srv.cfg.AppsDomain = "oncarlos.test" domain := fx.sqid + ".oncarlos.test" if status, _, out := rawReq(t, http.MethodPut, enableURL(fx, fx.sqid, "titogo"), member, `{}`); status != http.StatusOK { t.Fatalf("enable = %d %s", status, out) } // While no app is called acme, titogo may take acme's label. if status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, fx.sqid, "titogo"), member, `{"host":"acme.`+domain+`"}`); status != http.StatusCreated { t.Fatalf("instance at a free label = %d %s", status, out) } newApp := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/new" // An instance record is enough: the reconciler would make it a route. status, out := postJSON(t, newApp, map[string]string{"name": "acme"}, member) if status != http.StatusConflict { t.Fatalf("creating acme over titogo's instance = %d %v, want 409", status, out) } if msg, _ := out["error"].(string); !strings.Contains(msg, "acme."+domain) || !strings.Contains(msg, "titogo") { t.Errorf("the refusal should name the address and who holds it: %q", msg) } if has, _ := fx.st.HasAppIn(OpsAccountID, "acme"); has { t.Error("acme was created") } // So is a route on its own, whatever put it there. fx.addRoute(t, registry.Route{Host: "beta." + domain, App: "titogo", Account: fx.sqid, Kind: "instance"}) if status, out := postJSON(t, newApp, map[string]string{"name": "beta"}, member); status != http.StatusConflict { t.Fatalf("creating beta over titogo's route = %d %v, want 409", status, out) } // A route that names no app holds the host just as firmly: the domains // pass defers to it all the same. fx.addRoute(t, registry.Route{Host: "delta." + domain, Account: fx.sqid, Kind: "proxy"}) status, out = postJSON(t, newApp, map[string]string{"name": "delta"}, member) if status != http.StatusConflict { t.Fatalf("creating delta over an app-less route = %d %v, want 409", status, out) } if msg, _ := out["error"].(string); !strings.Contains(msg, "no app") { t.Errorf("the refusal should say the route has no app: %q", msg) } // Nor is a route that wears the name but serves another account the // name's own. fx.addRoute(t, registry.Route{Host: "epsilon." + domain, App: "epsilon", Account: "zzzz", Kind: "instance"}) status, out = postJSON(t, newApp, map[string]string{"name": "epsilon"}, member) if status != http.StatusConflict { t.Fatalf("creating epsilon over another account's route = %d %v, want 409", status, out) } if msg, _ := out["error"].(string); !strings.Contains(msg, "zzzz") { t.Errorf("the refusal should name the account the route serves: %q", msg) } // Trashing the sibling does not free the name: its record survives, and // restoring it would bring the instance back. if err := fx.st.TrashAppRow(OpsAccountID, "titogo", time.Now()); err != nil { t.Fatal(err) } status, out = postJSON(t, newApp, map[string]string{"name": "acme"}, member) if status != http.StatusConflict { t.Fatalf("creating acme over a trashed titogo's instance = %d %v, want 409", status, out) } if msg, _ := out["error"].(string); !strings.Contains(msg, "trash") { t.Errorf("the refusal should say titogo is in the trash: %q", msg) } // A name nobody holds is still just a name. if status, out := postJSON(t, newApp, map[string]string{"name": "gamma"}, member); status != http.StatusOK { t.Fatalf("creating a free name = %d %v, want 200", status, out) } } // Renaming onto a name whose apex a sibling holds is the same arrival by // another door, and is refused the same way. func TestRenameOntoAHeldApexIsRefused(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.AppsDomain = "oncarlos.test" freshApp(t, fx, "sprout") fx.addRoute(t, registry.Route{Host: "sapling." + fx.sqid + ".oncarlos.test", App: "kass", Account: fx.sqid, Kind: "instance"}) c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) form := url.Values{"csrf": {csrf}, "to": {"sapling"}, "confirmed": {"1"}} if status, loc, body := postForm(t, c, fx.u("/apps/sprout/rename"), form); strings.Contains(body, "Rename sprout to sapling?") { t.Fatalf("offered the rename onto a held apex = %d -> %q", status, loc) } if has, _ := fx.st.HasAppIn(OpsAccountID, "sapling"); has { t.Error("sprout was renamed onto an apex kass holds") } if has, _ := fx.st.HasAppIn(OpsAccountID, "sprout"); !has { t.Error("sprout should be untouched") } } // Moving an app into an account where another app holds its apex there is // the third arrival, and is refused before the card. func TestMoveIntoAnAccountWhereTheApexIsHeldIsRefused(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.AppsDomain = "oncarlos.test" const destID = 4242 if err := fx.st.CreateAccount(destID, "cdef", "Second", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } stampKassRoutes(t, fx, "cdef") fx.addRoute(t, registry.Route{Host: "kass.cdef.oncarlos.test", App: "other", Account: "cdef", Kind: "instance"}) c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) // Not confirmed: without the check this renders the move card, so the // card's absence is the refusal. status, _, body := postForm(t, c, fx.u("/apps/kass/move"), url.Values{"csrf": {csrf}}) if strings.Contains(body, "Move kass to Second?") { t.Fatalf("offered the move into an account where the apex is held = %d", status) } if has, _ := fx.st.HasAppIn(destID, "kass"); has { t.Error("kass was moved") } if has, _ := fx.st.HasAppIn(OpsAccountID, "kass"); !has { t.Error("kass should still be in its own account") } } // At boot the console names every route already sitting at another app's // apex — live or trashed, with another app, no app, or another account on // the route — and nothing else: an app's own apex, a label no app wears, and // hosts outside the apps domain are not squats. func TestTheStartupReportNamesSquatsAndNothingElse(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.AppsDomain = "oncarlos.test" now := time.Now() for _, app := range []string{"acme", "parked", "blank", "elsewhere"} { if _, err := fx.st.CreateAppIn(OpsAccountID, app, "paul@keymail.dev", now); err != nil { t.Fatal(err) } } if err := fx.st.TrashAppRow(OpsAccountID, "parked", now); err != nil { t.Fatal(err) } d := fx.sqid + ".oncarlos.test" for _, rt := range []registry.Route{ {Host: "acme." + d, App: "titogo", Account: fx.sqid, Kind: "instance"}, // squat on a live app {Host: "parked." + d, App: "titogo", Account: fx.sqid, Kind: "instance"}, // squat on a trashed one {Host: "blank." + d, Account: fx.sqid, Kind: "proxy"}, // squat with no app {Host: "elsewhere." + d, App: "elsewhere", Account: "zzzz", Kind: "proxy"}, // right name, wrong account {Host: "titogo." + d, App: "titogo", Account: fx.sqid, Kind: "instance"}, // its own apex {Host: "nobody." + d, App: "titogo", Account: fx.sqid, Kind: "instance"}, // no app by that name {Host: "acme.example.com", App: "titogo", Account: fx.sqid, Kind: "instance"}, } { fx.addRoute(t, rt) } got := fx.srv.apexSquats(context.Background()) if len(got) != 4 { t.Fatalf("report = %q, want the four squats", got) } joined := strings.Join(got, "\n") for _, want := range []string{"acme." + d, "parked." + d, "in the trash", "belongs to titogo", "blank." + d, "belongs to no app", "elsewhere." + d, "(zzzz)"} { if !strings.Contains(joined, want) { t.Errorf("report missing %q:\n%s", want, joined) } } }