@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright 2024–2025 Hong Minhee
3
+ Copyright 2024–2026 Hong Minhee
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
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/fedify/vocab";
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
- - Track sent activities with metadata
101
- - Simulate activity reception
102
- - Configure custom URI templates
103
- - Test queue-based activity processing
104
- - Mock document loaders and context loaders
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 __fedify_fedify_vocab = __toESM(require("@fedify/fedify/vocab"));
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 throwRouteError() {
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 ?? throwRouteError,
60
- getActorUri: getActorUri ?? throwRouteError,
61
- getObjectUri: getObjectUri ?? throwRouteError,
62
- getCollectionUri: getCollectionUri ?? throwRouteError,
63
- getOutboxUri: getOutboxUri ?? throwRouteError,
64
- getInboxUri: getInboxUri ?? throwRouteError,
65
- getFollowingUri: getFollowingUri ?? throwRouteError,
66
- getFollowersUri: getFollowersUri ?? throwRouteError,
67
- getLikedUri: getLikedUri ?? throwRouteError,
68
- getFeaturedUri: getFeaturedUri ?? throwRouteError,
69
- getFeaturedTagsUri: getFeaturedTagsUri ?? throwRouteError,
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, __fedify_fedify_vocab.lookupObject)(uri, {
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, __fedify_fedify_vocab.traverseCollection)(collection, {
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/fedify/vocab";
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: () => this,
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/fedify/vocab";
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/fedify/vocab";
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(_handle) {
535
- return Promise.resolve(null);
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(_cls, _values) {
538
- return Promise.resolve(null);
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(_identifier) {
671
- return Promise.resolve([]);
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/fedify/vocab";
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/fedify/vocab";
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
- export { createContext, createFederation, createInboxContext, createRequestContext };
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 { Activity } from "@fedify/fedify/vocab";
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/fedify/vocab";
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
- export { createContext, createFederation, createInboxContext, createRequestContext };
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 { lookupObject, traverseCollection } from "@fedify/fedify/vocab";
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 throwRouteError() {
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 ?? throwRouteError,
37
- getActorUri: getActorUri ?? throwRouteError,
38
- getObjectUri: getObjectUri ?? throwRouteError,
39
- getCollectionUri: getCollectionUri ?? throwRouteError,
40
- getOutboxUri: getOutboxUri ?? throwRouteError,
41
- getInboxUri: getInboxUri ?? throwRouteError,
42
- getFollowingUri: getFollowingUri ?? throwRouteError,
43
- getFollowersUri: getFollowersUri ?? throwRouteError,
44
- getLikedUri: getLikedUri ?? throwRouteError,
45
- getFeaturedUri: getFeaturedUri ?? throwRouteError,
46
- getFeaturedTagsUri: getFeaturedTagsUri ?? throwRouteError,
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/fedify/vocab";
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: () => this,
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/fedify/vocab";
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/fedify/vocab";
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(_handle) {
512
- return Promise.resolve(null);
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(_cls, _values) {
515
- return Promise.resolve(null);
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(_identifier) {
648
- return Promise.resolve([]);
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
- export { createContext, createFederation, createInboxContext, createRequestContext };
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.490.2+99a396d5",
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.490.2+99a396d5"
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
- "prepublish": "tsdown",
65
- "test": "tsdown && node --experimental-transform-types --test",
66
- "test:bun": "tsdown && bun test --timeout 15000",
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": "tsdown && node --experimental-transform-types --test && bun test --timeout 15000 && deno task test"
75
+ "test-all": "pnpm build && node --experimental-transform-types --test && bun test --timeout 15000 && deno task test"
69
76
  }
70
77
  }