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 "
  • buy milk"); Alcotest.(check bool) "counter" true (contains body "3 items left") let escapes_titles () = let app = fresh () in let c = visit app in ignore (post app c "/todos" "title=%3Cimg+src%3Dx+onerror%3Dalert(1)%3E&filter=all"); let body = page app c "/" in Alcotest.(check bool) "escaped" true (contains body "<img src=x onerror=alert(1)>"); Alcotest.(check bool) "no tag" false (contains body "|}); Alcotest.(check bool) "singular counter" true (contains all "1 item left"); Alcotest.(check bool) "clear completed shown" true (contains all "clear-completed"); Alcotest.(check bool) "all selected" true (contains all {|All|}); let active = page app c "/active" in Alcotest.(check bool) "active hides done" false (contains active {|data-id="1"|}); Alcotest.(check bool) "active selected" true (contains active {||}); let completed = page app c "/completed" in Alcotest.(check bool) "completed only" true (contains completed {|data-id="1"|} && not (contains completed {|data-id="2"|})) let toggle_all_and_clear () = let app = fresh () in let c = visit app in List.iter (fun t -> ignore (post app c "/todos" ("title=" ^ t))) [ "a"; "b"; "c" ]; ignore (post app c "/todos/2/toggle" ""); ignore (post app c "/todos/toggle-all" "filter=all"); let body = page app c "/" in Alcotest.(check int) "all completed" 3 (count body {|class="completed"|}); Alcotest.(check bool) "toggle-all checked" true (contains body {|form="toggle-all-form" checked|}); ignore (post app c "/todos/toggle-all" "filter=all"); Alcotest.(check int) "all active again" 0 (count (page app c "/") {|class="completed"|}); ignore (post app c "/todos/1/toggle" ""); ignore (post app c "/todos/clear-completed" "filter=completed"); let body = page app c "/" in Alcotest.(check int) "one cleared" 2 (count body "
  • |}); Alcotest.(check bool) "edit form" true (contains editing {|action="/todos/2/edit"|}); ignore (post app c "/todos/2/edit" "title=++renamed++&filter=all"); Alcotest.(check bool) "renamed" true (contains (page app c "/") ""); ignore (post app c "/todos/2/edit" "title=%C2%A0%E3%80%80&filter=all"); Alcotest.(check int) "empty edit deletes" 1 (count (page app c "/") "
  • mine" && not (contains body {|class="completed"|})) let origins () = let app = fresh () in let c = visit app in let r = post app c ~headers:[ ("origin", "https://evil.example") ] "/todos" "title=evil" in Alcotest.(check int) "cross-site refused" 403 r.status; let r = post app c ~headers:[ ("origin", origin) ] "/todos" "title=ok" in Alcotest.(check int) "own origin" 303 r.status; Alcotest.(check int) "one item" 1 (count (page app c "/") "
  • |} r.body && not (contains r.body "