| 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) |
| } |