[OCaml] Mobile-friendly clone of cgit.
1
(** URL dispatch: that {!Ogit.Routes.dispatch} recovers the intended route,
2
rejects malformed input, and inverts {!Ogit.Routes.path_of}. *)
3
4
let route_to_string = function
5
| Ogit.Routes.Root -> "Root"
6
| Ogit.Routes.Project_dir d -> "Project_dir " ^ d
7
| Ogit.Routes.Repo r -> "Repo " ^ r
8
| Ogit.Routes.Commits r -> "Commits " ^ r
9
| Ogit.Routes.Commits_branch (r, b) -> "Commits_branch (" ^ r ^ ", " ^ b ^ ")"
10
| Ogit.Routes.Commit (r, h) -> "Commit (" ^ r ^ ", " ^ h ^ ")"
11
| Ogit.Routes.Files r -> "Files " ^ r
12
| Ogit.Routes.File (r, h) -> "File (" ^ r ^ ", " ^ h ^ ")"
13
| Ogit.Routes.File_at (r, p) -> "File_at (" ^ r ^ ", " ^ p ^ ")"
14
| Ogit.Routes.Raw_file (r, h) -> "Raw_file (" ^ r ^ ", " ^ h ^ ")"
15
| Ogit.Routes.Raw_at (r, p) -> "Raw_at (" ^ r ^ ", " ^ p ^ ")"
16
17
let route_testable =
18
let pp fmt = function
19
| None -> Format.fprintf fmt "None"
20
| Some route -> Format.fprintf fmt "Some (%s)" (route_to_string route)
21
in
22
Alcotest.testable pp ( = )
23
24
let check_dispatch msg path expected =
25
Alcotest.check route_testable msg expected (Ogit.Routes.dispatch path)
26
27
let test_basic_actions () =
28
check_dispatch "summary" "myrepo/summary/" (Some (Repo "myrepo"));
29
check_dispatch "commits" "myrepo/commits/" (Some (Commits "myrepo"));
30
check_dispatch "files" "myrepo/files/" (Some (Files "myrepo"))
31
32
let full_hash = String.make 40 'a'
33
34
let test_parametric_actions () =
35
check_dispatch "commit with hash" "myrepo/commit/abc123"
36
(Some (Commit ("myrepo", "abc123")));
37
check_dispatch "file with full hash"
38
("myrepo/file/" ^ full_hash)
39
(Some (File ("myrepo", full_hash)));
40
check_dispatch "raw with full hash"
41
("myrepo/raw/" ^ full_hash)
42
(Some (Raw_file ("myrepo", full_hash)));
43
check_dispatch "commits for branch" "myrepo/commits/main"
44
(Some (Commits_branch ("myrepo", "main")))
45
46
let test_path_actions () =
47
check_dispatch "file at path" "myrepo/file/src/main.ml"
48
(Some (File_at ("myrepo", "src/main.ml")));
49
check_dispatch "file at single segment" "myrepo/file/README.md"
50
(Some (File_at ("myrepo", "README.md")));
51
check_dispatch "raw at path" "myrepo/raw/doc/logo.svg"
52
(Some (Raw_at ("myrepo", "doc/logo.svg")));
53
check_dispatch "encoded segment decodes" "myrepo/file/a%20b.txt"
54
(Some (File_at ("myrepo", "a b.txt")))
55
56
let test_nested_repo () =
57
check_dispatch "nested summary" "sub/dir/repo/summary/"
58
(Some (Repo "sub/dir/repo"));
59
check_dispatch "nested commit" "sub/repo/commit/abc"
60
(Some (Commit ("sub/repo", "abc")));
61
check_dispatch "deeply nested" "a/b/c/repo/files/"
62
(Some (Files "a/b/c/repo"))
63
64
(* A path without a reserved segment is reported as [Project_dir]; the handler
65
decides whether the filesystem holds a repository or a directory there. *)
66
let test_implicit_summary () =
67
check_dispatch "bare repo path" "myrepo" (Some (Project_dir "myrepo"));
68
check_dispatch "bare with trailing slash" "myrepo/"
69
(Some (Project_dir "myrepo"))
70
71
let test_root () = check_dispatch "empty path is root" "" (Some Root)
72
73
let test_malformed () =
74
check_dispatch "commit without hash" "myrepo/commit/" None;
75
check_dispatch "file without hash" "myrepo/file/" None;
76
check_dispatch "raw without hash" "myrepo/raw/" None;
77
check_dispatch "action without repo" "summary/" None;
78
check_dispatch "trailing junk after summary" "myrepo/summary/junk" None;
79
check_dispatch "trailing junk after files" "myrepo/files/extra" None;
80
check_dispatch "trailing junk after hash" "myrepo/commit/abc/def" None
81
82
(* The documented contract: dispatch inverts path_of for every route shape,
83
including file paths that need percent-encoding. *)
84
let test_round_trip () =
85
let samples =
86
Ogit.Routes.
87
[
88
Root;
89
Project_dir "dir";
90
Project_dir "nested/dir";
91
Repo "repo";
92
Repo "sub/repo";
93
Commits "repo";
94
Commits_branch ("repo", "main");
95
Commit ("repo", "abc123");
96
Files "repo";
97
File ("repo", String.make 40 'a');
98
File_at ("repo", "src/main.ml");
99
File_at ("repo", "dir with space/na%me.txt");
100
Raw_file ("repo", String.make 40 'b');
101
Raw_at ("repo", "doc/logo.svg");
102
]
103
in
104
List.iter
105
(fun route ->
106
let path = Ogit.Routes.path_of route in
107
let stripped = String.sub path 1 (String.length path - 1) in
108
Alcotest.check route_testable
109
(Printf.sprintf "round trip %s" (route_to_string route))
110
(Some route)
111
(Ogit.Routes.dispatch stripped))
112
samples
113
114
let suite =
115
( "dispatch",
116
[
117
Alcotest.test_case "basic actions" `Quick test_basic_actions;
118
Alcotest.test_case "parametric actions" `Quick test_parametric_actions;
119
Alcotest.test_case "path actions" `Quick test_path_actions;
120
Alcotest.test_case "nested repo" `Quick test_nested_repo;
121
Alcotest.test_case "implicit summary" `Quick test_implicit_summary;
122
Alcotest.test_case "root" `Quick test_root;
123
Alcotest.test_case "malformed URLs" `Quick test_malformed;
124
Alcotest.test_case "round trip" `Quick test_round_trip;
125
] )
126