package console // claim_invariant_fixtures_test.go is the fixture table TestClaimConsoleMutationInvariant // compares, exactly, with s.claimEntries.snapshot() on an enabled console. // // Every key is "route " or "worker ", spelled exactly as the // registrar records it. Every entry is a door (driven for real, foreign and // open), a refusal (an intentionally unsupported operation: the exact 409), a // read, or a nonApp entry. Reads and nonApp entries carry the reason they // cannot touch app-scoped state AND a probe (claimProbes, the // claim_invariant_probes_*.go tables): a valid authenticated request against a // staged resource, accepted only on the exact success status, with an effect // witness read from the world for every mutator. A rejected request is never // coverage. The probe census (claimProbeCensus) keeps the two tables in step. // // Doors drive account bab (release.OpsAccount, owned by the case's owner) // and its two enrolled apps: kass (shipped v1/v2, stable on v1, canary/x on // v2, two registry routes) and titogo (enrolled, nothing shipped). Paths are // literal so a door's request is exactly what a client sends. import ( "context" "encoding/json" "errors" "net/http" "net/url" "strconv" "strings" "testing" "time" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/dbdoor" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/relay" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // ---- table helpers ---------------------------------------------------------- func claimRead(reason string) claimFixture { return claimFixture{read: reason} } func claimNonApp(reason string) claimFixture { return claimFixture{nonApp: reason} } // claimRefs parses "bab/kass" (an enrolled or setup-created incarnation) and // "+bab/name" (a name the request itself creates). func claimRefs(specs ...string) []claimAppRef { out := make([]claimAppRef, 0, len(specs)) for _, s := range specs { fresh := strings.HasPrefix(s, "+") account, app, _ := strings.Cut(strings.TrimPrefix(s, "+"), "/") out = append(out, claimAppRef{Account: account, App: app, Fresh: fresh}) } return out } func claimAPI(method, path, body string) func(*testing.T, *claimCase) int { return func(t *testing.T, cx *claimCase) int { return cx.api(t, method, path, body) } } // claimAPIVar is claimAPI with "" in path or body replaced from cx.vars, // for names a setup only learns at runtime (a door key id, a proposal id). func claimAPIVar(method, path, body string) func(*testing.T, *claimCase) int { return func(t *testing.T, cx *claimCase) int { return cx.api(t, method, cx.expand(path), cx.expand(body)) } } func claimForm(path string, kv ...string) func(*testing.T, *claimCase) int { return func(t *testing.T, cx *claimCase) int { vals := make([]string, len(kv)) for i, v := range kv { vals[i] = cx.expand(v) } return cx.form(t, cx.expand(path), vals...) } } func claimWorker(name string) func(*testing.T, *claimCase) int { return func(t *testing.T, cx *claimCase) int { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() cx.srv.RunClaimWorker(ctx, name) return http.StatusOK } } // claimRefusedAPI is claimAPI for a refused fixture: the owner's request, // answered with its status and body. func claimRefusedAPI(method, path, body string) func(*testing.T, *claimCase) (int, string) { return func(t *testing.T, cx *claimCase) (int, string) { status, _, out := rawReq(t, method, cx.ts.URL+cx.expand(path), cx.bearer, cx.expand(body)) return status, out } } // claimRefusedForm is claimForm for a refused fixture: the signed-in // owner's form with the session's CSRF token (and any passkey step-up), // answered with its status and body. func claimRefusedForm(path string, kv ...string) func(*testing.T, *claimCase) (int, string) { return func(t *testing.T, cx *claimCase) (int, string) { v := url.Values{"csrf": {cx.csrf}} for i := 0; i+1 < len(kv); i += 2 { v.Add(kv[i], cx.expand(kv[i+1])) } status, _, out := postGated(t, cx.consoleFixture, cx.browser, cx.ts.URL+cx.expand(path), v) return status, out } } // claimLegacyAppTokenRevokeDoor is the valid authenticated request for a // token-revoke form at path: the signed-in owner (a member of bab, session // and CSRF as the browser has them) names the listing id of a live legacy app // token for bab/kass that the owner minted, and walks the step-up. A probe // without the session, the CSRF token or a real listing id is refused before // the mutation and so is not this fixture. // // check proves the mutation path was reached, not merely that a redirect // came back: after the open run the row must be gone from cli_tokens (the // hard DELETE ran), and after a foreign-exclusive run it must still be there. func claimLegacyAppTokenRevokeDoor(path string) *claimDoor { return &claimDoor{ apps: claimRefs("bab/kass"), setup: func(t *testing.T, cx *claimCase) { raw, err := cx.st.NewAppToken(OpsAccountID, "kass", claimFxOwner, "census legacy app token", time.Now()) if err != nil { t.Fatalf("stage legacy app token: %v", err) } cx.vars["apptok"] = hashToken(raw) if !cx.legacyAppTokenLive(t) { t.Fatal("the staged legacy app token is not in the owner's live listing") } }, do: claimForm(path, "id", ""), check: func(t *testing.T, cx *claimCase, open bool) string { live := cx.legacyAppTokenLive(t) switch { case open && live: return "the legacy app token is still live after the open run: the handler never reached the revoke" case !open && !live: return "the legacy app token was revoked while its app was closed under a foreign exclusive operation" } return "" }, } } // claimEmailStepUpDoor is the passkey step-up callback's door for a session // that signed in by email: the held action is the control-plane clear, which // writes titogo's row, and finish completes the step-up through the entry // under test with the emailed code or link token, returning the final status. // Each run is a fresh email session with a fresh step-up send budget: a // completed step-up stays fresh for StepUpWindow, and the per-address send // limit would otherwise answer the second run, so this door must be the // emailed step-up every time. func claimEmailStepUpDoor(finish func(t *testing.T, cx *claimCase, c *http.Client, code, link string) int) *claimDoor { return &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.mustForm(t, "/a/bab/apps/titogo/control-plane/set") cx.srv.consoleMail = &fakeConsoleMail{ready: true} cx.srv.challengeSent = make(chan struct{}, 4) }, do: func(t *testing.T, cx *claimCase) int { raw, err := cx.st.CreateSessionMethod(claimFxOwner, "", MethodEmail, time.Now()) if err != nil { t.Fatal(err) } c := claimBrowserWithCookie(t, cx, sessionCookie, raw) cx.srv.limits.mu.Lock() cx.srv.limits.stepUps = keyedWindow{} cx.srv.limits.mu.Unlock() m := cx.rec.mark() status, _, body := postForm(t, c, cx.ts.URL+"/a/bab/apps/titogo/control-plane/clear", url.Values{"csrf": {raw}}) if status != http.StatusOK || !strings.Contains(body, `action="/auth/email/stepup/code"`) { t.Fatalf("control-plane clear did not ask for an emailed confirmation: %d:\n%s", status, body) } for _, e := range cx.rec.since(m) { if !e.Control && (e.Unscoped || len(e.Scopes) != 0) { t.Fatalf("the held form wrote %s before the emailed step-up", e) } } select { case <-cx.srv.challengeSent: case <-time.After(10 * time.Second): t.Fatal("the step-up email was never sent") } sent := cx.srv.consoleMail.(*fakeConsoleMail).sentCopy() if len(sent) == 0 { t.Fatal("no step-up email reached the console mailer") } _, msg, _ := strings.Cut(sent[len(sent)-1], "|") cm, lm := stepUpCodeRe.FindStringSubmatch(msg), stepUpLinkRe.FindStringSubmatch(msg) if cm == nil || lm == nil { t.Fatalf("the step-up email has no code or link:\n%s", msg) } return finish(t, cx, c, cm[1], lm[1]) }, } } // legacyAppTokenLive reads the staged token's row from the owner's listing. func (cx *claimCase) legacyAppTokenLive(t *testing.T) bool { t.Helper() toks, err := cx.st.CLITokens(claimFxOwner) if err != nil { t.Fatal(err) } for _, tok := range toks { if tok.ID == cx.vars["apptok"] { return tok.IsApp() } } return false } // claimRetireDoor deletes one record-backed host as the owner. A delete the // console accepted (202) must name the exclusive deleting operation it owns, // with the host and incarnation as its durable intent; a closed foreign // operation is never accepted. The boxes then acknowledge that operation // (world staging, as readyClaims does for a create) and the real recovery // pass, which is what carries an owned delete on, runs until the record // delete has happened or the operation finished. func claimRetireDoor(path, host string) func(*testing.T, *claimCase) int { return func(t *testing.T, cx *claimCase) int { status, _, out := rawReq(t, http.MethodDelete, cx.ts.URL+path, cx.bearer, "") if status != http.StatusAccepted { return status } op := claimReplyField(t, out, "operation_id") claimReplyField(t, out, "phase") for range 3 { if !cx.ownsDelete(t, "bab", "titogo", op, host) { break } cx.ackClaim(t, host, op) claimWorker("StartClaimRecoveryPass")(t, cx) } return status } } // ownsDelete reports whether app's admission is still closed under op, an // exclusive deleting operation whose durable intent names host. Anything // else holding the admission after a 202 for op fails the case. func (cx *claimCase) ownsDelete(t *testing.T, account, app, op, host string) bool { t.Helper() a, err := hostops.ReadCanonicalAdmission(context.Background(), cx.rel, account, app) if err != nil { t.Fatal(err) } cur := a.Value.Op if cur == nil { return false } if cur.ID != op || cur.Kind != hostops.OpDeleting || cur.Intent["host"] != host || cur.Intent["incarnation"] == "" { t.Fatalf("delete answered 202 for operation %s, but %s/%s's admission holds %+v", op, account, app, cur) } return true } func (cx *claimCase) expand(s string) string { for k, v := range cx.vars { s = strings.ReplaceAll(s, "<"+k+">", v) } return s } // ---- staging (setup only; every one of these runs with apps open) ----------- // resignPlan turns the account's pointer digest marker on and keeps the // resign plan for kass's channel in vars, as `carlos channels resign` shows // it before the person confirms. func (cx *claimCase) resignPlan(t *testing.T, channel string) { t.Helper() ctx := context.Background() sg := release.RootSigner(cx.priv) if err := release.WriteDigestMarker(ctx, cx.rel, sg, "bab", claimFxOwner); err != nil { t.Fatalf("stage the digest marker: %v", err) } plan, err := sg.PlanResign(ctx, cx.rel, "bab", "kass", channel) if err != nil { t.Fatalf("stage a resign plan: %v", err) } hd, err := json.Marshal(plan.HistoryDigests) if err != nil { t.Fatal(err) } cx.vars["digest"], cx.vars["history_digests"] = plan.Digest, string(hd) } // authorizePlan deletes the promotion record kass's v1 promotion wrote and // keeps the authorize plan's digest in vars, as `carlos releases authorize` // shows it before the person confirms. func (cx *claimCase) authorizePlan(t *testing.T) { t.Helper() ctx := context.Background() if err := cx.rel.Delete(ctx, release.PromotionRecordKey("bab", "kass", "v1")); err != nil { t.Fatalf("clear kass v1's promotion record: %v", err) } plan, err := release.PlanAuthorize(ctx, cx.rel, "bab", "kass", "v1") if err != nil { t.Fatalf("stage an authorize plan: %v", err) } cx.vars["manifest_digest"] = plan.ManifestDigest } func (cx *claimCase) trash(t *testing.T, app string) { t.Helper() cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/"+app+"/delete", "", http.StatusOK) } func (cx *claimCase) temporary(t *testing.T, name string) { t.Helper() cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/new-temporary", `{"name":"`+name+`","ttl_seconds":86400}`, http.StatusOK) } func (cx *claimCase) fleet(t *testing.T) { t.Helper() cx.mustAPI(t, http.MethodPost, "/api/cli/fleets/bab/new", `{"name":"pi","bucket":"pi-fleet-data"}`, http.StatusOK) } // instanceWorld writes titogo's instances manifest (pool tito, region ie, // which the actor world has an active box for). func (cx *claimCase) instanceWorld(t *testing.T) { t.Helper() enableInstances(t, cx.consoleFixture, "titogo", titoManifest()) } // heldInstance is the host-retry door's world: one instance whose box reports // it held back by a failed build. func (cx *claimCase) heldInstance(t *testing.T, host string) { t.Helper() cx.instanceWorld(t) cx.instance(t, `{"host":"`+host+`","region":"ie","channel":"edge"}`) box := status.CollectBox(nil, status.Probes{MachineID: "claim-box"}, time.Now()) box.Metrics = &status.BoxMetrics{Rollout: &status.RolloutReport{Hosts: map[string]status.HostRollout{ host: {Account: "bab", App: "titogo", Installed: "v2", Serving: "v1", Good: "v1", Awake: true, HeldBy: "v2", Reason: status.ReasonHealth, HeldEpoch: 3, HeldBook: "claim-fixture-book", RetryV: status.HostRetryV}, }}} if err := status.WriteBox(context.Background(), cx.rel, box); err != nil { t.Fatal(err) } } func (cx *claimCase) instance(t *testing.T, body string) { t.Helper() cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/instances", body) cx.readyClaims(t) } // movableInstance is the move door's world: an unpooled, unregioned // manifest, one instance in ie, and a uk edge with a staging bucket. func (cx *claimCase) movableInstance(t *testing.T) { t.Helper() m := titoManifest() m.Regions = nil m.Pool = "" enableInstances(t, cx.consoleFixture, "titogo", m) cx.instance(t, `{"host":"acme.go.tito.io","region":"ie"}`) if _, err := cx.rel.Put(context.Background(), "control/edges/uk-box.json", []byte(`{"v":1,"box":"uk-box","eip":"1.2.3.4","placement":"uk","data_bucket":"data-uk","data_bucket_region":"eu-west-2","hosts":{}}`), ""); err != nil { t.Fatal(err) } } func (cx *claimCase) sidecarWorld(t *testing.T) { t.Helper() enableSidecars(t, cx.consoleFixture, "titogo", titoSidecarManifest()) } func (cx *claimCase) sidecar(t *testing.T) { t.Helper() cx.sidecarWorld(t) cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/sidecars", `{"name":"bot","region":"ie"}`) cx.readyClaims(t) } const claimMailDomain = "titogo.bab.oncarlos.com" func (cx *claimCase) emailDeclared(t *testing.T) { t.Helper() declareEmail(t, cx.consoleFixture, cx.bearer, "titogo", "hello@"+claimMailDomain) } func (cx *claimCase) emailDomainEnsured(t *testing.T) { t.Helper() cx.emailDeclared(t) cx.mailFake.StatusOnEnsure.DKIM, cx.mailFake.StatusOnEnsure.MailFrom, cx.mailFake.StatusOnEnsure.Sending = "SUCCESS", "SUCCESS", "SUCCESS" ensureEmailDomain(t, cx.consoleFixture, cx.bearer, "titogo", claimMailDomain) } func (cx *claimCase) emailProvisioned(t *testing.T) { t.Helper() provisionEmail(t, cx.consoleFixture, cx.mailFake, cx.bearer, "titogo", claimMailDomain, "hello@"+claimMailDomain) } func (cx *claimCase) emailPush(t *testing.T) { t.Helper() cx.mustAPI(t, http.MethodPut, "/api/cli/apps/bab/titogo/email/events/push", `{"path":"/mail/events"}`, http.StatusOK) } func (cx *claimCase) storeGranted(t *testing.T) { t.Helper() grantStore(t, cx.consoleFixture, cx.bearer) } func (cx *claimCase) scanRequested(t *testing.T) { t.Helper() cx.storeGranted(t) enableDeploymentScanning(t, cx.consoleFixture) requestScanning(t, cx.consoleFixture, cx.bearer, false) } func (cx *claimCase) scanGranted(t *testing.T) { t.Helper() cx.scanRequested(t) cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/store/scan/grant", `{}`, http.StatusOK) } // scanObjectStaged is scanGranted with claim-fixture.txt present in the // granted bucket, the object claimOneObjectLister lists. func (cx *claimCase) scanObjectStaged(t *testing.T) { t.Helper() cx.scanGranted(t) rec, _, err := objstore.Get(context.Background(), cx.rel, "bab", "titogo") if err != nil || rec == nil || rec.Provisioned == nil { t.Fatalf("granted store record %+v %v", rec, err) } cx.scanFake.PutObject(rec.Provisioned.Bucket, "claim-fixture.txt", `"claim-fixture"`, time.Now().Add(-time.Hour), 3) } // scanQueued stages one on-demand item for the drain to read. func (cx *claimCase) scanQueued(t *testing.T) { t.Helper() cx.scanObjectStaged(t) cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/store/scan/object", `{"key":"claim-fixture.txt"}`, http.StatusAccepted) } // scanMasterRotated replaces the deployment's callback master, so every // granted app is behind the current key id. func (cx *claimCase) scanMasterRotated(t *testing.T) { t.Helper() cx.scanGranted(t) cx.srv.cfg.CallbackSigner = callbackTestSigner(t, "claim-fixture-webhook-master-2", "claim-fixture-webhook-master") } // relayArmed stages one armed region with a loaded master and takes the // writer lease, as armRelay does for the flag-off fixtures. func (cx *claimCase) relayArmed(t *testing.T, masters string) { t.Helper() t.Setenv(relayMastersEnv("eu-west-1"), masters) if err := cx.srv.relayAcquire(context.Background()); err != nil { t.Fatalf("relay writer lease: %v", err) } } // relayStageMaster stages one more master for eu-west-1 (operator env) // and a live relay heartbeat that has loaded every staged kid (box side), // so a rotate has a new master to move onto. func (cx *claimCase) relayStageMaster(t *testing.T) { t.Helper() n, _ := strconv.Atoi(cx.vars["relay-masters"]) if n == 0 { n = 1 } n++ cx.vars["relay-masters"] = strconv.Itoa(n) masters, kids := make([]string, 0, n), make([]string, 0, n) for i := 1; i <= n; i++ { kid := "k" + strconv.Itoa(i) masters = append(masters, kid+":"+relayTestMaster(byte(i))) kids = append(kids, kid) } t.Setenv(relayMastersEnv("eu-west-1"), strings.Join(masters, ",")) seedHeartbeat(t, cx.consoleFixture, "box1", "eu-west-1", time.Minute, kids...) } func (cx *claimCase) relayEnabled(t *testing.T) { t.Helper() cx.relayArmed(t, "k1:"+relayTestMaster(1)) cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/relay", `{}`, http.StatusOK) } // proposal stages a guarded pipeline edit on titogo and records its id. func (cx *claimCase) proposal(t *testing.T) { t.Helper() setPipeline(t, cx.consoleFixture, "titogo", release.PipelineChannel{Name: "edge"}, release.PipelineChannel{Name: "production", Bake: "24h", Rules: release.ChannelRules{ChangeApprovals: 1}}) out := cx.mustAPI(t, http.MethodPut, "/api/cli/apps/bab/titogo/pipeline", `{"channels":[{"name":"edge"},{"name":"production","bake":"1h","rules":{"change_approvals":1}}]}`, http.StatusAccepted) cx.vars["pending"] = claimReplyField(t, out, "pending_id") } func (cx *claimCase) service(t *testing.T) { t.Helper() cx.mustAPI(t, http.MethodPost, "/api/cli/services/bab", `{"name":"svc-a","role":"publish","app":"titogo"}`, http.StatusOK, http.StatusCreated) } // freshBrowser signs the owner in on a new session. func (cx *claimCase) freshBrowser(t *testing.T) *http.Client { t.Helper() cx.sessions++ c := newBrowser(t) signUp(t, cx.consoleFixture, c, "code-claim-session-"+strconv.Itoa(cx.sessions)+"-"+claimSafeName(t.Name())) return c } func (cx *claimCase) attached(t *testing.T, app, host, route string) { t.Helper() cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/"+app+"/domains", attachBody(host, route), http.StatusOK) } // claimAliasRoute is titogo's provisioned instance: on an enabled console an // alias binds a host claim's incarnation, so its target must be a claimed, // serving record, never a legacy registry-only route like kass's. const claimAliasRoute = "acme.go.tito.io" // claimedRoute provisions claimAliasRoute (claimed and serving: readyClaims) // and publishes its ready registry row, which the browser attach door // aliases to. func (cx *claimCase) claimedRoute(t *testing.T) { t.Helper() cx.instanceWorld(t) cx.instance(t, `{"host":"`+claimAliasRoute+`","region":"ie"}`) cx.addRoute(t, registry.Route{Host: claimAliasRoute, Kind: "instance", Status: "ready", App: "titogo", Account: "bab", Addr: "127.0.0.1:9201"}) } // claimedAlias attaches host to claimAliasRoute through the attach door and // settles the alias registration it took. func (cx *claimCase) claimedAlias(t *testing.T, host string) { t.Helper() cx.claimedRoute(t) cx.attached(t, "titogo", host, claimAliasRoute) cx.settleRegistrations(t, "bab", "titogo") } // settleRegistrations removes every registration of account/app whose // effects settled and whose claim it completed, as the recovery sweep does. func (cx *claimCase) settleRegistrations(t *testing.T, account, app string) { t.Helper() ctx := context.Background() admission, err := hostops.ReadCanonicalAdmission(ctx, cx.rel, account, app) if err != nil { t.Fatal(err) } for id := range admission.Value.Registrations { if err := cx.srv.cfg.ClaimAuthority.Control.SettleRegistration(ctx, account, app, admission.Value.AppID, id, time.Now()); err != nil { t.Fatalf("settle registration %s of %s/%s: %v", id, account, app, err) } } } // ackClaim stages every participant box's execution acknowledgement for // host's claim operation op (box side, below every boundary). A claim not // (yet) under op is left alone. func (cx *claimCase) ackClaim(t *testing.T, host, op string) { t.Helper() obs, err := hostclaim.Read(context.Background(), cx.rel, host) if err != nil { t.Fatal(err) } if obs.Absent || obs.Claim.Op.ID != op { return } cx.ackExecution(t, obs) } // ackExecution acknowledges obs's operation for each participant that has // not acknowledged it yet, and returns the updated observation. func (cx *claimCase) ackExecution(t *testing.T, obs hostclaim.Observed) hostclaim.Observed { t.Helper() ctx := context.Background() roster, err := cx.srv.cfg.ClaimAuthority.Directory.Read(ctx) if err != nil { t.Fatal(err) } for _, box := range obs.Claim.Op.Participants { if _, done := obs.Claim.Op.Journal["exec_ack."+box]; done { continue } op := obs.Claim.Op.ID ack := hostclaim.ExecutionAcknowledgement{Op: op, Enrollment: roster.Roster.Boxes[box].Enrollment, BootID: "fixture-boot", ControllerID: "fixture-controller", LeaseETag: "fixture-lease", Expires: time.Now().Add(time.Minute)} if obs, err = hostclaim.Update(ctx, cx.rel, obs, hostclaim.AcknowledgeExecution(box, ack)); err != nil { t.Fatalf("acknowledge %s for %s: %v", op, box, err) } } return obs } // stagedMove writes a requested move to uk onto the instance record (world // staging below the boundary), so the cut and abort refusals answer a // request that would otherwise be valid. func (cx *claimCase) stagedMove(t *testing.T, more ...func(*provision.Record) error) { t.Helper() cx.movableInstance(t) for _, mut := range append([]func(*provision.Record) error{provision.MoveRequest("uk", time.Now())}, more...) { if _, err := provision.SetRecordMove(context.Background(), cx.rel, "bab", "titogo", "acme.go.tito.io", mut); err != nil { t.Fatal(err) } } } // claimOfflineMoveID is the offline move offlineMove stages. const claimOfflineMoveID = "mv_zyxwvutsrqponmlkjihgfedcba" // offlineMove is movableInstance with an offline move from ie to uk walked // through the record's own transitions until it reaches state: the state a // door acts on, so its request is valid. func (cx *claimCase) offlineMove(t *testing.T, state string) { t.Helper() cx.movableInstance(t) now := time.Now() steps := []func(*provision.Record) error{provision.OfflineStart(claimOfflineMoveID, "ie", "uk", claimFxOwner, now)} switch state { case provision.OfflineStopping: case provision.OfflineSealed, provision.OfflineLanded, provision.OfflineOpen: steps = append(steps, provision.OfflineSeal(claimOfflineMoveID, "ie-box", []string{"ie-box"}), provision.OfflineSealedBy(claimOfflineMoveID, "ie-box", provision.MoveSource{})) if state != provision.OfflineSealed { steps = append(steps, provision.OfflineFlip(claimOfflineMoveID), provision.OfflineLandedBy(claimOfflineMoveID, "uk")) } if state == provision.OfflineOpen { steps = append(steps, provision.OfflineOpenTransition(claimOfflineMoveID, now)) } default: t.Fatalf("offlineMove: no staging for %s", state) } for _, mut := range steps { if _, err := provision.SetRecordMove(context.Background(), cx.rel, "bab", "titogo", "acme.go.tito.io", mut); err != nil { t.Fatal(err) } } rec, err := provision.GetRecord(context.Background(), cx.rel, "bab", "titogo", "acme.go.tito.io") if err != nil || rec.Move == nil || rec.Move.State != state { t.Fatalf("offlineMove: staged %+v (%v), want %s", rec, err, state) } } // claimSecondSqid is the second account the account-move door moves into. var claimSecondSqid = domains.Sqid(2) func (cx *claimCase) secondAccount(t *testing.T) { t.Helper() if err := cx.st.CreateAccount(2, claimSecondSqid, "second", claimFxOwner, time.Now()); err != nil { t.Fatal(err) } cx.addRoute(t, registry.Route{Host: "titogo.second.example", Kind: "instance", Status: "ready", App: "titogo", Account: claimSecondSqid, Addr: "127.0.0.1:9101"}) } // expireNow moves a temporary app's expiry into the past through the // test-only seed scope: the clock, not a door, is what the pass reacts to. func (cx *claimCase) expireNow(t *testing.T, app string) { t.Helper() appID, err := cx.st.ClaimAppID(OpsAccountID, app) if err != nil { t.Fatal(err) } scope := []claimSQLScope{{AccountID: OpsAccountID, App: app, AppID: appID, Mode: claimModeEffect}} err = cx.st.withClaimSQL(gateCtx(t), scope, allowAllClaimSQL, func(v *Store) error { _, err := v.db.Exec(`UPDATE apps SET expires_at = ? WHERE account_id = ? AND app = ?`, time.Now().Add(-time.Minute).Unix(), OpsAccountID, app) return err }) if err != nil { t.Fatal(err) } } // claimOKTransport records every outbound send before answering 200. type claimOKTransport struct{ rec *claimRecorder } func (tr claimOKTransport) RoundTrip(r *http.Request) (*http.Response, error) { tr.rec.add(claimRawEvent{Surface: "http", Method: r.Method, Key: r.URL.String(), Unscoped: true}) return &http.Response{StatusCode: http.StatusOK, Status: "200 OK", Header: http.Header{}, Body: http.NoBody, Request: r}, nil } // crashedRestore stages an issuer that saved its restore receipt and died // before the marker delete: the recovery pass owns finishing it. func (cx *claimCase) crashedRestore(t *testing.T) { t.Helper() ctx := context.Background() cx.trash(t, "titogo") appID := cx.appIDs[claimAppRef{Account: "bab", App: "titogo"}] ctl := cx.srv.cfg.ClaimAuthority.Control owner := hostops.RegistrationOwner{Console: claimFxConsole, Enrollment: claimFxConsoleEnrollment, Controller: "claim-fixture-crashed"} op := "claim-fixture-crashed-restore" h, err := ctl.BeginExclusive(ctx, "bab", "titogo", appID, hostops.AppOperation{ID: op, Kind: hostops.OpAppMutation, AppID: appID, Phase: "issued", Intent: map[string]string{"action": "restore", "started": strconv.FormatInt(time.Now().Add(-time.Hour).Unix(), 10)}}, owner) if err != nil { t.Fatalf("crashed restore: begin: %v", err) } if _, _, err := h.Runner(time.Now).Issue(ctx, hostops.Effect{ID: op, OperationID: op, AppID: appID, Resource: hostops.OpAppMutation, Action: "restore", Owner: owner, State: "prepared"}); err != nil { t.Fatalf("crashed restore: issue: %v", err) } tomb, err := release.ReadAppTombstone(ctx, cx.rel, "bab", "titogo") if err != nil || tomb == nil { t.Fatalf("crashed restore: marker %+v %v", tomb, err) } _, tag, err := cx.rel.Get(ctx, release.AppTombstoneKey("bab", "titogo")) if err != nil { t.Fatal(err) } if err := h.PrepareRestore(ctx, tag, tomb.DeletedAt.Unix()); err != nil { t.Fatalf("crashed restore: prepare: %v", err) } } // ---- the table ---------------------------------------------------------------- var claimFixtures = map[string]claimFixture{ // ---- methodless routes (the handler checks r.Method itself) ---- "route /api/economics/backhaul": claimRead("operator backhaul report (GET only) computed from the economics view; writes nothing"), "route /api/economics/bill": claimNonApp("operator bill import, stored under economics/; names no app"), "route /fleet/architecture": claimRead("renders the deployment map from the cached snapshot; refreshing it is its own door"), "route /fleet/architecture/refresh": claimNonApp("rebuilds the deployment map snapshot from box and edge files; no app object or row"), "route /fleet/economics/bill": claimNonApp("operator bill form, stored under economics/"), "route /fleet/economics/scenarios": claimNonApp("operator pricing scenarios, stored under economics/"), "route /fleet/economics/tier": claimNonApp("an account's economics tier: an account record, not an app"), // ---- DELETE ---- // Alias-only detach is intentionally unsupported (claimUnsupported); the // alias it names is a real one, bound to a claimed instance. "route DELETE /api/cli/apps/{account}/{app}/domains/{host}": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.claimedAlias(t, "tickets.example.com") }, do: claimRefusedAPI(http.MethodDelete, "/api/cli/apps/bab/titogo/domains/tickets.example.com", ""), status: http.StatusConflict, msg: claimRefusalText(errClaimAliasDetachUnsupported), }}, "route DELETE /api/cli/apps/{account}/{app}/email/credentials/{name}": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.emailProvisioned(t) cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/email/credentials", `{"name":"laptop"}`, http.StatusOK) }, do: claimAPI(http.MethodDelete, "/api/cli/apps/bab/titogo/email/credentials/laptop", ""), }}, "route DELETE /api/cli/apps/{account}/{app}/email/domains/{domain}": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/email/domains", `{"domain":"`+claimMailDomain+`"}`, http.StatusOK) }, do: claimAPI(http.MethodDelete, "/api/cli/apps/bab/titogo/email/domains/"+claimMailDomain, ""), }}, "route DELETE /api/cli/apps/{account}/{app}/email/events/push": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.emailPush(t) }, do: claimAPI(http.MethodDelete, "/api/cli/apps/bab/titogo/email/events/push", ""), }}, "route DELETE /api/cli/apps/{account}/{app}/instances/{host}": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.instanceWorld(t) cx.instance(t, `{"host":"acme.go.tito.io","region":"ie"}`) }, do: claimRetireDoor("/api/cli/apps/bab/titogo/instances/acme.go.tito.io", "acme.go.tito.io"), }}, // Abort of a requested move: instance moves are intentionally // unsupported (claimUnsupported). "route DELETE /api/cli/apps/{account}/{app}/instances/{host}/move": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.stagedMove(t) }, do: claimRefusedAPI(http.MethodDelete, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move", ""), status: http.StatusConflict, msg: claimMoveUnsupportedMsg, }}, "route DELETE /api/cli/apps/{account}/{app}/sidecars/{name}": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.sidecar(t) }, do: claimRetireDoor("/api/cli/apps/bab/titogo/sidecars/bot", provision.SidecarHost("bot", "titogo", "bab")), }}, // ---- GET: reads ---- "route GET /a/{sqid}/apps/new": claimRead("renders the new-app form"), "route GET /a/{sqid}/apps/{app}": claimRead("renders the app page from channels, releases and registry routes"), "route GET /a/{sqid}/apps/{app}/account-review": claimRead("renders the route-account review card"), "route GET /a/{sqid}/apps/{app}/activity": claimRead("renders the app's audit trail"), "route GET /a/{sqid}/apps/{app}/analytics": claimRead("renders analytics read from the bucket"), "route GET /a/{sqid}/apps/{app}/availability": claimRead("renders edge availability counts"), "route GET /a/{sqid}/apps/{app}/deployments": claimRead("renders deployments (the releases page)"), "route GET /a/{sqid}/apps/{app}/errors": claimRead("renders grouped app errors"), "route GET /a/{sqid}/apps/{app}/instances": claimRead("renders the app's instance records"), "route GET /a/{sqid}/apps/{app}/instances/{host}": claimRead("renders one instance and polls its report over HTTP"), "route GET /a/{sqid}/apps/{app}/logs": claimRead("renders the logs page"), "route GET /a/{sqid}/apps/{app}/logs/data": claimRead("returns log chunks as JSON"), "route GET /a/{sqid}/apps/{app}/pipeline": claimRead("renders the pipeline form"), "route GET /a/{sqid}/apps/{app}/releases": claimRead("renders releases"), "route GET /a/{sqid}/apps/{app}/settings": claimRead("renders app settings"), "route GET /a/{sqid}/apps/{app}/settings/{section}": claimRead("renders one app settings section"), "route GET /a/{sqid}/fleet": claimRead("renders the account's fleet tab"), "route GET /a/{sqid}/fleet/byo": claimRead("renders bring-your-own-box instructions"), "route GET /a/{sqid}/fleets/{fleet}": claimRead("renders one fleet's boxes and placements"), "route GET /a/{sqid}/settings": claimRead("renders account settings"), "route GET /a/{sqid}/team": claimRead("renders members and invites"), "route GET /a/{sqid}/trash": claimRead("lists trashed apps; purge is the trash/delete door"), "route GET /a/{sqid}/{$}": claimRead("renders the account home"), "route GET /accounts/new": claimRead("renders the new-account form"), "route GET /api/cli/apps": claimRead("lists the caller's apps"), "route GET /api/cli/apps/{account}/list": claimRead("lists an account's apps with lifecycle times"), "route GET /api/cli/apps/{account}/{app}/analytics": claimRead("reads analytics"), "route GET /api/cli/apps/{account}/{app}/availability": claimRead("reads availability counts"), "route GET /api/cli/apps/{account}/{app}/channels": claimRead("reads channel pointers"), "route GET /api/cli/apps/{account}/{app}/config": claimRead("reads app config metadata (never secret values)"), "route GET /api/cli/apps/{account}/{app}/db/{host}/ltx/{level}": claimRead("serves exact LTX versions through lease.VersionedStore reads only"), "route GET /api/cli/apps/{account}/{app}/domains": claimRead("lists domain claims"), "route GET /api/cli/apps/{account}/{app}/domains/{host}": claimRead("reads one domain claim and its DNS status"), "route GET /api/cli/apps/{account}/{app}/door-keys": claimRead("lists door keys"), "route GET /api/cli/apps/{account}/{app}/edge-refs/{code}": claimRead("reads one edge record and the mesh key to verify the code"), "route GET /api/cli/apps/{account}/{app}/email": claimRead("reads the email record"), "route GET /api/cli/apps/{account}/{app}/email/events": claimRead("reads mail events"), "route GET /api/cli/apps/{account}/{app}/email/events/push": claimRead("reads the push hook status"), "route GET /api/cli/apps/{account}/{app}/errors": claimRead("reads grouped errors"), "route GET /api/cli/apps/{account}/{app}/features": claimRead("reads feature flags"), "route GET /api/cli/apps/{account}/{app}/geo": claimRead("looks an address up in the loaded GeoIP database; the instance gate only counts the caller's budget in memory"), "route GET /api/cli/apps/{account}/{app}/instances": claimRead("lists instance records"), "route GET /api/cli/apps/{account}/{app}/instances/{host}": claimRead("reads one instance record"), "route GET /api/cli/apps/{account}/{app}/instances/{host}/move": claimRead("reads one instance's move state"), "route GET /api/cli/apps/{account}/{app}/ledgers": claimRead("lists ledgers"), "route GET /api/cli/apps/{account}/{app}/ledgers/{ledger}/head": claimRead("reads a ledger head"), "route GET /api/cli/apps/{account}/{app}/logs": claimRead("reads log chunks"), "route GET /api/cli/apps/{account}/{app}/pending": claimRead("lists pending proposals"), "route GET /api/cli/apps/{account}/{app}/pipeline": claimRead("reads the pipeline"), "route GET /api/cli/apps/{account}/{app}/regions": claimRead("reads armed regions"), "route GET /api/cli/apps/{account}/{app}/relay": claimRead("reads relay status and heartbeats"), "route GET /api/cli/apps/{account}/{app}/releases": claimRead("lists releases"), "route GET /api/cli/apps/{account}/{app}/releases/{version}/authorize": claimRead( "reads one build's manifest digests"), "route GET /api/cli/apps/{account}/{app}/retention": claimRead("reads the retention policy"), "route GET /api/cli/apps/{account}/{app}/routes": claimRead("lists registry routes"), "route GET /api/cli/apps/{account}/{app}/schedules": claimRead("lists schedules and their box status"), "route GET /api/cli/apps/{account}/{app}/sidecars": claimRead("lists sidecar records"), "route GET /api/cli/apps/{account}/{app}/sidecars/{name}": claimRead("reads one sidecar record"), "route GET /api/cli/apps/{account}/{app}/store": claimRead("reads the store record (never a secret)"), "route GET /api/cli/apps/{account}/{app}/store/scan": claimRead("reads scanning status"), "route GET /api/cli/apps/{account}/{app}/store/scan/object": claimRead( "reads one object's verdict from its tags or a fallback result; parks on an in-process channel, writes nothing"), "route GET /api/cli/apps/{account}/{app}/tags": claimRead("lists release tags"), "route GET /api/cli/apps/{account}/{app}/tags/{version}": claimRead("reads one tag"), "route GET /api/cli/apps/{account}/{app}/version-target": claimRead("reads the version target"), "route GET /api/cli/fleets/{account}": claimRead("lists an account's fleets"), "route GET /api/cli/services/{account}": claimRead("lists service credentials"), "route GET /api/cli/status/instances": claimRead("operator live tenancy view"), "route GET /api/cli/status/layout": claimRead("operator layout view"), "route GET /api/cli/store/approvals": claimRead("operator entitlement queue"), "route GET /api/cli/accounts/{account}/plan": claimRead("operator view of an account's plan and limits"), "route GET /api/cli/system/antivirus": claimRead("reads the deployment scanning setting"), "route GET /api/cli/system/geoip": claimRead("operator view of the GeoIP pointer, box heartbeats and feed status (control/geoip/, status/, edge files); writes nothing"), "route GET /api/cli/system/jobs/{id}": claimRead("reads a system update job"), "route GET /api/cli/system/layout": claimRead("reads the deployment layout intent"), "route GET /api/cli/system/pointer-digests": claimRead("operator report of each account's pointer digest marker and the channels whose head has no digest; writes nothing"), "route GET /api/cli/system/status": claimRead("reads platform release status"), "route GET /apps": claimRead("legacy redirect into the account"), "route GET /apps/{app}": claimRead("legacy redirect into the account"), "route GET /auth/callback": claimNonApp("completes sign-in: session, user and possibly a person's first account"), "route GET /auth/cli": claimRead("renders the device-code approval page"), "route GET /auth/cli/tokens": claimRead("redirects to /settings"), "route GET /auth/cli/whoami": claimRead("names the bearer's principal"), "route GET /auth/confirm": claimRead("the retired confirmation link: renders the expired message for any token"), "route GET /auth/email/link": claimNonApp("an emailed link's confirm page: sets the login-CSRF nonce cookie; spends nothing"), "route GET /auth/signin": claimNonApp("starts an OAuth flow: the auth cookie and its PKCE state"), "route GET /fleet": claimRead("operator fleet view"), "route GET /fleet/economics": claimRead("operator economics view"), "route GET /fleet/economics/accounts": claimRead("operator per-account economics table"), "route GET /fleet/metrics": claimRead("operator box metrics"), "route GET /fleet/store": claimRead("operator entitlement queue page"), "route GET /healthz": claimRead("answers ok and the build"), "route GET /invite/{token}": claimRead("shows an invitation; joining is the POST"), "route GET /settings": claimRead("personal settings: the caller's own tokens"), "route GET /{$}": claimRead("the front door: redirects or renders sign-in"), // GET that writes: a replica file download streams versioned objects (reads // only) but audits every successful GET as an activity row of the app // (apidb.go: auditActivity), an app-scoped SQL write that needs admission. // The old census called it a read and probed it with a made-up span, which // is turned away before the audit, so it never saw the row. "route GET /api/cli/apps/{account}/{app}/db/{host}/ltx/{level}/{span}": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.dbWorld(t) cx.vars["dbhandle"] = cx.dbHandle(t) }, do: func(t *testing.T, cx *claimCase) int { return cx.apiReplyWith(t, http.MethodGet, dbdoor.GetPath("bab", "titogo", claimDBHost, 0, 1, 1), map[string]string{dbdoor.HandleHeader: cx.vars["dbhandle"]}).Status }, check: func(t *testing.T, cx *claimCase, open bool) string { n := cx.sqlCount(t, `SELECT COUNT(*) FROM audit WHERE action = ? AND app = ?`, dbAuditAction, "titogo") switch { case open && n == 0: return "the download wrote no db-read activity row: the handler never reached its audit" case !open && n != 0: return "a db-read activity row was written while the app was closed under a foreign exclusive operation" } return "" }, }}, // GET that writes: the passkey step-up callback replays the held form, // re-entering the ordinary handler. The held action here is the // control-plane clear, which writes titogo's row. "route GET /auth/stepup/callback": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.mustForm(t, "/a/bab/apps/titogo/control-plane/set") }, do: func(t *testing.T, cx *claimCase) int { // A fresh session per run: a completed step-up stays fresh for // StepUpWindow, and this door must be the callback every time. c := cx.freshBrowser(t) m := cx.rec.mark() status, loc, _ := postForm(t, c, cx.ts.URL+"/a/bab/apps/titogo/control-plane/clear", url.Values{"csrf": {sessionToken(t, cx.consoleFixture, c)}}) if status != http.StatusSeeOther || !strings.HasPrefix(loc, cx.fk.srv.URL) { t.Fatalf("control-plane clear did not ask for a passkey: %d -> %q", status, loc) } for _, e := range cx.rec.since(m) { if !e.Control && (e.Unscoped || len(e.Scopes) != 0) { t.Fatalf("the held form wrote %s before the step-up callback", e) } } status, _, _ = completeStepUp(t, cx.consoleFixture, c, loc) return status }, }}, // The update settings read (settings plan 3), apart so its long pattern // leaves the block above aligned as it was. "route GET /api/cli/apps/{account}/{app}/instances/{host}/updates": claimRead("reads one instance record, the app manifest and the box heartbeats"), // ---- PATCH ---- "route PATCH /api/cli/apps/{account}/{app}/instances/{host}": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.instanceWorld(t) cx.instance(t, `{"host":"ha.go.tito.io","region":"ie","kind":"service","addrs":["127.0.0.1:7001"]}`) }, do: claimAPI(http.MethodPatch, "/api/cli/apps/bab/titogo/instances/ha.go.tito.io", `{"addrs":["127.0.0.1:7002","127.0.0.1:7001"]}`), }}, // ---- POST: browser, account level ---- "route POST /a/{sqid}/analytics/setting": claimNonApp("the account-level analytics default (telemetry account record); each app's own setting is its own door"), "route POST /a/{sqid}/apps/new": {door: &claimDoor{ apps: claimRefs("+bab/claim-web-new"), do: claimForm("/a/bab/apps/new", "name", "claim-web-new"), }}, "route POST /a/{sqid}/apps/new/region": claimRead("answers a region guess as JSON; writes nothing"), "route POST /a/{sqid}/delete": claimNonApp("deletes an account only when it holds no app: AppCount is re-checked before the step-up and DeleteAccount refuses otherwise"), "route POST /a/{sqid}/errors/level": claimNonApp("the account-level error log level (telemetry account record)"), "route POST /a/{sqid}/fleets/{fleet}/detach": claimNonApp("detaches a fleet box in the registry; places no app"), "route POST /a/{sqid}/fleets/{fleet}/place": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.fleet(t) }, do: claimForm("/a/bab/fleets/pi/place", "app", "titogo"), }}, "route POST /a/{sqid}/fleets/{fleet}/rotate": claimNonApp("rotates a fleet box's credential in the registry"), "route POST /a/{sqid}/fleets/{fleet}/tags": claimNonApp("tags a fleet box in the registry"), "route POST /a/{sqid}/invite": claimNonApp("account invitations (invites table)"), "route POST /a/{sqid}/invite/revoke": claimNonApp("revokes an account invitation"), "route POST /a/{sqid}/members/remove": claimNonApp("account membership rows; a person's own tokens carry no app"), "route POST /a/{sqid}/members/transfer": claimNonApp("account ownership: membership roles"), "route POST /a/{sqid}/services/new": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimForm("/a/bab/services/new", "name", "svc-web", "role", "publish", "app", "titogo"), }}, "route POST /a/{sqid}/services/revoke": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.service(t) }, do: claimForm("/a/bab/services/revoke", "handle", "svc-a"), }}, "route POST /a/{sqid}/services/rotate": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.service(t) }, do: claimForm("/a/bab/services/rotate", "name", "svc-a"), }}, "route POST /a/{sqid}/store/apply": claimNonApp("applies for the account's storage entitlement (store/entitlements/)"), "route POST /a/{sqid}/trash/delete": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.purgeWorld(t) }, do: claimPurgeDoor, }}, "route POST /a/{sqid}/trash/restore": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.trash(t, "titogo") }, do: claimForm("/a/bab/trash/restore", "app", "titogo"), }}, // ---- POST: browser, app level ---- "route POST /a/{sqid}/apps/{app}/analytics/setting": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimForm("/a/bab/apps/kass/analytics/setting", "count", "edge", "door", "on"), }}, "route POST /a/{sqid}/apps/{app}/canary": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimForm("/a/bab/apps/kass/canary", "name", "rc", "version", "v1"), }}, "route POST /a/{sqid}/apps/{app}/canary/retire": {door: &claimDoor{ apps: claimRefs("bab/kass"), setup: func(t *testing.T, cx *claimCase) { cx.mustForm(t, "/a/bab/apps/kass/canary", "name", "rc", "version", "v1") }, do: claimForm("/a/bab/apps/kass/canary/retire", "name", "rc", "confirmed", "1"), }}, "route POST /a/{sqid}/apps/{app}/control-plane/clear": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.mustForm(t, "/a/bab/apps/titogo/control-plane/set") }, do: claimForm("/a/bab/apps/titogo/control-plane/clear"), }}, "route POST /a/{sqid}/apps/{app}/control-plane/set": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimForm("/a/bab/apps/titogo/control-plane/set"), }}, "route POST /a/{sqid}/apps/{app}/delete": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimForm("/a/bab/apps/titogo/delete", "confirm", "titogo"), }}, // An alias of titogo's claimed, ready instance route. "route POST /a/{sqid}/apps/{app}/domains/attach": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.claimedRoute(t) }, do: claimForm("/a/bab/apps/titogo/domains/attach", "host", "www.titogo-claim.example"), }}, // Alias-only detach is intentionally unsupported (claimUnsupported). "route POST /a/{sqid}/apps/{app}/domains/detach": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.claimedAlias(t, "www.titogo-claim.example") }, do: claimRefusedForm("/a/bab/apps/titogo/domains/detach", "host", "www.titogo-claim.example", "confirm", "www.titogo-claim.example"), status: http.StatusConflict, msg: claimRefusalText(errClaimAliasDetachUnsupported), }}, "route POST /a/{sqid}/apps/{app}/errors/level": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimForm("/a/bab/apps/kass/errors/level", "level", "warn"), }}, "route POST /a/{sqid}/apps/{app}/fasttrack": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimForm("/a/bab/apps/kass/fasttrack", "channel", "stable", "confirmed", "1"), }}, "route POST /a/{sqid}/apps/{app}/gate": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { // The gate lives on an instance record: without one the handler 404s. cx.instanceWorld(t) cx.instance(t, `{"host":"acme.go.tito.io","region":"ie"}`) }, do: claimForm("/a/bab/apps/titogo/gate", "host", "acme.go.tito.io", "action", "set", "password", "claim-fixture-gate-password"), }}, "route POST /a/{sqid}/apps/{app}/instances/{host}/restart": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimForm("/a/bab/apps/kass/instances/kass.carlosapp.dev/restart", "confirmed", "1"), }}, "route POST /a/{sqid}/apps/{app}/instances/{host}/retry": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.heldInstance(t, "web.go.tito.io") }, do: claimForm("/a/bab/apps/titogo/instances/web.go.tito.io/retry", "confirmed", "1"), check: func(t *testing.T, cx *claimCase, open bool) string { g, found, err := release.LoadHostRetries(context.Background(), cx.rel, "bab", "titogo") if err != nil { return err.Error() } asked := false if found { for _, r := range g.Requests { asked = asked || (r.Scope.Kind == release.RestartScopeRetry && r.Scope.Value == "web.go.tito.io") } } switch { case open && !asked: return "the retry request was not written: the handler never reached the host-retry log" case !open && asked: return "a retry request was written while the app was closed under a foreign exclusive operation" } return "" }, }}, // The instance page's Updates section (settings plan 5 Task 5): Save and // Update now, the browser twins of plan 3's PUT …/updates and POST // …/updates/install. Save sends only what changed, so the world has a box // that applies update settings and the form switches auto off (manual). "route POST /a/{sqid}/apps/{app}/instances/{host}/updates": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { t.Helper() claimWorldInstance(t, cx) box := status.CollectBox(nil, status.Probes{MachineID: "claim-box"}, time.Now()) box.Metrics = &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol} if err := status.WriteBox(context.Background(), cx.rel, box); err != nil { t.Fatal(err) } }, do: claimForm("/a/bab/apps/titogo/instances/acme.go.tito.io/updates", "form", "updates", "was_auto", "1"), check: func(t *testing.T, cx *claimCase, open bool) string { rec, err := provision.GetRecord(context.Background(), cx.rel, "bab", "titogo", "acme.go.tito.io") if err != nil { return err.Error() } switch manual := rec.Updates == release.UpdatesManual; { case open && !manual: return "the settings were not written: the handler never reached the narrowed log" case !open && manual: return "settings were written while the app was closed under a foreign exclusive operation" } return "" }, }}, "route POST /a/{sqid}/apps/{app}/instances/{host}/updates/install": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: claimWorldInstance, do: claimForm("/a/bab/apps/titogo/instances/acme.go.tito.io/updates/install"), check: func(t *testing.T, cx *claimCase, open bool) string { rec, err := provision.GetRecord(context.Background(), cx.rel, "bab", "titogo", "acme.go.tito.io") if err != nil { return err.Error() } switch asked := rec.InstallRequest != nil; { case open && !asked: return "the install request was not written: the handler never reached the narrowed log" case !open && asked: return "an install request was written while the app was closed under a foreign exclusive operation" } return "" }, }}, "route POST /a/{sqid}/apps/{app}/keep": {door: &claimDoor{ apps: claimRefs("bab/tmp-claim-keep"), setup: func(t *testing.T, cx *claimCase) { cx.temporary(t, "tmp-claim-keep") }, do: claimForm("/a/bab/apps/tmp-claim-keep/keep"), }}, // Account transfer is refused for every app on an enabled console // (claimUnsupported); the world is one where it would otherwise run. "route POST /a/{sqid}/apps/{app}/move": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo", "+"+claimSecondSqid+"/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.secondAccount(t) }, do: claimRefusedForm("/a/bab/apps/titogo/move", "confirmed", "1"), status: http.StatusConflict, msg: claimTransferUnsupportedMsg, }}, "route POST /a/{sqid}/apps/{app}/pending/{id}/confirm": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.proposal(t) }, do: claimForm("/a/bab/apps/titogo/pending//confirm"), }}, "route POST /a/{sqid}/apps/{app}/pending/{id}/reject": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.proposal(t) }, do: claimForm("/a/bab/apps/titogo/pending//reject"), }}, "route POST /a/{sqid}/apps/{app}/pipeline": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimForm("/a/bab/apps/titogo/pipeline", "ch.0.name", "edge", "ch.1.name", "production", "ch.1.bake", "24h"), }}, "route POST /a/{sqid}/apps/{app}/place": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.fleet(t) }, do: claimForm("/a/bab/apps/titogo/place", "target", "fleet/pi"), }}, "route POST /a/{sqid}/apps/{app}/production": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimForm("/a/bab/apps/kass/production", "production", "on"), }}, "route POST /a/{sqid}/apps/{app}/promote": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimForm("/a/bab/apps/kass/promote", "channel", "canary/x", "version", "v1", "confirmed", "1"), }}, // Rename is refused for every app on an enabled console // (claimUnsupported), recordless titogo included. "route POST /a/{sqid}/apps/{app}/rename": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo", "+bab/titogo-renamed"), do: claimRefusedForm("/a/bab/apps/titogo/rename", "to", "titogo-renamed", "confirmed", "1"), status: http.StatusConflict, msg: claimRenameUnsupportedMsg, }}, "route POST /a/{sqid}/apps/{app}/restart": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimForm("/a/bab/apps/kass/restart", "confirmed", "1"), }}, "route POST /a/{sqid}/apps/{app}/rollback": {door: &claimDoor{ apps: claimRefs("bab/kass"), setup: func(t *testing.T, cx *claimCase) { cx.mustForm(t, "/a/bab/apps/kass/promote", "channel", "canary/x", "version", "v1", "confirmed", "1") }, do: claimForm("/a/bab/apps/kass/rollback", "channel", "canary/x", "confirmed", "1"), }}, "route POST /a/{sqid}/apps/{app}/store/destroy": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.storeGranted(t) rec, _, err := objstore.Get(context.Background(), cx.rel, "bab", "titogo") if err != nil || rec == nil || rec.Provisioned == nil { t.Fatalf("store record %+v %v", rec, err) } cx.vars["bucket"] = rec.Provisioned.Bucket }, do: claimForm("/a/bab/apps/titogo/store/destroy", "confirm", ""), }}, "route POST /a/{sqid}/apps/{app}/swap": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimForm("/a/bab/apps/kass/swap", "swap", "hold", "swap_concurrency", "2"), }}, // ---- POST: unauthenticated, signed ---- // SES delivery events count against the sending app (mail_sender_counts). "route POST /api/ses/events": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.srv.snsVerify = func(snsEnvelope) error { return nil } // the signature check's own seam cx.emailProvisioned(t) }, do: func(t *testing.T, cx *claimCase) int { resp := sesPost(t, cx.consoleFixture, `{"Type":"Notification","TopicArn":"arn:aws:sns:eu-west-1:1:carlos-mail",`+ `"Message":"{\"eventType\":\"Delivery\",\"mail\":{\"source\":\"hello@`+claimMailDomain+`\",`+ `\"messageId\":\"claim-fixture-message\",\"destination\":[\"claim@example.com\"]},`+ `\"delivery\":{\"recipients\":[\"claim@example.com\"]}}",`+ `"SigningCertURL":"https://sns.eu-west-1.amazonaws.com/x.pem","Signature":"x"}`) resp.Body.Close() return resp.StatusCode }, }}, // ---- POST: CLI, account and deployment level ---- "route POST /accounts/new": claimNonApp("creates an account and its owner membership"), "route POST /api/cli/accounts/{account}/store/apply": claimNonApp("applies for the account's storage entitlement"), "route POST /api/cli/accounts/{account}/store/approve": claimNonApp("operator approves an account's storage entitlement"), "route POST /api/cli/accounts/{account}/plan": claimNonApp("operator sets an account's plan, packs and sponsored limits; names no app"), "route POST /api/cli/accounts/{account}/store/decline": claimNonApp("operator declines an account's storage entitlement"), "route POST /api/cli/fleets/{account}/new": claimNonApp("creates a customer fleet in the registry"), "route POST /api/cli/fleets/{account}/{fleet}/boxes": claimNonApp("enrolls a fleet box"), "route POST /api/cli/fleets/{account}/{fleet}/boxes/{label}/detach": claimNonApp("detaches a fleet box"), "route POST /api/cli/fleets/{account}/{fleet}/boxes/{label}/rotate": claimNonApp("rotates a fleet box's credential"), "route POST /api/cli/fleets/{account}/{fleet}/boxes/{label}/tags": claimNonApp("tags a fleet box"), "route POST /api/cli/ops/hosts/{host}/resolve": {refused: &claimRefusalFixture{ apps: []claimAppRef{{Account: "bab", App: "jam"}}, setup: func(t *testing.T, cx *claimCase) { cx.enroll(t, "bab", "jam") cx.hostLossWorld(t) in := cx.lossInspection(t) body, err := json.Marshal(map[string]any{"action": "declare-lost", "submission": claimLossSubmission(in)}) if err != nil { t.Fatal(err) } cx.vars["submission"] = string(body) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/ops/hosts/"+claimLossHost+"/resolve", ""), status: http.StatusConflict, msg: claimHostResolveUnsupportedMsg, }}, "route POST /api/cli/ops/hosts/{host}/resolve/inspect": claimRead("operator host-loss inspection: reads the claim, the record and the roster and signs a challenge; writes nothing"), "route POST /api/cli/system/antivirus": claimNonApp("the deployment scanning switch (system/antivirus.json)"), "route POST /api/cli/system/layout": claimNonApp("the deployment layout intent (control/layout.json)"), "route POST /api/cli/system/activation/cancel": claimNonApp("cancels an unpublished system activation"), "route POST /api/cli/system/pointer-digests": claimNonApp("operator switch: writes or removes each account's root-signed pointer digest marker (releases//pointer-digests), classified exactly by the mutation boundary"), "route POST /api/cli/system/rollback": claimNonApp("rolls the platform system pointer back"), "route POST /api/cli/system/seeds-floor": claimNonApp("operator seeds floor publish: control/seeds-protocol.json, classified exactly by the mutation boundary"), "route POST /api/cli/system/update": claimNonApp("moves the platform system pointer to a signed release"), "route PUT /api/cli/system/upload": claimNonApp("uploads and signs a platform system release"), "route PUT /api/cli/system/geoip": claimNonApp("operator GeoIP publish: the platform database blob and pointer under control/geoip/, each classified exactly by the mutation boundary"), "route DELETE /api/cli/system/geoip": claimNonApp("operator GeoIP off: deletes the pointer and sweeps the blobs under control/geoip/, each classified exactly by the mutation boundary"), "route POST /auth/cli/accounts": claimNonApp("creates an account for the bearer"), "route POST /auth/cli/approve": claimNonApp("approves a device-code login: cli_auth_requests"), "route POST /auth/cli/poll": claimNonApp("collects a device-code token: a person token, app ''"), "route POST /auth/cli/revoke": claimNonApp("revokes the bearer's own person token"), "route POST /auth/cli/start": claimNonApp("starts a device-code login"), // Not nonApp: main supports revoking a legacy kind='app' token here // (TestPersonalSettingsStillRevokesALegacyAppToken), and that DELETE is a // guarded cli_tokens write for the token's app. Person tokens (app '') are // the same handler's other branch; service credentials are refused. "route POST /auth/cli/tokens/revoke": {door: claimLegacyAppTokenRevokeDoor("/auth/cli/tokens/revoke")}, "route POST /auth/continue": claimNonApp("starts sign-in: email challenges"), "route POST /auth/email/code": claimNonApp("signs in by an emailed code: email challenges, user, session and step-up"), // Not nonApp, for the reason the passkey step-up callback is a door: an // email session's step-up (emailed link or code) replays the held form // through the router, re-entering the ordinary handler, so an app // mutation lands through these entries. The link's sign-in branch writes // only challenges, user, session and step-up. "route POST /auth/email/link": {door: claimEmailStepUpDoor(func(t *testing.T, cx *claimCase, c *http.Client, _, link string) int { status, body, nonce := openLink(t, cx.consoleFixture, c, link) if status != http.StatusOK || nonce == "" { t.Fatalf("the step-up link's confirm page = %d:\n%s", status, body) } status, _, _ = postLink(t, cx.consoleFixture, c, link, nonce) return status })}, "route POST /auth/email/stepup/code": {door: claimEmailStepUpDoor(func(t *testing.T, cx *claimCase, c *http.Client, code, _ string) int { status, _, _ := postForm(t, c, cx.ts.URL+"/auth/email/stepup/code", url.Values{"code": {code}}) return status })}, "route POST /auth/signout": claimNonApp("ends the session"), "route POST /fleet/store/{account}/{decision}": claimNonApp("operator entitlement decision for an account"), "route POST /invite/{token}": claimNonApp("accepts an invitation: membership"), "route POST /api/cli/apps/{account}/{app}/regions/guess": claimRead("answers a region guess from the caller's hints; writes nothing"), // Re-issues every relay-enabled app's key: app config and relay records. // Each run rotates onto its own newly staged master: the foreign run // moves the region's issuer pointer (deployment state), so the open run // needs a master that is not yet the issuer to be a valid request. "route POST /api/cli/relay/rotate": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.relayEnabled(t) }, do: func(t *testing.T, cx *claimCase) int { cx.relayStageMaster(t) return cx.api(t, http.MethodPost, "/api/cli/relay/rotate", `{"region":"eu-west-1"}`) }, }}, "route POST /api/cli/services/{account}": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/services/bab", `{"name":"svc-cli","role":"publish","app":"titogo"}`), }}, "route POST /api/cli/services/{account}/{name}/revoke": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.service(t) }, do: claimAPI(http.MethodPost, "/api/cli/services/bab/svc-a/revoke", `{}`), }}, "route POST /api/cli/services/{account}/{name}/rotate": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.service(t) }, do: claimAPI(http.MethodPost, "/api/cli/services/bab/svc-a/rotate", `{}`), }}, // ---- POST: CLI, app level ---- "route POST /api/cli/apps/{account}/new": {door: &claimDoor{ apps: claimRefs("+bab/claim-cli-new"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/new", `{"name":"claim-cli-new"}`), }}, "route POST /api/cli/apps/{account}/new-temporary": {door: &claimDoor{ apps: claimRefs("+bab/tmp-claim-cli"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/new-temporary", `{"name":"tmp-claim-cli","ttl_seconds":86400}`), }}, "route POST /api/cli/apps/{account}/{app}/build-probe": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/build-probe", `{"build_probe":"/api/version"}`), }}, "route POST /api/cli/apps/{account}/{app}/config/sync": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/config/sync", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/delete": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/delete", ""), }}, // An alias of titogo's claimed instance (a legacy registry-only route // such as kass's has no incarnation to bind and is refused). "route POST /api/cli/apps/{account}/{app}/domains": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.claimedRoute(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/domains", attachBody("tickets.example.com", claimAliasRoute)), }}, "route POST /api/cli/apps/{account}/{app}/door-keys": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/door-keys", `{"label":"router"}`), }}, "route POST /api/cli/apps/{account}/{app}/door-keys/{id}/revoke": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { out := cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/door-keys", `{"label":"router"}`, http.StatusCreated) var minted struct{ ID string } if err := json.Unmarshal([]byte(out), &minted); err != nil || minted.ID == "" { t.Fatalf("door key mint reply %q: %v", out, err) } cx.vars["doorkey"] = minted.ID }, do: claimAPIVar(http.MethodPost, "/api/cli/apps/bab/titogo/door-keys//revoke", ""), }}, "route POST /api/cli/apps/{account}/{app}/email": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/email", `{"from":"hello@`+claimMailDomain+`"}`), }}, "route POST /api/cli/apps/{account}/{app}/email/credentials": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.emailProvisioned(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/email/credentials", `{"name":"laptop"}`), }}, "route POST /api/cli/apps/{account}/{app}/email/domains": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.emailDeclared(t) cx.mailFake.StatusOnEnsure.DKIM = "SUCCESS" }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/email/domains", `{"domain":"`+claimMailDomain+`"}`), }}, "route POST /api/cli/apps/{account}/{app}/email/domains/{domain}/verify": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.emailDomainEnsured(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/email/domains/"+claimMailDomain+"/verify", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/email/enable": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.emailDomainEnsured(t) verifyEmailDomain(t, cx.consoleFixture, cx.bearer, "titogo", claimMailDomain) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/email/enable", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/email/pause": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.emailProvisioned(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/email/pause", `{"paused":true}`), }}, "route POST /api/cli/apps/{account}/{app}/email/rotate": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.emailProvisioned(t) cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/email/rotate", `{}`, http.StatusOK) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/email/rotate", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/features": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/features", `{"set":{"sandbox":"on"}}`), }}, "route POST /api/cli/apps/{account}/{app}/instance-token": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instance-token", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/instances": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.instanceWorld(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances", `{"host":"acme.go.tito.io","region":"ie"}`), }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/channel": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.instanceWorld(t) cx.instance(t, `{"host":"web.go.tito.io","region":"ie","channel":"edge"}`) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/web.go.tito.io/channel", `{"channel":"beta"}`), }}, // Seeded instances wait for the claim's protocol (#250, seeded-instances // spec §3.4): every import door refuses on a host-claim console before // it reads the record (claimUnsupported). "route PUT /api/cli/apps/{account}/{app}/instances/{host}/import": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), do: claimRefusedAPI(http.MethodPut, "/api/cli/apps/bab/titogo/instances/old.example.com/import", "SQLite format 3"), status: http.StatusConflict, msg: seedClaimConsoleRefusal().Msg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/import/stopped": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/old.example.com/import/stopped", ""), status: http.StatusConflict, msg: seedClaimConsoleRefusal().Msg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/import/commit": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/old.example.com/import/commit", ""), status: http.StatusConflict, msg: seedClaimConsoleRefusal().Msg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/import/abort": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/old.example.com/import/abort", ""), status: http.StatusConflict, msg: seedClaimConsoleRefusal().Msg, }}, // The legacy route assignment (seeded-instances spec §6.4) says which // app may import routes filed under the operator account. It waits for // #250 with the import doors, and refuses before it reads anything; the // app it names is the one whose imports it would widen. "route POST /api/cli/system/legacy-routes": {refused: &claimRefusalFixture{ apps: claimRefs("bab/kass"), do: claimRefusedAPI(http.MethodPost, "/api/cli/system/legacy-routes", `{"name":"kass","to":"bab/kass"}`), status: http.StatusConflict, msg: seedClaimConsoleRefusal().Msg, }}, // The forwarded db read (db region forward spec §6, §9.1) never signs // on a host-claim console (dbForwardSigner): an instance homed outside // the console's region gets main's residency sentence, after the record // read and before anything is written. The console stays in the home // region and the instance is in ie, so the sentence is the residency one. "route POST /api/cli/apps/{account}/{app}/db/{host}/reads": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.movableInstance(t) cx.srv.cfg.HibernateBucket = claimDBBucket cx.srv.cfg.DBForward = true }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/db/"+claimDBHost+"/reads", claimDBReadBody), status: http.StatusConflict, msg: claimDBReadResidencyMsg, }}, // carlos run waits for the claim's reservation protocol (#250): every // run route refuses on a host-claim console (claimUnsupported). "route POST /api/cli/apps/{account}/{app}/instances/{host}/runs": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.example.com/runs", `{}`), status: http.StatusConflict, msg: claimRunUnsupportedMsg, }}, "route GET /api/cli/apps/{account}/{app}/instances/{host}/runs": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), do: claimRefusedAPI(http.MethodGet, "/api/cli/apps/bab/titogo/instances/acme.example.com/runs?key=k", ""), status: http.StatusConflict, msg: claimRunUnsupportedMsg, }}, "route GET /api/cli/apps/{account}/{app}/instances/{host}/runs/{id}": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), do: claimRefusedAPI(http.MethodGet, "/api/cli/apps/bab/titogo/instances/acme.example.com/runs/run_aaaaaaaaaaaaaaaaaaaaaaaaaa", ""), status: http.StatusConflict, msg: claimRunUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/runs/{id}/cancel": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.example.com/runs/run_aaaaaaaaaaaaaaaaaaaaaaaaaa/cancel", `{}`), status: http.StatusConflict, msg: claimRunUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/runs/{id}/expire": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.example.com/runs/run_aaaaaaaaaaaaaaaaaaaaaaaaaa/expire", `{}`), status: http.StatusConflict, msg: claimRunUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/runs/{id}/ack": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.example.com/runs/run_aaaaaaaaaaaaaaaaaaaaaaaaaa/ack", `{}`), status: http.StatusConflict, msg: claimRunUnsupportedMsg, }}, // Instance moves are intentionally unsupported (claimUnsupported): the // request is valid (a staging region exists), the cut follows a // promote-ready move. "route POST /api/cli/apps/{account}/{app}/instances/{host}/move": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.movableInstance(t) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move", `{"region":"uk"}`), status: http.StatusConflict, msg: claimMoveUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/move/cut": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.stagedMove(t, provision.MovePromoteReady(time.Now())) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move/cut", ""), status: http.StatusConflict, msg: claimMoveUnsupportedMsg, }}, // The offline operator move's doors are intentionally unsupported on a // claim console (claimUnsupported, MV-O20), as the legacy move doors // are. Each request is valid: the start names a movable instance and // another region, and each verb finds the offline move in the state it // acts on. The status view refuses the same way. "route GET /api/cli/apps/{account}/{app}/instances/{host}/move/offline": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.offlineMove(t, provision.OfflineStopping) }, do: claimRefusedAPI(http.MethodGet, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move/offline", ""), status: http.StatusConflict, msg: claimOfflineMoveUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/move/offline": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.movableInstance(t) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move/offline", `{"region":"uk"}`), status: http.StatusConflict, msg: claimOfflineMoveUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/move/offline/abort": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.offlineMove(t, provision.OfflineStopping) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move/offline/abort", ""), status: http.StatusConflict, msg: claimOfflineMoveUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/move/offline/finish": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.offlineMove(t, provision.OfflineOpen) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move/offline/finish", ""), status: http.StatusConflict, msg: claimOfflineMoveUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/move/offline/flip": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.offlineMove(t, provision.OfflineSealed) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move/offline/flip", ""), status: http.StatusConflict, msg: claimOfflineMoveUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/move/offline/open": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.offlineMove(t, provision.OfflineLanded) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move/offline/open", ""), status: http.StatusConflict, msg: claimOfflineMoveUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/move/offline/stage": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.movableInstance(t) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move/offline/stage", `{"region":"uk"}`), status: http.StatusConflict, msg: claimOfflineMoveUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/move/offline/seal": {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.offlineMove(t, provision.OfflineStopping) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move/offline/seal", ""), status: http.StatusConflict, msg: claimOfflineMoveUnsupportedMsg, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/retry": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.heldInstance(t, "web.go.tito.io") }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/web.go.tito.io/retry", `{}`), check: func(t *testing.T, cx *claimCase, open bool) string { g, found, err := release.LoadHostRetries(context.Background(), cx.rel, "bab", "titogo") if err != nil { return err.Error() } asked := false if found { for _, r := range g.Requests { asked = asked || (r.Scope.Kind == release.RestartScopeRetry && r.Scope.Value == "web.go.tito.io") } } switch { case open && !asked: return "the retry request was not written: the handler never reached the host-retry log" case !open && asked: return "a retry request was written while the app was closed under a foreign exclusive operation" } return "" }, }}, "route POST /api/cli/apps/{account}/{app}/instances/{host}/updates/install": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: claimWorldInstance, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/updates/install", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/keep": {door: &claimDoor{ apps: claimRefs("bab/tmp-claim-keep"), setup: func(t *testing.T, cx *claimCase) { cx.temporary(t, "tmp-claim-keep") }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/tmp-claim-keep/keep", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/ledgers/publish": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/ledgers/carbon/append", `{"payload":{"kg_central":1}}`, http.StatusOK) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/ledgers/publish", `{"public":true,"ledgers":["carbon"],"title":"carbon"}`), }}, "route POST /api/cli/apps/{account}/{app}/ledgers/{ledger}/append": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/ledgers/carbon/append", `{"payload":{"kg_central":1}}`), }}, "route POST /api/cli/apps/{account}/{app}/ledgers/{ledger}/blobs": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: func(t *testing.T, cx *claimCase) int { req, err := http.NewRequest(http.MethodPost, cx.ts.URL+"/api/cli/apps/bab/titogo/ledgers/prompts/blobs", strings.NewReader("corpus")) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", "Bearer "+cx.bearer) res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } res.Body.Close() return res.StatusCode }, }}, "route POST /api/cli/apps/{account}/{app}/place": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.fleet(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/place", `{"target":"fleet/pi"}`), }}, "route POST /api/cli/apps/{account}/{app}/promote": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/kass/promote", `{"version":"v1","channel":"canary/claim"}`), }}, "route POST /api/cli/apps/{account}/{app}/relay": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.relayArmed(t, "k1:"+relayTestMaster(1)) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/relay", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/relay/adopted": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.addRoute(t, registry.Route{Host: "titogo.oncarlos.com", App: "titogo", Account: "bab", Kind: "service", Status: "ready", Addr: "10.0.0.9:8080"}) if err := appconfig.SetExecDelivery(context.Background(), cx.rel, "bab", "titogo", appconfig.ExecDeliveryFile); err != nil { t.Fatal(err) } cx.relayEnabled(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/relay/adopted", `{"host":"titogo.oncarlos.com"}`), }}, "route POST /api/cli/apps/{account}/{app}/relay/disable": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.relayEnabled(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/relay/disable", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/releases": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: func(t *testing.T, cx *claimCase) int { status, _ := postShip(t, cx.ts.URL+"/api/cli/apps/bab/titogo/releases", cx.bearer, []shipPart{metaPart(t, "abc1234", "binary"), {name: "linux-arm64", body: randomBytes(t, 1024)}}) return status }, }}, "route POST /api/cli/apps/{account}/{app}/restart": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/restart", ""), }}, "route POST /api/cli/apps/{account}/{app}/restart/scope": {door: &claimDoor{ apps: claimRefs("bab/kass"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/kass/restart/scope", `{"host":"kass.carlosapp.dev"}`), }}, "route POST /api/cli/apps/{account}/{app}/restore": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.trash(t, "titogo") }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/restore", ""), }}, "route POST /api/cli/apps/{account}/{app}/retention": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/retention", `{"keep": 50}`), }}, "route POST /api/cli/apps/{account}/{app}/channels/resign": {door: &claimDoor{ apps: claimRefs("bab/kass"), setup: func(t *testing.T, cx *claimCase) { cx.resignPlan(t, "canary/x") }, do: claimAPIVar(http.MethodPost, "/api/cli/apps/bab/kass/channels/resign", `{"channel":"canary/x","digest":"","history_digests":}`), check: func(t *testing.T, cx *claimCase, open bool) string { p, err := release.Channel(context.Background(), cx.rel, cx.pub, "bab", "kass", "canary/x") switch { case err != nil: return err.Error() case open && p.Digest != cx.vars["digest"]: return "canary/x was not re-signed with the confirmed digest" case !open && p.Digest != "": return "canary/x was re-signed while kass was foreign-owned" } return "" }, }}, "route POST /api/cli/apps/{account}/{app}/releases/{version}/authorize": {door: &claimDoor{ apps: claimRefs("bab/kass"), setup: func(t *testing.T, cx *claimCase) { cx.authorizePlan(t) }, do: claimAPIVar(http.MethodPost, "/api/cli/apps/bab/kass/releases/v1/authorize", `{"manifest_digest":""}`), check: func(t *testing.T, cx *claimCase, open bool) string { rec, err := release.ReadPromotionRecord(context.Background(), cx.rel, cx.pub, "bab", "kass", "v1") switch { case !open && errors.Is(err, release.ErrNoPromotionRecord): return "" case !open && err == nil: return "kass v1's promotion record was written while kass was foreign-owned" case err != nil: return err.Error() case !rec.Authorized || rec.ManifestDigest != cx.vars["manifest_digest"]: return "kass v1's promotion record is not an authorization of the confirmed digest" } return "" }, }}, "route POST /api/cli/apps/{account}/{app}/rollback": {door: &claimDoor{ apps: claimRefs("bab/kass"), setup: func(t *testing.T, cx *claimCase) { cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/kass/promote", `{"version":"v1","channel":"canary/x"}`, http.StatusOK) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/kass/rollback", `{"channel":"canary/x"}`), }}, "route POST /api/cli/apps/{account}/{app}/schedules": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/schedules", `{"name":"sync","every":"6h","path":"/jobs/sync"}`), }}, "route POST /api/cli/apps/{account}/{app}/schedules/{name}/delete": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/schedules", `{"name":"sync","every":"6h","path":"/jobs/sync"}`, http.StatusOK) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/schedules/sync/delete", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/schedules/{name}/run": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/schedules", `{"name":"sync","every":"6h","path":"/jobs/sync"}`, http.StatusOK) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/schedules/sync/run", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/sidecars": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.sidecarWorld(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/sidecars", `{"name":"bot","region":"ie"}`), }}, "route POST /api/cli/apps/{account}/{app}/sidecars/{name}/wake": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.sidecar(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/sidecars/bot/wake", ""), }}, "route POST /api/cli/apps/{account}/{app}/store": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/store/grant": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { declareStore(t, cx.consoleFixture, cx.bearer) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/grant", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/store/provision": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { declareStore(t, cx.consoleFixture, cx.bearer) approveStoreAccount(t, cx.consoleFixture, "operator@keymail.dev") }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/provision", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/store/rotate": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.storeGranted(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/rotate", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/store/rotate/finish": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.storeGranted(t) cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/store/rotate", `{}`, http.StatusOK) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/rotate/finish", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/store/scan": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.storeGranted(t) enableDeploymentScanning(t, cx.consoleFixture) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/scan", `{"encrypted":false}`), }}, "route POST /api/cli/apps/{account}/{app}/store/scan/enforce": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.scanGranted(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/scan/enforce", `{"on":true}`), }}, "route POST /api/cli/apps/{account}/{app}/store/scan/grant": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.scanRequested(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/scan/grant", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/store/scan/revoke": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.scanGranted(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/scan/revoke", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/store/scan/sweep": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.scanGranted(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/scan/sweep", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/store/scan/object": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.scanGranted(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/scan/object", `{"key":"claim-fixture.txt"}`), }}, "route POST /api/cli/apps/{account}/{app}/store/scan/callback-key/rotate": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.scanGranted(t) }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/titogo/store/scan/callback-key/rotate", `{}`), }}, // Deployment-wide, but every effect is one scanning app's config and // store record, each written under that app's own mutation. "route POST /api/cli/system/antivirus/rotate-webhook-key": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.scanMasterRotated(t) }, do: claimAPI(http.MethodPost, "/api/cli/system/antivirus/rotate-webhook-key", `{}`), }}, "route POST /api/cli/apps/{account}/{app}/ttl": {door: &claimDoor{ apps: claimRefs("bab/tmp-claim-ttl"), setup: func(t *testing.T, cx *claimCase) { cx.temporary(t, "tmp-claim-ttl") }, do: claimAPI(http.MethodPost, "/api/cli/apps/bab/tmp-claim-ttl/ttl", `{"ttl_seconds":432000}`), }}, // ---- PUT ---- "route PUT /api/cli/apps/{account}/{app}/config": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/config", `{"set":{"PORT":{"kind":"env","value":"8080"}}}`), }}, "route PUT /api/cli/apps/{account}/{app}/domains/{host}/paths": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.claimedAlias(t, "ti.to") }, do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/domains/ti.to/paths", `{"paths":{"paulca":"`+claimAliasRoute+`"}}`), }}, "route PUT /api/cli/apps/{account}/{app}/email/events/push": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/email/events/push", `{"path":"/mail/events"}`), }}, "route PUT /api/cli/apps/{account}/{app}/instances-manifest": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/instances-manifest", `{}`), }}, "route PUT /api/cli/apps/{account}/{app}/instances/{host}/updates": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: claimWorldInstance, do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/updates", `{"updates":"auto"}`), }}, "route PUT /api/cli/apps/{account}/{app}/pipeline": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/pipeline", `{"channels":[{"name":"edge"}]}`), }}, "route PUT /api/cli/apps/{account}/{app}/sidecars-manifest": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/sidecars-manifest", `{}`), }}, "route PUT /api/cli/apps/{account}/{app}/swap": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/swap", `{"swap":"hold"}`), }}, "route PUT /api/cli/apps/{account}/{app}/telemetry": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/telemetry", `{"errors":{"log_level":"warn"}}`), }}, // The app's instance update policy (settings plan 3 Task 8) writes the // instances manifest, as the instances-manifest door does. "route PUT /api/cli/apps/{account}/{app}/updates": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { t.Helper() cx.instanceWorld(t) }, do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/updates", `{"default_channel":"beta","instance_control":["updates"]}`), check: func(t *testing.T, cx *claimCase, open bool) string { m, err := provision.GetManifest(context.Background(), cx.rel, "bab", "titogo") switch { case err != nil: return err.Error() case open && (m.Channel != "beta" || m.Updates == nil): return "the titogo manifest carries no new policy after the open run: the handler never reached its write" case !open && (m.Channel != "stable" || m.Updates != nil): return "the titogo manifest's policy changed while titogo was closed under a foreign exclusive operation" } return "" }, }}, "route PUT /api/cli/apps/{account}/{app}/version-target": {door: &claimDoor{ apps: claimRefs("bab/titogo"), do: claimAPI(http.MethodPut, "/api/cli/apps/bab/titogo/version-target", `{"target":"0.5.0"}`), }}, // ---- workers ---- "worker LogPlacementDiscrepancies": claimRead("logs region drift between the registry and instance records; writes nothing"), "worker PublishMovesConsole": claimNonApp("the moves console record (control/moves-console.json): the platform's moves protocol and key fingerprint; no app, and nothing with the moves switch off"), "worker ReportApexSquats": claimRead("logs apex squats found in the registry; writes nothing"), "worker StartRunSweep": claimRead("off on a host-claim console, where every run route refuses: it logs that and writes nothing"), "worker StartSeedsPass": claimRead("off on a host-claim console, where every seed door and finishSeed refuse: it logs that and writes nothing"), "worker StartAlertWatcher": claimNonApp("box alert state under alerts/boxes and operator mail"), "worker StartEconomicsPass": claimNonApp("daily usage census, statements and bills: deployment and account economics"), "worker StartGeoipJobs": claimNonApp("GeoIP orphan sweep, D1 expiry and feed status under control/geoip/: the platform database, no app"), "worker StartHostReports": claimNonApp("this console's census report (status/consoles) and roster activation release"), "worker StartInfraPass": claimNonApp("the deployment's infrastructure inventory snapshot"), "worker StartPlanLimitsPass": claimNonApp("account plan rows and their limits records under accounts//limits.json; names no app"), "worker PublishPlansMarker": claimNonApp("the plans deployment marker (control/plans.json): whether boxes read limits; names no app"), "worker StartClaimRecoveryPass": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.crashedRestore(t) }, do: claimWorker("StartClaimRecoveryPass"), }}, "worker StartDoorKeyResign": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.mustAPI(t, http.MethodPost, "/api/cli/apps/bab/titogo/door-keys", `{"label":"router"}`, http.StatusCreated) }, do: claimWorker("StartDoorKeyResign"), }}, "worker StartExpiryPass": {door: &claimDoor{ apps: claimRefs("bab/tmp-claim-expiry"), setup: func(t *testing.T, cx *claimCase) { cx.temporary(t, "tmp-claim-expiry") cx.expireNow(t, "tmp-claim-expiry") }, do: claimWorker("StartExpiryPass"), }}, "worker StartMailEventPush": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { claimMustRunAlone(t, "worker StartMailEventPush") old := mailPushClient.Transport mailPushClient.Transport = claimOKTransport{rec: cx.rec} t.Cleanup(func() { mailPushClient.Transport = old }) cx.emailPush(t) appID := cx.appIDs[claimAppRef{Account: "bab", App: "titogo"}] scope := []claimSQLScope{{AccountID: OpsAccountID, App: "titogo", AppID: appID, Mode: claimModeEffect}} // Two queued events: the foreign run must leave both, the open run // delivers them. err := cx.st.withClaimSQL(gateCtx(t), scope, allowAllClaimSQL, func(v *Store) error { for _, notice := range []string{"claim-fixture-1", "claim-fixture-2"} { if err := v.EnqueueMailEventDelivery(context.Background(), "bab", "titogo", notice, []byte(`{}`), time.Now().Add(-time.Minute)); err != nil { return err } } return nil }) if err != nil { t.Fatal(err) } }, do: claimWorker("StartMailEventPush"), }}, "worker StartRelay": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.relayEnabled(t) ctx := context.Background() rec, tag, err := relay.GetRecord(ctx, cx.rel, "bab", "titogo") if err != nil || rec == nil { t.Fatalf("relay record %+v %v", rec, err) } // A converge the previous holder left half done. rec.Phase = relayPhaseConfig if _, err := relay.PutRecord(ctx, cx.rel, "bab", "titogo", rec, tag); err != nil { t.Fatal(err) } }, do: claimWorker("StartRelay"), }}, "worker StartScanPass": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.scanGranted(t) }, do: claimWorker("StartScanPass"), }}, "worker StartScanDrain": {door: &claimDoor{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.scanQueued(t) }, do: claimWorker("StartScanDrain"), }}, } // readyClaims stages the box acknowledgements and completed create before a // measured request. The actor simulator writes raw fixture state, while the // real executor check validates the roster, release floor and acknowledgements. func (cx *claimCase) readyClaims(t *testing.T) { t.Helper() ctx := context.Background() hosts, err := hostops.ListClaims(ctx, cx.rel) if err != nil { t.Fatal(err) } for _, host := range hosts { obs, err := hostclaim.Read(ctx, cx.rel, host) if err != nil { t.Fatal(err) } if obs.Absent || obs.Claim.Op.Kind != hostclaim.KindCreating { continue } obs = cx.ackExecution(t, obs) record, err := hostops.ReadRecord(ctx, cx.rel, obs.Claim.Holder.Key) if err != nil { t.Fatal(err) } executor, err := cx.srv.cfg.ClaimAuthority.Executor(ctx, obs, record) if err != nil { t.Fatalf("staged create executor: %v", err) } if _, err = hostclaim.Update(ctx, cx.rel, obs, hostclaim.CompleteCreate(obs.Claim.Op.ID, executor)); err != nil { t.Fatal(err) } cx.settleRegistrations(t, record.Holder.Account, record.Holder.App) } }