UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

1,158 lines 42.3 kB
import { createAnonymousActor, createServiceActor, createSystemActor, createTenant, createUserActor, } from "./audit.js"; import { createGate, } from "./policy.js"; /** * Create a recording event bus for testing. * * This bus records all published events for later assertion, * but does not support subscription (throws if 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 = { publish(event, payload) { events.push({ name: event.name, payload }); }, subscribe() { throw new Error("Not implemented for recording bus"); }, }; return { bus, events }; } /** * 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; } 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