package console import ( "context" "encoding/json" "fmt" "net/http" "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/svcrole" ) // The console's pointer digest doors (#253, spec ยง4.6): `carlos channels // resign`, `carlos channels digests` and the digest state on `carlos // channels`. func TestResignDoorPlansThenSigns(t *testing.T) { fx := newDigestsFixture(t) st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]string{"channel": "beta"}, fx.member) if st != http.StatusOK { t.Fatalf("plan: %d %s", st, out) } var plan struct { Version string `json:"version"` Digest string `json:"digest"` History []string `json:"history"` HistoryDigests []string `json:"history_digests"` Computed []bool `json:"computed"` } if err := json.Unmarshal([]byte(out), &plan); err != nil || plan.Digest == "" { t.Fatalf("plan body %s: %v", out, err) } if plan.Version != "v2" || len(plan.History) != 1 || plan.History[0] != "v1" || len(plan.HistoryDigests) != 1 || plan.HistoryDigests[0] == "" || len(plan.Computed) != 2 || !plan.Computed[0] || !plan.Computed[1] { t.Fatalf("plan = %+v, want v2 over v1, both digests read from the bucket now (beta carries no rules, so a member signs it)", plan) } if p, _ := release.Channel(context.Background(), fx.store, fx.pub, "bab", "km", "beta"); p.Digest != "" { t.Fatal("planning signed something") } st, out = postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]any{ "channel": "beta", "digest": plan.Digest, "history_digests": plan.HistoryDigests}, fx.member) if st != http.StatusOK || !strings.Contains(out, `"result":"resigned"`) { t.Fatalf("sign: %d %s", st, out) } p, err := release.Channel(context.Background(), fx.store, fx.pub, "bab", "km", "beta") if err != nil || p.Digest != plan.Digest || len(p.HistoryDigests) != 1 || p.HistoryDigests[0] != plan.HistoryDigests[0] { t.Fatalf("pointer after resign = %+v, %v; want the plan's digests", p, err) } events, err := fx.st.Events(OpsAccountID, "km", 10) if err != nil { t.Fatal(err) } found := false for _, e := range events { if e.Action == "resign" && e.Address == "alice@keymail.dev" && strings.Contains(e.Detail, "(cli)") { found = true } } if !found { t.Fatalf("events = %v, want a resign row by the member that names the CLI", events) } } func TestResignDoorRefusesDigestsThatAreNotThePlan(t *testing.T) { fx := newDigestsFixture(t) st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]any{ "channel": "beta", "digest": release.ManifestDigest([]byte("not it")), "history_digests": []string{""}}, fx.member) if st != http.StatusConflict || jsonErrorText(t, out) != fmt.Sprintf(release.TextResignChanged, "beta") { t.Fatalf("mismatched confirmation: %d %s", st, out) } if p, _ := release.Channel(context.Background(), fx.store, fx.pub, "bab", "km", "beta"); p.Digest != "" { t.Fatal("a refused confirmation signed something") } } func TestResignDoorRefusesServiceCredentials(t *testing.T) { fx := newDigestsFixture(t) st, _ := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]string{"channel": "stable"}, fx.serviceToken) if st != http.StatusNotFound && st != http.StatusForbidden { t.Fatalf("a service credential reached resign: %d", st) } } // TestResignDoorSpendsTheWriteBudget: resign signs a pointer, so it draws on // the same per-person write budget as rollback, before any store work. func TestResignDoorSpendsTheWriteBudget(t *testing.T) { fx := newDigestsFixture(t) fx.srv.cli.write = newRateLimiter(1, time.Minute) body := map[string]string{"channel": "stable"} if st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", body, fx.member); st != http.StatusOK { t.Fatalf("first request: %d %s", st, out) } if st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", body, fx.member); st != http.StatusTooManyRequests || jsonErrorText(t, out) != textTooManyRequests { t.Fatalf("second request over the budget: %d %s", st, out) } } // Astra r1 #8: the digest doors reuse the rollback door's one definition of // the rate-limit refusal instead of repeating its literal, so their source // adds no em dash. func TestDigestDoorsDefineNoEmDashLiteral(t *testing.T) { src, err := os.ReadFile("apidigests.go") if err != nil { t.Fatal(err) } if n := strings.Count(string(src), "\u2014"); n != 0 { t.Fatalf("apidigests.go carries %d em dash(es); reuse textTooManyRequests", n) } } func TestSystemDigestsDoorTurnsOnAndListsStragglers(t *testing.T) { fx := newDigestsFixture(t) ctx := context.Background() st, out := postRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", map[string]any{"on": false}, fx.member) if st != http.StatusNotFound { t.Fatalf("a member who is not an operator turned digests off: %d %s", st, out) } if m, err := release.ReadDigestMarker(ctx, fx.store, fx.pub, "bab"); m == nil || err != nil { t.Fatalf("the member's refused request moved the marker: %+v, %v", m, err) } st, out = postRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", map[string]any{"on": false}, fx.operator) if st != http.StatusOK || !strings.Contains(out, `"account":"bab","on":false`) { t.Fatalf("operator off: %d %s", st, out) } if m, err := release.ReadDigestMarker(ctx, fx.store, fx.pub, "bab"); m != nil || err != nil { t.Fatalf("marker after off = %+v, %v", m, err) } st, out = postRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", map[string]any{"on": true, "accounts": []string{"bab"}}, fx.operator) if st != http.StatusOK || !strings.Contains(out, `"account":"bab","on":true`) { t.Fatalf("operator on: %d %s", st, out) } if m, err := release.ReadDigestMarker(ctx, fx.store, fx.pub, "bab"); m == nil || err != nil { t.Fatalf("marker = %+v, %v", m, err) } st, out = getRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", fx.operator) if st != http.StatusOK || !strings.Contains(out, `"account":"bab","app":"km","channel":"stable"`) { t.Fatalf("stable has no digests yet and must be listed: %d %s", st, out) } if st, out := getRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", fx.member); st != http.StatusNotFound { t.Fatalf("a member who is not an operator read the report: %d %s", st, out) } } // TestSystemDigestsDoorListsOnlyHeadsWithoutDigests: once canary/x's head // carries a digest it leaves the list, and stable stays on it. func TestSystemDigestsDoorListsOnlyHeadsWithoutDigests(t *testing.T) { fx := newDigestsFixture(t) fx.markerThenPromote(t, "v2") st, out := getRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", fx.operator) if st != http.StatusOK { t.Fatalf("report: %d %s", st, out) } var rep struct { Channels []struct{ Account, App, Channel string } `json:"channels"` } if err := json.Unmarshal([]byte(out), &rep); err != nil { t.Fatalf("report body %s: %v", out, err) } listed := map[string]bool{} for _, c := range rep.Channels { listed[c.Account+"/"+c.App+"/"+c.Channel] = true } if !listed["bab/km/stable"] || listed["bab/km/canary/x"] { t.Fatalf("listed = %v, want km's stable and not its canary/x", listed) } } // Astra r1 #4: a channel whose pointer cannot be verified is not skipped as // if it carried digests. The account's row carries the error, so the CLI // reports it unknown and never calls the fleet ready. func TestSystemDigestsDoorReportsAChannelItCannotRead(t *testing.T) { fx := newDigestsFixture(t) ctx := context.Background() key := release.ChannelKey(release.OpsAccount, "km", "beta") _, etag, err := fx.store.Get(ctx, key) if err != nil { t.Fatal(err) } if _, err := fx.store.Put(ctx, key, []byte("junk"), etag); err != nil { t.Fatal(err) } st, out := getRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", fx.operator) if st != http.StatusOK { t.Fatalf("report: %d %s", st, out) } var rep struct { Accounts []struct{ Account, Error string } `json:"accounts"` Channels []struct{ Account, App, Channel string } `json:"channels"` } if err := json.Unmarshal([]byte(out), &rep); err != nil { t.Fatalf("report body %s: %v", out, err) } rows, _, err := release.DigestReport(ctx, fx.store, fx.pub, release.OpsAccount, "km") if err != nil { t.Fatal(err) } want := "" for _, r := range rows { if r.Channel == "beta" { want = r.Err } } if want == "" { t.Fatal("control: the junk pointer read as a channel state") } found := false for _, a := range rep.Accounts { if a.Account == release.OpsAccount { found = true if a.Error != want { t.Fatalf("bab's row error = %q, want %q", a.Error, want) } } } if !found { t.Fatalf("no row for bab: %s", out) } for _, c := range rep.Channels { if c.Channel == "beta" { t.Fatalf("the unreadable channel was listed as a state: %s", out) } } } func TestChannelsDoorCarriesDigestState(t *testing.T) { fx := newDigestsFixture(t) st, out := getRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels", fx.member) if st != http.StatusOK || !strings.Contains(out, `"digests":false`) || !strings.Contains(out, `"straggler":false`) || !strings.Contains(out, `"history_missing":1`) || !strings.Contains(out, `"pointer_digests":{"on":true`) { t.Fatalf("channels: %d %s", st, out) } } func TestRollbackDoorNamesResign(t *testing.T) { fx := newDigestsFixture(t) // v1 on canary/x before the marker fx.markerThenPromote(t, "v2") st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/rollback", map[string]string{"channel": "canary/x"}, fx.member) want := (&release.ResignNeededError{App: "km", Channel: "canary/x", Version: "v1"}).Error() if st != http.StatusConflict || jsonErrorText(t, out) != want { t.Fatalf("rollback onto a digest-less entry: %d %s, want 409 %q", st, out, want) } if cp := fx.channelVersion(t, "km", "canary/x"); cp.Version != "v2" { t.Fatalf("canary/x after a refused rollback = %q, want v2", cp.Version) } } // resignPlanOf asks the resign door for channel's plan, as a member may. func (fx *digestsFixture) resignPlanOf(t *testing.T, channel string) (digest string, historyDigests []string) { t.Helper() st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]string{"channel": channel}, fx.member) if st != http.StatusOK { t.Fatalf("plan %s: %d %s", channel, st, out) } var plan struct { Digest string `json:"digest"` HistoryDigests []string `json:"history_digests"` } if err := json.Unmarshal([]byte(out), &plan); err != nil || plan.Digest == "" { t.Fatalf("plan body %s: %v", out, err) } return plan.Digest, plan.HistoryDigests } // rollbackRefusal is what the rollback door answers the member for channel: // the refusal a resign of that channel must repeat. func (fx *digestsFixture) rollbackRefusal(t *testing.T, channel string) (int, string) { t.Helper() st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/rollback", map[string]string{"channel": channel}, fx.member) if st != http.StatusForbidden { t.Fatalf("control: the rollback door let the member roll %s back: %d %s", channel, st, out) } return st, out } // Astra r1 #2: signing a resign takes the rollback door's authority. A // member may read stable's plan, but confirming it is refused exactly as a // rollback of stable is, and the pointer is unchanged. func TestResignDoorRefusesAMemberOnStable(t *testing.T) { fx := newDigestsFixture(t) digest, hd := fx.resignPlanOf(t, "stable") wantSt, wantBody := fx.rollbackRefusal(t, "stable") st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]any{ "channel": "stable", "digest": digest, "history_digests": hd}, fx.member) if st != wantSt || out != wantBody { t.Fatalf("member resign of stable: %d %s, want the rollback door's %d %s", st, out, wantSt, wantBody) } if p := fx.channelVersion(t, "km", "stable"); p.Digest != "" || p.Version != "v2" { t.Fatalf("a refused resign moved stable: %+v", p) } } // Astra r1 #2's attack: a legacy stable pointer names v2, and v2's manifest // is replaced in the bucket (pruned and reshipped). The plan offers the new // bytes' digest; a member confirming it would bind stable's v2 to bytes no // one released. Refused as rollback is, and the pointer still signs nothing. func TestResignDoorRefusesALegacyHeadSubstitution(t *testing.T) { fx := newDigestsFixture(t) ctx := context.Background() key := release.ManifestKey(release.OpsAccount, "km", "v2") orig, etag, err := fx.store.Get(ctx, key) if err != nil { t.Fatal(err) } var m map[string]any if err := json.Unmarshal(orig, &m); err != nil { t.Fatal(err) } m["built_at"] = "2026-10-07T00:00:00Z" // other bytes under the same version swapped, err := json.Marshal(m) if err != nil { t.Fatal(err) } if _, err := fx.store.Put(ctx, key, swapped, etag); err != nil { t.Fatal(err) } digest, hd := fx.resignPlanOf(t, "stable") if digest != release.ManifestDigest(swapped) { t.Fatalf("the plan offers %s, want the substituted manifest's digest", digest) } wantSt, wantBody := fx.rollbackRefusal(t, "stable") st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]any{ "channel": "stable", "digest": digest, "history_digests": hd}, fx.member) if st != wantSt || out != wantBody { t.Fatalf("member resign of a substituted stable head: %d %s, want %d %s", st, out, wantSt, wantBody) } if p := fx.channelVersion(t, "km", "stable"); p.Digest != "" { t.Fatalf("stable was bound to the substituted bytes: %+v", p) } } // A pipelined channel's rules govern resign as they govern rollback, asked // inside the admitted mutation: a passkey rule refuses with the rollback // door's refusal, and an approvals rule (which rollback turns into a // proposal; resign has none to make) refuses the same way. The pointer is // unchanged either way. func TestResignDoorAsksTheChannelsPipelineRules(t *testing.T) { for _, rules := range []release.ChannelRules{{Passkey: true}, {PromoteApprovals: 1}} { fx := newDigestsFixture(t) digest, hd := fx.resignPlanOf(t, "beta") setPipeline(t, fx.consoleFixture, "km", release.PipelineChannel{Name: "beta", Rules: release.ChannelRules{Passkey: true}}) wantSt, wantBody := fx.rollbackRefusal(t, "beta") setPipeline(t, fx.consoleFixture, "km", release.PipelineChannel{Name: "beta", Rules: rules}) st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]any{ "channel": "beta", "digest": digest, "history_digests": hd}, fx.member) if st != wantSt || out != wantBody { t.Fatalf("rules %+v: member resign of beta: %d %s, want the rollback door's %d %s", rules, st, out, wantSt, wantBody) } if p := fx.channelVersion(t, "km", "beta"); p.Digest != "" { t.Fatalf("rules %+v: a refused resign signed beta: %+v", rules, p) } } } // jsonErrorText is the "error" of a jsonError body. func jsonErrorText(t *testing.T, body string) string { t.Helper() var e struct { Error string `json:"error"` } if err := json.Unmarshal([]byte(body), &e); err != nil { t.Fatalf("not a JSON error body: %s: %v", body, err) } return e.Error } // digestsFixture is newConsoleFixture with app km in the ops account bab: // v1 and v2 shipped, promoted v1 then v2 on stable and on beta, and v1 on // canary/x, all before the account's digest marker went on, so none of them carries a // digest. Then the marker, written with the root key (resign needs it, Astra // I3 #4). type digestsFixture struct { *consoleFixture store release.Store member string // a CLI bearer for alice, a member of bab operator string // a CLI bearer for paul, an owner of the ops account serviceToken string // an app-scoped svcrole.Publish credential for km } func newDigestsFixture(t *testing.T) *digestsFixture { t.Helper() fx := newConsoleFixture(t) now := time.Now() if _, err := fx.st.CreateAppIn(OpsAccountID, "km", "paul@keymail.dev", now); err != nil { t.Fatal(err) } if err := fx.st.AddMember(OpsAccountID, "alice@keymail.dev", RoleMember, now); err != nil { t.Fatal(err) } shipVersion(t, fx, "km", "v1") shipVersion(t, fx, "km", "v2") seedChannel(t, fx, "km", "stable", "v1") seedChannel(t, fx, "km", "stable", "v2") seedChannel(t, fx, "km", "canary/x", "v1") seedChannel(t, fx, "km", "beta", "v1") seedChannel(t, fx, "km", "beta", "v2") dx := &digestsFixture{ consoleFixture: fx, store: fx.rel, member: cliToken(t, fx, "alice@keymail.dev", "code-digests-member"), operator: cliToken(t, fx, "paul@keymail.dev", "code-digests-operator"), serviceToken: serviceToken(t, fx.st, OpsAccountID, "digests-ci", "km", svcrole.Publish), } dx.writeMarker(t) return dx } func (fx *digestsFixture) writeMarker(t *testing.T) { t.Helper() if err := release.WriteDigestMarker(context.Background(), fx.store, release.RootSigner(fx.priv), release.OpsAccount, "paul@keymail.dev"); err != nil { t.Fatal(err) } } // markerThenPromote writes the marker (a no-op when it already verifies) and // promotes v to canary/x, which therefore carries v's digest. func (fx *digestsFixture) markerThenPromote(t *testing.T, v string) { t.Helper() fx.writeMarker(t) seedChannel(t, fx.consoleFixture, "km", "canary/x", v) if cp := fx.channelVersion(t, "km", "canary/x"); cp.Digest == "" { t.Fatalf("canary/x promoted to %s with the marker on carries no digest", v) } }