@fedify/webfinger 2.5.0-dev.2448 → 2.5.0-dev.2492
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.
- package/deno.json +1 -1
- package/dist/lookup.test.cjs +599 -227
- package/dist/lookup.test.js +600 -227
- package/dist/mod.cjs +226 -160
- package/dist/mod.d.cts +29 -1
- package/dist/mod.d.ts +30 -2
- package/dist/mod.js +227 -162
- package/package.json +3 -3
- package/src/lookup-impl.ts +338 -0
- package/src/lookup.test.ts +569 -80
- package/src/lookup.ts +34 -231
package/src/lookup.test.ts
CHANGED
|
@@ -4,44 +4,122 @@ import { configure, type LogRecord, reset } from "@logtape/logtape";
|
|
|
4
4
|
import { withTimeout } from "es-toolkit";
|
|
5
5
|
import fetchMock from "fetch-mock";
|
|
6
6
|
import { deepStrictEqual, ok } from "node:assert/strict";
|
|
7
|
-
import dns from "node:dns/promises";
|
|
8
7
|
import type { ResourceDescriptor } from "./jrd.ts";
|
|
9
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
lookupWebFinger,
|
|
10
|
+
type LookupWebFingerOptions,
|
|
11
|
+
lookupWebFingerWithDependencies,
|
|
12
|
+
type WebFingerLookupOutcome,
|
|
13
|
+
} from "./lookup.ts";
|
|
14
|
+
import {
|
|
15
|
+
type LookupWebFingerDependencies,
|
|
16
|
+
lookupWebFingerInternal,
|
|
17
|
+
} from "./lookup-impl.ts";
|
|
18
|
+
import {
|
|
19
|
+
openDirectFetch,
|
|
20
|
+
openPublicFetch,
|
|
21
|
+
type PublicFetchSession,
|
|
22
|
+
} from "@fedify/vocab-runtime/internal";
|
|
23
|
+
import type { LookupAddress } from "node:dns";
|
|
24
|
+
import { test as nodeTest } from "node:test";
|
|
25
|
+
|
|
26
|
+
// Inject only the transport boundary, so the public tracing and metrics
|
|
27
|
+
// wrapper remains under test while fetch-mock sees the original hostname.
|
|
28
|
+
function lookupMock(
|
|
29
|
+
resource: URL | string,
|
|
30
|
+
options: LookupWebFingerOptions & { withDiagnostics: true },
|
|
31
|
+
): Promise<WebFingerLookupOutcome>;
|
|
32
|
+
function lookupMock(
|
|
33
|
+
resource: URL | string,
|
|
34
|
+
options?: LookupWebFingerOptions & { withDiagnostics?: false },
|
|
35
|
+
): Promise<ResourceDescriptor | null>;
|
|
36
|
+
function lookupMock(
|
|
37
|
+
resource: URL | string,
|
|
38
|
+
options: LookupWebFingerOptions & { withDiagnostics?: boolean } = {},
|
|
39
|
+
): Promise<ResourceDescriptor | null | WebFingerLookupOutcome> {
|
|
40
|
+
if (resource.toString().includes("fedify-test.internal")) {
|
|
41
|
+
return lookupWebFingerWithDependencies(resource, options, {
|
|
42
|
+
openHop: (url) =>
|
|
43
|
+
Promise.resolve({
|
|
44
|
+
url: new URL(url),
|
|
45
|
+
fetch: () => Promise.reject(new TypeError("Connection refused")),
|
|
46
|
+
close: () => Promise.resolve(),
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return lookupWebFingerWithDependencies(resource, options, {
|
|
51
|
+
openHop: options.allowPrivateAddress === true
|
|
52
|
+
? openDirectFetch
|
|
53
|
+
: openExampleDirectly,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function lookupDirect(
|
|
58
|
+
resource: URL | string,
|
|
59
|
+
options?: LookupWebFingerOptions,
|
|
60
|
+
): Promise<ResourceDescriptor | null> {
|
|
61
|
+
return lookupWebFingerWithDependencies(resource, options, {
|
|
62
|
+
openHop: openDirectFetch,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function lookupResolving(
|
|
67
|
+
addresses: readonly LookupAddress[],
|
|
68
|
+
): (hostname: string) => Promise<readonly LookupAddress[]> {
|
|
69
|
+
return () => Promise.resolve(addresses);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function openExampleDirectly(url: string): Promise<PublicFetchSession> {
|
|
73
|
+
return new URL(url).hostname === "example.com"
|
|
74
|
+
? openDirectFetch(url)
|
|
75
|
+
: openPublicFetch(url, {
|
|
76
|
+
lookup: lookupResolving([{ address: "93.184.215.14", family: 4 }]),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function lookupResourceWithDependencies(
|
|
81
|
+
resource: URL | string,
|
|
82
|
+
options: LookupWebFingerOptions,
|
|
83
|
+
deps: LookupWebFingerDependencies,
|
|
84
|
+
): Promise<ResourceDescriptor | null> {
|
|
85
|
+
return (await lookupWebFingerInternal(resource, options, deps)).resource;
|
|
86
|
+
}
|
|
10
87
|
|
|
11
88
|
test({
|
|
12
89
|
name: "lookupWebFinger()",
|
|
13
90
|
sanitizeOps: false,
|
|
14
91
|
sanitizeResources: false,
|
|
15
92
|
async fn(t) {
|
|
93
|
+
const responseTimers: ReturnType<typeof setTimeout>[] = [];
|
|
16
94
|
await t.step("invalid resource", async () => {
|
|
17
|
-
deepStrictEqual(await
|
|
18
|
-
deepStrictEqual(await
|
|
19
|
-
deepStrictEqual(await
|
|
20
|
-
deepStrictEqual(await
|
|
95
|
+
deepStrictEqual(await lookupMock("acct:johndoe"), null);
|
|
96
|
+
deepStrictEqual(await lookupMock(new URL("acct:johndoe")), null);
|
|
97
|
+
deepStrictEqual(await lookupMock("acct:johndoe@"), null);
|
|
98
|
+
deepStrictEqual(await lookupMock(new URL("acct:johndoe@")), null);
|
|
21
99
|
// Per RFC 7565, the acct: authority is bare `host`: no path,
|
|
22
100
|
// query, or fragment is allowed. Reject such inputs rather than
|
|
23
101
|
// forwarding them to a remote WebFinger lookup.
|
|
24
102
|
deepStrictEqual(
|
|
25
|
-
await
|
|
103
|
+
await lookupMock("acct:johndoe@example.com/exploit"),
|
|
26
104
|
null,
|
|
27
105
|
);
|
|
28
106
|
deepStrictEqual(
|
|
29
|
-
await
|
|
107
|
+
await lookupMock("acct:johndoe@example.com?x=1"),
|
|
30
108
|
null,
|
|
31
109
|
);
|
|
32
110
|
deepStrictEqual(
|
|
33
|
-
await
|
|
111
|
+
await lookupMock("acct:johndoe@example.com#frag"),
|
|
34
112
|
null,
|
|
35
113
|
);
|
|
36
114
|
});
|
|
37
115
|
|
|
38
116
|
await t.step("connection refused", async () => {
|
|
39
117
|
deepStrictEqual(
|
|
40
|
-
await
|
|
118
|
+
await lookupMock("acct:johndoe@fedify-test.internal"),
|
|
41
119
|
null,
|
|
42
120
|
);
|
|
43
121
|
deepStrictEqual(
|
|
44
|
-
await
|
|
122
|
+
await lookupMock("https://fedify-test.internal/foo"),
|
|
45
123
|
null,
|
|
46
124
|
);
|
|
47
125
|
});
|
|
@@ -59,10 +137,10 @@ test({
|
|
|
59
137
|
|
|
60
138
|
await t.step("not found", async () => {
|
|
61
139
|
deepStrictEqual(
|
|
62
|
-
await
|
|
140
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
63
141
|
null,
|
|
64
142
|
);
|
|
65
|
-
deepStrictEqual(await
|
|
143
|
+
deepStrictEqual(await lookupMock("https://example.com/foo"), null);
|
|
66
144
|
});
|
|
67
145
|
|
|
68
146
|
const expected: ResourceDescriptor = {
|
|
@@ -77,7 +155,7 @@ test({
|
|
|
77
155
|
|
|
78
156
|
await t.step("acct", async () => {
|
|
79
157
|
deepStrictEqual(
|
|
80
|
-
await
|
|
158
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
81
159
|
expected,
|
|
82
160
|
);
|
|
83
161
|
});
|
|
@@ -94,7 +172,7 @@ test({
|
|
|
94
172
|
|
|
95
173
|
await t.step("https", async () => {
|
|
96
174
|
deepStrictEqual(
|
|
97
|
-
await
|
|
175
|
+
await lookupMock("https://example.com/foo"),
|
|
98
176
|
expected2,
|
|
99
177
|
);
|
|
100
178
|
});
|
|
@@ -115,7 +193,7 @@ test({
|
|
|
115
193
|
// host extraction (after the last `@`) applies to `mailto:` just
|
|
116
194
|
// like `acct:`.
|
|
117
195
|
deepStrictEqual(
|
|
118
|
-
await
|
|
196
|
+
await lookupMock("mailto:juliet@example.com"),
|
|
119
197
|
mailtoExpected,
|
|
120
198
|
);
|
|
121
199
|
});
|
|
@@ -137,7 +215,7 @@ test({
|
|
|
137
215
|
// must accept them and forward the full resource URI to the
|
|
138
216
|
// WebFinger endpoint.
|
|
139
217
|
deepStrictEqual(
|
|
140
|
-
await
|
|
218
|
+
await lookupMock("mailto:juliet@example.com?subject=Hi"),
|
|
141
219
|
mailtoQueryExpected,
|
|
142
220
|
);
|
|
143
221
|
});
|
|
@@ -150,7 +228,7 @@ test({
|
|
|
150
228
|
|
|
151
229
|
await t.step("invalid response", async () => {
|
|
152
230
|
deepStrictEqual(
|
|
153
|
-
await
|
|
231
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
154
232
|
null,
|
|
155
233
|
);
|
|
156
234
|
});
|
|
@@ -171,9 +249,9 @@ test({
|
|
|
171
249
|
);
|
|
172
250
|
|
|
173
251
|
await t.step("private address", async () => {
|
|
174
|
-
deepStrictEqual(await
|
|
252
|
+
deepStrictEqual(await lookupMock("acct:test@localhost"), null);
|
|
175
253
|
deepStrictEqual(
|
|
176
|
-
await
|
|
254
|
+
await lookupMock("acct:test@localhost", {
|
|
177
255
|
allowPrivateAddress: true,
|
|
178
256
|
}),
|
|
179
257
|
{
|
|
@@ -204,7 +282,7 @@ test({
|
|
|
204
282
|
|
|
205
283
|
await t.step("redirection", async () => {
|
|
206
284
|
deepStrictEqual(
|
|
207
|
-
await
|
|
285
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
208
286
|
expected,
|
|
209
287
|
);
|
|
210
288
|
});
|
|
@@ -220,7 +298,7 @@ test({
|
|
|
220
298
|
|
|
221
299
|
await t.step("infinite redirection", async () => {
|
|
222
300
|
const result = await withTimeout(
|
|
223
|
-
() =>
|
|
301
|
+
() => lookupMock("acct:johndoe@example.com"),
|
|
224
302
|
2000,
|
|
225
303
|
);
|
|
226
304
|
deepStrictEqual(result, null);
|
|
@@ -237,7 +315,7 @@ test({
|
|
|
237
315
|
|
|
238
316
|
await t.step("redirection to different protocol", async () => {
|
|
239
317
|
deepStrictEqual(
|
|
240
|
-
await
|
|
318
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
241
319
|
null,
|
|
242
320
|
);
|
|
243
321
|
});
|
|
@@ -253,7 +331,7 @@ test({
|
|
|
253
331
|
|
|
254
332
|
await t.step("redirection to private address", async () => {
|
|
255
333
|
deepStrictEqual(
|
|
256
|
-
await
|
|
334
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
257
335
|
null,
|
|
258
336
|
);
|
|
259
337
|
});
|
|
@@ -280,7 +358,7 @@ test({
|
|
|
280
358
|
// Test with maxRedirection: 1 (should fail; mock has 2 redirects)
|
|
281
359
|
redirectCount = 0;
|
|
282
360
|
deepStrictEqual(
|
|
283
|
-
await
|
|
361
|
+
await lookupMock("acct:johndoe@example.com", {
|
|
284
362
|
maxRedirection: 1,
|
|
285
363
|
}),
|
|
286
364
|
null,
|
|
@@ -290,7 +368,7 @@ test({
|
|
|
290
368
|
// redirects, and `maxRedirection: N` follows up to N redirects)
|
|
291
369
|
redirectCount = 0;
|
|
292
370
|
deepStrictEqual(
|
|
293
|
-
await
|
|
371
|
+
await lookupMock("acct:johndoe@example.com", {
|
|
294
372
|
maxRedirection: 2,
|
|
295
373
|
}),
|
|
296
374
|
expected,
|
|
@@ -299,7 +377,7 @@ test({
|
|
|
299
377
|
// Test with maxRedirection: 3 (should succeed)
|
|
300
378
|
redirectCount = 0;
|
|
301
379
|
deepStrictEqual(
|
|
302
|
-
await
|
|
380
|
+
await lookupMock("acct:johndoe@example.com", {
|
|
303
381
|
maxRedirection: 3,
|
|
304
382
|
}),
|
|
305
383
|
expected,
|
|
@@ -308,7 +386,7 @@ test({
|
|
|
308
386
|
// Test with default maxRedirection: 5 (should succeed)
|
|
309
387
|
redirectCount = 0;
|
|
310
388
|
deepStrictEqual(
|
|
311
|
-
await
|
|
389
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
312
390
|
expected,
|
|
313
391
|
);
|
|
314
392
|
});
|
|
@@ -338,7 +416,7 @@ test({
|
|
|
338
416
|
},
|
|
339
417
|
);
|
|
340
418
|
deepStrictEqual(
|
|
341
|
-
await
|
|
419
|
+
await lookupMock("acct:johndoe@example.com", {
|
|
342
420
|
maxRedirection: 1,
|
|
343
421
|
}),
|
|
344
422
|
expected,
|
|
@@ -363,7 +441,7 @@ test({
|
|
|
363
441
|
},
|
|
364
442
|
);
|
|
365
443
|
deepStrictEqual(
|
|
366
|
-
await
|
|
444
|
+
await lookupMock("acct:johndoe@example.com", {
|
|
367
445
|
maxRedirection: 1,
|
|
368
446
|
}),
|
|
369
447
|
null,
|
|
@@ -380,14 +458,14 @@ test({
|
|
|
380
458
|
resolve({ body: expected });
|
|
381
459
|
}, 1000);
|
|
382
460
|
|
|
383
|
-
|
|
461
|
+
responseTimers.push(timeoutId);
|
|
384
462
|
}),
|
|
385
463
|
);
|
|
386
464
|
|
|
387
465
|
await t.step("request cancellation", async () => {
|
|
388
466
|
// Test cancelling a request immediately using AbortController
|
|
389
467
|
const controller = new AbortController();
|
|
390
|
-
const promise =
|
|
468
|
+
const promise = lookupMock("acct:johndoe@example.com", {
|
|
391
469
|
signal: controller.signal,
|
|
392
470
|
});
|
|
393
471
|
|
|
@@ -413,7 +491,7 @@ test({
|
|
|
413
491
|
resolve({ body: expected });
|
|
414
492
|
}, 1000);
|
|
415
493
|
|
|
416
|
-
|
|
494
|
+
responseTimers.push(timeoutId);
|
|
417
495
|
});
|
|
418
496
|
},
|
|
419
497
|
);
|
|
@@ -421,7 +499,7 @@ test({
|
|
|
421
499
|
await t.step("cancellation during redirection", async () => {
|
|
422
500
|
// Test cancelling a request during redirection process
|
|
423
501
|
const controller = new AbortController();
|
|
424
|
-
const promise =
|
|
502
|
+
const promise = lookupMock("acct:johndoe@example.com", {
|
|
425
503
|
signal: controller.signal,
|
|
426
504
|
});
|
|
427
505
|
|
|
@@ -439,7 +517,7 @@ test({
|
|
|
439
517
|
resolve({ body: expected });
|
|
440
518
|
}, 500);
|
|
441
519
|
|
|
442
|
-
|
|
520
|
+
responseTimers.push(timeoutId);
|
|
443
521
|
}),
|
|
444
522
|
);
|
|
445
523
|
|
|
@@ -449,7 +527,7 @@ test({
|
|
|
449
527
|
controller.abort();
|
|
450
528
|
|
|
451
529
|
// Use a signal that was already aborted before starting the request
|
|
452
|
-
const result = await
|
|
530
|
+
const result = await lookupMock("acct:johndoe@example.com", {
|
|
453
531
|
signal: controller.signal,
|
|
454
532
|
});
|
|
455
533
|
deepStrictEqual(result, null);
|
|
@@ -464,12 +542,13 @@ test({
|
|
|
464
542
|
await t.step("successful request with signal", async () => {
|
|
465
543
|
// Test successful request with a normal AbortController signal
|
|
466
544
|
const controller = new AbortController();
|
|
467
|
-
const result = await
|
|
545
|
+
const result = await lookupMock("acct:johndoe@example.com", {
|
|
468
546
|
signal: controller.signal,
|
|
469
547
|
});
|
|
470
548
|
deepStrictEqual(result, expected);
|
|
471
549
|
});
|
|
472
550
|
} finally {
|
|
551
|
+
for (const timer of responseTimers) clearTimeout(timer);
|
|
473
552
|
fetchMock.removeRoutes();
|
|
474
553
|
fetchMock.hardReset();
|
|
475
554
|
}
|
|
@@ -496,7 +575,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
496
575
|
{ body: expected },
|
|
497
576
|
);
|
|
498
577
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
499
|
-
const result = await
|
|
578
|
+
const result = await lookupMock("acct:johndoe@example.com", {
|
|
500
579
|
meterProvider,
|
|
501
580
|
});
|
|
502
581
|
deepStrictEqual(result, expected);
|
|
@@ -546,7 +625,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
546
625
|
{ body: { subject: "https://example.com/foo", links: [] } },
|
|
547
626
|
);
|
|
548
627
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
549
|
-
await
|
|
628
|
+
await lookupMock("https://example.com/foo", { meterProvider });
|
|
550
629
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
551
630
|
deepStrictEqual(counters.length, 1);
|
|
552
631
|
deepStrictEqual(
|
|
@@ -569,7 +648,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
569
648
|
{ body: { subject: "https://example.com:8443/foo", links: [] } },
|
|
570
649
|
);
|
|
571
650
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
572
|
-
await
|
|
651
|
+
await lookupMock("https://example.com:8443/foo", {
|
|
573
652
|
meterProvider,
|
|
574
653
|
});
|
|
575
654
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
@@ -588,7 +667,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
588
667
|
{ status: 404 },
|
|
589
668
|
);
|
|
590
669
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
591
|
-
const result = await
|
|
670
|
+
const result = await lookupMock("acct:johndoe@example.com", {
|
|
592
671
|
meterProvider,
|
|
593
672
|
});
|
|
594
673
|
deepStrictEqual(result, null);
|
|
@@ -623,7 +702,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
623
702
|
{ status: 410 },
|
|
624
703
|
);
|
|
625
704
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
626
|
-
await
|
|
705
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
627
706
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
628
707
|
ok(counter != null);
|
|
629
708
|
deepStrictEqual(
|
|
@@ -642,7 +721,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
642
721
|
{ status: 500 },
|
|
643
722
|
);
|
|
644
723
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
645
|
-
await
|
|
724
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
646
725
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
647
726
|
ok(counter != null);
|
|
648
727
|
deepStrictEqual(
|
|
@@ -662,7 +741,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
662
741
|
{ body: "not json" },
|
|
663
742
|
);
|
|
664
743
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
665
|
-
await
|
|
744
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
666
745
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
667
746
|
ok(counter != null);
|
|
668
747
|
deepStrictEqual(
|
|
@@ -678,7 +757,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
678
757
|
async () => {
|
|
679
758
|
fetchMock.removeRoutes();
|
|
680
759
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
681
|
-
const result = await
|
|
760
|
+
const result = await lookupMock(
|
|
682
761
|
"acct:johndoe@fedify-test.internal",
|
|
683
762
|
{ meterProvider },
|
|
684
763
|
);
|
|
@@ -705,7 +784,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
705
784
|
"records result=invalid for malformed acct: resources",
|
|
706
785
|
async () => {
|
|
707
786
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
708
|
-
const result = await
|
|
787
|
+
const result = await lookupMock("acct:johndoe", { meterProvider });
|
|
709
788
|
deepStrictEqual(result, null);
|
|
710
789
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
711
790
|
ok(counter != null);
|
|
@@ -742,7 +821,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
742
821
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
743
822
|
const result = await withTimeout(
|
|
744
823
|
() =>
|
|
745
|
-
|
|
824
|
+
lookupMock("acct:johndoe@example.com", {
|
|
746
825
|
meterProvider,
|
|
747
826
|
maxRedirection: 3,
|
|
748
827
|
}),
|
|
@@ -775,7 +854,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
775
854
|
},
|
|
776
855
|
);
|
|
777
856
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
778
|
-
await
|
|
857
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
779
858
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
780
859
|
ok(counter != null);
|
|
781
860
|
deepStrictEqual(
|
|
@@ -798,7 +877,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
798
877
|
},
|
|
799
878
|
);
|
|
800
879
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
801
|
-
await
|
|
880
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
802
881
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
803
882
|
ok(counter != null);
|
|
804
883
|
deepStrictEqual(
|
|
@@ -825,7 +904,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
825
904
|
},
|
|
826
905
|
);
|
|
827
906
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
828
|
-
await
|
|
907
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
829
908
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
830
909
|
ok(counter != null);
|
|
831
910
|
deepStrictEqual(
|
|
@@ -852,7 +931,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
852
931
|
// attempt to build a host from the URL, fail, and return null.
|
|
853
932
|
// The metric still records, and its scheme attribute must be
|
|
854
933
|
// bucketed as `other`.
|
|
855
|
-
await
|
|
934
|
+
await lookupMock("ssh://example.com/foo", { meterProvider });
|
|
856
935
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
857
936
|
ok(counter != null);
|
|
858
937
|
deepStrictEqual(
|
|
@@ -871,7 +950,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
871
950
|
{ body: expected },
|
|
872
951
|
);
|
|
873
952
|
const [_unused, recorder] = createTestMeterProvider();
|
|
874
|
-
await
|
|
953
|
+
await lookupMock("acct:johndoe@example.com");
|
|
875
954
|
deepStrictEqual(
|
|
876
955
|
recorder.getMeasurements("webfinger.lookup").length,
|
|
877
956
|
0,
|
|
@@ -909,14 +988,14 @@ test("lookupWebFinger() bounds resource descriptors", async () => {
|
|
|
909
988
|
),
|
|
910
989
|
);
|
|
911
990
|
deepStrictEqual(
|
|
912
|
-
await
|
|
991
|
+
await lookupMock("acct:alice@example.com", {
|
|
913
992
|
allowPrivateAddress: true,
|
|
914
993
|
}),
|
|
915
994
|
null,
|
|
916
995
|
);
|
|
917
996
|
oversized = false;
|
|
918
997
|
deepStrictEqual(
|
|
919
|
-
await
|
|
998
|
+
await lookupMock("acct:alice@example.com", {
|
|
920
999
|
allowPrivateAddress: true,
|
|
921
1000
|
}),
|
|
922
1001
|
{ subject: "acct:alice@example.com" },
|
|
@@ -927,28 +1006,18 @@ test("lookupWebFinger() bounds resource descriptors", async () => {
|
|
|
927
1006
|
});
|
|
928
1007
|
|
|
929
1008
|
test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
930
|
-
// The validator skips DNS when Deno has no network permission. Checked
|
|
931
|
-
// here rather than with top-level await, which the CommonJS build rejects.
|
|
932
|
-
if (
|
|
933
|
-
"Deno" in globalThis &&
|
|
934
|
-
(await Deno.permissions.query({ name: "net" })).state !== "granted"
|
|
935
|
-
) return;
|
|
936
1009
|
for (const result of ["throws", "empty", "private"] as const) {
|
|
937
1010
|
await t.step(result, async () => {
|
|
938
|
-
|
|
939
|
-
// node:dns/promises import; see the FIXME there.
|
|
940
|
-
const originalLookup = dns.lookup;
|
|
941
|
-
dns.lookup = (() =>
|
|
1011
|
+
const lookup = () =>
|
|
942
1012
|
result === "throws"
|
|
943
1013
|
? Promise.reject(new Error("Resolver unavailable"))
|
|
944
1014
|
: Promise.resolve(
|
|
945
1015
|
result === "empty" ? [] : [{ address: "127.0.0.1", family: 4 }],
|
|
946
|
-
)
|
|
1016
|
+
);
|
|
947
1017
|
const records: LogRecord[] = [];
|
|
948
1018
|
await configure({
|
|
949
1019
|
sinks: {
|
|
950
|
-
buffer: (record) =>
|
|
951
|
-
records.push(record),
|
|
1020
|
+
buffer: (record) => records.push(record),
|
|
952
1021
|
},
|
|
953
1022
|
loggers: [
|
|
954
1023
|
{ category: "fedify", sinks: ["buffer"], lowestLevel: "debug" },
|
|
@@ -958,7 +1027,13 @@ test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
|
958
1027
|
});
|
|
959
1028
|
try {
|
|
960
1029
|
deepStrictEqual(
|
|
961
|
-
await
|
|
1030
|
+
await lookupResourceWithDependencies(
|
|
1031
|
+
"acct:alice@dns-failure.invalid",
|
|
1032
|
+
{},
|
|
1033
|
+
{
|
|
1034
|
+
openHop: (url) => openPublicFetch(url, { lookup }),
|
|
1035
|
+
},
|
|
1036
|
+
),
|
|
962
1037
|
null,
|
|
963
1038
|
);
|
|
964
1039
|
const failures = records.filter((r) =>
|
|
@@ -984,7 +1059,6 @@ test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
|
984
1059
|
);
|
|
985
1060
|
} finally {
|
|
986
1061
|
await reset();
|
|
987
|
-
dns.lookup = originalLookup;
|
|
988
1062
|
}
|
|
989
1063
|
});
|
|
990
1064
|
}
|
|
@@ -994,11 +1068,10 @@ test("lookupWebFinger() with withDiagnostics: true", {
|
|
|
994
1068
|
sanitizeOps: false,
|
|
995
1069
|
sanitizeResources: false,
|
|
996
1070
|
}, async (t) => {
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
// refused" step above does.
|
|
1071
|
+
// Inject the same failed session as the connection-refused transport test,
|
|
1072
|
+
// so the diagnostics path does not depend on real DNS or networking.
|
|
1000
1073
|
await t.step("network_error", async () => {
|
|
1001
|
-
const outcome = await
|
|
1074
|
+
const outcome = await lookupMock(
|
|
1002
1075
|
"acct:johndoe@fedify-test.internal",
|
|
1003
1076
|
{ withDiagnostics: true },
|
|
1004
1077
|
);
|
|
@@ -1023,7 +1096,7 @@ test("lookupWebFinger() with withDiagnostics: true", {
|
|
|
1023
1096
|
"https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com",
|
|
1024
1097
|
{ body: expected },
|
|
1025
1098
|
);
|
|
1026
|
-
const outcome = await
|
|
1099
|
+
const outcome = await lookupMock("acct:johndoe@example.com", {
|
|
1027
1100
|
withDiagnostics: true,
|
|
1028
1101
|
});
|
|
1029
1102
|
deepStrictEqual(outcome.resource, expected);
|
|
@@ -1038,7 +1111,7 @@ test("lookupWebFinger() with withDiagnostics: true", {
|
|
|
1038
1111
|
"begin:https://example.com/.well-known/webfinger?",
|
|
1039
1112
|
{ status: 404 },
|
|
1040
1113
|
);
|
|
1041
|
-
const outcome = await
|
|
1114
|
+
const outcome = await lookupMock("acct:johndoe@example.com", {
|
|
1042
1115
|
withDiagnostics: true,
|
|
1043
1116
|
});
|
|
1044
1117
|
deepStrictEqual(outcome.resource, null);
|
|
@@ -1053,7 +1126,7 @@ test("lookupWebFinger() with withDiagnostics: true", {
|
|
|
1053
1126
|
"begin:https://example.com/.well-known/webfinger?",
|
|
1054
1127
|
{ body: "not json" },
|
|
1055
1128
|
);
|
|
1056
|
-
const outcome = await
|
|
1129
|
+
const outcome = await lookupMock("acct:johndoe@example.com", {
|
|
1057
1130
|
withDiagnostics: true,
|
|
1058
1131
|
});
|
|
1059
1132
|
deepStrictEqual(outcome.resource, null);
|
|
@@ -1074,7 +1147,7 @@ test("lookupWebFinger() with withDiagnostics: true", {
|
|
|
1074
1147
|
"begin:https://example.com/.well-known/webfinger?",
|
|
1075
1148
|
{ body: "null" },
|
|
1076
1149
|
);
|
|
1077
|
-
const outcome = await
|
|
1150
|
+
const outcome = await lookupMock("acct:johndoe@example.com", {
|
|
1078
1151
|
withDiagnostics: true,
|
|
1079
1152
|
});
|
|
1080
1153
|
deepStrictEqual(outcome.resource, null);
|
|
@@ -1094,7 +1167,7 @@ test("lookupWebFinger() with withDiagnostics: true", {
|
|
|
1094
1167
|
"begin:https://example.com/.well-known/webfinger?",
|
|
1095
1168
|
{ body: "[]" },
|
|
1096
1169
|
);
|
|
1097
|
-
const outcome = await
|
|
1170
|
+
const outcome = await lookupMock("acct:johndoe@example.com", {
|
|
1098
1171
|
withDiagnostics: true,
|
|
1099
1172
|
});
|
|
1100
1173
|
deepStrictEqual(outcome.resource, null);
|
|
@@ -1110,7 +1183,7 @@ test("lookupWebFinger() with withDiagnostics: true", {
|
|
|
1110
1183
|
"begin:https://example.com/.well-known/webfinger?",
|
|
1111
1184
|
{ status: 500 },
|
|
1112
1185
|
);
|
|
1113
|
-
const outcome = await
|
|
1186
|
+
const outcome = await lookupMock("acct:johndoe@example.com", {
|
|
1114
1187
|
withDiagnostics: true,
|
|
1115
1188
|
});
|
|
1116
1189
|
deepStrictEqual(outcome.resource, null);
|
|
@@ -1141,7 +1214,10 @@ test(
|
|
|
1141
1214
|
"https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com",
|
|
1142
1215
|
{ body: expected },
|
|
1143
1216
|
);
|
|
1144
|
-
const options: LookupWebFingerOptions = {
|
|
1217
|
+
const options: LookupWebFingerOptions = {
|
|
1218
|
+
userAgent: "test-agent",
|
|
1219
|
+
allowPrivateAddress: true,
|
|
1220
|
+
};
|
|
1145
1221
|
const result = await lookupWebFinger(
|
|
1146
1222
|
"acct:johndoe@example.com",
|
|
1147
1223
|
options,
|
|
@@ -1153,3 +1229,416 @@ test(
|
|
|
1153
1229
|
}
|
|
1154
1230
|
},
|
|
1155
1231
|
);
|
|
1232
|
+
|
|
1233
|
+
interface FakeResponse {
|
|
1234
|
+
readonly status?: number;
|
|
1235
|
+
readonly headers?: Record<string, string>;
|
|
1236
|
+
readonly body?: string;
|
|
1237
|
+
/** The `url` of the response, as a pinned fetch reports it. */
|
|
1238
|
+
readonly url?: string;
|
|
1239
|
+
/** Makes cancelling the body fail. */
|
|
1240
|
+
readonly cancelFails?: boolean;
|
|
1241
|
+
/** Makes cancelling the body never settle. */
|
|
1242
|
+
readonly cancelNeverSettles?: boolean;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
interface HopRecorder {
|
|
1246
|
+
readonly events: string[];
|
|
1247
|
+
readonly openHop: (url: string) => Promise<PublicFetchSession>;
|
|
1248
|
+
/**
|
|
1249
|
+
* Asserts that every opened session is closed, and that every response
|
|
1250
|
+
* body was consumed or cancelled before its session was closed.
|
|
1251
|
+
*/
|
|
1252
|
+
assertInvariants(): void;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// A fake server that records the lifecycle of each session and response.
|
|
1256
|
+
// Unlike real sessions, closing a fake session never cancels bodies, so that
|
|
1257
|
+
// the caller's own cleanup is observed:
|
|
1258
|
+
function recordHops(respond: (url: URL) => FakeResponse): HopRecorder {
|
|
1259
|
+
const events: string[] = [];
|
|
1260
|
+
const unsettled: string[] = [];
|
|
1261
|
+
let opens = 0;
|
|
1262
|
+
let closes = 0;
|
|
1263
|
+
let responses = 0;
|
|
1264
|
+
return {
|
|
1265
|
+
events,
|
|
1266
|
+
// deno-lint-ignore require-await
|
|
1267
|
+
async openHop(url) {
|
|
1268
|
+
opens++;
|
|
1269
|
+
events.push(`open ${url}`);
|
|
1270
|
+
const states: { id: number; settled: boolean }[] = [];
|
|
1271
|
+
let closed = false;
|
|
1272
|
+
return {
|
|
1273
|
+
url: new URL(url),
|
|
1274
|
+
// deno-lint-ignore require-await
|
|
1275
|
+
async fetch(input) {
|
|
1276
|
+
if (closed) throw new TypeError("The fetch session is closed.");
|
|
1277
|
+
const target = new URL(input instanceof Request ? input.url : input);
|
|
1278
|
+
const id = ++responses;
|
|
1279
|
+
events.push(`fetch ${target.href} #${id}`);
|
|
1280
|
+
const fake = respond(target);
|
|
1281
|
+
const state = { id, settled: false };
|
|
1282
|
+
states.push(state);
|
|
1283
|
+
let sent = false;
|
|
1284
|
+
const body = new ReadableStream<Uint8Array>({
|
|
1285
|
+
pull(controller) {
|
|
1286
|
+
if (!sent && fake.body != null && fake.body !== "") {
|
|
1287
|
+
sent = true;
|
|
1288
|
+
controller.enqueue(new TextEncoder().encode(fake.body));
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
state.settled = true;
|
|
1292
|
+
events.push(`consumed #${id}`);
|
|
1293
|
+
controller.close();
|
|
1294
|
+
},
|
|
1295
|
+
cancel() {
|
|
1296
|
+
state.settled = true;
|
|
1297
|
+
events.push(`cancel #${id}`);
|
|
1298
|
+
if (fake.cancelFails) throw new Error("Cancellation failed");
|
|
1299
|
+
if (fake.cancelNeverSettles) return new Promise(() => {});
|
|
1300
|
+
},
|
|
1301
|
+
}, { highWaterMark: 0 });
|
|
1302
|
+
const response = new Response(body, {
|
|
1303
|
+
status: fake.status ?? 200,
|
|
1304
|
+
headers: fake.headers,
|
|
1305
|
+
});
|
|
1306
|
+
if (fake.url != null) {
|
|
1307
|
+
Object.defineProperty(response, "url", { value: fake.url });
|
|
1308
|
+
}
|
|
1309
|
+
return response;
|
|
1310
|
+
},
|
|
1311
|
+
// deno-lint-ignore require-await
|
|
1312
|
+
async close() {
|
|
1313
|
+
if (closed) return;
|
|
1314
|
+
closed = true;
|
|
1315
|
+
closes++;
|
|
1316
|
+
for (const state of states) {
|
|
1317
|
+
if (!state.settled) unsettled.push(`#${state.id}`);
|
|
1318
|
+
}
|
|
1319
|
+
events.push(`close ${url}`);
|
|
1320
|
+
},
|
|
1321
|
+
};
|
|
1322
|
+
},
|
|
1323
|
+
assertInvariants() {
|
|
1324
|
+
deepStrictEqual(closes, opens);
|
|
1325
|
+
deepStrictEqual(unsettled, []);
|
|
1326
|
+
},
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
test("lookupWebFinger() closes each hop after cleaning up its body", async (t) => {
|
|
1331
|
+
const descriptor: ResourceDescriptor = {
|
|
1332
|
+
subject: "acct:johndoe@example.com",
|
|
1333
|
+
links: [],
|
|
1334
|
+
};
|
|
1335
|
+
const first =
|
|
1336
|
+
"https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com";
|
|
1337
|
+
const second = "https://example.com/.well-known/webfinger2";
|
|
1338
|
+
|
|
1339
|
+
await t.step("redirect", async () => {
|
|
1340
|
+
const recorder = recordHops((url) =>
|
|
1341
|
+
url.pathname === "/.well-known/webfinger"
|
|
1342
|
+
? {
|
|
1343
|
+
status: 302,
|
|
1344
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1345
|
+
body: "redirecting",
|
|
1346
|
+
// A pinned response's url has the IP address, which must not be
|
|
1347
|
+
// used as the base of the Location:
|
|
1348
|
+
url: url.href.replace("example.com", "93.184.215.14"),
|
|
1349
|
+
}
|
|
1350
|
+
: { body: JSON.stringify(descriptor) }
|
|
1351
|
+
);
|
|
1352
|
+
deepStrictEqual(
|
|
1353
|
+
await lookupResourceWithDependencies("acct:johndoe@example.com", {}, {
|
|
1354
|
+
openHop: recorder.openHop,
|
|
1355
|
+
}),
|
|
1356
|
+
descriptor,
|
|
1357
|
+
);
|
|
1358
|
+
deepStrictEqual(recorder.events, [
|
|
1359
|
+
`open ${first}`,
|
|
1360
|
+
`fetch ${first} #1`,
|
|
1361
|
+
"cancel #1",
|
|
1362
|
+
`close ${first}`,
|
|
1363
|
+
`open ${second}`,
|
|
1364
|
+
`fetch ${second} #2`,
|
|
1365
|
+
"consumed #2",
|
|
1366
|
+
`close ${second}`,
|
|
1367
|
+
]);
|
|
1368
|
+
recorder.assertInvariants();
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
await t.step("non-2xx response", async () => {
|
|
1372
|
+
const recorder = recordHops(() => ({ status: 404, body: "not found" }));
|
|
1373
|
+
deepStrictEqual(
|
|
1374
|
+
await lookupResourceWithDependencies("acct:johndoe@example.com", {}, {
|
|
1375
|
+
openHop: recorder.openHop,
|
|
1376
|
+
}),
|
|
1377
|
+
null,
|
|
1378
|
+
);
|
|
1379
|
+
deepStrictEqual(recorder.events, [
|
|
1380
|
+
`open ${first}`,
|
|
1381
|
+
`fetch ${first} #1`,
|
|
1382
|
+
"cancel #1",
|
|
1383
|
+
`close ${first}`,
|
|
1384
|
+
]);
|
|
1385
|
+
recorder.assertInvariants();
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
await t.step("too many redirections", async () => {
|
|
1389
|
+
const recorder = recordHops(() => ({
|
|
1390
|
+
status: 302,
|
|
1391
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1392
|
+
body: "redirecting",
|
|
1393
|
+
}));
|
|
1394
|
+
deepStrictEqual(
|
|
1395
|
+
await lookupResourceWithDependencies("acct:johndoe@example.com", {
|
|
1396
|
+
maxRedirection: 1,
|
|
1397
|
+
}, { openHop: recorder.openHop }),
|
|
1398
|
+
null,
|
|
1399
|
+
);
|
|
1400
|
+
deepStrictEqual(recorder.events, [
|
|
1401
|
+
`open ${first}`,
|
|
1402
|
+
`fetch ${first} #1`,
|
|
1403
|
+
"cancel #1",
|
|
1404
|
+
`close ${first}`,
|
|
1405
|
+
`open ${second}`,
|
|
1406
|
+
`fetch ${second} #2`,
|
|
1407
|
+
"cancel #2",
|
|
1408
|
+
`close ${second}`,
|
|
1409
|
+
]);
|
|
1410
|
+
recorder.assertInvariants();
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
await t.step("failed redirect body cancellation", async () => {
|
|
1414
|
+
const recorder = recordHops((url) =>
|
|
1415
|
+
url.pathname === "/.well-known/webfinger"
|
|
1416
|
+
? {
|
|
1417
|
+
status: 302,
|
|
1418
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1419
|
+
body: "redirecting",
|
|
1420
|
+
cancelFails: true,
|
|
1421
|
+
}
|
|
1422
|
+
: { body: JSON.stringify(descriptor) }
|
|
1423
|
+
);
|
|
1424
|
+
const records: LogRecord[] = [];
|
|
1425
|
+
await configure({
|
|
1426
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
1427
|
+
loggers: [
|
|
1428
|
+
{ category: "fedify", sinks: ["buffer"], lowestLevel: "debug" },
|
|
1429
|
+
{ category: ["logtape", "meta"], sinks: [] },
|
|
1430
|
+
],
|
|
1431
|
+
reset: true,
|
|
1432
|
+
});
|
|
1433
|
+
try {
|
|
1434
|
+
deepStrictEqual(
|
|
1435
|
+
await lookupResourceWithDependencies("acct:johndoe@example.com", {}, {
|
|
1436
|
+
openHop: recorder.openHop,
|
|
1437
|
+
}),
|
|
1438
|
+
descriptor,
|
|
1439
|
+
);
|
|
1440
|
+
const failures = records.filter((r) =>
|
|
1441
|
+
r.rawMessage ===
|
|
1442
|
+
"Failed to cancel the response body from {url}: {error}"
|
|
1443
|
+
);
|
|
1444
|
+
deepStrictEqual(
|
|
1445
|
+
failures.map((r) => [r.level, r.properties.url]),
|
|
1446
|
+
[["debug", first]],
|
|
1447
|
+
);
|
|
1448
|
+
const { error } = failures[0].properties;
|
|
1449
|
+
ok(error instanceof Error);
|
|
1450
|
+
deepStrictEqual(error.message, "Cancellation failed");
|
|
1451
|
+
} finally {
|
|
1452
|
+
await reset();
|
|
1453
|
+
}
|
|
1454
|
+
deepStrictEqual(recorder.events, [
|
|
1455
|
+
`open ${first}`,
|
|
1456
|
+
`fetch ${first} #1`,
|
|
1457
|
+
"cancel #1",
|
|
1458
|
+
`close ${first}`,
|
|
1459
|
+
`open ${second}`,
|
|
1460
|
+
`fetch ${second} #2`,
|
|
1461
|
+
"consumed #2",
|
|
1462
|
+
`close ${second}`,
|
|
1463
|
+
]);
|
|
1464
|
+
recorder.assertInvariants();
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
await t.step("never-settling body cancellation", async () => {
|
|
1468
|
+
const recorder = recordHops((url) =>
|
|
1469
|
+
url.pathname === "/.well-known/webfinger"
|
|
1470
|
+
? {
|
|
1471
|
+
status: 302,
|
|
1472
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1473
|
+
body: "redirecting",
|
|
1474
|
+
cancelNeverSettles: true,
|
|
1475
|
+
}
|
|
1476
|
+
: { status: 404, body: "not found", cancelNeverSettles: true }
|
|
1477
|
+
);
|
|
1478
|
+
deepStrictEqual(
|
|
1479
|
+
await settleWithin(
|
|
1480
|
+
lookupResourceWithDependencies("acct:johndoe@example.com", {}, {
|
|
1481
|
+
openHop: recorder.openHop,
|
|
1482
|
+
}),
|
|
1483
|
+
),
|
|
1484
|
+
null,
|
|
1485
|
+
);
|
|
1486
|
+
deepStrictEqual(recorder.events, [
|
|
1487
|
+
`open ${first}`,
|
|
1488
|
+
`fetch ${first} #1`,
|
|
1489
|
+
"cancel #1",
|
|
1490
|
+
`close ${first}`,
|
|
1491
|
+
`open ${second}`,
|
|
1492
|
+
`fetch ${second} #2`,
|
|
1493
|
+
"cancel #2",
|
|
1494
|
+
`close ${second}`,
|
|
1495
|
+
]);
|
|
1496
|
+
recorder.assertInvariants();
|
|
1497
|
+
});
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Rejects if the given promise does not settle within the timeout, so that
|
|
1502
|
+
* a hang fails the test instead of blocking it.
|
|
1503
|
+
*/
|
|
1504
|
+
async function settleWithin<T>(promise: Promise<T>, ms = 5000): Promise<T> {
|
|
1505
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1506
|
+
try {
|
|
1507
|
+
return await Promise.race([
|
|
1508
|
+
promise,
|
|
1509
|
+
new Promise<never>((_, reject) => {
|
|
1510
|
+
timer = setTimeout(
|
|
1511
|
+
() => reject(new Error(`Did not settle within ${ms} ms.`)),
|
|
1512
|
+
ms,
|
|
1513
|
+
);
|
|
1514
|
+
}),
|
|
1515
|
+
]);
|
|
1516
|
+
} finally {
|
|
1517
|
+
clearTimeout(timer);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// A body that a real session cannot cancel to completion: fetch-mock returns
|
|
1522
|
+
// a clone (a tee) of a Response instance, and the cancellation of a branch of
|
|
1523
|
+
// a tee settles only after the other branch is cancelled too. Its source's
|
|
1524
|
+
// cancel callback never settles either:
|
|
1525
|
+
function neverCancellableResponse(init: ResponseInit): Response {
|
|
1526
|
+
return new Response(
|
|
1527
|
+
new ReadableStream<Uint8Array>({
|
|
1528
|
+
start(controller) {
|
|
1529
|
+
controller.enqueue(new TextEncoder().encode("never cancelled"));
|
|
1530
|
+
},
|
|
1531
|
+
cancel: () => new Promise(() => {}),
|
|
1532
|
+
}),
|
|
1533
|
+
init,
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
test("lookupWebFinger() does not wait for cancelling response bodies", async (t) => {
|
|
1538
|
+
const descriptor: ResourceDescriptor = {
|
|
1539
|
+
subject: "acct:johndoe@example.com",
|
|
1540
|
+
links: [],
|
|
1541
|
+
};
|
|
1542
|
+
fetchMock.mockGlobal();
|
|
1543
|
+
try {
|
|
1544
|
+
await t.step("redirect", async () => {
|
|
1545
|
+
fetchMock.removeRoutes();
|
|
1546
|
+
fetchMock.get(
|
|
1547
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
1548
|
+
() =>
|
|
1549
|
+
neverCancellableResponse({
|
|
1550
|
+
status: 302,
|
|
1551
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1552
|
+
}),
|
|
1553
|
+
);
|
|
1554
|
+
fetchMock.get(
|
|
1555
|
+
"https://example.com/.well-known/webfinger2",
|
|
1556
|
+
() =>
|
|
1557
|
+
new Response(JSON.stringify(descriptor), {
|
|
1558
|
+
headers: { "Content-Type": "application/jrd+json" },
|
|
1559
|
+
}),
|
|
1560
|
+
);
|
|
1561
|
+
deepStrictEqual(
|
|
1562
|
+
await settleWithin(lookupDirect("acct:johndoe@example.com")),
|
|
1563
|
+
descriptor,
|
|
1564
|
+
);
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
await t.step("non-2xx response", async () => {
|
|
1568
|
+
fetchMock.removeRoutes();
|
|
1569
|
+
fetchMock.get(
|
|
1570
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
1571
|
+
() => neverCancellableResponse({ status: 404 }),
|
|
1572
|
+
);
|
|
1573
|
+
deepStrictEqual(
|
|
1574
|
+
await settleWithin(lookupDirect("acct:johndoe@example.com")),
|
|
1575
|
+
null,
|
|
1576
|
+
);
|
|
1577
|
+
});
|
|
1578
|
+
} finally {
|
|
1579
|
+
fetchMock.hardReset();
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
nodeTest(
|
|
1584
|
+
"lookupWebFinger() records network_error for rejected hop addresses",
|
|
1585
|
+
async () => {
|
|
1586
|
+
for (
|
|
1587
|
+
const lookup of [
|
|
1588
|
+
() => Promise.reject(new Error("DNS unavailable")),
|
|
1589
|
+
() => Promise.resolve([]),
|
|
1590
|
+
() => Promise.resolve([{ address: "127.0.0.1", family: 4 }]),
|
|
1591
|
+
]
|
|
1592
|
+
) {
|
|
1593
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1594
|
+
const result = await lookupWebFingerWithDependencies(
|
|
1595
|
+
"acct:alice@dns-failure.invalid",
|
|
1596
|
+
{ meterProvider },
|
|
1597
|
+
{ openHop: (url) => openPublicFetch(url, { lookup }) },
|
|
1598
|
+
);
|
|
1599
|
+
deepStrictEqual(result, null);
|
|
1600
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1601
|
+
ok(counter != null);
|
|
1602
|
+
deepStrictEqual(
|
|
1603
|
+
counter.attributes["webfinger.lookup.result"],
|
|
1604
|
+
"network_error",
|
|
1605
|
+
);
|
|
1606
|
+
deepStrictEqual(
|
|
1607
|
+
counter.attributes["activitypub.remote.host"],
|
|
1608
|
+
"dns-failure.invalid",
|
|
1609
|
+
);
|
|
1610
|
+
deepStrictEqual("http.response.status_code" in counter.attributes, false);
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
);
|
|
1614
|
+
|
|
1615
|
+
nodeTest(
|
|
1616
|
+
"WebFinger diagnostics classify non-object JSON after closing the hop",
|
|
1617
|
+
async () => {
|
|
1618
|
+
for (const body of ["null", "[]", "42", '"text"', "true"]) {
|
|
1619
|
+
const hops = recordHops(() => ({ body }));
|
|
1620
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1621
|
+
const outcome = await lookupWebFingerWithDependencies(
|
|
1622
|
+
"acct:alice@example.com",
|
|
1623
|
+
{ withDiagnostics: true, meterProvider },
|
|
1624
|
+
{ openHop: hops.openHop },
|
|
1625
|
+
);
|
|
1626
|
+
deepStrictEqual(outcome, {
|
|
1627
|
+
resource: null,
|
|
1628
|
+
result: "invalid",
|
|
1629
|
+
statusCode: 200,
|
|
1630
|
+
remoteHost: "example.com",
|
|
1631
|
+
});
|
|
1632
|
+
deepStrictEqual(
|
|
1633
|
+
hops.events.at(-1),
|
|
1634
|
+
"close https://example.com/.well-known/webfinger?resource=acct%3Aalice%40example.com",
|
|
1635
|
+
);
|
|
1636
|
+
hops.assertInvariants();
|
|
1637
|
+
deepStrictEqual(
|
|
1638
|
+
recorder.getMeasurement("webfinger.lookup")
|
|
1639
|
+
?.attributes["webfinger.lookup.result"],
|
|
1640
|
+
"invalid",
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
},
|
|
1644
|
+
);
|