@fedify/testing 2.4.0-pr.936.41 → 2.5.0-dev.2271
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/README.md +27 -0
- package/dist/mod.cjs +586 -173
- package/dist/mod.d.cts +72 -6
- package/dist/mod.d.ts +72 -6
- package/dist/mod.js +476 -71
- package/package.json +5 -3
package/dist/mod.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import { hasProofLike, hasSignatureLike } from "@fedify/fedify/sig";
|
|
3
|
-
import { Activity, CryptographicKey, Multikey, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
3
|
+
import { Activity, CryptographicKey, Multikey, PUBLIC_COLLECTION, Tombstone, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
4
|
+
import { arePortableUrisEqual, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getFe34Origin, parseDigestMultibase, parseHashlink, parseIri } from "@fedify/vocab-runtime";
|
|
4
5
|
import { RouterError } from "@fedify/fedify/federation";
|
|
5
6
|
import { delay } from "es-toolkit";
|
|
6
7
|
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
@@ -37,7 +38,7 @@ const noopMeterProvider$1 = { getMeter: () => ({
|
|
|
37
38
|
removeBatchObservableCallback: () => void 0
|
|
38
39
|
}) };
|
|
39
40
|
function createContext(values) {
|
|
40
|
-
const { federation, url = new URL("http://example.com/"), canonicalOrigin, data, documentLoader, contextLoader, meterProvider, tracerProvider, clone, getNodeInfoUri, getActorUri, getObjectUri, getCollectionUri, getOutboxUri, getMediaUploaderUri, getInboxUri, getFollowingUri, getFollowersUri, getLikedUri, getFeaturedUri, getFeaturedTagsUri, parseUri, getActorKeyPairs, getDocumentLoader, lookupObject: lookupObject$1, traverseCollection: traverseCollection$1, lookupNodeInfo, lookupWebFinger, sendActivity, routeActivity, enqueueTask, enqueueTaskMany } = values;
|
|
41
|
+
const { federation, url = new URL("http://example.com/"), canonicalOrigin, data, documentLoader, contextLoader, verifyPortableObject, meterProvider, tracerProvider, clone, getNodeInfoUri, getActorUri, getPortableActorUri, getObjectUri, getPortableObjectUri, getPortableInboxUri, getPortableOutboxUri, getPortableFollowingUri, getPortableFollowersUri, getPortableLikedUri, getPortableFeaturedUri, getPortableFeaturedTagsUri, getPortableCollectionUri, getCollectionUri, getOutboxUri, getMediaUploaderUri, getInboxUri, getFollowingUri, getFollowersUri, getLikedUri, getFeaturedUri, getFeaturedTagsUri, parseUri, getActorKeyPairs, getDocumentLoader, lookupObject: lookupObject$1, traverseCollection: traverseCollection$1, lookupNodeInfo, lookupWebFinger, sendActivity, routeActivity, enqueueTask, enqueueTaskMany } = values;
|
|
41
42
|
function throwRouterError() {
|
|
42
43
|
throw new RouterError("Not implemented");
|
|
43
44
|
}
|
|
@@ -50,6 +51,7 @@ function createContext(values) {
|
|
|
50
51
|
hostname: url.hostname,
|
|
51
52
|
documentLoader: documentLoader ?? mockDocumentLoader,
|
|
52
53
|
contextLoader: contextLoader ?? mockDocumentLoader,
|
|
54
|
+
...verifyPortableObject == null ? {} : { verifyPortableObject },
|
|
53
55
|
meterProvider: meterProvider ?? noopMeterProvider$1,
|
|
54
56
|
tracerProvider: tracerProvider ?? noopTracerProvider$1,
|
|
55
57
|
clone: clone ?? ((data) => createContext({
|
|
@@ -58,7 +60,17 @@ function createContext(values) {
|
|
|
58
60
|
})),
|
|
59
61
|
getNodeInfoUri: getNodeInfoUri ?? throwRouterError,
|
|
60
62
|
getActorUri: getActorUri ?? throwRouterError,
|
|
63
|
+
getPortableActorUri: getPortableActorUri ?? throwRouterError,
|
|
61
64
|
getObjectUri: getObjectUri ?? throwRouterError,
|
|
65
|
+
getPortableObjectUri: getPortableObjectUri ?? throwRouterError,
|
|
66
|
+
getPortableInboxUri: getPortableInboxUri ?? throwRouterError,
|
|
67
|
+
getPortableOutboxUri: getPortableOutboxUri ?? throwRouterError,
|
|
68
|
+
getPortableFollowingUri: getPortableFollowingUri ?? throwRouterError,
|
|
69
|
+
getPortableFollowersUri: getPortableFollowersUri ?? throwRouterError,
|
|
70
|
+
getPortableLikedUri: getPortableLikedUri ?? throwRouterError,
|
|
71
|
+
getPortableFeaturedUri: getPortableFeaturedUri ?? throwRouterError,
|
|
72
|
+
getPortableFeaturedTagsUri: getPortableFeaturedTagsUri ?? throwRouterError,
|
|
73
|
+
getPortableCollectionUri: getPortableCollectionUri ?? throwRouterError,
|
|
62
74
|
getCollectionUri: getCollectionUri ?? throwRouterError,
|
|
63
75
|
getOutboxUri: getOutboxUri ?? throwRouterError,
|
|
64
76
|
getMediaUploaderUri: getMediaUploaderUri ?? throwRouterError,
|
|
@@ -78,13 +90,15 @@ function createContext(values) {
|
|
|
78
90
|
lookupObject: lookupObject$1 ?? ((uri, options = {}) => {
|
|
79
91
|
return lookupObject(uri, {
|
|
80
92
|
documentLoader: options.documentLoader ?? documentLoader ?? mockDocumentLoader,
|
|
81
|
-
contextLoader: options.contextLoader ?? contextLoader ?? mockDocumentLoader
|
|
93
|
+
contextLoader: options.contextLoader ?? contextLoader ?? mockDocumentLoader,
|
|
94
|
+
verifyPortableObject: options.verifyPortableObject ?? verifyPortableObject
|
|
82
95
|
});
|
|
83
96
|
}),
|
|
84
97
|
traverseCollection: traverseCollection$1 ?? ((collection, options = {}) => {
|
|
85
98
|
return traverseCollection(collection, {
|
|
86
99
|
documentLoader: options.documentLoader ?? documentLoader ?? mockDocumentLoader,
|
|
87
|
-
contextLoader: options.contextLoader ?? contextLoader ?? mockDocumentLoader
|
|
100
|
+
contextLoader: options.contextLoader ?? contextLoader ?? mockDocumentLoader,
|
|
101
|
+
verifyPortableObject: options.verifyPortableObject ?? verifyPortableObject
|
|
88
102
|
});
|
|
89
103
|
}),
|
|
90
104
|
lookupNodeInfo: lookupNodeInfo ?? ((_params) => {
|
|
@@ -123,15 +137,59 @@ function createRequestContext(args) {
|
|
|
123
137
|
})),
|
|
124
138
|
request: args.request ?? new Request(args.url),
|
|
125
139
|
url: args.url,
|
|
140
|
+
portableRequest: args.portableRequest,
|
|
126
141
|
getActor: args.getActor ?? (() => Promise.resolve(null)),
|
|
127
142
|
getObject: args.getObject ?? (() => Promise.resolve(null)),
|
|
128
143
|
getSignedKey: args.getSignedKey ?? (() => Promise.resolve(null)),
|
|
129
144
|
getSignedKeyOwner: args.getSignedKeyOwner ?? (() => Promise.resolve(null)),
|
|
145
|
+
isSignedByAudience: args.isSignedByAudience ?? ((object, options) => isSignedByAudience(() => args.getSignedKeyOwner == null ? Promise.resolve(null) : args.getSignedKeyOwner(options ?? {}), object, options?.isMember)),
|
|
130
146
|
sendActivity: args.sendActivity ?? ((_params) => {
|
|
131
147
|
throw new Error("Not implemented");
|
|
132
148
|
})
|
|
133
149
|
};
|
|
134
150
|
}
|
|
151
|
+
const PUBLIC_IDS = /* @__PURE__ */ new Set([
|
|
152
|
+
PUBLIC_COLLECTION.href,
|
|
153
|
+
"as:Public",
|
|
154
|
+
"Public"
|
|
155
|
+
]);
|
|
156
|
+
function getPortableIri(id) {
|
|
157
|
+
try {
|
|
158
|
+
if (id.protocol === "ap:" || id.protocol === "ap+ef61:") return formatIri(id);
|
|
159
|
+
const portable = fromCompatibleEf61Id(id);
|
|
160
|
+
return portable == null ? null : formatIri(portable);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof TypeError) return null;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function isSameActorId(a, b) {
|
|
167
|
+
const portableA = getPortableIri(a);
|
|
168
|
+
const portableB = getPortableIri(b);
|
|
169
|
+
if (portableA != null || portableB != null) return portableA != null && portableB != null && arePortableUrisEqual(portableA, portableB);
|
|
170
|
+
return a.href === b.href;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The default implementation of `RequestContext.isSignedByAudience()` for
|
|
174
|
+
* testing, which checks the audience against `getSignedKeyOwner()`.
|
|
175
|
+
*/
|
|
176
|
+
async function isSignedByAudience(getSignedKeyOwner, object, isMember) {
|
|
177
|
+
const addressees = [
|
|
178
|
+
...object.toIds,
|
|
179
|
+
...object.ccIds,
|
|
180
|
+
...object.btoIds,
|
|
181
|
+
...object.bccIds,
|
|
182
|
+
...object.audienceIds
|
|
183
|
+
];
|
|
184
|
+
if (addressees.some((id) => PUBLIC_IDS.has(id.href))) return true;
|
|
185
|
+
const actor = await getSignedKeyOwner();
|
|
186
|
+
const actorId = actor?.id;
|
|
187
|
+
if (actor == null || actorId == null) return false;
|
|
188
|
+
if (addressees.some((id) => isSameActorId(id, actorId))) return true;
|
|
189
|
+
if (isMember == null) return false;
|
|
190
|
+
for (const addressee of addressees) if (await isMember(addressee, actor)) return true;
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
135
193
|
/**
|
|
136
194
|
* Creates an InboxContext for testing purposes.
|
|
137
195
|
* Not exported - used internally only. Public API is in mock.ts
|
|
@@ -225,6 +283,63 @@ function expandUriTemplate(template, values) {
|
|
|
225
283
|
}
|
|
226
284
|
});
|
|
227
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Builds an FEP-ef61 portable ID from a DID authority and a path, validating
|
|
288
|
+
* the authority the same way as `@fedify/fedify` does.
|
|
289
|
+
*/
|
|
290
|
+
function buildPortableUri(authority, path) {
|
|
291
|
+
if (typeof authority !== "string" || !/^did:[a-z0-9]+:[^/?#]+$/i.test(authority)) throw new TypeError("The authority of a portable ID must be a DID without a path, query, or fragment.");
|
|
292
|
+
const did = getFe34Origin(authority);
|
|
293
|
+
if (!isBase58BtcDidKey(did)) throw new TypeError("The did:key authority of a portable ID must be encoded in base58-btc, i.e., start with z.");
|
|
294
|
+
return parseIri(`ap+ef61://${did}${path}`);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Checks that a normalized DID, if it is a `did:key` DID, is encoded in
|
|
298
|
+
* base58-btc, as `@fedify/fedify` requires of portable IDs.
|
|
299
|
+
*/
|
|
300
|
+
function isBase58BtcDidKey(did) {
|
|
301
|
+
return !did.startsWith("did:key:") || /^z[1-9A-HJ-NP-Za-km-z]+$/.test(did.slice(8));
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Recognizes an FEP-ef61 portable ID, i.e., an `ap:` or `ap+ef61:` URI, or
|
|
305
|
+
* a compatible identifier on any gateway, validating it the same way as
|
|
306
|
+
* `Context.parseUri()` of `@fedify/fedify` does.
|
|
307
|
+
* @returns The DID and the path of the portable ID, `null` if it is
|
|
308
|
+
* malformed, or `undefined` if the URI is not a portable ID.
|
|
309
|
+
*/
|
|
310
|
+
function parseMockPortableId(uri) {
|
|
311
|
+
const portable = uri.protocol === "ap:" || uri.protocol === "ap+ef61:";
|
|
312
|
+
if (!portable && !((uri.protocol === "http:" || uri.protocol === "https:") && /^\/\.well-known\/apgateway\/did(?::|%3A)/i.test(uri.pathname))) return;
|
|
313
|
+
if (uri.username !== "" || uri.password !== "") return null;
|
|
314
|
+
try {
|
|
315
|
+
const id = portable ? parseIri(uri) : fromCompatibleEf61Id(uri.origin + uri.pathname);
|
|
316
|
+
if (id == null) return null;
|
|
317
|
+
canonicalizePortableUri(formatIri(id));
|
|
318
|
+
const authority = getFe34Origin(id);
|
|
319
|
+
if (!isBase58BtcDidKey(authority)) return null;
|
|
320
|
+
return {
|
|
321
|
+
authority,
|
|
322
|
+
path: id.pathname
|
|
323
|
+
};
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error instanceof TypeError) return null;
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Parses a path in the way {@link MockContext.parseUri} does, i.e., only
|
|
331
|
+
* `/users/{identifier}` and its subpaths are recognized, as actors.
|
|
332
|
+
*/
|
|
333
|
+
function parseMockPath(path) {
|
|
334
|
+
if (path.startsWith("/users/")) {
|
|
335
|
+
const parts = path.split("/");
|
|
336
|
+
if (parts.length >= 3) return {
|
|
337
|
+
type: "actor",
|
|
338
|
+
identifier: parts[2]
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
228
343
|
function validateOutboxListenerPath(path, dispatcherPath) {
|
|
229
344
|
if (!path.startsWith("/")) throw new TypeError("Path must start with a slash.");
|
|
230
345
|
if (dispatcherPath != null && dispatcherPath !== path) throw new TypeError("Outbox listener path and outbox dispatcher path must match.");
|
|
@@ -301,6 +416,7 @@ var MockFederation = class {
|
|
|
301
416
|
outboxListenerErrorHandler;
|
|
302
417
|
mediaUploaderCallback;
|
|
303
418
|
mediaUploaderAuthorizePredicate;
|
|
419
|
+
hashlinkMediaDispatcher;
|
|
304
420
|
followingDispatcher;
|
|
305
421
|
followersDispatcher;
|
|
306
422
|
likedDispatcher;
|
|
@@ -338,93 +454,103 @@ var MockFederation = class {
|
|
|
338
454
|
setActorDispatcher(path, dispatcher) {
|
|
339
455
|
this.actorDispatchers.set(path, dispatcher);
|
|
340
456
|
this.actorPath = path;
|
|
341
|
-
|
|
457
|
+
const setters = {
|
|
342
458
|
setKeyPairsDispatcher: (keyPairsDispatcher) => {
|
|
343
459
|
this.actorKeyPairsDispatcher = keyPairsDispatcher;
|
|
344
|
-
return
|
|
460
|
+
return setters;
|
|
345
461
|
},
|
|
346
|
-
mapHandle: () =>
|
|
347
|
-
mapAlias: () =>
|
|
348
|
-
|
|
462
|
+
mapHandle: () => setters,
|
|
463
|
+
mapAlias: () => setters,
|
|
464
|
+
mapPortableActorId: () => setters,
|
|
465
|
+
authorize: () => setters
|
|
349
466
|
};
|
|
467
|
+
return setters;
|
|
350
468
|
}
|
|
351
469
|
setObjectDispatcher(cls, path, dispatcher) {
|
|
352
470
|
this.objectDispatchers.set(path, dispatcher);
|
|
353
471
|
this.objectPaths.set(cls.typeId.href, path);
|
|
354
|
-
|
|
472
|
+
const setters = { authorize: () => setters };
|
|
473
|
+
return setters;
|
|
355
474
|
}
|
|
356
475
|
setInboxDispatcher(_path, dispatcher) {
|
|
357
476
|
this.inboxDispatcher = dispatcher;
|
|
358
|
-
|
|
359
|
-
setCounter: () =>
|
|
360
|
-
setFirstCursor: () =>
|
|
361
|
-
setLastCursor: () =>
|
|
362
|
-
authorize: () =>
|
|
477
|
+
const setters = {
|
|
478
|
+
setCounter: () => setters,
|
|
479
|
+
setFirstCursor: () => setters,
|
|
480
|
+
setLastCursor: () => setters,
|
|
481
|
+
authorize: () => setters
|
|
363
482
|
};
|
|
483
|
+
return setters;
|
|
364
484
|
}
|
|
365
485
|
setOutboxDispatcher(path, dispatcher) {
|
|
366
486
|
validateOutboxListenerPath(path, this.outboxListenersInitialized ? this.outboxPath : void 0);
|
|
367
487
|
this.outboxDispatcher = dispatcher;
|
|
368
488
|
this.outboxPath = path;
|
|
369
|
-
|
|
370
|
-
setCounter: () =>
|
|
371
|
-
setFirstCursor: () =>
|
|
372
|
-
setLastCursor: () =>
|
|
489
|
+
const setters = {
|
|
490
|
+
setCounter: () => setters,
|
|
491
|
+
setFirstCursor: () => setters,
|
|
492
|
+
setLastCursor: () => setters,
|
|
373
493
|
authorize: (predicate) => {
|
|
374
494
|
this.outboxDispatcherAuthorizePredicate = predicate;
|
|
375
|
-
return
|
|
495
|
+
return setters;
|
|
376
496
|
}
|
|
377
497
|
};
|
|
498
|
+
return setters;
|
|
378
499
|
}
|
|
379
500
|
setFollowingDispatcher(path, dispatcher) {
|
|
380
501
|
this.followingDispatcher = dispatcher;
|
|
381
502
|
this.followingPath = path;
|
|
382
|
-
|
|
383
|
-
setCounter: () =>
|
|
384
|
-
setFirstCursor: () =>
|
|
385
|
-
setLastCursor: () =>
|
|
386
|
-
authorize: () =>
|
|
503
|
+
const setters = {
|
|
504
|
+
setCounter: () => setters,
|
|
505
|
+
setFirstCursor: () => setters,
|
|
506
|
+
setLastCursor: () => setters,
|
|
507
|
+
authorize: () => setters
|
|
387
508
|
};
|
|
509
|
+
return setters;
|
|
388
510
|
}
|
|
389
511
|
setFollowersDispatcher(path, dispatcher) {
|
|
390
512
|
this.followersDispatcher = dispatcher;
|
|
391
513
|
this.followersPath = path;
|
|
392
|
-
|
|
393
|
-
setCounter: () =>
|
|
394
|
-
setFirstCursor: () =>
|
|
395
|
-
setLastCursor: () =>
|
|
396
|
-
authorize: () =>
|
|
514
|
+
const setters = {
|
|
515
|
+
setCounter: () => setters,
|
|
516
|
+
setFirstCursor: () => setters,
|
|
517
|
+
setLastCursor: () => setters,
|
|
518
|
+
authorize: () => setters
|
|
397
519
|
};
|
|
520
|
+
return setters;
|
|
398
521
|
}
|
|
399
522
|
setLikedDispatcher(path, dispatcher) {
|
|
400
523
|
this.likedDispatcher = dispatcher;
|
|
401
524
|
this.likedPath = path;
|
|
402
|
-
|
|
403
|
-
setCounter: () =>
|
|
404
|
-
setFirstCursor: () =>
|
|
405
|
-
setLastCursor: () =>
|
|
406
|
-
authorize: () =>
|
|
525
|
+
const setters = {
|
|
526
|
+
setCounter: () => setters,
|
|
527
|
+
setFirstCursor: () => setters,
|
|
528
|
+
setLastCursor: () => setters,
|
|
529
|
+
authorize: () => setters
|
|
407
530
|
};
|
|
531
|
+
return setters;
|
|
408
532
|
}
|
|
409
533
|
setFeaturedDispatcher(path, dispatcher) {
|
|
410
534
|
this.featuredDispatcher = dispatcher;
|
|
411
535
|
this.featuredPath = path;
|
|
412
|
-
|
|
413
|
-
setCounter: () =>
|
|
414
|
-
setFirstCursor: () =>
|
|
415
|
-
setLastCursor: () =>
|
|
416
|
-
authorize: () =>
|
|
536
|
+
const setters = {
|
|
537
|
+
setCounter: () => setters,
|
|
538
|
+
setFirstCursor: () => setters,
|
|
539
|
+
setLastCursor: () => setters,
|
|
540
|
+
authorize: () => setters
|
|
417
541
|
};
|
|
542
|
+
return setters;
|
|
418
543
|
}
|
|
419
544
|
setFeaturedTagsDispatcher(path, dispatcher) {
|
|
420
545
|
this.featuredTagsDispatcher = dispatcher;
|
|
421
546
|
this.featuredTagsPath = path;
|
|
422
|
-
|
|
423
|
-
setCounter: () =>
|
|
424
|
-
setFirstCursor: () =>
|
|
425
|
-
setLastCursor: () =>
|
|
426
|
-
authorize: () =>
|
|
547
|
+
const setters = {
|
|
548
|
+
setCounter: () => setters,
|
|
549
|
+
setFirstCursor: () => setters,
|
|
550
|
+
setLastCursor: () => setters,
|
|
551
|
+
authorize: () => setters
|
|
427
552
|
};
|
|
553
|
+
return setters;
|
|
428
554
|
}
|
|
429
555
|
setInboxListeners(inboxPath, sharedInboxPath) {
|
|
430
556
|
this.inboxPath = inboxPath;
|
|
@@ -443,6 +569,9 @@ var MockFederation = class {
|
|
|
443
569
|
onUnverifiedActivity() {
|
|
444
570
|
return this;
|
|
445
571
|
},
|
|
572
|
+
onRequestFinished() {
|
|
573
|
+
return this;
|
|
574
|
+
},
|
|
446
575
|
setSharedKeyDispatcher() {
|
|
447
576
|
return this;
|
|
448
577
|
},
|
|
@@ -483,6 +612,10 @@ var MockFederation = class {
|
|
|
483
612
|
return this;
|
|
484
613
|
} };
|
|
485
614
|
}
|
|
615
|
+
setHashlinkMediaDispatcher(dispatcher) {
|
|
616
|
+
if (this.hashlinkMediaDispatcher != null) throw new TypeError("Hashlink media dispatcher already set.");
|
|
617
|
+
this.hashlinkMediaDispatcher = dispatcher;
|
|
618
|
+
}
|
|
486
619
|
setOutboxPermanentFailureHandler(_handler) {}
|
|
487
620
|
async startQueue(contextData, options) {
|
|
488
621
|
this.contextData = contextData;
|
|
@@ -498,7 +631,7 @@ var MockFederation = class {
|
|
|
498
631
|
async processQueuedTask(contextData, _message) {
|
|
499
632
|
this.contextData = contextData;
|
|
500
633
|
}
|
|
501
|
-
createContext(baseUrlOrRequest, contextData) {
|
|
634
|
+
createContext(baseUrlOrRequest, contextData, options = {}) {
|
|
502
635
|
const mockFederation = this;
|
|
503
636
|
const request = baseUrlOrRequest instanceof Request ? baseUrlOrRequest : null;
|
|
504
637
|
return new MockContext({
|
|
@@ -507,10 +640,63 @@ var MockFederation = class {
|
|
|
507
640
|
data: contextData,
|
|
508
641
|
federation: mockFederation,
|
|
509
642
|
meterProvider: this.options.meterProvider,
|
|
510
|
-
tracerProvider: this.options.tracerProvider
|
|
643
|
+
tracerProvider: this.options.tracerProvider,
|
|
644
|
+
documentLoader: this.options.documentLoader,
|
|
645
|
+
contextLoader: this.options.contextLoader,
|
|
646
|
+
verifyPortableObject: this.options.verifyPortableObject,
|
|
647
|
+
portableRequest: options.portableRequest
|
|
511
648
|
});
|
|
512
649
|
}
|
|
513
650
|
async fetch(request, options) {
|
|
651
|
+
if (this.hashlinkMediaDispatcher != null) {
|
|
652
|
+
const url = new URL(request.url);
|
|
653
|
+
if ((url.protocol === "http:" || url.protocol === "https:") && url.pathname.startsWith("/.well-known/apgateway/")) {
|
|
654
|
+
const encoded = url.pathname.slice(23);
|
|
655
|
+
if (/^hl(?::|%3A)/i.test(encoded)) {
|
|
656
|
+
if (request.method !== "GET" && request.method !== "HEAD") return new Response("Method not allowed.", {
|
|
657
|
+
status: 405,
|
|
658
|
+
headers: {
|
|
659
|
+
Allow: "GET, HEAD",
|
|
660
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
let media;
|
|
664
|
+
try {
|
|
665
|
+
let hashlink;
|
|
666
|
+
try {
|
|
667
|
+
hashlink = decodeURIComponent(encoded);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
throw new TypeError("Invalid percent-encoding in the hashlink.", { cause: error });
|
|
670
|
+
}
|
|
671
|
+
const { digestMultibase } = parseHashlink(hashlink);
|
|
672
|
+
const { digest } = parseDigestMultibase(digestMultibase);
|
|
673
|
+
media = globalThis.Object.freeze({
|
|
674
|
+
hashlink: `hl:${digestMultibase}`,
|
|
675
|
+
digestMultibase,
|
|
676
|
+
algorithm: "sha2-256",
|
|
677
|
+
digest,
|
|
678
|
+
multihash: decodeMultibase(digestMultibase)
|
|
679
|
+
});
|
|
680
|
+
} catch (error) {
|
|
681
|
+
if (!(error instanceof TypeError)) throw error;
|
|
682
|
+
return new Response(request.method === "HEAD" ? null : "Malformed hashlink.", {
|
|
683
|
+
status: 400,
|
|
684
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
const context = this.createContext(request, options.contextData);
|
|
688
|
+
const response = await this.hashlinkMediaDispatcher(context, media);
|
|
689
|
+
if (response == null) return options.onNotFound == null ? new Response("Not Found", { status: 404 }) : await options.onNotFound(request);
|
|
690
|
+
if (request.method !== "HEAD" || response.body == null) return response;
|
|
691
|
+
response.body.cancel().catch(() => {});
|
|
692
|
+
return new Response(null, {
|
|
693
|
+
status: response.status,
|
|
694
|
+
statusText: response.statusText,
|
|
695
|
+
headers: response.headers
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
514
700
|
if (options.onNotFound) return options.onNotFound(request);
|
|
515
701
|
return new Response("Not Found", { status: 404 });
|
|
516
702
|
}
|
|
@@ -618,20 +804,24 @@ var MockFederation = class {
|
|
|
618
804
|
this.sentActivities = [];
|
|
619
805
|
}
|
|
620
806
|
setCollectionDispatcher(_name, _itemType, _path, _dispatcher) {
|
|
621
|
-
|
|
622
|
-
setCounter: () =>
|
|
623
|
-
setFirstCursor: () =>
|
|
624
|
-
setLastCursor: () =>
|
|
625
|
-
authorize: () =>
|
|
807
|
+
const setters = {
|
|
808
|
+
setCounter: () => setters,
|
|
809
|
+
setFirstCursor: () => setters,
|
|
810
|
+
setLastCursor: () => setters,
|
|
811
|
+
authorize: () => setters,
|
|
812
|
+
mapPortableOwner: () => setters
|
|
626
813
|
};
|
|
814
|
+
return setters;
|
|
627
815
|
}
|
|
628
816
|
setOrderedCollectionDispatcher(_name, _itemType, _path, _dispatcher) {
|
|
629
|
-
|
|
630
|
-
setCounter: () =>
|
|
631
|
-
setFirstCursor: () =>
|
|
632
|
-
setLastCursor: () =>
|
|
633
|
-
authorize: () =>
|
|
817
|
+
const setters = {
|
|
818
|
+
setCounter: () => setters,
|
|
819
|
+
setFirstCursor: () => setters,
|
|
820
|
+
setLastCursor: () => setters,
|
|
821
|
+
authorize: () => setters,
|
|
822
|
+
mapPortableOwner: () => setters
|
|
634
823
|
};
|
|
824
|
+
return setters;
|
|
635
825
|
}
|
|
636
826
|
};
|
|
637
827
|
/**
|
|
@@ -729,6 +919,10 @@ var MockContext = class MockContext {
|
|
|
729
919
|
tracerProvider;
|
|
730
920
|
request;
|
|
731
921
|
url;
|
|
922
|
+
portableRequest;
|
|
923
|
+
verifyPortableObject;
|
|
924
|
+
hasDocumentLoader;
|
|
925
|
+
hasContextLoader;
|
|
732
926
|
sentActivities = [];
|
|
733
927
|
constructor(options) {
|
|
734
928
|
const url = options.url ?? new URL("https://example.com");
|
|
@@ -738,6 +932,10 @@ var MockContext = class MockContext {
|
|
|
738
932
|
this.hostname = url.hostname;
|
|
739
933
|
this.url = url;
|
|
740
934
|
this.request = options.request ?? new Request(url);
|
|
935
|
+
this.portableRequest = options.portableRequest;
|
|
936
|
+
this.verifyPortableObject = options.verifyPortableObject;
|
|
937
|
+
this.hasDocumentLoader = options.hasDocumentLoader ?? options.documentLoader != null;
|
|
938
|
+
this.hasContextLoader = options.hasContextLoader ?? options.contextLoader != null;
|
|
741
939
|
this.data = options.data;
|
|
742
940
|
this.federation = options.federation;
|
|
743
941
|
this.documentLoader = options.documentLoader ?? (async (url) => ({
|
|
@@ -756,12 +954,16 @@ var MockContext = class MockContext {
|
|
|
756
954
|
}
|
|
757
955
|
return null;
|
|
758
956
|
}
|
|
759
|
-
async getObject(cls, values) {
|
|
957
|
+
async getObject(cls, values, options) {
|
|
760
958
|
if (this.federation instanceof MockFederation) {
|
|
761
959
|
const path = this.federation.objectPaths.get(cls.typeId.href);
|
|
762
960
|
if (path) {
|
|
763
961
|
const dispatcher = this.federation.objectDispatchers.get(path);
|
|
764
|
-
if (dispatcher)
|
|
962
|
+
if (dispatcher) {
|
|
963
|
+
const object = await dispatcher(this, values);
|
|
964
|
+
if (object instanceof Tombstone && !(object instanceof cls) && options?.tombstone !== "passthrough") return null;
|
|
965
|
+
return object;
|
|
966
|
+
}
|
|
765
967
|
}
|
|
766
968
|
}
|
|
767
969
|
return null;
|
|
@@ -772,6 +974,9 @@ var MockContext = class MockContext {
|
|
|
772
974
|
getSignedKeyOwner() {
|
|
773
975
|
return Promise.resolve(null);
|
|
774
976
|
}
|
|
977
|
+
isSignedByAudience(object, options) {
|
|
978
|
+
return isSignedByAudience(() => this.getSignedKeyOwner(), object, options?.isMember);
|
|
979
|
+
}
|
|
775
980
|
#resolveTaskDefinition(task) {
|
|
776
981
|
if (!(this.federation instanceof MockFederation)) throw new TypeError("No task definitions are available.");
|
|
777
982
|
const def = this.federation.taskDefinitions.get(task.name);
|
|
@@ -795,10 +1000,15 @@ var MockContext = class MockContext {
|
|
|
795
1000
|
clone(data) {
|
|
796
1001
|
return new MockContext({
|
|
797
1002
|
url: this.url,
|
|
1003
|
+
request: this.request,
|
|
798
1004
|
data,
|
|
799
1005
|
federation: this.federation,
|
|
800
1006
|
documentLoader: this.documentLoader,
|
|
801
1007
|
contextLoader: this.contextLoader,
|
|
1008
|
+
verifyPortableObject: this.verifyPortableObject,
|
|
1009
|
+
portableRequest: this.portableRequest,
|
|
1010
|
+
hasDocumentLoader: this.hasDocumentLoader,
|
|
1011
|
+
hasContextLoader: this.hasContextLoader,
|
|
802
1012
|
meterProvider: this.meterProvider,
|
|
803
1013
|
tracerProvider: this.tracerProvider
|
|
804
1014
|
});
|
|
@@ -825,6 +1035,46 @@ var MockContext = class MockContext {
|
|
|
825
1035
|
const path = globalThis.Object.entries(values).map(([key, value]) => `${key}/${value}`).join("/");
|
|
826
1036
|
return new URL(`/objects/${cls.name.toLowerCase()}/${path}`, this.origin);
|
|
827
1037
|
}
|
|
1038
|
+
getPortableActorUri(identifier, authority) {
|
|
1039
|
+
const { pathname } = this.getActorUri(identifier);
|
|
1040
|
+
return buildPortableUri(authority, pathname);
|
|
1041
|
+
}
|
|
1042
|
+
getPortableObjectUri(cls, values, authority) {
|
|
1043
|
+
const { pathname } = this.getObjectUri(cls, values);
|
|
1044
|
+
return buildPortableUri(authority, pathname);
|
|
1045
|
+
}
|
|
1046
|
+
getPortableInboxUri(identifier, authority) {
|
|
1047
|
+
const { pathname } = this.getInboxUri(identifier);
|
|
1048
|
+
return buildPortableUri(authority, pathname);
|
|
1049
|
+
}
|
|
1050
|
+
getPortableOutboxUri(identifier, authority) {
|
|
1051
|
+
const { pathname } = this.getOutboxUri(identifier);
|
|
1052
|
+
return buildPortableUri(authority, pathname);
|
|
1053
|
+
}
|
|
1054
|
+
getPortableFollowingUri(identifier, authority) {
|
|
1055
|
+
const { pathname } = this.getFollowingUri(identifier);
|
|
1056
|
+
return buildPortableUri(authority, pathname);
|
|
1057
|
+
}
|
|
1058
|
+
getPortableFollowersUri(identifier, authority) {
|
|
1059
|
+
const { pathname } = this.getFollowersUri(identifier);
|
|
1060
|
+
return buildPortableUri(authority, pathname);
|
|
1061
|
+
}
|
|
1062
|
+
getPortableLikedUri(identifier, authority) {
|
|
1063
|
+
const { pathname } = this.getLikedUri(identifier);
|
|
1064
|
+
return buildPortableUri(authority, pathname);
|
|
1065
|
+
}
|
|
1066
|
+
getPortableFeaturedUri(identifier, authority) {
|
|
1067
|
+
const { pathname } = this.getFeaturedUri(identifier);
|
|
1068
|
+
return buildPortableUri(authority, pathname);
|
|
1069
|
+
}
|
|
1070
|
+
getPortableFeaturedTagsUri(identifier, authority) {
|
|
1071
|
+
const { pathname } = this.getFeaturedTagsUri(identifier);
|
|
1072
|
+
return buildPortableUri(authority, pathname);
|
|
1073
|
+
}
|
|
1074
|
+
getPortableCollectionUri(name, values, authority) {
|
|
1075
|
+
const { pathname } = this.getCollectionUri(name, values);
|
|
1076
|
+
return buildPortableUri(authority, pathname);
|
|
1077
|
+
}
|
|
828
1078
|
getOutboxUri(identifier) {
|
|
829
1079
|
if (this.federation instanceof MockFederation && this.federation.outboxPath) {
|
|
830
1080
|
const path = expandUriTemplate(this.federation.outboxPath, { identifier });
|
|
@@ -889,15 +1139,16 @@ var MockContext = class MockContext {
|
|
|
889
1139
|
const path = globalThis.Object.entries(values).map(([key, value]) => `${key}/${value}`).join("/");
|
|
890
1140
|
return new URL(`/collections/${String(_name)}/${path}`, this.origin);
|
|
891
1141
|
}
|
|
892
|
-
parseUri(uri) {
|
|
893
|
-
if (uri
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1142
|
+
parseUri(uri, options = {}) {
|
|
1143
|
+
if (uri == null) return null;
|
|
1144
|
+
const portable = parseMockPortableId(uri);
|
|
1145
|
+
if (portable === void 0) return parseMockPath(uri.pathname);
|
|
1146
|
+
if (portable === null || !options.portable) return null;
|
|
1147
|
+
const result = parseMockPath(portable.path);
|
|
1148
|
+
return result == null ? null : {
|
|
1149
|
+
...result,
|
|
1150
|
+
authority: portable.authority
|
|
1151
|
+
};
|
|
901
1152
|
}
|
|
902
1153
|
async getActorKeyPairs(identifier) {
|
|
903
1154
|
if (this.federation instanceof MockFederation && this.federation.actorKeyPairsDispatcher) {
|
|
@@ -923,8 +1174,21 @@ var MockContext = class MockContext {
|
|
|
923
1174
|
if ("keyId" in params) return this.documentLoader;
|
|
924
1175
|
return Promise.resolve(this.documentLoader);
|
|
925
1176
|
}
|
|
926
|
-
lookupObject(
|
|
927
|
-
return Promise.resolve(null);
|
|
1177
|
+
lookupObject(uri, options = {}) {
|
|
1178
|
+
if (!this.hasDocumentLoader && options.documentLoader == null || (options.verifyPortableObject ?? this.verifyPortableObject) == null) return Promise.resolve(null);
|
|
1179
|
+
const value = typeof uri === "string" ? uri : formatIri(uri);
|
|
1180
|
+
if (!/^ap(?:\+ef61)?:\/\//i.test(value)) try {
|
|
1181
|
+
if (fromCompatibleEf61Id(value) == null) return Promise.resolve(null);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
if (error instanceof TypeError) return Promise.resolve(null);
|
|
1184
|
+
throw error;
|
|
1185
|
+
}
|
|
1186
|
+
return lookupObject(uri, {
|
|
1187
|
+
...options,
|
|
1188
|
+
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
1189
|
+
contextLoader: options.contextLoader ?? (this.hasContextLoader ? this.contextLoader : options.documentLoader ?? this.contextLoader),
|
|
1190
|
+
verifyPortableObject: options.verifyPortableObject ?? this.verifyPortableObject
|
|
1191
|
+
});
|
|
928
1192
|
}
|
|
929
1193
|
traverseCollection(_collection, _options) {
|
|
930
1194
|
return { async *[Symbol.asyncIterator]() {} };
|
|
@@ -977,6 +1241,147 @@ var MockContext = class MockContext {
|
|
|
977
1241
|
}
|
|
978
1242
|
};
|
|
979
1243
|
//#endregion
|
|
1244
|
+
//#region src/kv-tester.ts
|
|
1245
|
+
/**
|
|
1246
|
+
* Tests a {@link KvStore} implementation with a standard set of tests.
|
|
1247
|
+
*
|
|
1248
|
+
* The suite covers reads and writes, deletion, structured keys and values,
|
|
1249
|
+
* prefix listing, TTL expiration, and optional compare-and-swap behavior.
|
|
1250
|
+
* Every key is isolated below a random prefix and is deleted after the suite.
|
|
1251
|
+
*
|
|
1252
|
+
* @example
|
|
1253
|
+
* ```typescript ignore
|
|
1254
|
+
* import { test } from "@fedify/fixture";
|
|
1255
|
+
* import { testKvStore } from "@fedify/testing";
|
|
1256
|
+
* import { MyKvStore } from "./my-kv.ts";
|
|
1257
|
+
*
|
|
1258
|
+
* test("MyKvStore", () =>
|
|
1259
|
+
* testKvStore(
|
|
1260
|
+
* () => new MyKvStore(),
|
|
1261
|
+
* async ({ store }) => await store.close(),
|
|
1262
|
+
* )
|
|
1263
|
+
* );
|
|
1264
|
+
* ```
|
|
1265
|
+
*
|
|
1266
|
+
* @param getKvStore A factory that creates the store to test.
|
|
1267
|
+
* @param onFinally A cleanup function called after the suite completes.
|
|
1268
|
+
* @param options Optional configuration for the suite.
|
|
1269
|
+
*/
|
|
1270
|
+
async function testKvStore(getKvStore, onFinally, options = {}) {
|
|
1271
|
+
const store = await getKvStore();
|
|
1272
|
+
const prefix = [`fedify_test_${crypto.randomUUID()}`];
|
|
1273
|
+
const key = (...segments) => [...prefix, ...segments];
|
|
1274
|
+
try {
|
|
1275
|
+
strictEqual(await store.get(key("missing")), void 0);
|
|
1276
|
+
for (const [index, value] of [
|
|
1277
|
+
"text",
|
|
1278
|
+
42,
|
|
1279
|
+
true,
|
|
1280
|
+
null,
|
|
1281
|
+
[
|
|
1282
|
+
1,
|
|
1283
|
+
"two",
|
|
1284
|
+
false
|
|
1285
|
+
],
|
|
1286
|
+
{ nested: { value: "test" } }
|
|
1287
|
+
].entries()) {
|
|
1288
|
+
const valueKey = key("values", index.toString());
|
|
1289
|
+
await store.set(valueKey, value);
|
|
1290
|
+
deepStrictEqual(await store.get(valueKey), value);
|
|
1291
|
+
}
|
|
1292
|
+
const overwriteKey = key("overwrite");
|
|
1293
|
+
await store.set(overwriteKey, "before");
|
|
1294
|
+
await store.set(overwriteKey, "after");
|
|
1295
|
+
strictEqual(await store.get(overwriteKey), "after");
|
|
1296
|
+
const deleteKey = key("delete");
|
|
1297
|
+
await store.set(deleteKey, "value");
|
|
1298
|
+
await store.delete(deleteKey);
|
|
1299
|
+
strictEqual(await store.get(deleteKey), void 0);
|
|
1300
|
+
await store.delete(deleteKey);
|
|
1301
|
+
const isolatedKeys = [
|
|
1302
|
+
key("a", "b"),
|
|
1303
|
+
key("a,b"),
|
|
1304
|
+
key("a:b")
|
|
1305
|
+
];
|
|
1306
|
+
for (const [index, isolatedKey] of isolatedKeys.entries()) await store.set(isolatedKey, index);
|
|
1307
|
+
for (const [index, isolatedKey] of isolatedKeys.entries()) strictEqual(await store.get(isolatedKey), index);
|
|
1308
|
+
if (options.testTtl ?? true) {
|
|
1309
|
+
const expiringKey = key("ttl", "expiring");
|
|
1310
|
+
await store.set(expiringKey, "expired", { ttl: Temporal.Duration.from({ milliseconds: 50 }) });
|
|
1311
|
+
await delay(100);
|
|
1312
|
+
strictEqual(await store.get(expiringKey), void 0);
|
|
1313
|
+
deepStrictEqual(await collect(store.list(key("ttl"))), []);
|
|
1314
|
+
const refreshedKey = key("ttl", "refreshed");
|
|
1315
|
+
await store.set(refreshedKey, "temporary", { ttl: Temporal.Duration.from({ milliseconds: 50 }) });
|
|
1316
|
+
await store.set(refreshedKey, "persistent");
|
|
1317
|
+
await delay(100);
|
|
1318
|
+
strictEqual(await store.get(refreshedKey), "persistent");
|
|
1319
|
+
}
|
|
1320
|
+
const listPrefix = key("list");
|
|
1321
|
+
const listEntries = [
|
|
1322
|
+
{
|
|
1323
|
+
key: listPrefix,
|
|
1324
|
+
value: "exact"
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
key: key("list", "child"),
|
|
1328
|
+
value: "child"
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
key: key("list", "child", "nested"),
|
|
1332
|
+
value: "nested"
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
key: key("list-other"),
|
|
1336
|
+
value: "other"
|
|
1337
|
+
}
|
|
1338
|
+
];
|
|
1339
|
+
for (const entry of listEntries) await store.set(entry.key, entry.value);
|
|
1340
|
+
deepStrictEqual(normalize(await collect(store.list(listPrefix))), normalize(listEntries.slice(0, 3)));
|
|
1341
|
+
const prefixedEntries = await collect(store.list(prefix));
|
|
1342
|
+
deepStrictEqual(normalize(filterByPrefix(await collect(store.list()), prefix)), normalize(prefixedEntries));
|
|
1343
|
+
deepStrictEqual(normalize(filterByPrefix(await collect(store.list([])), prefix)), normalize(prefixedEntries));
|
|
1344
|
+
if (options.testCas ?? store.cas != null) {
|
|
1345
|
+
if (store.cas == null) throw new TypeError("KvStore.cas is not implemented");
|
|
1346
|
+
const cas = store.cas.bind(store);
|
|
1347
|
+
const casKey = key("cas", "value");
|
|
1348
|
+
strictEqual(await cas(casKey, "wrong", "value"), false);
|
|
1349
|
+
strictEqual(await cas(casKey, void 0, "created"), true);
|
|
1350
|
+
strictEqual(await cas(casKey, "wrong", "updated"), false);
|
|
1351
|
+
strictEqual(await cas(casKey, "created", "updated"), true);
|
|
1352
|
+
strictEqual(await store.get(casKey), "updated");
|
|
1353
|
+
strictEqual(await cas(casKey, "updated", void 0), true);
|
|
1354
|
+
strictEqual(await store.get(casKey), void 0);
|
|
1355
|
+
if (options.testTtl ?? true) {
|
|
1356
|
+
const expiredCasKey = key("cas", "expired");
|
|
1357
|
+
await store.set(expiredCasKey, "old", { ttl: Temporal.Duration.from({ milliseconds: 50 }) });
|
|
1358
|
+
await delay(100);
|
|
1359
|
+
strictEqual(await cas(expiredCasKey, void 0, "replaced"), true);
|
|
1360
|
+
const ttlCasKey = key("cas", "ttl");
|
|
1361
|
+
strictEqual(await cas(ttlCasKey, void 0, "temporary", { ttl: Temporal.Duration.from({ milliseconds: 50 }) }), true);
|
|
1362
|
+
await delay(100);
|
|
1363
|
+
strictEqual(await store.get(ttlCasKey), void 0);
|
|
1364
|
+
}
|
|
1365
|
+
const concurrentKey = key("cas", "concurrent");
|
|
1366
|
+
strictEqual((await Promise.all(Array.from({ length: 8 }, (_, index) => cas(concurrentKey, void 0, index)))).filter(Boolean).length, 1);
|
|
1367
|
+
}
|
|
1368
|
+
} finally {
|
|
1369
|
+
for await (const entry of store.list(prefix)) await store.delete(entry.key);
|
|
1370
|
+
await onFinally({ store });
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
async function collect(entries) {
|
|
1374
|
+
const results = [];
|
|
1375
|
+
for await (const entry of entries) results.push(entry);
|
|
1376
|
+
return results;
|
|
1377
|
+
}
|
|
1378
|
+
function normalize(entries) {
|
|
1379
|
+
return entries.map((entry) => JSON.stringify(entry)).sort();
|
|
1380
|
+
}
|
|
1381
|
+
function filterByPrefix(entries, prefix) {
|
|
1382
|
+
return entries.filter((entry) => prefix.every((segment, index) => entry.key[index] === segment));
|
|
1383
|
+
}
|
|
1384
|
+
//#endregion
|
|
980
1385
|
//#region src/mq-tester.ts
|
|
981
1386
|
/**
|
|
982
1387
|
* Tests a {@link MessageQueue} implementation with a standard set of tests.
|
|
@@ -1157,4 +1562,4 @@ async function waitFor(predicate, timeoutMs) {
|
|
|
1157
1562
|
}
|
|
1158
1563
|
const getRandomKey = (prefix) => `fedify_test_${prefix}_${crypto.randomUUID()}`;
|
|
1159
1564
|
//#endregion
|
|
1160
|
-
export { createContext, createFederation, createInboxContext, createOutboxContext, createRequestContext, getRandomKey, testMessageQueue, waitFor };
|
|
1565
|
+
export { createContext, createFederation, createInboxContext, createOutboxContext, createRequestContext, getRandomKey, testKvStore, testMessageQueue, waitFor };
|