@fedify/testing 2.0.0-dev.1604 → 2.0.0-dev.1690

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/dist/mod.cjs ADDED
@@ -0,0 +1,667 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ const __opentelemetry_api = __toESM(require("@opentelemetry/api"));
25
+ const __fedify_fedify_federation = __toESM(require("@fedify/fedify/federation"));
26
+ const __fedify_fedify_vocab = __toESM(require("@fedify/fedify/vocab"));
27
+ const __fedify_fedify_webfinger = __toESM(require("@fedify/fedify/webfinger"));
28
+
29
+ //#region src/docloader.ts
30
+ const mockDocumentLoader = async (url) => ({
31
+ contextUrl: null,
32
+ document: {},
33
+ documentUrl: url
34
+ });
35
+
36
+ //#endregion
37
+ //#region src/context.ts
38
+ function createContext(values) {
39
+ 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;
40
+ function throwRouteError() {
41
+ throw new __fedify_fedify_federation.RouterError("Not implemented");
42
+ }
43
+ return {
44
+ federation,
45
+ data,
46
+ origin: url.origin,
47
+ canonicalOrigin: canonicalOrigin ?? url.origin,
48
+ host: url.host,
49
+ hostname: url.hostname,
50
+ documentLoader: documentLoader ?? mockDocumentLoader,
51
+ contextLoader: contextLoader ?? mockDocumentLoader,
52
+ tracerProvider: tracerProvider ?? __opentelemetry_api.trace.getTracerProvider(),
53
+ clone: clone ?? ((data$1) => createContext({
54
+ ...values,
55
+ data: data$1
56
+ })),
57
+ getNodeInfoUri: getNodeInfoUri ?? throwRouteError,
58
+ getActorUri: getActorUri ?? throwRouteError,
59
+ getObjectUri: getObjectUri ?? throwRouteError,
60
+ getCollectionUri: getCollectionUri ?? throwRouteError,
61
+ getOutboxUri: getOutboxUri ?? throwRouteError,
62
+ getInboxUri: getInboxUri ?? throwRouteError,
63
+ getFollowingUri: getFollowingUri ?? throwRouteError,
64
+ getFollowersUri: getFollowersUri ?? throwRouteError,
65
+ getLikedUri: getLikedUri ?? throwRouteError,
66
+ getFeaturedUri: getFeaturedUri ?? throwRouteError,
67
+ getFeaturedTagsUri: getFeaturedTagsUri ?? throwRouteError,
68
+ parseUri: parseUri ?? ((_uri) => {
69
+ throw new Error("Not implemented");
70
+ }),
71
+ getDocumentLoader: getDocumentLoader ?? ((_params) => {
72
+ throw new Error("Not implemented");
73
+ }),
74
+ getActorKeyPairs: getActorKeyPairs ?? ((_handle) => Promise.resolve([])),
75
+ lookupObject: lookupObject ?? ((uri, options = {}) => {
76
+ return (0, __fedify_fedify_vocab.lookupObject)(uri, {
77
+ documentLoader: options.documentLoader ?? documentLoader ?? mockDocumentLoader,
78
+ contextLoader: options.contextLoader ?? contextLoader ?? mockDocumentLoader
79
+ });
80
+ }),
81
+ traverseCollection: traverseCollection ?? ((collection, options = {}) => {
82
+ return (0, __fedify_fedify_vocab.traverseCollection)(collection, {
83
+ documentLoader: options.documentLoader ?? documentLoader ?? mockDocumentLoader,
84
+ contextLoader: options.contextLoader ?? contextLoader ?? mockDocumentLoader
85
+ });
86
+ }),
87
+ lookupNodeInfo: lookupNodeInfo ?? ((_params) => {
88
+ throw new Error("Not implemented");
89
+ }),
90
+ lookupWebFinger: lookupWebFinger ?? ((resource, options = {}) => {
91
+ return (0, __fedify_fedify_webfinger.lookupWebFinger)(resource, options);
92
+ }),
93
+ sendActivity: sendActivity ?? ((_params) => {
94
+ throw new Error("Not implemented");
95
+ }),
96
+ routeActivity: routeActivity ?? ((_params) => {
97
+ throw new Error("Not implemented");
98
+ })
99
+ };
100
+ }
101
+ function createRequestContext(args) {
102
+ return {
103
+ ...createContext(args),
104
+ clone: args.clone ?? ((data) => createRequestContext({
105
+ ...args,
106
+ data
107
+ })),
108
+ request: args.request ?? new Request(args.url),
109
+ url: args.url,
110
+ getActor: args.getActor ?? (() => Promise.resolve(null)),
111
+ getObject: args.getObject ?? (() => Promise.resolve(null)),
112
+ getSignedKey: args.getSignedKey ?? (() => Promise.resolve(null)),
113
+ getSignedKeyOwner: args.getSignedKeyOwner ?? (() => Promise.resolve(null)),
114
+ sendActivity: args.sendActivity ?? ((_params) => {
115
+ throw new Error("Not implemented");
116
+ })
117
+ };
118
+ }
119
+ function createInboxContext(args) {
120
+ return {
121
+ ...createContext(args),
122
+ clone: args.clone ?? ((data) => createInboxContext({
123
+ ...args,
124
+ data
125
+ })),
126
+ recipient: args.recipient ?? null,
127
+ forwardActivity: args.forwardActivity ?? ((_params) => {
128
+ throw new Error("Not implemented");
129
+ })
130
+ };
131
+ }
132
+
133
+ //#endregion
134
+ //#region src/mock.ts
135
+ /**
136
+ * Helper function to expand URI templates with values.
137
+ * Supports simple placeholders like {identifier}, {handle}, etc.
138
+ * @param template The URI template pattern
139
+ * @param values The values to substitute
140
+ * @returns The expanded URI path
141
+ */
142
+ function expandUriTemplate(template, values) {
143
+ return template.replace(/{([^}]+)}/g, (match, key) => {
144
+ return values[key] || match;
145
+ });
146
+ }
147
+ /**
148
+ * A mock implementation of the {@link Federation} interface for unit testing.
149
+ * This class provides a way to test Fedify applications without needing
150
+ * a real federation setup.
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * import { Create } from "@fedify/fedify/vocab";
155
+ * import { MockFederation } from "@fedify/testing";
156
+ *
157
+ * // Create a mock federation with contextData
158
+ * const federation = new MockFederation<{ userId: string }>({
159
+ * contextData: { userId: "test-user" }
160
+ * });
161
+ *
162
+ * // Set up inbox listeners
163
+ * federation
164
+ * .setInboxListeners("/users/{identifier}/inbox")
165
+ * .on(Create, async (ctx, activity) => {
166
+ * console.log("Received:", activity);
167
+ * });
168
+ *
169
+ * // Simulate receiving an activity
170
+ * const createActivity = new Create({
171
+ * id: new URL("https://example.com/create/1"),
172
+ * actor: new URL("https://example.com/users/alice")
173
+ * });
174
+ * await federation.receiveActivity(createActivity);
175
+ * ```
176
+ *
177
+ * @template TContextData The context data to pass to the {@link Context}.
178
+ * @since 1.8.0
179
+ */
180
+ var MockFederation = class {
181
+ sentActivities = [];
182
+ queueStarted = false;
183
+ activeQueues = /* @__PURE__ */ new Set();
184
+ sentCounter = 0;
185
+ nodeInfoDispatcher;
186
+ webFingerDispatcher;
187
+ actorDispatchers = /* @__PURE__ */ new Map();
188
+ actorPath;
189
+ inboxPath;
190
+ outboxPath;
191
+ followingPath;
192
+ followersPath;
193
+ likedPath;
194
+ featuredPath;
195
+ featuredTagsPath;
196
+ nodeInfoPath;
197
+ sharedInboxPath;
198
+ objectPaths = /* @__PURE__ */ new Map();
199
+ objectDispatchers = /* @__PURE__ */ new Map();
200
+ inboxDispatcher;
201
+ outboxDispatcher;
202
+ followingDispatcher;
203
+ followersDispatcher;
204
+ likedDispatcher;
205
+ featuredDispatcher;
206
+ featuredTagsDispatcher;
207
+ inboxListeners = /* @__PURE__ */ new Map();
208
+ contextData;
209
+ receivedActivities = [];
210
+ constructor(options = {}) {
211
+ this.options = options;
212
+ this.contextData = options.contextData;
213
+ }
214
+ setNodeInfoDispatcher(path, dispatcher) {
215
+ this.nodeInfoDispatcher = dispatcher;
216
+ this.nodeInfoPath = path;
217
+ }
218
+ setWebFingerLinksDispatcher(dispatcher) {
219
+ this.webFingerDispatcher = dispatcher;
220
+ }
221
+ setActorDispatcher(path, dispatcher) {
222
+ this.actorDispatchers.set(path, dispatcher);
223
+ this.actorPath = path;
224
+ return {
225
+ setKeyPairsDispatcher: () => this,
226
+ mapHandle: () => this,
227
+ mapAlias: () => this,
228
+ authorize: () => this
229
+ };
230
+ }
231
+ setObjectDispatcher(cls, path, dispatcher) {
232
+ this.objectDispatchers.set(path, dispatcher);
233
+ this.objectPaths.set(cls.typeId.href, path);
234
+ return { authorize: () => this };
235
+ }
236
+ setInboxDispatcher(_path, dispatcher) {
237
+ this.inboxDispatcher = dispatcher;
238
+ return {
239
+ setCounter: () => this,
240
+ setFirstCursor: () => this,
241
+ setLastCursor: () => this,
242
+ authorize: () => this
243
+ };
244
+ }
245
+ setOutboxDispatcher(path, dispatcher) {
246
+ this.outboxDispatcher = dispatcher;
247
+ this.outboxPath = path;
248
+ return {
249
+ setCounter: () => this,
250
+ setFirstCursor: () => this,
251
+ setLastCursor: () => this,
252
+ authorize: () => this
253
+ };
254
+ }
255
+ setFollowingDispatcher(path, dispatcher) {
256
+ this.followingDispatcher = dispatcher;
257
+ this.followingPath = path;
258
+ return {
259
+ setCounter: () => this,
260
+ setFirstCursor: () => this,
261
+ setLastCursor: () => this,
262
+ authorize: () => this
263
+ };
264
+ }
265
+ setFollowersDispatcher(path, dispatcher) {
266
+ this.followersDispatcher = dispatcher;
267
+ this.followersPath = path;
268
+ return {
269
+ setCounter: () => this,
270
+ setFirstCursor: () => this,
271
+ setLastCursor: () => this,
272
+ authorize: () => this
273
+ };
274
+ }
275
+ setLikedDispatcher(path, dispatcher) {
276
+ this.likedDispatcher = dispatcher;
277
+ this.likedPath = path;
278
+ return {
279
+ setCounter: () => this,
280
+ setFirstCursor: () => this,
281
+ setLastCursor: () => this,
282
+ authorize: () => this
283
+ };
284
+ }
285
+ setFeaturedDispatcher(path, dispatcher) {
286
+ this.featuredDispatcher = dispatcher;
287
+ this.featuredPath = path;
288
+ return {
289
+ setCounter: () => this,
290
+ setFirstCursor: () => this,
291
+ setLastCursor: () => this,
292
+ authorize: () => this
293
+ };
294
+ }
295
+ setFeaturedTagsDispatcher(path, dispatcher) {
296
+ this.featuredTagsDispatcher = dispatcher;
297
+ this.featuredTagsPath = path;
298
+ return {
299
+ setCounter: () => this,
300
+ setFirstCursor: () => this,
301
+ setLastCursor: () => this,
302
+ authorize: () => this
303
+ };
304
+ }
305
+ setInboxListeners(inboxPath, sharedInboxPath) {
306
+ this.inboxPath = inboxPath;
307
+ this.sharedInboxPath = sharedInboxPath;
308
+ const self = this;
309
+ return {
310
+ on(type, listener) {
311
+ const typeName = type.name;
312
+ if (!self.inboxListeners.has(typeName)) self.inboxListeners.set(typeName, []);
313
+ self.inboxListeners.get(typeName).push(listener);
314
+ return this;
315
+ },
316
+ onError() {
317
+ return this;
318
+ },
319
+ setSharedKeyDispatcher() {
320
+ return this;
321
+ },
322
+ withIdempotency() {
323
+ return this;
324
+ }
325
+ };
326
+ }
327
+ async startQueue(contextData, options) {
328
+ this.contextData = contextData;
329
+ this.queueStarted = true;
330
+ if (options?.queue) this.activeQueues.add(options.queue);
331
+ else {
332
+ this.activeQueues.add("inbox");
333
+ this.activeQueues.add("outbox");
334
+ this.activeQueues.add("fanout");
335
+ }
336
+ }
337
+ async processQueuedTask(contextData, _message) {
338
+ this.contextData = contextData;
339
+ }
340
+ createContext(baseUrlOrRequest, contextData) {
341
+ const mockFederation = this;
342
+ if (baseUrlOrRequest instanceof Request) return createRequestContext({
343
+ url: new URL(baseUrlOrRequest.url),
344
+ request: baseUrlOrRequest,
345
+ data: contextData,
346
+ federation: mockFederation,
347
+ sendActivity: async (sender, recipients, activity, options) => {
348
+ const tempContext = new MockContext({
349
+ url: new URL(baseUrlOrRequest.url),
350
+ data: contextData,
351
+ federation: mockFederation
352
+ });
353
+ await tempContext.sendActivity(sender, recipients, activity, options);
354
+ }
355
+ });
356
+ else return new MockContext({
357
+ url: baseUrlOrRequest,
358
+ data: contextData,
359
+ federation: mockFederation
360
+ });
361
+ }
362
+ async fetch(request, options) {
363
+ if (options.onNotFound) return options.onNotFound(request);
364
+ return new Response("Not Found", { status: 404 });
365
+ }
366
+ /**
367
+ * Simulates receiving an activity. This method is specific to the mock
368
+ * implementation and is used for testing purposes.
369
+ *
370
+ * @param activity The activity to receive.
371
+ * @returns A promise that resolves when the activity has been processed.
372
+ * @since 1.8.0
373
+ */
374
+ async receiveActivity(activity) {
375
+ this.receivedActivities.push(activity);
376
+ const typeName = activity.constructor.name;
377
+ const listeners = this.inboxListeners.get(typeName) || [];
378
+ if (listeners.length > 0 && this.contextData === void 0) throw new Error("MockFederation.receiveActivity(): contextData is not initialized. Please provide contextData through the constructor or call startQueue() before receiving activities.");
379
+ for (const listener of listeners) {
380
+ const context = createInboxContext({
381
+ data: this.contextData,
382
+ federation: this
383
+ });
384
+ await listener(context, activity);
385
+ }
386
+ }
387
+ /**
388
+ * Clears all sent activities from the mock federation.
389
+ * This method is specific to the mock implementation and is used for
390
+ * testing purposes.
391
+ *
392
+ * @since 1.8.0
393
+ */
394
+ reset() {
395
+ this.sentActivities = [];
396
+ }
397
+ setCollectionDispatcher(_name, _itemType, _path, _dispatcher) {
398
+ return {
399
+ setCounter: () => this,
400
+ setFirstCursor: () => this,
401
+ setLastCursor: () => this,
402
+ authorize: () => this
403
+ };
404
+ }
405
+ setOrderedCollectionDispatcher(_name, _itemType, _path, _dispatcher) {
406
+ return {
407
+ setCounter: () => this,
408
+ setFirstCursor: () => this,
409
+ setLastCursor: () => this,
410
+ authorize: () => this
411
+ };
412
+ }
413
+ };
414
+ /**
415
+ * A mock implementation of the {@link Context} interface for unit testing.
416
+ * This class provides a way to test Fedify applications without needing
417
+ * a real federation context.
418
+ *
419
+ * @example
420
+ * ```typescript
421
+ * import { Person, Create } from "@fedify/fedify/vocab";
422
+ * import { MockContext, MockFederation } from "@fedify/testing";
423
+ *
424
+ * // Create a mock context
425
+ * const mockFederation = new MockFederation<{ userId: string }>();
426
+ * const context = new MockContext({
427
+ * url: new URL("https://example.com"),
428
+ * data: { userId: "test-user" },
429
+ * federation: mockFederation
430
+ * });
431
+ *
432
+ * // Send an activity
433
+ * const recipient = new Person({ id: new URL("https://example.com/users/bob") });
434
+ * const activity = new Create({
435
+ * id: new URL("https://example.com/create/1"),
436
+ * actor: new URL("https://example.com/users/alice")
437
+ * });
438
+ * await context.sendActivity(
439
+ * { identifier: "alice" },
440
+ * recipient,
441
+ * activity
442
+ * );
443
+ *
444
+ * // Check sent activities
445
+ * const sent = context.getSentActivities();
446
+ * console.log(sent[0].activity);
447
+ * ```
448
+ *
449
+ * @template TContextData The context data to pass to the {@link Context}.
450
+ * @since 1.8.0
451
+ */
452
+ var MockContext = class MockContext {
453
+ origin;
454
+ canonicalOrigin;
455
+ host;
456
+ hostname;
457
+ data;
458
+ federation;
459
+ documentLoader;
460
+ contextLoader;
461
+ tracerProvider;
462
+ sentActivities = [];
463
+ constructor(options) {
464
+ const url = options.url ?? new URL("https://example.com");
465
+ this.origin = url.origin;
466
+ this.canonicalOrigin = url.origin;
467
+ this.host = url.host;
468
+ this.hostname = url.hostname;
469
+ this.data = options.data;
470
+ this.federation = options.federation;
471
+ this.documentLoader = options.documentLoader ?? (async (url$1) => ({
472
+ contextUrl: null,
473
+ document: {},
474
+ documentUrl: url$1
475
+ }));
476
+ this.contextLoader = options.contextLoader ?? this.documentLoader;
477
+ this.tracerProvider = options.tracerProvider ?? __opentelemetry_api.trace.getTracerProvider();
478
+ }
479
+ clone(data) {
480
+ return new MockContext({
481
+ url: new URL(this.origin),
482
+ data,
483
+ federation: this.federation,
484
+ documentLoader: this.documentLoader,
485
+ contextLoader: this.contextLoader,
486
+ tracerProvider: this.tracerProvider
487
+ });
488
+ }
489
+ getNodeInfoUri() {
490
+ if (this.federation instanceof MockFederation && this.federation.nodeInfoPath) return new URL(this.federation.nodeInfoPath, this.origin);
491
+ return new URL("/nodeinfo/2.0", this.origin);
492
+ }
493
+ getActorUri(identifier) {
494
+ if (this.federation instanceof MockFederation && this.federation.actorPath) {
495
+ const path = expandUriTemplate(this.federation.actorPath, {
496
+ identifier,
497
+ handle: identifier
498
+ });
499
+ return new URL(path, this.origin);
500
+ }
501
+ return new URL(`/users/${identifier}`, this.origin);
502
+ }
503
+ getObjectUri(cls, values) {
504
+ if (this.federation instanceof MockFederation) {
505
+ const pathTemplate = this.federation.objectPaths.get(cls.typeId.href);
506
+ if (pathTemplate) {
507
+ const path$1 = expandUriTemplate(pathTemplate, values);
508
+ return new URL(path$1, this.origin);
509
+ }
510
+ }
511
+ const path = globalThis.Object.entries(values).map(([key, value]) => `${key}/${value}`).join("/");
512
+ return new URL(`/objects/${cls.name.toLowerCase()}/${path}`, this.origin);
513
+ }
514
+ getOutboxUri(identifier) {
515
+ if (this.federation instanceof MockFederation && this.federation.outboxPath) {
516
+ const path = expandUriTemplate(this.federation.outboxPath, {
517
+ identifier,
518
+ handle: identifier
519
+ });
520
+ return new URL(path, this.origin);
521
+ }
522
+ return new URL(`/users/${identifier}/outbox`, this.origin);
523
+ }
524
+ getInboxUri(identifier) {
525
+ if (identifier) {
526
+ if (this.federation instanceof MockFederation && this.federation.inboxPath) {
527
+ const path = expandUriTemplate(this.federation.inboxPath, {
528
+ identifier,
529
+ handle: identifier
530
+ });
531
+ return new URL(path, this.origin);
532
+ }
533
+ return new URL(`/users/${identifier}/inbox`, this.origin);
534
+ }
535
+ if (this.federation instanceof MockFederation && this.federation.sharedInboxPath) return new URL(this.federation.sharedInboxPath, this.origin);
536
+ return new URL("/inbox", this.origin);
537
+ }
538
+ getFollowingUri(identifier) {
539
+ if (this.federation instanceof MockFederation && this.federation.followingPath) {
540
+ const path = expandUriTemplate(this.federation.followingPath, {
541
+ identifier,
542
+ handle: identifier
543
+ });
544
+ return new URL(path, this.origin);
545
+ }
546
+ return new URL(`/users/${identifier}/following`, this.origin);
547
+ }
548
+ getFollowersUri(identifier) {
549
+ if (this.federation instanceof MockFederation && this.federation.followersPath) {
550
+ const path = expandUriTemplate(this.federation.followersPath, {
551
+ identifier,
552
+ handle: identifier
553
+ });
554
+ return new URL(path, this.origin);
555
+ }
556
+ return new URL(`/users/${identifier}/followers`, this.origin);
557
+ }
558
+ getLikedUri(identifier) {
559
+ if (this.federation instanceof MockFederation && this.federation.likedPath) {
560
+ const path = expandUriTemplate(this.federation.likedPath, {
561
+ identifier,
562
+ handle: identifier
563
+ });
564
+ return new URL(path, this.origin);
565
+ }
566
+ return new URL(`/users/${identifier}/liked`, this.origin);
567
+ }
568
+ getFeaturedUri(identifier) {
569
+ if (this.federation instanceof MockFederation && this.federation.featuredPath) {
570
+ const path = expandUriTemplate(this.federation.featuredPath, {
571
+ identifier,
572
+ handle: identifier
573
+ });
574
+ return new URL(path, this.origin);
575
+ }
576
+ return new URL(`/users/${identifier}/featured`, this.origin);
577
+ }
578
+ getFeaturedTagsUri(identifier) {
579
+ if (this.federation instanceof MockFederation && this.federation.featuredTagsPath) {
580
+ const path = expandUriTemplate(this.federation.featuredTagsPath, {
581
+ identifier,
582
+ handle: identifier
583
+ });
584
+ return new URL(path, this.origin);
585
+ }
586
+ return new URL(`/users/${identifier}/tags`, this.origin);
587
+ }
588
+ getCollectionUri(_name, values) {
589
+ const path = globalThis.Object.entries(values).map(([key, value]) => `${key}/${value}`).join("/");
590
+ return new URL(`/collections/${String(_name)}/${path}`, this.origin);
591
+ }
592
+ parseUri(uri) {
593
+ if (uri.pathname.startsWith("/users/")) {
594
+ const parts = uri.pathname.split("/");
595
+ if (parts.length >= 3) return {
596
+ type: "actor",
597
+ identifier: parts[2],
598
+ handle: parts[2]
599
+ };
600
+ }
601
+ return null;
602
+ }
603
+ getActorKeyPairs(_identifier) {
604
+ return Promise.resolve([]);
605
+ }
606
+ getDocumentLoader(params) {
607
+ if ("keyId" in params) return this.documentLoader;
608
+ return Promise.resolve(this.documentLoader);
609
+ }
610
+ lookupObject(_uri, _options) {
611
+ return Promise.resolve(null);
612
+ }
613
+ traverseCollection(_collection, _options) {
614
+ return { async *[Symbol.asyncIterator]() {} };
615
+ }
616
+ lookupNodeInfo(_url, _options) {
617
+ return Promise.resolve(void 0);
618
+ }
619
+ lookupWebFinger(_resource, _options) {
620
+ return Promise.resolve(null);
621
+ }
622
+ sendActivity(sender, recipients, activity, _options) {
623
+ this.sentActivities.push({
624
+ sender,
625
+ recipients,
626
+ activity
627
+ });
628
+ if (this.federation instanceof MockFederation) {
629
+ const queued = this.federation.queueStarted;
630
+ this.federation.sentActivities.push({
631
+ queued,
632
+ queue: queued ? "outbox" : void 0,
633
+ activity,
634
+ sentOrder: ++this.federation.sentCounter
635
+ });
636
+ }
637
+ return Promise.resolve();
638
+ }
639
+ routeActivity(_recipient, _activity, _options) {
640
+ return Promise.resolve(true);
641
+ }
642
+ /**
643
+ * Gets all activities that have been sent through this mock context.
644
+ * This method is specific to the mock implementation and is used for
645
+ * testing purposes.
646
+ *
647
+ * @returns An array of sent activity records.
648
+ */
649
+ getSentActivities() {
650
+ return [...this.sentActivities];
651
+ }
652
+ /**
653
+ * Clears all sent activities from the mock context.
654
+ * This method is specific to the mock implementation and is used for
655
+ * testing purposes.
656
+ */
657
+ reset() {
658
+ this.sentActivities = [];
659
+ }
660
+ };
661
+
662
+ //#endregion
663
+ exports.MockContext = MockContext;
664
+ exports.MockFederation = MockFederation;
665
+ exports.createContext = createContext;
666
+ exports.createInboxContext = createInboxContext;
667
+ exports.createRequestContext = createRequestContext;
package/dist/mod.d.cts ADDED
@@ -0,0 +1,297 @@
1
+ import { ActorCallbackSetters, ActorDispatcher, ActorKeyPair, CollectionCallbackSetters, CollectionDispatcher, Context, Federation, FederationFetchOptions, FederationStartQueueOptions, InboxContext, InboxListenerSetters, Message, NodeInfoDispatcher, ObjectCallbackSetters, ObjectDispatcher, ParseUriResult, RequestContext, RouteActivityOptions, SendActivityOptions, SendActivityOptionsForCollection, SenderKeyPair, WebFingerLinksDispatcher } from "@fedify/fedify/federation";
2
+ import { JsonValue, NodeInfo } from "@fedify/fedify/nodeinfo";
3
+ import { DocumentLoader } from "@fedify/fedify/runtime";
4
+ import { Activity, Actor, Collection, Hashtag, LookupObjectOptions, Object as Object$1, Recipient, TraverseCollectionOptions } from "@fedify/fedify/vocab";
5
+ import { ResourceDescriptor } from "@fedify/fedify/webfinger";
6
+ import { TracerProvider } from "@opentelemetry/api";
7
+
8
+ //#region src/mock.d.ts
9
+
10
+ /**
11
+ * Represents a sent activity with metadata about how it was sent.
12
+ * @since 1.8.0
13
+ */
14
+ interface SentActivity {
15
+ /** Whether the activity was queued or sent immediately. */
16
+ queued: boolean;
17
+ /** Which queue was used (if queued). */
18
+ queue?: "inbox" | "outbox" | "fanout";
19
+ /** The activity that was sent. */
20
+ activity: Activity;
21
+ /** The order in which the activity was sent (auto-incrementing counter). */
22
+ sentOrder: number;
23
+ }
24
+ /**
25
+ * A mock implementation of the {@link Federation} interface for unit testing.
26
+ * This class provides a way to test Fedify applications without needing
27
+ * a real federation setup.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import { Create } from "@fedify/fedify/vocab";
32
+ * import { MockFederation } from "@fedify/testing";
33
+ *
34
+ * // Create a mock federation with contextData
35
+ * const federation = new MockFederation<{ userId: string }>({
36
+ * contextData: { userId: "test-user" }
37
+ * });
38
+ *
39
+ * // Set up inbox listeners
40
+ * federation
41
+ * .setInboxListeners("/users/{identifier}/inbox")
42
+ * .on(Create, async (ctx, activity) => {
43
+ * console.log("Received:", activity);
44
+ * });
45
+ *
46
+ * // Simulate receiving an activity
47
+ * const createActivity = new Create({
48
+ * id: new URL("https://example.com/create/1"),
49
+ * actor: new URL("https://example.com/users/alice")
50
+ * });
51
+ * await federation.receiveActivity(createActivity);
52
+ * ```
53
+ *
54
+ * @template TContextData The context data to pass to the {@link Context}.
55
+ * @since 1.8.0
56
+ */
57
+ declare class MockFederation<TContextData> implements Federation<TContextData> {
58
+ private options;
59
+ sentActivities: SentActivity[];
60
+ queueStarted: boolean;
61
+ private activeQueues;
62
+ sentCounter: number;
63
+ private nodeInfoDispatcher?;
64
+ private webFingerDispatcher?;
65
+ private actorDispatchers;
66
+ actorPath?: string;
67
+ inboxPath?: string;
68
+ outboxPath?: string;
69
+ followingPath?: string;
70
+ followersPath?: string;
71
+ likedPath?: string;
72
+ featuredPath?: string;
73
+ featuredTagsPath?: string;
74
+ nodeInfoPath?: string;
75
+ sharedInboxPath?: string;
76
+ objectPaths: Map<string, string>;
77
+ private objectDispatchers;
78
+ private inboxDispatcher?;
79
+ private outboxDispatcher?;
80
+ private followingDispatcher?;
81
+ private followersDispatcher?;
82
+ private likedDispatcher?;
83
+ private featuredDispatcher?;
84
+ private featuredTagsDispatcher?;
85
+ private inboxListeners;
86
+ private contextData?;
87
+ private receivedActivities;
88
+ constructor(options?: {
89
+ contextData?: TContextData;
90
+ origin?: string;
91
+ tracerProvider?: TracerProvider;
92
+ });
93
+ setNodeInfoDispatcher(path: string, dispatcher: NodeInfoDispatcher<TContextData>): void;
94
+ setWebFingerLinksDispatcher(dispatcher: WebFingerLinksDispatcher<TContextData>): void;
95
+ setActorDispatcher(path: `${string}{identifier}${string}` | `${string}{handle}${string}`, dispatcher: ActorDispatcher<TContextData>): ActorCallbackSetters<TContextData>;
96
+ setObjectDispatcher<TObject extends Object$1, TParam extends string>(cls: (new (...args: any[]) => TObject) & {
97
+ typeId: URL;
98
+ }, path: string, dispatcher: ObjectDispatcher<TContextData, TObject, TParam>): ObjectCallbackSetters<TContextData, TObject, TParam>;
99
+ setInboxDispatcher(_path: `${string}{identifier}${string}` | `${string}{handle}${string}`, dispatcher: CollectionDispatcher<Activity, RequestContext<TContextData>, TContextData, void>): CollectionCallbackSetters<RequestContext<TContextData>, TContextData, void>;
100
+ setOutboxDispatcher(path: `${string}{identifier}${string}` | `${string}{handle}${string}`, dispatcher: CollectionDispatcher<Activity, RequestContext<TContextData>, TContextData, void>): CollectionCallbackSetters<RequestContext<TContextData>, TContextData, void>;
101
+ setFollowingDispatcher(path: `${string}{identifier}${string}` | `${string}{handle}${string}`, dispatcher: CollectionDispatcher<Actor | URL, RequestContext<TContextData>, TContextData, void>): CollectionCallbackSetters<RequestContext<TContextData>, TContextData, void>;
102
+ setFollowersDispatcher(path: `${string}{identifier}${string}` | `${string}{handle}${string}`, dispatcher: CollectionDispatcher<Recipient, Context<TContextData>, TContextData, URL>): CollectionCallbackSetters<Context<TContextData>, TContextData, URL>;
103
+ setLikedDispatcher(path: `${string}{identifier}${string}` | `${string}{handle}${string}`, dispatcher: CollectionDispatcher<Object$1 | URL, RequestContext<TContextData>, TContextData, void>): CollectionCallbackSetters<RequestContext<TContextData>, TContextData, void>;
104
+ setFeaturedDispatcher(path: `${string}{identifier}${string}` | `${string}{handle}${string}`, dispatcher: CollectionDispatcher<Object$1, RequestContext<TContextData>, TContextData, void>): CollectionCallbackSetters<RequestContext<TContextData>, TContextData, void>;
105
+ setFeaturedTagsDispatcher(path: `${string}{identifier}${string}` | `${string}{handle}${string}`, dispatcher: CollectionDispatcher<Hashtag, RequestContext<TContextData>, TContextData, void>): CollectionCallbackSetters<RequestContext<TContextData>, TContextData, void>;
106
+ setInboxListeners(inboxPath: `${string}{identifier}${string}` | `${string}{handle}${string}`, sharedInboxPath?: string): InboxListenerSetters<TContextData>;
107
+ startQueue(contextData: TContextData, options?: FederationStartQueueOptions): Promise<void>;
108
+ processQueuedTask(contextData: TContextData, _message: Message): Promise<void>;
109
+ createContext(baseUrl: URL, contextData: TContextData): Context<TContextData>;
110
+ createContext(request: Request, contextData: TContextData): RequestContext<TContextData>;
111
+ fetch(request: Request, options: FederationFetchOptions<TContextData>): Promise<Response>;
112
+ /**
113
+ * Simulates receiving an activity. This method is specific to the mock
114
+ * implementation and is used for testing purposes.
115
+ *
116
+ * @param activity The activity to receive.
117
+ * @returns A promise that resolves when the activity has been processed.
118
+ * @since 1.8.0
119
+ */
120
+ receiveActivity(activity: Activity): Promise<void>;
121
+ /**
122
+ * Clears all sent activities from the mock federation.
123
+ * This method is specific to the mock implementation and is used for
124
+ * testing purposes.
125
+ *
126
+ * @since 1.8.0
127
+ */
128
+ reset(): void;
129
+ setCollectionDispatcher<TObject extends Object$1, TParams extends Record<string, string>>(_name: string | symbol, _itemType: any, _path: any, _dispatcher: any): any;
130
+ setOrderedCollectionDispatcher<TObject extends Object$1, TParams extends Record<string, string>>(_name: string | symbol, _itemType: any, _path: any, _dispatcher: any): any;
131
+ }
132
+ /**
133
+ * A mock implementation of the {@link Context} interface for unit testing.
134
+ * This class provides a way to test Fedify applications without needing
135
+ * a real federation context.
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * import { Person, Create } from "@fedify/fedify/vocab";
140
+ * import { MockContext, MockFederation } from "@fedify/testing";
141
+ *
142
+ * // Create a mock context
143
+ * const mockFederation = new MockFederation<{ userId: string }>();
144
+ * const context = new MockContext({
145
+ * url: new URL("https://example.com"),
146
+ * data: { userId: "test-user" },
147
+ * federation: mockFederation
148
+ * });
149
+ *
150
+ * // Send an activity
151
+ * const recipient = new Person({ id: new URL("https://example.com/users/bob") });
152
+ * const activity = new Create({
153
+ * id: new URL("https://example.com/create/1"),
154
+ * actor: new URL("https://example.com/users/alice")
155
+ * });
156
+ * await context.sendActivity(
157
+ * { identifier: "alice" },
158
+ * recipient,
159
+ * activity
160
+ * );
161
+ *
162
+ * // Check sent activities
163
+ * const sent = context.getSentActivities();
164
+ * console.log(sent[0].activity);
165
+ * ```
166
+ *
167
+ * @template TContextData The context data to pass to the {@link Context}.
168
+ * @since 1.8.0
169
+ */
170
+ declare class MockContext<TContextData> implements Context<TContextData> {
171
+ readonly origin: string;
172
+ readonly canonicalOrigin: string;
173
+ readonly host: string;
174
+ readonly hostname: string;
175
+ readonly data: TContextData;
176
+ readonly federation: Federation<TContextData>;
177
+ readonly documentLoader: DocumentLoader;
178
+ readonly contextLoader: DocumentLoader;
179
+ readonly tracerProvider: TracerProvider;
180
+ private sentActivities;
181
+ constructor(options: {
182
+ url?: URL;
183
+ data: TContextData;
184
+ federation: Federation<TContextData>;
185
+ documentLoader?: DocumentLoader;
186
+ contextLoader?: DocumentLoader;
187
+ tracerProvider?: TracerProvider;
188
+ });
189
+ clone(data: TContextData): Context<TContextData>;
190
+ getNodeInfoUri(): URL;
191
+ getActorUri(identifier: string): URL;
192
+ getObjectUri<TObject extends Object$1>(cls: (new (...args: any[]) => TObject) & {
193
+ typeId: URL;
194
+ }, values: Record<string, string>): URL;
195
+ getOutboxUri(identifier: string): URL;
196
+ getInboxUri(identifier: string): URL;
197
+ getInboxUri(): URL;
198
+ getFollowingUri(identifier: string): URL;
199
+ getFollowersUri(identifier: string): URL;
200
+ getLikedUri(identifier: string): URL;
201
+ getFeaturedUri(identifier: string): URL;
202
+ getFeaturedTagsUri(identifier: string): URL;
203
+ getCollectionUri<TParam extends Record<string, string>>(_name: string | symbol, values: TParam): URL;
204
+ parseUri(uri: URL): ParseUriResult | null;
205
+ getActorKeyPairs(_identifier: string): Promise<ActorKeyPair[]>;
206
+ getDocumentLoader(params: {
207
+ handle: string;
208
+ } | {
209
+ identifier: string;
210
+ }): Promise<DocumentLoader>;
211
+ getDocumentLoader(params: {
212
+ keyId: URL;
213
+ privateKey: CryptoKey;
214
+ }): DocumentLoader;
215
+ lookupObject(_uri: URL | string, _options?: LookupObjectOptions): Promise<Object$1 | null>;
216
+ traverseCollection<TItem, TContext extends Context<TContextData>>(_collection: Collection | URL | null, _options?: TraverseCollectionOptions): AsyncIterable<TItem>;
217
+ lookupNodeInfo(url: URL | string, options?: {
218
+ parse?: "strict" | "best-effort";
219
+ } & any): Promise<NodeInfo | undefined>;
220
+ lookupNodeInfo(url: URL | string, options?: {
221
+ parse: "none";
222
+ } & any): Promise<JsonValue | undefined>;
223
+ lookupWebFinger(_resource: URL | `acct:${string}@${string}` | string, _options?: any): Promise<ResourceDescriptor | null>;
224
+ sendActivity(sender: SenderKeyPair | SenderKeyPair[] | {
225
+ identifier: string;
226
+ } | {
227
+ username: string;
228
+ } | {
229
+ handle: string;
230
+ }, recipients: Recipient | Recipient[], activity: Activity, options?: SendActivityOptions): Promise<void>;
231
+ sendActivity(sender: {
232
+ identifier: string;
233
+ } | {
234
+ username: string;
235
+ } | {
236
+ handle: string;
237
+ }, recipients: "followers", activity: Activity, options?: SendActivityOptionsForCollection): Promise<void>;
238
+ sendActivity(sender: SenderKeyPair | SenderKeyPair[] | {
239
+ identifier: string;
240
+ } | {
241
+ username: string;
242
+ } | {
243
+ handle: string;
244
+ }, recipients: Recipient | Recipient[], activity: Activity, options?: SendActivityOptions): Promise<void>;
245
+ sendActivity(sender: {
246
+ identifier: string;
247
+ } | {
248
+ username: string;
249
+ } | {
250
+ handle: string;
251
+ }, recipients: "followers", activity: Activity, options?: SendActivityOptionsForCollection): Promise<void>;
252
+ routeActivity(_recipient: string | null, _activity: Activity, _options?: RouteActivityOptions): Promise<boolean>;
253
+ /**
254
+ * Gets all activities that have been sent through this mock context.
255
+ * This method is specific to the mock implementation and is used for
256
+ * testing purposes.
257
+ *
258
+ * @returns An array of sent activity records.
259
+ */
260
+ getSentActivities(): Array<{
261
+ sender: SenderKeyPair | SenderKeyPair[] | {
262
+ identifier: string;
263
+ } | {
264
+ username: string;
265
+ } | {
266
+ handle: string;
267
+ };
268
+ recipients: Recipient | Recipient[] | "followers";
269
+ activity: Activity;
270
+ }>;
271
+ /**
272
+ * Clears all sent activities from the mock context.
273
+ * This method is specific to the mock implementation and is used for
274
+ * testing purposes.
275
+ */
276
+ reset(): void;
277
+ }
278
+ //#endregion
279
+ //#region src/context.d.ts
280
+ declare function createContext<TContextData>(values: Partial<Context<TContextData>> & {
281
+ url?: URL;
282
+ data: TContextData;
283
+ federation: Federation<TContextData>;
284
+ }): Context<TContextData>;
285
+ declare function createRequestContext<TContextData>(args: Partial<RequestContext<TContextData>> & {
286
+ url: URL;
287
+ data: TContextData;
288
+ federation: Federation<TContextData>;
289
+ }): RequestContext<TContextData>;
290
+ declare function createInboxContext<TContextData>(args: Partial<InboxContext<TContextData>> & {
291
+ url?: URL;
292
+ data: TContextData;
293
+ recipient?: string | null;
294
+ federation: Federation<TContextData>;
295
+ }): InboxContext<TContextData>;
296
+ //#endregion
297
+ export { MockContext, MockFederation, SentActivity, createContext, createInboxContext, createRequestContext };
package/dist/mod.js CHANGED
@@ -295,6 +295,9 @@ var MockFederation = class {
295
295
  },
296
296
  setSharedKeyDispatcher() {
297
297
  return this;
298
+ },
299
+ withIdempotency() {
300
+ return this;
298
301
  }
299
302
  };
300
303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/testing",
3
- "version": "2.0.0-dev.1604+23a1ea67",
3
+ "version": "2.0.0-dev.1690+304896b8",
4
4
  "description": "Testing utilities for Fedify applications",
5
5
  "keywords": [
6
6
  "fedify",
@@ -29,13 +29,18 @@
29
29
  "https://github.com/sponsors/dahlia"
30
30
  ],
31
31
  "type": "module",
32
- "main": "./dist/mod.js",
32
+ "main": "./dist/mod.cjs",
33
33
  "module": "./dist/mod.js",
34
34
  "types": "./dist/mod.d.ts",
35
35
  "exports": {
36
36
  ".": {
37
- "types": "./dist/mod.d.ts",
37
+ "types": {
38
+ "import": "./dist/mod.d.ts",
39
+ "require": "./dist/mod.d.cts",
40
+ "default": "./dist/mod.d.ts"
41
+ },
38
42
  "import": "./dist/mod.js",
43
+ "require": "./dist/mod.cjs",
39
44
  "default": "./dist/mod.js"
40
45
  },
41
46
  "./package.json": "./package.json"
@@ -45,7 +50,7 @@
45
50
  "package.json"
46
51
  ],
47
52
  "peerDependencies": {
48
- "@fedify/fedify": "^2.0.0-dev.1604+23a1ea67"
53
+ "@fedify/fedify": "^2.0.0-dev.1690+304896b8"
49
54
  },
50
55
  "dependencies": {
51
56
  "@opentelemetry/api": "^1.9.0"