atproto libraries implementation in ocaml
at main 22 kB view raw
1(** Identity tests for AT Protocol. 2 3 Tests the DID resolver module with mock HTTP responses. *) 4 5open Atproto_identity 6 7(** {1 Mock HTTP Handler} *) 8 9(** Global mock handler for HTTP GET *) 10let mock_http_handler : (Uri.t -> Did_resolver.http_response) ref = 11 ref (fun _ -> Did_resolver.{ status = 500; body = "No mock configured" }) 12 13(** Effect handler *) 14let http_effect_handler : type a. 15 a Effect.t -> ((a, _) Effect.Deep.continuation -> _) option = function 16 | Did_resolver.Http_get uri -> 17 Some (fun k -> Effect.Deep.continue k (!mock_http_handler uri)) 18 | _ -> None 19 20(** Run with mock HTTP *) 21let run_with_mock_http ~handler f = 22 mock_http_handler := handler; 23 Effect.Deep.match_with f () 24 { retc = (fun x -> x); exnc = raise; effc = http_effect_handler } 25 26(** {1 Sample DID Documents} *) 27 28let sample_plc_doc = 29 {|{ 30 "id": "did:plc:ewvi7nxzy7mbhbzdkr36ha", 31 "alsoKnownAs": ["at://jay.bsky.social"], 32 "verificationMethod": [ 33 { 34 "id": "did:plc:ewvi7nxzy7mbhbzdkr36ha#atproto", 35 "type": "Multikey", 36 "controller": "did:plc:ewvi7nxzy7mbhbzdkr36ha", 37 "publicKeyMultibase": "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 38 } 39 ], 40 "service": [ 41 { 42 "id": "#atproto_pds", 43 "type": "AtprotoPersonalDataServer", 44 "serviceEndpoint": "https://bsky.social" 45 } 46 ] 47}|} 48 49let sample_web_doc = 50 {|{ 51 "id": "did:web:example.com", 52 "alsoKnownAs": ["at://example.com"], 53 "verificationMethod": [ 54 { 55 "id": "did:web:example.com#atproto", 56 "type": "Multikey", 57 "controller": "did:web:example.com", 58 "publicKeyMultibase": "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 59 } 60 ], 61 "service": [ 62 { 63 "id": "#atproto_pds", 64 "type": "AtprotoPersonalDataServer", 65 "serviceEndpoint": "https://pds.example.com" 66 } 67 ] 68}|} 69 70(** {1 Tests} *) 71 72let test_resolve_plc () = 73 let handler uri = 74 let path = Uri.path uri in 75 if path = "/did:plc:ewvi7nxzy7mbhbzdkr36ha" then 76 Did_resolver.{ status = 200; body = sample_plc_doc } 77 else Did_resolver.{ status = 404; body = "Not found" } 78 in 79 run_with_mock_http ~handler (fun () -> 80 match Did_resolver.resolve "did:plc:ewvi7nxzy7mbhbzdkr36ha" with 81 | Ok doc -> 82 Alcotest.(check string) "id" "did:plc:ewvi7nxzy7mbhbzdkr36ha" doc.id; 83 Alcotest.(check bool) 84 "has alsoKnownAs" true 85 (List.length doc.also_known_as > 0); 86 Alcotest.(check bool) 87 "has verification methods" true 88 (List.length doc.verification_method > 0); 89 Alcotest.(check bool) "has services" true (List.length doc.service > 0) 90 | Error e -> Alcotest.fail (Did_resolver.error_to_string e)) 91 92let test_resolve_web () = 93 let handler uri = 94 let host = Uri.host uri |> Option.value ~default:"" in 95 let path = Uri.path uri in 96 if host = "example.com" && path = "/.well-known/did.json" then 97 Did_resolver.{ status = 200; body = sample_web_doc } 98 else Did_resolver.{ status = 404; body = "Not found" } 99 in 100 run_with_mock_http ~handler (fun () -> 101 match Did_resolver.resolve "did:web:example.com" with 102 | Ok doc -> 103 Alcotest.(check string) "id" "did:web:example.com" doc.id; 104 Alcotest.(check bool) 105 "has alsoKnownAs" true 106 (List.length doc.also_known_as > 0) 107 | Error e -> Alcotest.fail (Did_resolver.error_to_string e)) 108 109let test_get_handle () = 110 let handler _uri = Did_resolver.{ status = 200; body = sample_plc_doc } in 111 run_with_mock_http ~handler (fun () -> 112 match Did_resolver.resolve "did:plc:ewvi7nxzy7mbhbzdkr36ha" with 113 | Ok doc -> ( 114 match Did_resolver.get_handle doc with 115 | Some handle -> 116 Alcotest.(check string) 117 "handle" "jay.bsky.social" 118 (Atproto_syntax.Handle.to_string handle) 119 | None -> Alcotest.fail "expected handle") 120 | Error e -> Alcotest.fail (Did_resolver.error_to_string e)) 121 122let test_get_pds_endpoint () = 123 let handler _uri = Did_resolver.{ status = 200; body = sample_plc_doc } in 124 run_with_mock_http ~handler (fun () -> 125 match Did_resolver.resolve "did:plc:ewvi7nxzy7mbhbzdkr36ha" with 126 | Ok doc -> ( 127 match Did_resolver.get_pds_endpoint doc with 128 | Some pds -> 129 Alcotest.(check string) 130 "pds" "https://bsky.social" (Uri.to_string pds) 131 | None -> Alcotest.fail "expected PDS endpoint") 132 | Error e -> Alcotest.fail (Did_resolver.error_to_string e)) 133 134let test_get_signing_key () = 135 let handler _uri = Did_resolver.{ status = 200; body = sample_plc_doc } in 136 run_with_mock_http ~handler (fun () -> 137 match Did_resolver.resolve "did:plc:ewvi7nxzy7mbhbzdkr36ha" with 138 | Ok doc -> ( 139 match Did_resolver.get_signing_key doc with 140 | Some key -> 141 Alcotest.(check bool) 142 "key starts with z" true 143 (String.length key > 0 && key.[0] = 'z') 144 | None -> Alcotest.fail "expected signing key") 145 | Error e -> Alcotest.fail (Did_resolver.error_to_string e)) 146 147let test_not_found () = 148 let handler _uri = Did_resolver.{ status = 404; body = "Not found" } in 149 run_with_mock_http ~handler (fun () -> 150 match Did_resolver.resolve "did:plc:notfound" with 151 | Error Did_resolver.Not_found -> () 152 | Error e -> 153 Alcotest.fail 154 (Printf.sprintf "expected Not_found, got %s" 155 (Did_resolver.error_to_string e)) 156 | Ok _ -> Alcotest.fail "expected error") 157 158let test_http_error () = 159 let handler _uri = 160 Did_resolver.{ status = 500; body = "Internal Server Error" } 161 in 162 run_with_mock_http ~handler (fun () -> 163 match Did_resolver.resolve "did:plc:test" with 164 | Error (Did_resolver.Http_error (500, _)) -> () 165 | Error e -> 166 Alcotest.fail 167 (Printf.sprintf "expected Http_error 500, got %s" 168 (Did_resolver.error_to_string e)) 169 | Ok _ -> Alcotest.fail "expected error") 170 171let test_invalid_did () = 172 let handler _uri = Did_resolver.{ status = 200; body = sample_plc_doc } in 173 run_with_mock_http ~handler (fun () -> 174 match Did_resolver.resolve "invalid" with 175 | Error (Did_resolver.Invalid_did _) -> () 176 | Error e -> 177 Alcotest.fail 178 (Printf.sprintf "expected Invalid_did, got %s" 179 (Did_resolver.error_to_string e)) 180 | Ok _ -> Alcotest.fail "expected error") 181 182let test_unsupported_method () = 183 let handler _uri = Did_resolver.{ status = 200; body = sample_plc_doc } in 184 run_with_mock_http ~handler (fun () -> 185 match Did_resolver.resolve "did:key:z123" with 186 | Error (Did_resolver.Unsupported_method _) -> () 187 | Error e -> 188 Alcotest.fail 189 (Printf.sprintf "expected Unsupported_method, got %s" 190 (Did_resolver.error_to_string e)) 191 | Ok _ -> Alcotest.fail "expected error") 192 193(** {1 Handle Resolution Tests} *) 194 195(** Mock DNS handler *) 196let mock_dns_handler : (string -> Handle_resolver.dns_result) ref = 197 ref (fun _ -> Handle_resolver.Dns_not_found) 198 199(** Mock HTTP handler for handle resolution *) 200let mock_handle_http_handler : (Uri.t -> Handle_resolver.http_response) ref = 201 ref (fun _ -> Handle_resolver.{ status = 500; body = "No mock" }) 202 203(** Combined effect handler for handle resolution *) 204let handle_effect_handler : type a. 205 a Effect.t -> ((a, _) Effect.Deep.continuation -> _) option = function 206 | Handle_resolver.Dns_txt domain -> 207 Some (fun k -> Effect.Deep.continue k (!mock_dns_handler domain)) 208 | Handle_resolver.Http_get uri -> 209 Some (fun k -> Effect.Deep.continue k (!mock_handle_http_handler uri)) 210 | Did_resolver.Http_get uri -> 211 Some (fun k -> Effect.Deep.continue k (!mock_http_handler uri)) 212 | _ -> None 213 214(** Run with mock handlers for handle resolution *) 215let run_with_handle_mocks ~dns_handler ~http_handler f = 216 mock_dns_handler := dns_handler; 217 mock_handle_http_handler := http_handler; 218 Effect.Deep.match_with f () 219 { retc = (fun x -> x); exnc = raise; effc = handle_effect_handler } 220 221let test_handle_resolve_via_dns () = 222 let dns_handler domain = 223 if domain = "_atproto.alice.bsky.social" then 224 Handle_resolver.Dns_records [ "did=did:plc:alice123" ] 225 else Handle_resolver.Dns_not_found 226 in 227 let http_handler _uri = 228 Handle_resolver.{ status = 404; body = "Not found" } 229 in 230 run_with_handle_mocks ~dns_handler ~http_handler (fun () -> 231 match Handle_resolver.resolve_string "alice.bsky.social" with 232 | Ok did -> 233 Alcotest.(check string) 234 "did" "did:plc:alice123" 235 (Atproto_syntax.Did.to_string did) 236 | Error e -> Alcotest.fail (Handle_resolver.error_to_string e)) 237 238let test_handle_resolve_via_https () = 239 let dns_handler _domain = Handle_resolver.Dns_not_found in 240 let http_handler uri = 241 let host = Uri.host uri |> Option.value ~default:"" in 242 let path = Uri.path uri in 243 if host = "bob.example.com" && path = "/.well-known/atproto-did" then 244 Handle_resolver.{ status = 200; body = "did:web:bob.example.com" } 245 else Handle_resolver.{ status = 404; body = "Not found" } 246 in 247 run_with_handle_mocks ~dns_handler ~http_handler (fun () -> 248 match Handle_resolver.resolve_string "bob.example.com" with 249 | Ok did -> 250 Alcotest.(check string) 251 "did" "did:web:bob.example.com" 252 (Atproto_syntax.Did.to_string did) 253 | Error e -> Alcotest.fail (Handle_resolver.error_to_string e)) 254 255let test_handle_dns_priority () = 256 (* DNS should be tried first, even if HTTPS would work *) 257 let dns_handler domain = 258 if domain = "_atproto.test.example.com" then 259 Handle_resolver.Dns_records [ "did=did:plc:from-dns" ] 260 else Handle_resolver.Dns_not_found 261 in 262 let http_handler _uri = 263 Handle_resolver.{ status = 200; body = "did:plc:from-https" } 264 in 265 run_with_handle_mocks ~dns_handler ~http_handler (fun () -> 266 match Handle_resolver.resolve_string "test.example.com" with 267 | Ok did -> 268 Alcotest.(check string) 269 "prefers DNS" "did:plc:from-dns" 270 (Atproto_syntax.Did.to_string did) 271 | Error e -> Alcotest.fail (Handle_resolver.error_to_string e)) 272 273let test_handle_not_found () = 274 let dns_handler _domain = Handle_resolver.Dns_not_found in 275 let http_handler _uri = 276 Handle_resolver.{ status = 404; body = "Not found" } 277 in 278 run_with_handle_mocks ~dns_handler ~http_handler (fun () -> 279 match Handle_resolver.resolve_string "notfound.example.com" with 280 | Error Handle_resolver.No_did_record -> () 281 | Error e -> 282 Alcotest.fail 283 (Printf.sprintf "expected No_did_record, got %s" 284 (Handle_resolver.error_to_string e)) 285 | Ok _ -> Alcotest.fail "expected error") 286 287let test_handle_invalid () = 288 let dns_handler _domain = Handle_resolver.Dns_not_found in 289 let http_handler _uri = Handle_resolver.{ status = 404; body = "" } in 290 run_with_handle_mocks ~dns_handler ~http_handler (fun () -> 291 match Handle_resolver.resolve_string "invalid" with 292 | Error (Handle_resolver.Invalid_handle _) -> () 293 | Error e -> 294 Alcotest.fail 295 (Printf.sprintf "expected Invalid_handle, got %s" 296 (Handle_resolver.error_to_string e)) 297 | Ok _ -> Alcotest.fail "expected error") 298 299(** {1 Identity Verification Tests} *) 300 301(** Combined effect handler for identity verification *) 302let identity_effect_handler : type a. 303 a Effect.t -> ((a, _) Effect.Deep.continuation -> _) option = function 304 | Handle_resolver.Dns_txt domain -> 305 Some (fun k -> Effect.Deep.continue k (!mock_dns_handler domain)) 306 | Handle_resolver.Http_get uri -> 307 Some (fun k -> Effect.Deep.continue k (!mock_handle_http_handler uri)) 308 | Did_resolver.Http_get uri -> 309 Some (fun k -> Effect.Deep.continue k (!mock_http_handler uri)) 310 | _ -> None 311 312let run_with_identity_mocks ~did_handler ~dns_handler ~http_handler f = 313 mock_http_handler := did_handler; 314 mock_dns_handler := dns_handler; 315 mock_handle_http_handler := http_handler; 316 Effect.Deep.match_with f () 317 { retc = (fun x -> x); exnc = raise; effc = identity_effect_handler } 318 319let test_verify_did_success () = 320 (* Setup: DID doc has handle, handle resolves back to DID *) 321 let did_handler uri = 322 let path = Uri.path uri in 323 if path = "/did:plc:test123" then 324 Did_resolver. 325 { 326 status = 200; 327 body = 328 {|{ 329 "id": "did:plc:test123", 330 "alsoKnownAs": ["at://alice.example.com"], 331 "verificationMethod": [ 332 {"id": "#key", "type": "Multikey", "controller": "did:plc:test123", "publicKeyMultibase": "zTest123"} 333 ], 334 "service": [ 335 {"id": "#pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": "https://pds.example.com"} 336 ] 337 }|}; 338 } 339 else Did_resolver.{ status = 404; body = "Not found" } 340 in 341 let dns_handler domain = 342 if domain = "_atproto.alice.example.com" then 343 Handle_resolver.Dns_records [ "did=did:plc:test123" ] 344 else Handle_resolver.Dns_not_found 345 in 346 let http_handler _uri = Handle_resolver.{ status = 404; body = "" } in 347 run_with_identity_mocks ~did_handler ~dns_handler ~http_handler (fun () -> 348 let did = Atproto_syntax.Did.of_string_exn "did:plc:test123" in 349 match Identity.verify_did did with 350 | Ok identity -> 351 Alcotest.(check string) 352 "did" "did:plc:test123" 353 (Atproto_syntax.Did.to_string identity.did); 354 Alcotest.(check string) 355 "handle" "alice.example.com" 356 (Atproto_syntax.Handle.to_string identity.handle); 357 Alcotest.(check bool) 358 "has signing key" true 359 (Option.is_some identity.signing_key); 360 Alcotest.(check bool) 361 "has pds" true 362 (Option.is_some identity.pds_endpoint) 363 | Error e -> Alcotest.fail (Identity.error_to_string e)) 364 365let test_verify_handle_success () = 366 let did_handler uri = 367 let path = Uri.path uri in 368 if path = "/did:plc:bob456" then 369 Did_resolver. 370 { 371 status = 200; 372 body = 373 {|{ 374 "id": "did:plc:bob456", 375 "alsoKnownAs": ["at://bob.example.com"], 376 "verificationMethod": [], 377 "service": [] 378 }|}; 379 } 380 else Did_resolver.{ status = 404; body = "Not found" } 381 in 382 let dns_handler domain = 383 if domain = "_atproto.bob.example.com" then 384 Handle_resolver.Dns_records [ "did=did:plc:bob456" ] 385 else Handle_resolver.Dns_not_found 386 in 387 let http_handler _uri = Handle_resolver.{ status = 404; body = "" } in 388 run_with_identity_mocks ~did_handler ~dns_handler ~http_handler (fun () -> 389 let handle = Atproto_syntax.Handle.of_string_exn "bob.example.com" in 390 match Identity.verify_handle handle with 391 | Ok identity -> 392 Alcotest.(check string) 393 "did" "did:plc:bob456" 394 (Atproto_syntax.Did.to_string identity.did); 395 Alcotest.(check string) 396 "handle" "bob.example.com" 397 (Atproto_syntax.Handle.to_string identity.handle) 398 | Error e -> Alcotest.fail (Identity.error_to_string e)) 399 400let test_verify_bidirectional_success () = 401 let did_handler uri = 402 let path = Uri.path uri in 403 if path = "/did:plc:carol789" then 404 Did_resolver. 405 { 406 status = 200; 407 body = 408 {|{ 409 "id": "did:plc:carol789", 410 "alsoKnownAs": ["at://carol.example.com"], 411 "verificationMethod": [], 412 "service": [] 413 }|}; 414 } 415 else Did_resolver.{ status = 404; body = "Not found" } 416 in 417 let dns_handler domain = 418 if domain = "_atproto.carol.example.com" then 419 Handle_resolver.Dns_records [ "did=did:plc:carol789" ] 420 else Handle_resolver.Dns_not_found 421 in 422 let http_handler _uri = Handle_resolver.{ status = 404; body = "" } in 423 run_with_identity_mocks ~did_handler ~dns_handler ~http_handler (fun () -> 424 let did = Atproto_syntax.Did.of_string_exn "did:plc:carol789" in 425 let handle = Atproto_syntax.Handle.of_string_exn "carol.example.com" in 426 match Identity.verify_bidirectional did handle with 427 | Ok identity -> 428 Alcotest.(check string) 429 "did" "did:plc:carol789" 430 (Atproto_syntax.Did.to_string identity.did) 431 | Error e -> Alcotest.fail (Identity.error_to_string e)) 432 433let test_verify_did_handle_mismatch () = 434 (* Handle in doc doesn't match what we expect *) 435 let did_handler uri = 436 let path = Uri.path uri in 437 if path = "/did:plc:mismatch" then 438 Did_resolver. 439 { 440 status = 200; 441 body = 442 {|{ 443 "id": "did:plc:mismatch", 444 "alsoKnownAs": ["at://wrong.example.com"], 445 "verificationMethod": [], 446 "service": [] 447 }|}; 448 } 449 else Did_resolver.{ status = 404; body = "Not found" } 450 in 451 let dns_handler domain = 452 if domain = "_atproto.wrong.example.com" then 453 (* Handle resolves to different DID *) 454 Handle_resolver.Dns_records [ "did=did:plc:different" ] 455 else Handle_resolver.Dns_not_found 456 in 457 let http_handler _uri = Handle_resolver.{ status = 404; body = "" } in 458 run_with_identity_mocks ~did_handler ~dns_handler ~http_handler (fun () -> 459 let did = Atproto_syntax.Did.of_string_exn "did:plc:mismatch" in 460 match Identity.verify_did did with 461 | Error (Identity.Did_mismatch _) -> () 462 | Error e -> 463 Alcotest.fail 464 (Printf.sprintf "expected Did_mismatch, got %s" 465 (Identity.error_to_string e)) 466 | Ok _ -> Alcotest.fail "expected error") 467 468let test_verify_no_handle_in_doc () = 469 let did_handler uri = 470 let path = Uri.path uri in 471 if path = "/did:plc:nohandle" then 472 Did_resolver. 473 { 474 status = 200; 475 body = 476 {|{ 477 "id": "did:plc:nohandle", 478 "alsoKnownAs": [], 479 "verificationMethod": [], 480 "service": [] 481 }|}; 482 } 483 else Did_resolver.{ status = 404; body = "Not found" } 484 in 485 let dns_handler _domain = Handle_resolver.Dns_not_found in 486 let http_handler _uri = Handle_resolver.{ status = 404; body = "" } in 487 run_with_identity_mocks ~did_handler ~dns_handler ~http_handler (fun () -> 488 let did = Atproto_syntax.Did.of_string_exn "did:plc:nohandle" in 489 match Identity.verify_did did with 490 | Error Identity.No_handle_in_document -> () 491 | Error e -> 492 Alcotest.fail 493 (Printf.sprintf "expected No_handle_in_document, got %s" 494 (Identity.error_to_string e)) 495 | Ok _ -> Alcotest.fail "expected error") 496 497let test_verify_did_resolution_failed () = 498 let did_handler _uri = Did_resolver.{ status = 404; body = "Not found" } in 499 let dns_handler _domain = Handle_resolver.Dns_not_found in 500 let http_handler _uri = Handle_resolver.{ status = 404; body = "" } in 501 run_with_identity_mocks ~did_handler ~dns_handler ~http_handler (fun () -> 502 let did = Atproto_syntax.Did.of_string_exn "did:plc:notfound" in 503 match Identity.verify_did did with 504 | Error (Identity.Did_resolution_failed _) -> () 505 | Error e -> 506 Alcotest.fail 507 (Printf.sprintf "expected Did_resolution_failed, got %s" 508 (Identity.error_to_string e)) 509 | Ok _ -> Alcotest.fail "expected error") 510 511let test_verify_handle_resolution_failed () = 512 let did_handler _uri = 513 Did_resolver. 514 { 515 status = 200; 516 body = 517 {|{ 518 "id": "did:plc:test", 519 "alsoKnownAs": ["at://test.example.com"], 520 "verificationMethod": [], 521 "service": [] 522 }|}; 523 } 524 in 525 let dns_handler _domain = Handle_resolver.Dns_not_found in 526 let http_handler _uri = Handle_resolver.{ status = 404; body = "" } in 527 run_with_identity_mocks ~did_handler ~dns_handler ~http_handler (fun () -> 528 let handle = Atproto_syntax.Handle.of_string_exn "notfound.example.com" in 529 match Identity.verify_handle handle with 530 | Error (Identity.Handle_resolution_failed _) -> () 531 | Error e -> 532 Alcotest.fail 533 (Printf.sprintf "expected Handle_resolution_failed, got %s" 534 (Identity.error_to_string e)) 535 | Ok _ -> Alcotest.fail "expected error") 536 537(** {1 Test Suites} *) 538 539let resolver_tests = 540 [ 541 Alcotest.test_case "resolve did:plc" `Quick test_resolve_plc; 542 Alcotest.test_case "resolve did:web" `Quick test_resolve_web; 543 Alcotest.test_case "get handle" `Quick test_get_handle; 544 Alcotest.test_case "get PDS endpoint" `Quick test_get_pds_endpoint; 545 Alcotest.test_case "get signing key" `Quick test_get_signing_key; 546 Alcotest.test_case "not found" `Quick test_not_found; 547 Alcotest.test_case "http error" `Quick test_http_error; 548 Alcotest.test_case "invalid did" `Quick test_invalid_did; 549 Alcotest.test_case "unsupported method" `Quick test_unsupported_method; 550 ] 551 552let handle_resolver_tests = 553 [ 554 Alcotest.test_case "resolve via DNS" `Quick test_handle_resolve_via_dns; 555 Alcotest.test_case "resolve via HTTPS" `Quick test_handle_resolve_via_https; 556 Alcotest.test_case "DNS priority" `Quick test_handle_dns_priority; 557 Alcotest.test_case "not found" `Quick test_handle_not_found; 558 Alcotest.test_case "invalid handle" `Quick test_handle_invalid; 559 ] 560 561let identity_tests = 562 [ 563 Alcotest.test_case "verify DID success" `Quick test_verify_did_success; 564 Alcotest.test_case "verify handle success" `Quick test_verify_handle_success; 565 Alcotest.test_case "verify bidirectional" `Quick 566 test_verify_bidirectional_success; 567 Alcotest.test_case "DID mismatch" `Quick test_verify_did_handle_mismatch; 568 Alcotest.test_case "no handle in doc" `Quick test_verify_no_handle_in_doc; 569 Alcotest.test_case "DID resolution failed" `Quick 570 test_verify_did_resolution_failed; 571 Alcotest.test_case "handle resolution failed" `Quick 572 test_verify_handle_resolution_failed; 573 ] 574 575let () = 576 Alcotest.run "atproto-identity" 577 [ 578 ("did_resolver", resolver_tests); 579 ("handle_resolver", handle_resolver_tests); 580 ("identity", identity_tests); 581 ]