@fedify/webfinger 2.3.14 → 2.3.15
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 +558 -218
- package/dist/lookup.test.js +559 -218
- package/dist/mod.cjs +224 -153
- package/dist/mod.d.cts +27 -1
- package/dist/mod.d.ts +28 -2
- package/dist/mod.js +225 -155
- package/package.json +3 -3
- package/src/lookup-impl.ts +324 -0
- package/src/lookup.test.ts +515 -69
- package/src/lookup.ts +20 -219
package/src/lookup.test.ts
CHANGED
|
@@ -4,44 +4,112 @@ 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
|
+
type LookupWebFingerOptions,
|
|
10
|
+
lookupWebFingerWithDependencies,
|
|
11
|
+
} from "./lookup.ts";
|
|
12
|
+
import {
|
|
13
|
+
type LookupWebFingerDependencies,
|
|
14
|
+
lookupWebFingerInternal,
|
|
15
|
+
} from "./lookup-impl.ts";
|
|
16
|
+
import {
|
|
17
|
+
openDirectFetch,
|
|
18
|
+
openPublicFetch,
|
|
19
|
+
type PublicFetchSession,
|
|
20
|
+
} from "@fedify/vocab-runtime/internal";
|
|
21
|
+
import type { LookupAddress } from "node:dns";
|
|
22
|
+
import { test as nodeTest } from "node:test";
|
|
23
|
+
|
|
24
|
+
// Inject only the transport boundary, so the public tracing and metrics
|
|
25
|
+
// wrapper remains under test while fetch-mock sees the original hostname.
|
|
26
|
+
function lookupMock(
|
|
27
|
+
resource: URL | string,
|
|
28
|
+
options: LookupWebFingerOptions = {},
|
|
29
|
+
): Promise<ResourceDescriptor | null> {
|
|
30
|
+
if (resource.toString().includes("fedify-test.internal")) {
|
|
31
|
+
return lookupWebFingerWithDependencies(resource, options, {
|
|
32
|
+
openHop: (url) =>
|
|
33
|
+
Promise.resolve({
|
|
34
|
+
url: new URL(url),
|
|
35
|
+
fetch: () => Promise.reject(new TypeError("Connection refused")),
|
|
36
|
+
close: () => Promise.resolve(),
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return lookupWebFingerWithDependencies(resource, options, {
|
|
41
|
+
openHop: options.allowPrivateAddress === true
|
|
42
|
+
? openDirectFetch
|
|
43
|
+
: openExampleDirectly,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function lookupDirect(
|
|
48
|
+
resource: URL | string,
|
|
49
|
+
options?: LookupWebFingerOptions,
|
|
50
|
+
): Promise<ResourceDescriptor | null> {
|
|
51
|
+
return lookupWebFingerWithDependencies(resource, options, {
|
|
52
|
+
openHop: openDirectFetch,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function lookupResolving(
|
|
57
|
+
addresses: readonly LookupAddress[],
|
|
58
|
+
): (hostname: string) => Promise<readonly LookupAddress[]> {
|
|
59
|
+
return () => Promise.resolve(addresses);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function openExampleDirectly(url: string): Promise<PublicFetchSession> {
|
|
63
|
+
return new URL(url).hostname === "example.com"
|
|
64
|
+
? openDirectFetch(url)
|
|
65
|
+
: openPublicFetch(url, {
|
|
66
|
+
lookup: lookupResolving([{ address: "93.184.215.14", family: 4 }]),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function lookupResourceWithDependencies(
|
|
71
|
+
resource: URL | string,
|
|
72
|
+
options: LookupWebFingerOptions,
|
|
73
|
+
deps: LookupWebFingerDependencies,
|
|
74
|
+
): Promise<ResourceDescriptor | null> {
|
|
75
|
+
return (await lookupWebFingerInternal(resource, options, deps)).resource;
|
|
76
|
+
}
|
|
10
77
|
|
|
11
78
|
test({
|
|
12
79
|
name: "lookupWebFinger()",
|
|
13
80
|
sanitizeOps: false,
|
|
14
81
|
sanitizeResources: false,
|
|
15
82
|
async fn(t) {
|
|
83
|
+
const responseTimers: ReturnType<typeof setTimeout>[] = [];
|
|
16
84
|
await t.step("invalid resource", async () => {
|
|
17
|
-
deepStrictEqual(await
|
|
18
|
-
deepStrictEqual(await
|
|
19
|
-
deepStrictEqual(await
|
|
20
|
-
deepStrictEqual(await
|
|
85
|
+
deepStrictEqual(await lookupMock("acct:johndoe"), null);
|
|
86
|
+
deepStrictEqual(await lookupMock(new URL("acct:johndoe")), null);
|
|
87
|
+
deepStrictEqual(await lookupMock("acct:johndoe@"), null);
|
|
88
|
+
deepStrictEqual(await lookupMock(new URL("acct:johndoe@")), null);
|
|
21
89
|
// Per RFC 7565, the acct: authority is bare `host`: no path,
|
|
22
90
|
// query, or fragment is allowed. Reject such inputs rather than
|
|
23
91
|
// forwarding them to a remote WebFinger lookup.
|
|
24
92
|
deepStrictEqual(
|
|
25
|
-
await
|
|
93
|
+
await lookupMock("acct:johndoe@example.com/exploit"),
|
|
26
94
|
null,
|
|
27
95
|
);
|
|
28
96
|
deepStrictEqual(
|
|
29
|
-
await
|
|
97
|
+
await lookupMock("acct:johndoe@example.com?x=1"),
|
|
30
98
|
null,
|
|
31
99
|
);
|
|
32
100
|
deepStrictEqual(
|
|
33
|
-
await
|
|
101
|
+
await lookupMock("acct:johndoe@example.com#frag"),
|
|
34
102
|
null,
|
|
35
103
|
);
|
|
36
104
|
});
|
|
37
105
|
|
|
38
106
|
await t.step("connection refused", async () => {
|
|
39
107
|
deepStrictEqual(
|
|
40
|
-
await
|
|
108
|
+
await lookupMock("acct:johndoe@fedify-test.internal"),
|
|
41
109
|
null,
|
|
42
110
|
);
|
|
43
111
|
deepStrictEqual(
|
|
44
|
-
await
|
|
112
|
+
await lookupMock("https://fedify-test.internal/foo"),
|
|
45
113
|
null,
|
|
46
114
|
);
|
|
47
115
|
});
|
|
@@ -59,10 +127,10 @@ test({
|
|
|
59
127
|
|
|
60
128
|
await t.step("not found", async () => {
|
|
61
129
|
deepStrictEqual(
|
|
62
|
-
await
|
|
130
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
63
131
|
null,
|
|
64
132
|
);
|
|
65
|
-
deepStrictEqual(await
|
|
133
|
+
deepStrictEqual(await lookupMock("https://example.com/foo"), null);
|
|
66
134
|
});
|
|
67
135
|
|
|
68
136
|
const expected: ResourceDescriptor = {
|
|
@@ -77,7 +145,7 @@ test({
|
|
|
77
145
|
|
|
78
146
|
await t.step("acct", async () => {
|
|
79
147
|
deepStrictEqual(
|
|
80
|
-
await
|
|
148
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
81
149
|
expected,
|
|
82
150
|
);
|
|
83
151
|
});
|
|
@@ -94,7 +162,7 @@ test({
|
|
|
94
162
|
|
|
95
163
|
await t.step("https", async () => {
|
|
96
164
|
deepStrictEqual(
|
|
97
|
-
await
|
|
165
|
+
await lookupMock("https://example.com/foo"),
|
|
98
166
|
expected2,
|
|
99
167
|
);
|
|
100
168
|
});
|
|
@@ -115,7 +183,7 @@ test({
|
|
|
115
183
|
// host extraction (after the last `@`) applies to `mailto:` just
|
|
116
184
|
// like `acct:`.
|
|
117
185
|
deepStrictEqual(
|
|
118
|
-
await
|
|
186
|
+
await lookupMock("mailto:juliet@example.com"),
|
|
119
187
|
mailtoExpected,
|
|
120
188
|
);
|
|
121
189
|
});
|
|
@@ -137,7 +205,7 @@ test({
|
|
|
137
205
|
// must accept them and forward the full resource URI to the
|
|
138
206
|
// WebFinger endpoint.
|
|
139
207
|
deepStrictEqual(
|
|
140
|
-
await
|
|
208
|
+
await lookupMock("mailto:juliet@example.com?subject=Hi"),
|
|
141
209
|
mailtoQueryExpected,
|
|
142
210
|
);
|
|
143
211
|
});
|
|
@@ -150,7 +218,7 @@ test({
|
|
|
150
218
|
|
|
151
219
|
await t.step("invalid response", async () => {
|
|
152
220
|
deepStrictEqual(
|
|
153
|
-
await
|
|
221
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
154
222
|
null,
|
|
155
223
|
);
|
|
156
224
|
});
|
|
@@ -171,9 +239,9 @@ test({
|
|
|
171
239
|
);
|
|
172
240
|
|
|
173
241
|
await t.step("private address", async () => {
|
|
174
|
-
deepStrictEqual(await
|
|
242
|
+
deepStrictEqual(await lookupMock("acct:test@localhost"), null);
|
|
175
243
|
deepStrictEqual(
|
|
176
|
-
await
|
|
244
|
+
await lookupMock("acct:test@localhost", {
|
|
177
245
|
allowPrivateAddress: true,
|
|
178
246
|
}),
|
|
179
247
|
{
|
|
@@ -204,7 +272,7 @@ test({
|
|
|
204
272
|
|
|
205
273
|
await t.step("redirection", async () => {
|
|
206
274
|
deepStrictEqual(
|
|
207
|
-
await
|
|
275
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
208
276
|
expected,
|
|
209
277
|
);
|
|
210
278
|
});
|
|
@@ -220,7 +288,7 @@ test({
|
|
|
220
288
|
|
|
221
289
|
await t.step("infinite redirection", async () => {
|
|
222
290
|
const result = await withTimeout(
|
|
223
|
-
() =>
|
|
291
|
+
() => lookupMock("acct:johndoe@example.com"),
|
|
224
292
|
2000,
|
|
225
293
|
);
|
|
226
294
|
deepStrictEqual(result, null);
|
|
@@ -237,7 +305,7 @@ test({
|
|
|
237
305
|
|
|
238
306
|
await t.step("redirection to different protocol", async () => {
|
|
239
307
|
deepStrictEqual(
|
|
240
|
-
await
|
|
308
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
241
309
|
null,
|
|
242
310
|
);
|
|
243
311
|
});
|
|
@@ -253,7 +321,7 @@ test({
|
|
|
253
321
|
|
|
254
322
|
await t.step("redirection to private address", async () => {
|
|
255
323
|
deepStrictEqual(
|
|
256
|
-
await
|
|
324
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
257
325
|
null,
|
|
258
326
|
);
|
|
259
327
|
});
|
|
@@ -280,7 +348,7 @@ test({
|
|
|
280
348
|
// Test with maxRedirection: 1 (should fail; mock has 2 redirects)
|
|
281
349
|
redirectCount = 0;
|
|
282
350
|
deepStrictEqual(
|
|
283
|
-
await
|
|
351
|
+
await lookupMock("acct:johndoe@example.com", {
|
|
284
352
|
maxRedirection: 1,
|
|
285
353
|
}),
|
|
286
354
|
null,
|
|
@@ -290,7 +358,7 @@ test({
|
|
|
290
358
|
// redirects, and `maxRedirection: N` follows up to N redirects)
|
|
291
359
|
redirectCount = 0;
|
|
292
360
|
deepStrictEqual(
|
|
293
|
-
await
|
|
361
|
+
await lookupMock("acct:johndoe@example.com", {
|
|
294
362
|
maxRedirection: 2,
|
|
295
363
|
}),
|
|
296
364
|
expected,
|
|
@@ -299,7 +367,7 @@ test({
|
|
|
299
367
|
// Test with maxRedirection: 3 (should succeed)
|
|
300
368
|
redirectCount = 0;
|
|
301
369
|
deepStrictEqual(
|
|
302
|
-
await
|
|
370
|
+
await lookupMock("acct:johndoe@example.com", {
|
|
303
371
|
maxRedirection: 3,
|
|
304
372
|
}),
|
|
305
373
|
expected,
|
|
@@ -308,7 +376,7 @@ test({
|
|
|
308
376
|
// Test with default maxRedirection: 5 (should succeed)
|
|
309
377
|
redirectCount = 0;
|
|
310
378
|
deepStrictEqual(
|
|
311
|
-
await
|
|
379
|
+
await lookupMock("acct:johndoe@example.com"),
|
|
312
380
|
expected,
|
|
313
381
|
);
|
|
314
382
|
});
|
|
@@ -338,7 +406,7 @@ test({
|
|
|
338
406
|
},
|
|
339
407
|
);
|
|
340
408
|
deepStrictEqual(
|
|
341
|
-
await
|
|
409
|
+
await lookupMock("acct:johndoe@example.com", {
|
|
342
410
|
maxRedirection: 1,
|
|
343
411
|
}),
|
|
344
412
|
expected,
|
|
@@ -363,7 +431,7 @@ test({
|
|
|
363
431
|
},
|
|
364
432
|
);
|
|
365
433
|
deepStrictEqual(
|
|
366
|
-
await
|
|
434
|
+
await lookupMock("acct:johndoe@example.com", {
|
|
367
435
|
maxRedirection: 1,
|
|
368
436
|
}),
|
|
369
437
|
null,
|
|
@@ -380,14 +448,14 @@ test({
|
|
|
380
448
|
resolve({ body: expected });
|
|
381
449
|
}, 1000);
|
|
382
450
|
|
|
383
|
-
|
|
451
|
+
responseTimers.push(timeoutId);
|
|
384
452
|
}),
|
|
385
453
|
);
|
|
386
454
|
|
|
387
455
|
await t.step("request cancellation", async () => {
|
|
388
456
|
// Test cancelling a request immediately using AbortController
|
|
389
457
|
const controller = new AbortController();
|
|
390
|
-
const promise =
|
|
458
|
+
const promise = lookupMock("acct:johndoe@example.com", {
|
|
391
459
|
signal: controller.signal,
|
|
392
460
|
});
|
|
393
461
|
|
|
@@ -413,7 +481,7 @@ test({
|
|
|
413
481
|
resolve({ body: expected });
|
|
414
482
|
}, 1000);
|
|
415
483
|
|
|
416
|
-
|
|
484
|
+
responseTimers.push(timeoutId);
|
|
417
485
|
});
|
|
418
486
|
},
|
|
419
487
|
);
|
|
@@ -421,7 +489,7 @@ test({
|
|
|
421
489
|
await t.step("cancellation during redirection", async () => {
|
|
422
490
|
// Test cancelling a request during redirection process
|
|
423
491
|
const controller = new AbortController();
|
|
424
|
-
const promise =
|
|
492
|
+
const promise = lookupMock("acct:johndoe@example.com", {
|
|
425
493
|
signal: controller.signal,
|
|
426
494
|
});
|
|
427
495
|
|
|
@@ -439,7 +507,7 @@ test({
|
|
|
439
507
|
resolve({ body: expected });
|
|
440
508
|
}, 500);
|
|
441
509
|
|
|
442
|
-
|
|
510
|
+
responseTimers.push(timeoutId);
|
|
443
511
|
}),
|
|
444
512
|
);
|
|
445
513
|
|
|
@@ -449,7 +517,7 @@ test({
|
|
|
449
517
|
controller.abort();
|
|
450
518
|
|
|
451
519
|
// Use a signal that was already aborted before starting the request
|
|
452
|
-
const result = await
|
|
520
|
+
const result = await lookupMock("acct:johndoe@example.com", {
|
|
453
521
|
signal: controller.signal,
|
|
454
522
|
});
|
|
455
523
|
deepStrictEqual(result, null);
|
|
@@ -464,12 +532,13 @@ test({
|
|
|
464
532
|
await t.step("successful request with signal", async () => {
|
|
465
533
|
// Test successful request with a normal AbortController signal
|
|
466
534
|
const controller = new AbortController();
|
|
467
|
-
const result = await
|
|
535
|
+
const result = await lookupMock("acct:johndoe@example.com", {
|
|
468
536
|
signal: controller.signal,
|
|
469
537
|
});
|
|
470
538
|
deepStrictEqual(result, expected);
|
|
471
539
|
});
|
|
472
540
|
} finally {
|
|
541
|
+
for (const timer of responseTimers) clearTimeout(timer);
|
|
473
542
|
fetchMock.removeRoutes();
|
|
474
543
|
fetchMock.hardReset();
|
|
475
544
|
}
|
|
@@ -496,7 +565,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
496
565
|
{ body: expected },
|
|
497
566
|
);
|
|
498
567
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
499
|
-
const result = await
|
|
568
|
+
const result = await lookupMock("acct:johndoe@example.com", {
|
|
500
569
|
meterProvider,
|
|
501
570
|
});
|
|
502
571
|
deepStrictEqual(result, expected);
|
|
@@ -546,7 +615,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
546
615
|
{ body: { subject: "https://example.com/foo", links: [] } },
|
|
547
616
|
);
|
|
548
617
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
549
|
-
await
|
|
618
|
+
await lookupMock("https://example.com/foo", { meterProvider });
|
|
550
619
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
551
620
|
deepStrictEqual(counters.length, 1);
|
|
552
621
|
deepStrictEqual(
|
|
@@ -569,7 +638,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
569
638
|
{ body: { subject: "https://example.com:8443/foo", links: [] } },
|
|
570
639
|
);
|
|
571
640
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
572
|
-
await
|
|
641
|
+
await lookupMock("https://example.com:8443/foo", {
|
|
573
642
|
meterProvider,
|
|
574
643
|
});
|
|
575
644
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
@@ -588,7 +657,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
588
657
|
{ status: 404 },
|
|
589
658
|
);
|
|
590
659
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
591
|
-
const result = await
|
|
660
|
+
const result = await lookupMock("acct:johndoe@example.com", {
|
|
592
661
|
meterProvider,
|
|
593
662
|
});
|
|
594
663
|
deepStrictEqual(result, null);
|
|
@@ -623,7 +692,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
623
692
|
{ status: 410 },
|
|
624
693
|
);
|
|
625
694
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
626
|
-
await
|
|
695
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
627
696
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
628
697
|
ok(counter != null);
|
|
629
698
|
deepStrictEqual(
|
|
@@ -642,7 +711,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
642
711
|
{ status: 500 },
|
|
643
712
|
);
|
|
644
713
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
645
|
-
await
|
|
714
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
646
715
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
647
716
|
ok(counter != null);
|
|
648
717
|
deepStrictEqual(
|
|
@@ -662,7 +731,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
662
731
|
{ body: "not json" },
|
|
663
732
|
);
|
|
664
733
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
665
|
-
await
|
|
734
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
666
735
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
667
736
|
ok(counter != null);
|
|
668
737
|
deepStrictEqual(
|
|
@@ -678,7 +747,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
678
747
|
async () => {
|
|
679
748
|
fetchMock.removeRoutes();
|
|
680
749
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
681
|
-
const result = await
|
|
750
|
+
const result = await lookupMock(
|
|
682
751
|
"acct:johndoe@fedify-test.internal",
|
|
683
752
|
{ meterProvider },
|
|
684
753
|
);
|
|
@@ -705,7 +774,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
705
774
|
"records result=invalid for malformed acct: resources",
|
|
706
775
|
async () => {
|
|
707
776
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
708
|
-
const result = await
|
|
777
|
+
const result = await lookupMock("acct:johndoe", { meterProvider });
|
|
709
778
|
deepStrictEqual(result, null);
|
|
710
779
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
711
780
|
ok(counter != null);
|
|
@@ -742,7 +811,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
742
811
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
743
812
|
const result = await withTimeout(
|
|
744
813
|
() =>
|
|
745
|
-
|
|
814
|
+
lookupMock("acct:johndoe@example.com", {
|
|
746
815
|
meterProvider,
|
|
747
816
|
maxRedirection: 3,
|
|
748
817
|
}),
|
|
@@ -775,7 +844,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
775
844
|
},
|
|
776
845
|
);
|
|
777
846
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
778
|
-
await
|
|
847
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
779
848
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
780
849
|
ok(counter != null);
|
|
781
850
|
deepStrictEqual(
|
|
@@ -798,7 +867,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
798
867
|
},
|
|
799
868
|
);
|
|
800
869
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
801
|
-
await
|
|
870
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
802
871
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
803
872
|
ok(counter != null);
|
|
804
873
|
deepStrictEqual(
|
|
@@ -825,7 +894,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
825
894
|
},
|
|
826
895
|
);
|
|
827
896
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
828
|
-
await
|
|
897
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
829
898
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
830
899
|
ok(counter != null);
|
|
831
900
|
deepStrictEqual(
|
|
@@ -852,7 +921,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
852
921
|
// attempt to build a host from the URL, fail, and return null.
|
|
853
922
|
// The metric still records, and its scheme attribute must be
|
|
854
923
|
// bucketed as `other`.
|
|
855
|
-
await
|
|
924
|
+
await lookupMock("ssh://example.com/foo", { meterProvider });
|
|
856
925
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
857
926
|
ok(counter != null);
|
|
858
927
|
deepStrictEqual(
|
|
@@ -871,7 +940,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
871
940
|
{ body: expected },
|
|
872
941
|
);
|
|
873
942
|
const [_unused, recorder] = createTestMeterProvider();
|
|
874
|
-
await
|
|
943
|
+
await lookupMock("acct:johndoe@example.com");
|
|
875
944
|
deepStrictEqual(
|
|
876
945
|
recorder.getMeasurements("webfinger.lookup").length,
|
|
877
946
|
0,
|
|
@@ -909,14 +978,14 @@ test("lookupWebFinger() bounds resource descriptors", async () => {
|
|
|
909
978
|
),
|
|
910
979
|
);
|
|
911
980
|
deepStrictEqual(
|
|
912
|
-
await
|
|
981
|
+
await lookupMock("acct:alice@example.com", {
|
|
913
982
|
allowPrivateAddress: true,
|
|
914
983
|
}),
|
|
915
984
|
null,
|
|
916
985
|
);
|
|
917
986
|
oversized = false;
|
|
918
987
|
deepStrictEqual(
|
|
919
|
-
await
|
|
988
|
+
await lookupMock("acct:alice@example.com", {
|
|
920
989
|
allowPrivateAddress: true,
|
|
921
990
|
}),
|
|
922
991
|
{ subject: "acct:alice@example.com" },
|
|
@@ -927,28 +996,18 @@ test("lookupWebFinger() bounds resource descriptors", async () => {
|
|
|
927
996
|
});
|
|
928
997
|
|
|
929
998
|
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
999
|
for (const result of ["throws", "empty", "private"] as const) {
|
|
937
1000
|
await t.step(result, async () => {
|
|
938
|
-
|
|
939
|
-
// node:dns/promises import; see the FIXME there.
|
|
940
|
-
const originalLookup = dns.lookup;
|
|
941
|
-
dns.lookup = (() =>
|
|
1001
|
+
const lookup = () =>
|
|
942
1002
|
result === "throws"
|
|
943
1003
|
? Promise.reject(new Error("Resolver unavailable"))
|
|
944
1004
|
: Promise.resolve(
|
|
945
1005
|
result === "empty" ? [] : [{ address: "127.0.0.1", family: 4 }],
|
|
946
|
-
)
|
|
1006
|
+
);
|
|
947
1007
|
const records: LogRecord[] = [];
|
|
948
1008
|
await configure({
|
|
949
1009
|
sinks: {
|
|
950
|
-
buffer: (record) =>
|
|
951
|
-
records.push(record),
|
|
1010
|
+
buffer: (record) => records.push(record),
|
|
952
1011
|
},
|
|
953
1012
|
loggers: [
|
|
954
1013
|
{ category: "fedify", sinks: ["buffer"], lowestLevel: "debug" },
|
|
@@ -958,7 +1017,13 @@ test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
|
958
1017
|
});
|
|
959
1018
|
try {
|
|
960
1019
|
deepStrictEqual(
|
|
961
|
-
await
|
|
1020
|
+
await lookupResourceWithDependencies(
|
|
1021
|
+
"acct:alice@dns-failure.invalid",
|
|
1022
|
+
{},
|
|
1023
|
+
{
|
|
1024
|
+
openHop: (url) => openPublicFetch(url, { lookup }),
|
|
1025
|
+
},
|
|
1026
|
+
),
|
|
962
1027
|
null,
|
|
963
1028
|
);
|
|
964
1029
|
const failures = records.filter((r) =>
|
|
@@ -984,8 +1049,389 @@ test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
|
984
1049
|
);
|
|
985
1050
|
} finally {
|
|
986
1051
|
await reset();
|
|
987
|
-
dns.lookup = originalLookup;
|
|
988
1052
|
}
|
|
989
1053
|
});
|
|
990
1054
|
}
|
|
991
1055
|
});
|
|
1056
|
+
|
|
1057
|
+
interface FakeResponse {
|
|
1058
|
+
readonly status?: number;
|
|
1059
|
+
readonly headers?: Record<string, string>;
|
|
1060
|
+
readonly body?: string;
|
|
1061
|
+
/** The `url` of the response, as a pinned fetch reports it. */
|
|
1062
|
+
readonly url?: string;
|
|
1063
|
+
/** Makes cancelling the body fail. */
|
|
1064
|
+
readonly cancelFails?: boolean;
|
|
1065
|
+
/** Makes cancelling the body never settle. */
|
|
1066
|
+
readonly cancelNeverSettles?: boolean;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
interface HopRecorder {
|
|
1070
|
+
readonly events: string[];
|
|
1071
|
+
readonly openHop: (url: string) => Promise<PublicFetchSession>;
|
|
1072
|
+
/**
|
|
1073
|
+
* Asserts that every opened session is closed, and that every response
|
|
1074
|
+
* body was consumed or cancelled before its session was closed.
|
|
1075
|
+
*/
|
|
1076
|
+
assertInvariants(): void;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// A fake server that records the lifecycle of each session and response.
|
|
1080
|
+
// Unlike real sessions, closing a fake session never cancels bodies, so that
|
|
1081
|
+
// the caller's own cleanup is observed:
|
|
1082
|
+
function recordHops(respond: (url: URL) => FakeResponse): HopRecorder {
|
|
1083
|
+
const events: string[] = [];
|
|
1084
|
+
const unsettled: string[] = [];
|
|
1085
|
+
let opens = 0;
|
|
1086
|
+
let closes = 0;
|
|
1087
|
+
let responses = 0;
|
|
1088
|
+
return {
|
|
1089
|
+
events,
|
|
1090
|
+
// deno-lint-ignore require-await
|
|
1091
|
+
async openHop(url) {
|
|
1092
|
+
opens++;
|
|
1093
|
+
events.push(`open ${url}`);
|
|
1094
|
+
const states: { id: number; settled: boolean }[] = [];
|
|
1095
|
+
let closed = false;
|
|
1096
|
+
return {
|
|
1097
|
+
url: new URL(url),
|
|
1098
|
+
// deno-lint-ignore require-await
|
|
1099
|
+
async fetch(input) {
|
|
1100
|
+
if (closed) throw new TypeError("The fetch session is closed.");
|
|
1101
|
+
const target = new URL(input instanceof Request ? input.url : input);
|
|
1102
|
+
const id = ++responses;
|
|
1103
|
+
events.push(`fetch ${target.href} #${id}`);
|
|
1104
|
+
const fake = respond(target);
|
|
1105
|
+
const state = { id, settled: false };
|
|
1106
|
+
states.push(state);
|
|
1107
|
+
let sent = false;
|
|
1108
|
+
const body = new ReadableStream<Uint8Array>({
|
|
1109
|
+
pull(controller) {
|
|
1110
|
+
if (!sent && fake.body != null && fake.body !== "") {
|
|
1111
|
+
sent = true;
|
|
1112
|
+
controller.enqueue(new TextEncoder().encode(fake.body));
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
state.settled = true;
|
|
1116
|
+
events.push(`consumed #${id}`);
|
|
1117
|
+
controller.close();
|
|
1118
|
+
},
|
|
1119
|
+
cancel() {
|
|
1120
|
+
state.settled = true;
|
|
1121
|
+
events.push(`cancel #${id}`);
|
|
1122
|
+
if (fake.cancelFails) throw new Error("Cancellation failed");
|
|
1123
|
+
if (fake.cancelNeverSettles) return new Promise(() => {});
|
|
1124
|
+
},
|
|
1125
|
+
}, { highWaterMark: 0 });
|
|
1126
|
+
const response = new Response(body, {
|
|
1127
|
+
status: fake.status ?? 200,
|
|
1128
|
+
headers: fake.headers,
|
|
1129
|
+
});
|
|
1130
|
+
if (fake.url != null) {
|
|
1131
|
+
Object.defineProperty(response, "url", { value: fake.url });
|
|
1132
|
+
}
|
|
1133
|
+
return response;
|
|
1134
|
+
},
|
|
1135
|
+
// deno-lint-ignore require-await
|
|
1136
|
+
async close() {
|
|
1137
|
+
if (closed) return;
|
|
1138
|
+
closed = true;
|
|
1139
|
+
closes++;
|
|
1140
|
+
for (const state of states) {
|
|
1141
|
+
if (!state.settled) unsettled.push(`#${state.id}`);
|
|
1142
|
+
}
|
|
1143
|
+
events.push(`close ${url}`);
|
|
1144
|
+
},
|
|
1145
|
+
};
|
|
1146
|
+
},
|
|
1147
|
+
assertInvariants() {
|
|
1148
|
+
deepStrictEqual(closes, opens);
|
|
1149
|
+
deepStrictEqual(unsettled, []);
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
test("lookupWebFinger() closes each hop after cleaning up its body", async (t) => {
|
|
1155
|
+
const descriptor: ResourceDescriptor = {
|
|
1156
|
+
subject: "acct:johndoe@example.com",
|
|
1157
|
+
links: [],
|
|
1158
|
+
};
|
|
1159
|
+
const first =
|
|
1160
|
+
"https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com";
|
|
1161
|
+
const second = "https://example.com/.well-known/webfinger2";
|
|
1162
|
+
|
|
1163
|
+
await t.step("redirect", async () => {
|
|
1164
|
+
const recorder = recordHops((url) =>
|
|
1165
|
+
url.pathname === "/.well-known/webfinger"
|
|
1166
|
+
? {
|
|
1167
|
+
status: 302,
|
|
1168
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1169
|
+
body: "redirecting",
|
|
1170
|
+
// A pinned response's url has the IP address, which must not be
|
|
1171
|
+
// used as the base of the Location:
|
|
1172
|
+
url: url.href.replace("example.com", "93.184.215.14"),
|
|
1173
|
+
}
|
|
1174
|
+
: { body: JSON.stringify(descriptor) }
|
|
1175
|
+
);
|
|
1176
|
+
deepStrictEqual(
|
|
1177
|
+
await lookupResourceWithDependencies("acct:johndoe@example.com", {}, {
|
|
1178
|
+
openHop: recorder.openHop,
|
|
1179
|
+
}),
|
|
1180
|
+
descriptor,
|
|
1181
|
+
);
|
|
1182
|
+
deepStrictEqual(recorder.events, [
|
|
1183
|
+
`open ${first}`,
|
|
1184
|
+
`fetch ${first} #1`,
|
|
1185
|
+
"cancel #1",
|
|
1186
|
+
`close ${first}`,
|
|
1187
|
+
`open ${second}`,
|
|
1188
|
+
`fetch ${second} #2`,
|
|
1189
|
+
"consumed #2",
|
|
1190
|
+
`close ${second}`,
|
|
1191
|
+
]);
|
|
1192
|
+
recorder.assertInvariants();
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
await t.step("non-2xx response", async () => {
|
|
1196
|
+
const recorder = recordHops(() => ({ status: 404, body: "not found" }));
|
|
1197
|
+
deepStrictEqual(
|
|
1198
|
+
await lookupResourceWithDependencies("acct:johndoe@example.com", {}, {
|
|
1199
|
+
openHop: recorder.openHop,
|
|
1200
|
+
}),
|
|
1201
|
+
null,
|
|
1202
|
+
);
|
|
1203
|
+
deepStrictEqual(recorder.events, [
|
|
1204
|
+
`open ${first}`,
|
|
1205
|
+
`fetch ${first} #1`,
|
|
1206
|
+
"cancel #1",
|
|
1207
|
+
`close ${first}`,
|
|
1208
|
+
]);
|
|
1209
|
+
recorder.assertInvariants();
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
await t.step("too many redirections", async () => {
|
|
1213
|
+
const recorder = recordHops(() => ({
|
|
1214
|
+
status: 302,
|
|
1215
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1216
|
+
body: "redirecting",
|
|
1217
|
+
}));
|
|
1218
|
+
deepStrictEqual(
|
|
1219
|
+
await lookupResourceWithDependencies("acct:johndoe@example.com", {
|
|
1220
|
+
maxRedirection: 1,
|
|
1221
|
+
}, { openHop: recorder.openHop }),
|
|
1222
|
+
null,
|
|
1223
|
+
);
|
|
1224
|
+
deepStrictEqual(recorder.events, [
|
|
1225
|
+
`open ${first}`,
|
|
1226
|
+
`fetch ${first} #1`,
|
|
1227
|
+
"cancel #1",
|
|
1228
|
+
`close ${first}`,
|
|
1229
|
+
`open ${second}`,
|
|
1230
|
+
`fetch ${second} #2`,
|
|
1231
|
+
"cancel #2",
|
|
1232
|
+
`close ${second}`,
|
|
1233
|
+
]);
|
|
1234
|
+
recorder.assertInvariants();
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
await t.step("failed redirect body cancellation", async () => {
|
|
1238
|
+
const recorder = recordHops((url) =>
|
|
1239
|
+
url.pathname === "/.well-known/webfinger"
|
|
1240
|
+
? {
|
|
1241
|
+
status: 302,
|
|
1242
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1243
|
+
body: "redirecting",
|
|
1244
|
+
cancelFails: true,
|
|
1245
|
+
}
|
|
1246
|
+
: { body: JSON.stringify(descriptor) }
|
|
1247
|
+
);
|
|
1248
|
+
const records: LogRecord[] = [];
|
|
1249
|
+
await configure({
|
|
1250
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
1251
|
+
loggers: [
|
|
1252
|
+
{ category: "fedify", sinks: ["buffer"], lowestLevel: "debug" },
|
|
1253
|
+
{ category: ["logtape", "meta"], sinks: [] },
|
|
1254
|
+
],
|
|
1255
|
+
reset: true,
|
|
1256
|
+
});
|
|
1257
|
+
try {
|
|
1258
|
+
deepStrictEqual(
|
|
1259
|
+
await lookupResourceWithDependencies("acct:johndoe@example.com", {}, {
|
|
1260
|
+
openHop: recorder.openHop,
|
|
1261
|
+
}),
|
|
1262
|
+
descriptor,
|
|
1263
|
+
);
|
|
1264
|
+
const failures = records.filter((r) =>
|
|
1265
|
+
r.rawMessage ===
|
|
1266
|
+
"Failed to cancel the response body from {url}: {error}"
|
|
1267
|
+
);
|
|
1268
|
+
deepStrictEqual(
|
|
1269
|
+
failures.map((r) => [r.level, r.properties.url]),
|
|
1270
|
+
[["debug", first]],
|
|
1271
|
+
);
|
|
1272
|
+
const { error } = failures[0].properties;
|
|
1273
|
+
ok(error instanceof Error);
|
|
1274
|
+
deepStrictEqual(error.message, "Cancellation failed");
|
|
1275
|
+
} finally {
|
|
1276
|
+
await reset();
|
|
1277
|
+
}
|
|
1278
|
+
deepStrictEqual(recorder.events, [
|
|
1279
|
+
`open ${first}`,
|
|
1280
|
+
`fetch ${first} #1`,
|
|
1281
|
+
"cancel #1",
|
|
1282
|
+
`close ${first}`,
|
|
1283
|
+
`open ${second}`,
|
|
1284
|
+
`fetch ${second} #2`,
|
|
1285
|
+
"consumed #2",
|
|
1286
|
+
`close ${second}`,
|
|
1287
|
+
]);
|
|
1288
|
+
recorder.assertInvariants();
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
await t.step("never-settling body cancellation", async () => {
|
|
1292
|
+
const recorder = recordHops((url) =>
|
|
1293
|
+
url.pathname === "/.well-known/webfinger"
|
|
1294
|
+
? {
|
|
1295
|
+
status: 302,
|
|
1296
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1297
|
+
body: "redirecting",
|
|
1298
|
+
cancelNeverSettles: true,
|
|
1299
|
+
}
|
|
1300
|
+
: { status: 404, body: "not found", cancelNeverSettles: true }
|
|
1301
|
+
);
|
|
1302
|
+
deepStrictEqual(
|
|
1303
|
+
await settleWithin(
|
|
1304
|
+
lookupResourceWithDependencies("acct:johndoe@example.com", {}, {
|
|
1305
|
+
openHop: recorder.openHop,
|
|
1306
|
+
}),
|
|
1307
|
+
),
|
|
1308
|
+
null,
|
|
1309
|
+
);
|
|
1310
|
+
deepStrictEqual(recorder.events, [
|
|
1311
|
+
`open ${first}`,
|
|
1312
|
+
`fetch ${first} #1`,
|
|
1313
|
+
"cancel #1",
|
|
1314
|
+
`close ${first}`,
|
|
1315
|
+
`open ${second}`,
|
|
1316
|
+
`fetch ${second} #2`,
|
|
1317
|
+
"cancel #2",
|
|
1318
|
+
`close ${second}`,
|
|
1319
|
+
]);
|
|
1320
|
+
recorder.assertInvariants();
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Rejects if the given promise does not settle within the timeout, so that
|
|
1326
|
+
* a hang fails the test instead of blocking it.
|
|
1327
|
+
*/
|
|
1328
|
+
async function settleWithin<T>(promise: Promise<T>, ms = 5000): Promise<T> {
|
|
1329
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1330
|
+
try {
|
|
1331
|
+
return await Promise.race([
|
|
1332
|
+
promise,
|
|
1333
|
+
new Promise<never>((_, reject) => {
|
|
1334
|
+
timer = setTimeout(
|
|
1335
|
+
() => reject(new Error(`Did not settle within ${ms} ms.`)),
|
|
1336
|
+
ms,
|
|
1337
|
+
);
|
|
1338
|
+
}),
|
|
1339
|
+
]);
|
|
1340
|
+
} finally {
|
|
1341
|
+
clearTimeout(timer);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// A body that a real session cannot cancel to completion: fetch-mock returns
|
|
1346
|
+
// a clone (a tee) of a Response instance, and the cancellation of a branch of
|
|
1347
|
+
// a tee settles only after the other branch is cancelled too. Its source's
|
|
1348
|
+
// cancel callback never settles either:
|
|
1349
|
+
function neverCancellableResponse(init: ResponseInit): Response {
|
|
1350
|
+
return new Response(
|
|
1351
|
+
new ReadableStream<Uint8Array>({
|
|
1352
|
+
start(controller) {
|
|
1353
|
+
controller.enqueue(new TextEncoder().encode("never cancelled"));
|
|
1354
|
+
},
|
|
1355
|
+
cancel: () => new Promise(() => {}),
|
|
1356
|
+
}),
|
|
1357
|
+
init,
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
test("lookupWebFinger() does not wait for cancelling response bodies", async (t) => {
|
|
1362
|
+
const descriptor: ResourceDescriptor = {
|
|
1363
|
+
subject: "acct:johndoe@example.com",
|
|
1364
|
+
links: [],
|
|
1365
|
+
};
|
|
1366
|
+
fetchMock.mockGlobal();
|
|
1367
|
+
try {
|
|
1368
|
+
await t.step("redirect", async () => {
|
|
1369
|
+
fetchMock.removeRoutes();
|
|
1370
|
+
fetchMock.get(
|
|
1371
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
1372
|
+
() =>
|
|
1373
|
+
neverCancellableResponse({
|
|
1374
|
+
status: 302,
|
|
1375
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1376
|
+
}),
|
|
1377
|
+
);
|
|
1378
|
+
fetchMock.get(
|
|
1379
|
+
"https://example.com/.well-known/webfinger2",
|
|
1380
|
+
() =>
|
|
1381
|
+
new Response(JSON.stringify(descriptor), {
|
|
1382
|
+
headers: { "Content-Type": "application/jrd+json" },
|
|
1383
|
+
}),
|
|
1384
|
+
);
|
|
1385
|
+
deepStrictEqual(
|
|
1386
|
+
await settleWithin(lookupDirect("acct:johndoe@example.com")),
|
|
1387
|
+
descriptor,
|
|
1388
|
+
);
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
await t.step("non-2xx response", async () => {
|
|
1392
|
+
fetchMock.removeRoutes();
|
|
1393
|
+
fetchMock.get(
|
|
1394
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
1395
|
+
() => neverCancellableResponse({ status: 404 }),
|
|
1396
|
+
);
|
|
1397
|
+
deepStrictEqual(
|
|
1398
|
+
await settleWithin(lookupDirect("acct:johndoe@example.com")),
|
|
1399
|
+
null,
|
|
1400
|
+
);
|
|
1401
|
+
});
|
|
1402
|
+
} finally {
|
|
1403
|
+
fetchMock.hardReset();
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
nodeTest(
|
|
1408
|
+
"lookupWebFinger() records network_error for rejected hop addresses",
|
|
1409
|
+
async () => {
|
|
1410
|
+
for (
|
|
1411
|
+
const lookup of [
|
|
1412
|
+
() => Promise.reject(new Error("DNS unavailable")),
|
|
1413
|
+
() => Promise.resolve([]),
|
|
1414
|
+
() => Promise.resolve([{ address: "127.0.0.1", family: 4 }]),
|
|
1415
|
+
]
|
|
1416
|
+
) {
|
|
1417
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1418
|
+
const result = await lookupWebFingerWithDependencies(
|
|
1419
|
+
"acct:alice@dns-failure.invalid",
|
|
1420
|
+
{ meterProvider },
|
|
1421
|
+
{ openHop: (url) => openPublicFetch(url, { lookup }) },
|
|
1422
|
+
);
|
|
1423
|
+
deepStrictEqual(result, null);
|
|
1424
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1425
|
+
ok(counter != null);
|
|
1426
|
+
deepStrictEqual(
|
|
1427
|
+
counter.attributes["webfinger.lookup.result"],
|
|
1428
|
+
"network_error",
|
|
1429
|
+
);
|
|
1430
|
+
deepStrictEqual(
|
|
1431
|
+
counter.attributes["activitypub.remote.host"],
|
|
1432
|
+
"dns-failure.invalid",
|
|
1433
|
+
);
|
|
1434
|
+
deepStrictEqual("http.response.status_code" in counter.attributes, false);
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
);
|