@beignet/core
Version:
Core framework primitives for Beignet
1,211 lines • 44.2 kB
JavaScript
import { prepareEventPayloadForTransport } from "../events/index.js";
import { createAnonymousActor, createServiceActor, createSystemActor, createTenant, createUserActor, } from "./audit.js";
import { createGate, } from "./policy.js";
/**
* Create a recording event bus for testing.
*
* This bus validates canonical transport output and records published events
* asynchronously for later assertion. Await `publish(...)` before reading the
* captured log. Subscription is not supported and throws when called.
*
* @example
* ```ts
* const { bus, events } = createRecordingEventBus();
*
* // Inject bus into your use case
* await createUser({ ports: { eventBus: bus } });
*
* // Assert on recorded events
* expect(events).toHaveLength(1);
* expect(events[0].name).toBe("user.registered");
* expect(events[0].payload).toEqual({ userId: "123", email: "test@example.com" });
* ```
*/
export function createRecordingEventBus() {
const events = [];
const bus = {
async publish(event, payload, options) {
const prepared = await prepareEventPayloadForTransport(event, payload, options);
events.push({ name: event.name, payload: prepared.payload });
},
subscribe() {
throw new Error("Not implemented for recording bus");
},
};
return { bus, events };
}
/**
* Recording best-effort-work adapter for deterministic tests.
*
* Calls to `defer(...)` append work without running it. `flush()` runs the
* currently pending batch in FIFO order, then rejects with the callback error
* or an `AggregateError` after every callback in that batch has been attempted.
* Work deferred by a running callback remains pending for the next flush so
* one flush stays bounded.
*/
export function createRecordingBestEffortWork() {
const pending = [];
return {
bestEffortWork: {
defer(work) {
pending.push(work);
},
},
pending,
async flush() {
const batch = pending.splice(0);
const errors = [];
for (const work of batch) {
try {
await work();
}
catch (error) {
errors.push(error);
}
}
if (errors.length === 1) {
throw errors[0];
}
if (errors.length > 1) {
throw new AggregateError(errors, "Best-effort work batch failed.");
}
},
};
}
/**
* Create a recording job dispatcher for tests.
*
* The dispatcher records dispatch intent without running the job handler. Use
* this when a use case or listener should enqueue work but the test does not
* need to execute that work inline.
*
* @returns A job dispatcher plus its captured dispatches.
*/
export function createRecordingJobDispatcher() {
const dispatchedJobs = [];
const jobs = {
dispatch(job, payload) {
dispatchedJobs.push({ name: job.name, job, payload });
},
};
return { jobs, dispatchedJobs };
}
/**
* Create a recording schedule runner for tests.
*
* The runner records schedule run intent without executing the schedule
* handler. Use `createInlineScheduleRunner(...)` when the test should run the
* handler.
*
* @returns A schedule runner plus its captured runs.
*/
export function createRecordingScheduleRunner() {
const runs = [];
const runner = {
async run(schedule, options = {}) {
runs.push({
name: schedule.name,
schedule,
...(Object.hasOwn(options, "payload")
? { payload: options.payload }
: {}),
...(options.id !== undefined ? { id: options.id } : {}),
...(options.source !== undefined ? { source: options.source } : {}),
...(options.scheduledAt !== undefined
? { scheduledAt: options.scheduledAt }
: {}),
...(options.triggeredAt !== undefined
? { triggeredAt: options.triggeredAt }
: {}),
});
},
};
return { runner, runs };
}
/**
* Create a provider instrumentation port for tests.
*
* The port records every event and can optionally disable specific watchers.
* Use the returned `events` array with provider instrumentation assertion
* helpers.
*
* @returns A provider instrumentation port plus its captured events.
*/
export function createRecordingProviderInstrumentation(options = {}) {
const events = [];
const enabledWatchers = options.enabledWatchers
? new Set(options.enabledWatchers)
: null;
const disabledWatchers = new Set(options.disabledWatchers ?? []);
const instrumentation = {
record(event) {
events.push(event);
},
isWatcherEnabled(name) {
if (disabledWatchers.has(name))
return false;
return enabledWatchers ? enabledWatchers.has(name) : true;
},
};
return { instrumentation, events };
}
/**
* Create a predictable user actor for tests.
*
* Use this when authorization or audit assertions need a stable actor shape
* without repeating the `ActivityActor` object in every test.
*
* @param id - Stable user ID for the test actor.
* @param options - Optional display name, role, and metadata.
* @returns A user actor with `type: "user"`.
*/
export function createTestUserActor(id = "user_test", options = {}) {
const { role, metadata, ...actorOptions } = options;
const mergedMetadata = mergeRoleMetadata(metadata, role);
return createUserActor(id, {
...actorOptions,
...(mergedMetadata ? { metadata: mergedMetadata } : {}),
});
}
/**
* Create a predictable user actor for tests that exercise impersonation.
*
* The returned actor remains the effective user, with `metadata.impersonatorId`
* recording who initiated the impersonated access.
*
* @param id - Stable user ID being impersonated.
* @param options - Impersonator ID plus optional display name, role, and metadata.
* @returns A user actor with impersonation metadata.
*/
export function createTestImpersonatedUserActor(id, options) {
const { impersonatorId, ...actorOptions } = options;
return createTestUserActor(id, {
...actorOptions,
metadata: {
...actorOptions.metadata,
impersonatorId,
},
});
}
/**
* Create a predictable anonymous actor for tests.
*
* @param options - Optional display name or metadata.
* @returns An anonymous actor with `type: "anonymous"`.
*/
export function createTestAnonymousActor(options = {}) {
return createAnonymousActor(options);
}
/**
* Create a predictable service actor for tests.
*
* @param id - Stable service ID.
* @param options - Optional display name or metadata.
* @returns A service actor with `type: "service"`.
*/
export function createTestServiceActor(id = "service_test", options = {}) {
return createServiceActor(id, options);
}
/**
* Create a predictable system actor for tests.
*
* @param id - Stable system actor ID.
* @param options - Optional display name or metadata.
* @returns A system actor with `type: "system"`.
*/
export function createTestSystemActor(id = "system_test", options = {}) {
return createSystemActor(id, options);
}
/**
* Create a predictable tenant for tests.
*
* @param id - Stable tenant ID.
* @param options - Optional slug or metadata.
* @returns A tenant descriptor.
*/
export function createTestTenant(id = "tenant_test", options = {}) {
return createTenant(id, options);
}
/**
* Create the activity fields commonly copied onto app test contexts.
*
* @param options - Optional actor, tenant, request ID, and trace ID overrides.
* @returns Stable activity context fields for a test.
*/
export function createTestActivityContext(options = {}) {
return {
actor: options.actor ?? createTestUserActor(),
...(options.tenant === null
? {}
: { tenant: options.tenant ?? createTestTenant() }),
requestId: options.requestId ?? "test-request",
traceId: options.traceId ?? "test-trace",
};
}
/**
* Find the first audit entry matching the expected fields.
*
* @param entries - Audit entries captured by a memory or fake audit port.
* @param expectation - Partial audit fields to match.
* @returns The first matching entry, or `undefined`.
*/
export function findAuditEntry(entries, expectation) {
return entries.find((entry) => auditEntryMatches(entry, expectation));
}
/**
* Assert that an audit entry exists and return the matching entry.
*
* The helper throws a plain `Error`, so it works with Bun, Vitest, Jest, and
* other test runners.
*
* @param entries - Audit entries captured by a memory or fake audit port.
* @param expectation - Partial audit fields to match.
* @returns The matching audit entry.
* @throws Error when no entry matches.
*/
export function assertAuditEntry(entries, expectation) {
const entry = findAuditEntry(entries, expectation);
if (entry)
return entry;
throw new Error(`Expected audit entry matching ${formatAuditExpectation(expectation)}, but only found ${entries.length} entr${entries.length === 1 ? "y" : "ies"}.`);
}
/**
* Assert that no audit entry matches the expected fields.
*
* @param entries - Audit entries captured by a memory or fake audit port.
* @param expectation - Partial audit fields to reject.
* @throws Error when a matching entry exists.
*/
export function assertNoAuditEntry(entries, expectation) {
const entry = findAuditEntry(entries, expectation);
if (!entry)
return;
throw new Error(`Expected no audit entry matching ${formatAuditExpectation(expectation)}, but found ${entry.action}.`);
}
/**
* Find the first recorded event matching the expected fields.
*
* @param events - Events captured by `createRecordingEventBus(...)`.
* @param expectation - Partial event fields to match.
* @returns The first matching event, or `undefined`.
*/
export function findRecordedEvent(events, expectation) {
return events.find((event) => recordedEventMatches(event, expectation));
}
/**
* Assert that a recorded event exists and return the matching event.
*
* @param events - Events captured by `createRecordingEventBus(...)`.
* @param expectation - Partial event fields to match.
* @returns The matching event.
* @throws Error when no event matches.
*/
export function assertRecordedEvent(events, expectation) {
const event = findRecordedEvent(events, expectation);
if (event)
return event;
throw new Error(`Expected recorded event matching ${formatTestingExpectation(expectation)}, but only found ${events.length} ${pluralize("event", events.length)}.`);
}
/**
* Assert that no recorded event matches the expected fields.
*
* @param events - Events captured by `createRecordingEventBus(...)`.
* @param expectation - Partial event fields to reject.
* @throws Error when a matching event exists.
*/
export function assertNoRecordedEvent(events, expectation) {
const event = findRecordedEvent(events, expectation);
if (!event)
return;
throw new Error(`Expected no recorded event matching ${formatTestingExpectation(expectation)}, but found ${event.name}.`);
}
/**
* Find the first recorded job dispatch matching the expected fields.
*
* @param jobs - Job dispatches captured by `createRecordingJobDispatcher(...)`.
* @param expectation - Partial job fields to match.
* @returns The first matching dispatch, or `undefined`.
*/
export function findDispatchedJob(jobs, expectation) {
return jobs.find((job) => recordedJobMatches(job, expectation));
}
/**
* Assert that a job was dispatched and return the matching dispatch.
*
* @param jobs - Job dispatches captured by `createRecordingJobDispatcher(...)`.
* @param expectation - Partial job fields to match.
* @returns The matching dispatch.
* @throws Error when no dispatch matches.
*/
export function assertDispatchedJob(jobs, expectation) {
const job = findDispatchedJob(jobs, expectation);
if (job)
return job;
throw new Error(`Expected dispatched job matching ${formatTestingExpectation(expectation)}, but only found ${jobs.length} ${pluralize("job", jobs.length)}.`);
}
/**
* Assert that no job dispatch matches the expected fields.
*
* @param jobs - Job dispatches captured by `createRecordingJobDispatcher(...)`.
* @param expectation - Partial job fields to reject.
* @throws Error when a matching dispatch exists.
*/
export function assertNoDispatchedJob(jobs, expectation) {
const job = findDispatchedJob(jobs, expectation);
if (!job)
return;
throw new Error(`Expected no dispatched job matching ${formatTestingExpectation(expectation)}, but found ${job.name}.`);
}
/**
* Find the first recorded schedule run matching the expected fields.
*
* @param runs - Runs captured by `createRecordingScheduleRunner(...)`.
* @param expectation - Partial schedule fields to match.
* @returns The first matching schedule run, or `undefined`.
*/
export function findScheduleRun(runs, expectation) {
return runs.find((run) => recordedScheduleRunMatches(run, expectation));
}
/**
* Find the first provider instrumentation event matching the expected fields.
*
* @param source - Recording instrumentation result or event snapshot array.
* @param expectation - Partial event fields to match.
* @returns The first matching event, or `undefined`.
*/
export function findProviderInstrumentationEvent(source, expectation) {
return getProviderInstrumentationEvents(source).find((event) => providerInstrumentationEventMatches(event, expectation));
}
/**
* Assert that a provider instrumentation event exists and return it.
*
* @param source - Recording instrumentation result or event snapshot array.
* @param expectation - Partial event fields to match.
* @returns The matching event.
* @throws Error when no event matches.
*/
export function assertProviderInstrumentationEvent(source, expectation) {
const event = findProviderInstrumentationEvent(source, expectation);
if (event)
return event;
const events = getProviderInstrumentationEvents(source);
throw new Error(`Expected provider instrumentation event matching ${formatTestingExpectation(expectation)}, but only found ${events.length} ${pluralize("event", events.length)}.`);
}
/**
* Assert that no provider instrumentation event matches the expected fields.
*
* @param source - Recording instrumentation result or event snapshot array.
* @param expectation - Partial event fields to reject.
* @throws Error when a matching event exists.
*/
export function assertNoProviderInstrumentationEvent(source, expectation) {
const event = findProviderInstrumentationEvent(source, expectation);
if (!event)
return;
throw new Error(`Expected no provider instrumentation event matching ${formatTestingExpectation(expectation)}, but found ${event.type}.`);
}
/**
* Assert that a schedule run was recorded and return the matching run.
*
* @param runs - Runs captured by `createRecordingScheduleRunner(...)`.
* @param expectation - Partial schedule fields to match.
* @returns The matching schedule run.
* @throws Error when no run matches.
*/
export function assertScheduleRun(runs, expectation) {
const run = findScheduleRun(runs, expectation);
if (run)
return run;
throw new Error(`Expected schedule run matching ${formatTestingExpectation(expectation)}, but only found ${runs.length} ${pluralize("run", runs.length)}.`);
}
/**
* Assert that no schedule run matches the expected fields.
*
* @param runs - Runs captured by `createRecordingScheduleRunner(...)`.
* @param expectation - Partial schedule fields to reject.
* @throws Error when a matching run exists.
*/
export function assertNoScheduleRun(runs, expectation) {
const run = findScheduleRun(runs, expectation);
if (!run)
return;
throw new Error(`Expected no schedule run matching ${formatTestingExpectation(expectation)}, but found ${run.name}.`);
}
/**
* Find the first mail delivery matching the expected fields.
*
* @param deliveries - Deliveries captured by `createMemoryMailer(...)`.
* @param expectation - Partial delivery or message fields to match.
* @returns The first matching delivery, or `undefined`.
*/
export function findMailDelivery(deliveries, expectation) {
return deliveries.find((delivery) => mailDeliveryMatches(delivery, expectation));
}
/**
* Assert that a mail delivery exists and return the matching delivery.
*
* @param deliveries - Deliveries captured by `createMemoryMailer(...)`.
* @param expectation - Partial delivery or message fields to match.
* @returns The matching delivery.
* @throws Error when no delivery matches.
*/
export function assertMailDelivery(deliveries, expectation) {
const delivery = findMailDelivery(deliveries, expectation);
if (delivery)
return delivery;
throw new Error(`Expected mail delivery matching ${formatTestingExpectation(expectation)}, but only found ${deliveries.length} ${pluralize("delivery", deliveries.length)}.`);
}
/**
* Assert that no mail delivery matches the expected fields.
*
* @param deliveries - Deliveries captured by `createMemoryMailer(...)`.
* @param expectation - Partial delivery or message fields to reject.
* @throws Error when a matching delivery exists.
*/
export function assertNoMailDelivery(deliveries, expectation) {
const delivery = findMailDelivery(deliveries, expectation);
if (!delivery)
return;
throw new Error(`Expected no mail delivery matching ${formatTestingExpectation(expectation)}, but found ${delivery.message.subject}.`);
}
/**
* Find the first notification delivery matching the expected fields.
*
* @param deliveries - Deliveries captured by `createMemoryNotificationPort(...)`.
* @param expectation - Partial delivery fields to match.
* @returns The first matching delivery, or `undefined`.
*/
export function findNotificationDelivery(deliveries, expectation) {
return deliveries.find((delivery) => notificationDeliveryMatches(delivery, expectation));
}
/**
* Assert that a notification delivery exists and return the matching delivery.
*
* @param deliveries - Deliveries captured by `createMemoryNotificationPort(...)`.
* @param expectation - Partial delivery fields to match.
* @returns The matching delivery.
* @throws Error when no delivery matches.
*/
export function assertNotificationDelivery(deliveries, expectation) {
const delivery = findNotificationDelivery(deliveries, expectation);
if (delivery)
return delivery;
throw new Error(`Expected notification delivery matching ${formatTestingExpectation(expectation)}, but only found ${deliveries.length} ${pluralize("delivery", deliveries.length)}.`);
}
/**
* Assert that no notification delivery matches the expected fields.
*
* @param deliveries - Deliveries captured by `createMemoryNotificationPort(...)`.
* @param expectation - Partial delivery fields to reject.
* @throws Error when a matching delivery exists.
*/
export function assertNoNotificationDelivery(deliveries, expectation) {
const delivery = findNotificationDelivery(deliveries, expectation);
if (!delivery)
return;
throw new Error(`Expected no notification delivery matching ${formatTestingExpectation(expectation)}, but found ${delivery.notificationName}.`);
}
/**
* Assert that a storage object exists and optionally matches metadata/body
* expectations.
*
* This helper works against any `StoragePort`, not only memory storage.
*
* @param storage - Storage port under test.
* @param expectation - Object key and expected fields.
* @returns The matching object metadata.
* @throws Error when the object is missing or does not match.
*/
export async function assertStorageObject(storage, expectation) {
if (expectation.text !== undefined && expectation.bytes !== undefined) {
throw new Error(`Expected storage object "${expectation.key}" to check either text or bytes, not both.`);
}
const object = expectation.text !== undefined || expectation.bytes !== undefined
? await storage.get(expectation.key)
: await storage.stat(expectation.key);
if (!object) {
throw new Error(`Expected storage object "${expectation.key}" to exist.`);
}
if (!storageObjectMatches(object, expectation)) {
throw new Error(`Expected storage object "${expectation.key}" to match ${formatTestingExpectation(omitStorageBodyExpectation(expectation))}.`);
}
if (expectation.text !== undefined) {
const actualText = hasStorageTextReader(object)
? await object.text()
: undefined;
if (actualText !== expectation.text) {
throw new Error(`Expected storage object "${expectation.key}" text to match ${JSON.stringify(expectation.text)}.`);
}
}
else if (expectation.bytes !== undefined) {
const actualBytes = hasStorageBytesReader(object)
? await object.bytes()
: undefined;
if (!uint8ArrayMatches(actualBytes, expectation.bytes)) {
throw new Error(`Expected storage object "${expectation.key}" bytes to match.`);
}
}
return object;
}
/**
* Assert that a storage object does not exist.
*
* @param storage - Storage port under test.
* @param key - Object key expected to be absent.
* @throws Error when the object exists.
*/
export async function assertNoStorageObject(storage, key) {
const exists = await storage.exists(key);
if (!exists)
return;
throw new Error(`Expected storage object "${key}" not to exist.`);
}
/**
* Find the first outbox message matching expected fields.
*
* @param source - Memory outbox or message snapshot array.
* @param expectation - Partial message fields to match.
* @returns The first matching message, or `undefined`.
*/
export function findOutboxMessage(source, expectation) {
return getOutboxMessages(source).find((message) => outboxMessageMatches(message, expectation));
}
/**
* Find the first idempotency entry matching expected fields.
*
* @param source - Memory idempotency store or entry snapshot array.
* @param expectation - Partial entry fields to match.
* @returns The first matching entry, or `undefined`.
*/
export function findIdempotencyEntry(source, expectation) {
return getIdempotencyEntries(source).find((entry) => idempotencyEntryMatches(entry, expectation));
}
/**
* Assert that an idempotency entry exists and return it.
*
* @param source - Memory idempotency store or entry snapshot array.
* @param expectation - Partial entry fields to match.
* @returns The matching entry.
* @throws Error when no entry matches.
*/
export function assertIdempotencyEntry(source, expectation) {
const entry = findIdempotencyEntry(source, expectation);
if (entry)
return entry;
const entries = getIdempotencyEntries(source);
throw new Error(`Expected idempotency entry matching ${formatTestingExpectation(expectation)}, but only found ${entries.length} ${pluralize("entry", entries.length)}.`);
}
/**
* Assert that no idempotency entry matches expected fields.
*
* @param source - Memory idempotency store or entry snapshot array.
* @param expectation - Partial entry fields to reject.
* @throws Error when a matching entry exists.
*/
export function assertNoIdempotencyEntry(source, expectation) {
const entry = findIdempotencyEntry(source, expectation);
if (!entry)
return;
throw new Error(`Expected no idempotency entry matching ${formatTestingExpectation(expectation)}, but found ${entry.namespace}:${entry.key}.`);
}
/**
* Assert that a matching idempotency entry is still in progress.
*
* @param source - Memory idempotency store or entry snapshot array.
* @param expectation - Entry fields to match.
* @returns The matching in-progress entry.
*/
export function assertIdempotencyInProgress(source, expectation = {}) {
return assertIdempotencyEntry(source, {
...expectation,
status: "in-progress",
});
}
/**
* Assert that a matching idempotency entry completed.
*
* @param source - Memory idempotency store or entry snapshot array.
* @param expectation - Entry fields to match.
* @returns The matching completed entry.
*/
export function assertIdempotencyCompleted(source, expectation = {}) {
return assertIdempotencyEntry(source, {
...expectation,
status: "completed",
});
}
/**
* Assert that an outbox message exists and return it.
*
* @param source - Memory outbox or message snapshot array.
* @param expectation - Partial message fields to match.
* @returns The matching message.
* @throws Error when no message matches.
*/
export function assertOutboxMessage(source, expectation) {
const message = findOutboxMessage(source, expectation);
if (message)
return message;
const messages = getOutboxMessages(source);
throw new Error(`Expected outbox message matching ${formatTestingExpectation(expectation)}, but only found ${messages.length} ${pluralize("message", messages.length)}.`);
}
/**
* Assert that no outbox message matches expected fields.
*
* @param source - Memory outbox or message snapshot array.
* @param expectation - Partial message fields to reject.
* @throws Error when a matching message exists.
*/
export function assertNoOutboxMessage(source, expectation) {
const message = findOutboxMessage(source, expectation);
if (!message)
return;
throw new Error(`Expected no outbox message matching ${formatTestingExpectation(expectation)}, but found ${message.kind} "${message.name}".`);
}
/**
* Assert that an outbox message is pending.
*
* @param source - Memory outbox or message snapshot array.
* @param expectation - Message fields to match.
* @returns The matching pending message.
*/
export function assertOutboxPending(source, expectation = {}) {
return assertOutboxMessage(source, {
...expectation,
status: "pending",
});
}
/**
* Assert that an outbox message was delivered.
*
* @param source - Memory outbox or message snapshot array.
* @param expectation - Message fields to match.
* @returns The matching delivered message.
*/
export function assertOutboxDelivered(source, expectation = {}) {
return assertOutboxMessage(source, {
...expectation,
status: "delivered",
});
}
/**
* Assert that an outbox message is pending after at least one failed attempt.
*
* Use this for retry-scheduled assertions after `drainOutbox(...)` returns a
* retried count.
*
* @param source - Memory outbox or message snapshot array.
* @param expectation - Message fields to match.
* @returns The matching retry-scheduled message.
*/
export function assertOutboxRetryScheduled(source, expectation = {}) {
const message = assertOutboxMessage(source, {
...expectation,
status: "pending",
});
if (message.attempts < 1 || !message.lastError) {
throw new Error(`Expected outbox message "${message.id}" to have a recorded failed attempt.`);
}
return message;
}
/**
* Assert that an outbox message was dead-lettered.
*
* @param source - Memory outbox or message snapshot array.
* @param expectation - Message fields to match.
* @returns The matching dead-lettered message.
*/
export function assertOutboxDeadLettered(source, expectation = {}) {
return assertOutboxMessage(source, {
...expectation,
status: "deadLettered",
});
}
/**
* Assert that a drain result matches expected counts.
*
* @param result - Result returned by `drainOutbox(...)`.
* @param expectation - Partial count expectation.
* @throws Error when any supplied count differs.
*/
export function assertOutboxDrainResult(result, expectation) {
if (!outboxDrainResultMatches(result, expectation)) {
throw new Error(`Expected outbox drain result matching ${formatTestingExpectation(expectation)}, but received ${formatTestingExpectation(result)}.`);
}
}
/**
* Create a policy tester from the same options used by `createGate(...)`.
*
* Use this for table-driven authorization tests that document who can perform
* each ability against which subject.
*
* @param options - Policy definitions and optional denial mapper.
* @returns A policy tester with a gate plus matrix helpers.
*/
export function createPolicyTester(options) {
const gate = createGate(options);
return {
gate,
evaluateMatrix: (cases) => evaluatePolicyMatrix(gate, cases),
assertMatrix: (cases) => assertPolicyMatrix(gate, cases),
};
}
/**
* Evaluate a table of policy cases without throwing.
*
* @param gate - Gate under test.
* @param cases - Matrix cases to evaluate.
* @returns Structured result for each case.
*/
export async function evaluatePolicyMatrix(gate, cases) {
const results = [];
for (const matrixCase of cases) {
const decision = await inspectPolicyMatrixCase(gate, matrixCase);
const passed = policyMatrixCasePassed(matrixCase, decision);
results.push({
case: matrixCase,
decision,
passed,
message: passed
? undefined
: policyMatrixFailureMessage(matrixCase, decision),
});
}
return results;
}
/**
* Assert that all policy matrix cases pass.
*
* @param gate - Gate under test.
* @param cases - Matrix cases to evaluate.
* @throws Combined error listing every failed case.
*/
export async function assertPolicyMatrix(gate, cases) {
const results = await evaluatePolicyMatrix(gate, cases);
const failures = results.filter((result) => !result.passed);
if (failures.length === 0)
return;
throw new Error([
`Policy matrix failed for ${failures.length} case${failures.length === 1 ? "" : "s"}.`,
...failures.map((failure) => `- ${failure.message}`),
].join("\n"));
}
async function inspectPolicyMatrixCase(gate, matrixCase) {
const inspect = gate.inspect;
if ("subject" in matrixCase) {
return inspect(matrixCase.ctx, matrixCase.ability, matrixCase.subject);
}
return inspect(matrixCase.ctx, matrixCase.ability);
}
function policyMatrixCasePassed(matrixCase, decision) {
if (matrixCase.expected === "allow" && !decision.allowed)
return false;
if (matrixCase.expected === "deny" && decision.allowed)
return false;
if (matrixCase.reason && decision.allowed)
return false;
if (matrixCase.code && decision.allowed)
return false;
if (decision.allowed)
return true;
return ((!matrixCase.reason || decision.reason === matrixCase.reason) &&
(!matrixCase.code || decision.code === matrixCase.code));
}
function policyMatrixFailureMessage(matrixCase, decision) {
const expected = [matrixCase.expected];
if (matrixCase.reason)
expected.push(`reason "${matrixCase.reason}"`);
if (matrixCase.code)
expected.push(`code "${matrixCase.code}"`);
const actual = decision.allowed
? "allow"
: [
"deny",
decision.reason ? `reason "${decision.reason}"` : undefined,
decision.code ? `code "${decision.code}"` : undefined,
]
.filter(Boolean)
.join(", ");
return `${matrixCase.name}: expected ${expected.join(", ")}, received ${actual}.`;
}
function mergeRoleMetadata(metadata, role) {
if (!role)
return metadata;
return {
...metadata,
role,
};
}
function auditEntryMatches(entry, expectation) {
if (expectation.action !== undefined && entry.action !== expectation.action) {
return false;
}
if (expectation.outcome !== undefined &&
entry.outcome !== expectation.outcome) {
return false;
}
if (expectation.requestId !== undefined &&
entry.requestId !== expectation.requestId) {
return false;
}
if (expectation.traceId !== undefined &&
entry.traceId !== expectation.traceId) {
return false;
}
if (expectation.actorId !== undefined &&
entry.actor.id !== expectation.actorId) {
return false;
}
if (expectation.actorType !== undefined &&
entry.actor.type !== expectation.actorType) {
return false;
}
if (expectation.tenantId !== undefined &&
entry.tenant?.id !== expectation.tenantId) {
return false;
}
if (expectation.resourceId !== undefined &&
entry.resource?.id !== expectation.resourceId) {
return false;
}
if (expectation.resourceType !== undefined &&
entry.resource?.type !== expectation.resourceType) {
return false;
}
if (expectation.severity !== undefined &&
entry.metadata?.severity !== expectation.severity) {
return false;
}
return (partialObjectMatches(entry.actor, expectation.actor) &&
partialObjectMatches(entry.tenant, expectation.tenant) &&
partialObjectMatches(entry.resource, expectation.resource) &&
partialObjectMatches(entry.metadata, expectation.metadata));
}
function recordedEventMatches(event, expectation) {
if (expectation.name !== undefined && event.name !== expectation.name) {
return false;
}
return expectation.payload === undefined
? true
: valueMatches(event.payload, expectation.payload);
}
function recordedJobMatches(job, expectation) {
if (expectation.name !== undefined && job.name !== expectation.name) {
return false;
}
return expectation.payload === undefined
? true
: valueMatches(job.payload, expectation.payload);
}
function recordedScheduleRunMatches(run, expectation) {
if (expectation.name !== undefined && run.name !== expectation.name) {
return false;
}
if (expectation.id !== undefined && run.id !== expectation.id) {
return false;
}
if (expectation.source !== undefined && run.source !== expectation.source) {
return false;
}
return expectation.payload === undefined
? true
: valueMatches(run.payload, expectation.payload);
}
function getProviderInstrumentationEvents(source) {
return "events" in source ? source.events : source;
}
function providerInstrumentationEventMatches(event, expectation) {
const eventRecord = event;
const expectationRecord = expectation;
const directKeys = [
"type",
"id",
"timestamp",
"requestId",
"traceId",
"spanId",
"parentSpanId",
"traceparent",
"watcher",
"method",
"path",
"contractName",
"status",
"durationMs",
"summary",
"message",
"stack",
"useCaseName",
"name",
"kind",
"phase",
"error",
"eventName",
"jobName",
"messageId",
"messageKind",
"messageName",
"scheduleName",
"cron",
"timezone",
"action",
"label",
];
for (const key of directKeys) {
if (Object.hasOwn(expectation, key) &&
!valueMatches(eventRecord[key], expectationRecord[key])) {
return false;
}
}
if (expectation.providerName !== undefined &&
providerNameFromInstrumentationEvent(event) !== expectation.providerName) {
return false;
}
return expectation.details === undefined
? true
: valueMatches(event.details, expectation.details);
}
function providerNameFromInstrumentationEvent(event) {
if (event.type === "provider")
return event.providerName;
if (!event.details || typeof event.details !== "object")
return undefined;
return event.details.providerName;
}
function mailDeliveryMatches(delivery, expectation) {
if (expectation.id !== undefined && delivery.id !== expectation.id) {
return false;
}
if (expectation.subject !== undefined &&
delivery.message.subject !== expectation.subject) {
return false;
}
if (expectation.to !== undefined &&
!valueMatches(delivery.message.to, expectation.to)) {
return false;
}
if (expectation.from !== undefined &&
!valueMatches(delivery.message.from, expectation.from)) {
return false;
}
if (expectation.text !== undefined &&
delivery.message.text !== expectation.text) {
return false;
}
if (expectation.html !== undefined &&
delivery.message.html !== expectation.html) {
return false;
}
return (partialObjectMatches(delivery.message.headers, expectation.headers) &&
partialObjectMatches(delivery.message, expectation.message));
}
function notificationDeliveryMatches(delivery, expectation) {
if (expectation.id !== undefined && delivery.id !== expectation.id) {
return false;
}
if (expectation.notificationName !== undefined &&
delivery.notificationName !== expectation.notificationName) {
return false;
}
if (expectation.channels !== undefined &&
!valueMatches(delivery.channels, expectation.channels)) {
return false;
}
if (expectation.payload !== undefined &&
!valueMatches(delivery.payload, expectation.payload)) {
return false;
}
return partialObjectMatches(delivery.metadata, expectation.metadata);
}
function storageObjectMatches(object, expectation) {
if (object.key !== expectation.key)
return false;
if (expectation.size !== undefined && object.size !== expectation.size) {
return false;
}
if (expectation.contentType !== undefined &&
object.contentType !== expectation.contentType) {
return false;
}
if (expectation.cacheControl !== undefined &&
object.cacheControl !== expectation.cacheControl) {
return false;
}
if (expectation.visibility !== undefined &&
object.visibility !== expectation.visibility) {
return false;
}
return partialObjectMatches(object.metadata, expectation.metadata);
}
function getOutboxMessages(source) {
return "messages" in source ? source.messages : source;
}
function getIdempotencyEntries(source) {
if (Array.isArray(source))
return source;
return source.entries;
}
function idempotencyEntryMatches(entry, expectation) {
if (expectation.namespace !== undefined &&
entry.namespace !== expectation.namespace) {
return false;
}
if (expectation.key !== undefined && entry.key !== expectation.key) {
return false;
}
if (expectation.scopeKey !== undefined &&
entry.scopeKey !== expectation.scopeKey) {
return false;
}
if (expectation.fingerprint !== undefined &&
entry.fingerprint !== expectation.fingerprint) {
return false;
}
if (expectation.status !== undefined && entry.status !== expectation.status) {
return false;
}
if (expectation.result !== undefined &&
!valueMatches(entry.result, expectation.result)) {
return false;
}
if (expectation.reservedAt !== undefined &&
!valueMatches(entry.reservedAt, expectation.reservedAt)) {
return false;
}
if (expectation.completedAt !== undefined &&
!valueMatches(entry.completedAt, expectation.completedAt)) {
return false;
}
if (expectation.expiresAt !== undefined &&
!valueMatches(entry.expiresAt, expectation.expiresAt)) {
return false;
}
return true;
}
function outboxMessageMatches(message, expectation) {
if (expectation.id !== undefined && message.id !== expectation.id) {
return false;
}
if (expectation.kind !== undefined && message.kind !== expectation.kind) {
return false;
}
if (expectation.name !== undefined && message.name !== expectation.name) {
return false;
}
if (expectation.status !== undefined &&
message.status !== expectation.status) {
return false;
}
if (expectation.attempts !== undefined &&
message.attempts !== expectation.attempts) {
return false;
}
if (expectation.maxAttempts !== undefined &&
message.maxAttempts !== expectation.maxAttempts) {
return false;
}
if (expectation.deliveredAt !== undefined &&
!valueMatches(message.deliveredAt, expectation.deliveredAt)) {
return false;
}
if (expectation.lastError !== undefined &&
!valueMatches(message.lastError, expectation.lastError)) {
return false;
}
return expectation.payload === undefined
? true
: valueMatches(message.payload, expectation.payload);
}
function outboxDrainResultMatches(result, expectation) {
if (expectation.claimed !== undefined &&
result.claimed !== expectation.claimed) {
return false;
}
if (expectation.delivered !== undefined &&
result.delivered !== expectation.delivered) {
return false;
}
if (expectation.retried !== undefined &&
result.retried !== expectation.retried) {
return false;
}
if (expectation.deadLettered !== undefined &&
result.deadLettered !== expectation.deadLettered) {
return false;
}
if (expectation.abandonedDeadLettered !== undefined &&
result.abandonedDeadLettered !== expectation.abandonedDeadLettered) {
return false;
}
if (expectation.settlementFailed !== undefined &&
result.settlementFailed !== expectation.settlementFailed) {
return false;
}
if (expectation.leaseLost !== undefined &&
result.leaseLost !== expectation.leaseLost) {
return false;
}
return true;
}
function hasStorageTextReader(object) {
return typeof object.text === "function";
}
function hasStorageBytesReader(object) {
return typeof object.bytes === "function";
}
function omitStorageBodyExpectation(expectation) {
const { bytes: _bytes, text: _text, ...metadataExpectation } = expectation;
return metadataExpectation;
}
function partialObjectMatches(actual, expected) {
if (!expected)
return true;
if (!actual || typeof actual !== "object")
return false;
const actualRecord = actual;
for (const [key, expectedValue] of Object.entries(expected)) {
if (!valueMatches(actualRecord[key], expectedValue))
return false;
}
return true;
}
function valueMatches(actual, expected) {
if (actual instanceof Date || expected instanceof Date) {
return (actual instanceof Date &&
expected instanceof Date &&
actual.getTime() === expected.getTime());
}
if (actual instanceof Uint8Array || expected instanceof Uint8Array) {
return uint8ArrayMatches(actual, expected);
}
if (expected &&
typeof expected === "object" &&
!Array.isArray(expected) &&
actual &&
typeof actual === "object" &&
!Array.isArray(actual)) {
return partialObjectMatches(actual, expected);
}
if (Array.isArray(expected) || Array.isArray(actual)) {
return JSON.stringify(actual) === JSON.stringify(expected);
}
return Object.is(actual, expected);
}
function uint8ArrayMatches(actual, expected) {
if (!(actual instanceof Uint8Array) || !(expected instanceof Uint8Array)) {
return false;
}
if (actual.byteLength !== expected.byteLength)
return false;
return actual.every((byte, index) => byte === expected[index]);
}
function pluralize(word, count) {
return count === 1 ? word : `${word}s`;
}
function formatTestingExpectation(expectation) {
return JSON.stringify(expectation);
}
function formatAuditExpectation(expectation) {
return formatTestingExpectation(expectation);
}
//# sourceMappingURL=testing.js.map