@fedify/webfinger 2.2.19 → 2.2.20

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