@fedify/testing 1.8.1-pr.283.1138

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