objective categorical abstract machine language personal data server
at main 377 lines 13 kB view raw
1open Alcotest 2open Lwt.Syntax 3open Test_support 4 5let run_lwt f = Lwt_main.run (f ()) 6 7(* helpers *) 8let test_string = testable Fmt.string String.equal 9 10let test_bytes = 11 testable 12 (Fmt.of_to_string (fun b -> String.sub (Bytes.to_string b) 0 10)) 13 Bytes.equal 14 15(** query tests *) 16 17let test_query_success () = 18 run_lwt 19 @@ fun () -> 20 let response = 21 Mock_http.json_response 22 (`Assoc 23 [("did", `String "did:plc:123"); ("handle", `String "test.bsky.social")] 24 ) 25 in 26 let* result, requests = 27 Test_utils.with_mock_responses [response] (fun (module C) client -> 28 C.query client "com.atproto.identity.resolveHandle" 29 (`Assoc [("handle", `String "test.bsky.social")]) 30 (fun json -> 31 let open Yojson.Safe.Util in 32 Ok (json |> member "did" |> to_string) ) ) 33 in 34 check test_string "result" "did:plc:123" result ; 35 check int "request count" 1 (List.length requests) ; 36 let req = List.hd requests in 37 Test_utils.assert_request_path "/xrpc/com.atproto.identity.resolveHandle" req ; 38 Test_utils.assert_request_method `GET req ; 39 Test_utils.assert_request_query_param "handle" "test.bsky.social" req ; 40 Lwt.return_unit 41 42let test_query_with_multiple_params () = 43 run_lwt 44 @@ fun () -> 45 let response = Mock_http.json_response (`Assoc [("followers", `List [])]) in 46 let* _, requests = 47 Test_utils.with_mock_responses [response] (fun (module C) client -> 48 C.query client "app.bsky.graph.getFollowers" 49 (`Assoc 50 [ ("actor", `String "did:plc:123") 51 ; ("limit", `Int 50) 52 ; ("cursor", `String "abc123") ] ) 53 (fun _ -> Ok ()) ) 54 in 55 let req = List.hd requests in 56 Test_utils.assert_request_query_param "actor" "did:plc:123" req ; 57 Test_utils.assert_request_query_param "limit" "50" req ; 58 Test_utils.assert_request_query_param "cursor" "abc123" req ; 59 Lwt.return_unit 60 61let test_query_error_response () = 62 run_lwt 63 @@ fun () -> 64 let response = 65 Mock_http.error_response ~status:`Bad_request ~error:"InvalidHandle" 66 ~message:"Handle not found" () 67 in 68 let* () = 69 Test_utils.with_mock_responses [response] (fun (module C) client -> 70 Lwt.catch 71 (fun () -> 72 let* _ = 73 C.query client "com.atproto.identity.resolveHandle" 74 (`Assoc [("handle", `String "invalid")]) 75 (fun _ -> Ok ()) 76 in 77 fail "should have raised Xrpc_error" ) 78 (function 79 | Hermes.Xrpc_error {status; error; message} -> 80 check int "status" 400 status ; 81 check test_string "error" "InvalidHandle" error ; 82 check (option test_string) "message" (Some "Handle not found") 83 message ; 84 Lwt.return_unit 85 | e -> 86 Lwt.reraise e ) ) 87 |> Lwt.map fst 88 in 89 Lwt.return_unit 90 91let test_query_empty_response () = 92 run_lwt 93 @@ fun () -> 94 let response = Mock_http.empty_response () in 95 let* result, _ = 96 Test_utils.with_mock_responses [response] (fun (module C) client -> 97 C.query client "some.endpoint" (`Assoc []) (fun _ -> Ok "empty") ) 98 in 99 check test_string "result" "empty" result ; 100 Lwt.return_unit 101 102let test_query_bytes () = 103 run_lwt 104 @@ fun () -> 105 let response = 106 Mock_http.bytes_response ~content_type:"image/jpeg" "fake-image-data" 107 in 108 let* (data, content_type), requests = 109 Test_utils.with_mock_responses [response] (fun (module C) client -> 110 C.query_bytes client "com.atproto.sync.getBlob" 111 (`Assoc [("did", `String "did:plc:123"); ("cid", `String "bafyabc")]) ) 112 in 113 check test_bytes "data" (Bytes.of_string "fake-image-data") data ; 114 check test_string "content_type" "image/jpeg" content_type ; 115 let req = List.hd requests in 116 Test_utils.assert_request_has_header "accept" "*/*" req ; 117 Lwt.return_unit 118 119(** procedure tests *) 120 121let test_procedure_success () = 122 run_lwt 123 @@ fun () -> 124 let response = 125 Mock_http.json_response 126 (`Assoc [("uri", `String "at://did:plc:123/app.bsky.feed.post/abc")]) 127 in 128 let* result, requests = 129 Test_utils.with_mock_responses [response] (fun (module C) client -> 130 C.procedure client "com.atproto.repo.createRecord" (`Assoc []) 131 (Some 132 (`Assoc 133 [ ("repo", `String "did:plc:123") 134 ; ("collection", `String "app.bsky.feed.post") 135 ; ( "record" 136 , `Assoc [("text", `String "This post was sent from PDSls")] 137 ) ] ) ) 138 (fun json -> 139 let open Yojson.Safe.Util in 140 Ok (json |> member "uri" |> to_string) ) ) 141 in 142 check test_string "uri" "at://did:plc:123/app.bsky.feed.post/abc" result ; 143 let req = List.hd requests in 144 Test_utils.assert_request_method `POST req ; 145 Test_utils.assert_request_path "/xrpc/com.atproto.repo.createRecord" req ; 146 Test_utils.assert_request_has_header "content-type" "application/json" req ; 147 Test_utils.assert_request_body_contains "This post was sent from PDSls" req ; 148 Lwt.return_unit 149 150let test_procedure_no_input () = 151 run_lwt 152 @@ fun () -> 153 let response = Mock_http.empty_response () in 154 let* _, requests = 155 Test_utils.with_mock_responses [response] (fun (module C) client -> 156 C.procedure client "com.atproto.server.deleteSession" (`Assoc []) None 157 (fun _ -> Ok () ) ) 158 in 159 let req = List.hd requests in 160 Test_utils.assert_request_method `POST req ; 161 check (option test_string) "body" (Some "") req.body ; 162 Lwt.return_unit 163 164let test_procedure_bytes () = 165 run_lwt 166 @@ fun () -> 167 let response = Mock_http.empty_response () in 168 let* result, requests = 169 Test_utils.with_mock_responses [response] (fun (module C) client -> 170 C.procedure_bytes client "com.atproto.repo.importRepo" (`Assoc []) 171 (Some (Bytes.of_string "fake-car-data")) 172 ~content_type:"application/vnd.ipld.car" ) 173 in 174 check (option (pair test_bytes test_string)) "result" None result ; 175 let req = List.hd requests in 176 Test_utils.assert_request_has_header "content-type" "application/vnd.ipld.car" 177 req ; 178 Test_utils.assert_request_has_header "accept" "*/*" req ; 179 check (option test_string) "body" (Some "fake-car-data") req.body ; 180 Lwt.return_unit 181 182let test_procedure_blob () = 183 run_lwt 184 @@ fun () -> 185 let response = 186 Mock_http.json_response 187 (`Assoc 188 [ ( "blob" 189 , `Assoc 190 [ ("$type", `String "blob") 191 ; ("ref", `Assoc [("$link", `String "bafyabc")]) 192 ; ("mimeType", `String "image/jpeg") 193 ; ("size", `Int 1234) ] ) ] ) 194 in 195 let* result, requests = 196 Test_utils.with_mock_responses [response] (fun (module C) client -> 197 C.procedure_blob client "com.atproto.repo.uploadBlob" (`Assoc []) 198 (Bytes.of_string "fake-image-bytes") ~content_type:"image/jpeg" 199 (fun json -> 200 let open Yojson.Safe.Util in 201 Ok (json |> member "blob" |> member "mimeType" |> to_string) ) ) 202 in 203 check test_string "mimeType" "image/jpeg" result ; 204 let req = List.hd requests in 205 Test_utils.assert_request_has_header "content-type" "image/jpeg" req ; 206 check (option test_string) "body" (Some "fake-image-bytes") req.body ; 207 Lwt.return_unit 208 209(** authentication tests *) 210 211let test_auth_header_added () = 212 run_lwt 213 @@ fun () -> 214 let response = Mock_http.json_response (`Assoc []) in 215 let* _, requests = 216 Test_utils.with_mock_responses [response] (fun (module C) client -> 217 let session = Test_utils.make_test_session () in 218 C.set_session client session ; 219 C.query client "some.endpoint" (`Assoc []) (fun _ -> Ok ()) ) 220 in 221 let req = List.hd requests in 222 Test_utils.assert_request_has_auth_header req ; 223 Lwt.return_unit 224 225let test_session_can_be_cleared () = 226 run_lwt 227 @@ fun () -> 228 let response = Mock_http.json_response (`Assoc []) in 229 let* _, requests = 230 Test_utils.with_mock_responses [response] (fun (module C) client -> 231 let session = Test_utils.make_test_session () in 232 C.set_session client session ; 233 C.clear_session client ; 234 C.query client "some.endpoint" (`Assoc []) (fun _ -> Ok ()) ) 235 in 236 let req = List.hd requests in 237 let has_auth = Cohttp.Header.get req.headers "authorization" in 238 check (option test_string) "no auth header" None has_auth ; 239 Lwt.return_unit 240 241(** error handling tests *) 242 243let test_401_unauthorized () = 244 run_lwt 245 @@ fun () -> 246 let response = 247 Mock_http.error_response ~status:`Unauthorized ~error:"AuthRequired" 248 ~message:"Authentication required" () 249 in 250 let* () = 251 Test_utils.with_mock_responses [response] (fun (module C) client -> 252 Lwt.catch 253 (fun () -> 254 let* _ = 255 C.query client "some.protected.endpoint" (`Assoc []) (fun _ -> 256 Ok () ) 257 in 258 fail "should have raised" ) 259 (function 260 | Hermes.Xrpc_error {status= 401; error= "AuthRequired"; _} -> 261 Lwt.return_unit 262 | e -> 263 Lwt.reraise e ) ) 264 |> Lwt.map fst 265 in 266 Lwt.return_unit 267 268let test_500_server_error () = 269 run_lwt 270 @@ fun () -> 271 let response = 272 Mock_http.error_response ~status:`Internal_server_error 273 ~error:"InternalServerError" () 274 in 275 let* () = 276 Test_utils.with_mock_responses [response] (fun (module C) client -> 277 Lwt.catch 278 (fun () -> 279 let* _ = 280 C.query client "some.endpoint" (`Assoc []) (fun _ -> Ok ()) 281 in 282 fail "Should have raised" ) 283 (function 284 | Hermes.Xrpc_error {status= 500; _} -> 285 Lwt.return_unit 286 | e -> 287 Lwt.reraise e ) ) 288 |> Lwt.map fst 289 in 290 Lwt.return_unit 291 292let test_malformed_error_response () = 293 run_lwt 294 @@ fun () -> 295 let response = 296 { Mock_http.status= `Bad_request 297 ; headers= [("content-type", "application/json")] 298 ; body= "not valid json" } 299 in 300 let* () = 301 Test_utils.with_mock_responses [response] (fun (module C) client -> 302 Lwt.catch 303 (fun () -> 304 let* _ = 305 C.query client "some.endpoint" (`Assoc []) (fun _ -> Ok ()) 306 in 307 fail "should have raised" ) 308 (function 309 | Hermes.Xrpc_error {status= 400; error= "UnknownError"; _} -> 310 Lwt.return_unit 311 | e -> 312 Lwt.reraise e ) ) 313 |> Lwt.map fst 314 in 315 Lwt.return_unit 316 317(** client creation tests *) 318 319let test_make_client () = 320 let client = Hermes.make_client ~service:"https://api.bsky.app" () in 321 let service = Hermes.get_service client in 322 check (option test_string) "host" (Some "api.bsky.app") (Uri.host service) 323 324let test_client_service_urls () = 325 let urls = 326 [ "https://bsky.social" 327 ; "https://api.bsky.app" 328 ; "http://localhost:3000" 329 ; "https://pds.example.com:8080" ] 330 in 331 List.iter 332 (fun url -> 333 let client = Hermes.make_client ~service:url () in 334 let service = Hermes.get_service client in 335 check bool "service set" true (String.length (Uri.to_string service) > 0) ) 336 urls 337 338let test_get_session_unauthenticated () = 339 let client = Hermes.make_client ~service:"https://example.com" () in 340 check (option reject) "no session" None (Hermes.get_session client) 341 342(** tests *) 343 344let query_tests = 345 [ ("query success", `Quick, test_query_success) 346 ; ("query with multiple params", `Quick, test_query_with_multiple_params) 347 ; ("query error response", `Quick, test_query_error_response) 348 ; ("query empty response", `Quick, test_query_empty_response) 349 ; ("query bytes", `Quick, test_query_bytes) ] 350 351let procedure_tests = 352 [ ("procedure success", `Quick, test_procedure_success) 353 ; ("procedure no input", `Quick, test_procedure_no_input) 354 ; ("procedure bytes", `Quick, test_procedure_bytes) 355 ; ("procedure blob", `Quick, test_procedure_blob) ] 356 357let auth_tests = 358 [ ("auth header added", `Quick, test_auth_header_added) 359 ; ("session can be cleared", `Quick, test_session_can_be_cleared) ] 360 361let error_tests = 362 [ ("401 unauthorized", `Quick, test_401_unauthorized) 363 ; ("500 server error", `Quick, test_500_server_error) 364 ; ("malformed error response", `Quick, test_malformed_error_response) ] 365 366let creation_tests = 367 [ ("make_client", `Quick, test_make_client) 368 ; ("service URLs", `Quick, test_client_service_urls) 369 ; ("get_session unauthenticated", `Quick, test_get_session_unauthenticated) ] 370 371let () = 372 run "Client" 373 [ ("query", query_tests) 374 ; ("procedure", procedure_tests) 375 ; ("auth", auth_tests) 376 ; ("errors", error_tests) 377 ; ("creation", creation_tests) ]