[OCaml] High Intensity Training Online
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.
test/test_web.ml
@@ -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