@fedify/webfinger 2.3.0-dev.1137 → 2.3.0-dev.1150
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 +506 -143
- package/dist/lookup.test.js +508 -145
- package/dist/mod.cjs +148 -18
- package/dist/mod.d.cts +33 -2
- package/dist/mod.d.ts +33 -2
- package/dist/mod.js +148 -18
- package/package.json +2 -2
- package/src/lookup.test.ts +773 -237
- package/src/lookup.ts +253 -22
package/src/lookup.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { test } from "@fedify/fixture";
|
|
1
|
+
import { createTestMeterProvider, test } from "@fedify/fixture";
|
|
2
2
|
import { withTimeout } from "es-toolkit";
|
|
3
3
|
import fetchMock from "fetch-mock";
|
|
4
|
-
import { deepStrictEqual } from "node:assert/strict";
|
|
4
|
+
import { deepStrictEqual, ok } from "node:assert/strict";
|
|
5
5
|
import type { ResourceDescriptor } from "./jrd.ts";
|
|
6
6
|
import { lookupWebFinger } from "./lookup.ts";
|
|
7
7
|
|
|
@@ -15,6 +15,21 @@ test({
|
|
|
15
15
|
deepStrictEqual(await lookupWebFinger(new URL("acct:johndoe")), null);
|
|
16
16
|
deepStrictEqual(await lookupWebFinger("acct:johndoe@"), null);
|
|
17
17
|
deepStrictEqual(await lookupWebFinger(new URL("acct:johndoe@")), null);
|
|
18
|
+
// Per RFC 7565, the acct: authority is bare `host`: no path,
|
|
19
|
+
// query, or fragment is allowed. Reject such inputs rather than
|
|
20
|
+
// forwarding them to a remote WebFinger lookup.
|
|
21
|
+
deepStrictEqual(
|
|
22
|
+
await lookupWebFinger("acct:johndoe@example.com/exploit"),
|
|
23
|
+
null,
|
|
24
|
+
);
|
|
25
|
+
deepStrictEqual(
|
|
26
|
+
await lookupWebFinger("acct:johndoe@example.com?x=1"),
|
|
27
|
+
null,
|
|
28
|
+
);
|
|
29
|
+
deepStrictEqual(
|
|
30
|
+
await lookupWebFinger("acct:johndoe@example.com#frag"),
|
|
31
|
+
null,
|
|
32
|
+
);
|
|
18
33
|
});
|
|
19
34
|
|
|
20
35
|
await t.step("connection refused", async () => {
|
|
@@ -29,81 +44,117 @@ test({
|
|
|
29
44
|
});
|
|
30
45
|
|
|
31
46
|
fetchMock.spyGlobal();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
// Wrap the rest of the outer test in try/finally so the global
|
|
48
|
+
// `fetch` spy is torn down even if a t.step assertion below
|
|
49
|
+
// throws. Matches the cleanup pattern of the metrics test
|
|
50
|
+
// further down in this file.
|
|
51
|
+
try {
|
|
52
|
+
fetchMock.get(
|
|
53
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
54
|
+
{ status: 404 },
|
|
55
|
+
);
|
|
36
56
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
await t.step("not found", async () => {
|
|
58
|
+
deepStrictEqual(
|
|
59
|
+
await lookupWebFinger("acct:johndoe@example.com"),
|
|
60
|
+
null,
|
|
61
|
+
);
|
|
62
|
+
deepStrictEqual(await lookupWebFinger("https://example.com/foo"), null);
|
|
63
|
+
});
|
|
41
64
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
const expected: ResourceDescriptor = {
|
|
66
|
+
subject: "acct:johndoe@example.com",
|
|
67
|
+
links: [],
|
|
68
|
+
};
|
|
69
|
+
fetchMock.removeRoutes();
|
|
70
|
+
fetchMock.get(
|
|
71
|
+
"https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com",
|
|
72
|
+
{ body: expected },
|
|
73
|
+
);
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
await t.step("acct", async () => {
|
|
76
|
+
deepStrictEqual(
|
|
77
|
+
await lookupWebFinger("acct:johndoe@example.com"),
|
|
78
|
+
expected,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const expected2: ResourceDescriptor = {
|
|
83
|
+
subject: "https://example.com/foo",
|
|
84
|
+
links: [],
|
|
85
|
+
};
|
|
86
|
+
fetchMock.removeRoutes();
|
|
87
|
+
fetchMock.get(
|
|
88
|
+
"https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo",
|
|
89
|
+
{ body: expected2 },
|
|
56
90
|
);
|
|
57
|
-
});
|
|
58
91
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo",
|
|
66
|
-
{ body: expected2 },
|
|
67
|
-
);
|
|
92
|
+
await t.step("https", async () => {
|
|
93
|
+
deepStrictEqual(
|
|
94
|
+
await lookupWebFinger("https://example.com/foo"),
|
|
95
|
+
expected2,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
68
98
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
99
|
+
const mailtoExpected: ResourceDescriptor = {
|
|
100
|
+
subject: "mailto:juliet@example.com",
|
|
101
|
+
links: [],
|
|
102
|
+
};
|
|
103
|
+
fetchMock.removeRoutes();
|
|
104
|
+
fetchMock.get(
|
|
105
|
+
"https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com",
|
|
106
|
+
{ body: mailtoExpected },
|
|
73
107
|
);
|
|
74
|
-
});
|
|
75
108
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
await t.step("mailto", async () => {
|
|
110
|
+
// RFC 7033 permits any URI as a WebFinger resource, and RFC 7565
|
|
111
|
+
// explicitly references `mailto:` as an example. The opaque-path
|
|
112
|
+
// host extraction (after the last `@`) applies to `mailto:` just
|
|
113
|
+
// like `acct:`.
|
|
114
|
+
deepStrictEqual(
|
|
115
|
+
await lookupWebFinger("mailto:juliet@example.com"),
|
|
116
|
+
mailtoExpected,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
81
119
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
120
|
+
const mailtoQueryExpected: ResourceDescriptor = {
|
|
121
|
+
subject: "mailto:juliet@example.com?subject=Hi",
|
|
122
|
+
links: [],
|
|
123
|
+
};
|
|
124
|
+
fetchMock.removeRoutes();
|
|
125
|
+
fetchMock.get(
|
|
126
|
+
"https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com%3Fsubject%3DHi",
|
|
127
|
+
{ body: mailtoQueryExpected },
|
|
128
|
+
);
|
|
85
129
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
],
|
|
98
|
-
},
|
|
99
|
-
);
|
|
130
|
+
await t.step("mailto with hfields", async () => {
|
|
131
|
+
// RFC 6068 §2 allows `mailto:` URIs to carry `?hfields=...`
|
|
132
|
+
// header fields and fragment identifiers. Unlike `acct:`,
|
|
133
|
+
// those components are part of the grammar, so the lookup
|
|
134
|
+
// must accept them and forward the full resource URI to the
|
|
135
|
+
// WebFinger endpoint.
|
|
136
|
+
deepStrictEqual(
|
|
137
|
+
await lookupWebFinger("mailto:juliet@example.com?subject=Hi"),
|
|
138
|
+
mailtoQueryExpected,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
100
141
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
142
|
+
fetchMock.removeRoutes();
|
|
143
|
+
fetchMock.get(
|
|
144
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
145
|
+
{ body: "not json" },
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await t.step("invalid response", async () => {
|
|
149
|
+
deepStrictEqual(
|
|
150
|
+
await lookupWebFinger("acct:johndoe@example.com"),
|
|
151
|
+
null,
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
fetchMock.removeRoutes();
|
|
156
|
+
fetchMock.get(
|
|
157
|
+
"begin:https://localhost/.well-known/webfinger?",
|
|
107
158
|
{
|
|
108
159
|
subject: "acct:test@localhost",
|
|
109
160
|
links: [
|
|
@@ -115,217 +166,702 @@ test({
|
|
|
115
166
|
],
|
|
116
167
|
},
|
|
117
168
|
);
|
|
118
|
-
});
|
|
119
169
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
170
|
+
await t.step("private address", async () => {
|
|
171
|
+
deepStrictEqual(await lookupWebFinger("acct:test@localhost"), null);
|
|
172
|
+
deepStrictEqual(
|
|
173
|
+
await lookupWebFinger("acct:test@localhost", {
|
|
174
|
+
allowPrivateAddress: true,
|
|
175
|
+
}),
|
|
176
|
+
{
|
|
177
|
+
subject: "acct:test@localhost",
|
|
178
|
+
links: [
|
|
179
|
+
{
|
|
180
|
+
rel: "self",
|
|
181
|
+
type: "application/activity+json",
|
|
182
|
+
href: "https://localhost/actor",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
});
|
|
132
188
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
189
|
+
fetchMock.removeRoutes();
|
|
190
|
+
fetchMock.get(
|
|
191
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
192
|
+
{
|
|
193
|
+
status: 302,
|
|
194
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
fetchMock.get(
|
|
198
|
+
"begin:https://example.com/.well-known/webfinger2",
|
|
199
|
+
{ body: expected },
|
|
137
200
|
);
|
|
138
|
-
});
|
|
139
201
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
},
|
|
147
|
-
);
|
|
202
|
+
await t.step("redirection", async () => {
|
|
203
|
+
deepStrictEqual(
|
|
204
|
+
await lookupWebFinger("acct:johndoe@example.com"),
|
|
205
|
+
expected,
|
|
206
|
+
);
|
|
207
|
+
});
|
|
148
208
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
209
|
+
fetchMock.removeRoutes();
|
|
210
|
+
fetchMock.get(
|
|
211
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
212
|
+
{
|
|
213
|
+
status: 302,
|
|
214
|
+
headers: { Location: "/.well-known/webfinger" },
|
|
215
|
+
},
|
|
153
216
|
);
|
|
154
|
-
deepStrictEqual(result, null);
|
|
155
|
-
});
|
|
156
217
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
);
|
|
218
|
+
await t.step("infinite redirection", async () => {
|
|
219
|
+
const result = await withTimeout(
|
|
220
|
+
() => lookupWebFinger("acct:johndoe@example.com"),
|
|
221
|
+
2000,
|
|
222
|
+
);
|
|
223
|
+
deepStrictEqual(result, null);
|
|
224
|
+
});
|
|
165
225
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
226
|
+
fetchMock.removeRoutes();
|
|
227
|
+
fetchMock.get(
|
|
228
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
229
|
+
{
|
|
230
|
+
status: 302,
|
|
231
|
+
headers: { Location: "ftp://example.com/" },
|
|
232
|
+
},
|
|
233
|
+
);
|
|
169
234
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
},
|
|
177
|
-
);
|
|
235
|
+
await t.step("redirection to different protocol", async () => {
|
|
236
|
+
deepStrictEqual(
|
|
237
|
+
await lookupWebFinger("acct:johndoe@example.com"),
|
|
238
|
+
null,
|
|
239
|
+
);
|
|
240
|
+
});
|
|
178
241
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
242
|
+
fetchMock.removeRoutes();
|
|
243
|
+
fetchMock.get(
|
|
244
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
245
|
+
{
|
|
246
|
+
status: 302,
|
|
247
|
+
headers: { Location: "https://localhost/" },
|
|
248
|
+
},
|
|
249
|
+
);
|
|
182
250
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
251
|
+
await t.step("redirection to private address", async () => {
|
|
252
|
+
deepStrictEqual(
|
|
253
|
+
await lookupWebFinger("acct:johndoe@example.com"),
|
|
254
|
+
null,
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
fetchMock.removeRoutes();
|
|
259
|
+
let redirectCount = 0;
|
|
260
|
+
fetchMock.get(
|
|
261
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
262
|
+
() => {
|
|
263
|
+
redirectCount++;
|
|
264
|
+
if (redirectCount < 3) {
|
|
265
|
+
return {
|
|
266
|
+
status: 302,
|
|
267
|
+
headers: {
|
|
268
|
+
Location: `/.well-known/webfinger?redirect=${redirectCount}`,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return { body: expected };
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
await t.step("custom maxRedirection", async () => {
|
|
277
|
+
// Test with maxRedirection: 1 (should fail; mock has 2 redirects)
|
|
278
|
+
redirectCount = 0;
|
|
279
|
+
deepStrictEqual(
|
|
280
|
+
await lookupWebFinger("acct:johndoe@example.com", {
|
|
281
|
+
maxRedirection: 1,
|
|
282
|
+
}),
|
|
283
|
+
null,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Test with maxRedirection: 2 (should succeed; mock has exactly 2
|
|
287
|
+
// redirects, and `maxRedirection: N` follows up to N redirects)
|
|
288
|
+
redirectCount = 0;
|
|
289
|
+
deepStrictEqual(
|
|
290
|
+
await lookupWebFinger("acct:johndoe@example.com", {
|
|
291
|
+
maxRedirection: 2,
|
|
292
|
+
}),
|
|
293
|
+
expected,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Test with maxRedirection: 3 (should succeed)
|
|
297
|
+
redirectCount = 0;
|
|
298
|
+
deepStrictEqual(
|
|
299
|
+
await lookupWebFinger("acct:johndoe@example.com", {
|
|
300
|
+
maxRedirection: 3,
|
|
301
|
+
}),
|
|
302
|
+
expected,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Test with default maxRedirection: 5 (should succeed)
|
|
306
|
+
redirectCount = 0;
|
|
307
|
+
deepStrictEqual(
|
|
308
|
+
await lookupWebFinger("acct:johndoe@example.com"),
|
|
309
|
+
expected,
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Regression: `maxRedirection: 1` must allow exactly one 302 to be
|
|
314
|
+
// followed. An earlier implementation incremented the counter before
|
|
315
|
+
// the `>=` check, so `maxRedirection: 1` rejected the first redirect
|
|
316
|
+
// instead of following it. The expected semantics is "follow up to
|
|
317
|
+
// N redirects".
|
|
318
|
+
await t.step(
|
|
319
|
+
"maxRedirection: 1 follows exactly one redirect",
|
|
320
|
+
async () => {
|
|
321
|
+
// Mock with a single redirect: 302 → 200. Under the corrected
|
|
322
|
+
// semantics, `maxRedirection: 1` follows it and reaches the body.
|
|
323
|
+
fetchMock.removeRoutes();
|
|
324
|
+
let count = 0;
|
|
325
|
+
fetchMock.get(
|
|
326
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
327
|
+
() => {
|
|
328
|
+
count++;
|
|
329
|
+
return count < 2
|
|
330
|
+
? {
|
|
331
|
+
status: 302,
|
|
332
|
+
headers: { Location: "/.well-known/webfinger?after=1" },
|
|
333
|
+
}
|
|
334
|
+
: { body: expected };
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
deepStrictEqual(
|
|
338
|
+
await lookupWebFinger("acct:johndoe@example.com", {
|
|
339
|
+
maxRedirection: 1,
|
|
340
|
+
}),
|
|
341
|
+
expected,
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// Mock with two redirects. `maxRedirection: 1` rejects the
|
|
345
|
+
// second redirect.
|
|
346
|
+
fetchMock.removeRoutes();
|
|
347
|
+
count = 0;
|
|
348
|
+
fetchMock.get(
|
|
349
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
350
|
+
() => {
|
|
351
|
+
count++;
|
|
352
|
+
return count < 3
|
|
353
|
+
? {
|
|
354
|
+
status: 302,
|
|
355
|
+
headers: {
|
|
356
|
+
Location: `/.well-known/webfinger?after=${count}`,
|
|
357
|
+
},
|
|
358
|
+
}
|
|
359
|
+
: { body: expected };
|
|
194
360
|
},
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
361
|
+
);
|
|
362
|
+
deepStrictEqual(
|
|
363
|
+
await lookupWebFinger("acct:johndoe@example.com", {
|
|
364
|
+
maxRedirection: 1,
|
|
365
|
+
}),
|
|
366
|
+
null,
|
|
367
|
+
);
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
fetchMock.removeRoutes();
|
|
372
|
+
fetchMock.get(
|
|
373
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
374
|
+
() =>
|
|
375
|
+
new Promise((resolve) => {
|
|
376
|
+
const timeoutId = setTimeout(() => {
|
|
377
|
+
resolve({ body: expected });
|
|
378
|
+
}, 1000);
|
|
379
|
+
|
|
380
|
+
return () => clearTimeout(timeoutId);
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
await t.step("request cancellation", async () => {
|
|
385
|
+
// Test cancelling a request immediately using AbortController
|
|
386
|
+
const controller = new AbortController();
|
|
387
|
+
const promise = lookupWebFinger("acct:johndoe@example.com", {
|
|
388
|
+
signal: controller.signal,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Abort the request right after starting it
|
|
392
|
+
controller.abort();
|
|
393
|
+
deepStrictEqual(await promise, null);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
fetchMock.removeRoutes();
|
|
397
|
+
let redirectCount2 = 0;
|
|
398
|
+
fetchMock.get(
|
|
399
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
400
|
+
() => {
|
|
401
|
+
redirectCount2++;
|
|
402
|
+
if (redirectCount2 === 1) {
|
|
403
|
+
return {
|
|
404
|
+
status: 302,
|
|
405
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return new Promise((resolve) => {
|
|
409
|
+
const timeoutId = setTimeout(() => {
|
|
410
|
+
resolve({ body: expected });
|
|
411
|
+
}, 1000);
|
|
412
|
+
|
|
413
|
+
return () => clearTimeout(timeoutId);
|
|
414
|
+
});
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
await t.step("cancellation during redirection", async () => {
|
|
419
|
+
// Test cancelling a request during redirection process
|
|
420
|
+
const controller = new AbortController();
|
|
421
|
+
const promise = lookupWebFinger("acct:johndoe@example.com", {
|
|
422
|
+
signal: controller.signal,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Cancel during the delayed second request after redirection
|
|
426
|
+
setTimeout(() => controller.abort(), 100);
|
|
427
|
+
deepStrictEqual(await promise, null);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
fetchMock.removeRoutes();
|
|
431
|
+
fetchMock.get(
|
|
432
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
433
|
+
() =>
|
|
434
|
+
new Promise((resolve) => {
|
|
435
|
+
const timeoutId = setTimeout(() => {
|
|
436
|
+
resolve({ body: expected });
|
|
437
|
+
}, 500);
|
|
438
|
+
|
|
439
|
+
return () => clearTimeout(timeoutId);
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
await t.step("cancellation with immediate abort", async () => {
|
|
444
|
+
// Test starting a request with an already aborted AbortController
|
|
445
|
+
const controller = new AbortController();
|
|
446
|
+
controller.abort();
|
|
447
|
+
|
|
448
|
+
// Use a signal that was already aborted before starting the request
|
|
449
|
+
const result = await lookupWebFinger("acct:johndoe@example.com", {
|
|
450
|
+
signal: controller.signal,
|
|
451
|
+
});
|
|
452
|
+
deepStrictEqual(result, null);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
fetchMock.removeRoutes();
|
|
456
|
+
fetchMock.get(
|
|
457
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
458
|
+
{ body: expected },
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
await t.step("successful request with signal", async () => {
|
|
462
|
+
// Test successful request with a normal AbortController signal
|
|
463
|
+
const controller = new AbortController();
|
|
464
|
+
const result = await lookupWebFinger("acct:johndoe@example.com", {
|
|
465
|
+
signal: controller.signal,
|
|
466
|
+
});
|
|
467
|
+
deepStrictEqual(result, expected);
|
|
468
|
+
});
|
|
469
|
+
} finally {
|
|
470
|
+
fetchMock.removeRoutes();
|
|
471
|
+
fetchMock.hardReset();
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
477
|
+
sanitizeOps: false,
|
|
478
|
+
sanitizeResources: false,
|
|
479
|
+
}, async (t) => {
|
|
480
|
+
fetchMock.spyGlobal();
|
|
481
|
+
try {
|
|
482
|
+
const expected: ResourceDescriptor = {
|
|
483
|
+
subject: "acct:johndoe@example.com",
|
|
484
|
+
links: [],
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
await t.step(
|
|
488
|
+
"records result=found for a successful acct lookup",
|
|
489
|
+
async () => {
|
|
490
|
+
fetchMock.removeRoutes();
|
|
491
|
+
fetchMock.get(
|
|
492
|
+
"https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com",
|
|
493
|
+
{ body: expected },
|
|
494
|
+
);
|
|
495
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
496
|
+
const result = await lookupWebFinger("acct:johndoe@example.com", {
|
|
497
|
+
meterProvider,
|
|
498
|
+
});
|
|
499
|
+
deepStrictEqual(result, expected);
|
|
500
|
+
|
|
501
|
+
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
502
|
+
deepStrictEqual(counters.length, 1);
|
|
503
|
+
deepStrictEqual(counters[0].type, "counter");
|
|
504
|
+
deepStrictEqual(counters[0].value, 1);
|
|
505
|
+
deepStrictEqual(
|
|
506
|
+
counters[0].attributes["webfinger.lookup.result"],
|
|
507
|
+
"found",
|
|
508
|
+
);
|
|
509
|
+
deepStrictEqual(
|
|
510
|
+
counters[0].attributes["webfinger.resource.scheme"],
|
|
511
|
+
"acct",
|
|
512
|
+
);
|
|
513
|
+
deepStrictEqual(
|
|
514
|
+
counters[0].attributes["activitypub.remote.host"],
|
|
515
|
+
"example.com",
|
|
516
|
+
);
|
|
517
|
+
deepStrictEqual(
|
|
518
|
+
counters[0].attributes["http.response.status_code"],
|
|
519
|
+
200,
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const durations = recorder.getMeasurements("webfinger.lookup.duration");
|
|
523
|
+
deepStrictEqual(durations.length, 1);
|
|
524
|
+
deepStrictEqual(durations[0].type, "histogram");
|
|
525
|
+
deepStrictEqual(
|
|
526
|
+
durations[0].attributes["webfinger.lookup.result"],
|
|
527
|
+
"found",
|
|
528
|
+
);
|
|
529
|
+
deepStrictEqual(
|
|
530
|
+
durations[0].attributes["webfinger.resource.scheme"],
|
|
531
|
+
"acct",
|
|
532
|
+
);
|
|
533
|
+
ok(typeof durations[0].value === "number" && durations[0].value >= 0);
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
await t.step(
|
|
538
|
+
"records scheme=https for an https resource lookup",
|
|
539
|
+
async () => {
|
|
540
|
+
fetchMock.removeRoutes();
|
|
541
|
+
fetchMock.get(
|
|
542
|
+
"https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo",
|
|
543
|
+
{ body: { subject: "https://example.com/foo", links: [] } },
|
|
544
|
+
);
|
|
545
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
546
|
+
await lookupWebFinger("https://example.com/foo", { meterProvider });
|
|
547
|
+
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
548
|
+
deepStrictEqual(counters.length, 1);
|
|
549
|
+
deepStrictEqual(
|
|
550
|
+
counters[0].attributes["webfinger.resource.scheme"],
|
|
551
|
+
"https",
|
|
552
|
+
);
|
|
553
|
+
deepStrictEqual(
|
|
554
|
+
counters[0].attributes["webfinger.lookup.result"],
|
|
555
|
+
"found",
|
|
556
|
+
);
|
|
198
557
|
},
|
|
199
558
|
);
|
|
200
559
|
|
|
201
|
-
await t.step("
|
|
202
|
-
|
|
203
|
-
|
|
560
|
+
await t.step("records result=not_found with status 404", async () => {
|
|
561
|
+
fetchMock.removeRoutes();
|
|
562
|
+
fetchMock.get(
|
|
563
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
564
|
+
{ status: 404 },
|
|
565
|
+
);
|
|
566
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
567
|
+
const result = await lookupWebFinger("acct:johndoe@example.com", {
|
|
568
|
+
meterProvider,
|
|
569
|
+
});
|
|
570
|
+
deepStrictEqual(result, null);
|
|
571
|
+
|
|
572
|
+
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
573
|
+
deepStrictEqual(counters.length, 1);
|
|
204
574
|
deepStrictEqual(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
575
|
+
counters[0].attributes["webfinger.lookup.result"],
|
|
576
|
+
"not_found",
|
|
577
|
+
);
|
|
578
|
+
deepStrictEqual(
|
|
579
|
+
counters[0].attributes["http.response.status_code"],
|
|
580
|
+
404,
|
|
581
|
+
);
|
|
582
|
+
deepStrictEqual(
|
|
583
|
+
counters[0].attributes["activitypub.remote.host"],
|
|
584
|
+
"example.com",
|
|
209
585
|
);
|
|
210
586
|
|
|
211
|
-
|
|
212
|
-
|
|
587
|
+
const durations = recorder.getMeasurements("webfinger.lookup.duration");
|
|
588
|
+
deepStrictEqual(durations.length, 1);
|
|
213
589
|
deepStrictEqual(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}),
|
|
217
|
-
expected,
|
|
590
|
+
durations[0].attributes["webfinger.lookup.result"],
|
|
591
|
+
"not_found",
|
|
218
592
|
);
|
|
593
|
+
});
|
|
219
594
|
|
|
220
|
-
|
|
221
|
-
|
|
595
|
+
await t.step("records result=not_found with status 410", async () => {
|
|
596
|
+
fetchMock.removeRoutes();
|
|
597
|
+
fetchMock.get(
|
|
598
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
599
|
+
{ status: 410 },
|
|
600
|
+
);
|
|
601
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
602
|
+
await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
|
|
603
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
604
|
+
ok(counter != null);
|
|
222
605
|
deepStrictEqual(
|
|
223
|
-
|
|
224
|
-
|
|
606
|
+
counter.attributes["webfinger.lookup.result"],
|
|
607
|
+
"not_found",
|
|
225
608
|
);
|
|
609
|
+
deepStrictEqual(counter.attributes["http.response.status_code"], 410);
|
|
226
610
|
});
|
|
227
611
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
612
|
+
await t.step(
|
|
613
|
+
"records result=error for non-2xx, non-404/410 HTTP responses",
|
|
614
|
+
async () => {
|
|
615
|
+
fetchMock.removeRoutes();
|
|
616
|
+
fetchMock.get(
|
|
617
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
618
|
+
{ status: 500 },
|
|
619
|
+
);
|
|
620
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
621
|
+
await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
|
|
622
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
623
|
+
ok(counter != null);
|
|
624
|
+
deepStrictEqual(
|
|
625
|
+
counter.attributes["webfinger.lookup.result"],
|
|
626
|
+
"error",
|
|
627
|
+
);
|
|
628
|
+
deepStrictEqual(counter.attributes["http.response.status_code"], 500);
|
|
629
|
+
},
|
|
239
630
|
);
|
|
240
631
|
|
|
241
|
-
await t.step(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
redirectCount2++;
|
|
259
|
-
if (redirectCount2 === 1) {
|
|
260
|
-
return {
|
|
261
|
-
status: 302,
|
|
262
|
-
headers: { Location: "/.well-known/webfinger2" },
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
return new Promise((resolve) => {
|
|
266
|
-
const timeoutId = setTimeout(() => {
|
|
267
|
-
resolve({ body: expected });
|
|
268
|
-
}, 1000);
|
|
269
|
-
|
|
270
|
-
return () => clearTimeout(timeoutId);
|
|
271
|
-
});
|
|
632
|
+
await t.step(
|
|
633
|
+
"records result=invalid for malformed JSON bodies",
|
|
634
|
+
async () => {
|
|
635
|
+
fetchMock.removeRoutes();
|
|
636
|
+
fetchMock.get(
|
|
637
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
638
|
+
{ body: "not json" },
|
|
639
|
+
);
|
|
640
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
641
|
+
await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
|
|
642
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
643
|
+
ok(counter != null);
|
|
644
|
+
deepStrictEqual(
|
|
645
|
+
counter.attributes["webfinger.lookup.result"],
|
|
646
|
+
"invalid",
|
|
647
|
+
);
|
|
648
|
+
deepStrictEqual(counter.attributes["http.response.status_code"], 200);
|
|
272
649
|
},
|
|
273
650
|
);
|
|
274
651
|
|
|
275
|
-
await t.step(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
652
|
+
await t.step(
|
|
653
|
+
"records result=network_error when fetch never reaches the remote",
|
|
654
|
+
async () => {
|
|
655
|
+
fetchMock.removeRoutes();
|
|
656
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
657
|
+
const result = await lookupWebFinger(
|
|
658
|
+
"acct:johndoe@fedify-test.internal",
|
|
659
|
+
{ meterProvider },
|
|
660
|
+
);
|
|
661
|
+
deepStrictEqual(result, null);
|
|
662
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
663
|
+
ok(counter != null);
|
|
664
|
+
deepStrictEqual(
|
|
665
|
+
counter.attributes["webfinger.lookup.result"],
|
|
666
|
+
"network_error",
|
|
667
|
+
);
|
|
668
|
+
deepStrictEqual(
|
|
669
|
+
"http.response.status_code" in counter.attributes,
|
|
670
|
+
false,
|
|
671
|
+
"no HTTP response means no status code attribute",
|
|
672
|
+
);
|
|
673
|
+
deepStrictEqual(
|
|
674
|
+
counter.attributes["activitypub.remote.host"],
|
|
675
|
+
"fedify-test.internal",
|
|
676
|
+
);
|
|
677
|
+
},
|
|
678
|
+
);
|
|
281
679
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
680
|
+
await t.step(
|
|
681
|
+
"records result=invalid for malformed acct: resources",
|
|
682
|
+
async () => {
|
|
683
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
684
|
+
const result = await lookupWebFinger("acct:johndoe", { meterProvider });
|
|
685
|
+
deepStrictEqual(result, null);
|
|
686
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
687
|
+
ok(counter != null);
|
|
688
|
+
deepStrictEqual(
|
|
689
|
+
counter.attributes["webfinger.lookup.result"],
|
|
690
|
+
"invalid",
|
|
691
|
+
);
|
|
692
|
+
deepStrictEqual(
|
|
693
|
+
counter.attributes["webfinger.resource.scheme"],
|
|
694
|
+
"acct",
|
|
695
|
+
);
|
|
696
|
+
deepStrictEqual(
|
|
697
|
+
"activitypub.remote.host" in counter.attributes,
|
|
698
|
+
false,
|
|
699
|
+
"a malformed acct resource has no usable remote host",
|
|
700
|
+
);
|
|
701
|
+
},
|
|
702
|
+
);
|
|
286
703
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
704
|
+
await t.step(
|
|
705
|
+
"records result=invalid when the redirect chain exceeds maxRedirection",
|
|
706
|
+
async () => {
|
|
707
|
+
fetchMock.removeRoutes();
|
|
708
|
+
// The redirect Location drops the original `?resource=...` query
|
|
709
|
+
// string, so the second hop's URL no longer contains a `?`. The
|
|
710
|
+
// route pattern omits the trailing `?` so it still matches.
|
|
711
|
+
fetchMock.get(
|
|
712
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
713
|
+
{
|
|
714
|
+
status: 302,
|
|
715
|
+
headers: { Location: "/.well-known/webfinger" },
|
|
716
|
+
},
|
|
717
|
+
);
|
|
718
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
719
|
+
const result = await withTimeout(
|
|
720
|
+
() =>
|
|
721
|
+
lookupWebFinger("acct:johndoe@example.com", {
|
|
722
|
+
meterProvider,
|
|
723
|
+
maxRedirection: 3,
|
|
724
|
+
}),
|
|
725
|
+
2000,
|
|
726
|
+
);
|
|
727
|
+
deepStrictEqual(result, null);
|
|
728
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
729
|
+
ok(counter != null);
|
|
730
|
+
deepStrictEqual(
|
|
731
|
+
counter.attributes["webfinger.lookup.result"],
|
|
732
|
+
"invalid",
|
|
733
|
+
);
|
|
734
|
+
deepStrictEqual(counter.attributes["http.response.status_code"], 302);
|
|
735
|
+
deepStrictEqual(
|
|
736
|
+
counter.attributes["activitypub.remote.host"],
|
|
737
|
+
"example.com",
|
|
738
|
+
);
|
|
739
|
+
},
|
|
298
740
|
);
|
|
299
741
|
|
|
300
|
-
await t.step(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
742
|
+
await t.step(
|
|
743
|
+
"records result=invalid for cross-protocol redirects",
|
|
744
|
+
async () => {
|
|
745
|
+
fetchMock.removeRoutes();
|
|
746
|
+
fetchMock.get(
|
|
747
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
748
|
+
{
|
|
749
|
+
status: 302,
|
|
750
|
+
headers: { Location: "ftp://example.com/" },
|
|
751
|
+
},
|
|
752
|
+
);
|
|
753
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
754
|
+
await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
|
|
755
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
756
|
+
ok(counter != null);
|
|
757
|
+
deepStrictEqual(
|
|
758
|
+
counter.attributes["webfinger.lookup.result"],
|
|
759
|
+
"invalid",
|
|
760
|
+
);
|
|
761
|
+
deepStrictEqual(counter.attributes["http.response.status_code"], 302);
|
|
762
|
+
},
|
|
763
|
+
);
|
|
304
764
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
765
|
+
await t.step(
|
|
766
|
+
"records result=network_error when a redirect points to a private address",
|
|
767
|
+
async () => {
|
|
768
|
+
fetchMock.removeRoutes();
|
|
769
|
+
fetchMock.get(
|
|
770
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
771
|
+
{
|
|
772
|
+
status: 302,
|
|
773
|
+
headers: { Location: "https://localhost/" },
|
|
774
|
+
},
|
|
775
|
+
);
|
|
776
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
777
|
+
await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
|
|
778
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
779
|
+
ok(counter != null);
|
|
780
|
+
deepStrictEqual(
|
|
781
|
+
counter.attributes["webfinger.lookup.result"],
|
|
782
|
+
"network_error",
|
|
783
|
+
);
|
|
784
|
+
deepStrictEqual(
|
|
785
|
+
counter.attributes["activitypub.remote.host"],
|
|
786
|
+
"localhost",
|
|
787
|
+
"remote.host reflects the latest URL we attempted, even after a redirect",
|
|
788
|
+
);
|
|
789
|
+
},
|
|
790
|
+
);
|
|
311
791
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
792
|
+
await t.step(
|
|
793
|
+
"records result=invalid for malformed Location headers",
|
|
794
|
+
async () => {
|
|
795
|
+
fetchMock.removeRoutes();
|
|
796
|
+
fetchMock.get(
|
|
797
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
798
|
+
{
|
|
799
|
+
status: 302,
|
|
800
|
+
headers: { Location: "http://[bad" },
|
|
801
|
+
},
|
|
802
|
+
);
|
|
803
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
804
|
+
await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
|
|
805
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
806
|
+
ok(counter != null);
|
|
807
|
+
deepStrictEqual(
|
|
808
|
+
counter.attributes["webfinger.lookup.result"],
|
|
809
|
+
"invalid",
|
|
810
|
+
);
|
|
811
|
+
deepStrictEqual(counter.attributes["http.response.status_code"], 302);
|
|
812
|
+
deepStrictEqual(
|
|
813
|
+
counter.attributes["activitypub.remote.host"],
|
|
814
|
+
"example.com",
|
|
815
|
+
);
|
|
816
|
+
},
|
|
316
817
|
);
|
|
317
818
|
|
|
318
|
-
await t.step(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
819
|
+
await t.step(
|
|
820
|
+
"buckets unknown resource schemes as 'other' to keep metric cardinality bounded",
|
|
821
|
+
async () => {
|
|
822
|
+
// Lookups whose redirect chain ends on an unusual scheme (or a
|
|
823
|
+
// resource the caller passes with a non-fediverse scheme) must
|
|
824
|
+
// not leak that scheme into the metric attribute.
|
|
825
|
+
fetchMock.removeRoutes();
|
|
826
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
827
|
+
// `ssh:` is not a WebFinger scheme; lookupWebFingerInternal will
|
|
828
|
+
// attempt to build a host from the URL, fail, and return null.
|
|
829
|
+
// The metric still records, and its scheme attribute must be
|
|
830
|
+
// bucketed as `other`.
|
|
831
|
+
await lookupWebFinger("ssh://example.com/foo", { meterProvider });
|
|
832
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
833
|
+
ok(counter != null);
|
|
834
|
+
deepStrictEqual(
|
|
835
|
+
counter.attributes["webfinger.resource.scheme"],
|
|
836
|
+
"other",
|
|
837
|
+
);
|
|
838
|
+
},
|
|
839
|
+
);
|
|
326
840
|
|
|
841
|
+
await t.step(
|
|
842
|
+
"omits measurements when no meterProvider is provided",
|
|
843
|
+
async () => {
|
|
844
|
+
fetchMock.removeRoutes();
|
|
845
|
+
fetchMock.get(
|
|
846
|
+
"https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com",
|
|
847
|
+
{ body: expected },
|
|
848
|
+
);
|
|
849
|
+
const [_unused, recorder] = createTestMeterProvider();
|
|
850
|
+
await lookupWebFinger("acct:johndoe@example.com");
|
|
851
|
+
deepStrictEqual(
|
|
852
|
+
recorder.getMeasurements("webfinger.lookup").length,
|
|
853
|
+
0,
|
|
854
|
+
);
|
|
855
|
+
deepStrictEqual(
|
|
856
|
+
recorder.getMeasurements("webfinger.lookup.duration").length,
|
|
857
|
+
0,
|
|
858
|
+
);
|
|
859
|
+
},
|
|
860
|
+
);
|
|
861
|
+
} finally {
|
|
862
|
+
fetchMock.removeRoutes();
|
|
327
863
|
fetchMock.hardReset();
|
|
328
|
-
}
|
|
864
|
+
}
|
|
329
865
|
});
|
|
330
866
|
|
|
331
867
|
// cSpell: ignore johndoe
|