(** URL dispatch: that {!Ogit.Routes.dispatch} recovers the intended route, rejects malformed input, and inverts {!Ogit.Routes.path_of}. *) let route_to_string = function | Ogit.Routes.Root -> "Root" | Ogit.Routes.Project_dir d -> "Project_dir " ^ d | Ogit.Routes.Repo r -> "Repo " ^ r | Ogit.Routes.Commits r -> "Commits " ^ r | Ogit.Routes.Commits_branch (r, b) -> "Commits_branch (" ^ r ^ ", " ^ b ^ ")" | Ogit.Routes.Commit (r, h) -> "Commit (" ^ r ^ ", " ^ h ^ ")" | Ogit.Routes.Files r -> "Files " ^ r | Ogit.Routes.File (r, h) -> "File (" ^ r ^ ", " ^ h ^ ")" | Ogit.Routes.File_at (r, p) -> "File_at (" ^ r ^ ", " ^ p ^ ")" | Ogit.Routes.Raw_file (r, h) -> "Raw_file (" ^ r ^ ", " ^ h ^ ")" | Ogit.Routes.Raw_at (r, p) -> "Raw_at (" ^ r ^ ", " ^ p ^ ")" let route_testable = let pp fmt = function | None -> Format.fprintf fmt "None" | Some route -> Format.fprintf fmt "Some (%s)" (route_to_string route) in Alcotest.testable pp ( = ) let check_dispatch msg path expected = Alcotest.check route_testable msg expected (Ogit.Routes.dispatch path) let test_basic_actions () = check_dispatch "summary" "myrepo/summary/" (Some (Repo "myrepo")); check_dispatch "commits" "myrepo/commits/" (Some (Commits "myrepo")); check_dispatch "files" "myrepo/files/" (Some (Files "myrepo")) let full_hash = String.make 40 'a' let test_parametric_actions () = check_dispatch "commit with hash" "myrepo/commit/abc123" (Some (Commit ("myrepo", "abc123"))); check_dispatch "file with full hash" ("myrepo/file/" ^ full_hash) (Some (File ("myrepo", full_hash))); check_dispatch "raw with full hash" ("myrepo/raw/" ^ full_hash) (Some (Raw_file ("myrepo", full_hash))); check_dispatch "commits for branch" "myrepo/commits/main" (Some (Commits_branch ("myrepo", "main"))) let test_path_actions () = check_dispatch "file at path" "myrepo/file/src/main.ml" (Some (File_at ("myrepo", "src/main.ml"))); check_dispatch "file at single segment" "myrepo/file/README.md" (Some (File_at ("myrepo", "README.md"))); check_dispatch "raw at path" "myrepo/raw/doc/logo.svg" (Some (Raw_at ("myrepo", "doc/logo.svg"))); check_dispatch "encoded segment decodes" "myrepo/file/a%20b.txt" (Some (File_at ("myrepo", "a b.txt"))) let test_nested_repo () = check_dispatch "nested summary" "sub/dir/repo/summary/" (Some (Repo "sub/dir/repo")); check_dispatch "nested commit" "sub/repo/commit/abc" (Some (Commit ("sub/repo", "abc"))); check_dispatch "deeply nested" "a/b/c/repo/files/" (Some (Files "a/b/c/repo")) (* A path without a reserved segment is reported as [Project_dir]; the handler decides whether the filesystem holds a repository or a directory there. *) let test_implicit_summary () = check_dispatch "bare repo path" "myrepo" (Some (Project_dir "myrepo")); check_dispatch "bare with trailing slash" "myrepo/" (Some (Project_dir "myrepo")) let test_root () = check_dispatch "empty path is root" "" (Some Root) let test_malformed () = check_dispatch "commit without hash" "myrepo/commit/" None; check_dispatch "file without hash" "myrepo/file/" None; check_dispatch "raw without hash" "myrepo/raw/" None; check_dispatch "action without repo" "summary/" None; check_dispatch "trailing junk after summary" "myrepo/summary/junk" None; check_dispatch "trailing junk after files" "myrepo/files/extra" None; check_dispatch "trailing junk after hash" "myrepo/commit/abc/def" None (* The documented contract: dispatch inverts path_of for every route shape, including file paths that need percent-encoding. *) let test_round_trip () = let samples = Ogit.Routes. [ Root; Project_dir "dir"; Project_dir "nested/dir"; Repo "repo"; Repo "sub/repo"; Commits "repo"; Commits_branch ("repo", "main"); Commit ("repo", "abc123"); Files "repo"; File ("repo", String.make 40 'a'); File_at ("repo", "src/main.ml"); File_at ("repo", "dir with space/na%me.txt"); Raw_file ("repo", String.make 40 'b'); Raw_at ("repo", "doc/logo.svg"); ] in List.iter (fun route -> let path = Ogit.Routes.path_of route in let stripped = String.sub path 1 (String.length path - 1) in Alcotest.check route_testable (Printf.sprintf "round trip %s" (route_to_string route)) (Some route) (Ogit.Routes.dispatch stripped)) samples let suite = ( "dispatch", [ Alcotest.test_case "basic actions" `Quick test_basic_actions; Alcotest.test_case "parametric actions" `Quick test_parametric_actions; Alcotest.test_case "path actions" `Quick test_path_actions; Alcotest.test_case "nested repo" `Quick test_nested_repo; Alcotest.test_case "implicit summary" `Quick test_implicit_summary; Alcotest.test_case "root" `Quick test_root; Alcotest.test_case "malformed URLs" `Quick test_malformed; Alcotest.test_case "round trip" `Quick test_round_trip; ] )