package console // The /fleet/architecture page (archpage.go): its gate, its honest empty // state, and its refresh door. What these tests own is the DOOR's shape — // who may see the page at all, what it says when it has never had an // inventory, and that the refresh button cannot be driven from another // site. The graph itself is proven in archgraph_test.go and the drawing in // archlayout_test.go. import ( "context" "errors" "net/http" "net/url" "path/filepath" "regexp" "strconv" "strings" "testing" "time" "github.com/carlosframework/platform/internal/economics" "github.com/carlosframework/platform/internal/infraview" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // TestFleetOperatorOnly's shape against /fleet/architecture: signed out, // signed in but not an operator, and a revoked operator all get the exact // 404 an unrouted path gets — byte-identical, so the route's existence is // not probeable. func TestArchitectureOperatorOnly(t *testing.T) { fx := newConsoleFixture(t) out := newBrowser(t) ghostStatus, _, ghost := get(t, out, fx.ts.URL+"/this-route-does-not-exist") if ghostStatus != http.StatusNotFound { t.Fatalf("sanity: missing route = %d", ghostStatus) } if st, _, body := get(t, out, fx.ts.URL+"/fleet/architecture"); st != http.StatusNotFound || body != ghost { t.Fatalf("signed-out = %d %q, want the same 404 as a missing route", st, body) } owner := signInPaul(t, fx) if st, _, body := get(t, owner, fx.ts.URL+"/fleet/architecture"); st != http.StatusNotFound || body != ghost { t.Fatalf("non-operator = %d %q, want the same 404", st, body) } if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } st, _, body := get(t, owner, fx.ts.URL+"/fleet/architecture") if st != 200 || !strings.Contains(body, "archsvg") { t.Fatalf("operator view = %d, want 200 with the diagram:\n%s", st, body) } if err := fx.st.SetOperator("paul@keymail.dev", false); err != nil { t.Fatal(err) } if st, _, body := get(t, owner, fx.ts.URL+"/fleet/architecture"); st != http.StatusNotFound || body != ghost { t.Fatalf("revoked = %d %q, want the same 404", st, body) } } // The refresh door's gate is the page's, and a wrong-method request is // byte-identical to a missing route: a "POST /path" registration would // answer a GET with 405 + Allow, which leaks that the route exists. func TestArchitectureRefreshOperatorOnly(t *testing.T) { fx := newConsoleFixture(t) out := newBrowser(t) ghostStatus, _, ghost := get(t, out, fx.ts.URL+"/this-route-does-not-exist") if ghostStatus != http.StatusNotFound { t.Fatalf("sanity: missing route = %d", ghostStatus) } if st, _, body := postForm(t, out, fx.ts.URL+"/fleet/architecture/refresh", url.Values{}); st != http.StatusNotFound || body != ghost { t.Fatalf("signed-out refresh = %d %q, want the same 404 as a missing route", st, body) } c := signInPaul(t, fx) if st, _, body := postForm(t, c, fx.ts.URL+"/fleet/architecture/refresh", url.Values{}); st != http.StatusNotFound || body != ghost { t.Fatalf("non-operator refresh = %d %q, want the same 404", st, body) } if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } // A GET on the door is a missing route too, for the same reason. if st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture/refresh"); st != http.StatusNotFound || body != ghost { t.Fatalf("GET on the refresh door = %d %q, want the same 404", st, body) } } // With no inventory ever taken, the page still renders and says WHICH of // the three states it is in — never a confident "no resources". func TestArchitecturePageWithoutInventory(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture") if st != 200 { t.Fatalf("= %d, want 200", st) } if !strings.Contains(body, "No inventory has been taken yet") { t.Errorf("page must name the never-taken state:\n%s", body) } if strings.Contains(body, "no resources") { t.Errorf("page must not claim an empty estate it never read") } } // A deployment whose inventory grant was refused says so in those words — // the second of the three states, and the one that must never be rendered // as "nothing here". func TestArchitecturePageUngranted(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } fx.srv.infraDisabled.Store(true) st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture") if st != 200 { t.Fatalf("= %d, want 200", st) } if !strings.Contains(body, "no infrastructure read grant") { t.Errorf("page must name the ungranted state:\n%s", body) } if strings.Contains(body, "No inventory has been taken yet") { t.Errorf("ungranted must not be reported as never-taken") } } // The refresh door: CSRF-checked, single-flighted, and it writes an // inventory the page then reads back. func TestArchitectureRefreshDoor(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } called := 0 fx.srv.inventory = func(ctx context.Context) (infraview.Inventory, error) { called++ return infraview.Inventory{Provider: "aws", Account: "123456789012", TakenAt: time.Now().UTC()}, nil } // No CSRF token: refused, and the pass must not have run. st, _, _ := postForm(t, c, fx.ts.URL+"/fleet/architecture/refresh", url.Values{}) if st != http.StatusForbidden { t.Fatalf("no-CSRF refresh = %d, want 403", st) } if called != 0 { t.Fatalf("a refused refresh must not run the pass") } // With the token: runs once and redirects back to the page. csrf := sessionToken(t, fx, c) st, loc, body := postForm(t, c, fx.ts.URL+"/fleet/architecture/refresh", url.Values{"csrf": {csrf}}) if st != http.StatusSeeOther || loc != "/fleet/architecture" { t.Fatalf("refresh = %d -> %q, want 303 back to the page:\n%s", st, loc, body) } if called != 1 { t.Fatalf("pass ran %d times, want 1", called) } // And the page now reports the estate rather than one of the three // missing states. st, _, body = get(t, c, fx.ts.URL+"/fleet/architecture") if st != 200 || !strings.Contains(body, "123456789012") { t.Fatalf("page after refresh = %d, want the inventoried account:\n%s", st, body) } if strings.Contains(body, "No inventory has been taken yet") { t.Errorf("page still claims no inventory after a successful refresh") } } // The refresh door does NOT clear the auth latch: a deployment with no IAM // grant gets the same honest answer however many times the button is // pressed, rather than a fresh AWS attempt per click. func TestArchitectureRefreshKeepsLatch(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } called := 0 fx.srv.inventory = func(ctx context.Context) (infraview.Inventory, error) { called++ return infraview.Inventory{}, nil } fx.srv.infraDisabled.Store(true) csrf := sessionToken(t, fx, c) st, loc, body := postForm(t, c, fx.ts.URL+"/fleet/architecture/refresh", url.Values{"csrf": {csrf}}) if st != http.StatusSeeOther || loc != "/fleet/architecture" { t.Fatalf("refresh = %d -> %q, want 303 back to the page:\n%s", st, loc, body) } if called != 0 { t.Fatalf("a latched-off deployment must not call the provider (called %d)", called) } if !fx.srv.infraDisabled.Load() { t.Errorf("the refresh door cleared the auth latch") } if fx.srv.infraRefreshing.Load() { t.Errorf("the single-flight latch was left set after the door returned") } } // The deployment bucket reaches the graph through Config, not the // environment: a console configured with one draws the box -> bucket edge // the spec mandates. func TestArchitectureDrawsConfiguredBucket(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } fx.srv.cfg.Bucket = "carlos-deploy-fixture" st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture") if st != 200 { t.Fatalf("= %d, want 200", st) } if !strings.Contains(body, "carlos-deploy-fixture") { t.Errorf("the configured deployment bucket is not drawn:\n%s", body) } } // The page is two estates, both headed and both drawn — including on a // deployment where the second one is empty, which is what most accounts // are. A page that dropped the second heading when it had nothing under it // would be the page quietly deciding the account holds nothing else, which // is a claim rather than a read. func TestArchitectureDrawsBothEstates(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture") if st != 200 { t.Fatalf("= %d, want 200", st) } if !strings.Contains(body, "

CARLOS

") { t.Errorf("the CARLOS estate is not headed:\n%s", body) } if !strings.Contains(body, "

Also in this account

") { t.Errorf("the second estate is not headed:\n%s", body) } // Estate-neutral wording: this console serves carloku, keymail and // woodstar too, and "legacy" describes only Tito's own account. if strings.Contains(body, "legacy") || strings.Contains(body, "Legacy") { t.Errorf("the second bank must not be named for one deployment's history:\n%s", body) } // One diagram per REGION BAND, each under its own heading — not one per // estate. That is what makes the page scroll down instead of across, so // a page that had quietly gone back to one picture per estate would draw // the same content and be the bug this shape exists to fix. bands := strings.Count(body, `

`) if bands < 2 { t.Fatalf("page draws %d bands — a page back to one picture per estate is the bug this shape exists to fix:\n%s", bands, body) } if n := strings.Count(body, `class="archsvg"`); n != bands { t.Errorf("page draws %d diagrams under %d band headings — every band has exactly one picture", n, bands) } // Every drawn picture fits the console's 64rem main column, which is the // whole user-visible point: the page grows downwards, never sideways. for _, w := range regexp.MustCompile(` 1024 { t.Errorf("a band is %dpx wide — the page would scroll sideways", px) } } } // A read that failed is SAID on the page. An empty Boxes section with // nothing beside it would read as "this deployment has no boxes" — the one // claim a page whose registry read just failed must not make. func TestArchitectureNamesAFailedRead(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } fx.srv.cfg.Registry = filepath.Join(t.TempDir(), "no-such-dir", "registry.db") st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture") if st != 200 { t.Fatalf("= %d, want 200 — a failed registry read costs its section, not the page", st) } if !strings.Contains(body, "Reads that failed") || !strings.Contains(body, "route registry could not be opened") { t.Errorf("the failed registry read is not named on the page:\n%s", body) } } // A POST to the PAGE is the same 404 an unrouted path gives. A // "GET /fleet/architecture" mux pattern would answer 405 + Allow, which // distinguishes this route from a missing one to anyone who guesses the URL // — the same hole the refresh door is registered bare to avoid. func TestArchitecturePageRefusesOtherMethods(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } _, _, ghost := postForm(t, newBrowser(t), fx.ts.URL+"/this-route-does-not-exist", url.Values{}) st, _, body := postForm(t, c, fx.ts.URL+"/fleet/architecture", url.Values{}) if st != http.StatusNotFound || body != ghost { t.Fatalf("POST on the page = %d %q, want the same 404 as a missing route", st, body) } } // archBrokenFleetStore fails exactly the one read the box list comes from, // leaving every other read on the page working — the shape of a real bad // moment, not a dead bucket. type archBrokenFleetStore struct{ release.Store } func (s archBrokenFleetStore) Get(ctx context.Context, key string) ([]byte, string, error) { if key == "status/fleet.json" { return nil, "", errors.New("bucket having a bad moment") } return s.Store.Get(ctx, key) } // An empty box list has two causes and the page must not confuse them. With // the fleet read broken, "No box has reported" would be a confident negative // claim about an estate nobody managed to look at. func TestArchitectureUnreadBoxesIsNotAnEmptyEstate(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } // First, the honest empty state: nothing broken, no box reporting. if st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture"); st != 200 || !strings.Contains(body, "No box has reported to this deployment yet") { t.Fatalf("a genuinely empty fleet = %d, want the empty-state sentence:\n%s", st, body) } fx.srv.cfg.Releases = archBrokenFleetStore{fx.rel} st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture") if st != 200 { t.Fatalf("= %d, want 200 — a failed fleet read costs its section, not the page", st) } if strings.Contains(body, "No box has reported to this deployment yet") { t.Errorf("the page claims an empty estate it failed to read:\n%s", body) } if !strings.Contains(body, "The box list could not be read") { t.Errorf("the page must say the box list is unread:\n%s", body) } if !strings.Contains(body, "published fleet status could not be read") { t.Errorf("the failed read must also be named at the foot:\n%s", body) } } // A box card with real content in it: what the estate says the machine is, // what the rate card says it costs, the apps and counts CARLOS reports, and // the drift chip where the two disagree. Without this every field inside // {{range .Boxes}} is unexercised — the fixture's fleet is empty. func TestArchitectureBoxCardShowsEstateAndDrift(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } ctx := context.Background() // The box CARLOS reports: two static routes, one resident, running a // known build, carrying the fixture's own kass route. if err := status.WriteBox(ctx, fx.rel, status.Box{ GeneratedAt: time.Now(), Label: "ada-stargazing", Region: "eu-west-1", Routes: status.RouteCounts{Static: 2, Resident: 1}, Metrics: &status.BoxMetrics{ Release: "v9.9.9-test", RouteVersions: map[string]string{"kass.carlosapp.dev": "abc123"}, }, }); err != nil { t.Fatal(err) } reg, err := registry.Open(fx.regPath) if err != nil { t.Fatal(err) } routes, err := reg.List() if err != nil { t.Fatal(err) } f, err := status.BuildFleet(ctx, fx.rel, routes, time.Now(), 5*time.Minute) if err != nil { t.Fatal(err) } if err := status.PublishFleet(ctx, fx.rel, f); err != nil { t.Fatal(err) } // The rate card prices this box explicitly at 30 GB of EBS... sc := economics.DefaultScenario() sc.Name = "test" sc.RateCard.Boxes["ada-stargazing"] = economics.BoxSpec{HourUSD: 0.02, Cores: 2, MemGB: 4, EBSGB: 30} putStoreJSON(t, fx.rel, economics.ScenarioKey("test"), sc) putStoreJSON(t, fx.rel, economics.ActiveKey, economics.Active{Schema: 1, Name: "test"}) // ...while the estate has 40 GB attached to the machine behind it. fx.srv.inventory = func(context.Context) (infraview.Inventory, error) { return infraview.Inventory{Provider: "aws", Account: "1234", TakenAt: time.Now().UTC(), Resources: []infraview.Resource{ {Provider: "aws", Kind: infraview.KindMachine, ID: "i-123", Name: "ada-stargazing", Region: "eu-west-1", Attrs: map[string]string{"type": "t4g.medium", "state": "running"}}, {Provider: "aws", Kind: infraview.KindDisk, ID: "vol-1", Attrs: map[string]string{"size_gb": "40", "attached_to": "i-123"}}, }}, nil } if err := fx.srv.infraPass(ctx); err != nil { t.Fatal(err) } st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture") if st != 200 { t.Fatalf("= %d, want 200", st) } for _, want := range []string{ "ada-stargazing", // the card is there at all "eu-west-1", // region "t4g.medium", // discovered instance type "running", // discovered state "2 cores", // rate-card cores (spec §7: cores/memory) "4 GB", // rate-card memory "40 GB disk", // discovered disk "/mo", // rate-card price "kass", // the app, joined through the registry "static 2", // fleet counts "resident 1", // "v9.9.9-test", // the release the box reports "chip chip-stale", // drift wears the fact-to-watch chip "rate card says 30 GB of EB", // and says what disagrees "the estate has 40 GB", // } { if !strings.Contains(body, want) { t.Errorf("box card is missing %q in:\n%s", want, body) } } // A box the rate card names explicitly must NOT also report the // priced-by-the-default-spec drift. if strings.Contains(body, "priced by the default spec") { t.Errorf("an explicitly priced box reported default-spec drift:\n%s", body) } } // The counts come from the published fleet aggregate and nowhere else. A box // whose own snapshot is on the bucket but which the last fleet merge did not // describe has NO counts — and "static 0 · resident 0 · hibernating 0" would // be three confident claims about a read that never happened, printed beside // an awake/asleep that correctly says it does not know. func TestArchitectureBoxWithoutAFleetRowSaysCountsUnknown(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } // Written, but never merged into status/fleet.json: the box exists in the // host table alone. if err := status.WriteBox(context.Background(), fx.rel, status.Box{ GeneratedAt: time.Now(), Label: "grace-unmerged", Region: "eu-west-1", Routes: status.RouteCounts{Static: 2, Resident: 1}, Metrics: &status.BoxMetrics{Release: "v9.9.9-test"}, }); err != nil { t.Fatal(err) } st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture") if st != 200 { t.Fatalf("= %d, want 200", st) } if !strings.Contains(body, "grace-unmerged") { t.Fatalf("the host-table box has no card:\n%s", body) } if !strings.Contains(body, "counts unknown") { t.Errorf("a box the fleet never described must say its counts are unknown:\n%s", body) } if strings.Contains(body, "static 0") || strings.Contains(body, "hibernating 0") { t.Errorf("the page states a count for a read that never happened:\n%s", body) } }