carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3// The action confirm interstitial (confirm.go). The properties under test are
4// the ones the file's own doc comment calls load-bearing: the card describes a
5// request that would otherwise proceed, it never renders for someone the owner
6// 404 is meant to turn away, and it sits strictly BETWEEN that 404 and the
7// passkey — so an owner sees the card first and is asked for the passkey only
8// on the confirmed replay.
9//
10// Every card test also lands the confirmed POST. A card that renders but whose
11// resubmit does not carry the action through is a dead end, and asserting only
12// on the markup would not notice.
13
14import (
15 "context"
16 "errors"
17 "net/http"
18 "net/url"
19 "os"
20 "strings"
21 "testing"
22 "time"
23
24 "github.com/carlosframework/platform/internal/registry"
25 "github.com/carlosframework/platform/internal/release"
26)
27
28// confirmCard asserts the response IS a confirm card — 200, the titled
29// heading, the confirmed=1 marker the resubmit turns on, the CSRF token
30// carried through, and a submit button — and hands the body back for the
31// card-specific assertions. Everything here is what makes the card a working
32// resubmit rather than a screenshot of one.
33func confirmCard(t *testing.T, status int, body, title, csrf string) string {
34 t.Helper()
35 if status != http.StatusOK {
36 t.Fatalf("confirm step = %d, want a 200 card:\n%s", status, body)
37 }
38 if !strings.Contains(body, title) {
39 t.Fatalf("card is not titled %q:\n%s", title, body)
40 }
41 if !strings.Contains(body, `<input type="hidden" name="confirmed" value="1">`) {
42 t.Fatalf("card carries no confirmed=1 marker, so its button cannot land the action:\n%s", body)
43 }
44 if !strings.Contains(body, `<input type="hidden" name="csrf" value="`+csrf+`">`) {
45 t.Fatalf("card carries no csrf field, so its resubmit would be refused:\n%s", body)
46 }
47 if !strings.Contains(body, `<button type="submit"`) {
48 t.Fatalf("card has no submit button:\n%s", body)
49 }
50 return body
51}
52
53// notACard is the negative: whatever this response is, it must not be an
54// interstitial. Used wherever a card would concede that an action exists for
55// somebody (the owner-only 404s) or that it would otherwise proceed (the
56// cheap refusals).
57func notACard(t *testing.T, body string) {
58 t.Helper()
59 if strings.Contains(body, `name="confirmed"`) || strings.Contains(body, "confirm-card") {
60 t.Fatalf("a confirm card was rendered where none may be:\n%s", body)
61 }
62}
63
64// The ordinary promote: the unconfirmed POST is answered with the card and
65// changes nothing; the same POST plus confirmed=1 moves the channel.
66func TestConfirmPromoteCardThenLands(t *testing.T) {
67 fx := newConsoleFixture(t)
68 shipVersion(t, fx, "kass", "v3")
69 seedChannel(t, fx, "kass", "edge", "v3") // beta is entered from edge
70 c := signInPaul(t, fx)
71 csrf := sessionToken(t, fx, c)
72 form := url.Values{"csrf": {csrf}, "channel": {"beta"}, "version": {"v3"}}
73
74 status, _, body := postForm(t, c, fx.u("/apps/kass/promote"), form)
75 card := confirmCard(t, status, body, "Promote v3 to beta?", csrf)
76 // The card carries what the resubmit needs to be the SAME request.
77 for _, want := range []string{
78 `<input type="hidden" name="channel" value="beta">`,
79 `<input type="hidden" name="version" value="v3">`,
80 } {
81 if !strings.Contains(card, want) {
82 t.Fatalf("card does not carry %s:\n%s", want, card)
83 }
84 }
85 if !strings.Contains(card, "Promote v3") {
86 t.Fatalf("the card's button does not name the promotion:\n%s", card)
87 }
88 // A card is a question, not a deferral: beta does not exist yet.
89 if _, err := release.Channel(context.Background(), fx.rel, fx.pub,
90 release.OpsAccount, "kass", "beta"); !errors.Is(err, os.ErrNotExist) {
91 t.Fatalf("beta = %v after an UNconfirmed promote, want it still absent", err)
92 }
93
94 form.Set("confirmed", "1")
95 status, loc, body := postForm(t, c, fx.u("/apps/kass/promote"), form)
96 if status != http.StatusSeeOther || loc != fx.path("/apps/kass") {
97 t.Fatalf("confirmed promote = %d -> %q, want 303 to the app page:\n%s", status, loc, body)
98 }
99 if p := fx.channelVersion(t, "kass", "beta"); p.Version != "v3" {
100 t.Fatalf("beta = %s after a confirmed promote, want v3", p.Version)
101 }
102 if _, _, page := get(t, c, fx.u("/apps/kass")); !strings.Contains(page, "Promoted v3 to beta.") {
103 t.Fatalf("no promote flash:\n%s", page)
104 }
105}
106
107// The stable-target card is the one with a real choice on it: bake is off for
108// this app, so the card offers to turn it on. And the card is strictly for
109// owners — a member probing the stable rung meets the byte-identical 404 with
110// no card in it, because a card would concede the action exists for somebody.
111func TestConfirmStablePromoteOffersBakeAndHidesFromMembers(t *testing.T) {
112 fx := newConsoleFixture(t)
113 shipVersion(t, fx, "kass", "v3")
114 seedChannel(t, fx, "kass", "beta", "v3") // stable is entered from beta
115 c := signInPaul(t, fx)
116 csrf := sessionToken(t, fx, c)
117
118 status, loc, body := postForm(t, c, fx.u("/apps/kass/promote"),
119 url.Values{"csrf": {csrf}, "channel": {"stable"}, "version": {"v3"}})
120 // The card comes BEFORE the passkey: an owner is asked "do you mean it?"
121 // first, and only the confirmed replay meets requireFreshAuth.
122 if strings.HasPrefix(loc, fx.fk.srv.URL) {
123 t.Fatalf("the owner was sent to Keymail before the card was shown (-> %q)", loc)
124 }
125 card := confirmCard(t, status, body, "Promote v3 to stable?", csrf)
126 if !strings.Contains(card, `name="enable_bake"`) {
127 t.Fatalf("the stable card carries no enable_bake choice for a bake-off app:\n%s", card)
128 }
129 if !strings.Contains(card, "puts v3 in front of everyone") {
130 t.Fatalf("the stable card does not warn what stable means:\n%s", card)
131 }
132 if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v1" {
133 t.Fatalf("stable moved to %s on an unconfirmed post", p.Version)
134 }
135
136 // A member: the byte-identical 404, and not a card in sight.
137 if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
138 t.Fatal(err)
139 }
140 fx.setUser("sam@keymail.dev")
141 cm := newBrowser(t)
142 signUp(t, fx, cm, "code-sam-confirm-stable")
143 mcsrf := sessionToken(t, fx, cm)
144 status, _, memberBody := postForm(t, cm, fx.u("/apps/kass/promote"),
145 url.Values{"csrf": {mcsrf}, "channel": {"stable"}, "version": {"v3"}})
146 if status != http.StatusNotFound {
147 t.Fatalf("member promoting to stable = %d, want 404", status)
148 }
149 notACard(t, memberBody)
150 strangerBody := strangerRefusal(t, fx, "code-zoe-confirm-stable", fx.u("/apps/kass/promote"),
151 url.Values{"channel": {"stable"}, "version": {"v3"}})
152 if memberBody != strangerBody {
153 t.Fatalf("the two refusals are not byte-identical:\n(member) %q\n(stranger) %q", memberBody, strangerBody)
154 }
155 if memberBody != "404 page not found\n" {
156 t.Fatalf("refusal body = %q, want the stdlib http.NotFound output", memberBody)
157 }
158}
159
160// TestConfirmPromoteCardIsPipelineAware: the HoldFor sweep (release
161// pipelines v2, Task 12) — the confirm card must never tell a member
162// something the boxes will not do. A pipelined channel's bake comes from
163// ITS OWN window, on a name the legacy rung table has never heard of; the
164// legacy enable_bake checkbox — which only ever flips the production flag a
165// pipelined app's boxes ignore (spec §3) — has nothing to offer here and
166// must not appear; and the passkey note follows the channel's OWN rule
167// rather than the legacy "stable" name.
168func TestConfirmPromoteCardIsPipelineAware(t *testing.T) {
169 fx, c, csrf := memberFixture(t) // paul owns, kim is a member
170 pipelinedApp(t, fx, "widget", "abc1234",
171 release.PipelineChannel{Name: "edge"},
172 release.PipelineChannel{Name: "production", Bake: "24h", Rules: release.ChannelRules{Passkey: true}})
173
174 // Any member — not just the owner — reaches the card: the pipeline's own
175 // rules govern this channel, not the legacy stable-is-owner-only gate.
176 status, _, body := postForm(t, c, fx.u("/apps/widget/promote"),
177 url.Values{"csrf": {csrf}, "channel": {"production"}, "version": {"abc1234"}})
178 card := confirmCard(t, status, body, "Promote abc1234 to production?", csrf)
179 if !strings.Contains(card, "waits ~24h on production") {
180 t.Fatalf("card does not carry the pipeline's OWN 24h window:\n%s", card)
181 }
182 if strings.Contains(card, `name="enable_bake"`) {
183 t.Fatalf("a pipelined channel's card offered the legacy enable_bake checkbox, which its boxes ignore:\n%s", card)
184 }
185 if !strings.Contains(card, "really you with your passkey") {
186 t.Fatalf("channel production carries a passkey rule but the card said nothing about it:\n%s", card)
187 }
188
189 // An unruled channel of the same pipeline (edge: no bake, no rules)
190 // carries neither line — nothing to wait for, nothing to confirm.
191 status, _, body = postForm(t, c, fx.u("/apps/widget/promote"),
192 url.Values{"csrf": {csrf}, "channel": {"edge"}, "version": {"abc1234"}})
193 card = confirmCard(t, status, body, "Promote abc1234 to edge?", csrf)
194 if strings.Contains(card, "safety delay") {
195 t.Fatalf("edge has no bake window in this pipeline, but the card mentions one:\n%s", card)
196 }
197 if strings.Contains(card, "your passkey") {
198 t.Fatalf("edge carries no passkey rule, but the card promised one:\n%s", card)
199 }
200}
201
202// The rollback card names BOTH versions — a rollback card that cannot say
203// where it is going has no business asking anyone to confirm it — and the
204// confirmed post re-points the channel.
205func TestConfirmRollbackCardShowsBothVersions(t *testing.T) {
206 fx := newConsoleFixture(t)
207 // stable: v1 (fixture) then v2, so the pointer has a history to go back to.
208 seedChannel(t, fx, "kass", "stable", "v2")
209 c := signInPaul(t, fx)
210 csrf := sessionToken(t, fx, c)
211 form := url.Values{"csrf": {csrf}, "channel": {"stable"}}
212
213 status, _, body := postForm(t, c, fx.u("/apps/kass/rollback"), form)
214 card := confirmCard(t, status, body, "Roll back stable to v1?", csrf)
215 if !strings.Contains(card, "stable serves v2 now.") {
216 t.Fatalf("the card does not say where the channel is now:\n%s", card)
217 }
218 if !strings.Contains(card, "Roll back to v1") {
219 t.Fatalf("the card's button does not name the target version:\n%s", card)
220 }
221 if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v2" {
222 t.Fatalf("stable = %s on an unconfirmed rollback, want it untouched at v2", p.Version)
223 }
224
225 form.Set("confirmed", "1")
226 // stable is gated (owner + step-up, mirroring promoteGate's legacy path
227 // — Task 8/9 review), so the confirmed post walks the passkey round trip.
228 if status, _, body := postGated(t, fx, c, fx.u("/apps/kass/rollback"), form); status != http.StatusSeeOther {
229 t.Fatalf("confirmed rollback = %d:\n%s", status, body)
230 }
231 if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v1" {
232 t.Fatalf("stable = %s after a confirmed rollback, want v1", p.Version)
233 }
234 if _, _, page := get(t, c, fx.u("/apps/kass")); !strings.Contains(page, "Rolled back stable to v1.") {
235 t.Fatalf("no rollback flash:\n%s", page)
236 }
237}
238
239// Fast-track is the whole gate order in one handler: member → 404 with no
240// card; owner → card BEFORE any Keymail round trip; confirmed owner → the
241// passkey, and only then the waiver.
242func TestConfirmFastTrackCardComesBeforeStepUp(t *testing.T) {
243 fx := newConsoleFixture(t)
244 if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
245 t.Fatal(err)
246 }
247 fx.setUser("sam@keymail.dev")
248 cm := newBrowser(t)
249 signUp(t, fx, cm, "code-sam-confirm-ft")
250 mcsrf := sessionToken(t, fx, cm)
251 status, _, memberBody := postForm(t, cm, fx.u("/apps/kass/fasttrack"),
252 url.Values{"csrf": {mcsrf}, "channel": {"stable"}})
253 if status != http.StatusNotFound {
254 t.Fatalf("member fast-tracking = %d, want 404", status)
255 }
256 notACard(t, memberBody)
257 if memberBody != "404 page not found\n" {
258 t.Fatalf("refusal body = %q, want the stdlib http.NotFound output", memberBody)
259 }
260
261 fx.setUser("paul@keymail.dev")
262 c := signInPaul(t, fx)
263 csrf := sessionToken(t, fx, c)
264 form := url.Values{"csrf": {csrf}, "channel": {"stable"}}
265
266 status, loc, body := postForm(t, c, fx.u("/apps/kass/fasttrack"), form)
267 if strings.HasPrefix(loc, fx.fk.srv.URL) {
268 t.Fatalf("the owner met the passkey before the card (-> %q) — confirm asks \"do you mean it?\" first", loc)
269 }
270 card := confirmCard(t, status, body, "Skip the safety delay for v1 on stable?", csrf)
271 if !strings.Contains(card, "The safety delay is the window where a new build proves itself") {
272 t.Fatalf("the fast-track card carries no warning:\n%s", card)
273 }
274 if p := fx.channelVersion(t, "kass", "stable"); p.FastTrack {
275 t.Fatal("the window was waived by merely asking")
276 }
277
278 // The confirmed replay is where the passkey is demanded — and where it lands.
279 form.Set("confirmed", "1")
280 status, loc, _ = postForm(t, c, fx.u("/apps/kass/fasttrack"), form)
281 if status != http.StatusSeeOther || !strings.HasPrefix(loc, fx.fk.srv.URL) {
282 t.Fatalf("confirmed fast-track = %d -> %q, want a redirect into Keymail", status, loc)
283 }
284 if p := fx.channelVersion(t, "kass", "stable"); p.FastTrack {
285 t.Fatal("the window was waived with no passkey confirmation")
286 }
287 if status, _, body := completeStepUp(t, fx, c, loc); status != http.StatusSeeOther {
288 t.Fatalf("resumed fast-track = %d:\n%s", status, body)
289 }
290 if p := fx.channelVersion(t, "kass", "stable"); !p.FastTrack {
291 t.Fatalf("pointer = %+v after the confirmed, stepped-up waiver, want fast_track set", p)
292 }
293}
294
295// The retire card states exactly the two facts the handler established before
296// asking — the canary exists, and nothing follows it — so neither a canary
297// with a host on it nor a name that was never a canary gets one. The confirmed
298// post deletes the pointer.
299func TestConfirmRetireCanaryCardAndDeletion(t *testing.T) {
300 fx := newConsoleFixture(t)
301 c := signInPaul(t, fx)
302 csrf := sessionToken(t, fx, c)
303
304 // No such canary: a refusal, never a card describing a deletion that
305 // could not happen.
306 status, _, body := postForm(t, c, fx.u("/apps/kass/canary/retire"),
307 url.Values{"csrf": {csrf}, "name": {"nope"}})
308 if status != http.StatusSeeOther {
309 t.Fatalf("retire of a missing canary = %d, want the flash redirect:\n%s", status, body)
310 }
311 notACard(t, body)
312
313 // The fixture's canary/x exists and nothing follows it: the card.
314 form := url.Values{"csrf": {csrf}, "name": {"x"}}
315 status, _, body = postForm(t, c, fx.u("/apps/kass/canary/retire"), form)
316 card := confirmCard(t, status, body, "Retire canary x?", csrf)
317 for _, want := range []string{
318 "Deletes channel canary/x",
319 "No host follows it",
320 `<input type="hidden" name="name" value="x">`,
321 } {
322 if !strings.Contains(card, want) {
323 t.Fatalf("the retire card is missing %q:\n%s", want, card)
324 }
325 }
326 if p := fx.channelVersion(t, "kass", "canary/x"); p.Version != "v2" {
327 t.Fatalf("canary/x = %+v after an unconfirmed retire, want it untouched", p)
328 }
329
330 form.Set("confirmed", "1")
331 if status, _, body := postForm(t, c, fx.u("/apps/kass/canary/retire"), form); status != http.StatusSeeOther {
332 t.Fatalf("confirmed retire = %d:\n%s", status, body)
333 }
334 if _, err := release.Channel(context.Background(), fx.rel, fx.pub,
335 release.OpsAccount, "kass", "canary/x"); !errors.Is(err, os.ErrNotExist) {
336 t.Fatalf("canary/x = %v after a confirmed retire, want it gone", err)
337 }
338
339 // A canary with a follower never reaches the card at all — the refusal
340 // fires first, and a card here would be describing a deletion the handler
341 // has already decided against.
342 seedChannel(t, fx, "kass", "canary/y", "v2")
343 fx.addRoute(t, registry.Route{Host: "rc.kass.carlosapp.dev", Kind: "instance", Status: "ready",
344 Version: "v2", App: "kass", Channel: "canary/y", Addr: "127.0.0.1:9021"})
345 status, _, body = postForm(t, c, fx.u("/apps/kass/canary/retire"),
346 url.Values{"csrf": {csrf}, "name": {"y"}})
347 if status != http.StatusSeeOther {
348 t.Fatalf("retire of a followed canary = %d, want the flash redirect:\n%s", status, body)
349 }
350 notACard(t, body)
351 if p := fx.channelVersion(t, "kass", "canary/y"); p.Version != "v2" {
352 t.Fatalf("canary/y = %+v, want the followed canary untouched", p)
353 }
354 if _, _, page := get(t, c, fx.u("/apps/kass")); !strings.Contains(page,
355 "still has a host (rc.kass.carlosapp.dev) following it") {
356 t.Fatalf("no follower refusal flash:\n%s", page)
357 }
358}
359