UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

824 lines 25.1 kB
import { type MemoryErrorReporterPort } from "../error-reporting/index.js"; import { type MemoryFlagsPort } from "../flags/index.js"; import { type MemoryIdempotencyStore } from "../idempotency/index.js"; import { type MemoryLocksPort } from "../locks/index.js"; import { type MemoryMailerPort } from "../mail/index.js"; import { type MemoryNotificationPort } from "../notifications/index.js"; import { type MemoryOutboxPort } from "../outbox/index.js"; import { type MemoryPaymentsPort } from "../payments/index.js"; import { type ActivityActor, type ActivityTenant, type AuditLogPort, type MemoryAuditLogPort } from "../ports/audit.js"; import { type CachePort } from "../ports/cache.js"; import { type ClockPort } from "../ports/clock.js"; import type { EventBusPort, JobDispatcherPort } from "../ports/events.js"; import { type IdGeneratorPort } from "../ports/id-generator.js"; import { type AnyPorts, type BoundGate, type GatePort, type LoggerPort, type RateLimitPort, type StoragePort, type UnitOfWorkPort } from "../ports/index.js"; import { type RecordedEvent, type RecordedJobDispatch } from "../ports/testing.js"; import type { AnyServiceProvider, ProviderSetupResult } from "../providers/provider.js"; import { type MemorySearchPort } from "../search/index.js"; export * from "../ports/testing.js"; /** * Value that may be returned synchronously or asynchronously. */ export type MaybePromise<T> = T | Promise<T>; /** * Common Beignet ports created by `createTestPorts(...)`. */ export interface CommonTestPorts<TxPorts = AnyPorts> { [key: string]: unknown; /** * Ambient-enriched in-memory audit log. Entries recorded through this port * inherit actor, tenant, request ID, and trace ID from the active request * context, matching production server behavior. Use the fixture's `audit` * for entry assertions. */ audit: AuditLogPort; /** * In-memory string cache. */ cache: CachePort; /** * Mutable deterministic test clock. */ clock: ClockPort; /** * Recording event bus. */ eventBus: EventBusPort; /** * In-memory error reporter. */ errorReporter: MemoryErrorReporterPort; /** * Authorization gate. Apps with real policies should provide their own gate * through `overrides`. */ gate: GatePort<unknown, []>; /** * In-memory feature flags. */ flags: MemoryFlagsPort; /** * In-memory idempotency store. */ idempotency: MemoryIdempotencyStore; /** * Sequence or UUID generator used by tests. */ ids: IdGeneratorPort; /** * Recording job dispatcher. */ jobs: JobDispatcherPort; /** * No-op logger. */ logger: LoggerPort; /** * In-memory lease-backed locks. */ locks: MemoryLocksPort; /** * In-memory mailer. */ mailer: MemoryMailerPort; /** * In-memory notification port. */ notifications: MemoryNotificationPort; /** * In-memory outbox. */ outbox: MemoryOutboxPort; /** * In-memory payments port. */ payments: MemoryPaymentsPort; /** * In-memory search port. */ search: MemorySearchPort; /** * In-memory rate limiter. */ rateLimit: RateLimitPort; /** * In-memory object storage. */ storage: StoragePort; /** * No-op Unit of Work over the configured transaction ports. */ uow: UnitOfWorkPort<TxPorts>; } /** * Captured state and ports returned by `createTestPorts(...)`. */ export interface TestPortsFixture<Ports extends AnyPorts = CommonTestPorts, TxPorts = Ports> { /** * App ports to pass into a use-case or route test context. */ ports: Ports; /** * Memory audit port, exposed for assertions. */ audit: MemoryAuditLogPort; /** * Recording event bus port. */ eventBus: EventBusPort; /** * Recorded events published through `eventBus`. */ events: RecordedEvent[]; /** * Memory error reporter, exposed for assertions. */ errorReporter: MemoryErrorReporterPort; /** * Recording job dispatcher port. */ jobs: JobDispatcherPort; /** * Jobs dispatched through `jobs`. */ dispatchedJobs: RecordedJobDispatch[]; /** * Memory mailer, exposed for delivery assertions. */ mailer: MemoryMailerPort; /** * Memory notification port, exposed for delivery assertions. */ notifications: MemoryNotificationPort; /** * Memory outbox, exposed for durable workflow assertions. */ outbox: MemoryOutboxPort; /** * Memory payments port. */ payments: MemoryPaymentsPort; /** * Memory storage port. */ storage: StoragePort; /** * Memory idempotency store. */ idempotency: MemoryIdempotencyStore; /** * Test cache port. */ cache: CachePort; /** * Test feature flags port. */ flags: MemoryFlagsPort; /** * Test rate-limit port. */ rateLimit: RateLimitPort; /** * Test logger port. */ logger: LoggerPort; /** * Test locks port. */ locks: MemoryLocksPort; /** * Test search port. */ search: MemorySearchPort; /** * Test clock port. */ clock: ClockPort; /** * Test ID generator. */ ids: IdGeneratorPort; /** * Unit of Work port installed on `ports`. */ uow: UnitOfWorkPort<TxPorts>; } /** * Unit of Work behavior for `createTestPorts(...)`. */ export interface CreateTestPortsTransactionOptions<Ports extends AnyPorts, TxPorts> { /** * Transaction-scoped ports or a function that derives them from final ports. * * Defaults to the final `ports` object. */ ports?: TxPorts | ((ports: Ports) => TxPorts); /** * Hook run after the transaction callback resolves. */ afterCommit?: (tx: TxPorts) => MaybePromise<void>; /** * Hook run after the transaction callback throws. */ afterRollback?: (error: unknown, tx: TxPorts) => MaybePromise<void>; /** * Enqueue `tx.events` (a buffered domain event recorder) to `ports.outbox` * after the transaction commits, and clear it after a rollback. * * Requires `transaction.ports` to include an `events` recorder created by * `createDomainEventRecorder()`. */ outbox?: boolean; } /** * Override shape for one test port. * * Object-valued ports may be supplied one level deep as partials: the missing * members are completed behind a proxy that throws a named error on use. * Function-valued ports must be supplied whole, so the check for functions * happens before the object branch. */ export type TestPortOverride<Port> = Port extends (...args: never[]) => unknown ? Port : Port extends object ? Partial<Port> : Port; /** * Typed partial port overrides accepted by `createTestPorts(...)` and * `createTestContext(...)`. */ export type TestPortsOverrides<Ports extends AnyPorts> = { [K in keyof Ports]?: TestPortOverride<Ports[K]>; }; /** * Options for `createTestPorts(...)`. */ export interface CreateTestPortsOptions<Ports extends AnyPorts, TxPorts = Ports> { /** * App-owned default ports, usually imported from `infra/port-wiring`. Common * Beignet test defaults replace matching keys from `base`; use `overrides` * for app ports that should win. * * Values are intentionally loose: boot-time app ports may bind wider types * (or deferred placeholders) than the provider-contributed runtime ports. */ base?: { [K in keyof Ports]?: unknown; }; /** * Test-specific ports that should replace generated defaults or `base`. * * Plain-object ports may be supplied as one-level-deep partials; missing * members throw a named error when called. Function-valued ports, class * instances, and other exotic objects are passed through whole. */ overrides?: TestPortsOverrides<Ports>; /** * Clock implementation. Defaults to a frozen clock at the Unix epoch. */ clock?: ClockPort; /** * ID generator. Defaults to UUIDs. */ ids?: IdGeneratorPort; /** * Unit of Work configuration. A no-op UOW is installed by default. */ transaction?: CreateTestPortsTransactionOptions<Ports, TxPorts>; } /** * Create standard memory/fake Beignet ports for tests. * * Use this as the starting point for use-case and route tests, then layer * app-owned repositories or provider fakes through `base` and `overrides`. * * @param options - Optional app ports, overrides, and Unit of Work behavior. * @returns Ports plus captured state for assertions. */ export declare function createTestPorts<Ports extends AnyPorts = CommonTestPorts, TxPorts = Ports>(options?: CreateTestPortsOptions<Ports, TxPorts>): TestPortsFixture<Ports, TxPorts>; /** * Bound gate type inferred from a ports object. */ export type TestBoundGateForPorts<Ports extends AnyPorts> = Ports extends { gate?: { bind: (ctx: never) => infer Bound; }; } ? Bound : BoundGate<readonly []>; /** * Common app context fields built by `createTestContextFactory(...)`. */ export type TestContextFields<Ports extends AnyPorts> = { /** * Actor under test. */ actor: ActivityActor; /** * Optional tenant under test. */ tenant?: ActivityTenant; /** * Stable request ID. */ requestId: string; /** * Stable trace ID. */ traceId?: string; /** * Optional auth/session value. */ auth?: unknown; /** * App ports under test. */ ports: Ports; /** * Bound authorization gate when `ports.gate` is available. */ gate?: TestBoundGateForPorts<Ports>; }; /** * Options for `createTestContextFactory(...)`. */ export interface CreateTestContextFactoryOptions<Ctx, Ports extends AnyPorts> { /** * Ports or a callback returning ports for each created context. */ ports: Ports | (() => Ports); /** * Actor used by default. * * @default createTestUserActor() */ actor?: ActivityActor; /** * Tenant used by default. Pass `null` to omit tenant context. */ tenant?: ActivityTenant | null; /** * Request ID used by default. */ requestId?: string; /** * Trace ID used by default. */ traceId?: string; /** * Auth/session value added to the context. */ auth?: unknown; /** * Additional app-specific context fields. */ extra?: Partial<Ctx> | ((args: TestContextFields<Ports>) => Partial<Ctx>); } /** * Per-call overrides for a test context factory. */ export type TestContextFactoryOverrides<Ctx, Ports extends AnyPorts> = Partial<Pick<CreateTestContextFactoryOptions<Ctx, Ports>, "actor" | "tenant" | "requestId" | "traceId" | "auth" | "extra">> & { /** * Ports for this context call. */ ports?: Ports; }; /** * Create a repeatable app context factory for use-case and route tests. * * The factory adds actor, tenant, request ID, trace ID, ports, and auth fields, * and attaches a live `ctx.gate` automatically when `ports.gate` exposes * `bind(...)`. The gate is attached after all `extra` and override fields are * merged, so it always authorizes against the final context identity. * * @param options - Default context values. * @returns A function that creates one app context. */ export declare function createTestContextFactory<Ctx extends object, Ports extends AnyPorts>(options: CreateTestContextFactoryOptions<Ctx, Ports>): (overrides?: TestContextFactoryOverrides<Ctx, Ports>) => Ctx; declare const disposeSymbol: typeof Symbol.dispose; /** * Options for `createTestContext(...)`. */ export interface CreateTestContextOptions<Ctx, Ports extends AnyPorts> { /** * Actor under test. * * @default createTestSystemActor("test-system") */ actor?: ActivityActor; /** * Tenant under test. Pass `null` to omit tenant context. * * @default createTestTenant() */ tenant?: ActivityTenant | null; /** * Request ID exposed on the context. * * @default "test-request" */ requestId?: string; /** * Trace ID exposed on the context. * * @default "test-trace" */ traceId?: string; /** * Auth/session value added to the context. * * @default null */ auth?: unknown; /** * App-owned default ports, usually imported from `infra/port-wiring`. Common * Beignet test defaults replace matching keys from `base`; use `ports` for * app ports that should win. */ base?: { [K in keyof Ports]?: unknown; }; /** * Typed partial port overrides that replace generated defaults or `base`. */ ports?: TestPortsOverrides<Ports>; /** * Clock implementation. Defaults to a frozen clock at the Unix epoch. */ clock?: ClockPort; /** * ID generator. Defaults to UUIDs. */ ids?: IdGeneratorPort; /** * Unit of Work configuration. A no-op UOW is installed by default. */ transaction?: CreateTestPortsTransactionOptions<Ports, unknown>; /** * Additional app-specific context fields. An explicit `gate` here wins over * the kit-attached gate. */ extra?: Partial<Ctx>; /** * Enter the ambient request context with the test actor, tenant, request * ID, and trace ID so ambient enrichment (such as the default audit port) * works like production. Call `dispose()` (or use `using`) to clear it. * * @default true */ ambient?: boolean; } /** * Test context fixture returned by `createTestContext(...)`. */ export interface TestContextFixture<Ctx, Ports extends AnyPorts> extends TestPortsFixture<Ports, unknown> { /** * Assembled app context under test. */ ctx: Ctx; /** * Clear the ambient request context entered by this fixture. */ dispose(): void; /** * Explicit-resource-management alias for `dispose()`, so fixtures work with * `using`. */ [disposeSymbol](): void; } /** * Create a one-call test context fixture for jobs, listeners, schedules, * notifications, payments, tasks, and use-case tests. * * The fixture builds common memory ports through `createTestPorts(...)`, * assembles an app context with actor, tenant, request ID, trace ID, auth, * and a live bound gate, and enters the ambient request context so ambient * enrichment matches production. Reading an app port that is neither a kit * default nor supplied throws a named error on use. * * @example * ```ts * const makeContext = createTestContext<AppContext>(); * * using fixture = makeContext({ * ports: { issues: { create: async (input) => issueRecord(input) } }, * }); * await runCreateIssue(fixture.ctx); * ``` * * @returns A factory that creates one disposable test context fixture. */ export declare function createTestContext<Ctx extends { ports: AnyPorts; }>(): (options?: CreateTestContextOptions<Ctx, Ctx["ports"]>) => TestContextFixture<Ctx, Ctx["ports"]>; /** * Options for `installProviderForTest(...)`. */ export interface InstallProviderForTestOptions { /** * Environment variables used to resolve the provider's config through the * same loader the server uses (including the provider's `overrides`). * Ignored when `config` is passed explicitly. */ env?: Record<string, string | undefined>; /** * Base app ports visible to provider setup, such as `{ devtools }`. */ ports?: Record<string, unknown>; /** * Config passed to provider setup as-is. The provider config schema is not * run, matching server startup where config is validated before setup. */ config?: unknown; /** * Service-context factory passed to setup and lifecycle hooks. The default * factory rejects, mirroring the late-bound runtime factory before all * providers have started. */ createServiceContext?: (input?: unknown) => Promise<unknown>; } /** * Installed provider fixture returned by `installProviderForTest(...)`. */ export interface InstalledProviderFixture { /** * Base ports merged with the ports contributed by provider setup. */ ports: Record<string, unknown>; /** * Raw provider setup result, exposed for assertions on contributed ports * and optional lifecycle hooks. */ result: ProviderSetupResult<Record<string, unknown>, Record<string, unknown>>; /** * Run the provider `start` hook when the provider declares one. */ start(): Promise<void>; /** * Run the provider `stop` hook when the provider declares one. */ stop(): Promise<void>; } /** * Run provider setup against test ports and return the merged ports plus * lifecycle runners. * * Use this in provider tests instead of hand-rolling the setup, port merge, * and lifecycle plumbing around `provider.setup(...)`. * * @param provider - Provider under test. * @param options - Base ports, raw config, and service-context factory. * @returns Merged ports, the raw setup result, and start/stop runners. */ export declare function installProviderForTest(provider: AnyServiceProvider, options?: InstallProviderForTestOptions): Promise<InstalledProviderFixture>; /** * Arguments passed to a factory default builder. */ export interface FactoryBuildArgs<Name extends string = string> { /** * Factory name. */ readonly name: Name; /** * Current sequence value. */ readonly sequence: number; /** * ID generator available to the factory. */ readonly ids: IdGeneratorPort; /** * Clock available to the factory. */ readonly clock: ClockPort; } /** * Arguments passed to a factory persistence function. */ export type FactoryPersistArgs<Name extends string = string> = FactoryBuildArgs<Name>; /** * Overrides accepted by factory build and create methods. */ export type FactoryOverrides<Value extends object, Name extends string = string> = Partial<Value> | ((value: Value, args: FactoryBuildArgs<Name>) => Partial<Value>); /** * Options for declaring a test data factory. */ export interface CreateFactoryOptions<Name extends string, Value extends object, Ctx = unknown, Created = Value> { /** * Build the default value for the current sequence. */ defaults(args: FactoryBuildArgs<Name>): Value; /** * Persist a built value and return the created record. */ persist?(ctx: Ctx, value: Value, args: FactoryPersistArgs<Name>): MaybePromise<Created>; /** * ID generator to expose to the factory. */ ids?: IdGeneratorPort; /** * Clock to expose to the factory. */ clock?: ClockPort; /** * Initial sequence number. Defaults to `1`. */ start?: number; } /** * Declared test data factory. */ export interface FactoryDef<Name extends string = string, Value extends object = object, Ctx = unknown, Created = Value> { /** * Discriminator for factory definitions. */ readonly kind: "factory"; /** * Factory name. */ readonly name: Name; /** * Build an in-memory value. */ build(overrides?: FactoryOverrides<Value, Name>): Value; /** * Build multiple in-memory values. */ buildList(count: number, overrides?: FactoryOverrides<Value, Name>): Value[]; /** * Build and persist a value. */ create(ctx: Ctx, overrides?: FactoryOverrides<Value, Name>): Promise<Created>; /** * Build and persist multiple values. */ createList(ctx: Ctx, count: number, overrides?: FactoryOverrides<Value, Name>): Promise<Created[]>; /** * Reset the factory sequence. */ resetSequence(next?: number): void; } /** * Options for declaring a seed. */ export interface DefineSeedOptions<Ctx> { /** * Optional human-readable seed description. */ description?: string; /** * Execute the seed. */ run(ctx: Ctx): MaybePromise<void>; } /** * Declared seed that can be run with `runSeeds(...)`. */ export interface SeedDef<Ctx = unknown, Name extends string = string> { /** * Discriminator for seed definitions. */ readonly kind: "seed"; /** * Seed name. */ readonly name: Name; /** * Optional human-readable seed description. */ readonly description?: string; /** * Execute the seed. */ run(ctx: Ctx): MaybePromise<void>; } /** * Options for running a list of seeds. */ export interface RunSeedsOptions<Ctx> { /** * Context passed to every seed. */ ctx: Ctx; /** * Seeds to run in order. */ seeds: readonly SeedDef<Ctx>[]; } /** * Options for creating a database test harness. */ export interface CreateDatabaseTestHarnessOptions<Database, Ctx> { /** * Create an isolated app-owned database fixture. */ create(): MaybePromise<Database>; /** * Build the context passed to factories and seeds from the database fixture. */ ctx(database: Database): Ctx; /** * Reset the database fixture, when supported by the app. */ reset?(database: Database): MaybePromise<void>; /** * Close and dispose the database fixture. */ close?(database: Database): MaybePromise<void>; /** * Factories whose sequences should reset before each setup and reset. */ factories?: readonly Pick<FactoryDef, "resetSequence">[]; /** * Default seeds available to `setup({ seed: true })` and `session.runSeeds()`. */ seeds?: readonly SeedDef<Ctx>[]; } /** * Options for setting up one database test session. */ export interface DatabaseTestSetupOptions<Ctx> { /** * Run seeds after creating the database session. * * Pass `true` to use the harness default seeds, or pass an explicit seed * list for this setup. */ seed?: boolean | readonly SeedDef<Ctx>[]; } /** * Active database fixture created by `createDatabaseTestHarness(...)`. */ export interface DatabaseTestSession<Database, Ctx> { /** * App-owned database fixture returned by the harness `create` function. */ readonly database: Database; /** * Context built from the database fixture for factories and seeds. */ readonly ctx: Ctx; /** * Run the default harness seeds or an explicit seed list. */ runSeeds(seeds?: readonly SeedDef<Ctx>[]): Promise<void>; /** * Reset factory sequences and the database fixture. */ reset(): Promise<void>; /** * Close and dispose this session. */ close(): Promise<void>; } /** * Database test harness that coordinates app-owned database fixtures with * Beignet factories and seeds. */ export interface DatabaseTestHarness<Database, Ctx> { /** * Create one isolated database test session. */ setup(options?: DatabaseTestSetupOptions<Ctx>): Promise<DatabaseTestSession<Database, Ctx>>; /** * Reset all configured factory sequences. */ resetFactories(): void; /** * Close all sessions that have not already been closed. */ cleanup(): Promise<void>; } /** * Error thrown when a seed fails. */ export declare class SeedRunError extends Error { /** * Seed that failed. */ readonly seed: SeedDef; /** * Original thrown value. */ readonly cause: unknown; constructor(seed: SeedDef, cause: unknown); } /** * Reset one or more factory sequences to their configured starting values. */ export declare function resetFactories(...factories: readonly Pick<FactoryDef, "resetSequence">[]): void; /** * Create a typed test data factory with deterministic sequence support. */ export declare function createFactory<const Name extends string, Value extends object, Ctx = unknown, Created = Value>(name: Name, options: CreateFactoryOptions<Name, Value, Ctx, Created>): FactoryDef<Name, Value, Ctx, Created>; /** * Define a named seed. */ export declare function defineSeed<const Name extends string, Ctx = unknown>(name: Name, options: DefineSeedOptions<Ctx>): SeedDef<Ctx, Name>; /** * Run seeds in order and wrap failures with the seed name. */ export declare function runSeeds<Ctx>(options: RunSeedsOptions<Ctx>): Promise<void>; /** * Create a database test harness for app-owned database fixtures. * * The harness does not know about an ORM or database provider. Apps provide * creation, reset, close, and context mapping functions, while Beignet handles * the repetitive test lifecycle around factory sequences and seed execution. */ export declare function createDatabaseTestHarness<Database, Ctx>(options: CreateDatabaseTestHarnessOptions<Database, Ctx>): DatabaseTestHarness<Database, Ctx>; //# sourceMappingURL=index.d.ts.map