test expand end-to-end flow coverage

Exercise returning sign-in, saved-record additions, invalid slot requests, and unknown routines through the real web stack. Add full lifecycle, recovery-gate, and trainee-isolation flows so persistence and ownership failures are detected at the user boundary.

Commit
10d131c3a1bb39dac4731a91d9746a390a98b30c
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
test/test_web.ml
index 1d3004dd..1338eae8 100644..100644
@@ -123,6 +123,74 @@
123 123 let sign_in_new client =
124 124 register client ~username:"lifter" ~password:"heavyduty1"
125 125
126 Added: (* Sign in through the credential form, as a returning trainee would. Unlike
127 Added: [register], this exercises the [POST /login] path. *)
128 Added: let sign_in client ~username ~password =
129 Added: let page = body (get client "/login") in
130 Added: let token = Option.get (csrf_token page) in
131 Added: post client "/login"
132 Added: [ ("dream.csrf", token); ("username", username); ("password", password) ]
133 Added:
134 Added: (* An application whose clock the test drives. A shared [Memory_repo] and a
135 Added: mutable [now] cell let a deep test advance time across the recovery gate,
136 Added: which the wall-clock default cannot. Registration is open so the test can
137 Added: create the trainee it drives. *)
138 Added: let clocked_client () =
139 Added: let now = ref 0 in
140 Added: let handlers =
141 Added: Handlers.make ~repo:(Memory_repo.create ())
142 Added: ~now:(fun () -> Recovery.timestamp_of_unix_seconds !now)
143 Added: ~registration_open:true ()
144 Added: in
145 Added: let app =
146 Added: Dream.memory_sessions @@ Dream.router (Handlers.routes handlers) |> fun h ->
147 Added: Dream.set_secret "test-secret-value" h
148 Added: in
149 Added: ({ app; jar = [] }, now)
150 Added:
151 Added: let day n = n * 86_400
152 Added:
153 Added: (* Drive a trainee to the start of a fresh Day 1 workout: select the ideal
154 Added: routine and begin without an override. Returns the client. *)
155 Added: let start_day_one c =
156 Added: let token = Option.get (csrf_token (body (get c "/"))) in
157 Added: let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
158 Added: let token = Option.get (csrf_token (body (get c "/"))) in
159 Added: post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
160 Added:
161 Added: (* Record every slot of the in-progress Day 1 workout, one at a time, addressed
162 Added: by [?slot=]. Slot 0 is the flyes/incline-press pair; slot 3 is the
163 Added: french-press/dips pair; slots 1 and 2 are single lateral movements. *)
164 Added: let record_all_day_one_slots c =
165 Added: let record_pair slot =
166 Added: let token =
167 Added: Option.get (csrf_token (body (get c ("/workout?slot=" ^ slot))))
168 Added: in
169 Added: post c ("/workout/slots/" ^ slot)
170 Added: [
171 Added: ("dream.csrf", token);
172 Added: ("iso_load", "20");
173 Added: ("iso_reps", "9");
174 Added: ("comp_load", "60");
175 Added: ("comp_reps", "7");
176 Added: ("extension", "");
177 Added: ]
178 Added: in
179 Added: let record_single slot load reps =
180 Added: let token =
181 Added: Option.get (csrf_token (body (get c ("/workout?slot=" ^ slot))))
182 Added: in
183 Added: post c ("/workout/slots/" ^ slot)
184 Added: [
185 Added: ("dream.csrf", token); ("load", load); ("reps", reps); ("extension", "");
186 Added: ]
187 Added: in
188 Added: let _ = record_pair "0" in
189 Added: let _ = record_single "1" "12" "8" in
190 Added: let _ = record_single "2" "10" "9" in
191 Added: let _ = record_pair "3" in
192 Added: ()
193 Added:
126 194 let route_tests =
127 195 [
128 196 ( "web.layout",
@@ -1524,6 +1592,508 @@
1524 1592 Alcotest.(check bool)
1525 1593 "shows navigation actions" true
1526 1594 (contains ~substring:"Logbook" page) );
1595 Added: ] );
1596 Added: ( "web.sign_in",
1597 Added: [
1598 Added: ( "a returning trainee signs in with their credentials",
1599 Added: `Quick,
1600 Added: fun () ->
1601 Added: (* Register, then sign out, then sign back in through the credential
1602 Added: form. This exercises [POST /login], which registration's
1603 Added: auto-sign-in never touches. *)
1604 Added: let c = client () in
1605 Added: let _ = sign_in_new c in
1606 Added: let token = Option.get (csrf_token (body (get c "/"))) in
1607 Added: let _ = post c "/logout" [ ("dream.csrf", token) ] in
1608 Added: Alcotest.(check int)
1609 Added: "signed out, overview redirects" 303
1610 Added: (status (get c "/"));
1611 Added: let signed_in =
1612 Added: sign_in c ~username:"lifter" ~password:"heavyduty1"
1613 Added: in
1614 Added: Alcotest.(check int) "sign-in redirects" 303 (status signed_in);
1615 Added: Alcotest.(check bool)
1616 Added: "sign-in lands on Home" true
1617 Added: (List.mem "/" (Dream.headers signed_in "Location"));
1618 Added: Alcotest.(check int)
1619 Added: "authenticated overview" 200
1620 Added: (status (get c "/")) );
1621 Added: ( "a wrong password is refused with an unauthorized message",
1622 Added: `Quick,
1623 Added: fun () ->
1624 Added: let c = client () in
1625 Added: let _ = sign_in_new c in
1626 Added: let token = Option.get (csrf_token (body (get c "/"))) in
1627 Added: let _ = post c "/logout" [ ("dream.csrf", token) ] in
1628 Added: let refused =
1629 Added: sign_in c ~username:"lifter" ~password:"not-the-password"
1630 Added: in
1631 Added: Alcotest.(check int) "rejected" 401 (status refused);
1632 Added: Alcotest.(check bool)
1633 Added: "explains the mismatch without leaking which field failed" true
1634 Added: (contains ~substring:"do not match" (body refused));
1635 Added: (* The rejected attempt establishes no session. *)
1636 Added: Alcotest.(check int)
1637 Added: "still unauthenticated" 303
1638 Added: (status (get c "/")) );
1639 Added: ( "an unknown username is refused like a wrong password",
1640 Added: `Quick,
1641 Added: fun () ->
1642 Added: let c = client () in
1643 Added: let refused = sign_in c ~username:"nobody" ~password:"heavyduty1" in
1644 Added: Alcotest.(check int) "rejected" 401 (status refused);
1645 Added: Alcotest.(check bool)
1646 Added: "gives the same generic message" true
1647 Added: (contains ~substring:"do not match" (body refused)) );
1648 Added: ( "an already-authenticated visit to the sign-in page redirects Home",
1649 Added: `Quick,
1650 Added: fun () ->
1651 Added: let c = client () in
1652 Added: let _ = sign_in_new c in
1653 Added: let response = get c "/login" in
1654 Added: Alcotest.(check int) "redirects" 303 (status response);
1655 Added: Alcotest.(check bool)
1656 Added: "sends an authenticated visitor Home" true
1657 Added: (List.mem "/" (Dream.headers response "Location")) );
1658 Added: ( "an already-authenticated visit to the register page redirects Home",
1659 Added: `Quick,
1660 Added: fun () ->
1661 Added: let c = client () in
1662 Added: let _ = sign_in_new c in
1663 Added: let response = get c "/register" in
1664 Added: Alcotest.(check int) "redirects" 303 (status response);
1665 Added: Alcotest.(check bool)
1666 Added: "sends an authenticated visitor Home" true
1667 Added: (List.mem "/" (Dream.headers response "Location")) );
1668 Added: ] );
1669 Added: ( "web.routine_selection",
1670 Added: [
1671 Added: ( "selecting an unknown routine is not found",
1672 Added: `Quick,
1673 Added: fun () ->
1674 Added: let c = client () in
1675 Added: let _ = sign_in_new c in
1676 Added: let token = Option.get (csrf_token (body (get c "/"))) in
1677 Added: let response =
1678 Added: post c "/routines/no-such-routine/select"
1679 Added: [ ("dream.csrf", token) ]
1680 Added: in
1681 Added: Alcotest.(check int) "responds 404" 404 (status response);
1682 Added: Alcotest.(check bool)
1683 Added: "names the missing routine" true
1684 Added: (contains ~substring:"not in the catalogue" (body response)) );
1685 Added: ] );
1686 Added: ( "web.saved_record_add",
1687 Added: [
1688 Added: ( "an outstanding slot of a saved workout can be recorded, not only \
1689 Added: corrected",
1690 Added: `Quick,
1691 Added: fun () ->
1692 Added: (* Finish a workout with a single slot recorded, so the saved record
1693 Added: still has outstanding slots. Then record one of those through the
1694 Added: plain add path [POST /logbook/:id/slots/:d], which the edit tests
1695 Added: never exercise. *)
1696 Added: let c = client () in
1697 Added: let _ = sign_in_new c in
1698 Added: let token = Option.get (csrf_token (body (get c "/"))) in
1699 Added: let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
1700 Added: let token = Option.get (csrf_token (body (get c "/"))) in
1701 Added: let _ =
1702 Added: post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
1703 Added: in
1704 Added: (* Record only slot 1, then finish. Slots 0, 2, 3 stay outstanding. *)
1705 Added: let token =
1706 Added: Option.get (csrf_token (body (get c "/workout?slot=1")))
1707 Added: in
1708 Added: let _ =
1709 Added: post c "/workout/slots/1"
1710 Added: [
1711 Added: ("dream.csrf", token);
1712 Added: ("load", "12");
1713 Added: ("reps", "8");
1714 Added: ("extension", "");
1715 Added: ]
1716 Added: in
1717 Added: let token = Option.get (csrf_token (body (get c "/workout"))) in
1718 Added: let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
1719 Added: (* The saved workout is w1, still incomplete. Its slot 2 awaits a
1720 Added: record; add it through the non-edit path. *)
1721 Added: let record_page = body (get c "/logbook/w1?slot=2") in
1722 Added: Alcotest.(check bool)
1723 Added: "the add form posts to the plain slot route" true
1724 Added: (contains ~substring:"/logbook/w1/slots/2\"" record_page);
1725 Added: let token = Option.get (csrf_token record_page) in
1726 Added: let added =
1727 Added: post c "/logbook/w1/slots/2"
1728 Added: [
1729 Added: ("dream.csrf", token);
1730 Added: ("load", "10");
1731 Added: ("reps", "9");
1732 Added: ("extension", "");
1733 Added: ]
1734 Added: in
1735 Added: Alcotest.(check int)
1736 Added: "recorded a saved-workout slot" 303 (status added);
1737 Added: let after = body (get c "/logbook/w1?slot=2") in
1738 Added: Alcotest.(check bool)
1739 Added: "now counts two filled slots" true
1740 Added: (contains ~substring:"2 of 4 recorded" after);
1741 Added: Alcotest.(check bool)
1742 Added: "shows the newly recorded load" true
1743 Added: (contains ~substring:"10" after) );
1744 Added: ( "recording an already-filled slot through the add path is refused",
1745 Added: `Quick,
1746 Added: fun () ->
1747 Added: (* The add path lists only outstanding slots. A filled slot is not
1748 Added: awaiting a record, so posting to it is a 404, not a silent second
1749 Added: entry that would add volume. *)
1750 Added: let c = client () in
1751 Added: let _ = sign_in_new c in
1752 Added: let token = Option.get (csrf_token (body (get c "/"))) in
1753 Added: let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
1754 Added: let token = Option.get (csrf_token (body (get c "/"))) in
1755 Added: let _ =
1756 Added: post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
1757 Added: in
1758 Added: let token =
1759 Added: Option.get (csrf_token (body (get c "/workout?slot=1")))
1760 Added: in
1761 Added: let _ =
1762 Added: post c "/workout/slots/1"
1763 Added: [
1764 Added: ("dream.csrf", token);
1765 Added: ("load", "12");
1766 Added: ("reps", "8");
1767 Added: ("extension", "");
1768 Added: ]
1769 Added: in
1770 Added: let token = Option.get (csrf_token (body (get c "/workout"))) in
1771 Added: let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
1772 Added: (* Slot 1 is filled. Re-posting to its add path is refused. *)
1773 Added: let token =
1774 Added: Option.get (csrf_token (body (get c "/logbook/w1?slot=2")))
1775 Added: in
1776 Added: let refused =
1777 Added: post c "/logbook/w1/slots/1"
1778 Added: [
1779 Added: ("dream.csrf", token);
1780 Added: ("load", "20");
1781 Added: ("reps", "6");
1782 Added: ("extension", "");
1783 Added: ]
1784 Added: in
1785 Added: Alcotest.(check int)
1786 Added: "filled slot is not awaiting" 404 (status refused);
1787 Added: Alcotest.(check bool)
1788 Added: "names the slot as not awaiting" true
1789 Added: (contains ~substring:"not awaiting a record" (body refused)) );
1790 Added: ] );
1791 Added: ( "web.slot_bounds",
1792 Added: [
1793 Added: ( "posting to a slot beyond the workout is not found",
1794 Added: `Quick,
1795 Added: fun () ->
1796 Added: (* Day 1 has four slots (0..3). Slot 9 does not exist, so the log
1797 Added: handler answers 404 rather than recording phantom volume. *)
1798 Added: let c = client () in
1799 Added: let _ = sign_in_new c in
1800 Added: let _ = start_day_one c in
1801 Added: let token = Option.get (csrf_token (body (get c "/workout"))) in
1802 Added: let response =
1803 Added: post c "/workout/slots/9"
1804 Added: [
1805 Added: ("dream.csrf", token);
1806 Added: ("load", "10");
1807 Added: ("reps", "8");
1808 Added: ("extension", "");
1809 Added: ]
1810 Added: in
1811 Added: Alcotest.(check int)
1812 Added: "out-of-range slot is not found" 404 (status response);
1813 Added: Alcotest.(check bool)
1814 Added: "names the slot as not awaiting" true
1815 Added: (contains ~substring:"not awaiting a record" (body response)) );
1816 Added: ( "editing a slot beyond the workout is not found",
1817 Added: `Quick,
1818 Added: fun () ->
1819 Added: let c = client () in
1820 Added: let _ = sign_in_new c in
1821 Added: let _ = start_day_one c in
1822 Added: let token = Option.get (csrf_token (body (get c "/workout"))) in
1823 Added: let response =
1824 Added: post c "/workout/slots/9/edit"
1825 Added: [
1826 Added: ("dream.csrf", token);
1827 Added: ("load", "10");
1828 Added: ("reps", "8");
1829 Added: ("extension", "");
1830 Added: ]
1831 Added: in
1832 Added: Alcotest.(check int)
1833 Added: "out-of-range edit is not found" 404 (status response) );
1834 Added: ( "logging with no workout in progress is not found",
1835 Added: `Quick,
1836 Added: fun () ->
1837 Added: let c = client () in
1838 Added: let _ = sign_in_new c in
1839 Added: (* No routine selected, no workout begun: the CSRF token comes from
1840 Added: Home, but the slot route has no workout to record against. *)
1841 Added: let token = Option.get (csrf_token (body (get c "/"))) in
1842 Added: let response =
1843 Added: post c "/workout/slots/0"
1844 Added: [
1845 Added: ("dream.csrf", token);
1846 Added: ("load", "10");
1847 Added: ("reps", "8");
1848 Added: ("extension", "");
1849 Added: ]
1850 Added: in
1851 Added: Alcotest.(check int) "no workout is not found" 404 (status response);
1852 Added: Alcotest.(check bool)
1853 Added: "names the absent workout" true
1854 Added: (contains ~substring:"No workout is in progress" (body response))
1855 Added: );
1856 Added: ] );
1857 Added: ( "web.duplicate_feedback",
1858 Added: [
1859 Added: ( "the flow rejects a step that does not match the session's position",
1860 Added: `Quick,
1861 Added: fun () ->
1862 Added: (* The flow advances one factor at a time and stores its position in
1863 Added: the session. A post whose [step] disagrees with that position —
1864 Added: a stale form, a double submit, or a crafted body — is refused as
1865 Added: an invalid form rather than recording a factor twice. This is the
1866 Added: web tier's guard against a duplicate signal ever reaching the
1867 Added: domain. *)
1868 Added: let c = client () in
1869 Added: let _ = sign_in_new c in
1870 Added: let start = body (get c "/logbook?feedback=start") in
1871 Added: let token = Option.get (csrf_token start) in
1872 Added: (* The session is at step 0; post step 3 instead. *)
1873 Added: let mismatched =
1874 Added: post c "/feedback"
1875 Added: [
1876 Added: ("dream.csrf", token);
1877 Added: ("step", "3");
1878 Added: ("action", "next");
1879 Added: ("choice", "below");
1880 Added: ]
1881 Added: in
1882 Added: Alcotest.(check int)
1883 Added: "mismatched step redirects" 303 (status mismatched);
1884 Added: let logbook_page = body (get c "/logbook") in
1885 Added: Alcotest.(check bool)
1886 Added: "reports the mismatch as an invalid form" true
1887 Added: (contains ~substring:"The submitted form is not valid."
1888 Added: logbook_page);
1889 Added: (* The flow did not advance: it still shows the first factor. *)
1890 Added: Alcotest.(check bool)
1891 Added: "the flow stays on the first factor" true
1892 Added: (contains ~substring:">Sleep</h3>" logbook_page
1893 Added: && contains ~substring:"1 of 5 factors" logbook_page) );
1894 Added: ( "answering every factor stores exactly one signal per category",
1895 Added: `Quick,
1896 Added: fun () ->
1897 Added: (* The flow admits one answer per factor, so the stored report holds
1898 Added: one signal per category and never a duplicate. Walking all five
1899 Added: factors and saving proves the whole set persists without a
1900 Added: duplicate-signal rejection. *)
1901 Added: let c = client () in
1902 Added: let _ = sign_in_new c in
1903 Added: let _ = get c "/logbook?feedback=start" in
1904 Added: let rec advance step =
1905 Added: if step < 5 then begin
1906 Added: let token = Option.get (csrf_token (body (get c "/logbook"))) in
1907 Added: let _ =
1908 Added: post c "/feedback"
1909 Added: [
1910 Added: ("dream.csrf", token);
1911 Added: ("step", string_of_int step);
1912 Added: ("action", "next");
1913 Added: ("choice", "below");
1914 Added: ]
1915 Added: in
1916 Added: advance (step + 1)
1917 Added: end
1918 Added: in
1919 Added: advance 0;
1920 Added: let token = Option.get (csrf_token (body (get c "/logbook"))) in
1921 Added: let saved =
1922 Added: post c "/feedback"
1923 Added: [ ("dream.csrf", token); ("step", "5"); ("action", "save") ]
1924 Added: in
1925 Added: Alcotest.(check int) "final save redirects" 303 (status saved);
1926 Added: let logbook_page = body (get c "/logbook") in
1927 Added: Alcotest.(check bool)
1928 Added: "stores one signal per distinct factor" true
1929 Added: (contains ~substring:"Sleep below usual" logbook_page
1930 Added: && contains ~substring:"Appetite below usual" logbook_page
1931 Added: && contains ~substring:"Readiness below usual" logbook_page
1932 Added: && contains ~substring:"Motivation below usual" logbook_page
1933 Added: && contains ~substring:"Difficulty below usual" logbook_page) );
1934 Added: ] );
1935 Added: ( "web.lifecycle",
1936 Added: [
1937 Added: ( "a full cycle: begin Day 1, record every slot, finish, and see it \
1938 Added: saved complete",
1939 Added: `Quick,
1940 Added: fun () ->
1941 Added: (* A deep end-to-end pass over the primary flow with a driven clock.
1942 Added: Begin Day 1, record all four slots, finish, and confirm the
1943 Added: logbook persists a complete Day 1 with four stimuli. *)
1944 Added: let c, now = clocked_client () in
1945 Added: now := day 10;
1946 Added: let _ = register c ~username:"lifter" ~password:"heavyduty1" in
1947 Added: let _ = start_day_one c in
1948 Added: (* Fresh workout: nothing recorded yet. *)
1949 Added: Alcotest.(check bool)
1950 Added: "opens with no slots recorded" true
1951 Added: (contains ~substring:"0 of 4 recorded" (body (get c "/workout")));
1952 Added: record_all_day_one_slots c;
1953 Added: Alcotest.(check bool)
1954 Added: "counts all four slots recorded" true
1955 Added: (contains ~substring:"4 of 4 recorded" (body (get c "/workout")));
1956 Added: let token = Option.get (csrf_token (body (get c "/workout"))) in
1957 Added: let finished = post c "/workout/finish" [ ("dream.csrf", token) ] in
1958 Added: Alcotest.(check int) "finish redirects" 303 (status finished);
1959 Added: Alcotest.(check bool)
1960 Added: "sends the trainee to the logbook feedback prompt" true
1961 Added: (List.mem "/logbook?prompt=feedback"
1962 Added: (Dream.headers finished "Location"));
1963 Added: let logbook_page = body (get c "/logbook") in
1964 Added: Alcotest.(check bool)
1965 Added: "the logbook lists the saved Day 1 with all its stimuli" true
1966 Added: (contains ~substring:"Day 1 — 4 stimuli" logbook_page);
1967 Added: Alcotest.(check bool)
1968 Added: "marks the saved workout complete" true
1969 Added: (contains ~substring:"complete" logbook_page);
1970 Added: (* The saved workout is durable and reachable at its own URL. *)
1971 Added: let record_page = body (get c "/logbook/w1") in
1972 Added: Alcotest.(check bool)
1973 Added: "the saved workout persists all four slots" true
1974 Added: (contains ~substring:"4 of 4 recorded" record_page) );
1975 Added: ( "the recovery gate blocks the next workout until enough rest, then \
1976 Added: Day 2 begins",
1977 Added: `Quick,
1978 Added: fun () ->
1979 Added: (* The deepest flow: after finishing Day 1, the next workout is
1980 Added: gated until 48 hours of recovery pass. The web tier drives the
1981 Added: clock, so this proves the gate over HTTP, not just in the
1982 Added: service. Beginning too early is refused; resting past the
1983 Added: interval clears the gate and advances the cycle to Day 2. *)
1984 Added: let c, now = clocked_client () in
1985 Added: now := day 10;
1986 Added: let _ = register c ~username:"lifter" ~password:"heavyduty1" in
1987 Added: let _ = start_day_one c in
1988 Added: record_all_day_one_slots c;
1989 Added: let token = Option.get (csrf_token (body (get c "/workout"))) in
1990 Added: let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
1991 Added: (* One hour later: recovery is far from complete. Home shows the
1992 Added: gate and beginning without an override is refused. *)
1993 Added: now := day 10 + 3600;
1994 Added: let home = body (get c "/") in
1995 Added: Alcotest.(check bool)
1996 Added: "Home shows the recovery gate" true
1997 Added: (contains ~substring:"requires recovery" home);
1998 Added: let token = Option.get (csrf_token home) in
1999 Added: let refused =
2000 Added: post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
2001 Added: in
2002 Added: Alcotest.(check int)
2003 Added: "an early begin is bounced back" 303 (status refused);
2004 Added: Alcotest.(check bool)
2005 Added: "the early begin returns Home, not the workout" true
2006 Added: (List.mem "/" (Dream.headers refused "Location"));
2007 Added: Alcotest.(check int)
2008 Added: "no workout was started" 303
2009 Added: (status (get c "/workout"));
2010 Added: (* Rest two full days: past the 48-hour training interval. The gate
2011 Added: clears and the next begin succeeds. *)
2012 Added: now := day 13;
2013 Added: let home = body (get c "/") in
2014 Added: Alcotest.(check bool)
2015 Added: "Home no longer requires recovery" false
2016 Added: (contains ~substring:"requires recovery" home);
2017 Added: let token = Option.get (csrf_token home) in
2018 Added: let began =
2019 Added: post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
2020 Added: in
2021 Added: Alcotest.(check int) "the rested begin succeeds" 303 (status began);
2022 Added: Alcotest.(check bool)
2023 Added: "the rested begin reaches the workout" true
2024 Added: (List.mem "/workout" (Dream.headers began "Location"));
2025 Added: let workout = body (get c "/workout") in
2026 Added: Alcotest.(check bool)
2027 Added: "the cycle has advanced to Day 2" true
2028 Added: (contains ~substring:"<h1>Day 2</h1>" workout);
2029 Added: Alcotest.(check bool)
2030 Added: "the rested workout shows no early-training notice" false
2031 Added: (contains ~substring:"Begun before recovery finished." workout) );
2032 Added: ] );
2033 Added: ( "web.isolation",
2034 Added: [
2035 Added: ( "two trainees never share workout or logbook state",
2036 Added: `Quick,
2037 Added: fun () ->
2038 Added: (* Two trainees on the same store, each with their own cookie jar.
2039 Added: One begins and finishes a workout; the other must see none of it:
2040 Added: not the in-progress workout, not the saved record. This proves
2041 Added: the per-trainee scoping the service promises, over HTTP. *)
2042 Added: let alice = client () in
2043 Added: let _ = register alice ~username:"alice" ~password:"heavyduty1" in
2044 Added: let bob = { alice with jar = [] } in
2045 Added: let _ = register bob ~username:"bobby" ~password:"heavyduty1" in
2046 Added: (* Alice selects and begins a workout, records a slot. *)
2047 Added: let token = Option.get (csrf_token (body (get alice "/"))) in
2048 Added: let _ =
2049 Added: post alice "/routines/ideal/select" [ ("dream.csrf", token) ]
2050 Added: in
2051 Added: let token = Option.get (csrf_token (body (get alice "/"))) in
2052 Added: let _ =
2053 Added: post alice "/workout"
2054 Added: [ ("dream.csrf", token); ("override", "false") ]
2055 Added: in
2056 Added: let token =
2057 Added: Option.get (csrf_token (body (get alice "/workout?slot=1")))
2058 Added: in
2059 Added: let _ =
2060 Added: post alice "/workout/slots/1"
2061 Added: [
2062 Added: ("dream.csrf", token);
2063 Added: ("load", "12");
2064 Added: ("reps", "8");
2065 Added: ("extension", "");
2066 Added: ]
2067 Added: in
2068 Added: (* Bob has no workout in progress: his workout view redirects. *)
2069 Added: Alcotest.(check int)
2070 Added: "Bob has no workout in progress" 303
2071 Added: (status (get bob "/workout"));
2072 Added: (* Bob's Home does not show Alice's in-progress card. *)
2073 Added: let bob_home = body (get bob "/") in
2074 Added: Alcotest.(check bool)
2075 Added: "Bob's Home has no in-progress card" false
2076 Added: (contains ~substring:"in-progress-card" bob_home);
2077 Added: (* Alice finishes; her record becomes w1 for her. *)
2078 Added: let token = Option.get (csrf_token (body (get alice "/workout"))) in
2079 Added: let _ = post alice "/workout/finish" [ ("dream.csrf", token) ] in
2080 Added: let alice_logbook = body (get alice "/logbook") in
2081 Added: Alcotest.(check bool)
2082 Added: "Alice sees her saved workout" true
2083 Added: (contains ~substring:"/logbook/w1" alice_logbook);
2084 Added: (* Bob's logbook is empty and Alice's record is not reachable from
2085 Added: Bob's session. Ids are per-trainee, so Bob's own w1 does not
2086 Added: exist yet. *)
2087 Added: let bob_logbook = body (get bob "/logbook") in
2088 Added: Alcotest.(check bool)
2089 Added: "Bob's logbook shows nothing logged" true
2090 Added: (contains ~substring:"Nothing logged yet." bob_logbook);
2091 Added: Alcotest.(check bool)
2092 Added: "Bob's logbook does not list Alice's record" false
2093 Added: (contains ~substring:"Day 1 — " bob_logbook);
2094 Added: Alcotest.(check int)
2095 Added: "Bob cannot open a record he does not own" 404
2096 Added: (status (get bob "/logbook/w1")) );
1527 2097 ] );
1528 2098 ]
1529 2099