@fedify/testing 2.2.0-dev.869 → 2.2.0-pr.695.23
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 +12 -171
- package/dist/mod.d.cts +2 -25
- package/dist/mod.d.ts +2 -25
- package/dist/mod.js +14 -172
- package/package.json +2 -2
package/dist/mod.cjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const { Temporal } = require("@js-temporal/polyfill");
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
let _fedify_fedify_sig = require("@fedify/fedify/sig");
|
|
4
3
|
let _fedify_vocab = require("@fedify/vocab");
|
|
5
4
|
let _fedify_fedify_federation = require("@fedify/fedify/federation");
|
|
6
5
|
let es_toolkit = require("es-toolkit");
|
|
@@ -113,9 +112,6 @@ function createRequestContext(args) {
|
|
|
113
112
|
* @since 1.8.0
|
|
114
113
|
*/
|
|
115
114
|
function createInboxContext(args) {
|
|
116
|
-
const forwardActivity = args.forwardActivity ?? ((_forwarder, _recipients, _options) => {
|
|
117
|
-
throw new Error("Not implemented");
|
|
118
|
-
});
|
|
119
115
|
return {
|
|
120
116
|
...createContext(args),
|
|
121
117
|
clone: args.clone ?? ((data) => createInboxContext({
|
|
@@ -123,29 +119,9 @@ function createInboxContext(args) {
|
|
|
123
119
|
data
|
|
124
120
|
})),
|
|
125
121
|
recipient: args.recipient ?? null,
|
|
126
|
-
forwardActivity
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Creates an OutboxContext for testing purposes.
|
|
131
|
-
* Not exported - used internally only. Public API is in mock.ts
|
|
132
|
-
* @param args Partial OutboxContext properties
|
|
133
|
-
* @returns An OutboxContext instance
|
|
134
|
-
* @since 2.2.0
|
|
135
|
-
*/
|
|
136
|
-
function createOutboxContext(args) {
|
|
137
|
-
const forwardActivity = args.forwardActivity ?? ((_forwarder, _recipients, _options) => {
|
|
138
|
-
throw new Error("Not implemented");
|
|
139
|
-
});
|
|
140
|
-
return {
|
|
141
|
-
...createContext(args),
|
|
142
|
-
clone: args.clone ?? ((data) => createOutboxContext({
|
|
143
|
-
...args,
|
|
144
|
-
data
|
|
145
|
-
})),
|
|
146
|
-
identifier: args.identifier,
|
|
147
|
-
hasDeliveredActivity: args.hasDeliveredActivity ?? (() => false),
|
|
148
|
-
forwardActivity
|
|
122
|
+
forwardActivity: args.forwardActivity ?? ((_params) => {
|
|
123
|
+
throw new Error("Not implemented");
|
|
124
|
+
})
|
|
149
125
|
};
|
|
150
126
|
}
|
|
151
127
|
//#endregion
|
|
@@ -155,41 +131,17 @@ const noopTracerProvider = { getTracer: () => ({
|
|
|
155
131
|
startSpan: () => void 0
|
|
156
132
|
}) };
|
|
157
133
|
/**
|
|
158
|
-
* Helper function to expand URI templates
|
|
159
|
-
* Supports
|
|
134
|
+
* Helper function to expand URI templates with values.
|
|
135
|
+
* Supports simple placeholders like {identifier}, etc.
|
|
160
136
|
* @param template The URI template pattern
|
|
161
137
|
* @param values The values to substitute
|
|
162
138
|
* @returns The expanded URI path
|
|
163
139
|
*/
|
|
164
140
|
function expandUriTemplate(template, values) {
|
|
165
|
-
return template.replace(/{([
|
|
166
|
-
|
|
167
|
-
if (value == null) return match;
|
|
168
|
-
switch (operator) {
|
|
169
|
-
case "": return encodeURIComponent(value);
|
|
170
|
-
case "+": return encodeURI(value);
|
|
171
|
-
case "#": return `#${encodeURI(value)}`;
|
|
172
|
-
case ".": return `.${encodeURIComponent(value)}`;
|
|
173
|
-
case "/": return `/${encodeURIComponent(value)}`;
|
|
174
|
-
case ";": return `;${key}=${encodeURIComponent(value)}`;
|
|
175
|
-
case "?": return `?${key}=${encodeURIComponent(value)}`;
|
|
176
|
-
case "&": return `&${key}=${encodeURIComponent(value)}`;
|
|
177
|
-
default: return match;
|
|
178
|
-
}
|
|
141
|
+
return template.replace(/{([^}]+)}/g, (match, key) => {
|
|
142
|
+
return values[key] || match;
|
|
179
143
|
});
|
|
180
144
|
}
|
|
181
|
-
function validateOutboxListenerPath(path, dispatcherPath) {
|
|
182
|
-
if (!path.startsWith("/")) throw new TypeError("Path must start with a slash.");
|
|
183
|
-
if (dispatcherPath != null && dispatcherPath !== path) throw new TypeError("Outbox listener path and outbox dispatcher path must match.");
|
|
184
|
-
const operatorMatches = globalThis.Array.from(path.matchAll(/{([+#./;?&]?)([A-Za-z_][A-Za-z0-9_]*)}/g));
|
|
185
|
-
if (operatorMatches.some((match) => [
|
|
186
|
-
"?",
|
|
187
|
-
"&",
|
|
188
|
-
"#"
|
|
189
|
-
].includes(match[1]) && match[2] === "identifier")) throw new TypeError("Path for outbox cannot use query or fragment expansion for identifier.");
|
|
190
|
-
const variables = operatorMatches.map((match) => match[2]);
|
|
191
|
-
if (variables.length !== 1 || variables[0] !== "identifier") throw new TypeError("Path for outbox must have exactly one variable named identifier.");
|
|
192
|
-
}
|
|
193
145
|
/**
|
|
194
146
|
* A mock implementation of the {@link Federation} interface for unit testing.
|
|
195
147
|
* This class provides a way to test Fedify applications without needing
|
|
@@ -246,17 +198,12 @@ var MockFederation = class {
|
|
|
246
198
|
objectDispatchers = /* @__PURE__ */ new Map();
|
|
247
199
|
inboxDispatcher;
|
|
248
200
|
outboxDispatcher;
|
|
249
|
-
outboxAuthorizePredicate;
|
|
250
|
-
outboxDispatcherAuthorizePredicate;
|
|
251
|
-
outboxListenerErrorHandler;
|
|
252
201
|
followingDispatcher;
|
|
253
202
|
followersDispatcher;
|
|
254
203
|
likedDispatcher;
|
|
255
204
|
featuredDispatcher;
|
|
256
205
|
featuredTagsDispatcher;
|
|
257
206
|
inboxListeners = /* @__PURE__ */ new Map();
|
|
258
|
-
outboxListeners = /* @__PURE__ */ new Map();
|
|
259
|
-
outboxListenersInitialized = false;
|
|
260
207
|
contextData;
|
|
261
208
|
receivedActivities = [];
|
|
262
209
|
constructor(options = {}) {
|
|
@@ -298,17 +245,13 @@ var MockFederation = class {
|
|
|
298
245
|
};
|
|
299
246
|
}
|
|
300
247
|
setOutboxDispatcher(path, dispatcher) {
|
|
301
|
-
validateOutboxListenerPath(path, this.outboxListenersInitialized ? this.outboxPath : void 0);
|
|
302
248
|
this.outboxDispatcher = dispatcher;
|
|
303
249
|
this.outboxPath = path;
|
|
304
250
|
return {
|
|
305
251
|
setCounter: () => this,
|
|
306
252
|
setFirstCursor: () => this,
|
|
307
253
|
setLastCursor: () => this,
|
|
308
|
-
authorize: (
|
|
309
|
-
this.outboxDispatcherAuthorizePredicate = predicate;
|
|
310
|
-
return this;
|
|
311
|
-
}
|
|
254
|
+
authorize: () => this
|
|
312
255
|
};
|
|
313
256
|
}
|
|
314
257
|
setFollowingDispatcher(path, dispatcher) {
|
|
@@ -386,28 +329,6 @@ var MockFederation = class {
|
|
|
386
329
|
}
|
|
387
330
|
};
|
|
388
331
|
}
|
|
389
|
-
setOutboxListeners(outboxPath) {
|
|
390
|
-
if (this.outboxListenersInitialized) throw new TypeError("Outbox listeners already set.");
|
|
391
|
-
validateOutboxListenerPath(outboxPath, this.outboxPath);
|
|
392
|
-
this.outboxListenersInitialized = true;
|
|
393
|
-
this.outboxPath = outboxPath;
|
|
394
|
-
const self = this;
|
|
395
|
-
return {
|
|
396
|
-
on(type, listener) {
|
|
397
|
-
if (self.outboxListeners.has(type)) throw new TypeError("Listener already set for this type.");
|
|
398
|
-
self.outboxListeners.set(type, listener);
|
|
399
|
-
return this;
|
|
400
|
-
},
|
|
401
|
-
onError(handler) {
|
|
402
|
-
self.outboxListenerErrorHandler = handler;
|
|
403
|
-
return this;
|
|
404
|
-
},
|
|
405
|
-
authorize(predicate) {
|
|
406
|
-
self.outboxAuthorizePredicate = predicate;
|
|
407
|
-
return this;
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
332
|
setOutboxPermanentFailureHandler(_handler) {}
|
|
412
333
|
async startQueue(contextData, options) {
|
|
413
334
|
this.contextData = contextData;
|
|
@@ -424,10 +345,8 @@ var MockFederation = class {
|
|
|
424
345
|
}
|
|
425
346
|
createContext(baseUrlOrRequest, contextData) {
|
|
426
347
|
const mockFederation = this;
|
|
427
|
-
const request = baseUrlOrRequest instanceof Request ? baseUrlOrRequest : null;
|
|
428
348
|
return new MockContext({
|
|
429
|
-
url:
|
|
430
|
-
request,
|
|
349
|
+
url: baseUrlOrRequest instanceof Request ? new URL(baseUrlOrRequest.url) : baseUrlOrRequest,
|
|
431
350
|
data: contextData,
|
|
432
351
|
federation: mockFederation
|
|
433
352
|
});
|
|
@@ -455,81 +374,6 @@ var MockFederation = class {
|
|
|
455
374
|
}), activity);
|
|
456
375
|
}
|
|
457
376
|
/**
|
|
458
|
-
* Simulates posting an activity to a local actor outbox.
|
|
459
|
-
* This method is specific to the mock implementation and is used for
|
|
460
|
-
* testing purposes.
|
|
461
|
-
*
|
|
462
|
-
* @param identifier The identifier of the outbox owner.
|
|
463
|
-
* @param activity The activity to post.
|
|
464
|
-
* @returns A promise that resolves when the activity has been processed.
|
|
465
|
-
* @since 2.2.0
|
|
466
|
-
*/
|
|
467
|
-
async postOutboxActivity(identifier, activity) {
|
|
468
|
-
if (!this.outboxListenersInitialized) throw new Error("MockFederation.postOutboxActivity(): setOutboxListeners() is not initialized.");
|
|
469
|
-
let ctor = activity.constructor;
|
|
470
|
-
let listener = this.outboxListeners.get(ctor);
|
|
471
|
-
while (listener == null && ctor !== _fedify_vocab.Activity) {
|
|
472
|
-
ctor = globalThis.Object.getPrototypeOf(ctor);
|
|
473
|
-
listener = this.outboxListeners.get(ctor);
|
|
474
|
-
}
|
|
475
|
-
if (listener != null && this.contextData === void 0) throw new Error("MockFederation.postOutboxActivity(): contextData is not initialized. Please provide contextData through the constructor or call startQueue() before posting activities.");
|
|
476
|
-
const origin = new URL(this.options.origin ?? "https://example.com");
|
|
477
|
-
const routingContext = this.createContext(origin, this.contextData);
|
|
478
|
-
const postedJson = await activity.toJsonLd({ contextLoader: routingContext.contextLoader });
|
|
479
|
-
const request = new Request(routingContext.getOutboxUri(identifier), {
|
|
480
|
-
method: "POST",
|
|
481
|
-
body: JSON.stringify(postedJson),
|
|
482
|
-
headers: { "content-type": "application/activity+json" }
|
|
483
|
-
});
|
|
484
|
-
const baseContext = this.createContext(request, this.contextData);
|
|
485
|
-
const rawActivity = postedJson;
|
|
486
|
-
const deliveryState = { delivered: false };
|
|
487
|
-
const createMockOutboxContext = () => createOutboxContext({
|
|
488
|
-
...baseContext,
|
|
489
|
-
clone: void 0,
|
|
490
|
-
federation: this,
|
|
491
|
-
identifier,
|
|
492
|
-
hasDeliveredActivity: () => deliveryState.delivered,
|
|
493
|
-
sendActivity: async (sender, recipients, outboundActivity, options) => {
|
|
494
|
-
await baseContext.sendActivity(sender, recipients, outboundActivity, options);
|
|
495
|
-
deliveryState.delivered = true;
|
|
496
|
-
},
|
|
497
|
-
forwardActivity: async (forwarder, recipients, options) => {
|
|
498
|
-
const hasProof = (0, _fedify_fedify_sig.hasProofLike)(rawActivity);
|
|
499
|
-
const hasLds = (0, _fedify_fedify_sig.hasSignatureLike)(rawActivity);
|
|
500
|
-
if (options?.skipIfUnsigned && !hasProof && !hasLds) return;
|
|
501
|
-
await baseContext.sendActivity(forwarder, recipients, activity, {
|
|
502
|
-
...options,
|
|
503
|
-
rawActivity
|
|
504
|
-
});
|
|
505
|
-
deliveryState.delivered = true;
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
const actor = await baseContext.getActor(identifier);
|
|
509
|
-
if (actor == null) throw new Error(`Actor ${JSON.stringify(identifier)} not found.`);
|
|
510
|
-
const authorizePredicate = this.outboxAuthorizePredicate ?? this.outboxDispatcherAuthorizePredicate;
|
|
511
|
-
if (authorizePredicate != null && !await authorizePredicate(baseContext, identifier)) throw new Error("Unauthorized.");
|
|
512
|
-
const expectedActorId = actor.id ?? baseContext.getActorUri(identifier);
|
|
513
|
-
if (activity.actorIds.length < 1) {
|
|
514
|
-
const error = /* @__PURE__ */ new Error("The posted activity has no actor.");
|
|
515
|
-
await this.outboxListenerErrorHandler?.(createMockOutboxContext(), error);
|
|
516
|
-
throw error;
|
|
517
|
-
}
|
|
518
|
-
if (!activity.actorIds.every((actorId) => actorId.href === expectedActorId.href)) {
|
|
519
|
-
const error = /* @__PURE__ */ new Error("The activity actor does not match the outbox owner.");
|
|
520
|
-
await this.outboxListenerErrorHandler?.(createMockOutboxContext(), error);
|
|
521
|
-
throw error;
|
|
522
|
-
}
|
|
523
|
-
if (listener == null) return;
|
|
524
|
-
const context = createMockOutboxContext();
|
|
525
|
-
try {
|
|
526
|
-
await listener(context, activity);
|
|
527
|
-
} catch (error) {
|
|
528
|
-
await this.outboxListenerErrorHandler?.(context, error);
|
|
529
|
-
throw error;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
377
|
* Clears all sent activities from the mock federation.
|
|
534
378
|
* This method is specific to the mock implementation and is used for
|
|
535
379
|
* testing purposes.
|
|
@@ -658,7 +502,7 @@ var MockContext = class MockContext {
|
|
|
658
502
|
this.host = url.host;
|
|
659
503
|
this.hostname = url.hostname;
|
|
660
504
|
this.url = url;
|
|
661
|
-
this.request =
|
|
505
|
+
this.request = new Request(url);
|
|
662
506
|
this.data = options.data;
|
|
663
507
|
this.federation = options.federation;
|
|
664
508
|
this.documentLoader = options.documentLoader ?? (async (url) => ({
|
|
@@ -827,12 +671,11 @@ var MockContext = class MockContext {
|
|
|
827
671
|
lookupWebFinger(_resource, _options) {
|
|
828
672
|
return Promise.resolve(null);
|
|
829
673
|
}
|
|
830
|
-
sendActivity(sender, recipients, activity,
|
|
674
|
+
sendActivity(sender, recipients, activity, _options) {
|
|
831
675
|
this.sentActivities.push({
|
|
832
676
|
sender,
|
|
833
677
|
recipients,
|
|
834
|
-
activity
|
|
835
|
-
rawActivity: options?.rawActivity
|
|
678
|
+
activity
|
|
836
679
|
});
|
|
837
680
|
if (this.federation instanceof MockFederation) {
|
|
838
681
|
const queued = this.federation.queueStarted;
|
|
@@ -840,7 +683,6 @@ var MockContext = class MockContext {
|
|
|
840
683
|
queued,
|
|
841
684
|
queue: queued ? "outbox" : void 0,
|
|
842
685
|
activity,
|
|
843
|
-
rawActivity: options?.rawActivity,
|
|
844
686
|
sentOrder: ++this.federation.sentCounter
|
|
845
687
|
});
|
|
846
688
|
}
|
|
@@ -1052,7 +894,6 @@ const getRandomKey = (prefix) => `fedify_test_${prefix}_${crypto.randomUUID()}`;
|
|
|
1052
894
|
exports.createContext = createContext;
|
|
1053
895
|
exports.createFederation = createFederation;
|
|
1054
896
|
exports.createInboxContext = createInboxContext;
|
|
1055
|
-
exports.createOutboxContext = createOutboxContext;
|
|
1056
897
|
exports.createRequestContext = createRequestContext;
|
|
1057
898
|
exports.getRandomKey = getRandomKey;
|
|
1058
899
|
exports.testMessageQueue = testMessageQueue;
|
package/dist/mod.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Federation, InboxContext,
|
|
1
|
+
import { Context, Federation, InboxContext, RequestContext } from "@fedify/fedify/federation";
|
|
2
2
|
import { Activity } from "@fedify/vocab";
|
|
3
3
|
import { MessageQueue } from "@fedify/fedify";
|
|
4
4
|
|
|
@@ -27,12 +27,6 @@ declare function createRequestContext<TContextData>(args: Partial<RequestContext
|
|
|
27
27
|
*/
|
|
28
28
|
type TestInboxContext<TContextData> = InboxContext<TContextData>;
|
|
29
29
|
/**
|
|
30
|
-
* Test-specific OutboxContext type alias.
|
|
31
|
-
* This indirection helps avoid JSR type analyzer issues.
|
|
32
|
-
* @since 2.2.0
|
|
33
|
-
*/
|
|
34
|
-
type TestOutboxContext<TContextData> = OutboxContext<TContextData>;
|
|
35
|
-
/**
|
|
36
30
|
* Creates an InboxContext for testing purposes.
|
|
37
31
|
* Not exported - used internally only. Public API is in mock.ts
|
|
38
32
|
* @param args Partial InboxContext properties
|
|
@@ -45,19 +39,6 @@ declare function createInboxContext<TContextData>(args: Partial<InboxContext<TCo
|
|
|
45
39
|
recipient?: string | null;
|
|
46
40
|
federation: Federation<TContextData>;
|
|
47
41
|
}): TestInboxContext<TContextData>;
|
|
48
|
-
/**
|
|
49
|
-
* Creates an OutboxContext for testing purposes.
|
|
50
|
-
* Not exported - used internally only. Public API is in mock.ts
|
|
51
|
-
* @param args Partial OutboxContext properties
|
|
52
|
-
* @returns An OutboxContext instance
|
|
53
|
-
* @since 2.2.0
|
|
54
|
-
*/
|
|
55
|
-
declare function createOutboxContext<TContextData>(args: Partial<OutboxContext<TContextData>> & {
|
|
56
|
-
url?: URL;
|
|
57
|
-
data: TContextData;
|
|
58
|
-
identifier: string;
|
|
59
|
-
federation: Federation<TContextData>;
|
|
60
|
-
}): TestOutboxContext<TContextData>;
|
|
61
42
|
//#endregion
|
|
62
43
|
//#region src/mock.d.ts
|
|
63
44
|
/**
|
|
@@ -71,8 +52,6 @@ interface SentActivity {
|
|
|
71
52
|
queue?: "inbox" | "outbox" | "fanout";
|
|
72
53
|
/** The activity that was sent. */
|
|
73
54
|
activity: Activity;
|
|
74
|
-
/** The raw forwarded payload, if preserved by the caller. */
|
|
75
|
-
rawActivity?: unknown;
|
|
76
55
|
/** The order in which the activity was sent (auto-incrementing counter). */
|
|
77
56
|
sentOrder: number;
|
|
78
57
|
}
|
|
@@ -87,7 +66,6 @@ interface TestContext<TContextData> extends Omit<Context<TContextData>, "clone">
|
|
|
87
66
|
sender: any;
|
|
88
67
|
recipients: any;
|
|
89
68
|
activity: Activity;
|
|
90
|
-
rawActivity?: unknown;
|
|
91
69
|
}>;
|
|
92
70
|
reset(): void;
|
|
93
71
|
}
|
|
@@ -101,7 +79,6 @@ interface TestFederation<TContextData> extends Omit<Federation<TContextData>, "c
|
|
|
101
79
|
queueStarted: boolean;
|
|
102
80
|
sentCounter: number;
|
|
103
81
|
receiveActivity(activity: Activity): Promise<void>;
|
|
104
|
-
postOutboxActivity(identifier: string, activity: Activity): Promise<void>;
|
|
105
82
|
reset(): void;
|
|
106
83
|
createContext(baseUrlOrRequest: URL | Request, contextData: TContextData): TestContext<TContextData>;
|
|
107
84
|
}
|
|
@@ -217,4 +194,4 @@ declare function testMessageQueue<MQ extends MessageQueue>(getMessageQueue: () =
|
|
|
217
194
|
declare function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void>;
|
|
218
195
|
declare const getRandomKey: (prefix: string) => string;
|
|
219
196
|
//#endregion
|
|
220
|
-
export { type TestMessageQueueOptions, createContext, createFederation, createInboxContext,
|
|
197
|
+
export { type TestMessageQueueOptions, createContext, createFederation, createInboxContext, createRequestContext, getRandomKey, testMessageQueue, waitFor };
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import { Activity } from "@fedify/vocab";
|
|
3
|
-
import { Context, Federation, InboxContext,
|
|
3
|
+
import { Context, Federation, InboxContext, RequestContext } from "@fedify/fedify/federation";
|
|
4
4
|
import { MessageQueue } from "@fedify/fedify";
|
|
5
5
|
|
|
6
6
|
//#region src/context.d.ts
|
|
@@ -28,12 +28,6 @@ declare function createRequestContext<TContextData>(args: Partial<RequestContext
|
|
|
28
28
|
*/
|
|
29
29
|
type TestInboxContext<TContextData> = InboxContext<TContextData>;
|
|
30
30
|
/**
|
|
31
|
-
* Test-specific OutboxContext type alias.
|
|
32
|
-
* This indirection helps avoid JSR type analyzer issues.
|
|
33
|
-
* @since 2.2.0
|
|
34
|
-
*/
|
|
35
|
-
type TestOutboxContext<TContextData> = OutboxContext<TContextData>;
|
|
36
|
-
/**
|
|
37
31
|
* Creates an InboxContext for testing purposes.
|
|
38
32
|
* Not exported - used internally only. Public API is in mock.ts
|
|
39
33
|
* @param args Partial InboxContext properties
|
|
@@ -46,19 +40,6 @@ declare function createInboxContext<TContextData>(args: Partial<InboxContext<TCo
|
|
|
46
40
|
recipient?: string | null;
|
|
47
41
|
federation: Federation<TContextData>;
|
|
48
42
|
}): TestInboxContext<TContextData>;
|
|
49
|
-
/**
|
|
50
|
-
* Creates an OutboxContext for testing purposes.
|
|
51
|
-
* Not exported - used internally only. Public API is in mock.ts
|
|
52
|
-
* @param args Partial OutboxContext properties
|
|
53
|
-
* @returns An OutboxContext instance
|
|
54
|
-
* @since 2.2.0
|
|
55
|
-
*/
|
|
56
|
-
declare function createOutboxContext<TContextData>(args: Partial<OutboxContext<TContextData>> & {
|
|
57
|
-
url?: URL;
|
|
58
|
-
data: TContextData;
|
|
59
|
-
identifier: string;
|
|
60
|
-
federation: Federation<TContextData>;
|
|
61
|
-
}): TestOutboxContext<TContextData>;
|
|
62
43
|
//#endregion
|
|
63
44
|
//#region src/mock.d.ts
|
|
64
45
|
/**
|
|
@@ -72,8 +53,6 @@ interface SentActivity {
|
|
|
72
53
|
queue?: "inbox" | "outbox" | "fanout";
|
|
73
54
|
/** The activity that was sent. */
|
|
74
55
|
activity: Activity;
|
|
75
|
-
/** The raw forwarded payload, if preserved by the caller. */
|
|
76
|
-
rawActivity?: unknown;
|
|
77
56
|
/** The order in which the activity was sent (auto-incrementing counter). */
|
|
78
57
|
sentOrder: number;
|
|
79
58
|
}
|
|
@@ -88,7 +67,6 @@ interface TestContext<TContextData> extends Omit<Context<TContextData>, "clone">
|
|
|
88
67
|
sender: any;
|
|
89
68
|
recipients: any;
|
|
90
69
|
activity: Activity;
|
|
91
|
-
rawActivity?: unknown;
|
|
92
70
|
}>;
|
|
93
71
|
reset(): void;
|
|
94
72
|
}
|
|
@@ -102,7 +80,6 @@ interface TestFederation<TContextData> extends Omit<Federation<TContextData>, "c
|
|
|
102
80
|
queueStarted: boolean;
|
|
103
81
|
sentCounter: number;
|
|
104
82
|
receiveActivity(activity: Activity): Promise<void>;
|
|
105
|
-
postOutboxActivity(identifier: string, activity: Activity): Promise<void>;
|
|
106
83
|
reset(): void;
|
|
107
84
|
createContext(baseUrlOrRequest: URL | Request, contextData: TContextData): TestContext<TContextData>;
|
|
108
85
|
}
|
|
@@ -218,4 +195,4 @@ declare function testMessageQueue<MQ extends MessageQueue>(getMessageQueue: () =
|
|
|
218
195
|
declare function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void>;
|
|
219
196
|
declare const getRandomKey: (prefix: string) => string;
|
|
220
197
|
//#endregion
|
|
221
|
-
export { type TestMessageQueueOptions, createContext, createFederation, createInboxContext,
|
|
198
|
+
export { type TestMessageQueueOptions, createContext, createFederation, createInboxContext, createRequestContext, getRandomKey, testMessageQueue, waitFor };
|
package/dist/mod.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
-
import {
|
|
3
|
-
import { Activity, CryptographicKey, Multikey, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
2
|
+
import { CryptographicKey, Multikey, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
4
3
|
import { RouterError } from "@fedify/fedify/federation";
|
|
5
4
|
import { delay } from "es-toolkit";
|
|
6
5
|
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
@@ -112,9 +111,6 @@ function createRequestContext(args) {
|
|
|
112
111
|
* @since 1.8.0
|
|
113
112
|
*/
|
|
114
113
|
function createInboxContext(args) {
|
|
115
|
-
const forwardActivity = args.forwardActivity ?? ((_forwarder, _recipients, _options) => {
|
|
116
|
-
throw new Error("Not implemented");
|
|
117
|
-
});
|
|
118
114
|
return {
|
|
119
115
|
...createContext(args),
|
|
120
116
|
clone: args.clone ?? ((data) => createInboxContext({
|
|
@@ -122,29 +118,9 @@ function createInboxContext(args) {
|
|
|
122
118
|
data
|
|
123
119
|
})),
|
|
124
120
|
recipient: args.recipient ?? null,
|
|
125
|
-
forwardActivity
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Creates an OutboxContext for testing purposes.
|
|
130
|
-
* Not exported - used internally only. Public API is in mock.ts
|
|
131
|
-
* @param args Partial OutboxContext properties
|
|
132
|
-
* @returns An OutboxContext instance
|
|
133
|
-
* @since 2.2.0
|
|
134
|
-
*/
|
|
135
|
-
function createOutboxContext(args) {
|
|
136
|
-
const forwardActivity = args.forwardActivity ?? ((_forwarder, _recipients, _options) => {
|
|
137
|
-
throw new Error("Not implemented");
|
|
138
|
-
});
|
|
139
|
-
return {
|
|
140
|
-
...createContext(args),
|
|
141
|
-
clone: args.clone ?? ((data) => createOutboxContext({
|
|
142
|
-
...args,
|
|
143
|
-
data
|
|
144
|
-
})),
|
|
145
|
-
identifier: args.identifier,
|
|
146
|
-
hasDeliveredActivity: args.hasDeliveredActivity ?? (() => false),
|
|
147
|
-
forwardActivity
|
|
121
|
+
forwardActivity: args.forwardActivity ?? ((_params) => {
|
|
122
|
+
throw new Error("Not implemented");
|
|
123
|
+
})
|
|
148
124
|
};
|
|
149
125
|
}
|
|
150
126
|
//#endregion
|
|
@@ -154,41 +130,17 @@ const noopTracerProvider = { getTracer: () => ({
|
|
|
154
130
|
startSpan: () => void 0
|
|
155
131
|
}) };
|
|
156
132
|
/**
|
|
157
|
-
* Helper function to expand URI templates
|
|
158
|
-
* Supports
|
|
133
|
+
* Helper function to expand URI templates with values.
|
|
134
|
+
* Supports simple placeholders like {identifier}, etc.
|
|
159
135
|
* @param template The URI template pattern
|
|
160
136
|
* @param values The values to substitute
|
|
161
137
|
* @returns The expanded URI path
|
|
162
138
|
*/
|
|
163
139
|
function expandUriTemplate(template, values) {
|
|
164
|
-
return template.replace(/{([
|
|
165
|
-
|
|
166
|
-
if (value == null) return match;
|
|
167
|
-
switch (operator) {
|
|
168
|
-
case "": return encodeURIComponent(value);
|
|
169
|
-
case "+": return encodeURI(value);
|
|
170
|
-
case "#": return `#${encodeURI(value)}`;
|
|
171
|
-
case ".": return `.${encodeURIComponent(value)}`;
|
|
172
|
-
case "/": return `/${encodeURIComponent(value)}`;
|
|
173
|
-
case ";": return `;${key}=${encodeURIComponent(value)}`;
|
|
174
|
-
case "?": return `?${key}=${encodeURIComponent(value)}`;
|
|
175
|
-
case "&": return `&${key}=${encodeURIComponent(value)}`;
|
|
176
|
-
default: return match;
|
|
177
|
-
}
|
|
140
|
+
return template.replace(/{([^}]+)}/g, (match, key) => {
|
|
141
|
+
return values[key] || match;
|
|
178
142
|
});
|
|
179
143
|
}
|
|
180
|
-
function validateOutboxListenerPath(path, dispatcherPath) {
|
|
181
|
-
if (!path.startsWith("/")) throw new TypeError("Path must start with a slash.");
|
|
182
|
-
if (dispatcherPath != null && dispatcherPath !== path) throw new TypeError("Outbox listener path and outbox dispatcher path must match.");
|
|
183
|
-
const operatorMatches = globalThis.Array.from(path.matchAll(/{([+#./;?&]?)([A-Za-z_][A-Za-z0-9_]*)}/g));
|
|
184
|
-
if (operatorMatches.some((match) => [
|
|
185
|
-
"?",
|
|
186
|
-
"&",
|
|
187
|
-
"#"
|
|
188
|
-
].includes(match[1]) && match[2] === "identifier")) throw new TypeError("Path for outbox cannot use query or fragment expansion for identifier.");
|
|
189
|
-
const variables = operatorMatches.map((match) => match[2]);
|
|
190
|
-
if (variables.length !== 1 || variables[0] !== "identifier") throw new TypeError("Path for outbox must have exactly one variable named identifier.");
|
|
191
|
-
}
|
|
192
144
|
/**
|
|
193
145
|
* A mock implementation of the {@link Federation} interface for unit testing.
|
|
194
146
|
* This class provides a way to test Fedify applications without needing
|
|
@@ -245,17 +197,12 @@ var MockFederation = class {
|
|
|
245
197
|
objectDispatchers = /* @__PURE__ */ new Map();
|
|
246
198
|
inboxDispatcher;
|
|
247
199
|
outboxDispatcher;
|
|
248
|
-
outboxAuthorizePredicate;
|
|
249
|
-
outboxDispatcherAuthorizePredicate;
|
|
250
|
-
outboxListenerErrorHandler;
|
|
251
200
|
followingDispatcher;
|
|
252
201
|
followersDispatcher;
|
|
253
202
|
likedDispatcher;
|
|
254
203
|
featuredDispatcher;
|
|
255
204
|
featuredTagsDispatcher;
|
|
256
205
|
inboxListeners = /* @__PURE__ */ new Map();
|
|
257
|
-
outboxListeners = /* @__PURE__ */ new Map();
|
|
258
|
-
outboxListenersInitialized = false;
|
|
259
206
|
contextData;
|
|
260
207
|
receivedActivities = [];
|
|
261
208
|
constructor(options = {}) {
|
|
@@ -297,17 +244,13 @@ var MockFederation = class {
|
|
|
297
244
|
};
|
|
298
245
|
}
|
|
299
246
|
setOutboxDispatcher(path, dispatcher) {
|
|
300
|
-
validateOutboxListenerPath(path, this.outboxListenersInitialized ? this.outboxPath : void 0);
|
|
301
247
|
this.outboxDispatcher = dispatcher;
|
|
302
248
|
this.outboxPath = path;
|
|
303
249
|
return {
|
|
304
250
|
setCounter: () => this,
|
|
305
251
|
setFirstCursor: () => this,
|
|
306
252
|
setLastCursor: () => this,
|
|
307
|
-
authorize: (
|
|
308
|
-
this.outboxDispatcherAuthorizePredicate = predicate;
|
|
309
|
-
return this;
|
|
310
|
-
}
|
|
253
|
+
authorize: () => this
|
|
311
254
|
};
|
|
312
255
|
}
|
|
313
256
|
setFollowingDispatcher(path, dispatcher) {
|
|
@@ -385,28 +328,6 @@ var MockFederation = class {
|
|
|
385
328
|
}
|
|
386
329
|
};
|
|
387
330
|
}
|
|
388
|
-
setOutboxListeners(outboxPath) {
|
|
389
|
-
if (this.outboxListenersInitialized) throw new TypeError("Outbox listeners already set.");
|
|
390
|
-
validateOutboxListenerPath(outboxPath, this.outboxPath);
|
|
391
|
-
this.outboxListenersInitialized = true;
|
|
392
|
-
this.outboxPath = outboxPath;
|
|
393
|
-
const self = this;
|
|
394
|
-
return {
|
|
395
|
-
on(type, listener) {
|
|
396
|
-
if (self.outboxListeners.has(type)) throw new TypeError("Listener already set for this type.");
|
|
397
|
-
self.outboxListeners.set(type, listener);
|
|
398
|
-
return this;
|
|
399
|
-
},
|
|
400
|
-
onError(handler) {
|
|
401
|
-
self.outboxListenerErrorHandler = handler;
|
|
402
|
-
return this;
|
|
403
|
-
},
|
|
404
|
-
authorize(predicate) {
|
|
405
|
-
self.outboxAuthorizePredicate = predicate;
|
|
406
|
-
return this;
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
331
|
setOutboxPermanentFailureHandler(_handler) {}
|
|
411
332
|
async startQueue(contextData, options) {
|
|
412
333
|
this.contextData = contextData;
|
|
@@ -423,10 +344,8 @@ var MockFederation = class {
|
|
|
423
344
|
}
|
|
424
345
|
createContext(baseUrlOrRequest, contextData) {
|
|
425
346
|
const mockFederation = this;
|
|
426
|
-
const request = baseUrlOrRequest instanceof Request ? baseUrlOrRequest : null;
|
|
427
347
|
return new MockContext({
|
|
428
|
-
url:
|
|
429
|
-
request,
|
|
348
|
+
url: baseUrlOrRequest instanceof Request ? new URL(baseUrlOrRequest.url) : baseUrlOrRequest,
|
|
430
349
|
data: contextData,
|
|
431
350
|
federation: mockFederation
|
|
432
351
|
});
|
|
@@ -454,81 +373,6 @@ var MockFederation = class {
|
|
|
454
373
|
}), activity);
|
|
455
374
|
}
|
|
456
375
|
/**
|
|
457
|
-
* Simulates posting an activity to a local actor outbox.
|
|
458
|
-
* This method is specific to the mock implementation and is used for
|
|
459
|
-
* testing purposes.
|
|
460
|
-
*
|
|
461
|
-
* @param identifier The identifier of the outbox owner.
|
|
462
|
-
* @param activity The activity to post.
|
|
463
|
-
* @returns A promise that resolves when the activity has been processed.
|
|
464
|
-
* @since 2.2.0
|
|
465
|
-
*/
|
|
466
|
-
async postOutboxActivity(identifier, activity) {
|
|
467
|
-
if (!this.outboxListenersInitialized) throw new Error("MockFederation.postOutboxActivity(): setOutboxListeners() is not initialized.");
|
|
468
|
-
let ctor = activity.constructor;
|
|
469
|
-
let listener = this.outboxListeners.get(ctor);
|
|
470
|
-
while (listener == null && ctor !== Activity) {
|
|
471
|
-
ctor = globalThis.Object.getPrototypeOf(ctor);
|
|
472
|
-
listener = this.outboxListeners.get(ctor);
|
|
473
|
-
}
|
|
474
|
-
if (listener != null && this.contextData === void 0) throw new Error("MockFederation.postOutboxActivity(): contextData is not initialized. Please provide contextData through the constructor or call startQueue() before posting activities.");
|
|
475
|
-
const origin = new URL(this.options.origin ?? "https://example.com");
|
|
476
|
-
const routingContext = this.createContext(origin, this.contextData);
|
|
477
|
-
const postedJson = await activity.toJsonLd({ contextLoader: routingContext.contextLoader });
|
|
478
|
-
const request = new Request(routingContext.getOutboxUri(identifier), {
|
|
479
|
-
method: "POST",
|
|
480
|
-
body: JSON.stringify(postedJson),
|
|
481
|
-
headers: { "content-type": "application/activity+json" }
|
|
482
|
-
});
|
|
483
|
-
const baseContext = this.createContext(request, this.contextData);
|
|
484
|
-
const rawActivity = postedJson;
|
|
485
|
-
const deliveryState = { delivered: false };
|
|
486
|
-
const createMockOutboxContext = () => createOutboxContext({
|
|
487
|
-
...baseContext,
|
|
488
|
-
clone: void 0,
|
|
489
|
-
federation: this,
|
|
490
|
-
identifier,
|
|
491
|
-
hasDeliveredActivity: () => deliveryState.delivered,
|
|
492
|
-
sendActivity: async (sender, recipients, outboundActivity, options) => {
|
|
493
|
-
await baseContext.sendActivity(sender, recipients, outboundActivity, options);
|
|
494
|
-
deliveryState.delivered = true;
|
|
495
|
-
},
|
|
496
|
-
forwardActivity: async (forwarder, recipients, options) => {
|
|
497
|
-
const hasProof = hasProofLike(rawActivity);
|
|
498
|
-
const hasLds = hasSignatureLike(rawActivity);
|
|
499
|
-
if (options?.skipIfUnsigned && !hasProof && !hasLds) return;
|
|
500
|
-
await baseContext.sendActivity(forwarder, recipients, activity, {
|
|
501
|
-
...options,
|
|
502
|
-
rawActivity
|
|
503
|
-
});
|
|
504
|
-
deliveryState.delivered = true;
|
|
505
|
-
}
|
|
506
|
-
});
|
|
507
|
-
const actor = await baseContext.getActor(identifier);
|
|
508
|
-
if (actor == null) throw new Error(`Actor ${JSON.stringify(identifier)} not found.`);
|
|
509
|
-
const authorizePredicate = this.outboxAuthorizePredicate ?? this.outboxDispatcherAuthorizePredicate;
|
|
510
|
-
if (authorizePredicate != null && !await authorizePredicate(baseContext, identifier)) throw new Error("Unauthorized.");
|
|
511
|
-
const expectedActorId = actor.id ?? baseContext.getActorUri(identifier);
|
|
512
|
-
if (activity.actorIds.length < 1) {
|
|
513
|
-
const error = /* @__PURE__ */ new Error("The posted activity has no actor.");
|
|
514
|
-
await this.outboxListenerErrorHandler?.(createMockOutboxContext(), error);
|
|
515
|
-
throw error;
|
|
516
|
-
}
|
|
517
|
-
if (!activity.actorIds.every((actorId) => actorId.href === expectedActorId.href)) {
|
|
518
|
-
const error = /* @__PURE__ */ new Error("The activity actor does not match the outbox owner.");
|
|
519
|
-
await this.outboxListenerErrorHandler?.(createMockOutboxContext(), error);
|
|
520
|
-
throw error;
|
|
521
|
-
}
|
|
522
|
-
if (listener == null) return;
|
|
523
|
-
const context = createMockOutboxContext();
|
|
524
|
-
try {
|
|
525
|
-
await listener(context, activity);
|
|
526
|
-
} catch (error) {
|
|
527
|
-
await this.outboxListenerErrorHandler?.(context, error);
|
|
528
|
-
throw error;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
376
|
* Clears all sent activities from the mock federation.
|
|
533
377
|
* This method is specific to the mock implementation and is used for
|
|
534
378
|
* testing purposes.
|
|
@@ -657,7 +501,7 @@ var MockContext = class MockContext {
|
|
|
657
501
|
this.host = url.host;
|
|
658
502
|
this.hostname = url.hostname;
|
|
659
503
|
this.url = url;
|
|
660
|
-
this.request =
|
|
504
|
+
this.request = new Request(url);
|
|
661
505
|
this.data = options.data;
|
|
662
506
|
this.federation = options.federation;
|
|
663
507
|
this.documentLoader = options.documentLoader ?? (async (url) => ({
|
|
@@ -826,12 +670,11 @@ var MockContext = class MockContext {
|
|
|
826
670
|
lookupWebFinger(_resource, _options) {
|
|
827
671
|
return Promise.resolve(null);
|
|
828
672
|
}
|
|
829
|
-
sendActivity(sender, recipients, activity,
|
|
673
|
+
sendActivity(sender, recipients, activity, _options) {
|
|
830
674
|
this.sentActivities.push({
|
|
831
675
|
sender,
|
|
832
676
|
recipients,
|
|
833
|
-
activity
|
|
834
|
-
rawActivity: options?.rawActivity
|
|
677
|
+
activity
|
|
835
678
|
});
|
|
836
679
|
if (this.federation instanceof MockFederation) {
|
|
837
680
|
const queued = this.federation.queueStarted;
|
|
@@ -839,7 +682,6 @@ var MockContext = class MockContext {
|
|
|
839
682
|
queued,
|
|
840
683
|
queue: queued ? "outbox" : void 0,
|
|
841
684
|
activity,
|
|
842
|
-
rawActivity: options?.rawActivity,
|
|
843
685
|
sentOrder: ++this.federation.sentCounter
|
|
844
686
|
});
|
|
845
687
|
}
|
|
@@ -1048,4 +890,4 @@ async function waitFor(predicate, timeoutMs) {
|
|
|
1048
890
|
}
|
|
1049
891
|
const getRandomKey = (prefix) => `fedify_test_${prefix}_${crypto.randomUUID()}`;
|
|
1050
892
|
//#endregion
|
|
1051
|
-
export { createContext, createFederation, createInboxContext,
|
|
893
|
+
export { createContext, createFederation, createInboxContext, createRequestContext, getRandomKey, testMessageQueue, waitFor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/testing",
|
|
3
|
-
"version": "2.2.0-
|
|
3
|
+
"version": "2.2.0-pr.695.23+d0b31ca2",
|
|
4
4
|
"description": "Testing utilities for Fedify applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fedify",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"package.json"
|
|
51
51
|
],
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@fedify/fedify": "^2.2.0-
|
|
53
|
+
"@fedify/fedify": "^2.2.0-pr.695.23+d0b31ca2"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"es-toolkit": "1.43.0"
|