@fedify/testing 2.0.0-dev.1561 → 2.0.0-dev.158
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/mod.cjs +733 -0
- package/dist/mod.d.cts +126 -0
- package/dist/mod.d.ts +83 -254
- package/dist/mod.js +111 -41
- package/package.json +10 -8
package/dist/mod.cjs
ADDED
|
@@ -0,0 +1,733 @@
|
|
|
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 __fedify_fedify_federation = __toESM(require("@fedify/fedify/federation"));
|
|
25
|
+
const __fedify_vocab = __toESM(require("@fedify/vocab"));
|
|
26
|
+
|
|
27
|
+
//#region src/docloader.ts
|
|
28
|
+
const mockDocumentLoader = async (url) => ({
|
|
29
|
+
contextUrl: null,
|
|
30
|
+
document: {},
|
|
31
|
+
documentUrl: url
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/context.ts
|
|
36
|
+
const noopTracerProvider$1 = { getTracer: () => ({
|
|
37
|
+
startActiveSpan: () => void 0,
|
|
38
|
+
startSpan: () => void 0
|
|
39
|
+
}) };
|
|
40
|
+
function createContext(values) {
|
|
41
|
+
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() {
|
|
43
|
+
throw new __fedify_fedify_federation.RouterError("Not implemented");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
federation,
|
|
47
|
+
data,
|
|
48
|
+
origin: url.origin,
|
|
49
|
+
canonicalOrigin: canonicalOrigin ?? url.origin,
|
|
50
|
+
host: url.host,
|
|
51
|
+
hostname: url.hostname,
|
|
52
|
+
documentLoader: documentLoader ?? mockDocumentLoader,
|
|
53
|
+
contextLoader: contextLoader ?? mockDocumentLoader,
|
|
54
|
+
tracerProvider: tracerProvider ?? noopTracerProvider$1,
|
|
55
|
+
clone: clone ?? ((data$1) => createContext({
|
|
56
|
+
...values,
|
|
57
|
+
data: data$1
|
|
58
|
+
})),
|
|
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,
|
|
70
|
+
parseUri: parseUri ?? ((_uri) => {
|
|
71
|
+
throw new Error("Not implemented");
|
|
72
|
+
}),
|
|
73
|
+
getDocumentLoader: getDocumentLoader ?? ((_params) => {
|
|
74
|
+
throw new Error("Not implemented");
|
|
75
|
+
}),
|
|
76
|
+
getActorKeyPairs: getActorKeyPairs ?? ((_handle) => Promise.resolve([])),
|
|
77
|
+
lookupObject: lookupObject ?? ((uri, options = {}) => {
|
|
78
|
+
return (0, __fedify_vocab.lookupObject)(uri, {
|
|
79
|
+
documentLoader: options.documentLoader ?? documentLoader ?? mockDocumentLoader,
|
|
80
|
+
contextLoader: options.contextLoader ?? contextLoader ?? mockDocumentLoader
|
|
81
|
+
});
|
|
82
|
+
}),
|
|
83
|
+
traverseCollection: traverseCollection ?? ((collection, options = {}) => {
|
|
84
|
+
return (0, __fedify_vocab.traverseCollection)(collection, {
|
|
85
|
+
documentLoader: options.documentLoader ?? documentLoader ?? mockDocumentLoader,
|
|
86
|
+
contextLoader: options.contextLoader ?? contextLoader ?? mockDocumentLoader
|
|
87
|
+
});
|
|
88
|
+
}),
|
|
89
|
+
lookupNodeInfo: lookupNodeInfo ?? ((_params) => {
|
|
90
|
+
throw new Error("Not implemented");
|
|
91
|
+
}),
|
|
92
|
+
lookupWebFinger: lookupWebFinger ?? ((_resource, _options = {}) => {
|
|
93
|
+
return Promise.resolve(null);
|
|
94
|
+
}),
|
|
95
|
+
sendActivity: sendActivity ?? ((_params) => {
|
|
96
|
+
throw new Error("Not implemented");
|
|
97
|
+
}),
|
|
98
|
+
routeActivity: routeActivity ?? ((_params) => {
|
|
99
|
+
throw new Error("Not implemented");
|
|
100
|
+
})
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Creates a RequestContext for testing purposes.
|
|
105
|
+
* Not exported - used internally only. Public API is in mock.ts
|
|
106
|
+
* @param args Partial RequestContext properties
|
|
107
|
+
* @returns A RequestContext instance
|
|
108
|
+
* @since 1.8.0
|
|
109
|
+
*/
|
|
110
|
+
function createRequestContext(args) {
|
|
111
|
+
return {
|
|
112
|
+
...createContext(args),
|
|
113
|
+
clone: args.clone ?? ((data) => createRequestContext({
|
|
114
|
+
...args,
|
|
115
|
+
data
|
|
116
|
+
})),
|
|
117
|
+
request: args.request ?? new Request(args.url),
|
|
118
|
+
url: args.url,
|
|
119
|
+
getActor: args.getActor ?? (() => Promise.resolve(null)),
|
|
120
|
+
getObject: args.getObject ?? (() => Promise.resolve(null)),
|
|
121
|
+
getSignedKey: args.getSignedKey ?? (() => Promise.resolve(null)),
|
|
122
|
+
getSignedKeyOwner: args.getSignedKeyOwner ?? (() => Promise.resolve(null)),
|
|
123
|
+
sendActivity: args.sendActivity ?? ((_params) => {
|
|
124
|
+
throw new Error("Not implemented");
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates an InboxContext for testing purposes.
|
|
130
|
+
* Not exported - used internally only. Public API is in mock.ts
|
|
131
|
+
* @param args Partial InboxContext properties
|
|
132
|
+
* @returns An InboxContext instance
|
|
133
|
+
* @since 1.8.0
|
|
134
|
+
*/
|
|
135
|
+
function createInboxContext(args) {
|
|
136
|
+
return {
|
|
137
|
+
...createContext(args),
|
|
138
|
+
clone: args.clone ?? ((data) => createInboxContext({
|
|
139
|
+
...args,
|
|
140
|
+
data
|
|
141
|
+
})),
|
|
142
|
+
recipient: args.recipient ?? null,
|
|
143
|
+
forwardActivity: args.forwardActivity ?? ((_params) => {
|
|
144
|
+
throw new Error("Not implemented");
|
|
145
|
+
})
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/mock.ts
|
|
151
|
+
const noopTracerProvider = { getTracer: () => ({
|
|
152
|
+
startActiveSpan: () => void 0,
|
|
153
|
+
startSpan: () => void 0
|
|
154
|
+
}) };
|
|
155
|
+
/**
|
|
156
|
+
* Helper function to expand URI templates with values.
|
|
157
|
+
* Supports simple placeholders like {identifier}, {handle}, etc.
|
|
158
|
+
* @param template The URI template pattern
|
|
159
|
+
* @param values The values to substitute
|
|
160
|
+
* @returns The expanded URI path
|
|
161
|
+
*/
|
|
162
|
+
function expandUriTemplate(template, values) {
|
|
163
|
+
return template.replace(/{([^}]+)}/g, (match, key) => {
|
|
164
|
+
return values[key] || match;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A mock implementation of the {@link Federation} interface for unit testing.
|
|
169
|
+
* This class provides a way to test Fedify applications without needing
|
|
170
|
+
* a real federation setup.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* import { Create } from "@fedify/vocab";
|
|
175
|
+
* import { createFederation } from "@fedify/testing";
|
|
176
|
+
*
|
|
177
|
+
* // Create a mock federation with contextData
|
|
178
|
+
* const federation = createFederation<{ userId: string }>({
|
|
179
|
+
* contextData: { userId: "test-user" }
|
|
180
|
+
* });
|
|
181
|
+
*
|
|
182
|
+
* // Set up inbox listeners
|
|
183
|
+
* federation
|
|
184
|
+
* .setInboxListeners("/users/{identifier}/inbox")
|
|
185
|
+
* .on(Create, async (ctx: any, activity: any) => {
|
|
186
|
+
* console.log("Received:", activity);
|
|
187
|
+
* });
|
|
188
|
+
*
|
|
189
|
+
* // Simulate receiving an activity
|
|
190
|
+
* const createActivity = new Create({
|
|
191
|
+
* id: new URL("https://example.com/create/1"),
|
|
192
|
+
* actor: new URL("https://example.com/users/alice")
|
|
193
|
+
* });
|
|
194
|
+
* await federation.receiveActivity(createActivity);
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* @template TContextData The context data to pass to the {@link Context}.
|
|
198
|
+
* @since 1.8.0
|
|
199
|
+
*/
|
|
200
|
+
var MockFederation = class {
|
|
201
|
+
sentActivities = [];
|
|
202
|
+
queueStarted = false;
|
|
203
|
+
activeQueues = /* @__PURE__ */ new Set();
|
|
204
|
+
sentCounter = 0;
|
|
205
|
+
nodeInfoDispatcher;
|
|
206
|
+
webFingerDispatcher;
|
|
207
|
+
actorDispatchers = /* @__PURE__ */ new Map();
|
|
208
|
+
actorPath;
|
|
209
|
+
inboxPath;
|
|
210
|
+
outboxPath;
|
|
211
|
+
followingPath;
|
|
212
|
+
followersPath;
|
|
213
|
+
likedPath;
|
|
214
|
+
featuredPath;
|
|
215
|
+
featuredTagsPath;
|
|
216
|
+
nodeInfoPath;
|
|
217
|
+
sharedInboxPath;
|
|
218
|
+
objectPaths = /* @__PURE__ */ new Map();
|
|
219
|
+
objectDispatchers = /* @__PURE__ */ new Map();
|
|
220
|
+
inboxDispatcher;
|
|
221
|
+
outboxDispatcher;
|
|
222
|
+
followingDispatcher;
|
|
223
|
+
followersDispatcher;
|
|
224
|
+
likedDispatcher;
|
|
225
|
+
featuredDispatcher;
|
|
226
|
+
featuredTagsDispatcher;
|
|
227
|
+
inboxListeners = /* @__PURE__ */ new Map();
|
|
228
|
+
contextData;
|
|
229
|
+
receivedActivities = [];
|
|
230
|
+
constructor(options = {}) {
|
|
231
|
+
this.options = options;
|
|
232
|
+
this.contextData = options.contextData;
|
|
233
|
+
}
|
|
234
|
+
setNodeInfoDispatcher(path, dispatcher) {
|
|
235
|
+
this.nodeInfoDispatcher = dispatcher;
|
|
236
|
+
this.nodeInfoPath = path;
|
|
237
|
+
}
|
|
238
|
+
setWebFingerLinksDispatcher(dispatcher) {
|
|
239
|
+
this.webFingerDispatcher = dispatcher;
|
|
240
|
+
}
|
|
241
|
+
setActorDispatcher(path, dispatcher) {
|
|
242
|
+
this.actorDispatchers.set(path, dispatcher);
|
|
243
|
+
this.actorPath = path;
|
|
244
|
+
return {
|
|
245
|
+
setKeyPairsDispatcher: () => this,
|
|
246
|
+
mapHandle: () => this,
|
|
247
|
+
mapAlias: () => this,
|
|
248
|
+
authorize: () => this
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
setObjectDispatcher(cls, path, dispatcher) {
|
|
252
|
+
this.objectDispatchers.set(path, dispatcher);
|
|
253
|
+
this.objectPaths.set(cls.typeId.href, path);
|
|
254
|
+
return { authorize: () => this };
|
|
255
|
+
}
|
|
256
|
+
setInboxDispatcher(_path, dispatcher) {
|
|
257
|
+
this.inboxDispatcher = dispatcher;
|
|
258
|
+
return {
|
|
259
|
+
setCounter: () => this,
|
|
260
|
+
setFirstCursor: () => this,
|
|
261
|
+
setLastCursor: () => this,
|
|
262
|
+
authorize: () => this
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
setOutboxDispatcher(path, dispatcher) {
|
|
266
|
+
this.outboxDispatcher = dispatcher;
|
|
267
|
+
this.outboxPath = path;
|
|
268
|
+
return {
|
|
269
|
+
setCounter: () => this,
|
|
270
|
+
setFirstCursor: () => this,
|
|
271
|
+
setLastCursor: () => this,
|
|
272
|
+
authorize: () => this
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
setFollowingDispatcher(path, dispatcher) {
|
|
276
|
+
this.followingDispatcher = dispatcher;
|
|
277
|
+
this.followingPath = path;
|
|
278
|
+
return {
|
|
279
|
+
setCounter: () => this,
|
|
280
|
+
setFirstCursor: () => this,
|
|
281
|
+
setLastCursor: () => this,
|
|
282
|
+
authorize: () => this
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
setFollowersDispatcher(path, dispatcher) {
|
|
286
|
+
this.followersDispatcher = dispatcher;
|
|
287
|
+
this.followersPath = path;
|
|
288
|
+
return {
|
|
289
|
+
setCounter: () => this,
|
|
290
|
+
setFirstCursor: () => this,
|
|
291
|
+
setLastCursor: () => this,
|
|
292
|
+
authorize: () => this
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
setLikedDispatcher(path, dispatcher) {
|
|
296
|
+
this.likedDispatcher = dispatcher;
|
|
297
|
+
this.likedPath = path;
|
|
298
|
+
return {
|
|
299
|
+
setCounter: () => this,
|
|
300
|
+
setFirstCursor: () => this,
|
|
301
|
+
setLastCursor: () => this,
|
|
302
|
+
authorize: () => this
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
setFeaturedDispatcher(path, dispatcher) {
|
|
306
|
+
this.featuredDispatcher = dispatcher;
|
|
307
|
+
this.featuredPath = path;
|
|
308
|
+
return {
|
|
309
|
+
setCounter: () => this,
|
|
310
|
+
setFirstCursor: () => this,
|
|
311
|
+
setLastCursor: () => this,
|
|
312
|
+
authorize: () => this
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
setFeaturedTagsDispatcher(path, dispatcher) {
|
|
316
|
+
this.featuredTagsDispatcher = dispatcher;
|
|
317
|
+
this.featuredTagsPath = path;
|
|
318
|
+
return {
|
|
319
|
+
setCounter: () => this,
|
|
320
|
+
setFirstCursor: () => this,
|
|
321
|
+
setLastCursor: () => this,
|
|
322
|
+
authorize: () => this
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
setInboxListeners(inboxPath, sharedInboxPath) {
|
|
326
|
+
this.inboxPath = inboxPath;
|
|
327
|
+
this.sharedInboxPath = sharedInboxPath;
|
|
328
|
+
const self = this;
|
|
329
|
+
return {
|
|
330
|
+
on(type, listener) {
|
|
331
|
+
const typeName = type.name;
|
|
332
|
+
if (!self.inboxListeners.has(typeName)) self.inboxListeners.set(typeName, []);
|
|
333
|
+
self.inboxListeners.get(typeName).push(listener);
|
|
334
|
+
return this;
|
|
335
|
+
},
|
|
336
|
+
onError() {
|
|
337
|
+
return this;
|
|
338
|
+
},
|
|
339
|
+
setSharedKeyDispatcher() {
|
|
340
|
+
return this;
|
|
341
|
+
},
|
|
342
|
+
withIdempotency() {
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
async startQueue(contextData, options) {
|
|
348
|
+
this.contextData = contextData;
|
|
349
|
+
this.queueStarted = true;
|
|
350
|
+
if (options?.queue) this.activeQueues.add(options.queue);
|
|
351
|
+
else {
|
|
352
|
+
this.activeQueues.add("inbox");
|
|
353
|
+
this.activeQueues.add("outbox");
|
|
354
|
+
this.activeQueues.add("fanout");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async processQueuedTask(contextData, _message) {
|
|
358
|
+
this.contextData = contextData;
|
|
359
|
+
}
|
|
360
|
+
createContext(baseUrlOrRequest, contextData) {
|
|
361
|
+
const mockFederation = this;
|
|
362
|
+
const url = baseUrlOrRequest instanceof Request ? new URL(baseUrlOrRequest.url) : baseUrlOrRequest;
|
|
363
|
+
return new MockContext({
|
|
364
|
+
url,
|
|
365
|
+
data: contextData,
|
|
366
|
+
federation: mockFederation
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
async fetch(request, options) {
|
|
370
|
+
if (options.onNotFound) return options.onNotFound(request);
|
|
371
|
+
return new Response("Not Found", { status: 404 });
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Simulates receiving an activity. This method is specific to the mock
|
|
375
|
+
* implementation and is used for testing purposes.
|
|
376
|
+
*
|
|
377
|
+
* @param activity The activity to receive.
|
|
378
|
+
* @returns A promise that resolves when the activity has been processed.
|
|
379
|
+
* @since 1.8.0
|
|
380
|
+
*/
|
|
381
|
+
async receiveActivity(activity) {
|
|
382
|
+
this.receivedActivities.push(activity);
|
|
383
|
+
const typeName = activity.constructor.name;
|
|
384
|
+
const listeners = this.inboxListeners.get(typeName) || [];
|
|
385
|
+
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.");
|
|
386
|
+
for (const listener of listeners) {
|
|
387
|
+
const context = createInboxContext({
|
|
388
|
+
data: this.contextData,
|
|
389
|
+
federation: this
|
|
390
|
+
});
|
|
391
|
+
await listener(context, activity);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Clears all sent activities from the mock federation.
|
|
396
|
+
* This method is specific to the mock implementation and is used for
|
|
397
|
+
* testing purposes.
|
|
398
|
+
*
|
|
399
|
+
* @since 1.8.0
|
|
400
|
+
*/
|
|
401
|
+
reset() {
|
|
402
|
+
this.sentActivities = [];
|
|
403
|
+
}
|
|
404
|
+
setCollectionDispatcher(_name, _itemType, _path, _dispatcher) {
|
|
405
|
+
return {
|
|
406
|
+
setCounter: () => this,
|
|
407
|
+
setFirstCursor: () => this,
|
|
408
|
+
setLastCursor: () => this,
|
|
409
|
+
authorize: () => this
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
setOrderedCollectionDispatcher(_name, _itemType, _path, _dispatcher) {
|
|
413
|
+
return {
|
|
414
|
+
setCounter: () => this,
|
|
415
|
+
setFirstCursor: () => this,
|
|
416
|
+
setLastCursor: () => this,
|
|
417
|
+
authorize: () => this
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* Creates a mock Federation instance for testing purposes.
|
|
423
|
+
*
|
|
424
|
+
* @template TContextData The type of context data to use
|
|
425
|
+
* @param options Optional configuration for the mock federation
|
|
426
|
+
* @returns A Federation instance that can be used for testing
|
|
427
|
+
* @since 1.9.1
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```typescript
|
|
431
|
+
* import { Create } from "@fedify/vocab";
|
|
432
|
+
* import { createFederation } from "@fedify/testing";
|
|
433
|
+
*
|
|
434
|
+
* // Create a mock federation with contextData
|
|
435
|
+
* const federation = createFederation<{ userId: string }>({
|
|
436
|
+
* contextData: { userId: "test-user" }
|
|
437
|
+
* });
|
|
438
|
+
*
|
|
439
|
+
* // Set up inbox listeners
|
|
440
|
+
* federation
|
|
441
|
+
* .setInboxListeners("/users/{identifier}/inbox")
|
|
442
|
+
* .on(Create, async (ctx, activity) => {
|
|
443
|
+
* console.log("Received:", activity);
|
|
444
|
+
* });
|
|
445
|
+
*
|
|
446
|
+
* // Simulate receiving an activity
|
|
447
|
+
* const createActivity = new Create({
|
|
448
|
+
* id: new URL("https://example.com/create/1"),
|
|
449
|
+
* actor: new URL("https://example.com/users/alice")
|
|
450
|
+
* });
|
|
451
|
+
* await federation.receiveActivity(createActivity);
|
|
452
|
+
*
|
|
453
|
+
* // Check sent activities
|
|
454
|
+
* console.log(federation.sentActivities);
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
function createFederation(options = {}) {
|
|
458
|
+
return new MockFederation(options);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* A mock implementation of the {@link Context} interface for unit testing.
|
|
462
|
+
* This class provides a way to test Fedify applications without needing
|
|
463
|
+
* a real federation context.
|
|
464
|
+
*
|
|
465
|
+
* Note: This class is not exported from the public API to avoid JSR type
|
|
466
|
+
* analyzer issues. The MockContext class has complex type dependencies that
|
|
467
|
+
* can cause JSR's type analyzer to hang during processing (issue #468).
|
|
468
|
+
* Use {@link MockFederation.createContext}, {@link createContext},
|
|
469
|
+
* {@link createRequestContext}, or {@link createInboxContext} instead.
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```typescript
|
|
473
|
+
* import { Person, Create } from "@fedify/vocab";
|
|
474
|
+
* import { createFederation } from "@fedify/testing";
|
|
475
|
+
*
|
|
476
|
+
* // Create a mock federation and context
|
|
477
|
+
* const federation = createFederation<{ userId: string }>();
|
|
478
|
+
* const context = federation.createContext(
|
|
479
|
+
* new URL("https://example.com"),
|
|
480
|
+
* { userId: "test-user" }
|
|
481
|
+
* );
|
|
482
|
+
*
|
|
483
|
+
* // Send an activity
|
|
484
|
+
* const recipient = new Person({ id: new URL("https://example.com/users/bob") });
|
|
485
|
+
* const activity = new Create({
|
|
486
|
+
* id: new URL("https://example.com/create/1"),
|
|
487
|
+
* actor: new URL("https://example.com/users/alice")
|
|
488
|
+
* });
|
|
489
|
+
* await context.sendActivity(
|
|
490
|
+
* { identifier: "alice" },
|
|
491
|
+
* recipient,
|
|
492
|
+
* activity
|
|
493
|
+
* );
|
|
494
|
+
*
|
|
495
|
+
* // Check sent activities from the federation
|
|
496
|
+
* const sent = federation.sentActivities;
|
|
497
|
+
* console.log(sent[0].activity);
|
|
498
|
+
* ```
|
|
499
|
+
*
|
|
500
|
+
* @template TContextData The context data to pass to the {@link Context}.
|
|
501
|
+
* @since 1.8.0
|
|
502
|
+
*/
|
|
503
|
+
var MockContext = class MockContext {
|
|
504
|
+
origin;
|
|
505
|
+
canonicalOrigin;
|
|
506
|
+
host;
|
|
507
|
+
hostname;
|
|
508
|
+
data;
|
|
509
|
+
federation;
|
|
510
|
+
documentLoader;
|
|
511
|
+
contextLoader;
|
|
512
|
+
tracerProvider;
|
|
513
|
+
request;
|
|
514
|
+
url;
|
|
515
|
+
sentActivities = [];
|
|
516
|
+
constructor(options) {
|
|
517
|
+
const url = options.url ?? new URL("https://example.com");
|
|
518
|
+
this.origin = url.origin;
|
|
519
|
+
this.canonicalOrigin = url.origin;
|
|
520
|
+
this.host = url.host;
|
|
521
|
+
this.hostname = url.hostname;
|
|
522
|
+
this.url = url;
|
|
523
|
+
this.request = new Request(url);
|
|
524
|
+
this.data = options.data;
|
|
525
|
+
this.federation = options.federation;
|
|
526
|
+
this.documentLoader = options.documentLoader ?? (async (url$1) => ({
|
|
527
|
+
contextUrl: null,
|
|
528
|
+
document: {},
|
|
529
|
+
documentUrl: url$1
|
|
530
|
+
}));
|
|
531
|
+
this.contextLoader = options.contextLoader ?? this.documentLoader;
|
|
532
|
+
this.tracerProvider = options.tracerProvider ?? noopTracerProvider;
|
|
533
|
+
}
|
|
534
|
+
getActor(_handle) {
|
|
535
|
+
return Promise.resolve(null);
|
|
536
|
+
}
|
|
537
|
+
getObject(_cls, _values) {
|
|
538
|
+
return Promise.resolve(null);
|
|
539
|
+
}
|
|
540
|
+
getSignedKey() {
|
|
541
|
+
return Promise.resolve(null);
|
|
542
|
+
}
|
|
543
|
+
getSignedKeyOwner() {
|
|
544
|
+
return Promise.resolve(null);
|
|
545
|
+
}
|
|
546
|
+
clone(data) {
|
|
547
|
+
return new MockContext({
|
|
548
|
+
url: this.url,
|
|
549
|
+
data,
|
|
550
|
+
federation: this.federation,
|
|
551
|
+
documentLoader: this.documentLoader,
|
|
552
|
+
contextLoader: this.contextLoader,
|
|
553
|
+
tracerProvider: this.tracerProvider
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
getNodeInfoUri() {
|
|
557
|
+
if (this.federation instanceof MockFederation && this.federation.nodeInfoPath) return new URL(this.federation.nodeInfoPath, this.origin);
|
|
558
|
+
return new URL("/nodeinfo/2.0", this.origin);
|
|
559
|
+
}
|
|
560
|
+
getActorUri(identifier) {
|
|
561
|
+
if (this.federation instanceof MockFederation && this.federation.actorPath) {
|
|
562
|
+
const path = expandUriTemplate(this.federation.actorPath, {
|
|
563
|
+
identifier,
|
|
564
|
+
handle: identifier
|
|
565
|
+
});
|
|
566
|
+
return new URL(path, this.origin);
|
|
567
|
+
}
|
|
568
|
+
return new URL(`/users/${identifier}`, this.origin);
|
|
569
|
+
}
|
|
570
|
+
getObjectUri(cls, values) {
|
|
571
|
+
if (this.federation instanceof MockFederation) {
|
|
572
|
+
const pathTemplate = this.federation.objectPaths.get(cls.typeId.href);
|
|
573
|
+
if (pathTemplate) {
|
|
574
|
+
const path$1 = expandUriTemplate(pathTemplate, values);
|
|
575
|
+
return new URL(path$1, this.origin);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const path = globalThis.Object.entries(values).map(([key, value]) => `${key}/${value}`).join("/");
|
|
579
|
+
return new URL(`/objects/${cls.name.toLowerCase()}/${path}`, this.origin);
|
|
580
|
+
}
|
|
581
|
+
getOutboxUri(identifier) {
|
|
582
|
+
if (this.federation instanceof MockFederation && this.federation.outboxPath) {
|
|
583
|
+
const path = expandUriTemplate(this.federation.outboxPath, {
|
|
584
|
+
identifier,
|
|
585
|
+
handle: identifier
|
|
586
|
+
});
|
|
587
|
+
return new URL(path, this.origin);
|
|
588
|
+
}
|
|
589
|
+
return new URL(`/users/${identifier}/outbox`, this.origin);
|
|
590
|
+
}
|
|
591
|
+
getInboxUri(identifier) {
|
|
592
|
+
if (identifier) {
|
|
593
|
+
if (this.federation instanceof MockFederation && this.federation.inboxPath) {
|
|
594
|
+
const path = expandUriTemplate(this.federation.inboxPath, {
|
|
595
|
+
identifier,
|
|
596
|
+
handle: identifier
|
|
597
|
+
});
|
|
598
|
+
return new URL(path, this.origin);
|
|
599
|
+
}
|
|
600
|
+
return new URL(`/users/${identifier}/inbox`, this.origin);
|
|
601
|
+
}
|
|
602
|
+
if (this.federation instanceof MockFederation && this.federation.sharedInboxPath) return new URL(this.federation.sharedInboxPath, this.origin);
|
|
603
|
+
return new URL("/inbox", this.origin);
|
|
604
|
+
}
|
|
605
|
+
getFollowingUri(identifier) {
|
|
606
|
+
if (this.federation instanceof MockFederation && this.federation.followingPath) {
|
|
607
|
+
const path = expandUriTemplate(this.federation.followingPath, {
|
|
608
|
+
identifier,
|
|
609
|
+
handle: identifier
|
|
610
|
+
});
|
|
611
|
+
return new URL(path, this.origin);
|
|
612
|
+
}
|
|
613
|
+
return new URL(`/users/${identifier}/following`, this.origin);
|
|
614
|
+
}
|
|
615
|
+
getFollowersUri(identifier) {
|
|
616
|
+
if (this.federation instanceof MockFederation && this.federation.followersPath) {
|
|
617
|
+
const path = expandUriTemplate(this.federation.followersPath, {
|
|
618
|
+
identifier,
|
|
619
|
+
handle: identifier
|
|
620
|
+
});
|
|
621
|
+
return new URL(path, this.origin);
|
|
622
|
+
}
|
|
623
|
+
return new URL(`/users/${identifier}/followers`, this.origin);
|
|
624
|
+
}
|
|
625
|
+
getLikedUri(identifier) {
|
|
626
|
+
if (this.federation instanceof MockFederation && this.federation.likedPath) {
|
|
627
|
+
const path = expandUriTemplate(this.federation.likedPath, {
|
|
628
|
+
identifier,
|
|
629
|
+
handle: identifier
|
|
630
|
+
});
|
|
631
|
+
return new URL(path, this.origin);
|
|
632
|
+
}
|
|
633
|
+
return new URL(`/users/${identifier}/liked`, this.origin);
|
|
634
|
+
}
|
|
635
|
+
getFeaturedUri(identifier) {
|
|
636
|
+
if (this.federation instanceof MockFederation && this.federation.featuredPath) {
|
|
637
|
+
const path = expandUriTemplate(this.federation.featuredPath, {
|
|
638
|
+
identifier,
|
|
639
|
+
handle: identifier
|
|
640
|
+
});
|
|
641
|
+
return new URL(path, this.origin);
|
|
642
|
+
}
|
|
643
|
+
return new URL(`/users/${identifier}/featured`, this.origin);
|
|
644
|
+
}
|
|
645
|
+
getFeaturedTagsUri(identifier) {
|
|
646
|
+
if (this.federation instanceof MockFederation && this.federation.featuredTagsPath) {
|
|
647
|
+
const path = expandUriTemplate(this.federation.featuredTagsPath, {
|
|
648
|
+
identifier,
|
|
649
|
+
handle: identifier
|
|
650
|
+
});
|
|
651
|
+
return new URL(path, this.origin);
|
|
652
|
+
}
|
|
653
|
+
return new URL(`/users/${identifier}/tags`, this.origin);
|
|
654
|
+
}
|
|
655
|
+
getCollectionUri(_name, values) {
|
|
656
|
+
const path = globalThis.Object.entries(values).map(([key, value]) => `${key}/${value}`).join("/");
|
|
657
|
+
return new URL(`/collections/${String(_name)}/${path}`, this.origin);
|
|
658
|
+
}
|
|
659
|
+
parseUri(uri) {
|
|
660
|
+
if (uri.pathname.startsWith("/users/")) {
|
|
661
|
+
const parts = uri.pathname.split("/");
|
|
662
|
+
if (parts.length >= 3) return {
|
|
663
|
+
type: "actor",
|
|
664
|
+
identifier: parts[2],
|
|
665
|
+
handle: parts[2]
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
getActorKeyPairs(_identifier) {
|
|
671
|
+
return Promise.resolve([]);
|
|
672
|
+
}
|
|
673
|
+
getDocumentLoader(params) {
|
|
674
|
+
if ("keyId" in params) return this.documentLoader;
|
|
675
|
+
return Promise.resolve(this.documentLoader);
|
|
676
|
+
}
|
|
677
|
+
lookupObject(_uri, _options) {
|
|
678
|
+
return Promise.resolve(null);
|
|
679
|
+
}
|
|
680
|
+
traverseCollection(_collection, _options) {
|
|
681
|
+
return { async *[Symbol.asyncIterator]() {} };
|
|
682
|
+
}
|
|
683
|
+
lookupNodeInfo(_url, _options) {
|
|
684
|
+
return Promise.resolve(void 0);
|
|
685
|
+
}
|
|
686
|
+
lookupWebFinger(_resource, _options) {
|
|
687
|
+
return Promise.resolve(null);
|
|
688
|
+
}
|
|
689
|
+
sendActivity(sender, recipients, activity, _options) {
|
|
690
|
+
this.sentActivities.push({
|
|
691
|
+
sender,
|
|
692
|
+
recipients,
|
|
693
|
+
activity
|
|
694
|
+
});
|
|
695
|
+
if (this.federation instanceof MockFederation) {
|
|
696
|
+
const queued = this.federation.queueStarted;
|
|
697
|
+
this.federation.sentActivities.push({
|
|
698
|
+
queued,
|
|
699
|
+
queue: queued ? "outbox" : void 0,
|
|
700
|
+
activity,
|
|
701
|
+
sentOrder: ++this.federation.sentCounter
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return Promise.resolve();
|
|
705
|
+
}
|
|
706
|
+
routeActivity(_recipient, _activity, _options) {
|
|
707
|
+
return Promise.resolve(true);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Gets all activities that have been sent through this mock context.
|
|
711
|
+
* This method is specific to the mock implementation and is used for
|
|
712
|
+
* testing purposes.
|
|
713
|
+
*
|
|
714
|
+
* @returns An array of sent activity records.
|
|
715
|
+
*/
|
|
716
|
+
getSentActivities() {
|
|
717
|
+
return [...this.sentActivities];
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Clears all sent activities from the mock context.
|
|
721
|
+
* This method is specific to the mock implementation and is used for
|
|
722
|
+
* testing purposes.
|
|
723
|
+
*/
|
|
724
|
+
reset() {
|
|
725
|
+
this.sentActivities = [];
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
//#endregion
|
|
730
|
+
exports.createContext = createContext;
|
|
731
|
+
exports.createFederation = createFederation;
|
|
732
|
+
exports.createInboxContext = createInboxContext;
|
|
733
|
+
exports.createRequestContext = createRequestContext;
|