View raw

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