package console import ( "context" "encoding/json" "fmt" "net/http" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // seedLayoutBox writes a box snapshot carrying a layout report at the given // store and label — the console-side fixture for a door that reads // status.LoadBoxes AND the customer-fleet boxes apisystemupdate.go's // systemBoxes already knows how to find (amendment R15, R21). func seedLayoutBox(t *testing.T, store release.Store, label string, at time.Time, report *status.LayoutReport) { t.Helper() b := status.CollectBox(nil, status.Probes{Release: "v1", Layout: report}, at) b.Label = label if err := status.WriteBox(context.Background(), store, b); err != nil { t.Fatal(err) } } // TestAPIStatusLayoutGate: operator only, the identical posture // handleAPIStatusInstances has (apistatusinstances.go:91-104) — a // non-operator gets the byte-identical 404 an unknown route gives, so the // refusal names neither the rows nor whether the caller holds the bit. func TestAPIStatusLayoutGate(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-apistatuslayout") ghostStatus, ghostBody := getBearer(t, fx.ts.URL+"/this-api-route-does-not-exist", token) if status, _ := getBearer(t, fx.ts.URL+"/api/cli/status/layout", ""); status != http.StatusUnauthorized { t.Fatalf("unauthenticated = %d; want the CLI door's 401", status) } if status, body := getBearer(t, fx.ts.URL+"/api/cli/status/layout", token); status != ghostStatus || string(body) != string(ghostBody) { t.Fatalf("non-operator = %d %q; want the ghost 404 (%d %q)", status, body, ghostStatus, ghostBody) } if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } status, body := getBearer(t, fx.ts.URL+"/api/cli/status/layout", token) if status != http.StatusOK { t.Fatalf("operator = %d %q", status, body) } var out struct { Boxes []apiStatusLayoutBox `json:"boxes"` } if err := json.Unmarshal(body, &out); err != nil { t.Fatalf("unreadable: %v: %s", err, body) } if out.Boxes == nil { t.Error("boxes decoded as null; want an empty array") } } // TestAPIStatusLayoutListsBoxesRowsAndStale pins the door's shape: one // entry per box that has reported a layout, its rows, and Stale computed // from status.BoxStale the same way every other private reader gets it — // plus a box registered to a customer fleet, read from // fleets//status/boxes/ the way apisystemupdate.go's systemBoxes // already does (R15, R21). func TestAPIStatusLayoutListsBoxesRowsAndStale(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-apistatuslayoutrows") if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } now := time.Now() seedLayoutBox(t, fx.rel, "fresh-box", now, &status.LayoutReport{ Mode: "nested", Default: true, Rows: map[string]status.RowLayout{ "a.example.com": {Layout: "nested"}, "b.example.com": {Layout: "foreign", Reason: "db and socket are neither default"}, }, }) // Stale: past staleFactor(3)*SnapshotPeriod(5m) = 15 minutes dark. seedLayoutBox(t, fx.rel, "stale-box", now.Add(-20*time.Minute), &status.LayoutReport{ Mode: "flat", DefaultBlocked: "isolation not ready: probe fixture absent", Rows: map[string]status.RowLayout{"c.example.com": {Layout: "flat"}}, }) // A customer-fleet box: registered to the registry's fleet table, and // its own snapshot written under the fleet's prefix — the shape // systemBoxes reads, distinct from status/boxes/ (finding S14). // A private handle, not openRegistry's shared one: since #208 the // console's handle is process-owned and must not be closed by a caller. reg, err := registry.Open(fx.regPath) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) if err := reg.UpsertFleet(registry.Fleet{Name: "pi", Account: fx.sqid, Bucket: "pi-bucket"}); err != nil { t.Fatal(err) } fleetStore := release.NewPrefixStore(fx.rel, "fleets/pi/") seedLayoutBox(t, fleetStore, "fleet-box", now, &status.LayoutReport{ Mode: "flat", Rows: map[string]status.RowLayout{"d.example.com": {Layout: "flat"}}, }) status, body := getBearer(t, fx.ts.URL+"/api/cli/status/layout", token) if status != http.StatusOK { t.Fatalf("GET /api/cli/status/layout = %d %q", status, body) } var out struct { Boxes []apiStatusLayoutBox `json:"boxes"` } if err := json.Unmarshal(body, &out); err != nil { t.Fatalf("unreadable: %v: %s", err, body) } if len(out.Boxes) != 3 { t.Fatalf("boxes = %+v, want fresh-box, stale-box and fleet-box", out.Boxes) } byLabel := map[string]apiStatusLayoutBox{} for _, b := range out.Boxes { byLabel[b.Box] = b } fresh := byLabel["fresh-box"] if !fresh.Default || fresh.DefaultBlocked != "" { t.Errorf("fresh-box = %+v, want Default carried through from the snapshot", fresh) } if stale := byLabel["stale-box"]; stale.Default || stale.DefaultBlocked != "isolation not ready: probe fixture absent" { t.Errorf("stale-box = %+v, want DefaultBlocked carried through", stale) } if !fresh.Mixed { t.Errorf("fresh-box has a nested row beside a foreign one (uid carlos, no namespace) and is not marked mixed: %+v", fresh) } if fresh.Mode != "nested" || fresh.Stale { t.Errorf("fresh-box = %+v", fresh) } if len(fresh.Rows) != 2 { t.Fatalf("fresh-box rows = %+v", fresh.Rows) } byHost := map[string]apiStatusLayoutRow{} for _, r := range fresh.Rows { byHost[r.Host] = r } if byHost["a.example.com"].Layout != "nested" || byHost["a.example.com"].Reason != "" { t.Errorf("a.example.com = %+v", byHost["a.example.com"]) } if byHost["b.example.com"].Layout != "foreign" || byHost["b.example.com"].Reason == "" { t.Errorf("b.example.com = %+v", byHost["b.example.com"]) } stale := byLabel["stale-box"] if stale.Mode != "flat" || !stale.Stale { t.Errorf("stale-box = %+v, want Stale true", stale) } fleetBox := byLabel["fleet-box"] if fleetBox.Mode != "flat" || len(fleetBox.Rows) != 1 || fleetBox.Rows[0].Host != "d.example.com" { t.Errorf("fleet-box = %+v, want the fleet-prefixed snapshot to surface here", fleetBox) } } // layoutMixed is the warning's rule: a nested row beside one that runs as // uid carlos with no namespace (flat, foreign or external). A box that is // all nested, or has no nested row, is not mixed. func TestLayoutMixedNeedsANestedAndAFlatRow(t *testing.T) { rows := func(layouts ...string) []apiStatusLayoutRow { var out []apiStatusLayoutRow for i, l := range layouts { out = append(out, apiStatusLayoutRow{Host: fmt.Sprintf("h%d.example.com", i), Layout: l}) } return out } for _, tc := range []struct { layouts []string want bool }{ {[]string{"nested", "flat"}, true}, {[]string{"flat", "fenced", "nested"}, true}, {[]string{"nested", "nested"}, false}, {[]string{"flat", "flat"}, false}, {[]string{"nested", "foreign"}, true}, {[]string{"nested", "external"}, true}, {[]string{"nested", "fenced", "conflict"}, false}, {[]string{"foreign", "external", "flat"}, false}, {nil, false}, } { if got := layoutMixed("flat", rows(tc.layouts...)); got != tc.want { t.Errorf("layoutMixed(flat, %v) = %v, want %v", tc.layouts, got, tc.want) } } // Under a nested intent an unisolated row is mixed with or without a // nested sibling; an all-nested or empty box is not. for _, tc := range []struct { layouts []string want bool }{ {[]string{"foreign", "external"}, true}, {[]string{"flat"}, true}, {[]string{"nested", "nested"}, false}, {[]string{"fenced"}, false}, {nil, false}, } { if got := layoutMixed("nested", rows(tc.layouts...)); got != tc.want { t.Errorf("layoutMixed(nested, %v) = %v, want %v", tc.layouts, got, tc.want) } } }