open Hito_app open Support let suite = [ ( "web.app_feedback", [ ( "the app feedback screen and its tabs are served", `Quick, fun () -> let c = client () in let _ = sign_in_new c in Alcotest.(check int) "write tab served" 200 (status (get c "/app-feedback")); Alcotest.(check int) "submitted tab served" 200 (status (get c "/app-feedback?tab=submitted")) ); ( "blank app feedback is refused; a real message is stored", `Quick, fun () -> let c, repo = client_with_repo () in let _ = register c ~username:"lifter" ~password:"heavyduty1" in let trainee = the_trainee repo ~username:"lifter" in let stored_count () = List.length (Lwt_main.run (Test_repo.app_feedback repo ~viewer:trainee.Trainee.id)) in let token = Option.get (csrf_token (body (get c "/app-feedback"))) 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")); (* Blank input stored nothing. *) Alcotest.(check int) "no report stored for blank input" 0 (stored_count ()); 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")); (* The report now exists with the exact message. *) let reports = Lwt_main.run (Test_repo.app_feedback repo ~viewer:trainee.Trainee.id) in Alcotest.(check int) "one report stored" 1 (List.length reports); Alcotest.(check string) "stores the exact message" "The app is clear.\nThank you!" (List.hd reports).Repository.message ); ( "feedback records its author and accepts another trainee's vote", `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 submit client message = let token = Option.get (csrf_token (body (get client "/app-feedback"))) 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")); (* Bob's report is t2:1 — the vote control on Alice's view posts there. Alice upvotes it. *) let list_page = body (get alice "/app-feedback?tab=submitted") in 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) ); ( "an owner can edit and remove their feedback", `Quick, fun () -> let c, repo = client_with_repo () in let _ = register c ~username:"lifter" ~password:"heavyduty1" in let trainee = the_trainee repo ~username:"lifter" in let token = Option.get (csrf_token (body (get c "/app-feedback"))) in let _ = post c "/app-feedback/submit" [ ("dream.csrf", token); ("message", "First report") ] in let feedback_id () = (List.hd (Lwt_main.run (Test_repo.app_feedback repo ~viewer:trainee.Trainee.id))) .Repository.feedback_id |> Repository.app_feedback_id_to_string in let id = feedback_id () in (* A blank edit is refused and changes nothing. *) let token = Option.get (csrf_token (body (get c "/app-feedback?tab=submitted"))) in let blank = post c ("/app-feedback/" ^ id ^ "/edit") [ ("dream.csrf", token); ("message", " ") ] in Alcotest.(check int) "blank edit redirects" 303 (status blank); Alcotest.(check string) "message unchanged by blank edit" "First report" (List.hd (Lwt_main.run (Test_repo.app_feedback repo ~viewer:trainee.Trainee.id))) .Repository.message; (* A real edit changes the stored message. *) let token = Option.get (csrf_token (body (get c "/app-feedback?tab=submitted"))) in let edited = post c ("/app-feedback/" ^ id ^ "/edit") [ ("dream.csrf", token); ("message", "Edited report") ] in Alcotest.(check int) "edit redirects" 303 (status edited); Alcotest.(check string) "message updated by edit" "Edited report" (List.hd (Lwt_main.run (Test_repo.app_feedback repo ~viewer:trainee.Trainee.id))) .Repository.message; (* Removal deletes the report. *) let token = Option.get (csrf_token (body (get c "/app-feedback?tab=submitted"))) in let removed = post c ("/app-feedback/" ^ id ^ "/remove") [ ("dream.csrf", token) ] in Alcotest.(check int) "remove redirects" 303 (status removed); Alcotest.(check int) "report removed" 0 (List.length (Lwt_main.run (Test_repo.app_feedback repo ~viewer:trainee.Trainee.id))) ); ( "another trainee cannot edit or remove app feedback", `Quick, fun () -> let alice, repo = client_with_repo () 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_trainee = the_trainee repo ~username:"bobby" in (* Bob owns t2:1. Alice attempts to edit and remove it. Both are refused at the ownership check, so the report and its message survive. *) let token = Option.get (csrf_token (body (get bob "/app-feedback"))) in let _ = post bob "/app-feedback/submit" [ ("dream.csrf", token); ("message", "Bob owns this") ] in let stored () = Lwt_main.run (Test_repo.app_feedback repo ~viewer:bob_trainee.Trainee.id) in Alcotest.(check int) "one report before cross-user attempts" 1 (List.length (stored ())); let token = Option.get (csrf_token (body (get alice "/app-feedback?tab=submitted"))) 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 token = Option.get (csrf_token (body (get alice "/app-feedback?tab=submitted"))) in let removed = post alice "/app-feedback/t2:1/remove" [ ("dream.csrf", token) ] in Alcotest.(check int) "cross-user remove redirects" 303 (status removed); (* The report still exists with its original message: the cross-user attempts changed nothing. *) let after = stored () in Alcotest.(check int) "report survives cross-user attempts" 1 (List.length after); Alcotest.(check string) "message is unchanged" "Bob owns this" (List.hd after).Repository.message ); ] ); ]