open Todo
let contains s sub =
let n = String.length sub in
let rec go i = i + n <= String.length s && (String.sub s i n = sub || go (i + 1)) in
go 0
let count s sub =
let n = String.length sub in
let rec go i acc =
if i + n > String.length s then acc
else if String.sub s i n = sub then go (i + n) (acc + 1)
else go (i + 1) acc
in
go 0 0
(* --- Form ---------------------------------------------------------------- *)
let form_decoding () =
Alcotest.(check (list (pair string string)))
"plus, escapes, utf-8, empty, bare key"
[ ("title", "buy milk & eggs"); ("filter", "active"); ("u", "naïve 🚀"); ("e", ""); ("k", "") ]
(Form.parse "title=buy+milk+%26+eggs&filter=active&u=na%C3%AFve%20%F0%9F%9A%80&e=&k");
Alcotest.(check string) "malformed escapes are kept" "100%" (Form.decode "100%");
Alcotest.(check string) "bad hex is kept" "%zz" (Form.decode "%zz")
let trimming () =
Alcotest.(check string) "ascii" "a b" (Form.trim " \t a b \n");
Alcotest.(check string) "nbsp, ideographic, em space, bom" "naïve 🚀"
(Form.trim "\u{00A0}\u{3000}\u{2003}\u{FEFF}naïve 🚀\u{00A0}\u{202F}");
Alcotest.(check string) "only nbsp" "" (Form.trim "\u{00A0}\u{00A0}");
Alcotest.(check string) "inner kept" "a\u{00A0}b" (Form.trim "a\u{00A0}b");
Alcotest.(check string) "empty" "" (Form.trim "")
let cookies () =
Alcotest.(check (option string)) "finds by name" (Some "abc")
(Form.cookie "a=1; todomvc_list=abc; b=2" "todomvc_list");
Alcotest.(check (option string)) "missing" None (Form.cookie "a=1" "todomvc_list");
Alcotest.(check (option string)) "garbled" None (Form.cookie "%%%" "todomvc_list")
let escaping () =
Alcotest.(check string) "html" "<img src=x> & "q" '"
(View.escape {| & "q" '|})
(* --- App ----------------------------------------------------------------- *)
let origin = "http://127.0.0.1:8080"
let fresh () = App.create ~store:(Store.open_ ":memory:") ~origin ~version:"test-version"
let request ?(meth = "GET") ?(headers = []) ?(body = "") ?cookie target =
let uri = Uri.of_string target in
let headers = match cookie with Some c -> ("cookie", "todomvc_list=" ^ c) :: headers | None -> headers in
{ App.meth; path = Uri.path uri;
query = List.map (fun (k, v) -> (k, String.concat "," v)) (Uri.query uri);
header = (fun h -> List.assoc_opt h headers); body }
let header (r : App.response) name = List.assoc_opt name r.headers
let set_cookie (r : App.response) =
match header r "set-cookie" with
| Some c -> String.sub c (String.length "todomvc_list=") 32
| None -> Alcotest.fail "no set-cookie"
(* A visitor: their cookie, from a first GET. *)
let visit app = set_cookie (App.handle app (request "/"))
let post app cookie ?headers target body =
App.handle app (request ~meth:"POST" ?headers ~cookie ~body target)
let page app cookie target = (App.handle app (request ~cookie target)).body
let first_cookie () =
let app = fresh () in
let r = App.handle app (request "/") in
Alcotest.(check int) "status" 200 r.status;
let c = Option.get (header r "set-cookie") in
Alcotest.(check bool) "attributes" true
(contains c "; Path=/" && contains c "; HttpOnly" && contains c "; SameSite=Lax"
&& not (contains c "Secure"));
Alcotest.(check bool) "empty list hides main and footer" false
(contains r.body {|class="main"|} || contains r.body {|class="footer"|});
let again = App.handle app (request ~cookie:(set_cookie r) "/") in
Alcotest.(check (option string)) "known cookie kept" None (header again "set-cookie")
let secure_cookie () =
let app = App.create ~store:(Store.open_ ":memory:") ~origin:"https://x.example" ~version:"v" in
let c = Option.get (header (App.handle app (request "/")) "set-cookie") in
Alcotest.(check bool) "Secure over https" true (contains c "; Secure")
let garbled_cookie () =
let app = fresh () in
List.iter
(fun bad ->
let r = App.handle app (request ~cookie:bad "/") in
Alcotest.(check int) "status" 200 r.status;
Alcotest.(check bool) "fresh cookie" true (header r "set-cookie" <> None))
[ "%%%"; ""; String.make 32 'a'; "not-a-token" ]
let add_and_redirect () =
let app = fresh () in
let c = visit app in
let r = post app c "/todos" "title=++buy+milk++&filter=active" in
Alcotest.(check int) "303" 303 r.status;
Alcotest.(check (option string)) "back to filter" (Some "/active") (header r "location");
let r = post app c "/todos" "title=x&filter=bogus" in
Alcotest.(check (option string)) "unknown filter" (Some "/") (header r "location");
let r = post app c "/todos" "title=y" in
Alcotest.(check (option string)) "missing filter" (Some "/") (header r "location");
ignore (post app c "/todos" "title=+++&filter=all");
ignore (post app c "/todos" "title=%C2%A0%C2%A0&filter=all");
let body = page app c "/" in
Alcotest.(check int) "three items, blank skipped" 3 (count body "