(** Web tier tests. Sessions and CSRF are exercised end to end: a small client carries the session cookie between requests and pulls the CSRF token out of the rendered page, exactly as a browser would. The in-memory repository stands in for SQLite; session storage is Dream's in-memory back end. *) open Hito_app module Handlers = Hito_web.Handlers.Make (Memory_repo) (* A fresh, fully wired application: secret, in-memory sessions, routes. Registration is opened here so the auth-flow tests can create trainees via [/register]; production keeps it closed. *) let app () = let handlers = Handlers.make ~repo:(Memory_repo.create ()) ~registration_open:true () in Dream.memory_sessions @@ Dream.router (Handlers.routes handlers) |> fun handler -> Dream.set_secret "test-secret-value" handler let status response = Dream.status response |> Dream.status_to_int let body response = Lwt_main.run (Dream.body response) let contains ~substring string = let n = String.length substring in let rec at i = if i + n > String.length string then false else if String.sub string i n = substring then true else at (i + 1) in at 0 (* --- a cookie-carrying client --- *) (* Extract cookies (name=value) from all Set-Cookie response headers. *) let cookies_of response = Dream.headers response "Set-Cookie" |> List.filter_map (fun sc -> match String.index_opt sc ';' with | Some i -> Some (String.sub sc 0 i) | None -> Some sc) type client = { app : Dream.handler; mutable jar : string list } let client () = { app = app (); jar = [] } let cookie_header client = if client.jar = [] then [] else [ ("Cookie", String.concat "; " client.jar) ] let remember client response = (* Replace cookies of the same name; keep the rest. *) List.iter (fun fresh -> let name = match String.index_opt fresh '=' with | Some i -> String.sub fresh 0 i | None -> fresh in client.jar <- fresh :: List.filter (fun old -> not (String.length old >= String.length name && String.sub old 0 (String.length name) = name && (String.length old = String.length name || old.[String.length name] = '='))) client.jar) (cookies_of response) let get client target = let response = Dream.test client.app (Dream.request ~method_:`GET ~target ~headers:(cookie_header client) "") in remember client response; response let post client target fields = let body = fields |> List.map (fun (k, v) -> Dream.to_percent_encoded k ^ "=" ^ Dream.to_percent_encoded v) |> String.concat "&" in let response = Dream.test client.app (Dream.request ~method_:`POST ~target ~headers: (("Content-Type", "application/x-www-form-urlencoded") :: cookie_header client) body) in remember client response; response (* A tiny substring search, since Str is not a dependency here. *) let index_from ~needle haystack start = let n = String.length needle and h = String.length haystack in let rec at i = if i + n > h then None else if String.sub haystack i n = needle then Some i else at (i + 1) in if start < 0 then None else at start (* Pull the CSRF token value out of a rendered form. *) let csrf_token html = match index_from ~needle:"name=\"dream.csrf\"" html 0 with | None -> None | Some idx -> ( match index_from ~needle:"value=\"" html idx with | None -> None | Some v -> let start = v + String.length "value=\"" in let stop = String.index_from html start '"' in Some (String.sub html start (stop - start))) let register client ~username ~password = let page = body (get client "/register") in let token = Option.get (csrf_token page) in post client "/register" [ ("dream.csrf", token); ("username", username); ("password", password) ] let sign_in_new client = register client ~username:"lifter" ~password:"heavyduty1" (* Sign in through the credential form, as a returning trainee would. Unlike [register], this exercises the [POST /login] path. *) let sign_in client ~username ~password = let page = body (get client "/login") in let token = Option.get (csrf_token page) in post client "/login" [ ("dream.csrf", token); ("username", username); ("password", password) ] (* An application whose clock the test drives. A shared [Memory_repo] and a mutable [now] cell let a deep test advance time across the recovery gate, which the wall-clock default cannot. Registration is open so the test can create the trainee it drives. *) let clocked_client () = let now = ref 0 in let handlers = Handlers.make ~repo:(Memory_repo.create ()) ~now:(fun () -> Recovery.timestamp_of_unix_seconds !now) ~registration_open:true () in let app = Dream.memory_sessions @@ Dream.router (Handlers.routes handlers) |> fun h -> Dream.set_secret "test-secret-value" h in ({ app; jar = [] }, now) let day n = n * 86_400 (* Drive a trainee to the start of a fresh Day 1 workout: select the ideal routine and begin without an override. Returns the client. *) let start_day_one c = let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in post c "/workout" [ ("dream.csrf", token); ("override", "false") ] (* Record every slot of the in-progress Day 1 workout, one at a time, addressed by [?slot=]. Slot 0 is the flyes/incline-press pair; slot 3 is the french-press/dips pair; slots 1 and 2 are single lateral movements. *) let record_all_day_one_slots c = let record_pair slot comp_load = let token = Option.get (csrf_token (body (get c ("/workout?slot=" ^ slot)))) in post c ("/workout/slots/" ^ slot) [ ("dream.csrf", token); ("iso_load", "20"); ("iso_reps", "9"); ("comp_load", comp_load); ("comp_reps", "7"); ("extension", ""); ] in let record_single slot load reps = let token = Option.get (csrf_token (body (get c ("/workout?slot=" ^ slot)))) in post c ("/workout/slots/" ^ slot) [ ("dream.csrf", token); ("load", load); ("reps", reps); ("extension", ""); ] in let _ = record_pair "0" "60" in let _ = record_single "1" "12" "8" in let _ = record_single "2" "10" "9" in let _ = record_pair "3" "0" in () let route_tests = [ ( "web.layout", [ ( "the desktop main surface is narrower than the shell", `Quick, fun () -> let c = client () in let stylesheet = body (get c "/assets/hito.css") in Alcotest.(check bool) "desktop page surface cap" true (contains ~substring:"@media (min-width: 42.0625rem)" stylesheet && contains ~substring:"max-width: 52rem" stylesheet && contains ~substring:"margin-inline: auto" stylesheet) ); ] ); ( "web.auth", [ ( "an unauthenticated visit to the overview redirects to sign-in", `Quick, fun () -> let c = client () in let response = get c "/" in Alcotest.(check int) "redirect" 303 (status response) ); ( "the sign-in page renders a CSRF-protected form", `Quick, fun () -> let c = client () in let page = body (get c "/login") in Alcotest.(check bool) "has csrf field" true (contains ~substring:"dream.csrf" page); Alcotest.(check bool) "has a password field" true (contains ~substring:"type=\"password\"" page); Alcotest.(check bool) "starts the main content with a page-level heading" true (contains ~substring:"

Sign in

" page); Alcotest.(check bool) "loads the app client and marks the sign-in form" true (contains ~substring:"data-hito-app-shell" page && contains ~substring:"data-hito-app-form" page && contains ~substring:"/assets/workout-client.js" page); Alcotest.(check bool) "offers a skip link to the main landmark" true (contains ~substring:"skip-link" page && contains ~substring:"href=\"#main-content\"" page && contains ~substring:"id=\"main-content\"" page) ); ( "registration signs the trainee in and reaches the overview", `Quick, fun () -> let c = client () in let registered = sign_in_new c in Alcotest.(check int) "registered, redirected" 303 (status registered); let overview = get c "/" in Alcotest.(check int) "authenticated overview" 200 (status overview); Alcotest.(check bool) "shows the routine catalog" true (contains ~substring:"Routines" (body overview)); Alcotest.(check bool) "starts the overview with a page-level heading" true (contains ~substring:"

Routines

" (body overview)); Alcotest.(check bool) "labels the logbook tab Logbook" true (contains ~substring:">Logbook" (body overview)); Alcotest.(check bool) "shows the username on a profile control opening the profile page" true (contains ~substring:"class=\"profile\"" (body overview) && contains ~substring:"href=\"/profile\"" (body overview) && contains ~substring:">lifter" (body overview)); Alcotest.(check bool) "omits the header username" false (contains ~substring:"account-username" (body overview)); Alcotest.(check bool) "declares the document language" true (contains ~substring:"" (body overview)); Alcotest.(check bool) "marks the overview as the current navigation destination" true (contains ~substring:"aria-current=\"page\"" (body overview)); Alcotest.(check bool) "shows primary navigation" true (contains ~substring:"primary-nav" (body overview)); Alcotest.(check bool) "shows bottom navigation" true (contains ~substring:"bottom-nav" (body overview)) ); ( "authenticated pages carry inline nav and a header sign-out", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let page = body (get c "/") in (* Inline affordances: the primary nav and the header sign-out form both exist in the markup, ready for responsive CSS to reveal or hide them. *) Alcotest.(check bool) "keeps the inline primary navigation" true (contains ~substring:"class=\"primary-nav\"" page); Alcotest.(check bool) "keeps the header sign-out" true (contains ~substring:"class=\"account\"" page); (* No hamburger menu at any width: the disclosure and its classes are gone. *) Alcotest.(check bool) "renders no hamburger menu" false (contains ~substring:"class=\"menu\"" page); Alcotest.(check bool) "renders no menu toggle" false (contains ~substring:"menu-toggle" page); Alcotest.(check bool) "renders no menu panel" false (contains ~substring:"menu-panel" page) ); ( "the header sign-out is a CSRF-protected control", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let page = body (get c "/") in (* Isolate the account area so the assertions read the sign-out control rather than another form on the page. *) let account = let start = Option.get (index_from ~needle:"class=\"account\"" page 0) in let stop = Option.get (index_from ~needle:"" page start) in String.sub page start (stop - start) in Alcotest.(check bool) "carries a sign-out form" true (contains ~substring:"action=\"/logout\"" account); Alcotest.(check bool) "the sign-out is CSRF-protected" true (contains ~substring:"dream.csrf" account); Alcotest.(check bool) "the sign-out is labelled" true (contains ~substring:"Sign out" account) ); ( "navigation offers three actions that toggle with logging", `Quick, fun () -> let c = client () in let _ = sign_in_new c in (* Not logging: the middle action is Routine, labelled "Routine", and no workout link is present. The three actions are Home, Routine, and the username history link. *) let idle = body (get c "/") in Alcotest.(check bool) "labels the home action" true (contains ~substring:">Home" idle); Alcotest.(check bool) "offers the routine action when idle" true (contains ~substring:">Routine" idle); Alcotest.(check bool) "hides the current workout action when idle" false (contains ~substring:">Current workout" idle); (* Start a workout, then the middle action becomes Current workout, pointing at /workout, and the standalone Routine action is gone. *) let token = Option.get (csrf_token idle) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let home_page = body (get c "/") in let token = Option.get (csrf_token home_page) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let logging = body (get c "/logbook") in Alcotest.(check bool) "swaps in the current workout action while logging" true (contains ~substring:">Current workout" logging); Alcotest.(check bool) "drops the routine action while logging" false (contains ~substring:">Routine" logging); (* Home shows an in-progress card that links to the current workout, rather than the recovery gate or the workout form. *) let home_logging = body (get c "/") in Alcotest.(check bool) "home shows the in-progress card while logging" true (contains ~substring:"in-progress-card" home_logging); Alcotest.(check bool) "the in-progress card links to the current workout" true (contains ~substring:"href=\"/workout\"" home_logging) ); ( "only the app shell carries the page class, so active state is single", `Quick, fun () -> (* Regression guard for the double-highlight bug: while a workout is in progress, the body must not carry a stale page- class. Only the app-shell div holds the page class, which the client replaces on navigation, so exactly one bottom-nav action can be active. *) let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let logbook = body (get c "/logbook") in Alcotest.(check bool) "the body carries no page class" false (contains ~substring:"class=\"hito-app page-" logbook); Alcotest.(check bool) "the app shell marks the logbook page" true (contains ~substring:"app-shell page-logbook" logbook); Alcotest.(check bool) "the app shell does not also mark the workout page" false (contains ~substring:"app-shell page-workout" logbook) ); ( "auth pages omit the hamburger menu", `Quick, fun () -> let c = client () in let page = body (get c "/register") in Alcotest.(check bool) "no hamburger menu" false (contains ~substring:"class=\"menu\"" page) ); ( "the sign-in page renders a username field", `Quick, fun () -> let c = client () in let page = body (get c "/login") in Alcotest.(check bool) "has a username field" true (contains ~substring:"name=\"username\"" page) ); ( "auth pages omit navigation", `Quick, fun () -> let c = client () in let page = body (get c "/register") in Alcotest.(check bool) "no primary navigation" false (contains ~substring:"primary-nav" page); Alcotest.(check bool) "no bottom navigation" false (contains ~substring:"bottom-nav" page) ); ( "a form post without a CSRF token is refused", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let response = post c "/routines/ideal/select" [ (* no dream.csrf *) ] in Alcotest.(check int) "redirects after the refused post" 303 (status response); let page = body (get c "/") in Alcotest.(check bool) "preserves the CSRF error as a toast" true (contains ~substring:"The submitted form is not valid." page) ); ( "registering a too-short username is refused with a message", `Quick, fun () -> let c = client () in let response = register c ~username:"abc" ~password:"heavyduty1" in Alcotest.(check int) "rejected" 400 (status response); Alcotest.(check bool) "explains the minimum length" true (contains ~substring:"at least 4 characters" (body response)); Alcotest.(check bool) "announces the error" true (contains ~substring:"role=\"alert\"" (body response)) ); ( "registering a too-long username is refused with a message", `Quick, fun () -> let c = client () in let response = register c ~username:"abcdefghijklmnopqrstu" ~password:"heavyduty1" in Alcotest.(check int) "rejected" 400 (status response); Alcotest.(check bool) "explains the maximum length" true (contains ~substring:"at most 20 characters" (body response)) ); ( "registering with a short password succeeds", `Quick, fun () -> let c = client () in let response = register c ~username:"lifter" ~password:"x" in Alcotest.(check int) "registered, redirected" 303 (status response); Alcotest.(check int) "authenticated overview" 200 (status (get c "/")) ); ( "with registration closed the routes are gone and the link is hidden", `Quick, fun () -> (* The production default: [registration_open] unset. The [/register] routes are absent (404) and the sign-in page omits the Register link. *) let handlers = Handlers.make ~repo:(Memory_repo.create ()) () in let app = Dream.memory_sessions @@ Dream.router (Handlers.routes handlers) |> fun h -> Dream.set_secret "test-secret-value" h in let c = { app; jar = [] } in Alcotest.(check int) "GET /register is not found" 404 (status (get c "/register")); let login = body (get c "/login") in Alcotest.(check bool) "no Register link on sign-in" false (contains ~substring:"href=\"/register\"" login) ); ] ); ( "web.flow", [ ( "a state-changing POST redirects and carries a flash to the GET", `Quick, fun () -> (* POST-redirect-GET: selecting a routine answers 303, and the success message rides a one-shot flash onto the redirected page rather than rendering inline on the POST response. *) let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let selection = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in Alcotest.(check int) "selection redirects" 303 (status selection); Alcotest.(check bool) "the POST body renders no page" true (String.length (body selection) = 0 || not (contains ~substring:" let c = client () in let _ = sign_in_new c in let select_page = body (get c "/") in Alcotest.(check bool) "enhances authenticated overview navigation" true (contains ~substring:"data-hito-app-link" select_page && contains ~substring:"data-hito-app-content" select_page && contains ~substring:"data-hito-page-title" select_page && contains ~substring:"/assets/workout-client.js" select_page); Alcotest.(check bool) "marks routine selection for in-place submission" true (contains ~substring:"data-hito-app-form" select_page); let token = Option.get (csrf_token select_page) in let selection = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in Alcotest.(check int) "routine selected" 303 (status selection); let home_page = body (get c "/") in let token = Option.get (csrf_token home_page) in let started = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in Alcotest.(check int) "workout started" 303 (status started); let workout_page = body (get c "/workout") in Alcotest.(check bool) "renders Day 1 as the page heading" true (contains ~substring:"

Day 1

" workout_page); Alcotest.(check bool) "renders an exercise dropdown selector" true (contains ~substring:"exercise-select" workout_page); (* The exercise label stays for screen readers but is visually hidden, so the dropdown reads without a redundant caption. *) Alcotest.(check bool) "keeps the exercise label for the select" true (contains ~substring:"for=\"exercise-choice\"" workout_page); Alcotest.(check bool) "hides the exercise label visually with sr-only" true (contains ~substring:"sr-only" workout_page); Alcotest.(check bool) "does not use the old tab strip class" false (contains ~substring:"slot-tab" workout_page); Alcotest.(check bool) "does not use the old button group class" false (contains ~substring:"exercise-group" workout_page); Alcotest.(check bool) "marks the exercise select for enhancement" true (contains ~substring:"data-hito-exercise-select" workout_page); Alcotest.(check bool) "preselects the active slot option" true (contains ~substring:"selected" workout_page); Alcotest.(check bool) "offers a later slot as a select option" true (contains ~substring:"value=\"1\"" workout_page); Alcotest.(check bool) "opens on the first slot's record form" true (contains ~substring:"/workout/slots/0" workout_page); Alcotest.(check bool) "marks the logging fieldset without a boulder top border" true (contains ~substring:"logging-fieldset" workout_page); Alcotest.(check bool) "marks the current workout as progressively enhanced" true (contains ~substring:"data-hito-workout" workout_page); Alcotest.(check bool) "marks the replaceable workout content" true (contains ~substring:"data-hito-workout-content" workout_page); Alcotest.(check bool) "marks the exercise form for enhancement" true (contains ~substring:"data-hito-exercise-form" workout_page); Alcotest.(check bool) "marks record forms while retaining their CSRF field" true (contains ~substring:"data-hito-workout-form" workout_page && contains ~substring:"dream.csrf" workout_page); Alcotest.(check bool) "loads only the workout client" true (contains ~substring:"/assets/workout-client.js" workout_page); let asset = get c "/assets/workout-client.js" in Alcotest.(check int) "serves the workout client" 200 (status asset); Alcotest.(check bool) "client has JavaScript content type" true (List.mem "application/javascript; charset=utf-8" (Dream.headers asset "Content-Type")) ); ( "the routine page lists workouts in a basic table", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let selection_page = body (get c "/") in let token = Option.get (csrf_token selection_page) in let selection = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in Alcotest.(check int) "routine selected" 303 (status selection); let page = body (get c "/routine") in Alcotest.(check bool) "uses a table" true (contains ~substring:" let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let page = body (get c "/workout") in Alcotest.(check bool) "wraps the fieldset in one shared grid" true (contains ~substring:"class=\"logging-grid\"" page); Alcotest.(check bool) "groups a movement's cells without a nested grid" true (contains ~substring:"class=\"movement\"" page); Alcotest.(check bool) "no longer uses a per-row exercise-fields grid" false (contains ~substring:"class=\"exercise-fields\"" page); Alcotest.(check bool) "still labels load" true (contains ~substring:"Load (kg)" page); Alcotest.(check bool) "hints the load field with its unit" true (contains ~substring:"placeholder=\"kg\"" page); Alcotest.(check bool) "hints the reps field" true (contains ~substring:"placeholder=\"reps\"" page); Alcotest.(check bool) "drops the redundant fieldset legend" false (contains ~substring:"" page) ); ( "a recorded slot can be corrected, replacing it rather than adding \ volume", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in (* Slot 1 is the single Laterals set. Open its tab, record it, then correct it — one slot at a time, addressed by [?slot=]. *) let page = body (get c "/workout?slot=1") in let token = Option.get (csrf_token page) in let recorded = post c "/workout/slots/1" [ ("dream.csrf", token); ("load", "12"); ("reps", "8"); ("extension", ""); ] in Alcotest.(check int) "recorded" 303 (status recorded); (* The default view now opens on slot 0, not the recorded slot. *) let default_page = body (get c "/workout") in Alcotest.(check bool) "the default view does not show the recorded slot's edit form" false (contains ~substring:"/workout/slots/1/edit" default_page); Alcotest.(check bool) "counts one filled slot" true (contains ~substring:"1 of 4 recorded" default_page); (* The recorded slot's tab shows its correction form. *) let page = body (get c "/workout?slot=1") in Alcotest.(check bool) "shows a correction form for the slot" true (contains ~substring:"/workout/slots/1/edit" page); let token = Option.get (csrf_token page) in let corrected = post c "/workout/slots/1/edit" [ ("dream.csrf", token); ("load", "16"); ("reps", "6"); ("extension", ""); ] in Alcotest.(check int) "corrected" 303 (status corrected); let page = body (get c "/workout?slot=1") in Alcotest.(check bool) "still one filled slot, not two" true (contains ~substring:"1 of 4 recorded" page); Alcotest.(check bool) "shows the corrected load" true (contains ~substring:"16" page) ); ( "a saved workout's slot can be corrected from the logbook view", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let token = Option.get (csrf_token (body (get c "/workout?slot=1"))) in let _ = post c "/workout/slots/1" [ ("dream.csrf", token); ("load", "12"); ("reps", "8"); ("extension", ""); ] in let token = Option.get (csrf_token (body (get c "/workout"))) in let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in (* The one saved workout is w1 for this trainee. Its recorded slot is reached through the same [?slot=] state. *) let record_page = body (get c "/logbook/w1?slot=1") in Alcotest.(check bool) "renders an exercise dropdown for the saved workout" true (contains ~substring:"exercise-select" record_page); Alcotest.(check bool) "does not use the old tab strip class" false (contains ~substring:"slot-tab" record_page); Alcotest.(check bool) "does not enhance the saved exercise selector" false (contains ~substring:"data-hito-exercise-form" record_page); Alcotest.(check bool) "does not mark saved history as replaceable workout content" false (contains ~substring:"data-hito-workout-content" record_page); Alcotest.(check bool) "marks saved history as generic app content" true (contains ~substring:"data-hito-app-content" record_page); Alcotest.(check bool) "loads the app client for saved history navigation" true (contains ~substring:"/assets/workout-client.js" record_page); Alcotest.(check bool) "enhances saved-slot corrections through the generic form path" true (contains ~substring:"data-hito-app-form" record_page); Alcotest.(check bool) "offers a saved-slot correction form" true (contains ~substring:"/logbook/w1/slots/1/edit" record_page); let token = Option.get (csrf_token record_page) in let corrected = post c "/logbook/w1/slots/1/edit" [ ("dream.csrf", token); ("load", "20"); ("reps", "7"); ("extension", ""); ] in Alcotest.(check int) "corrected" 303 (status corrected); let record_page = body (get c "/logbook/w1?slot=1") in Alcotest.(check bool) "still one filled slot" true (contains ~substring:"1 of 4 recorded" record_page); Alcotest.(check bool) "shows the corrected load" true (contains ~substring:"20" record_page) ); ( "invalid workout fields identify and describe their errors", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let page = body (get c "/workout") in let token = Option.get (csrf_token page) in let invalid = post c "/workout/slots/0" [ ("dream.csrf", token); ("iso_load", "not-a-number"); ("iso_reps", "8"); ("comp_load", "40"); ("comp_reps", "6"); ("extension", ""); ] in Alcotest.(check int) "invalid field redirects" 303 (status invalid); let invalid_page = body (get c "/workout") in Alcotest.(check bool) "preserves a useful invalid-input error as a toast" true (contains ~substring:"data-hito-toast" invalid_page && contains ~substring:"role=\"status\"" invalid_page && contains ~substring:"Enter a valid number." invalid_page && not (contains ~substring:"error." invalid_page)) ); ( "the workout view shows one slot at a time and defaults to the first \ incomplete slot", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in (* Nothing recorded yet: the default view opens on slot 0 and shows only slot 0's record form, not slot 1's. *) let page = body (get c "/workout") in Alcotest.(check bool) "shows the first slot's form" true (contains ~substring:"action=\"/workout/slots/0\"" page); Alcotest.(check bool) "does not also show a later slot's form" false (contains ~substring:"action=\"/workout/slots/1\"" page); (* Record slot 0. The default now moves to the next incomplete slot, slot 1. *) let token = Option.get (csrf_token page) in let recorded = post c "/workout/slots/0" [ ("dream.csrf", token); ("iso_load", "10"); ("iso_reps", "8"); ("comp_load", "40"); ("comp_reps", "6"); ("extension", ""); ] in Alcotest.(check int) "recorded slot 0" 303 (status recorded); let page = body (get c "/workout") in Alcotest.(check bool) "default now opens on slot 1's record form" true (contains ~substring:"action=\"/workout/slots/1\"" page) ); ( "signing out returns the trainee to the sign-in page", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let page = body (get c "/") in let token = Option.get (csrf_token page) in let out = post c "/logout" [ ("dream.csrf", token) ] in Alcotest.(check int) "logout redirect" 303 (status out); let after = get c "/" in Alcotest.(check int) "overview now redirects" 303 (status after) ); ( "the current workout offers a CSRF-protected cancel control", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let page = body (get c "/workout") in Alcotest.(check bool) "posts to the cancel route" true (contains ~substring:"action=\"/workout/cancel\"" page); Alcotest.(check bool) "labels the control Cancel logging" true (contains ~substring:"Cancel logging" page); Alcotest.(check bool) "renders it as a secondary submit" true (contains ~substring:"class=\"secondary\"" page); Alcotest.(check bool) "carries a confirmation modal for cancellation" true (contains ~substring:"data-hito-confirm-modal" page); Alcotest.(check bool) "marks the cancel form for confirmation" true (contains ~substring:"data-hito-confirm-form" page); Alcotest.(check bool) "uses native navigation after confirmation" true (contains ~substring:"data-hito-cancel-form" page); (* The modal copy must match the discard semantics: cancelling leaves no record. It must not promise a saved, editable entry, which would tell the user the workout was not really cancelled. *) Alcotest.(check bool) "warns that the workout is discarded" true (contains ~substring:"discarded and leaves no record" page); Alcotest.(check bool) "does not promise a saved editable entry" false (contains ~substring:"saved as a partial logbook entry" page) ); ( "the current workout shows a sticky timer bar", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let page = body (get c "/workout") in Alcotest.(check bool) "renders the timer bar while logging" true (contains ~substring:"data-hito-workout-timer" page); Alcotest.(check bool) "carries the workout start epoch" true (contains ~substring:"data-started=" page); let client_script = body (get c "/assets/workout-client.js") in Alcotest.(check bool) "serves the DOM-ready timer startup hook" true (contains ~substring:"DOMContentLoaded" client_script) ); ( "cancelling a workout without a CSRF token leaves the view cleanly", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let response = post c "/workout/cancel" [ (* no dream.csrf *) ] in (* No invalid-form response: cancelling always navigates away to Home. A forged or missing token skips the discard, so the workout remains in progress. *) Alcotest.(check int) "navigates away without an error page" 303 (status response); Alcotest.(check bool) "returns Home" true (List.mem "/" (Dream.headers response "Location")); Alcotest.(check int) "workout still in progress" 200 (status (get c "/workout")) ); ( "cancelling after recording a set discards the workout", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in (* Record the single Laterals set at slot 1. *) let token = Option.get (csrf_token (body (get c "/workout?slot=1"))) in let recorded = post c "/workout/slots/1" [ ("dream.csrf", token); ("load", "12"); ("reps", "8"); ("extension", ""); ] in Alcotest.(check int) "recorded a set" 303 (status recorded); (* Cancel with a valid token. It discards the in-progress workout and returns Home. *) let token = Option.get (csrf_token (body (get c "/workout"))) in let cancelled = post c "/workout/cancel" [ ("dream.csrf", token) ] in Alcotest.(check int) "cancel returns Home" 303 (status cancelled); Alcotest.(check bool) "returns Home, not the logbook" true (List.mem "/" (Dream.headers cancelled "Location")); (* No current workout: the workout view redirects away. *) Alcotest.(check int) "no workout in progress" 303 (status (get c "/workout")); (* The workout left no record: an abandoned session is not evidence, so the logbook holds nothing and no record opens. *) let logbook_page = body (get c "/logbook") in Alcotest.(check bool) "logbook shows no saved workout" false (contains ~substring:"/logbook/w1" logbook_page); Alcotest.(check int) "the discarded workout has no record" 404 (status (get c "/logbook/w1")) ); ( "cancellation returns Home after discarding the workout", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let token = Option.get (csrf_token (body (get c "/workout"))) in let cancelled = post c "/workout/cancel" [ ("dream.csrf", token) ] in Alcotest.(check bool) "cancel redirects to Home" true (List.mem "/" (Dream.headers cancelled "Location")); let home_page = body (get c "/") in Alcotest.(check bool) "Home has no current workout card" false (contains ~substring:"Workout in progress" home_page) ); ( "the saved logbook view does not offer a cancel control", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let token = Option.get (csrf_token (body (get c "/workout"))) in let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in let record_page = body (get c "/logbook/w1") in Alcotest.(check bool) "the saved view has no cancel route" false (contains ~substring:"/workout/cancel" record_page); Alcotest.(check bool) "the saved view has no cancel label" false (contains ~substring:"Cancel logging" record_page); Alcotest.(check bool) "shows primary navigation" true (contains ~substring:"primary-nav" record_page); Alcotest.(check bool) "shows bottom navigation" true (contains ~substring:"bottom-nav" record_page) ); ( "the logbook opens sequential feedback in a modal", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let logbook_page = body (get c "/logbook") in Alcotest.(check bool) "still renders the feedback form" true (contains ~substring:"feedback-form" logbook_page); Alcotest.(check bool) "offers a feedback modal trigger" true (contains ~substring:"data-hito-feedback-open" logbook_page && contains ~substring:"Record feedback" logbook_page); Alcotest.(check bool) "renders a native feedback modal" true (contains ~substring:"data-hito-feedback-modal" logbook_page); Alcotest.(check bool) "renders only the first factor initially" true (contains ~substring:">Sleep" logbook_page && contains ~substring:"1 — very poor" logbook_page && not (contains ~substring:">Appetite" logbook_page)); Alcotest.(check bool) "renders a progress bar and factor skip" true (contains ~substring:" let c = client () in let _ = sign_in_new c in let start = body (get c "/logbook?feedback=start") in Alcotest.(check bool) "start opens the modal" true (contains ~substring:"feedback-modal" start && contains ~substring:">Sleep" start); let token = Option.get (csrf_token start) in let next = post c "/feedback" [ ("dream.csrf", token); ("step", "0"); ("action", "next"); ("choice", "1"); ] in Alcotest.(check int) "first factor redirects" 303 (status next); let appetite = body (get c "/logbook") in Alcotest.(check bool) "moves to appetite with progress" true (contains ~substring:">Appetite" appetite && contains ~substring:"2 of 5 factors" appetite); let token = Option.get (csrf_token appetite) in let skipped = post c "/feedback" [ ("dream.csrf", token); ("step", "1"); ("action", "skip") ] in Alcotest.(check int) "factor skip redirects" 303 (status skipped); let readiness = body (get c "/logbook") in Alcotest.(check bool) "skipping moves to the next factor" true (contains ~substring:">Readiness" readiness && contains ~substring:"3 of 5 factors" readiness); let token = Option.get (csrf_token readiness) in ignore (post c "/feedback" [ ("dream.csrf", token); ("step", "2"); ("action", "next"); ("choice", "3"); ]); let motivation = body (get c "/logbook") in let token = Option.get (csrf_token motivation) in ignore (post c "/feedback" [ ("dream.csrf", token); ("step", "3"); ("action", "next"); ("choice", "5"); ]); let difficulty = body (get c "/logbook") in let token = Option.get (csrf_token difficulty) in let notes = post c "/feedback" [ ("dream.csrf", token); ("step", "4"); ("action", "skip") ] in Alcotest.(check int) "last factor skip redirects" 303 (status notes); let notes_page = body (get c "/logbook") in Alcotest.(check bool) "shows optional observations after factors" true (contains ~substring:">Anything else?" notes_page && contains ~substring:"5 of 5 factors" notes_page); let token = Option.get (csrf_token notes_page) in let saved = post c "/feedback" [ ("dream.csrf", token); ("step", "5"); ("action", "save"); ("pain", "true"); ] in Alcotest.(check int) "final feedback redirects" 303 (status saved); let logbook_page = body (get c "/logbook") in Alcotest.(check bool) "stores answered factors and final observations" true (contains ~substring:"Sleep 1 (very poor)" logbook_page && contains ~substring:"Readiness 3" logbook_page && contains ~substring:"Motivation 5 (very good)" logbook_page && contains ~substring:"Pain" logbook_page); Alcotest.(check bool) "does not store skipped factors" true ((not (contains ~substring:"Appetite" logbook_page)) && not (contains ~substring:"Difficulty" logbook_page)) ); ( "skipping the entire feedback flow records nothing", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let start = body (get c "/logbook?feedback=start") in let token = Option.get (csrf_token start) in let skipped = post c "/feedback/cancel" [ ("dream.csrf", token) ] in Alcotest.(check int) "full-flow skip redirects" 303 (status skipped); let logbook_page = body (get c "/logbook") in Alcotest.(check bool) "shows the skip notice" true (contains ~substring:"Feedback skipped." logbook_page); Alcotest.(check bool) "does not add an empty report" false (contains ~substring:"No signals" logbook_page) ); ( "back returns to the previous factor and keeps answers", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let start = body (get c "/logbook?feedback=start") in Alcotest.(check bool) "the first factor offers no back control" false (contains ~substring:"value=\"back\"" start); let token = Option.get (csrf_token start) in let _ = post c "/feedback" [ ("dream.csrf", token); ("step", "0"); ("action", "next"); ("choice", "5"); ] in let appetite = body (get c "/logbook") in Alcotest.(check bool) "the second factor offers a back control" true (contains ~substring:">Appetite" appetite && contains ~substring:"value=\"back\"" appetite); let token = Option.get (csrf_token appetite) in let back = post c "/feedback" [ ("dream.csrf", token); ("step", "1"); ("action", "back") ] in Alcotest.(check int) "back redirects" 303 (status back); let sleep_again = body (get c "/logbook") in Alcotest.(check bool) "back returns to the first factor" true (contains ~substring:">Sleep" sleep_again && contains ~substring:"1 of 5 factors" sleep_again); (* The earlier answer survives Back. Skipping forward from here never touches it, so saving still stores the sleep signal. *) let rec skip_to_notes page = let token = Option.get (csrf_token page) in let step_of = if contains ~substring:">Sleep" page then 0 else if contains ~substring:">Appetite" page then 1 else if contains ~substring:">Readiness" page then 2 else if contains ~substring:">Motivation" page then 3 else if contains ~substring:">Perceived difficulty" page then 4 else -1 in if step_of < 0 then page else begin let _ = post c "/feedback" [ ("dream.csrf", token); ("step", string_of_int step_of); ("action", "skip"); ] in skip_to_notes (body (get c "/logbook")) end in let notes = skip_to_notes sleep_again in let token = Option.get (csrf_token notes) in let _ = post c "/feedback" [ ("dream.csrf", token); ("step", "5"); ("action", "save") ] in let logbook_page = body (get c "/logbook") in Alcotest.(check bool) "the pre-back answer persists" true (contains ~substring:"Sleep 5 (very good)" logbook_page) ); ( "closing the feedback modal cancels the flow", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let start = body (get c "/logbook?feedback=start") in (* Answer the first factor, then close: the flow is discarded and reopening starts fresh at the first factor. *) let token = Option.get (csrf_token start) in let _ = post c "/feedback" [ ("dream.csrf", token); ("step", "0"); ("action", "next"); ("choice", "5"); ] in let appetite = body (get c "/logbook") in let token = Option.get (csrf_token appetite) in (* The close control posts to the cancel route. *) let closed = post c "/feedback/cancel" [ ("dream.csrf", token) ] in Alcotest.(check int) "close redirects" 303 (status closed); let reopened = body (get c "/logbook?feedback=start") in Alcotest.(check bool) "reopening starts fresh at the first factor" true (contains ~substring:">Sleep" reopened && contains ~substring:"1 of 5 factors" reopened) ); ( "submitting feedback stores it and it appears on the logbook", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let start = body (get c "/logbook?feedback=start") in let rec advance step page = if step = 5 then page else let token = Option.get (csrf_token page) in ignore (post c "/feedback" [ ("dream.csrf", token); ("step", string_of_int step); ("action", "next"); ("choice", if step = 0 then "1" else "skip"); ]); advance (step + 1) (body (get c "/logbook")) in let notes = advance 0 start in let token = Option.get (csrf_token notes) in let stored = post c "/feedback" [ ("dream.csrf", token); ("step", "5"); ("action", "save"); ("pain", "true"); ] in Alcotest.(check int) "feedback stored" 303 (status stored); let logbook_page = body (get c "/logbook") in Alcotest.(check bool) "shows the reported sleep signal" true (contains ~substring:"Sleep 1 (very poor)" logbook_page); Alcotest.(check bool) "shows the reported pain signal" true (contains ~substring:"Pain" logbook_page) ); ( "the logbook graphs leveled feedback once two reports exist", `Quick, fun () -> let c = client () in let _ = sign_in_new c in (* Record one full feedback report: answer sleep, skip the rest. *) let record_sleep score = let start = body (get c "/logbook?feedback=start") in let token = Option.get (csrf_token start) in let _ = post c "/feedback" [ ("dream.csrf", token); ("step", "0"); ("action", "next"); ("choice", score); ] in let rec skip step = if step < 5 then begin let token = Option.get (csrf_token (body (get c "/logbook"))) in let _ = post c "/feedback" [ ("dream.csrf", token); ("step", string_of_int step); ("action", "skip"); ] in skip (step + 1) end in skip 1; let token = Option.get (csrf_token (body (get c "/logbook"))) in ignore (post c "/feedback" [ ("dream.csrf", token); ("step", "5"); ("action", "save") ]) in (* One report: no graph yet, a single point is not a trend. *) record_sleep "2"; let one = body (get c "/logbook") in Alcotest.(check bool) "no graph for a single report" false (contains ~substring:"feedback-graph" one); (* A second report: the graph appears with an SVG polyline. *) record_sleep "4"; let two = body (get c "/logbook") in Alcotest.(check bool) "graphs feedback over time" true (contains ~substring:"feedback-graph" two && contains ~substring:" let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let token = Option.get (csrf_token (body (get c "/workout"))) in let finished = post c "/workout/finish" [ ("dream.csrf", token) ] in Alcotest.(check int) "finish redirects" 303 (status finished); Alcotest.(check bool) "redirects to the logbook with a feedback prompt" true (List.mem "/logbook?prompt=feedback" (Dream.headers finished "Location")); let prompted = body (get c "/logbook?prompt=feedback") in Alcotest.(check bool) "shows the feedback suggestion" true (contains ~substring:"add feedback while it is fresh" prompted); (* The gated form opens on a suggestion, so the prompt lands on a visible form rather than a collapsed disclosure. *) Alcotest.(check bool) "opens the feedback modal on a suggestion" true (contains ~substring:"data-hito-feedback-modal" prompted && contains ~substring:" open" prompted) ); ( "beginning under override is gated behind a confirmation modal", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let token = Option.get (csrf_token (body (get c "/workout"))) in let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in (* Recovery has not elapsed, so Home shows the override gate. The begin form carries the shared confirm attributes and a matching dialog, so the deviation is possible but never silent. *) let home = body (get c "/") in Alcotest.(check bool) "still requires recovery" true (contains ~substring:"requires recovery" home); Alcotest.(check bool) "gates the override submit behind the confirm machinery" true (contains ~substring:"data-hito-confirm-cancel" home); Alcotest.(check bool) "posts the override through a confirm form" true (contains ~substring:"data-hito-confirm-form" home); Alcotest.(check bool) "carries the confirmation modal" true (contains ~substring:"data-hito-confirm-modal" home); Alcotest.(check bool) "still submits the override without script" true (contains ~substring:"value=\"true\"" home) ); ( "an overridden workout shows the early notice as a toast", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let token = Option.get (csrf_token (body (get c "/workout"))) in let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in (* Recovery has not elapsed, so the next workout must be started under override. *) let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "true") ] in let workout = body (get c "/workout") in Alcotest.(check bool) "renders the notice as a toast" true (contains ~substring:"data-hito-toast" workout); Alcotest.(check bool) "announces the toast politely" true (contains ~substring:"role=\"status\"" workout); Alcotest.(check bool) "keeps the early-workout wording" true (contains ~substring:"Begun before recovery finished." workout); Alcotest.(check bool) "no longer renders the notice as a plain warn block" false (contains ~substring:"
Begun before recovery" workout) ); ] ); ( "web.app_feedback", [ ( "the header offers an app feedback button beside the profile control", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let page = body (get c "/") in Alcotest.(check bool) "renders the app feedback button" true (contains ~substring:"class=\"app-feedback-button\"" page); Alcotest.(check bool) "links to the app feedback screen" true (contains ~substring:"href=\"/app-feedback\"" page); Alcotest.(check bool) "labels the button Feedback" true (contains ~substring:">Feedback" page) ); ( "the app feedback screen has write and submitted tabs", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let write_page = body (get c "/app-feedback") in Alcotest.(check bool) "starts with an app feedback heading" true (contains ~substring:"

App feedback

" write_page); Alcotest.(check bool) "offers the write tab" true (contains ~substring:"Write feedback" write_page); Alcotest.(check bool) "offers the submitted tab" true (contains ~substring:"Submitted feedback" write_page); Alcotest.(check bool) "renders a freeform textarea" true (contains ~substring:"id=\"app-feedback-message\"" write_page && contains ~substring:" let c = client () in let _ = sign_in_new c in let page = body (get c "/app-feedback") in let token = Option.get (csrf_token page) in let blank = post c "/app-feedback/submit" [ ("dream.csrf", token); ("message", " ") ] in Alcotest.(check int) "blank message redirects" 303 (status blank); Alcotest.(check bool) "redirects to the write tab" true (List.mem "/app-feedback?tab=write" (Dream.headers blank "Location")); let blank_page = body (get c "/app-feedback?tab=write") in Alcotest.(check bool) "preserves the validation error as a toast" true (contains ~substring:"Enter feedback before submitting" blank_page); let token = Option.get (csrf_token (body (get c "/app-feedback"))) in let submitted = post c "/app-feedback/submit" [ ("dream.csrf", token); ("message", "The app is clear.\nThank you!"); ] in Alcotest.(check int) "message is accepted" 303 (status submitted); Alcotest.(check bool) "redirects to submitted feedback" true (List.mem "/app-feedback?tab=submitted" (Dream.headers submitted "Location")); let list_page = body (get c "/app-feedback?tab=submitted") in Alcotest.(check bool) "shows the success toast" true (contains ~substring:"Feedback submitted." list_page); Alcotest.(check bool) "lists the submitted message" true (contains ~substring:"The app is clear.\nThank you!" list_page); Alcotest.(check bool) "shows its timestamp" true (contains ~substring:"app-feedback-time" list_page) ); ( "feedback lists authors and accepts another user's vote", `Quick, fun () -> let alice = client () in let _ = sign_in_new alice in let bob = { alice with jar = [] } in let _ = register bob ~username:"bobby" ~password:"heavyduty1" in let submit client message = let page = body (get client "/app-feedback") in let token = Option.get (csrf_token page) in post client "/app-feedback/submit" [ ("dream.csrf", token); ("message", message) ] in Alcotest.(check int) "alice feedback accepted" 303 (status (submit alice "Alice report")); Alcotest.(check int) "bob feedback accepted" 303 (status (submit bob "Bob report")); let list_page = body (get alice "/app-feedback?tab=submitted") in Alcotest.(check bool) "shows the author" true (contains ~substring:"bobby — 1 contributions" list_page); Alcotest.(check bool) "shows the ranked vote control" true (contains ~substring:"action=\"/app-feedback/t2:1/upvote\"" list_page); let token = Option.get (csrf_token list_page) in let voted = post alice "/app-feedback/t2:1/upvote" [ ("dream.csrf", token) ] in Alcotest.(check int) "upvote redirects" 303 (status voted); let ranked = body (get alice "/app-feedback?tab=submitted") in Alcotest.(check bool) "shows the vote count" true (contains ~substring:"Upvotes: 1" ranked); Alcotest.(check bool) "marks the viewer's vote" true (contains ~substring:"value=\"Upvoted\"" ranked); let bob_page = body (get bob "/app-feedback?tab=submitted") in Alcotest.(check bool) "shows owner actions" true (contains ~substring:"action=\"/app-feedback/t2:1/edit\"" bob_page && contains ~substring:"action=\"/app-feedback/t2:1/remove\"" bob_page); Alcotest.(check bool) "enhances owner actions through the app form path" true (contains ~substring:"app-feedback-edit-form" bob_page && contains ~substring:"app-feedback-remove-form" bob_page && contains ~substring:"data-hito-app-form" bob_page); let edit_token = Option.get (csrf_token bob_page) in let blank = post bob "/app-feedback/t2:1/edit" [ ("dream.csrf", edit_token); ("message", " ") ] in Alcotest.(check int) "blank edit redirects" 303 (status blank); let blank_page = body (get bob "/app-feedback?tab=submitted") in Alcotest.(check bool) "blank edit is refused" true (contains ~substring:"Enter feedback before submitting." blank_page && contains ~substring:"Bob report" blank_page); let edit_token = Option.get (csrf_token blank_page) in let edited = post bob "/app-feedback/t2:1/edit" [ ("dream.csrf", edit_token); ("message", "Edited report") ] in Alcotest.(check int) "edit redirects" 303 (status edited); let edited_page = body (get bob "/app-feedback?tab=submitted") in Alcotest.(check bool) "shows edited feedback" true (contains ~substring:"Edited report" edited_page); let remove_token = Option.get (csrf_token edited_page) in let removed = post bob "/app-feedback/t2:1/remove" [ ("dream.csrf", remove_token) ] in Alcotest.(check int) "remove redirects" 303 (status removed); let after_remove = body (get bob "/app-feedback?tab=submitted") in Alcotest.(check bool) "removes the owner's feedback" false (contains ~substring:"Edited report" after_remove) ); ( "another trainee cannot edit or remove app feedback", `Quick, fun () -> let alice = client () in let _ = register alice ~username:"alice" ~password:"heavyduty1" in let bob = { alice with jar = [] } in let _ = register bob ~username:"bobby" ~password:"heavyduty1" in let bob_page = body (get bob "/app-feedback") in let token = Option.get (csrf_token bob_page) in let _ = post bob "/app-feedback/submit" [ ("dream.csrf", token); ("message", "Bob owns this") ] in let alice_page = body (get alice "/app-feedback?tab=submitted") in let token = Option.get (csrf_token alice_page) in let edited = post alice "/app-feedback/t2:1/edit" [ ("dream.csrf", token); ("message", "Taken over") ] in Alcotest.(check int) "cross-user edit redirects" 303 (status edited); let after_edit = body (get alice "/app-feedback?tab=submitted") in Alcotest.(check bool) "cross-user edit changes nothing" true (contains ~substring:"Bob owns this" after_edit && not (contains ~substring:"Taken over" after_edit)); let token = Option.get (csrf_token after_edit) in let removed = post alice "/app-feedback/t2:1/remove" [ ("dream.csrf", token) ] in Alcotest.(check int) "cross-user remove redirects" 303 (status removed); let after_remove = body (get alice "/app-feedback?tab=submitted") in Alcotest.(check bool) "cross-user remove changes nothing" true (contains ~substring:"Bob owns this" after_remove) ); ] ); ( "web.profile", [ ( "the profile page offers username and password forms", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let page = body (get c "/profile") in Alcotest.(check bool) "starts with a page heading" true (contains ~substring:"

Profile

" page); Alcotest.(check bool) "posts a username change to its route" true (contains ~substring:"action=\"/profile/username\"" page); Alcotest.(check bool) "posts a password change to its route" true (contains ~substring:"action=\"/profile/password\"" page); Alcotest.(check bool) "prefills the current username" true (contains ~substring:"value=\"lifter\"" page); Alcotest.(check bool) "asks for the current password" true (contains ~substring:"name=\"current\"" page); Alcotest.(check bool) "shows the username value, not a bare field" true (contains ~substring:"profile-value-text" page); Alcotest.(check bool) "offers a change-username button that opens a dialog" true (contains ~substring:"data-hito-dialog-open=\"username-dialog\"" page); Alcotest.(check bool) "offers a change-password button that opens a dialog" true (contains ~substring:"data-hito-dialog-open=\"password-dialog\"" page); Alcotest.(check bool) "wraps each edit form in a dialog" true (contains ~substring:" let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/profile"))) in let changed = post c "/profile/username" [ ("dream.csrf", token); ("username", "athlete") ] in Alcotest.(check int) "redirects after the change" 303 (status changed); Alcotest.(check bool) "redirects back to the profile with a notice" true (List.mem "/profile?changed=username" (Dream.headers changed "Location")); let page = body (get c "/profile?changed=username") in Alcotest.(check bool) "confirms the change" true (contains ~substring:"Username changed" page); Alcotest.(check bool) "shows the new name in the profile control" true (contains ~substring:">athlete" page) ); ( "a taken username is refused with an inline error", `Quick, fun () -> let c = client () in let _ = sign_in_new c in (* Register a second account in the same store, via a client that shares the app but keeps its own cookie jar. *) let other = { c with jar = [] } in let _ = register other ~username:"taken" ~password:"heavyduty1" in let token = Option.get (csrf_token (body (get c "/profile"))) in let refused = post c "/profile/username" [ ("dream.csrf", token); ("username", "taken") ] in Alcotest.(check int) "renders an invalid response" 400 (status refused); Alcotest.(check bool) "explains the name is taken" true (contains ~substring:"already registered" (body refused)); Alcotest.(check bool) "opens the username dialog to show the error" true (contains ~substring:"class=\"profile-dialog\" open" (body refused)) ); ( "changing the password requires the current one", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/profile"))) in let refused = post c "/profile/password" [ ("dream.csrf", token); ("current", "wrongpass"); ("next", "newsecret1"); ] in Alcotest.(check int) "renders an invalid response" 400 (status refused); Alcotest.(check bool) "explains the current password is wrong" true (contains ~substring:"current password is not correct" (body refused)); (* Now change it with the correct current password. *) let token = Option.get (csrf_token (body (get c "/profile"))) in let changed = post c "/profile/password" [ ("dream.csrf", token); ("current", "heavyduty1"); ("next", "newsecret1"); ] in Alcotest.(check int) "redirects after the change" 303 (status changed); Alcotest.(check bool) "redirects back to the profile with a notice" true (List.mem "/profile?changed=password" (Dream.headers changed "Location")) ); ] ); ( "web.errors", [ ( "an unknown path renders a branded 404 with navigation", `Quick, fun () -> let c = client () in let response = get c "/no-such-page" in Alcotest.(check int) "responds 404" 404 (status response); let page = body response in Alcotest.(check bool) "names the failure without leaking server detail" true (contains ~substring:"Page not found" page); Alcotest.(check bool) "keeps the desktop top navigation" true (contains ~substring:"primary-nav" page); Alcotest.(check bool) "keeps the mobile bottom navigation" true (contains ~substring:"bottom-nav" page); Alcotest.(check bool) "offers a route back Home" true (contains ~substring:"href=\"/\"" page) ); ( "the 404 page never depends on a session", `Quick, fun () -> (* No sign-in: an unauthenticated visitor still gets the branded page and its navigation, not an empty body. *) let c = client () in let page = body (get c "/deep/unknown/path") in Alcotest.(check bool) "renders the branded page" true (contains ~substring:"Page not found" page); Alcotest.(check bool) "shows navigation actions" true (contains ~substring:"Logbook" page) ); ] ); ( "web.sign_in", [ ( "a returning trainee signs in with their credentials", `Quick, fun () -> (* Register, then sign out, then sign back in through the credential form. This exercises [POST /login], which registration's auto-sign-in never touches. *) let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/logout" [ ("dream.csrf", token) ] in Alcotest.(check int) "signed out, overview redirects" 303 (status (get c "/")); let signed_in = sign_in c ~username:"lifter" ~password:"heavyduty1" in Alcotest.(check int) "sign-in redirects" 303 (status signed_in); Alcotest.(check bool) "sign-in lands on Home" true (List.mem "/" (Dream.headers signed_in "Location")); Alcotest.(check int) "authenticated overview" 200 (status (get c "/")) ); ( "a wrong password is refused with an unauthorized message", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/logout" [ ("dream.csrf", token) ] in let refused = sign_in c ~username:"lifter" ~password:"not-the-password" in Alcotest.(check int) "rejected" 401 (status refused); Alcotest.(check bool) "explains the mismatch without leaking which field failed" true (contains ~substring:"do not match" (body refused)); (* The rejected attempt establishes no session. *) Alcotest.(check int) "still unauthenticated" 303 (status (get c "/")) ); ( "an unknown username is refused like a wrong password", `Quick, fun () -> let c = client () in let refused = sign_in c ~username:"nobody" ~password:"heavyduty1" in Alcotest.(check int) "rejected" 401 (status refused); Alcotest.(check bool) "gives the same generic message" true (contains ~substring:"do not match" (body refused)) ); ( "an already-authenticated visit to the sign-in page redirects Home", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let response = get c "/login" in Alcotest.(check int) "redirects" 303 (status response); Alcotest.(check bool) "sends an authenticated visitor Home" true (List.mem "/" (Dream.headers response "Location")) ); ( "an already-authenticated visit to the register page redirects Home", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let response = get c "/register" in Alcotest.(check int) "redirects" 303 (status response); Alcotest.(check bool) "sends an authenticated visitor Home" true (List.mem "/" (Dream.headers response "Location")) ); ] ); ( "web.routine_selection", [ ( "selecting an unknown routine is not found", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let response = post c "/routines/no-such-routine/select" [ ("dream.csrf", token) ] in Alcotest.(check int) "responds 404" 404 (status response); Alcotest.(check bool) "names the missing routine" true (contains ~substring:"not in the catalogue" (body response)) ); ] ); ( "web.saved_record_add", [ ( "an outstanding slot of a saved workout can be recorded, not only \ corrected", `Quick, fun () -> (* Finish a workout with a single slot recorded, so the saved record still has outstanding slots. Then record one of those through the plain add path [POST /logbook/:id/slots/:d], which the edit tests never exercise. *) let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in (* Record only slot 1, then finish. Slots 0, 2, 3 stay outstanding. *) let token = Option.get (csrf_token (body (get c "/workout?slot=1"))) in let _ = post c "/workout/slots/1" [ ("dream.csrf", token); ("load", "12"); ("reps", "8"); ("extension", ""); ] in let token = Option.get (csrf_token (body (get c "/workout"))) in let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in (* The saved workout is w1, still incomplete. Its slot 2 awaits a record; add it through the non-edit path. *) let record_page = body (get c "/logbook/w1?slot=2") in Alcotest.(check bool) "the add form posts to the plain slot route" true (contains ~substring:"/logbook/w1/slots/2\"" record_page); let token = Option.get (csrf_token record_page) in let added = post c "/logbook/w1/slots/2" [ ("dream.csrf", token); ("load", "10"); ("reps", "9"); ("extension", ""); ] in Alcotest.(check int) "recorded a saved-workout slot" 303 (status added); let after = body (get c "/logbook/w1?slot=2") in Alcotest.(check bool) "now counts two filled slots" true (contains ~substring:"2 of 4 recorded" after); Alcotest.(check bool) "shows the newly recorded load" true (contains ~substring:"10" after) ); ( "recording an already-filled slot through the add path is refused", `Quick, fun () -> (* The add path lists only outstanding slots. A filled slot is not awaiting a record, so posting to it is a 404, not a silent second entry that would add volume. *) let c = client () in let _ = sign_in_new c in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get c "/"))) in let _ = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in let token = Option.get (csrf_token (body (get c "/workout?slot=1"))) in let _ = post c "/workout/slots/1" [ ("dream.csrf", token); ("load", "12"); ("reps", "8"); ("extension", ""); ] in let token = Option.get (csrf_token (body (get c "/workout"))) in let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in (* Slot 1 is filled. Re-posting to its add path is refused. *) let token = Option.get (csrf_token (body (get c "/logbook/w1?slot=2"))) in let refused = post c "/logbook/w1/slots/1" [ ("dream.csrf", token); ("load", "20"); ("reps", "6"); ("extension", ""); ] in Alcotest.(check int) "filled slot is not awaiting" 404 (status refused); Alcotest.(check bool) "names the slot as not awaiting" true (contains ~substring:"not awaiting a record" (body refused)) ); ] ); ( "web.slot_bounds", [ ( "posting to a slot beyond the workout is not found", `Quick, fun () -> (* Day 1 has four slots (0..3). Slot 9 does not exist, so the log handler answers 404 rather than recording phantom volume. *) let c = client () in let _ = sign_in_new c in let _ = start_day_one c in let token = Option.get (csrf_token (body (get c "/workout"))) in let response = post c "/workout/slots/9" [ ("dream.csrf", token); ("load", "10"); ("reps", "8"); ("extension", ""); ] in Alcotest.(check int) "out-of-range slot is not found" 404 (status response); Alcotest.(check bool) "names the slot as not awaiting" true (contains ~substring:"not awaiting a record" (body response)) ); ( "editing a slot beyond the workout is not found", `Quick, fun () -> let c = client () in let _ = sign_in_new c in let _ = start_day_one c in let token = Option.get (csrf_token (body (get c "/workout"))) in let response = post c "/workout/slots/9/edit" [ ("dream.csrf", token); ("load", "10"); ("reps", "8"); ("extension", ""); ] in Alcotest.(check int) "out-of-range edit is not found" 404 (status response) ); ( "logging with no workout in progress is not found", `Quick, fun () -> let c = client () in let _ = sign_in_new c in (* No routine selected, no workout begun: the CSRF token comes from Home, but the slot route has no workout to record against. *) let token = Option.get (csrf_token (body (get c "/"))) in let response = post c "/workout/slots/0" [ ("dream.csrf", token); ("load", "10"); ("reps", "8"); ("extension", ""); ] in Alcotest.(check int) "no workout is not found" 404 (status response); Alcotest.(check bool) "names the absent workout" true (contains ~substring:"No workout is in progress" (body response)) ); ] ); ( "web.duplicate_feedback", [ ( "the flow rejects a step that does not match the session's position", `Quick, fun () -> (* The flow advances one factor at a time and stores its position in the session. A post whose [step] disagrees with that position — a stale form, a double submit, or a crafted body — is refused as an invalid form rather than recording a factor twice. This is the web tier's guard against a duplicate signal ever reaching the domain. *) let c = client () in let _ = sign_in_new c in let start = body (get c "/logbook?feedback=start") in let token = Option.get (csrf_token start) in (* The session is at step 0; post step 3 instead. *) let mismatched = post c "/feedback" [ ("dream.csrf", token); ("step", "3"); ("action", "next"); ("choice", "1"); ] in Alcotest.(check int) "mismatched step redirects" 303 (status mismatched); let logbook_page = body (get c "/logbook") in Alcotest.(check bool) "reports the mismatch as an invalid form" true (contains ~substring:"The submitted form is not valid." logbook_page); (* The flow did not advance: it still shows the first factor. *) Alcotest.(check bool) "the flow stays on the first factor" true (contains ~substring:">Sleep" logbook_page && contains ~substring:"1 of 5 factors" logbook_page) ); ( "answering every factor stores exactly one signal per category", `Quick, fun () -> (* The flow admits one answer per factor, so the stored report holds one signal per category and never a duplicate. Walking all five factors and saving proves the whole set persists without a duplicate-signal rejection. *) let c = client () in let _ = sign_in_new c in let _ = get c "/logbook?feedback=start" in let rec advance step = if step < 5 then begin let token = Option.get (csrf_token (body (get c "/logbook"))) in let _ = post c "/feedback" [ ("dream.csrf", token); ("step", string_of_int step); ("action", "next"); ("choice", "1"); ] in advance (step + 1) end in advance 0; let token = Option.get (csrf_token (body (get c "/logbook"))) in let saved = post c "/feedback" [ ("dream.csrf", token); ("step", "5"); ("action", "save") ] in Alcotest.(check int) "final save redirects" 303 (status saved); let logbook_page = body (get c "/logbook") in Alcotest.(check bool) "stores one signal per distinct factor" true (contains ~substring:"Sleep 1 (very poor)" logbook_page && contains ~substring:"Appetite 1 (very poor)" logbook_page && contains ~substring:"Readiness 1 (very poor)" logbook_page && contains ~substring:"Motivation 1 (very poor)" logbook_page && contains ~substring:"Difficulty 1 (very poor)" logbook_page) ); ] ); ( "web.lifecycle", [ ( "a full cycle: begin Day 1, record every slot, finish, and see it \ saved complete", `Quick, fun () -> (* A deep end-to-end pass over the primary flow with a driven clock. Begin Day 1, record all four slots, finish, and confirm the logbook persists a complete Day 1 with four stimuli. *) let c, now = clocked_client () in now := day 10; let _ = register c ~username:"lifter" ~password:"heavyduty1" in let _ = start_day_one c in (* Fresh workout: nothing recorded yet. *) Alcotest.(check bool) "opens with no slots recorded" true (contains ~substring:"0 of 4 recorded" (body (get c "/workout"))); record_all_day_one_slots c; Alcotest.(check bool) "counts all four slots recorded" true (contains ~substring:"4 of 4 recorded" (body (get c "/workout"))); let token = Option.get (csrf_token (body (get c "/workout"))) in let finished = post c "/workout/finish" [ ("dream.csrf", token) ] in Alcotest.(check int) "finish redirects" 303 (status finished); Alcotest.(check bool) "sends the trainee to the logbook feedback prompt" true (List.mem "/logbook?prompt=feedback" (Dream.headers finished "Location")); let logbook_page = body (get c "/logbook") in Alcotest.(check bool) "the logbook lists the saved Day 1 with all its stimuli" true (contains ~substring:"Day 1 — 4 stimuli" logbook_page); Alcotest.(check bool) "marks the saved workout complete" true (contains ~substring:"complete" logbook_page); (* The saved workout is durable and reachable at its own URL. *) let record_page = body (get c "/logbook/w1") in Alcotest.(check bool) "the saved workout persists all four slots" true (contains ~substring:"4 of 4 recorded" record_page) ); ( "the recovery gate blocks the next workout until enough rest, then \ Day 2 begins", `Quick, fun () -> (* The deepest flow: after finishing Day 1, the next workout is gated until 48 hours of recovery pass. The web tier drives the clock, so this proves the gate over HTTP, not just in the service. Beginning too early is refused; resting past the interval clears the gate and advances the cycle to Day 2. *) let c, now = clocked_client () in now := day 10; let _ = register c ~username:"lifter" ~password:"heavyduty1" in let _ = start_day_one c in record_all_day_one_slots c; let token = Option.get (csrf_token (body (get c "/workout"))) in let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in (* One hour later: recovery is far from complete. Home shows the gate and beginning without an override is refused. *) now := day 10 + 3600; let home = body (get c "/") in Alcotest.(check bool) "Home shows the recovery gate" true (contains ~substring:"requires recovery" home); let token = Option.get (csrf_token home) in let refused = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in Alcotest.(check int) "an early begin is bounced back" 303 (status refused); Alcotest.(check bool) "the early begin returns Home, not the workout" true (List.mem "/" (Dream.headers refused "Location")); Alcotest.(check int) "no workout was started" 303 (status (get c "/workout")); (* Rest two full days: past the 48-hour training interval. The gate clears and the next begin succeeds. *) now := day 13; let home = body (get c "/") in Alcotest.(check bool) "Home no longer requires recovery" false (contains ~substring:"requires recovery" home); let token = Option.get (csrf_token home) in let began = post c "/workout" [ ("dream.csrf", token); ("override", "false") ] in Alcotest.(check int) "the rested begin succeeds" 303 (status began); Alcotest.(check bool) "the rested begin reaches the workout" true (List.mem "/workout" (Dream.headers began "Location")); let workout = body (get c "/workout") in Alcotest.(check bool) "the cycle has advanced to Day 2" true (contains ~substring:"

Day 2

" workout); Alcotest.(check bool) "the rested workout shows no early-training notice" false (contains ~substring:"Begun before recovery finished." workout) ); ] ); ( "web.isolation", [ ( "two trainees never share workout or logbook state", `Quick, fun () -> (* Two trainees on the same store, each with their own cookie jar. One begins and finishes a workout; the other must see none of it: not the in-progress workout, not the saved record. This proves the per-trainee scoping the service promises, over HTTP. *) let alice = client () in let _ = register alice ~username:"alice" ~password:"heavyduty1" in let bob = { alice with jar = [] } in let _ = register bob ~username:"bobby" ~password:"heavyduty1" in (* Alice selects and begins a workout, records a slot. *) let token = Option.get (csrf_token (body (get alice "/"))) in let _ = post alice "/routines/ideal/select" [ ("dream.csrf", token) ] in let token = Option.get (csrf_token (body (get alice "/"))) in let _ = post alice "/workout" [ ("dream.csrf", token); ("override", "false") ] in let token = Option.get (csrf_token (body (get alice "/workout?slot=1"))) in let _ = post alice "/workout/slots/1" [ ("dream.csrf", token); ("load", "12"); ("reps", "8"); ("extension", ""); ] in (* Bob has no workout in progress: his workout view redirects. *) Alcotest.(check int) "Bob has no workout in progress" 303 (status (get bob "/workout")); (* Bob's Home does not show Alice's in-progress card. *) let bob_home = body (get bob "/") in Alcotest.(check bool) "Bob's Home has no in-progress card" false (contains ~substring:"in-progress-card" bob_home); (* Alice finishes; her record becomes w1 for her. *) let token = Option.get (csrf_token (body (get alice "/workout"))) in let _ = post alice "/workout/finish" [ ("dream.csrf", token) ] in let alice_logbook = body (get alice "/logbook") in Alcotest.(check bool) "Alice sees her saved workout" true (contains ~substring:"/logbook/w1" alice_logbook); (* Bob's logbook is empty and Alice's record is not reachable from Bob's session. Ids are per-trainee, so Bob's own w1 does not exist yet. *) let bob_logbook = body (get bob "/logbook") in Alcotest.(check bool) "Bob's logbook shows nothing logged" true (contains ~substring:"Nothing logged yet." bob_logbook); Alcotest.(check bool) "Bob's logbook does not list Alice's record" false (contains ~substring:"Day 1 — " bob_logbook); Alcotest.(check int) "Bob cannot open a record he does not own" 404 (status (get bob "/logbook/w1")) ); ] ); ] let suite = route_tests