open Hito_app open Support let suite = [ ( "web.import", (* The Hevy import web flow, end to end. Each test drives the multipart upload and the review POSTs, then asserts status, redirect location, and Repository state — never rendered HTML. The CSRF token is read from the profile page body, as a browser would. *) let header = "title,start_time,end_time,description,exercise_title,superset_id,exercise_notes,set_index,set_type,weight_kg,reps,distance_km,duration_seconds,rpe\r\n" in (* One native row. Datetimes are Hevy local wall time. *) let row ~title ~exercise ~set_type ~weight ~reps ?(set_index = 0) ?(start = "01 Jan 2024, 10:00") ?(finish = "01 Jan 2024, 11:00") () = Printf.sprintf "%s,%S,%S,,%s,,,%d,%s,%s,%d,,,\r\n" title start finish exercise set_index set_type weight reps in let csv rows = header ^ String.concat "" rows in (* Register, then read a CSRF token from the profile page. *) let signed_in_profile_token c = let _ = register c ~username:"lifter" ~password:"heavyduty1" in Option.get (csrf_token (body (get c "/profile"))) in let review_token c url = Option.get (csrf_token (body (get c url))) in let batches repo trainee = Lwt_main.run (Test_repo.import_batches repo trainee.Trainee.id) in let imported repo trainee = Lwt_main.run (Test_repo.imported_records repo trainee.Trainee.id) in [ ( "a valid native export uploads to a stored batch with immutable source", `Quick, fun () -> let c, repo = client_with_repo ~import_open:true () in let token = signed_in_profile_token c in let trainee = the_trainee repo ~username:"lifter" in let source = csv [ row ~title:"Legs" ~exercise:"Barbell Squat" ~set_type:"failure" ~weight:"100" ~reps:8 (); ] in let response = post_multipart c "/profile/import" ~file:(Some ("csv", "workouts.csv", source)) ~fields:[ ("dream.csrf", token); ("utc_offset_seconds", "0") ] in Alcotest.(check int) "303 to review" 303 (status response); Alcotest.(check bool) "redirects to the review URL" true (index_from ~needle:"/profile/import/" (location response) 0 |> Option.is_some); let stored = batches repo trainee in Alcotest.(check int) "one batch stored" 1 (List.length stored); Alcotest.(check string) "source retained verbatim" source (Workout_import.batch_source (List.hd stored)) ); ( "a duplicate upload is refused, the override stores a second batch", `Quick, fun () -> let c, repo = client_with_repo ~import_open:true () in let token = signed_in_profile_token c in let trainee = the_trainee repo ~username:"lifter" in let source = csv [ row ~title:"Legs" ~exercise:"Barbell Squat" ~set_type:"failure" ~weight:"100" ~reps:8 (); ] in let upload ?(allow = false) () = let token = Option.get (csrf_token (body (get c "/profile"))) in let fields = [ ("dream.csrf", token); ("utc_offset_seconds", "0") ] @ if allow then [ ("allow_duplicate", "true") ] else [] in post_multipart c "/profile/import" ~file:(Some ("csv", "workouts.csv", source)) ~fields in ignore token; let first = upload () in Alcotest.(check int) "first upload 303" 303 (status first); let second = upload () in Alcotest.(check int) "duplicate redirects to profile" 303 (status second); Alcotest.(check string) "duplicate lands on profile" "/profile" (location second); Alcotest.(check int) "still one batch after duplicate" 1 (List.length (batches repo trainee)); let overridden = upload ~allow:true () in Alcotest.(check int) "override 303" 303 (status overridden); let stored = batches repo trainee in Alcotest.(check int) "override stores a second batch" 2 (List.length stored); (* The two batches have distinct ids, so the override did not overwrite the original. *) let ids = List.map (fun b -> Workout_import.batch_id_to_string (Workout_import.batch_identity b)) stored in Alcotest.(check bool) "the two batch ids differ" true (match ids with [ a; b ] -> a <> b | _ -> false) ); ( "warmup and unsupported rows persist as warnings, not sets", `Quick, fun () -> let c, repo = client_with_repo ~import_open:true () in let token = signed_in_profile_token c in let trainee = the_trainee repo ~username:"lifter" in let source = csv [ row ~title:"Legs" ~exercise:"Barbell Squat" ~set_type:"warmup" ~weight:"40" ~reps:10 ~set_index:0 (); row ~title:"Legs" ~exercise:"Barbell Squat" ~set_type:"wibble" ~weight:"50" ~reps:10 ~set_index:1 (); row ~title:"Legs" ~exercise:"Barbell Squat" ~set_type:"failure" ~weight:"100" ~reps:8 ~set_index:2 (); ] in let response = post_multipart c "/profile/import" ~file:(Some ("csv", "workouts.csv", source)) ~fields:[ ("dream.csrf", token); ("utc_offset_seconds", "0") ] in Alcotest.(check int) "303 to review" 303 (status response); let batch = List.hd (batches repo trainee) in (* Two warnings: the warmup and the unknown set type. *) Alcotest.(check int) "two rows dropped as warnings" 2 (List.length (Workout_import.batch_warnings batch)); (* The workout keeps only the one failure set; no provisional set was invented for the dropped rows. *) let w = List.hd (Workout_import.batch_workouts batch) in Alcotest.(check int) "only the failure set survives" 1 (List.length (Workout_import.workout_sets w)) ); ( "map, confirm, then promote writes an imported record", `Quick, fun () -> let c, repo = client_with_repo ~import_open:true () in let token = signed_in_profile_token c in let trainee = the_trainee repo ~username:"lifter" in (* A single normal set: it needs both a mapping and a normal-set confirmation before it can promote. *) let source = csv [ row ~title:"Legs" ~exercise:"Barbell Squat" ~set_type:"normal" ~weight:"100" ~reps:8 (); ] in let _ = post_multipart c "/profile/import" ~file:(Some ("csv", "workouts.csv", source)) ~fields:[ ("dream.csrf", token); ("utc_offset_seconds", "0") ] in let batch = List.hd (batches repo trainee) in let bid = Workout_import.batch_id_to_string (Workout_import.batch_identity batch) in let wid = Workout_import.workout_id_to_string (Workout_import.workout_identity (List.hd (Workout_import.batch_workouts batch))) in let review = "/profile/import/" ^ bid in let map_url = Printf.sprintf "/profile/import/%s/workouts/%s/map" bid wid in let t = review_token c review in let _ = post c map_url [ ("dream.csrf", t); ("source_name", "Barbell Squat"); ("exercise_id", "squats"); ] in let t = review_token c review in let confirm_url = Printf.sprintf "/profile/import/%s/workouts/%s/confirm" bid wid in let _ = post c confirm_url [ ("dream.csrf", t); ("acknowledgement", "clean form") ] in let t = review_token c review in let promote_url = Printf.sprintf "/profile/import/%s/workouts/%s/promote" bid wid in let promote = post c promote_url [ ("dream.csrf", t) ] in Alcotest.(check int) "promotion redirects" 303 (status promote); Alcotest.(check int) "one imported record exists" 1 (List.length (imported repo trainee)) ); ( "a dropset-only workout promotes nothing, writing no record", `Quick, fun () -> (* A drop set is discarded on import — HD1 keeps only the drive to failure. A workout of nothing but a drop set therefore has no promotable set and writes no record. *) let c, repo = client_with_repo ~import_open:true () in let token = signed_in_profile_token c in let trainee = the_trainee repo ~username:"lifter" in let source = csv [ row ~title:"Legs" ~exercise:"Barbell Squat" ~set_type:"dropset" ~weight:"100" ~reps:8 (); ] in let _ = post_multipart c "/profile/import" ~file:(Some ("csv", "workouts.csv", source)) ~fields:[ ("dream.csrf", token); ("utc_offset_seconds", "0") ] in let batch = List.hd (batches repo trainee) in let bid = Workout_import.batch_id_to_string (Workout_import.batch_identity batch) in let wid = Workout_import.workout_id_to_string (Workout_import.workout_identity (List.hd (Workout_import.batch_workouts batch))) in let review = "/profile/import/" ^ bid in (* Even mapping the discarded set's exercise cannot make it promote. *) let t = review_token c review in let _ = post c (Printf.sprintf "/profile/import/%s/workouts/%s/map" bid wid) [ ("dream.csrf", t); ("source_name", "Barbell Squat"); ("exercise_id", "squats"); ] in let t = review_token c review in let promote = post c (Printf.sprintf "/profile/import/%s/workouts/%s/promote" bid wid) [ ("dream.csrf", t) ] in Alcotest.(check int) "still a redirect" 303 (status promote); Alcotest.(check int) "no record written for a dropset-only workout" 0 (List.length (imported repo trainee)) ); ( "a partial batch promotion promotes the eligible sibling and retains \ the blocked one", `Quick, fun () -> let c, repo = client_with_repo ~import_open:true () in let token = signed_in_profile_token c in let trainee = the_trainee repo ~username:"lifter" in (* Two workouts: one ready failure set, one dropset-only workout that promotes nothing once its drop set is discarded. *) let source = csv [ row ~title:"Legs" ~exercise:"Barbell Squat" ~set_type:"failure" ~weight:"100" ~reps:8 ~start:"01 Jan 2024, 10:00" ~finish:"01 Jan 2024, 11:00" (); row ~title:"Arms" ~exercise:"Barbell Curl" ~set_type:"dropset" ~weight:"30" ~reps:8 ~start:"02 Jan 2024, 10:00" ~finish:"02 Jan 2024, 11:00" (); ] in let _ = post_multipart c "/profile/import" ~file:(Some ("csv", "workouts.csv", source)) ~fields:[ ("dream.csrf", token); ("utc_offset_seconds", "0") ] in let batch = List.hd (batches repo trainee) in let bid = Workout_import.batch_id_to_string (Workout_import.batch_identity batch) in let review = "/profile/import/" ^ bid in (* Map the ready workout's exercise so its only blocker clears. *) let ready_wid = List.find_map (fun w -> if String.equal (Workout_import.workout_title w) "Legs" then Some (Workout_import.workout_id_to_string (Workout_import.workout_identity w)) else None) (Workout_import.batch_workouts batch) |> Option.get in let t = review_token c review in let _ = post c (Printf.sprintf "/profile/import/%s/workouts/%s/map" bid ready_wid) [ ("dream.csrf", t); ("source_name", "Barbell Squat"); ("exercise_id", "squats"); ] in let t = review_token c review in let promote = post c (Printf.sprintf "/profile/import/%s/promote" bid) [ ("dream.csrf", t) ] in Alcotest.(check int) "batch promotion redirects" 303 (status promote); (* Only the eligible sibling is promoted. The dropset-only workout has nothing to promote, writes no record, and stays in the batch. *) Alcotest.(check int) "one record from the eligible sibling" 1 (List.length (imported repo trainee)) ); ( "an unknown batch GET is 404, an unknown exercise mapping changes \ nothing", `Quick, fun () -> let c, repo = client_with_repo ~import_open:true () in let token = signed_in_profile_token c in let trainee = the_trainee repo ~username:"lifter" in (* Unknown batch id: the review GET is a 404. *) ignore token; Alcotest.(check int) "unknown batch review is 404" 404 (status (get c "/profile/import/no-such-batch")); (* Upload a real batch, then map to an id absent from the catalog. No mapping is recorded. *) let source = csv [ row ~title:"Legs" ~exercise:"Barbell Squat" ~set_type:"normal" ~weight:"100" ~reps:8 (); ] in let up_token = Option.get (csrf_token (body (get c "/profile"))) in let _ = post_multipart c "/profile/import" ~file:(Some ("csv", "workouts.csv", source)) ~fields: [ ("dream.csrf", up_token); ("utc_offset_seconds", "0") ] in let batch = List.hd (batches repo trainee) in let bid = Workout_import.batch_id_to_string (Workout_import.batch_identity batch) in let wid = Workout_import.workout_id_to_string (Workout_import.workout_identity (List.hd (Workout_import.batch_workouts batch))) in let review = "/profile/import/" ^ bid in let t = review_token c review in let response = post c (Printf.sprintf "/profile/import/%s/workouts/%s/map" bid wid) [ ("dream.csrf", t); ("source_name", "Barbell Squat"); ("exercise_id", "not-an-exercise"); ] in Alcotest.(check int) "unknown exercise redirects, not errors" 303 (status response); (* The stored workout still has no mapping for the source name. *) let reloaded = List.hd (batches repo trainee) in let w = List.hd (Workout_import.batch_workouts reloaded) in Alcotest.(check bool) "no mapping was recorded" true (Option.is_none (Workout_import.workout_mapping w ~source_name:"Barbell Squat")) ); ] ); ]