atproto libraries implementation in ocaml
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 ]