@fedify/webfinger 2.0.35-dev.2480 → 2.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,47 @@
1
1
  import { test } from "@fedify/fixture";
2
2
  import { UrlError } from "@fedify/vocab-runtime";
3
+ import {
4
+ openDirectFetch,
5
+ openPublicFetch,
6
+ type PublicFetchSession,
7
+ } from "@fedify/vocab-runtime/internal";
3
8
  import { configure, type LogRecord, reset } from "@logtape/logtape";
4
9
  import { withTimeout } from "es-toolkit";
5
10
  import fetchMock from "fetch-mock";
6
11
  import { deepStrictEqual, ok } from "node:assert/strict";
7
- import dns from "node:dns/promises";
12
+ import type { LookupAddress } from "node:dns";
8
13
  import type { ResourceDescriptor } from "./jrd.ts";
9
- import { lookupWebFinger } from "./lookup.ts";
14
+ import { lookupWebFinger, type LookupWebFingerOptions } from "./lookup.ts";
15
+ import { lookupWebFingerInternal } from "./lookup-impl.ts";
16
+
17
+ // fetch-mock routes match hostnames, but on runtimes that pin connections
18
+ // the request URL has the resolved IP address instead. Tests of the response
19
+ // handling therefore skip address validation, and tests of the address policy
20
+ // inject the DNS lookup:
21
+ function lookupDirect(
22
+ resource: URL | string,
23
+ options?: LookupWebFingerOptions,
24
+ ): Promise<ResourceDescriptor | null> {
25
+ return lookupWebFingerInternal(resource, options, {
26
+ openHop: openDirectFetch,
27
+ });
28
+ }
29
+
30
+ function lookupResolving(
31
+ addresses: readonly LookupAddress[],
32
+ ): (hostname: string) => Promise<readonly LookupAddress[]> {
33
+ return () => Promise.resolve(addresses);
34
+ }
35
+
36
+ // Lets example.com through without validation, and validates every other
37
+ // host with a lookup that resolves to a public address:
38
+ function openExampleDirectly(url: string): Promise<PublicFetchSession> {
39
+ return new URL(url).hostname === "example.com"
40
+ ? openDirectFetch(url)
41
+ : openPublicFetch(url, {
42
+ lookup: lookupResolving([{ address: "93.184.215.14", family: 4 }]),
43
+ });
44
+ }
10
45
 
11
46
  test({
12
47
  name: "lookupWebFinger()",
@@ -31,6 +66,47 @@ test({
31
66
  );
32
67
  });
33
68
 
69
+ await t.step(
70
+ "fedify-test.internal resolving to a private address",
71
+ async () => {
72
+ fetchMock.mockGlobal();
73
+ fetchMock.catch(500);
74
+ try {
75
+ const hosts: string[] = [];
76
+ const openHop = (url: string) =>
77
+ openPublicFetch(url, {
78
+ lookup: (hostname) => {
79
+ hosts.push(hostname);
80
+ return Promise.resolve([{ address: "10.0.0.1", family: 4 }]);
81
+ },
82
+ });
83
+ deepStrictEqual(
84
+ await lookupWebFingerInternal(
85
+ "acct:johndoe@fedify-test.internal",
86
+ {},
87
+ { openHop },
88
+ ),
89
+ null,
90
+ );
91
+ deepStrictEqual(
92
+ await lookupWebFingerInternal(
93
+ "https://fedify-test.internal/foo",
94
+ {},
95
+ { openHop },
96
+ ),
97
+ null,
98
+ );
99
+ deepStrictEqual(hosts, [
100
+ "fedify-test.internal",
101
+ "fedify-test.internal",
102
+ ]);
103
+ deepStrictEqual(fetchMock.callHistory.calls().length, 0);
104
+ } finally {
105
+ fetchMock.hardReset();
106
+ }
107
+ },
108
+ );
109
+
34
110
  fetchMock.spyGlobal();
35
111
  fetchMock.get(
36
112
  "begin:https://example.com/.well-known/webfinger?",
@@ -38,8 +114,8 @@ test({
38
114
  );
39
115
 
40
116
  await t.step("not found", async () => {
41
- deepStrictEqual(await lookupWebFinger("acct:johndoe@example.com"), null);
42
- deepStrictEqual(await lookupWebFinger("https://example.com/foo"), null);
117
+ deepStrictEqual(await lookupDirect("acct:johndoe@example.com"), null);
118
+ deepStrictEqual(await lookupDirect("https://example.com/foo"), null);
43
119
  });
44
120
 
45
121
  const expected: ResourceDescriptor = {
@@ -54,7 +130,7 @@ test({
54
130
 
55
131
  await t.step("acct", async () => {
56
132
  deepStrictEqual(
57
- await lookupWebFinger("acct:johndoe@example.com"),
133
+ await lookupDirect("acct:johndoe@example.com"),
58
134
  expected,
59
135
  );
60
136
  });
@@ -71,7 +147,7 @@ test({
71
147
 
72
148
  await t.step("https", async () => {
73
149
  deepStrictEqual(
74
- await lookupWebFinger("https://example.com/foo"),
150
+ await lookupDirect("https://example.com/foo"),
75
151
  expected2,
76
152
  );
77
153
  });
@@ -83,7 +159,7 @@ test({
83
159
  );
84
160
 
85
161
  await t.step("invalid response", async () => {
86
- deepStrictEqual(await lookupWebFinger("acct:johndoe@example.com"), null);
162
+ deepStrictEqual(await lookupDirect("acct:johndoe@example.com"), null);
87
163
  });
88
164
 
89
165
  fetchMock.removeRoutes();
@@ -135,7 +211,7 @@ test({
135
211
 
136
212
  await t.step("redirection", async () => {
137
213
  deepStrictEqual(
138
- await lookupWebFinger("acct:johndoe@example.com"),
214
+ await lookupDirect("acct:johndoe@example.com"),
139
215
  expected,
140
216
  );
141
217
  });
@@ -151,7 +227,7 @@ test({
151
227
 
152
228
  await t.step("infinite redirection", async () => {
153
229
  const result = await withTimeout(
154
- () => lookupWebFinger("acct:johndoe@example.com"),
230
+ () => lookupDirect("acct:johndoe@example.com"),
155
231
  2000,
156
232
  );
157
233
  deepStrictEqual(result, null);
@@ -167,7 +243,7 @@ test({
167
243
  );
168
244
 
169
245
  await t.step("redirection to different protocol", async () => {
170
- deepStrictEqual(await lookupWebFinger("acct:johndoe@example.com"), null);
246
+ deepStrictEqual(await lookupDirect("acct:johndoe@example.com"), null);
171
247
  });
172
248
 
173
249
  fetchMock.removeRoutes();
@@ -180,7 +256,12 @@ test({
180
256
  );
181
257
 
182
258
  await t.step("redirection to private address", async () => {
183
- deepStrictEqual(await lookupWebFinger("acct:johndoe@example.com"), null);
259
+ deepStrictEqual(
260
+ await lookupWebFingerInternal("acct:johndoe@example.com", {}, {
261
+ openHop: openExampleDirectly,
262
+ }),
263
+ null,
264
+ );
184
265
  });
185
266
 
186
267
  fetchMock.removeRoutes();
@@ -205,7 +286,7 @@ test({
205
286
  // Test with maxRedirection: 2 (should fail)
206
287
  redirectCount = 0;
207
288
  deepStrictEqual(
208
- await lookupWebFinger("acct:johndoe@example.com", {
289
+ await lookupDirect("acct:johndoe@example.com", {
209
290
  maxRedirection: 2,
210
291
  }),
211
292
  null,
@@ -214,7 +295,7 @@ test({
214
295
  // Test with maxRedirection: 3 (should succeed)
215
296
  redirectCount = 0;
216
297
  deepStrictEqual(
217
- await lookupWebFinger("acct:johndoe@example.com", {
298
+ await lookupDirect("acct:johndoe@example.com", {
218
299
  maxRedirection: 3,
219
300
  }),
220
301
  expected,
@@ -223,7 +304,7 @@ test({
223
304
  // Test with default maxRedirection: 5 (should succeed)
224
305
  redirectCount = 0;
225
306
  deepStrictEqual(
226
- await lookupWebFinger("acct:johndoe@example.com"),
307
+ await lookupDirect("acct:johndoe@example.com"),
227
308
  expected,
228
309
  );
229
310
  });
@@ -244,7 +325,7 @@ test({
244
325
  await t.step("request cancellation", async () => {
245
326
  // Test cancelling a request immediately using AbortController
246
327
  const controller = new AbortController();
247
- const promise = lookupWebFinger("acct:johndoe@example.com", {
328
+ const promise = lookupDirect("acct:johndoe@example.com", {
248
329
  signal: controller.signal,
249
330
  });
250
331
 
@@ -278,7 +359,7 @@ test({
278
359
  await t.step("cancellation during redirection", async () => {
279
360
  // Test cancelling a request during redirection process
280
361
  const controller = new AbortController();
281
- const promise = lookupWebFinger("acct:johndoe@example.com", {
362
+ const promise = lookupDirect("acct:johndoe@example.com", {
282
363
  signal: controller.signal,
283
364
  });
284
365
 
@@ -306,10 +387,18 @@ test({
306
387
  controller.abort();
307
388
 
308
389
  // Use a signal that was already aborted before starting the request
309
- const result = await lookupWebFinger("acct:johndoe@example.com", {
390
+ const result = await lookupDirect("acct:johndoe@example.com", {
310
391
  signal: controller.signal,
311
392
  });
312
393
  deepStrictEqual(result, null);
394
+
395
+ // Opening a validated session is aborted before the DNS lookup:
396
+ deepStrictEqual(
397
+ await lookupWebFinger("acct:johndoe@example.com", {
398
+ signal: controller.signal,
399
+ }),
400
+ null,
401
+ );
313
402
  });
314
403
 
315
404
  fetchMock.removeRoutes();
@@ -321,7 +410,7 @@ test({
321
410
  await t.step("successful request with signal", async () => {
322
411
  // Test successful request with a normal AbortController signal
323
412
  const controller = new AbortController();
324
- const result = await lookupWebFinger("acct:johndoe@example.com", {
413
+ const result = await lookupDirect("acct:johndoe@example.com", {
325
414
  signal: controller.signal,
326
415
  });
327
416
  deepStrictEqual(result, expected);
@@ -370,28 +459,18 @@ test("lookupWebFinger() bounds resource descriptors", async () => {
370
459
  });
371
460
 
372
461
  test("lookupWebFinger() logs DNS failures as such", async (t) => {
373
- // The validator skips DNS when Deno has no network permission. Checked
374
- // here rather than with top-level await, which the CommonJS build rejects.
375
- if (
376
- "Deno" in globalThis &&
377
- (await Deno.permissions.query({ name: "net" })).state !== "granted"
378
- ) return;
379
462
  for (const result of ["throws", "empty", "private"] as const) {
380
463
  await t.step(result, async () => {
381
- // Stubbing works only because vocab-runtime's url.ts uses the default
382
- // node:dns/promises import; see the FIXME there.
383
- const originalLookup = dns.lookup;
384
- dns.lookup = (() =>
464
+ const lookup = () =>
385
465
  result === "throws"
386
466
  ? Promise.reject(new Error("Resolver unavailable"))
387
467
  : Promise.resolve(
388
468
  result === "empty" ? [] : [{ address: "127.0.0.1", family: 4 }],
389
- )) as typeof dns.lookup;
469
+ );
390
470
  const records: LogRecord[] = [];
391
471
  await configure({
392
472
  sinks: {
393
- buffer: (record) =>
394
- records.push(record),
473
+ buffer: (record) => records.push(record),
395
474
  },
396
475
  loggers: [
397
476
  { category: "fedify", sinks: ["buffer"], lowestLevel: "debug" },
@@ -401,7 +480,9 @@ test("lookupWebFinger() logs DNS failures as such", async (t) => {
401
480
  });
402
481
  try {
403
482
  deepStrictEqual(
404
- await lookupWebFinger("acct:alice@dns-failure.invalid"),
483
+ await lookupWebFingerInternal("acct:alice@dns-failure.invalid", {}, {
484
+ openHop: (url) => openPublicFetch(url, { lookup }),
485
+ }),
405
486
  null,
406
487
  );
407
488
  const failures = records.filter((r) =>
@@ -427,8 +508,357 @@ test("lookupWebFinger() logs DNS failures as such", async (t) => {
427
508
  );
428
509
  } finally {
429
510
  await reset();
430
- dns.lookup = originalLookup;
431
511
  }
432
512
  });
433
513
  }
434
514
  });
515
+
516
+ interface FakeResponse {
517
+ readonly status?: number;
518
+ readonly headers?: Record<string, string>;
519
+ readonly body?: string;
520
+ /** The `url` of the response, as a pinned fetch reports it. */
521
+ readonly url?: string;
522
+ /** Makes cancelling the body fail. */
523
+ readonly cancelFails?: boolean;
524
+ /** Makes cancelling the body never settle. */
525
+ readonly cancelNeverSettles?: boolean;
526
+ }
527
+
528
+ interface HopRecorder {
529
+ readonly events: string[];
530
+ readonly openHop: (url: string) => Promise<PublicFetchSession>;
531
+ /**
532
+ * Asserts that every opened session is closed, and that every response
533
+ * body was consumed or cancelled before its session was closed.
534
+ */
535
+ assertInvariants(): void;
536
+ }
537
+
538
+ // A fake server that records the lifecycle of each session and response.
539
+ // Unlike real sessions, closing a fake session never cancels bodies, so that
540
+ // the caller's own cleanup is observed:
541
+ function recordHops(respond: (url: URL) => FakeResponse): HopRecorder {
542
+ const events: string[] = [];
543
+ const unsettled: string[] = [];
544
+ let opens = 0;
545
+ let closes = 0;
546
+ let responses = 0;
547
+ return {
548
+ events,
549
+ // deno-lint-ignore require-await
550
+ async openHop(url) {
551
+ opens++;
552
+ events.push(`open ${url}`);
553
+ const states: { id: number; settled: boolean }[] = [];
554
+ let closed = false;
555
+ return {
556
+ url: new URL(url),
557
+ // deno-lint-ignore require-await
558
+ async fetch(input) {
559
+ if (closed) throw new TypeError("The fetch session is closed.");
560
+ const target = new URL(input instanceof Request ? input.url : input);
561
+ const id = ++responses;
562
+ events.push(`fetch ${target.href} #${id}`);
563
+ const fake = respond(target);
564
+ const state = { id, settled: false };
565
+ states.push(state);
566
+ let sent = false;
567
+ const body = new ReadableStream<Uint8Array>({
568
+ pull(controller) {
569
+ if (!sent && fake.body != null && fake.body !== "") {
570
+ sent = true;
571
+ controller.enqueue(new TextEncoder().encode(fake.body));
572
+ return;
573
+ }
574
+ state.settled = true;
575
+ events.push(`consumed #${id}`);
576
+ controller.close();
577
+ },
578
+ cancel() {
579
+ state.settled = true;
580
+ events.push(`cancel #${id}`);
581
+ if (fake.cancelFails) throw new Error("Cancellation failed");
582
+ if (fake.cancelNeverSettles) return new Promise(() => {});
583
+ },
584
+ }, { highWaterMark: 0 });
585
+ const response = new Response(body, {
586
+ status: fake.status ?? 200,
587
+ headers: fake.headers,
588
+ });
589
+ if (fake.url != null) {
590
+ Object.defineProperty(response, "url", { value: fake.url });
591
+ }
592
+ return response;
593
+ },
594
+ // deno-lint-ignore require-await
595
+ async close() {
596
+ if (closed) return;
597
+ closed = true;
598
+ closes++;
599
+ for (const state of states) {
600
+ if (!state.settled) unsettled.push(`#${state.id}`);
601
+ }
602
+ events.push(`close ${url}`);
603
+ },
604
+ };
605
+ },
606
+ assertInvariants() {
607
+ deepStrictEqual(closes, opens);
608
+ deepStrictEqual(unsettled, []);
609
+ },
610
+ };
611
+ }
612
+
613
+ test("lookupWebFinger() closes each hop after cleaning up its body", async (t) => {
614
+ const descriptor: ResourceDescriptor = {
615
+ subject: "acct:johndoe@example.com",
616
+ links: [],
617
+ };
618
+ const first =
619
+ "https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com";
620
+ const second = "https://example.com/.well-known/webfinger2";
621
+
622
+ await t.step("redirect", async () => {
623
+ const recorder = recordHops((url) =>
624
+ url.pathname === "/.well-known/webfinger"
625
+ ? {
626
+ status: 302,
627
+ headers: { Location: "/.well-known/webfinger2" },
628
+ body: "redirecting",
629
+ // A pinned response's url has the IP address, which must not be
630
+ // used as the base of the Location:
631
+ url: url.href.replace("example.com", "93.184.215.14"),
632
+ }
633
+ : { body: JSON.stringify(descriptor) }
634
+ );
635
+ deepStrictEqual(
636
+ await lookupWebFingerInternal("acct:johndoe@example.com", {}, {
637
+ openHop: recorder.openHop,
638
+ }),
639
+ descriptor,
640
+ );
641
+ deepStrictEqual(recorder.events, [
642
+ `open ${first}`,
643
+ `fetch ${first} #1`,
644
+ "cancel #1",
645
+ `close ${first}`,
646
+ `open ${second}`,
647
+ `fetch ${second} #2`,
648
+ "consumed #2",
649
+ `close ${second}`,
650
+ ]);
651
+ recorder.assertInvariants();
652
+ });
653
+
654
+ await t.step("non-2xx response", async () => {
655
+ const recorder = recordHops(() => ({ status: 404, body: "not found" }));
656
+ deepStrictEqual(
657
+ await lookupWebFingerInternal("acct:johndoe@example.com", {}, {
658
+ openHop: recorder.openHop,
659
+ }),
660
+ null,
661
+ );
662
+ deepStrictEqual(recorder.events, [
663
+ `open ${first}`,
664
+ `fetch ${first} #1`,
665
+ "cancel #1",
666
+ `close ${first}`,
667
+ ]);
668
+ recorder.assertInvariants();
669
+ });
670
+
671
+ await t.step("too many redirections", async () => {
672
+ const recorder = recordHops(() => ({
673
+ status: 302,
674
+ headers: { Location: "/.well-known/webfinger2" },
675
+ body: "redirecting",
676
+ }));
677
+ deepStrictEqual(
678
+ await lookupWebFingerInternal("acct:johndoe@example.com", {
679
+ maxRedirection: 2,
680
+ }, { openHop: recorder.openHop }),
681
+ null,
682
+ );
683
+ deepStrictEqual(recorder.events, [
684
+ `open ${first}`,
685
+ `fetch ${first} #1`,
686
+ "cancel #1",
687
+ `close ${first}`,
688
+ `open ${second}`,
689
+ `fetch ${second} #2`,
690
+ "cancel #2",
691
+ `close ${second}`,
692
+ ]);
693
+ recorder.assertInvariants();
694
+ });
695
+
696
+ await t.step("failed redirect body cancellation", async () => {
697
+ const recorder = recordHops((url) =>
698
+ url.pathname === "/.well-known/webfinger"
699
+ ? {
700
+ status: 302,
701
+ headers: { Location: "/.well-known/webfinger2" },
702
+ body: "redirecting",
703
+ cancelFails: true,
704
+ }
705
+ : { body: JSON.stringify(descriptor) }
706
+ );
707
+ const records: LogRecord[] = [];
708
+ await configure({
709
+ sinks: { buffer: (record) => records.push(record) },
710
+ loggers: [
711
+ { category: "fedify", sinks: ["buffer"], lowestLevel: "debug" },
712
+ { category: ["logtape", "meta"], sinks: [] },
713
+ ],
714
+ reset: true,
715
+ });
716
+ try {
717
+ deepStrictEqual(
718
+ await lookupWebFingerInternal("acct:johndoe@example.com", {}, {
719
+ openHop: recorder.openHop,
720
+ }),
721
+ descriptor,
722
+ );
723
+ const failures = records.filter((r) =>
724
+ r.rawMessage ===
725
+ "Failed to cancel the response body from {url}: {error}"
726
+ );
727
+ deepStrictEqual(
728
+ failures.map((r) => [r.level, r.properties.url]),
729
+ [["debug", first]],
730
+ );
731
+ const { error } = failures[0].properties;
732
+ ok(error instanceof Error);
733
+ deepStrictEqual(error.message, "Cancellation failed");
734
+ } finally {
735
+ await reset();
736
+ }
737
+ deepStrictEqual(recorder.events, [
738
+ `open ${first}`,
739
+ `fetch ${first} #1`,
740
+ "cancel #1",
741
+ `close ${first}`,
742
+ `open ${second}`,
743
+ `fetch ${second} #2`,
744
+ "consumed #2",
745
+ `close ${second}`,
746
+ ]);
747
+ recorder.assertInvariants();
748
+ });
749
+
750
+ await t.step("never-settling body cancellation", async () => {
751
+ const recorder = recordHops((url) =>
752
+ url.pathname === "/.well-known/webfinger"
753
+ ? {
754
+ status: 302,
755
+ headers: { Location: "/.well-known/webfinger2" },
756
+ body: "redirecting",
757
+ cancelNeverSettles: true,
758
+ }
759
+ : { status: 404, body: "not found", cancelNeverSettles: true }
760
+ );
761
+ deepStrictEqual(
762
+ await settleWithin(
763
+ lookupWebFingerInternal("acct:johndoe@example.com", {}, {
764
+ openHop: recorder.openHop,
765
+ }),
766
+ ),
767
+ null,
768
+ );
769
+ deepStrictEqual(recorder.events, [
770
+ `open ${first}`,
771
+ `fetch ${first} #1`,
772
+ "cancel #1",
773
+ `close ${first}`,
774
+ `open ${second}`,
775
+ `fetch ${second} #2`,
776
+ "cancel #2",
777
+ `close ${second}`,
778
+ ]);
779
+ recorder.assertInvariants();
780
+ });
781
+ });
782
+
783
+ /**
784
+ * Rejects if the given promise does not settle within the timeout, so that
785
+ * a hang fails the test instead of blocking it.
786
+ */
787
+ async function settleWithin<T>(promise: Promise<T>, ms = 5000): Promise<T> {
788
+ let timer: ReturnType<typeof setTimeout> | undefined;
789
+ try {
790
+ return await Promise.race([
791
+ promise,
792
+ new Promise<never>((_, reject) => {
793
+ timer = setTimeout(
794
+ () => reject(new Error(`Did not settle within ${ms} ms.`)),
795
+ ms,
796
+ );
797
+ }),
798
+ ]);
799
+ } finally {
800
+ clearTimeout(timer);
801
+ }
802
+ }
803
+
804
+ // A body that a real session cannot cancel to completion: fetch-mock returns
805
+ // a clone (a tee) of a Response instance, and the cancellation of a branch of
806
+ // a tee settles only after the other branch is cancelled too. Its source's
807
+ // cancel callback never settles either:
808
+ function neverCancellableResponse(init: ResponseInit): Response {
809
+ return new Response(
810
+ new ReadableStream<Uint8Array>({
811
+ start(controller) {
812
+ controller.enqueue(new TextEncoder().encode("never cancelled"));
813
+ },
814
+ cancel: () => new Promise(() => {}),
815
+ }),
816
+ init,
817
+ );
818
+ }
819
+
820
+ test("lookupWebFinger() does not wait for cancelling response bodies", async (t) => {
821
+ const descriptor: ResourceDescriptor = {
822
+ subject: "acct:johndoe@example.com",
823
+ links: [],
824
+ };
825
+ fetchMock.mockGlobal();
826
+ try {
827
+ await t.step("redirect", async () => {
828
+ fetchMock.removeRoutes();
829
+ fetchMock.get(
830
+ "begin:https://example.com/.well-known/webfinger?",
831
+ () =>
832
+ neverCancellableResponse({
833
+ status: 302,
834
+ headers: { Location: "/.well-known/webfinger2" },
835
+ }),
836
+ );
837
+ fetchMock.get(
838
+ "https://example.com/.well-known/webfinger2",
839
+ () =>
840
+ new Response(JSON.stringify(descriptor), {
841
+ headers: { "Content-Type": "application/jrd+json" },
842
+ }),
843
+ );
844
+ deepStrictEqual(
845
+ await settleWithin(lookupDirect("acct:johndoe@example.com")),
846
+ descriptor,
847
+ );
848
+ });
849
+
850
+ await t.step("non-2xx response", async () => {
851
+ fetchMock.removeRoutes();
852
+ fetchMock.get(
853
+ "begin:https://example.com/.well-known/webfinger?",
854
+ () => neverCancellableResponse({ status: 404 }),
855
+ );
856
+ deepStrictEqual(
857
+ await settleWithin(lookupDirect("acct:johndoe@example.com")),
858
+ null,
859
+ );
860
+ });
861
+ } finally {
862
+ fetchMock.hardReset();
863
+ }
864
+ });