package console // The account-level Fleet tab (accountfleet.go): GET /a/{sqid}/fleet, a // MEMBER-readable page that answers "where does this account's work run?" in // two halves — the shared platform fleet, presented purely from the public // status/fleet.json aggregate, and the account's own fleets from the // registry. The properties that matter, in the order they'd hurt if lost: // the page never 500s because one of its three independent reads had a bad // moment (the loadFleetPage/overview.go posture), it never shows another // account's fleets, and the shared card's "Active" claim tracks placement — // an account whose every route sits on its own fleet must not be told its // apps run on the shared one. import ( "context" "errors" "net/http" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/status" ) // seedSharedBoxes publishes a status/fleet.json carrying one box per region // name given, through the same BuildFleet/PublishFleet path production uses // (seedFleetJSON's shape, with the regions the tab renders as chips). func seedSharedBoxes(t *testing.T, fx *consoleFixture, regions ...string) { t.Helper() ctx := context.Background() for i, region := range regions { if err := status.WriteBox(ctx, fx.rel, status.Box{ GeneratedAt: time.Now(), Label: []string{"ada-stargazing", "grace-moonlit", "alan-seaside"}[i], Region: region, }); 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) } } // fixtureAccount is the account value the assemble-level tests pass in — the // same {ID, Sqid} pair the gated handler would hand it. func fixtureAccount(fx *consoleFixture) *Account { return &Account{ID: OpsAccountID, Sqid: fx.sqid, Name: "ops"} } // TestAccountFleetTabMemberReadable: the tab is a READ, so a plain member // sees it exactly as the owner does; a non-member gets the account gate's // byte-identical 404, the same answer an unrouted URL gives. func TestAccountFleetTabMemberReadable(t *testing.T) { fx, token := fleetFixture(t, "code-acctfleet-gate") if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/new", map[string]string{"name": "acme", "bucket": "acme-data"}, token); st != http.StatusOK { t.Fatalf("create fleet = %d %v", st, body) } if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil { t.Fatal(err) } owner := newBrowser(t) signUp(t, fx, owner, "code-acctfleet-owner") st, _, body := get(t, owner, fx.u("/fleet")) if st != http.StatusOK { t.Fatalf("owner GET /fleet = %d, want 200:\n%s", st, body) } for _, want := range []string{"Shared fleet", "Your fleets", "acme", "Bring your own fleet"} { if !strings.Contains(body, want) { t.Fatalf("owner fleet tab missing %q in:\n%s", want, body) } } fx.setUser("sam@keymail.dev") member := newBrowser(t) signUp(t, fx, member, "code-acctfleet-member") st, _, memberBody := get(t, member, fx.u("/fleet")) if st != http.StatusOK { t.Fatalf("member GET /fleet = %d, want 200:\n%s", st, memberBody) } for _, want := range []string{"Shared fleet", "acme"} { if !strings.Contains(memberBody, want) { t.Fatalf("member fleet tab missing %q in:\n%s", want, memberBody) } } fx.setUser("bob@keymail.dev") stranger := newBrowser(t) signUp(t, fx, stranger, "code-acctfleet-bob") nonMemberStatus, _, nonMemberBody := get(t, stranger, fx.u("/fleet")) ghostStatus, _, ghostBody := get(t, stranger, fx.ts.URL+"/definitely-not-a-page") if nonMemberStatus != http.StatusNotFound || nonMemberStatus != ghostStatus { t.Fatalf("non-member fleet tab = %d, unrouted page = %d; want both 404", nonMemberStatus, ghostStatus) } if nonMemberBody != ghostBody { t.Fatalf("non-member fleet tab body differs from an unrouted page's:\n%q\n%q", nonMemberBody, ghostBody) } } // TestAccountFleetTabNavLink: the tab is reachable — every account page // carries the Fleet nav entry, and it marks itself current on this one. func TestAccountFleetTabNavLink(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) // Nav LINKS carry the preferred segment — the account's name, "ops" for // the fixture's built-in account — while forms keep posting to the sqid // path (accountPage: Nav vs Base). st, _, apps := get(t, c, fx.u("/")) if st != http.StatusOK { t.Fatalf("account home = %d:\n%s", st, apps) } if !strings.Contains(apps, `href="/a/ops/fleet"`) { t.Fatalf("account home nav has no Fleet link:\n%s", apps) } st, _, body := get(t, c, fx.u("/fleet")) if st != http.StatusOK { t.Fatalf("GET /fleet = %d:\n%s", st, body) } if !strings.Contains(body, `href="/a/ops/fleet" aria-current="page"`) { t.Fatalf("Fleet tab does not mark its own nav entry current:\n%s", body) } } // TestAccountFleetSharedCardFromFleetJSON: the shared card is a PRESENTATION // of the public aggregate — box count and the distinct regions, deduped and // sorted, straight off status/fleet.json. func TestAccountFleetSharedCardFromFleetJSON(t *testing.T) { fx := newConsoleFixture(t) seedSharedBoxes(t, fx, "UK · London", "EU · Ireland") card, _, _, warn := fx.srv.assembleAccountFleet(context.Background(), fixtureAccount(fx)) if warn != "" { t.Fatalf("warn = %q, want none on a healthy read", warn) } if card.Boxes != 2 { t.Fatalf("card.Boxes = %d, want 2", card.Boxes) } if len(card.Regions) != 2 || card.Regions[0] != "EU · Ireland" || card.Regions[1] != "UK · London" { t.Fatalf("card.Regions = %v, want both regions, sorted", card.Regions) } // And they reach the page. c := signInPaul(t, fx) st, _, body := get(t, c, fx.u("/fleet")) if st != http.StatusOK { t.Fatalf("GET /fleet = %d:\n%s", st, body) } for _, want := range []string{"EU · Ireland", "UK · London", "2 box"} { if !strings.Contains(body, want) { t.Fatalf("fleet tab missing %q in:\n%s", want, body) } } } // TestAccountFleetActiveFlipsWithPlacement: "Active" is a claim about THIS // account's routes — true while any of its apps still runs on the shared // fleet, false once every one of them is placed on a fleet of its own. An // account told "your apps run here" when they do not is the failure this // test exists to prevent. func TestAccountFleetActiveFlipsWithPlacement(t *testing.T) { fx, token := fleetFixture(t, "code-acctfleet-active") reg := openTestReg(t, fx) if err := reg.Upsert(registry.Route{ Host: "kass.example.dev", Kind: "instance", App: "kass", Account: fx.sqid, }); err != nil { t.Fatal(err) } acct := fixtureAccount(fx) card, _, _, _ := fx.srv.assembleAccountFleet(context.Background(), acct) if !card.Active || card.PlatformApps != 1 { t.Fatalf("unplaced route: Active=%v PlatformApps=%d, want true/1", card.Active, card.PlatformApps) } if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/new", map[string]string{"name": "acme", "bucket": "acme-data"}, token); st != http.StatusOK { t.Fatalf("create fleet = %d %v", st, body) } if st, body := postJSON(t, placeURL(fx, "kass"), map[string]string{"target": "fleet/acme"}, token); st != http.StatusOK { t.Fatalf("place = %d %v, want 200", st, body) } card, own, _, _ := fx.srv.assembleAccountFleet(context.Background(), acct) if card.Active || card.PlatformApps != 0 { t.Fatalf("every route placed: Active=%v PlatformApps=%d, want false/0", card.Active, card.PlatformApps) } if len(own) != 1 || own[0].Apps != 1 { t.Fatalf("own fleets = %+v, want acme carrying the one placed app", own) } } // TestAccountFleetListsOnlyOwnFleets: fleet names are one flat cross-account // namespace (fleets.go's package doc), so the index MUST filter by owner — // a fleet belonging to another account must not appear here any more than it // resolves on the detail page. func TestAccountFleetListsOnlyOwnFleets(t *testing.T) { fx, token := fleetFixture(t, "code-acctfleet-own") reg := openTestReg(t, fx) if _, err := reg.CreateFleet(registry.Fleet{Name: "stranger", Account: "zzz", Bucket: "not-yours"}); err != nil { t.Fatal(err) } for _, name := range []string{"acme", "beta"} { if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/new", map[string]string{"name": name, "bucket": name + "-data"}, token); st != http.StatusOK { t.Fatalf("create fleet %s = %d %v", name, st, body) } } if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/acme/boxes", map[string]string{"label": "pi-1"}, token); st != http.StatusOK { t.Fatalf("add-box = %d %v", st, body) } writeFleetHeartbeat(t, fx, "acme", status.Box{GeneratedAt: time.Now(), Label: "pi-1"}) _, own, ownKnown, warn := fx.srv.assembleAccountFleet(context.Background(), fixtureAccount(fx)) if warn != "" { t.Fatalf("warn = %q, want none", warn) } if !ownKnown { t.Fatalf("fleetsKnown = false on a healthy registry read") } if len(own) != 2 || own[0].Name != "acme" || own[1].Name != "beta" { t.Fatalf("own fleets = %+v, want exactly acme and beta", own) } if own[0].Boxes != 1 || own[0].Reachable != 1 { t.Fatalf("acme = %+v, want 1 box, 1 reachable", own[0]) } if own[1].Boxes != 0 || own[1].Reachable != 0 { t.Fatalf("beta = %+v, want no boxes", own[1]) } c := signInPaul(t, fx) st, _, body := get(t, c, fx.u("/fleet")) if st != http.StatusOK { t.Fatalf("GET /fleet = %d:\n%s", st, body) } if strings.Contains(body, "stranger") { t.Fatalf("another account's fleet leaked onto the tab:\n%s", body) } for _, want := range []string{`href="/a/ops/fleets/acme"`, `href="/a/ops/fleets/beta"`} { if !strings.Contains(body, want) { t.Fatalf("fleet tab missing link %q in:\n%s", want, body) } } } // TestAccountFleetDegradesWithoutSnapshot: a deployment whose status-lease // holder has never published status/fleet.json still serves the tab — the // shared card renders its empty state, and the account's own fleets (a // completely independent read) are unaffected. func TestAccountFleetDegradesWithoutSnapshot(t *testing.T) { fx, token := fleetFixture(t, "code-acctfleet-nosnapshot") if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/new", map[string]string{"name": "acme", "bucket": "acme-data"}, token); st != http.StatusOK { t.Fatalf("create fleet = %d %v", st, body) } c := signInPaul(t, fx) st, _, body := get(t, c, fx.u("/fleet")) if st != http.StatusOK { t.Fatalf("GET /fleet with no published snapshot = %d, want 200 (degrade, don't 500):\n%s", st, body) } if !strings.Contains(body, "No shared boxes are reporting right now.") { t.Fatalf("missing the shared card's empty state:\n%s", body) } if !strings.Contains(body, "acme") { t.Fatalf("the account's own fleets (an independent read) went missing too:\n%s", body) } } // TestAccountFleetUnreadableRegistryClaimsNothing: one failed registry read // takes out BOTH registry-fed sections, and neither may fall through to its // zero value and make a positive claim from it — not the card ("none of your // apps run here", about routes it never looked at) and not the index ("no // fleets of your own yet", about a list it never got). "Could not check" and // "checked, and no" are different sentences in both places. func TestAccountFleetUnreadableRegistryClaimsNothing(t *testing.T) { fx := newConsoleFixture(t) seedSharedBoxes(t, fx, "EU · Ireland") c := signInPaul(t, fx) // A path whose parent directory does not exist: registry.Open fails, and // a failed open is never cached (openRegistry, view.go). fx.srv.cfg.Registry = filepath.Join(t.TempDir(), "gone", "registry.db") st, _, body := get(t, c, fx.u("/fleet")) if st != http.StatusOK { t.Fatalf("GET /fleet with an unreadable registry = %d, want 200 (degrade, don't 500):\n%s", st, body) } if strings.Contains(body, "None of your apps run on the shared fleet") || strings.Contains(body, "Not used") { t.Fatalf("an unread registry rendered as a positive \"not used\" verdict:\n%s", body) } if !strings.Contains(body, "Whether your apps run here could not be read just now.") { t.Fatalf("missing the honest unknown-placement state:\n%s", body) } if strings.Contains(body, "No fleets of your own yet") { t.Fatalf("an unread registry rendered as a positive \"you have no fleets\" verdict:\n%s", body) } if !strings.Contains(body, "Your fleets could not be read just now.") { t.Fatalf("missing the honest unknown-fleets state:\n%s", body) } // The half that DID read is still shown. if !strings.Contains(body, "EU · Ireland") { t.Fatalf("the shared card's own (successful) read went missing:\n%s", body) } } // TestAccountFleetEmptyStatesAreClaimsOnlyWhenEarned is the positive half of // the test above: a HEALTHY registry that genuinely holds no fleets for this // account does get the flat "none" wording — the honest-unknown copy must not // have swallowed the ordinary empty state along with the lie. func TestAccountFleetEmptyStatesAreClaimsOnlyWhenEarned(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) st, _, body := get(t, c, fx.u("/fleet")) if st != http.StatusOK { t.Fatalf("GET /fleet = %d:\n%s", st, body) } if !strings.Contains(body, "No fleets of your own yet.") { t.Fatalf("an account with no fleets and a healthy registry lost its empty state:\n%s", body) } if strings.Contains(body, "could not be read just now") { t.Fatalf("a healthy read rendered as unknown:\n%s", body) } } // TestFleetBYOPageMemberReadable: the BYO instructions page is member- // readable, the same gate as the Fleet tab itself — a member deciding // whether to bring a box needs to read the recipe as much as the owner // does. It must carry the exact commands and env var an operator will // actually type/set, and the Hardening section the checklist lives under. func TestFleetBYOPageMemberReadable(t *testing.T) { fx := newConsoleFixture(t) c := signInPaul(t, fx) st, _, body := get(t, c, fx.u("/fleet/byo")) if st != http.StatusOK { t.Fatalf("GET /fleet/byo = %d, want 200:\n%s", st, body) } for _, want := range []string{"carlos fleets create", "CARLOS_TUNNEL_NAME", "Hardening"} { if !strings.Contains(body, want) { t.Fatalf("BYO page missing %q in:\n%s", want, body) } } // The primary env contract is bucketless: no platform-bucket var or // credential belongs there (the box reaches releases/heartbeats through // the console's edge with its tunnel token). CARLOS_DEPLOYMENT_BUCKET // still appears, but only inside the demoted legacy-direct-S3 note. if !strings.Contains(body, "Legacy direct-S3") { t.Fatalf("BYO page missing the legacy direct-S3 note in:\n%s", body) } } // TestAccountFleetDegradesOnStoreFailure: a bucket having a moment must cost // the shared card, never the page — the fleets index is registry-side and // keeps rendering, with a flash that says which half is unknown. func TestAccountFleetDegradesOnStoreFailure(t *testing.T) { fx, token := fleetFixture(t, "code-acctfleet-storefail") if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/new", map[string]string{"name": "acme", "bucket": "acme-data"}, token); st != http.StatusOK { t.Fatalf("create fleet = %d %v", st, body) } seedSharedBoxes(t, fx, "EU · Ireland") c := signInPaul(t, fx) // Only the aggregate itself is unreadable: the per-fleet heartbeat reads // still work, so a degraded shared card is the ONLY thing this costs. fx.srv.cfg.Releases = errOnKeyStore{Store: fx.rel, key: "status/fleet.json", err: errors.New("bucket having a moment")} st, _, body := get(t, c, fx.u("/fleet")) if st != http.StatusOK { t.Fatalf("GET /fleet with an unreadable store = %d, want 200 (degrade, don't 500):\n%s", st, body) } if !strings.Contains(body, "acme") { t.Fatalf("the registry-side fleets index went missing on a BUCKET failure:\n%s", body) } if !strings.Contains(body, "Could not read the shared fleet") { t.Fatalf("a failed shared-fleet read rendered as if nothing were wrong:\n%s", body) } if strings.Contains(body, "EU · Ireland") { t.Fatalf("a failed read still showed the last-known regions:\n%s", body) } }