@fedify/testing
Version:
Testing utilities for Fedify applications
221 lines (220 loc) • 7.86 kB
text/typescript
/// <reference lib="esnext.temporal" />
import { Context, Federation, InboxContext, OutboxContext, RequestContext } from "@fedify/fedify/federation";
import { Activity } from "@fedify/vocab";
import { MessageQueue } from "@fedify/fedify";
//#region src/context.d.ts
declare function createContext<TContextData>(values: Partial<Context<TContextData>> & {
url?: URL;
data: TContextData;
federation: Federation<TContextData>;
}): Context<TContextData>;
/**
* Creates a RequestContext for testing purposes.
* Not exported - used internally only. Public API is in mock.ts
* @param args Partial RequestContext properties
* @returns A RequestContext instance
* @since 1.8.0
*/
declare function createRequestContext<TContextData>(args: Partial<RequestContext<TContextData>> & {
url: URL;
data: TContextData;
federation: Federation<TContextData>;
}): RequestContext<TContextData>;
/**
* Test-specific InboxContext type alias.
* This indirection helps avoid JSR type analyzer issues.
* @since 1.9.1
*/
type TestInboxContext<TContextData> = InboxContext<TContextData>;
/**
* Test-specific OutboxContext type alias.
* This indirection helps avoid JSR type analyzer issues.
* @since 2.2.0
*/
type TestOutboxContext<TContextData> = OutboxContext<TContextData>;
/**
* Creates an InboxContext for testing purposes.
* Not exported - used internally only. Public API is in mock.ts
* @param args Partial InboxContext properties
* @returns An InboxContext instance
* @since 1.8.0
*/
declare function createInboxContext<TContextData>(args: Partial<InboxContext<TContextData>> & {
url?: URL;
data: TContextData;
recipient?: string | null;
federation: Federation<TContextData>;
}): TestInboxContext<TContextData>;
/**
* Creates an OutboxContext for testing purposes.
* Not exported - used internally only. Public API is in mock.ts
* @param args Partial OutboxContext properties
* @returns An OutboxContext instance
* @since 2.2.0
*/
declare function createOutboxContext<TContextData>(args: Partial<OutboxContext<TContextData>> & {
url?: URL;
data: TContextData;
identifier: string;
federation: Federation<TContextData>;
}): TestOutboxContext<TContextData>;
//#endregion
//#region src/mock.d.ts
/**
* Represents a sent activity with metadata about how it was sent.
* @since 1.8.0
*/
interface SentActivity {
/** Whether the activity was queued or sent immediately. */
queued: boolean;
/** Which queue was used (if queued). */
queue?: "inbox" | "outbox" | "fanout";
/** The activity that was sent. */
activity: Activity;
/** The raw forwarded payload, if preserved by the caller. */
rawActivity?: unknown;
/** The order in which the activity was sent (auto-incrementing counter). */
sentOrder: number;
}
/**
* A mock Context interface for testing purposes.
* Extends the standard Context interface with additional testing utilities.
* @since 1.9.1
*/
interface TestContext<TContextData> extends Omit<Context<TContextData>, "clone">, Pick<RequestContext<TContextData>, "request" | "url" | "getActor" | "getObject" | "getSignedKey" | "getSignedKeyOwner" | "sendActivity" | "routeActivity"> {
clone(data: TContextData): TestContext<TContextData>;
getSentActivities(): Array<{
sender: any;
recipients: any;
activity: Activity;
rawActivity?: unknown;
}>;
reset(): void;
}
/**
* A mock Federation interface for testing purposes.
* Extends the standard Federation interface with additional testing utilities.
* @since 1.9.1
*/
interface TestFederation<TContextData> extends Omit<Federation<TContextData>, "createContext"> {
sentActivities: SentActivity[];
queueStarted: boolean;
sentCounter: number;
receiveActivity(activity: Activity): Promise<void>;
postOutboxActivity(identifier: string, activity: Activity): Promise<void>;
reset(): void;
createContext(baseUrlOrRequest: URL | Request, contextData: TContextData): TestContext<TContextData>;
}
/**
* Creates a mock Federation instance for testing purposes.
*
* @template TContextData The type of context data to use
* @param options Optional configuration for the mock federation
* @returns A Federation instance that can be used for testing
* @since 1.9.1
*
* @example
* ```typescript
* import { Create } from "@fedify/vocab";
* import { createFederation } from "@fedify/testing";
*
* // Create a mock federation with contextData
* const federation = createFederation<{ userId: string }>({
* contextData: { userId: "test-user" }
* });
*
* // Set up inbox listeners
* federation
* .setInboxListeners("/users/{identifier}/inbox")
* .on(Create, async (ctx, activity) => {
* console.log("Received:", activity);
* });
*
* // Simulate receiving an activity
* const createActivity = new Create({
* id: new URL("https://example.com/create/1"),
* actor: new URL("https://example.com/users/alice")
* });
* await federation.receiveActivity(createActivity);
*
* // Check sent activities
* console.log(federation.sentActivities);
* ```
*/
declare function createFederation<TContextData>(options?: {
contextData?: TContextData;
origin?: string;
tracerProvider?: any;
}): TestFederation<TContextData>;
//#endregion
//#region src/mq-tester.d.ts
/**
* Options for {@link testMessageQueue}.
*/
interface TestMessageQueueOptions {
/**
* Whether to test ordering key support. If `true`, tests will verify that
* messages with the same ordering key are processed in order, while messages
* with different ordering keys can be processed in parallel.
*
* Set this to `true` only if your message queue implementation supports
* the `orderingKey` option.
*
* @default false
*/
readonly testOrderingKey?: boolean;
}
/**
* Tests a {@link MessageQueue} implementation with a standard set of tests.
*
* This function runs tests for:
* - `enqueue()`: Basic message enqueueing
* - `enqueue()` with delay: Delayed message enqueueing
* - `enqueueMany()`: Bulk message enqueueing
* - `enqueueMany()` with delay: Delayed bulk message enqueueing
* - Multiple listeners: Ensures messages are processed by only one listener
* - Ordering key support (optional): Ensures messages with the same ordering
* key are processed in order
*
* @example
* ```typescript ignore
* import { test } from "@fedify/fixture";
* import { testMessageQueue } from "@fedify/testing";
* import { MyMessageQueue } from "./my-mq.ts";
*
* test("MyMessageQueue", () =>
* testMessageQueue(
* () => new MyMessageQueue(),
* async ({ mq1, mq2, controller }) => {
* controller.abort();
* await mq1.close();
* await mq2.close();
* },
* { testOrderingKey: true }, // Enable ordering key tests
* )
* );
* ```
*
* @param getMessageQueue A factory function that creates a new message queue
* instance. It should return a new instance each time
* to ensure test isolation, but both instances should
* share the same underlying storage/channel.
* @param onFinally A cleanup function called after all tests complete.
* It receives both message queue instances and the abort
* controller used for the listeners.
* @param options Optional configuration for the test suite.
* @returns A promise that resolves when all tests pass.
*/
declare function testMessageQueue<MQ extends MessageQueue>(getMessageQueue: () => MQ | Promise<MQ>, onFinally: ({
mq1,
mq2,
controller
}: {
mq1: MQ;
mq2: MQ;
controller: AbortController;
}) => Promise<void> | void, options?: TestMessageQueueOptions): Promise<void>;
declare function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void>;
declare const getRandomKey: (prefix: string) => string;
//#endregion
export { type TestMessageQueueOptions, createContext, createFederation, createInboxContext, createOutboxContext, createRequestContext, getRandomKey, testMessageQueue, waitFor };