@fedify/testing 2.0.0-pr.490.2 → 2.0.0-pr.559.4
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/LICENSE +1 -1
- package/README.md +9 -7
- package/dist/mod.cjs +246 -26
- package/dist/mod.d.cts +74 -3
- package/dist/mod.d.ts +75 -3
- package/dist/mod.js +241 -24
- package/package.json +15 -8
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ interface for unit testing:
|
|
|
40
40
|
|
|
41
41
|
~~~~ typescript
|
|
42
42
|
import { MockFederation } from "@fedify/testing";
|
|
43
|
-
import { Create } from "@fedify/
|
|
43
|
+
import { Create } from "@fedify/vocab";
|
|
44
44
|
|
|
45
45
|
// Create a mock federation
|
|
46
46
|
const federation = new MockFederation<{ userId: string }>();
|
|
@@ -95,10 +95,12 @@ The package also exports helper functions for creating various context types:
|
|
|
95
95
|
- `createRequestContext()`: Creates a request context
|
|
96
96
|
- `createInboxContext()`: Creates an inbox context
|
|
97
97
|
|
|
98
|
-
## Features
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
99
|
+
Features
|
|
100
|
+
--------
|
|
101
|
+
|
|
102
|
+
- Track sent activities with metadata
|
|
103
|
+
- Simulate activity reception
|
|
104
|
+
- Configure custom URI templates
|
|
105
|
+
- Test queue-based activity processing
|
|
106
|
+
- Mock document loaders and context loaders
|
package/dist/mod.cjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
|
|
2
|
+
const { Temporal } = require("@js-temporal/polyfill");
|
|
3
|
+
|
|
1
4
|
//#region rolldown:runtime
|
|
2
5
|
var __create = Object.create;
|
|
3
6
|
var __defProp = Object.defineProperty;
|
|
@@ -21,8 +24,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
24
|
}) : target, mod));
|
|
22
25
|
|
|
23
26
|
//#endregion
|
|
27
|
+
const __fedify_vocab = __toESM(require("@fedify/vocab"));
|
|
24
28
|
const __fedify_fedify_federation = __toESM(require("@fedify/fedify/federation"));
|
|
25
|
-
const
|
|
29
|
+
const es_toolkit = __toESM(require("es-toolkit"));
|
|
30
|
+
const node_assert_strict = __toESM(require("node:assert/strict"));
|
|
26
31
|
|
|
27
32
|
//#region src/docloader.ts
|
|
28
33
|
const mockDocumentLoader = async (url) => ({
|
|
@@ -39,7 +44,7 @@ const noopTracerProvider$1 = { getTracer: () => ({
|
|
|
39
44
|
}) };
|
|
40
45
|
function createContext(values) {
|
|
41
46
|
const { federation, url = new URL("http://example.com/"), canonicalOrigin, data, documentLoader, contextLoader, tracerProvider, clone, getNodeInfoUri, getActorUri, getObjectUri, getCollectionUri, getOutboxUri, getInboxUri, getFollowingUri, getFollowersUri, getLikedUri, getFeaturedUri, getFeaturedTagsUri, parseUri, getActorKeyPairs, getDocumentLoader, lookupObject, traverseCollection, lookupNodeInfo, lookupWebFinger, sendActivity, routeActivity } = values;
|
|
42
|
-
function
|
|
47
|
+
function throwRouterError() {
|
|
43
48
|
throw new __fedify_fedify_federation.RouterError("Not implemented");
|
|
44
49
|
}
|
|
45
50
|
return {
|
|
@@ -56,17 +61,17 @@ function createContext(values) {
|
|
|
56
61
|
...values,
|
|
57
62
|
data: data$1
|
|
58
63
|
})),
|
|
59
|
-
getNodeInfoUri: getNodeInfoUri ??
|
|
60
|
-
getActorUri: getActorUri ??
|
|
61
|
-
getObjectUri: getObjectUri ??
|
|
62
|
-
getCollectionUri: getCollectionUri ??
|
|
63
|
-
getOutboxUri: getOutboxUri ??
|
|
64
|
-
getInboxUri: getInboxUri ??
|
|
65
|
-
getFollowingUri: getFollowingUri ??
|
|
66
|
-
getFollowersUri: getFollowersUri ??
|
|
67
|
-
getLikedUri: getLikedUri ??
|
|
68
|
-
getFeaturedUri: getFeaturedUri ??
|
|
69
|
-
getFeaturedTagsUri: getFeaturedTagsUri ??
|
|
64
|
+
getNodeInfoUri: getNodeInfoUri ?? throwRouterError,
|
|
65
|
+
getActorUri: getActorUri ?? throwRouterError,
|
|
66
|
+
getObjectUri: getObjectUri ?? throwRouterError,
|
|
67
|
+
getCollectionUri: getCollectionUri ?? throwRouterError,
|
|
68
|
+
getOutboxUri: getOutboxUri ?? throwRouterError,
|
|
69
|
+
getInboxUri: getInboxUri ?? throwRouterError,
|
|
70
|
+
getFollowingUri: getFollowingUri ?? throwRouterError,
|
|
71
|
+
getFollowersUri: getFollowersUri ?? throwRouterError,
|
|
72
|
+
getLikedUri: getLikedUri ?? throwRouterError,
|
|
73
|
+
getFeaturedUri: getFeaturedUri ?? throwRouterError,
|
|
74
|
+
getFeaturedTagsUri: getFeaturedTagsUri ?? throwRouterError,
|
|
70
75
|
parseUri: parseUri ?? ((_uri) => {
|
|
71
76
|
throw new Error("Not implemented");
|
|
72
77
|
}),
|
|
@@ -75,13 +80,13 @@ function createContext(values) {
|
|
|
75
80
|
}),
|
|
76
81
|
getActorKeyPairs: getActorKeyPairs ?? ((_handle) => Promise.resolve([])),
|
|
77
82
|
lookupObject: lookupObject ?? ((uri, options = {}) => {
|
|
78
|
-
return (0,
|
|
83
|
+
return (0, __fedify_vocab.lookupObject)(uri, {
|
|
79
84
|
documentLoader: options.documentLoader ?? documentLoader ?? mockDocumentLoader,
|
|
80
85
|
contextLoader: options.contextLoader ?? contextLoader ?? mockDocumentLoader
|
|
81
86
|
});
|
|
82
87
|
}),
|
|
83
88
|
traverseCollection: traverseCollection ?? ((collection, options = {}) => {
|
|
84
|
-
return (0,
|
|
89
|
+
return (0, __fedify_vocab.traverseCollection)(collection, {
|
|
85
90
|
documentLoader: options.documentLoader ?? documentLoader ?? mockDocumentLoader,
|
|
86
91
|
contextLoader: options.contextLoader ?? contextLoader ?? mockDocumentLoader
|
|
87
92
|
});
|
|
@@ -171,7 +176,7 @@ function expandUriTemplate(template, values) {
|
|
|
171
176
|
*
|
|
172
177
|
* @example
|
|
173
178
|
* ```typescript
|
|
174
|
-
* import { Create } from "@fedify/
|
|
179
|
+
* import { Create } from "@fedify/vocab";
|
|
175
180
|
* import { createFederation } from "@fedify/testing";
|
|
176
181
|
*
|
|
177
182
|
* // Create a mock federation with contextData
|
|
@@ -205,6 +210,7 @@ var MockFederation = class {
|
|
|
205
210
|
nodeInfoDispatcher;
|
|
206
211
|
webFingerDispatcher;
|
|
207
212
|
actorDispatchers = /* @__PURE__ */ new Map();
|
|
213
|
+
actorKeyPairsDispatcher;
|
|
208
214
|
actorPath;
|
|
209
215
|
inboxPath;
|
|
210
216
|
outboxPath;
|
|
@@ -242,7 +248,10 @@ var MockFederation = class {
|
|
|
242
248
|
this.actorDispatchers.set(path, dispatcher);
|
|
243
249
|
this.actorPath = path;
|
|
244
250
|
return {
|
|
245
|
-
setKeyPairsDispatcher: () =>
|
|
251
|
+
setKeyPairsDispatcher: (keyPairsDispatcher) => {
|
|
252
|
+
this.actorKeyPairsDispatcher = keyPairsDispatcher;
|
|
253
|
+
return this;
|
|
254
|
+
},
|
|
246
255
|
mapHandle: () => this,
|
|
247
256
|
mapAlias: () => this,
|
|
248
257
|
authorize: () => this
|
|
@@ -344,6 +353,7 @@ var MockFederation = class {
|
|
|
344
353
|
}
|
|
345
354
|
};
|
|
346
355
|
}
|
|
356
|
+
setOutboxPermanentFailureHandler(_handler) {}
|
|
347
357
|
async startQueue(contextData, options) {
|
|
348
358
|
this.contextData = contextData;
|
|
349
359
|
this.queueStarted = true;
|
|
@@ -428,7 +438,7 @@ var MockFederation = class {
|
|
|
428
438
|
*
|
|
429
439
|
* @example
|
|
430
440
|
* ```typescript
|
|
431
|
-
* import { Create } from "@fedify/
|
|
441
|
+
* import { Create } from "@fedify/vocab";
|
|
432
442
|
* import { createFederation } from "@fedify/testing";
|
|
433
443
|
*
|
|
434
444
|
* // Create a mock federation with contextData
|
|
@@ -470,7 +480,7 @@ function createFederation(options = {}) {
|
|
|
470
480
|
*
|
|
471
481
|
* @example
|
|
472
482
|
* ```typescript
|
|
473
|
-
* import { Person, Create } from "@fedify/
|
|
483
|
+
* import { Person, Create } from "@fedify/vocab";
|
|
474
484
|
* import { createFederation } from "@fedify/testing";
|
|
475
485
|
*
|
|
476
486
|
* // Create a mock federation and context
|
|
@@ -531,11 +541,22 @@ var MockContext = class MockContext {
|
|
|
531
541
|
this.contextLoader = options.contextLoader ?? this.documentLoader;
|
|
532
542
|
this.tracerProvider = options.tracerProvider ?? noopTracerProvider;
|
|
533
543
|
}
|
|
534
|
-
getActor(
|
|
535
|
-
|
|
544
|
+
async getActor(handle) {
|
|
545
|
+
if (this.federation instanceof MockFederation && this.federation.actorPath) {
|
|
546
|
+
const dispatcher = this.federation.actorDispatchers.get(this.federation.actorPath);
|
|
547
|
+
if (dispatcher) return await dispatcher(this, handle);
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
536
550
|
}
|
|
537
|
-
getObject(
|
|
538
|
-
|
|
551
|
+
async getObject(cls, values) {
|
|
552
|
+
if (this.federation instanceof MockFederation) {
|
|
553
|
+
const path = this.federation.objectPaths.get(cls.typeId.href);
|
|
554
|
+
if (path) {
|
|
555
|
+
const dispatcher = this.federation.objectDispatchers.get(path);
|
|
556
|
+
if (dispatcher) return await dispatcher(this, values);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
539
560
|
}
|
|
540
561
|
getSignedKey() {
|
|
541
562
|
return Promise.resolve(null);
|
|
@@ -667,8 +688,25 @@ var MockContext = class MockContext {
|
|
|
667
688
|
}
|
|
668
689
|
return null;
|
|
669
690
|
}
|
|
670
|
-
getActorKeyPairs(
|
|
671
|
-
|
|
691
|
+
async getActorKeyPairs(identifier) {
|
|
692
|
+
if (this.federation instanceof MockFederation && this.federation.actorKeyPairsDispatcher) {
|
|
693
|
+
const keyPairs = await this.federation.actorKeyPairsDispatcher(this, identifier);
|
|
694
|
+
const owner = this.getActorUri(identifier);
|
|
695
|
+
return keyPairs.map((kp) => ({
|
|
696
|
+
...kp,
|
|
697
|
+
cryptographicKey: new __fedify_vocab.CryptographicKey({
|
|
698
|
+
id: kp.keyId,
|
|
699
|
+
owner,
|
|
700
|
+
publicKey: kp.publicKey
|
|
701
|
+
}),
|
|
702
|
+
multikey: new __fedify_vocab.Multikey({
|
|
703
|
+
id: kp.keyId,
|
|
704
|
+
controller: owner,
|
|
705
|
+
publicKey: kp.publicKey
|
|
706
|
+
})
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
return [];
|
|
672
710
|
}
|
|
673
711
|
getDocumentLoader(params) {
|
|
674
712
|
if ("keyId" in params) return this.documentLoader;
|
|
@@ -726,8 +764,190 @@ var MockContext = class MockContext {
|
|
|
726
764
|
}
|
|
727
765
|
};
|
|
728
766
|
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region src/mq-tester.ts
|
|
769
|
+
/**
|
|
770
|
+
* Tests a {@link MessageQueue} implementation with a standard set of tests.
|
|
771
|
+
*
|
|
772
|
+
* This function runs tests for:
|
|
773
|
+
* - `enqueue()`: Basic message enqueueing
|
|
774
|
+
* - `enqueue()` with delay: Delayed message enqueueing
|
|
775
|
+
* - `enqueueMany()`: Bulk message enqueueing
|
|
776
|
+
* - `enqueueMany()` with delay: Delayed bulk message enqueueing
|
|
777
|
+
* - Multiple listeners: Ensures messages are processed by only one listener
|
|
778
|
+
* - Ordering key support (optional): Ensures messages with the same ordering
|
|
779
|
+
* key are processed in order
|
|
780
|
+
*
|
|
781
|
+
* @example
|
|
782
|
+
* ```typescript ignore
|
|
783
|
+
* import { test } from "@fedify/fixture";
|
|
784
|
+
* import { testMessageQueue } from "@fedify/testing";
|
|
785
|
+
* import { MyMessageQueue } from "./my-mq.ts";
|
|
786
|
+
*
|
|
787
|
+
* test("MyMessageQueue", () =>
|
|
788
|
+
* testMessageQueue(
|
|
789
|
+
* () => new MyMessageQueue(),
|
|
790
|
+
* async ({ mq1, mq2, controller }) => {
|
|
791
|
+
* controller.abort();
|
|
792
|
+
* await mq1.close();
|
|
793
|
+
* await mq2.close();
|
|
794
|
+
* },
|
|
795
|
+
* { testOrderingKey: true }, // Enable ordering key tests
|
|
796
|
+
* )
|
|
797
|
+
* );
|
|
798
|
+
* ```
|
|
799
|
+
*
|
|
800
|
+
* @param getMessageQueue A factory function that creates a new message queue
|
|
801
|
+
* instance. It should return a new instance each time
|
|
802
|
+
* to ensure test isolation, but both instances should
|
|
803
|
+
* share the same underlying storage/channel.
|
|
804
|
+
* @param onFinally A cleanup function called after all tests complete.
|
|
805
|
+
* It receives both message queue instances and the abort
|
|
806
|
+
* controller used for the listeners.
|
|
807
|
+
* @param options Optional configuration for the test suite.
|
|
808
|
+
* @returns A promise that resolves when all tests pass.
|
|
809
|
+
*/
|
|
810
|
+
async function testMessageQueue(getMessageQueue, onFinally, options = {}) {
|
|
811
|
+
const mq1 = await getMessageQueue();
|
|
812
|
+
const mq2 = await getMessageQueue();
|
|
813
|
+
const controller = new AbortController();
|
|
814
|
+
try {
|
|
815
|
+
const messages = [];
|
|
816
|
+
const listening1 = mq1.listen((message) => {
|
|
817
|
+
messages.push(message);
|
|
818
|
+
}, { signal: controller.signal });
|
|
819
|
+
const listening2 = mq2.listen((message) => {
|
|
820
|
+
messages.push(message);
|
|
821
|
+
}, { signal: controller.signal });
|
|
822
|
+
await mq1.enqueue("Hello, world!");
|
|
823
|
+
await waitFor(() => messages.length > 0, 15e3);
|
|
824
|
+
(0, node_assert_strict.deepStrictEqual)(messages, ["Hello, world!"]);
|
|
825
|
+
let started = Date.now();
|
|
826
|
+
await mq1.enqueue("Delayed message", { delay: Temporal.Duration.from({ seconds: 3 }) });
|
|
827
|
+
await waitFor(() => messages.length > 1, 15e3);
|
|
828
|
+
(0, node_assert_strict.deepStrictEqual)(messages, ["Hello, world!", "Delayed message"]);
|
|
829
|
+
(0, node_assert_strict.ok)(Date.now() - started >= 3e3, "Delayed message should be delivered after at least 3 seconds");
|
|
830
|
+
if (mq1.enqueueMany != null) {
|
|
831
|
+
while (messages.length > 0) messages.pop();
|
|
832
|
+
const batchMessages = [
|
|
833
|
+
"First batch message",
|
|
834
|
+
"Second batch message",
|
|
835
|
+
"Third batch message"
|
|
836
|
+
];
|
|
837
|
+
await mq1.enqueueMany(batchMessages);
|
|
838
|
+
await waitFor(() => messages.length >= batchMessages.length, 15e3);
|
|
839
|
+
(0, node_assert_strict.deepStrictEqual)(new Set(messages), new Set(batchMessages));
|
|
840
|
+
while (messages.length > 0) messages.pop();
|
|
841
|
+
started = Date.now();
|
|
842
|
+
const delayedBatchMessages = ["Delayed batch 1", "Delayed batch 2"];
|
|
843
|
+
await mq1.enqueueMany(delayedBatchMessages, { delay: Temporal.Duration.from({ seconds: 2 }) });
|
|
844
|
+
await waitFor(() => messages.length >= delayedBatchMessages.length, 15e3);
|
|
845
|
+
(0, node_assert_strict.deepStrictEqual)(new Set(messages), new Set(delayedBatchMessages));
|
|
846
|
+
(0, node_assert_strict.ok)(Date.now() - started >= 2e3, "Delayed batch messages should be delivered after at least 2 seconds");
|
|
847
|
+
}
|
|
848
|
+
while (messages.length > 0) messages.pop();
|
|
849
|
+
const bulkCount = 100;
|
|
850
|
+
for (let i = 0; i < bulkCount; i++) await mq1.enqueue(`message-${i}`);
|
|
851
|
+
await waitFor(() => messages.length >= bulkCount, 3e4);
|
|
852
|
+
const expectedMessages = new Set(Array.from({ length: bulkCount }, (_, i) => `message-${i}`));
|
|
853
|
+
(0, node_assert_strict.deepStrictEqual)(new Set(messages), expectedMessages);
|
|
854
|
+
if (options.testOrderingKey) {
|
|
855
|
+
while (messages.length > 0) messages.pop();
|
|
856
|
+
const orderTracker = {
|
|
857
|
+
keyA: [],
|
|
858
|
+
keyB: [],
|
|
859
|
+
noKey: []
|
|
860
|
+
};
|
|
861
|
+
controller.abort();
|
|
862
|
+
await listening1;
|
|
863
|
+
await listening2;
|
|
864
|
+
const orderController = new AbortController();
|
|
865
|
+
const orderMessages = [];
|
|
866
|
+
const orderListening1 = mq1.listen((message) => {
|
|
867
|
+
orderMessages.push(message);
|
|
868
|
+
const trackKey = message.key ?? "noKey";
|
|
869
|
+
if (trackKey in orderTracker) orderTracker[trackKey].push(message.value);
|
|
870
|
+
}, { signal: orderController.signal });
|
|
871
|
+
const orderListening2 = mq2.listen((message) => {
|
|
872
|
+
orderMessages.push(message);
|
|
873
|
+
const trackKey = message.key ?? "noKey";
|
|
874
|
+
if (trackKey in orderTracker) orderTracker[trackKey].push(message.value);
|
|
875
|
+
}, { signal: orderController.signal });
|
|
876
|
+
await mq1.enqueue({
|
|
877
|
+
key: "keyA",
|
|
878
|
+
value: 1
|
|
879
|
+
}, { orderingKey: "keyA" });
|
|
880
|
+
await mq1.enqueue({
|
|
881
|
+
key: "keyB",
|
|
882
|
+
value: 1
|
|
883
|
+
}, { orderingKey: "keyB" });
|
|
884
|
+
await mq1.enqueue({
|
|
885
|
+
key: "keyA",
|
|
886
|
+
value: 2
|
|
887
|
+
}, { orderingKey: "keyA" });
|
|
888
|
+
await mq1.enqueue({
|
|
889
|
+
key: "keyB",
|
|
890
|
+
value: 2
|
|
891
|
+
}, { orderingKey: "keyB" });
|
|
892
|
+
await mq1.enqueue({
|
|
893
|
+
key: "keyA",
|
|
894
|
+
value: 3
|
|
895
|
+
}, { orderingKey: "keyA" });
|
|
896
|
+
await mq1.enqueue({
|
|
897
|
+
key: "keyB",
|
|
898
|
+
value: 3
|
|
899
|
+
}, { orderingKey: "keyB" });
|
|
900
|
+
await mq1.enqueue({
|
|
901
|
+
key: null,
|
|
902
|
+
value: 1
|
|
903
|
+
});
|
|
904
|
+
await mq1.enqueue({
|
|
905
|
+
key: null,
|
|
906
|
+
value: 2
|
|
907
|
+
});
|
|
908
|
+
await waitFor(() => orderMessages.length >= 8, 3e4);
|
|
909
|
+
(0, node_assert_strict.deepStrictEqual)(orderTracker.keyA, [
|
|
910
|
+
1,
|
|
911
|
+
2,
|
|
912
|
+
3
|
|
913
|
+
], "Messages with orderingKey 'keyA' should be processed in order");
|
|
914
|
+
(0, node_assert_strict.deepStrictEqual)(orderTracker.keyB, [
|
|
915
|
+
1,
|
|
916
|
+
2,
|
|
917
|
+
3
|
|
918
|
+
], "Messages with orderingKey 'keyB' should be processed in order");
|
|
919
|
+
(0, node_assert_strict.strictEqual)(orderTracker.noKey.length, 2, "Messages without ordering key should all be received");
|
|
920
|
+
(0, node_assert_strict.ok)(orderTracker.noKey.includes(1) && orderTracker.noKey.includes(2), "Messages without ordering key should contain values 1 and 2");
|
|
921
|
+
orderController.abort();
|
|
922
|
+
await orderListening1;
|
|
923
|
+
await orderListening2;
|
|
924
|
+
} else {
|
|
925
|
+
controller.abort();
|
|
926
|
+
await listening1;
|
|
927
|
+
await listening2;
|
|
928
|
+
}
|
|
929
|
+
} finally {
|
|
930
|
+
await onFinally({
|
|
931
|
+
mq1,
|
|
932
|
+
mq2,
|
|
933
|
+
controller
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
async function waitFor(predicate, timeoutMs) {
|
|
938
|
+
const started = Date.now();
|
|
939
|
+
while (!predicate()) {
|
|
940
|
+
await (0, es_toolkit.delay)(500);
|
|
941
|
+
if (Date.now() - started > timeoutMs) throw new Error("Timeout");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const getRandomKey = (prefix) => `fedify_test_${prefix}_${crypto.randomUUID()}`;
|
|
945
|
+
|
|
729
946
|
//#endregion
|
|
730
947
|
exports.createContext = createContext;
|
|
731
948
|
exports.createFederation = createFederation;
|
|
732
949
|
exports.createInboxContext = createInboxContext;
|
|
733
|
-
exports.createRequestContext = createRequestContext;
|
|
950
|
+
exports.createRequestContext = createRequestContext;
|
|
951
|
+
exports.getRandomKey = getRandomKey;
|
|
952
|
+
exports.testMessageQueue = testMessageQueue;
|
|
953
|
+
exports.waitFor = waitFor;
|
package/dist/mod.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Context, Federation, InboxContext, RequestContext } from "@fedify/fedify/federation";
|
|
2
|
-
import { Activity } from "@fedify/
|
|
2
|
+
import { Activity } from "@fedify/vocab";
|
|
3
|
+
import { MessageQueue } from "@fedify/fedify";
|
|
3
4
|
|
|
4
5
|
//#region src/context.d.ts
|
|
5
6
|
declare function createContext<TContextData>(values: Partial<Context<TContextData>> & {
|
|
@@ -91,7 +92,7 @@ interface TestFederation<TContextData> extends Omit<Federation<TContextData>, "c
|
|
|
91
92
|
*
|
|
92
93
|
* @example
|
|
93
94
|
* ```typescript
|
|
94
|
-
* import { Create } from "@fedify/
|
|
95
|
+
* import { Create } from "@fedify/vocab";
|
|
95
96
|
* import { createFederation } from "@fedify/testing";
|
|
96
97
|
*
|
|
97
98
|
* // Create a mock federation with contextData
|
|
@@ -123,4 +124,74 @@ declare function createFederation<TContextData>(options?: {
|
|
|
123
124
|
tracerProvider?: any;
|
|
124
125
|
}): TestFederation<TContextData>;
|
|
125
126
|
//#endregion
|
|
126
|
-
|
|
127
|
+
//#region src/mq-tester.d.ts
|
|
128
|
+
/**
|
|
129
|
+
* Options for {@link testMessageQueue}.
|
|
130
|
+
*/
|
|
131
|
+
interface TestMessageQueueOptions {
|
|
132
|
+
/**
|
|
133
|
+
* Whether to test ordering key support. If `true`, tests will verify that
|
|
134
|
+
* messages with the same ordering key are processed in order, while messages
|
|
135
|
+
* with different ordering keys can be processed in parallel.
|
|
136
|
+
*
|
|
137
|
+
* Set this to `true` only if your message queue implementation supports
|
|
138
|
+
* the `orderingKey` option.
|
|
139
|
+
*
|
|
140
|
+
* @default false
|
|
141
|
+
*/
|
|
142
|
+
readonly testOrderingKey?: boolean;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Tests a {@link MessageQueue} implementation with a standard set of tests.
|
|
146
|
+
*
|
|
147
|
+
* This function runs tests for:
|
|
148
|
+
* - `enqueue()`: Basic message enqueueing
|
|
149
|
+
* - `enqueue()` with delay: Delayed message enqueueing
|
|
150
|
+
* - `enqueueMany()`: Bulk message enqueueing
|
|
151
|
+
* - `enqueueMany()` with delay: Delayed bulk message enqueueing
|
|
152
|
+
* - Multiple listeners: Ensures messages are processed by only one listener
|
|
153
|
+
* - Ordering key support (optional): Ensures messages with the same ordering
|
|
154
|
+
* key are processed in order
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript ignore
|
|
158
|
+
* import { test } from "@fedify/fixture";
|
|
159
|
+
* import { testMessageQueue } from "@fedify/testing";
|
|
160
|
+
* import { MyMessageQueue } from "./my-mq.ts";
|
|
161
|
+
*
|
|
162
|
+
* test("MyMessageQueue", () =>
|
|
163
|
+
* testMessageQueue(
|
|
164
|
+
* () => new MyMessageQueue(),
|
|
165
|
+
* async ({ mq1, mq2, controller }) => {
|
|
166
|
+
* controller.abort();
|
|
167
|
+
* await mq1.close();
|
|
168
|
+
* await mq2.close();
|
|
169
|
+
* },
|
|
170
|
+
* { testOrderingKey: true }, // Enable ordering key tests
|
|
171
|
+
* )
|
|
172
|
+
* );
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* @param getMessageQueue A factory function that creates a new message queue
|
|
176
|
+
* instance. It should return a new instance each time
|
|
177
|
+
* to ensure test isolation, but both instances should
|
|
178
|
+
* share the same underlying storage/channel.
|
|
179
|
+
* @param onFinally A cleanup function called after all tests complete.
|
|
180
|
+
* It receives both message queue instances and the abort
|
|
181
|
+
* controller used for the listeners.
|
|
182
|
+
* @param options Optional configuration for the test suite.
|
|
183
|
+
* @returns A promise that resolves when all tests pass.
|
|
184
|
+
*/
|
|
185
|
+
declare function testMessageQueue<MQ extends MessageQueue>(getMessageQueue: () => MQ | Promise<MQ>, onFinally: ({
|
|
186
|
+
mq1,
|
|
187
|
+
mq2,
|
|
188
|
+
controller
|
|
189
|
+
}: {
|
|
190
|
+
mq1: MQ;
|
|
191
|
+
mq2: MQ;
|
|
192
|
+
controller: AbortController;
|
|
193
|
+
}) => Promise<void> | void, options?: TestMessageQueueOptions): Promise<void>;
|
|
194
|
+
declare function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void>;
|
|
195
|
+
declare const getRandomKey: (prefix: string) => string;
|
|
196
|
+
//#endregion
|
|
197
|
+
export { TestMessageQueueOptions, createContext, createFederation, createInboxContext, createRequestContext, getRandomKey, testMessageQueue, waitFor };
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
+
import { Activity } from "@fedify/vocab";
|
|
1
3
|
import { Context, Federation, InboxContext, RequestContext } from "@fedify/fedify/federation";
|
|
2
|
-
import {
|
|
4
|
+
import { MessageQueue } from "@fedify/fedify";
|
|
3
5
|
|
|
4
6
|
//#region src/context.d.ts
|
|
5
7
|
declare function createContext<TContextData>(values: Partial<Context<TContextData>> & {
|
|
@@ -91,7 +93,7 @@ interface TestFederation<TContextData> extends Omit<Federation<TContextData>, "c
|
|
|
91
93
|
*
|
|
92
94
|
* @example
|
|
93
95
|
* ```typescript
|
|
94
|
-
* import { Create } from "@fedify/
|
|
96
|
+
* import { Create } from "@fedify/vocab";
|
|
95
97
|
* import { createFederation } from "@fedify/testing";
|
|
96
98
|
*
|
|
97
99
|
* // Create a mock federation with contextData
|
|
@@ -123,4 +125,74 @@ declare function createFederation<TContextData>(options?: {
|
|
|
123
125
|
tracerProvider?: any;
|
|
124
126
|
}): TestFederation<TContextData>;
|
|
125
127
|
//#endregion
|
|
126
|
-
|
|
128
|
+
//#region src/mq-tester.d.ts
|
|
129
|
+
/**
|
|
130
|
+
* Options for {@link testMessageQueue}.
|
|
131
|
+
*/
|
|
132
|
+
interface TestMessageQueueOptions {
|
|
133
|
+
/**
|
|
134
|
+
* Whether to test ordering key support. If `true`, tests will verify that
|
|
135
|
+
* messages with the same ordering key are processed in order, while messages
|
|
136
|
+
* with different ordering keys can be processed in parallel.
|
|
137
|
+
*
|
|
138
|
+
* Set this to `true` only if your message queue implementation supports
|
|
139
|
+
* the `orderingKey` option.
|
|
140
|
+
*
|
|
141
|
+
* @default false
|
|
142
|
+
*/
|
|
143
|
+
readonly testOrderingKey?: boolean;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Tests a {@link MessageQueue} implementation with a standard set of tests.
|
|
147
|
+
*
|
|
148
|
+
* This function runs tests for:
|
|
149
|
+
* - `enqueue()`: Basic message enqueueing
|
|
150
|
+
* - `enqueue()` with delay: Delayed message enqueueing
|
|
151
|
+
* - `enqueueMany()`: Bulk message enqueueing
|
|
152
|
+
* - `enqueueMany()` with delay: Delayed bulk message enqueueing
|
|
153
|
+
* - Multiple listeners: Ensures messages are processed by only one listener
|
|
154
|
+
* - Ordering key support (optional): Ensures messages with the same ordering
|
|
155
|
+
* key are processed in order
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript ignore
|
|
159
|
+
* import { test } from "@fedify/fixture";
|
|
160
|
+
* import { testMessageQueue } from "@fedify/testing";
|
|
161
|
+
* import { MyMessageQueue } from "./my-mq.ts";
|
|
162
|
+
*
|
|
163
|
+
* test("MyMessageQueue", () =>
|
|
164
|
+
* testMessageQueue(
|
|
165
|
+
* () => new MyMessageQueue(),
|
|
166
|
+
* async ({ mq1, mq2, controller }) => {
|
|
167
|
+
* controller.abort();
|
|
168
|
+
* await mq1.close();
|
|
169
|
+
* await mq2.close();
|
|
170
|
+
* },
|
|
171
|
+
* { testOrderingKey: true }, // Enable ordering key tests
|
|
172
|
+
* )
|
|
173
|
+
* );
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* @param getMessageQueue A factory function that creates a new message queue
|
|
177
|
+
* instance. It should return a new instance each time
|
|
178
|
+
* to ensure test isolation, but both instances should
|
|
179
|
+
* share the same underlying storage/channel.
|
|
180
|
+
* @param onFinally A cleanup function called after all tests complete.
|
|
181
|
+
* It receives both message queue instances and the abort
|
|
182
|
+
* controller used for the listeners.
|
|
183
|
+
* @param options Optional configuration for the test suite.
|
|
184
|
+
* @returns A promise that resolves when all tests pass.
|
|
185
|
+
*/
|
|
186
|
+
declare function testMessageQueue<MQ extends MessageQueue>(getMessageQueue: () => MQ | Promise<MQ>, onFinally: ({
|
|
187
|
+
mq1,
|
|
188
|
+
mq2,
|
|
189
|
+
controller
|
|
190
|
+
}: {
|
|
191
|
+
mq1: MQ;
|
|
192
|
+
mq2: MQ;
|
|
193
|
+
controller: AbortController;
|
|
194
|
+
}) => Promise<void> | void, options?: TestMessageQueueOptions): Promise<void>;
|
|
195
|
+
declare function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void>;
|
|
196
|
+
declare const getRandomKey: (prefix: string) => string;
|
|
197
|
+
//#endregion
|
|
198
|
+
export { TestMessageQueueOptions, createContext, createFederation, createInboxContext, createRequestContext, getRandomKey, testMessageQueue, waitFor };
|
package/dist/mod.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
3
|
+
|
|
4
|
+
import { CryptographicKey, Multikey, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
1
5
|
import { RouterError } from "@fedify/fedify/federation";
|
|
2
|
-
import {
|
|
6
|
+
import { delay } from "es-toolkit";
|
|
7
|
+
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
3
8
|
|
|
4
9
|
//#region src/docloader.ts
|
|
5
10
|
const mockDocumentLoader = async (url) => ({
|
|
@@ -16,7 +21,7 @@ const noopTracerProvider$1 = { getTracer: () => ({
|
|
|
16
21
|
}) };
|
|
17
22
|
function createContext(values) {
|
|
18
23
|
const { federation, url = new URL("http://example.com/"), canonicalOrigin, data, documentLoader, contextLoader, tracerProvider, clone, getNodeInfoUri, getActorUri, getObjectUri, getCollectionUri, getOutboxUri, getInboxUri, getFollowingUri, getFollowersUri, getLikedUri, getFeaturedUri, getFeaturedTagsUri, parseUri, getActorKeyPairs, getDocumentLoader, lookupObject: lookupObject$1, traverseCollection: traverseCollection$1, lookupNodeInfo, lookupWebFinger, sendActivity, routeActivity } = values;
|
|
19
|
-
function
|
|
24
|
+
function throwRouterError() {
|
|
20
25
|
throw new RouterError("Not implemented");
|
|
21
26
|
}
|
|
22
27
|
return {
|
|
@@ -33,17 +38,17 @@ function createContext(values) {
|
|
|
33
38
|
...values,
|
|
34
39
|
data: data$1
|
|
35
40
|
})),
|
|
36
|
-
getNodeInfoUri: getNodeInfoUri ??
|
|
37
|
-
getActorUri: getActorUri ??
|
|
38
|
-
getObjectUri: getObjectUri ??
|
|
39
|
-
getCollectionUri: getCollectionUri ??
|
|
40
|
-
getOutboxUri: getOutboxUri ??
|
|
41
|
-
getInboxUri: getInboxUri ??
|
|
42
|
-
getFollowingUri: getFollowingUri ??
|
|
43
|
-
getFollowersUri: getFollowersUri ??
|
|
44
|
-
getLikedUri: getLikedUri ??
|
|
45
|
-
getFeaturedUri: getFeaturedUri ??
|
|
46
|
-
getFeaturedTagsUri: getFeaturedTagsUri ??
|
|
41
|
+
getNodeInfoUri: getNodeInfoUri ?? throwRouterError,
|
|
42
|
+
getActorUri: getActorUri ?? throwRouterError,
|
|
43
|
+
getObjectUri: getObjectUri ?? throwRouterError,
|
|
44
|
+
getCollectionUri: getCollectionUri ?? throwRouterError,
|
|
45
|
+
getOutboxUri: getOutboxUri ?? throwRouterError,
|
|
46
|
+
getInboxUri: getInboxUri ?? throwRouterError,
|
|
47
|
+
getFollowingUri: getFollowingUri ?? throwRouterError,
|
|
48
|
+
getFollowersUri: getFollowersUri ?? throwRouterError,
|
|
49
|
+
getLikedUri: getLikedUri ?? throwRouterError,
|
|
50
|
+
getFeaturedUri: getFeaturedUri ?? throwRouterError,
|
|
51
|
+
getFeaturedTagsUri: getFeaturedTagsUri ?? throwRouterError,
|
|
47
52
|
parseUri: parseUri ?? ((_uri) => {
|
|
48
53
|
throw new Error("Not implemented");
|
|
49
54
|
}),
|
|
@@ -148,7 +153,7 @@ function expandUriTemplate(template, values) {
|
|
|
148
153
|
*
|
|
149
154
|
* @example
|
|
150
155
|
* ```typescript
|
|
151
|
-
* import { Create } from "@fedify/
|
|
156
|
+
* import { Create } from "@fedify/vocab";
|
|
152
157
|
* import { createFederation } from "@fedify/testing";
|
|
153
158
|
*
|
|
154
159
|
* // Create a mock federation with contextData
|
|
@@ -182,6 +187,7 @@ var MockFederation = class {
|
|
|
182
187
|
nodeInfoDispatcher;
|
|
183
188
|
webFingerDispatcher;
|
|
184
189
|
actorDispatchers = /* @__PURE__ */ new Map();
|
|
190
|
+
actorKeyPairsDispatcher;
|
|
185
191
|
actorPath;
|
|
186
192
|
inboxPath;
|
|
187
193
|
outboxPath;
|
|
@@ -219,7 +225,10 @@ var MockFederation = class {
|
|
|
219
225
|
this.actorDispatchers.set(path, dispatcher);
|
|
220
226
|
this.actorPath = path;
|
|
221
227
|
return {
|
|
222
|
-
setKeyPairsDispatcher: () =>
|
|
228
|
+
setKeyPairsDispatcher: (keyPairsDispatcher) => {
|
|
229
|
+
this.actorKeyPairsDispatcher = keyPairsDispatcher;
|
|
230
|
+
return this;
|
|
231
|
+
},
|
|
223
232
|
mapHandle: () => this,
|
|
224
233
|
mapAlias: () => this,
|
|
225
234
|
authorize: () => this
|
|
@@ -321,6 +330,7 @@ var MockFederation = class {
|
|
|
321
330
|
}
|
|
322
331
|
};
|
|
323
332
|
}
|
|
333
|
+
setOutboxPermanentFailureHandler(_handler) {}
|
|
324
334
|
async startQueue(contextData, options) {
|
|
325
335
|
this.contextData = contextData;
|
|
326
336
|
this.queueStarted = true;
|
|
@@ -405,7 +415,7 @@ var MockFederation = class {
|
|
|
405
415
|
*
|
|
406
416
|
* @example
|
|
407
417
|
* ```typescript
|
|
408
|
-
* import { Create } from "@fedify/
|
|
418
|
+
* import { Create } from "@fedify/vocab";
|
|
409
419
|
* import { createFederation } from "@fedify/testing";
|
|
410
420
|
*
|
|
411
421
|
* // Create a mock federation with contextData
|
|
@@ -447,7 +457,7 @@ function createFederation(options = {}) {
|
|
|
447
457
|
*
|
|
448
458
|
* @example
|
|
449
459
|
* ```typescript
|
|
450
|
-
* import { Person, Create } from "@fedify/
|
|
460
|
+
* import { Person, Create } from "@fedify/vocab";
|
|
451
461
|
* import { createFederation } from "@fedify/testing";
|
|
452
462
|
*
|
|
453
463
|
* // Create a mock federation and context
|
|
@@ -508,11 +518,22 @@ var MockContext = class MockContext {
|
|
|
508
518
|
this.contextLoader = options.contextLoader ?? this.documentLoader;
|
|
509
519
|
this.tracerProvider = options.tracerProvider ?? noopTracerProvider;
|
|
510
520
|
}
|
|
511
|
-
getActor(
|
|
512
|
-
|
|
521
|
+
async getActor(handle) {
|
|
522
|
+
if (this.federation instanceof MockFederation && this.federation.actorPath) {
|
|
523
|
+
const dispatcher = this.federation.actorDispatchers.get(this.federation.actorPath);
|
|
524
|
+
if (dispatcher) return await dispatcher(this, handle);
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
513
527
|
}
|
|
514
|
-
getObject(
|
|
515
|
-
|
|
528
|
+
async getObject(cls, values) {
|
|
529
|
+
if (this.federation instanceof MockFederation) {
|
|
530
|
+
const path = this.federation.objectPaths.get(cls.typeId.href);
|
|
531
|
+
if (path) {
|
|
532
|
+
const dispatcher = this.federation.objectDispatchers.get(path);
|
|
533
|
+
if (dispatcher) return await dispatcher(this, values);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
516
537
|
}
|
|
517
538
|
getSignedKey() {
|
|
518
539
|
return Promise.resolve(null);
|
|
@@ -644,8 +665,25 @@ var MockContext = class MockContext {
|
|
|
644
665
|
}
|
|
645
666
|
return null;
|
|
646
667
|
}
|
|
647
|
-
getActorKeyPairs(
|
|
648
|
-
|
|
668
|
+
async getActorKeyPairs(identifier) {
|
|
669
|
+
if (this.federation instanceof MockFederation && this.federation.actorKeyPairsDispatcher) {
|
|
670
|
+
const keyPairs = await this.federation.actorKeyPairsDispatcher(this, identifier);
|
|
671
|
+
const owner = this.getActorUri(identifier);
|
|
672
|
+
return keyPairs.map((kp) => ({
|
|
673
|
+
...kp,
|
|
674
|
+
cryptographicKey: new CryptographicKey({
|
|
675
|
+
id: kp.keyId,
|
|
676
|
+
owner,
|
|
677
|
+
publicKey: kp.publicKey
|
|
678
|
+
}),
|
|
679
|
+
multikey: new Multikey({
|
|
680
|
+
id: kp.keyId,
|
|
681
|
+
controller: owner,
|
|
682
|
+
publicKey: kp.publicKey
|
|
683
|
+
})
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
return [];
|
|
649
687
|
}
|
|
650
688
|
getDocumentLoader(params) {
|
|
651
689
|
if ("keyId" in params) return this.documentLoader;
|
|
@@ -704,4 +742,183 @@ var MockContext = class MockContext {
|
|
|
704
742
|
};
|
|
705
743
|
|
|
706
744
|
//#endregion
|
|
707
|
-
|
|
745
|
+
//#region src/mq-tester.ts
|
|
746
|
+
/**
|
|
747
|
+
* Tests a {@link MessageQueue} implementation with a standard set of tests.
|
|
748
|
+
*
|
|
749
|
+
* This function runs tests for:
|
|
750
|
+
* - `enqueue()`: Basic message enqueueing
|
|
751
|
+
* - `enqueue()` with delay: Delayed message enqueueing
|
|
752
|
+
* - `enqueueMany()`: Bulk message enqueueing
|
|
753
|
+
* - `enqueueMany()` with delay: Delayed bulk message enqueueing
|
|
754
|
+
* - Multiple listeners: Ensures messages are processed by only one listener
|
|
755
|
+
* - Ordering key support (optional): Ensures messages with the same ordering
|
|
756
|
+
* key are processed in order
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* ```typescript ignore
|
|
760
|
+
* import { test } from "@fedify/fixture";
|
|
761
|
+
* import { testMessageQueue } from "@fedify/testing";
|
|
762
|
+
* import { MyMessageQueue } from "./my-mq.ts";
|
|
763
|
+
*
|
|
764
|
+
* test("MyMessageQueue", () =>
|
|
765
|
+
* testMessageQueue(
|
|
766
|
+
* () => new MyMessageQueue(),
|
|
767
|
+
* async ({ mq1, mq2, controller }) => {
|
|
768
|
+
* controller.abort();
|
|
769
|
+
* await mq1.close();
|
|
770
|
+
* await mq2.close();
|
|
771
|
+
* },
|
|
772
|
+
* { testOrderingKey: true }, // Enable ordering key tests
|
|
773
|
+
* )
|
|
774
|
+
* );
|
|
775
|
+
* ```
|
|
776
|
+
*
|
|
777
|
+
* @param getMessageQueue A factory function that creates a new message queue
|
|
778
|
+
* instance. It should return a new instance each time
|
|
779
|
+
* to ensure test isolation, but both instances should
|
|
780
|
+
* share the same underlying storage/channel.
|
|
781
|
+
* @param onFinally A cleanup function called after all tests complete.
|
|
782
|
+
* It receives both message queue instances and the abort
|
|
783
|
+
* controller used for the listeners.
|
|
784
|
+
* @param options Optional configuration for the test suite.
|
|
785
|
+
* @returns A promise that resolves when all tests pass.
|
|
786
|
+
*/
|
|
787
|
+
async function testMessageQueue(getMessageQueue, onFinally, options = {}) {
|
|
788
|
+
const mq1 = await getMessageQueue();
|
|
789
|
+
const mq2 = await getMessageQueue();
|
|
790
|
+
const controller = new AbortController();
|
|
791
|
+
try {
|
|
792
|
+
const messages = [];
|
|
793
|
+
const listening1 = mq1.listen((message) => {
|
|
794
|
+
messages.push(message);
|
|
795
|
+
}, { signal: controller.signal });
|
|
796
|
+
const listening2 = mq2.listen((message) => {
|
|
797
|
+
messages.push(message);
|
|
798
|
+
}, { signal: controller.signal });
|
|
799
|
+
await mq1.enqueue("Hello, world!");
|
|
800
|
+
await waitFor(() => messages.length > 0, 15e3);
|
|
801
|
+
deepStrictEqual(messages, ["Hello, world!"]);
|
|
802
|
+
let started = Date.now();
|
|
803
|
+
await mq1.enqueue("Delayed message", { delay: Temporal.Duration.from({ seconds: 3 }) });
|
|
804
|
+
await waitFor(() => messages.length > 1, 15e3);
|
|
805
|
+
deepStrictEqual(messages, ["Hello, world!", "Delayed message"]);
|
|
806
|
+
ok(Date.now() - started >= 3e3, "Delayed message should be delivered after at least 3 seconds");
|
|
807
|
+
if (mq1.enqueueMany != null) {
|
|
808
|
+
while (messages.length > 0) messages.pop();
|
|
809
|
+
const batchMessages = [
|
|
810
|
+
"First batch message",
|
|
811
|
+
"Second batch message",
|
|
812
|
+
"Third batch message"
|
|
813
|
+
];
|
|
814
|
+
await mq1.enqueueMany(batchMessages);
|
|
815
|
+
await waitFor(() => messages.length >= batchMessages.length, 15e3);
|
|
816
|
+
deepStrictEqual(new Set(messages), new Set(batchMessages));
|
|
817
|
+
while (messages.length > 0) messages.pop();
|
|
818
|
+
started = Date.now();
|
|
819
|
+
const delayedBatchMessages = ["Delayed batch 1", "Delayed batch 2"];
|
|
820
|
+
await mq1.enqueueMany(delayedBatchMessages, { delay: Temporal.Duration.from({ seconds: 2 }) });
|
|
821
|
+
await waitFor(() => messages.length >= delayedBatchMessages.length, 15e3);
|
|
822
|
+
deepStrictEqual(new Set(messages), new Set(delayedBatchMessages));
|
|
823
|
+
ok(Date.now() - started >= 2e3, "Delayed batch messages should be delivered after at least 2 seconds");
|
|
824
|
+
}
|
|
825
|
+
while (messages.length > 0) messages.pop();
|
|
826
|
+
const bulkCount = 100;
|
|
827
|
+
for (let i = 0; i < bulkCount; i++) await mq1.enqueue(`message-${i}`);
|
|
828
|
+
await waitFor(() => messages.length >= bulkCount, 3e4);
|
|
829
|
+
const expectedMessages = new Set(Array.from({ length: bulkCount }, (_, i) => `message-${i}`));
|
|
830
|
+
deepStrictEqual(new Set(messages), expectedMessages);
|
|
831
|
+
if (options.testOrderingKey) {
|
|
832
|
+
while (messages.length > 0) messages.pop();
|
|
833
|
+
const orderTracker = {
|
|
834
|
+
keyA: [],
|
|
835
|
+
keyB: [],
|
|
836
|
+
noKey: []
|
|
837
|
+
};
|
|
838
|
+
controller.abort();
|
|
839
|
+
await listening1;
|
|
840
|
+
await listening2;
|
|
841
|
+
const orderController = new AbortController();
|
|
842
|
+
const orderMessages = [];
|
|
843
|
+
const orderListening1 = mq1.listen((message) => {
|
|
844
|
+
orderMessages.push(message);
|
|
845
|
+
const trackKey = message.key ?? "noKey";
|
|
846
|
+
if (trackKey in orderTracker) orderTracker[trackKey].push(message.value);
|
|
847
|
+
}, { signal: orderController.signal });
|
|
848
|
+
const orderListening2 = mq2.listen((message) => {
|
|
849
|
+
orderMessages.push(message);
|
|
850
|
+
const trackKey = message.key ?? "noKey";
|
|
851
|
+
if (trackKey in orderTracker) orderTracker[trackKey].push(message.value);
|
|
852
|
+
}, { signal: orderController.signal });
|
|
853
|
+
await mq1.enqueue({
|
|
854
|
+
key: "keyA",
|
|
855
|
+
value: 1
|
|
856
|
+
}, { orderingKey: "keyA" });
|
|
857
|
+
await mq1.enqueue({
|
|
858
|
+
key: "keyB",
|
|
859
|
+
value: 1
|
|
860
|
+
}, { orderingKey: "keyB" });
|
|
861
|
+
await mq1.enqueue({
|
|
862
|
+
key: "keyA",
|
|
863
|
+
value: 2
|
|
864
|
+
}, { orderingKey: "keyA" });
|
|
865
|
+
await mq1.enqueue({
|
|
866
|
+
key: "keyB",
|
|
867
|
+
value: 2
|
|
868
|
+
}, { orderingKey: "keyB" });
|
|
869
|
+
await mq1.enqueue({
|
|
870
|
+
key: "keyA",
|
|
871
|
+
value: 3
|
|
872
|
+
}, { orderingKey: "keyA" });
|
|
873
|
+
await mq1.enqueue({
|
|
874
|
+
key: "keyB",
|
|
875
|
+
value: 3
|
|
876
|
+
}, { orderingKey: "keyB" });
|
|
877
|
+
await mq1.enqueue({
|
|
878
|
+
key: null,
|
|
879
|
+
value: 1
|
|
880
|
+
});
|
|
881
|
+
await mq1.enqueue({
|
|
882
|
+
key: null,
|
|
883
|
+
value: 2
|
|
884
|
+
});
|
|
885
|
+
await waitFor(() => orderMessages.length >= 8, 3e4);
|
|
886
|
+
deepStrictEqual(orderTracker.keyA, [
|
|
887
|
+
1,
|
|
888
|
+
2,
|
|
889
|
+
3
|
|
890
|
+
], "Messages with orderingKey 'keyA' should be processed in order");
|
|
891
|
+
deepStrictEqual(orderTracker.keyB, [
|
|
892
|
+
1,
|
|
893
|
+
2,
|
|
894
|
+
3
|
|
895
|
+
], "Messages with orderingKey 'keyB' should be processed in order");
|
|
896
|
+
strictEqual(orderTracker.noKey.length, 2, "Messages without ordering key should all be received");
|
|
897
|
+
ok(orderTracker.noKey.includes(1) && orderTracker.noKey.includes(2), "Messages without ordering key should contain values 1 and 2");
|
|
898
|
+
orderController.abort();
|
|
899
|
+
await orderListening1;
|
|
900
|
+
await orderListening2;
|
|
901
|
+
} else {
|
|
902
|
+
controller.abort();
|
|
903
|
+
await listening1;
|
|
904
|
+
await listening2;
|
|
905
|
+
}
|
|
906
|
+
} finally {
|
|
907
|
+
await onFinally({
|
|
908
|
+
mq1,
|
|
909
|
+
mq2,
|
|
910
|
+
controller
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
async function waitFor(predicate, timeoutMs) {
|
|
915
|
+
const started = Date.now();
|
|
916
|
+
while (!predicate()) {
|
|
917
|
+
await delay(500);
|
|
918
|
+
if (Date.now() - started > timeoutMs) throw new Error("Timeout");
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
const getRandomKey = (prefix) => `fedify_test_${prefix}_${crypto.randomUUID()}`;
|
|
922
|
+
|
|
923
|
+
//#endregion
|
|
924
|
+
export { createContext, createFederation, createInboxContext, createRequestContext, getRandomKey, testMessageQueue, waitFor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/testing",
|
|
3
|
-
"version": "2.0.0-pr.
|
|
3
|
+
"version": "2.0.0-pr.559.4+6357309b",
|
|
4
4
|
"description": "Testing utilities for Fedify applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fedify",
|
|
@@ -50,21 +50,28 @@
|
|
|
50
50
|
"package.json"
|
|
51
51
|
],
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@fedify/fedify": "^2.0.0-pr.
|
|
53
|
+
"@fedify/fedify": "^2.0.0-pr.559.4+6357309b"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"es-toolkit": "1.43.0"
|
|
54
57
|
},
|
|
55
58
|
"devDependencies": {
|
|
56
59
|
"@js-temporal/polyfill": "^0.5.1",
|
|
57
60
|
"@std/assert": "npm:@jsr/std__assert@^1.0.13",
|
|
58
61
|
"@std/async": "npm:@jsr/std__async@^1.0.13",
|
|
59
62
|
"tsdown": "^0.12.9",
|
|
60
|
-
"typescript": "^5.9.3"
|
|
63
|
+
"typescript": "^5.9.3",
|
|
64
|
+
"@fedify/fixture": "^2.0.0"
|
|
61
65
|
},
|
|
62
66
|
"scripts": {
|
|
63
|
-
"build": "tsdown",
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
+
"build:self": "tsdown",
|
|
68
|
+
"build": "pnpm --filter @fedify/testing... run build:self",
|
|
69
|
+
"prepublish": "pnpm build",
|
|
70
|
+
"pretest": "pnpm build",
|
|
71
|
+
"test": "node --experimental-transform-types --test",
|
|
72
|
+
"pretest:bun": "pnpm build",
|
|
73
|
+
"test:bun": "bun test --timeout 15000",
|
|
67
74
|
"test:deno": "deno task test",
|
|
68
|
-
"test-all": "
|
|
75
|
+
"test-all": "pnpm build && node --experimental-transform-types --test && bun test --timeout 15000 && deno task test"
|
|
69
76
|
}
|
|
70
77
|
}
|