carlos / platform Public

console: make the bake-control tests bite — real step-up probe, byte-identical bodies

Review follow-up to 0e36109. The handlers were right; two of the tests were
not testing what they claimed.

- Fast-track's passkey gate was never exercised: postGated is transparent, so
  it walks the round trip only if one is demanded — deleting requireFreshAuth
  left the test green. It now posts un-stepped-up first and asserts the
  redirect into Keymail AND that the pointer did not gain fast_track, then
  completes the step-up (ladder_test.go's own pattern). Verified by mutation:
  with the step-up deleted the test fails.
- The member-404 tests asserted status only, and "byte-identical" is the
  actual constraint. Each now compares the member's refusal against a real
  non-member's refusal of the same route, and pins both to http.NotFound's
  literal output (apicli_test.go's pattern).
- The production-off 404 test seeds the flag ON first, so "the flag stayed
  put" is an assertion about a value that could have moved.
- The bake-note test's opening promote is checked, so the absence assertion
  cannot pass because the promote failed.
- TestPromotionsDisabledWithoutKey covers /production and /fasttrack: it is
  this repo's enumeration of every mutation route refusing without a key, and
  both of these sign.
- The three new actions are asserted in the CONSOLE's own activity trail
  (store events), which the pointer's audit trail does not stand in for.
- handleProduction's doc comment now lists the gates it actually applies, and
  says that owner + step-up are the OFF direction's alone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK
Paul Campbell 81a62f98e23ba730977d5fe57d5ed4140128e75b parent 0e36109
3 files changed, +131 −12
  • internal/console/bakecontrols_test.go +109 −10
  • internal/console/server.go +8 −2
  • internal/console/server_test.go +14 −0
diff --git a/internal/console/bakecontrols_test.go b/internal/console/bakecontrols_test.go
index c252ef2af..1ec838ced 100644
--- a/internal/console/bakecontrols_test.go
+++ b/internal/console/bakecontrols_test.go
@@ -30,6 +30,44 @@ func productionFlag(t *testing.T, fx *consoleFixture, app string) *release.Produ
return f
}
+// strangerRefusal is what someone with no membership at all gets from one of
+// this account's app routes: a signed-in browser belonging to nobody here.
+// It is the yardstick the member's refusal must match byte for byte.
+func strangerRefusal(t *testing.T, fx *consoleFixture, code, u string, form url.Values) string {
+ t.Helper()
+ fx.fk.mu.Lock()
+ prev := fx.fk.address
+ fx.fk.mu.Unlock()
+ fx.setUser("zoe@keymail.dev") // signs up into an account of her own
+ defer fx.setUser(prev)
+ c := newBrowser(t)
+ signUp(t, fx, c, code)
+ form.Set("csrf", sessionToken(t, fx, c))
+ status, _, body := postForm(t, c, u, form)
+ if status != http.StatusNotFound {
+ t.Fatalf("non-member posting %s = %d, want 404", u, status)
+ }
+ return body
+}
+
+// auditedBy asserts the console's OWN activity trail (store events, which is
+// what the app page renders) recorded this action for this actor. The
+// pointer's audit trail is a different record with a different purpose; a
+// console action that leaves no console event is an invisible one.
+func auditedBy(t *testing.T, fx *consoleFixture, app, action, actor, detailPart string) {
+ t.Helper()
+ events, err := fx.st.Events(OpsAccountID, app, 20)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, e := range events {
+ if e.Action == action && e.Address == actor && strings.Contains(e.Detail, detailPart) {
+ return
+ }
+ }
+ t.Fatalf("events = %+v, want a %q row by %s mentioning %q", events, action, actor, detailPart)
+}
+
func TestProductionOnIsAnyMembers(t *testing.T) {
fx := newConsoleFixture(t)
if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
@@ -48,6 +86,7 @@ func TestProductionOnIsAnyMembers(t *testing.T) {
if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
t.Fatalf("flag = %+v, want production on", f)
}
+ auditedBy(t, fx, "kass", "production", "sam@keymail.dev", "production: on")
}
func TestProductionOffIsOwnerPasskeyGated(t *testing.T) {
@@ -76,12 +115,19 @@ func TestProductionOffIsOwnerPasskeyGated(t *testing.T) {
if f := productionFlag(t, fx, "kass"); f == nil || f.Production {
t.Fatal("the flag did not flip off after confirmation")
}
+ auditedBy(t, fx, "kass", "production", "paul@keymail.dev", "production: off")
}
// A MEMBER probing the off direction meets the byte-identical 404 — before
// any step-up redirect could confirm the account exists.
func TestProductionOffMemberGets404(t *testing.T) {
fx := newConsoleFixture(t)
+ // Seeded ON, so "the flag stayed put" is an assertion about a value that
+ // could have moved rather than about an absent object.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
t.Fatal(err)
}
@@ -89,21 +135,51 @@ func TestProductionOffMemberGets404(t *testing.T) {
c := newBrowser(t)
signUp(t, fx, c, "code-sam-bake2")
csrf := sessionToken(t, fx, c)
- status, _, _ := postForm(t, c, fx.u("/apps/kass/production"),
- url.Values{"csrf": {csrf}, "production": {"off"}})
+ form := url.Values{"csrf": {csrf}, "production": {"off"}}
+ status, _, memberBody := postForm(t, c, fx.u("/apps/kass/production"), form)
if status != http.StatusNotFound {
t.Fatalf("member turning bake OFF = %d, want 404", status)
}
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatalf("flag = %+v after a member's refused post, want it untouched", f)
+ }
+ // Byte-identical, not merely same-status: the difference between "not
+ // allowed" and "no such account" is not information this hands out.
+ strangerBody := strangerRefusal(t, fx, "code-zoe-bake2", fx.u("/apps/kass/production"),
+ url.Values{"production": {"off"}})
+ if memberBody != strangerBody {
+ t.Fatalf("the two refusals are not byte-identical:\n(member) %q\n(stranger) %q", memberBody, strangerBody)
+ }
+ // http.NotFound's own, well-known output — pinned so a future change to
+ // either path is caught even if it stays self-consistent.
+ if memberBody != "404 page not found\n" {
+ t.Fatalf("refusal body = %q, want the stdlib http.NotFound output", memberBody)
+ }
}
+// Fast-track is owner + passkey, ALWAYS. The un-stepped-up post is what
+// proves the second half: postGated alone would stay green with the step-up
+// call deleted, because it only walks the round trip when one is demanded.
func TestFastTrackIsOwnerPasskeyGatedAndLands(t *testing.T) {
fx := newConsoleFixture(t)
c := signInPaul(t, fx)
csrf := sessionToken(t, fx, c)
- // The fixture's stable already points at v1; waive it as the owner with
- // the full passkey round trip.
- postGated(t, fx, c, fx.u("/apps/kass/fasttrack"),
+
+ // The fixture's stable already points at v1. Without the passkey the
+ // waiver does not happen.
+ status, loc, _ := postForm(t, c, fx.u("/apps/kass/fasttrack"),
url.Values{"csrf": {csrf}, "channel": {"stable"}})
+ if status != http.StatusSeeOther || !strings.HasPrefix(loc, fx.fk.srv.URL) {
+ t.Fatalf("un-stepped-up fast-track = %d -> %q, want a redirect into Keymail", status, loc)
+ }
+ if p := fx.channelVersion(t, "kass", "stable"); p.FastTrack {
+ t.Fatal("the bake window was waived with no passkey confirmation")
+ }
+
+ // With it, the held action resumes and lands on the pointer.
+ if status, _, body := completeStepUp(t, fx, c, loc); status != http.StatusSeeOther {
+ t.Fatalf("resumed fast-track = %d:\n%s", status, body)
+ }
p := fx.channelVersion(t, "kass", "stable")
if !p.FastTrack {
t.Fatalf("pointer = %+v, want fast_track set", p)
@@ -111,6 +187,7 @@ func TestFastTrackIsOwnerPasskeyGatedAndLands(t *testing.T) {
if len(p.Audit) == 0 || !p.Audit[0].FastTrack || p.Audit[0].Actor != "paul@keymail.dev" {
t.Fatalf("audit[0] = %+v, want the waiver recorded with its actor", p.Audit)
}
+ auditedBy(t, fx, "kass", "fast-track", "paul@keymail.dev", "waived the bake window for v1 on stable")
}
// A MEMBER probing fast-track meets the byte-identical 404 — the loud
@@ -124,7 +201,7 @@ func TestFastTrackMemberGets404(t *testing.T) {
c := newBrowser(t)
signUp(t, fx, c, "code-sam-bake3")
csrf := sessionToken(t, fx, c)
- status, _, _ := postForm(t, c, fx.u("/apps/kass/fasttrack"),
+ status, _, memberBody := postForm(t, c, fx.u("/apps/kass/fasttrack"),
url.Values{"csrf": {csrf}, "channel": {"stable"}})
if status != http.StatusNotFound {
t.Fatalf("member fast-tracking = %d, want 404", status)
@@ -132,6 +209,18 @@ func TestFastTrackMemberGets404(t *testing.T) {
if p := fx.channelVersion(t, "kass", "stable"); p.FastTrack {
t.Fatal("a member's post waived the bake window")
}
+ // Byte-identical, not merely same-status: the difference between "not
+ // allowed" and "no such account" is not information this hands out.
+ strangerBody := strangerRefusal(t, fx, "code-zoe-bake3", fx.u("/apps/kass/fasttrack"),
+ url.Values{"channel": {"stable"}})
+ if memberBody != strangerBody {
+ t.Fatalf("the two refusals are not byte-identical:\n(member) %q\n(stranger) %q", memberBody, strangerBody)
+ }
+ // http.NotFound's own, well-known output — pinned so a future change to
+ // either path is caught even if it stays self-consistent.
+ if memberBody != "404 page not found\n" {
+ t.Fatalf("refusal body = %q, want the stdlib http.NotFound output", memberBody)
+ }
}
// The stable-promote prompt (spec §7): enable_bake rides the same
@@ -171,8 +260,16 @@ func TestPromoteBakeNoteFollowsTheFlag(t *testing.T) {
// No flag at all: beta holds, but this app does not bake. (edge first —
// the ladder promotes a build one rung at a time.)
seedChannel(t, fx, "kass", "edge", "v3")
- postForm(t, c, fx.u("/apps/kass/promote"),
- url.Values{"csrf": {csrf}, "version": {"v3"}, "channel": {"beta"}})
+ // Checked, not fired and forgotten: a refused promote would make the
+ // absence assertion below true for the wrong reason.
+ if status, loc, body := postForm(t, c, fx.u("/apps/kass/promote"),
+ url.Values{"csrf": {csrf}, "version": {"v3"}, "channel": {"beta"}}); status != http.StatusSeeOther ||
+ loc != fx.path("/apps/kass") {
+ t.Fatalf("beta promote = %d -> %q:\n%s", status, loc, body)
+ }
+ if p := fx.channelVersion(t, "kass", "beta"); p.Version != "v3" {
+ t.Fatalf("beta = %+v, want v3", p)
+ }
if _, _, body := get(t, c, fx.u("/apps/kass")); strings.Contains(body, "Adopts after the bake window") {
t.Fatalf("a flagless app was promised a bake window:\n%s", body)
}
@@ -184,8 +281,10 @@ func TestPromoteBakeNoteFollowsTheFlag(t *testing.T) {
}
shipVersion(t, fx, "kass", "v4")
seedChannel(t, fx, "kass", "edge", "v4")
- postForm(t, c, fx.u("/apps/kass/promote"),
- url.Values{"csrf": {csrf}, "version": {"v4"}, "channel": {"beta"}})
+ if status, _, body := postForm(t, c, fx.u("/apps/kass/promote"),
+ url.Values{"csrf": {csrf}, "version": {"v4"}, "channel": {"beta"}}); status != http.StatusSeeOther {
+ t.Fatalf("beta promote of v4 = %d:\n%s", status, body)
+ }
if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, "Adopts after the bake window") {
t.Fatalf("a production app's held promotion did not mention the window:\n%s", body)
}
diff --git a/internal/console/server.go b/internal/console/server.go
index eae1c5167..3f2eb2b15 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -1469,8 +1469,14 @@ func (s *Server) handleRollback(w http.ResponseWriter, r *http.Request) {
// controls spec §7). ON is any member's to do, instantly — the safe
// direction stays cheap. OFF removes a supply-chain guard from every
// subscriber of the app, so it is the owner's, behind the passkey, and the
-// fleet ratchets it in on each box's own clock. Gate order is the mandated
-// one: session → CSRF → owner (byte-identical 404) → requireFreshAuth LAST.
+// fleet ratchets it in on each box's own clock.
+//
+// Gate order is the mandated one, and the two owner-side gates apply to the
+// OFF direction ONLY: session/membership (accountApp) → CSRF → signing-key
+// availability → value parse → for OFF: owner (byte-identical 404) →
+// requireFreshAuth LAST. The refusals that cost nothing and reveal nothing
+// come first; the 404 comes before any step-up redirect, which would
+// otherwise confirm to a member-probing-as-stranger that the account exists.
func (s *Server) handleProduction(w http.ResponseWriter, r *http.Request) {
addr, raw, acct, role, app, ok := s.accountApp(w, r)
if !ok {
diff --git a/internal/console/server_test.go b/internal/console/server_test.go
index c959921af..ed6b2731d 100644
--- a/internal/console/server_test.go
+++ b/internal/console/server_test.go
@@ -880,6 +880,20 @@ func TestPromotionsDisabledWithoutKey(t *testing.T) {
url.Values{"csrf": {csrf}, "name": {"x"}}); status != http.StatusServiceUnavailable {
t.Fatalf("keyless canary retire = %d, want 503", status)
}
+ // The bake controls sign too — the flag is a root-signed object and
+ // fast-track re-signs a pointer, so both are mutations a keyless console
+ // must refuse rather than fail halfway through.
+ if status, _, _ := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"on"}}); status != http.StatusServiceUnavailable {
+ t.Fatalf("keyless production flag = %d, want 503", status)
+ }
+ if status, _, _ := postForm(t, c, fx.u("/apps/kass/fasttrack"),
+ url.Values{"csrf": {csrf}, "channel": {"stable"}}); status != http.StatusServiceUnavailable {
+ t.Fatalf("keyless fast-track = %d, want 503", status)
+ }
+ if f, err := release.GetProduction(context.Background(), fx.rel, fx.pub, release.OpsAccount, "kass"); err != nil || f != nil {
+ t.Fatalf("keyless console wrote a production flag: %+v (%v)", f, err)
+ }
if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v1" {
t.Fatalf("keyless console moved a pointer to %s", p.Version)
}