[OCaml] High Intensity Training Online
test verify POST-redirect-GET and document the policy
Add a focused test that a state-changing POST answers 303, renders no page body, and carries its success message to the redirected GET as a one-shot session flash that is gone on reload. Document the policy in the architecture: every form uses POST-redirect-GET to the originating page; a transient result rides the session flash or a query flag, while field-level validation re-renders the form in place with a 400 so entered values and per-field messages survive without breaking the redirect rule. The audit found every state-changing POST already redirects; this records the decision and guards it with a test.
Changed files
ARCHITECTURE.org
@@ -443,6 +443,19 @@
443
443
valid, whether recovery is complete, or whether a correction is
444
444
volume.
445
445
446
Added:
*Every form uses POST-redirect-GET.* A state-changing POST answers with
447
Added:
a 303 redirect to the page that started the flow — the workout, the
448
Added:
logbook, the profile, or the app-feedback tab — so a refresh never
449
Added:
re-posts. The redirect target carries any message two ways: a transient
450
Added:
success or error note rides a one-shot session flash (=hito.flash=),
451
Added:
read once and shown as a =data-hito-toast=; a confirmation that must
452
Added:
survive a bookmarked reload rides a query flag such as
453
Added:
=/profile?changed=username=. Field-level validation is the one
454
Added:
exception: a rejected login, registration, or profile edit re-renders
455
Added:
its form in place with a =400= status, so the entered values and the
456
Added:
per-field messages survive. That response carries no history entry and
457
Added:
never mutates state, so it does not break the redirect rule.
458
Added:
446
459
* Running it
447
460
448
461
#+begin_src sh
test/test_web.ml
@@ -491,6 +491,32 @@
491
491
] );
492
492
( "web.flow",
493
493
[
494
Added:
( "a state-changing POST redirects and carries a flash to the GET",
495
Added:
`Quick,
496
Added:
fun () ->
497
Added:
(* POST-redirect-GET: selecting a routine answers 303, and the
498
Added:
success message rides a one-shot flash onto the redirected page
499
Added:
rather than rendering inline on the POST response. *)
500
Added:
let c = client () in
501
Added:
let _ = sign_in_new c in
502
Added:
let token = Option.get (csrf_token (body (get c "/"))) in
503
Added:
let selection =
504
Added:
post c "/routines/ideal/select" [ ("dream.csrf", token) ]
505
Added:
in
506
Added:
Alcotest.(check int) "selection redirects" 303 (status selection);
507
Added:
Alcotest.(check bool)
508
Added:
"the POST body renders no page" true
509
Added:
(String.length (body selection) = 0
510
Added:
|| not (contains ~substring:"<h1" (body selection)));
511
Added:
let home_page = body (get c "/") in
512
Added:
Alcotest.(check bool)
513
Added:
"the redirected GET shows the flash toast" true
514
Added:
(contains ~substring:"data-hito-toast" home_page
515
Added:
&& contains ~substring:"Routine selected." home_page);
516
Added:
let home_again = body (get c "/") in
517
Added:
Alcotest.(check bool)
518
Added:
"the flash is one-shot and gone on reload" false
519
Added:
(contains ~substring:"Routine selected." home_again) );
494
520
( "a signed-in trainee starts a workout and sees the exercise buttons",
495
521
`Quick,
496
522
fun () ->