[OCaml] Mobile-friendly clone of cgit.
1
(** Router error handling: that invalid names, hashes and missing objects
2
produce the intended failure category. *)
3
4
open Test_helpers
5
6
let test_invalid_repo_name () =
7
with_temp_directory "ogit-router" (fun root ->
8
let config = Ogit.Config.{ default with git_project_root = root } in
9
let request = Dream.test (Dream.router (Ogit.Handlers.routes config)) in
10
let status =
11
Dream.request ~target:"/.hidden/summary/" "" |> request |> Dream.status
12
in
13
Alcotest.(check int) "400" 400 (Dream.status_to_int status))
14
15
let test_missing_repo () =
16
with_temp_directory "ogit-router" (fun root ->
17
let config = Ogit.Config.{ default with git_project_root = root } in
18
let request = Dream.test (Dream.router (Ogit.Handlers.routes config)) in
19
let status =
20
Dream.request ~target:"/missing/summary/" "" |> request |> Dream.status
21
in
22
Alcotest.(check int) "404" 404 (Dream.status_to_int status))
23
24
let test_invalid_hash () =
25
with_temp_directory "ogit-router" (fun root ->
26
let name = "project" in
27
let path = Filename.concat root name in
28
Unix.mkdir path 0o755;
29
ignore (git [ "-C"; path; "init"; "-q"; "-b"; "main" ]);
30
ignore (git [ "-C"; path; "config"; "user.name"; "Test" ]);
31
ignore (git [ "-C"; path; "config"; "user.email"; "t@t.invalid" ]);
32
Out_channel.with_open_text (Filename.concat path "f.txt") (fun ch ->
33
output_string ch "x\n");
34
ignore (git [ "-C"; path; "add"; "." ]);
35
ignore (git [ "-C"; path; "commit"; "-q"; "-m"; "init" ]);
36
let config = Ogit.Config.{ default with git_project_root = root } in
37
let request = Dream.test (Dream.router (Ogit.Handlers.routes config)) in
38
let status =
39
Dream.request ~target:"/project/commit/not-a-hash" ""
40
|> request |> Dream.status
41
in
42
Alcotest.(check int) "400" 400 (Dream.status_to_int status))
43
44
let test_missing_object () =
45
with_temp_directory "ogit-router" (fun root ->
46
let name = "project" in
47
let path = Filename.concat root name in
48
Unix.mkdir path 0o755;
49
ignore (git [ "-C"; path; "init"; "-q"; "-b"; "main" ]);
50
ignore (git [ "-C"; path; "config"; "user.name"; "Test" ]);
51
ignore (git [ "-C"; path; "config"; "user.email"; "t@t.invalid" ]);
52
Out_channel.with_open_text (Filename.concat path "f.txt") (fun ch ->
53
output_string ch "x\n");
54
ignore (git [ "-C"; path; "add"; "." ]);
55
ignore (git [ "-C"; path; "commit"; "-q"; "-m"; "init" ]);
56
let config = Ogit.Config.{ default with git_project_root = root } in
57
let request = Dream.test (Dream.router (Ogit.Handlers.routes config)) in
58
let target = "/project/commit/" ^ String.make 40 'a' in
59
let status = Dream.request ~target "" |> request |> Dream.status in
60
Alcotest.(check int) "404" 404 (Dream.status_to_int status))
61
62
(* Raw responses must never let repository content run with the site's
63
authority: the sandbox policy and nosniff header are the contract. An SVG is
64
used because it is the content type that made the headers necessary. *)
65
let test_raw_response_headers () =
66
with_temp_directory "ogit-router" (fun root ->
67
let name = "project" in
68
let path = Filename.concat root name in
69
Unix.mkdir path 0o755;
70
ignore (git [ "-C"; path; "init"; "-q"; "-b"; "main" ]);
71
ignore (git [ "-C"; path; "config"; "user.name"; "Test" ]);
72
ignore (git [ "-C"; path; "config"; "user.email"; "t@t.invalid" ]);
73
Out_channel.with_open_text (Filename.concat path "img.svg") (fun ch ->
74
output_string ch "<svg xmlns=\"http://www.w3.org/2000/svg\"/>\n");
75
ignore (git [ "-C"; path; "add"; "." ]);
76
ignore (git [ "-C"; path; "commit"; "-q"; "-m"; "init" ]);
77
let blob_hash = git [ "-C"; path; "rev-parse"; "HEAD:img.svg" ] in
78
let config = Ogit.Config.{ default with git_project_root = root } in
79
let request = Dream.test (Dream.router (Ogit.Handlers.routes config)) in
80
let target = "/project/raw/" ^ blob_hash in
81
let response = Dream.request ~target "" |> request in
82
Alcotest.(check int)
83
"200" 200
84
(Dream.status_to_int (Dream.status response));
85
let header name =
86
Dream.header response name |> Option.value ~default:"<missing>"
87
in
88
Alcotest.(check string)
89
"content type" "image/svg+xml" (header "Content-Type");
90
Alcotest.(check string)
91
"sandboxed" "sandbox"
92
(header "Content-Security-Policy");
93
Alcotest.(check string)
94
"nosniff" "nosniff"
95
(header "X-Content-Type-Options"))
96
97
(* Files are addressable by their path from the repository root, for both the
98
rendered page and the raw content. *)
99
let test_file_by_path () =
100
with_temp_directory "ogit-router" (fun root ->
101
let name = "project" in
102
let path = Filename.concat root name in
103
Unix.mkdir path 0o755;
104
ignore (git [ "-C"; path; "init"; "-q"; "-b"; "main" ]);
105
ignore (git [ "-C"; path; "config"; "user.name"; "Test" ]);
106
ignore (git [ "-C"; path; "config"; "user.email"; "t@t.invalid" ]);
107
let dir = Filename.concat path "dir" in
108
Unix.mkdir dir 0o755;
109
Out_channel.with_open_text (Filename.concat dir "nested.txt") (fun ch ->
110
output_string ch "nested content\n");
111
ignore (git [ "-C"; path; "add"; "." ]);
112
ignore (git [ "-C"; path; "commit"; "-q"; "-m"; "init" ]);
113
let config = Ogit.Config.{ default with git_project_root = root } in
114
let request = Dream.test (Dream.router (Ogit.Handlers.routes config)) in
115
let status target =
116
Dream.request ~target "" |> request |> Dream.status
117
|> Dream.status_to_int
118
in
119
Alcotest.(check int) "blob page" 200 (status "/project/file/dir/nested.txt");
120
Alcotest.(check int) "tree page" 200 (status "/project/file/dir");
121
Alcotest.(check int) "raw blob" 200 (status "/project/raw/dir/nested.txt");
122
Alcotest.(check int) "missing path" 404 (status "/project/file/dir/none");
123
Alcotest.(check int) "raw tree" 400 (status "/project/raw/dir"))
124
125
(* The page cache is keyed on the repository's head hash: a new commit must be
126
visible immediately, because it changes the key rather than the entry. *)
127
let test_cache_sees_new_commit () =
128
with_temp_directory "ogit-router" (fun root ->
129
let name = "project" in
130
let path = Filename.concat root name in
131
Unix.mkdir path 0o755;
132
ignore (git [ "-C"; path; "init"; "-q"; "-b"; "main" ]);
133
ignore (git [ "-C"; path; "config"; "user.name"; "Test" ]);
134
ignore (git [ "-C"; path; "config"; "user.email"; "t@t.invalid" ]);
135
let write content =
136
Out_channel.with_open_text (Filename.concat path "f.txt") (fun ch ->
137
output_string ch content)
138
in
139
let commit message =
140
ignore (git [ "-C"; path; "add"; "." ]);
141
ignore (git [ "-C"; path; "commit"; "-q"; "-m"; message ])
142
in
143
write "generation-one\n";
144
commit "first";
145
let config = Ogit.Config.{ default with git_project_root = root } in
146
let request = Dream.test (Dream.router (Ogit.Handlers.routes config)) in
147
let body target =
148
Dream.request ~target "" |> request |> Dream.body |> Lwt_main.run
149
in
150
let contains haystack needle =
151
let nl = String.length needle and hl = String.length haystack in
152
let rec at i = i + nl <= hl && (String.sub haystack i nl = needle || at (i + 1)) in
153
at 0
154
in
155
(* Twice, so the second read comes from the cache. *)
156
Alcotest.(check bool)
157
"first read" true
158
(contains (body "/project/raw/f.txt") "generation-one");
159
Alcotest.(check bool)
160
"cached read" true
161
(contains (body "/project/raw/f.txt") "generation-one");
162
write "generation-two\n";
163
commit "second";
164
Alcotest.(check bool)
165
"new commit visible" true
166
(contains (body "/project/raw/f.txt") "generation-two"))
167
168
let suite =
169
( "router",
170
[
171
Alcotest.test_case "invalid repo name" `Slow test_invalid_repo_name;
172
Alcotest.test_case "missing repo" `Slow test_missing_repo;
173
Alcotest.test_case "invalid hash" `Slow test_invalid_hash;
174
Alcotest.test_case "missing object" `Slow test_missing_object;
175
Alcotest.test_case "raw response headers" `Slow test_raw_response_headers;
176
Alcotest.test_case "file by path" `Slow test_file_by_path;
177
Alcotest.test_case "cache sees new commit" `Slow
178
test_cache_sees_new_commit;
179
] )
180