@beignet/core 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +173 -0
- package/README.md +821 -30
- package/dist/application/index.d.ts +28 -2
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +140 -12
- package/dist/application/index.js.map +1 -1
- package/dist/client/client.d.ts +2 -2
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +136 -48
- package/dist/client/client.js.map +1 -1
- package/dist/client/error-messages.d.ts +14 -0
- package/dist/client/error-messages.d.ts.map +1 -0
- package/dist/client/error-messages.js +23 -0
- package/dist/client/error-messages.js.map +1 -0
- package/dist/client/index.d.ts +8 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +6 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +35 -5
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client-only.d.ts +8 -0
- package/dist/client-only.d.ts.map +1 -0
- package/dist/client-only.js +8 -0
- package/dist/client-only.js.map +1 -0
- package/dist/config/index.d.ts +5 -5
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/contracts/catalog-errors.d.ts +27 -0
- package/dist/contracts/catalog-errors.d.ts.map +1 -0
- package/dist/contracts/catalog-errors.js +69 -0
- package/dist/contracts/catalog-errors.js.map +1 -0
- package/dist/contracts/contract-builder.d.ts +15 -12
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +15 -41
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts +11 -8
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +13 -40
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/contract-like.d.ts +1 -1
- package/dist/contracts/contract-like.d.ts.map +1 -1
- package/dist/contracts/index.d.ts +13 -9
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +9 -5
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/openapi-meta.d.ts +48 -0
- package/dist/contracts/openapi-meta.d.ts.map +1 -1
- package/dist/contracts/openapi-meta.js +3 -0
- package/dist/contracts/openapi-meta.js.map +1 -1
- package/dist/contracts/path-template.d.ts +1 -1
- package/dist/contracts/path-template.js +2 -2
- package/dist/contracts/path-template.js.map +1 -1
- package/dist/contracts/schema-shape.d.ts +37 -0
- package/dist/contracts/schema-shape.d.ts.map +1 -0
- package/dist/contracts/schema-shape.js +61 -0
- package/dist/contracts/schema-shape.js.map +1 -0
- package/dist/contracts/success-status.d.ts +32 -0
- package/dist/contracts/success-status.d.ts.map +1 -0
- package/dist/contracts/success-status.js +18 -0
- package/dist/contracts/success-status.js.map +1 -0
- package/dist/contracts/types.d.ts +25 -5
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.js.map +1 -1
- package/dist/contracts/utils.d.ts +1 -1
- package/dist/contracts/utils.d.ts.map +1 -1
- package/dist/contracts/utils.js +1 -1
- package/dist/contracts/utils.js.map +1 -1
- package/dist/domain/events.d.ts +1 -1
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js +1 -1
- package/dist/domain/events.js.map +1 -1
- package/dist/domain/index.d.ts +3 -3
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +3 -3
- package/dist/domain/index.js.map +1 -1
- package/dist/errors/catalog.d.ts +9 -1
- package/dist/errors/catalog.d.ts.map +1 -1
- package/dist/errors/catalog.js +7 -1
- package/dist/errors/catalog.js.map +1 -1
- package/dist/errors/http.d.ts +10 -0
- package/dist/errors/http.d.ts.map +1 -1
- package/dist/errors/http.js +11 -1
- package/dist/errors/http.js.map +1 -1
- package/dist/errors/index.d.ts +4 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +4 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/errors/response.d.ts +4 -1
- package/dist/errors/response.d.ts.map +1 -1
- package/dist/errors/response.js.map +1 -1
- package/dist/events/index.d.ts +10 -12
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +10 -10
- package/dist/events/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +5 -3
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts +148 -16
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +174 -14
- package/dist/jobs/index.js.map +1 -1
- package/dist/notifications/index.d.ts +14 -16
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +14 -14
- package/dist/notifications/index.js.map +1 -1
- package/dist/openapi/index.d.ts +8 -3
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +41 -29
- package/dist/openapi/index.js.map +1 -1
- package/dist/openapi/schema-introspector.d.ts +37 -0
- package/dist/openapi/schema-introspector.d.ts.map +1 -1
- package/dist/openapi/schema-introspector.js +23 -17
- package/dist/openapi/schema-introspector.js.map +1 -1
- package/dist/outbox/index.d.ts +18 -4
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +104 -4
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/audit.d.ts +56 -10
- package/dist/ports/audit.d.ts.map +1 -1
- package/dist/ports/audit.js +71 -3
- package/dist/ports/audit.js.map +1 -1
- package/dist/ports/auth.d.ts +92 -0
- package/dist/ports/auth.d.ts.map +1 -1
- package/dist/ports/auth.js +92 -0
- package/dist/ports/auth.js.map +1 -1
- package/dist/ports/events.d.ts +2 -2
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/index.d.ts +62 -33
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +28 -34
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/policy.d.ts +32 -3
- package/dist/ports/policy.d.ts.map +1 -1
- package/dist/ports/policy.js +13 -2
- package/dist/ports/policy.js.map +1 -1
- package/dist/ports/testing.d.ts +1030 -2
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +1031 -1
- package/dist/ports/testing.js.map +1 -1
- package/dist/ports/unbound.d.ts +21 -0
- package/dist/ports/unbound.d.ts.map +1 -0
- package/dist/ports/unbound.js +57 -0
- package/dist/ports/unbound.js.map +1 -0
- package/dist/ports/unit-of-work.d.ts +1 -1
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +1 -1
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/providers/index.d.ts +3 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts +46 -5
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js +25 -6
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/providers/metadata.d.ts +39 -0
- package/dist/providers/metadata.d.ts.map +1 -0
- package/dist/providers/metadata.js +169 -0
- package/dist/providers/metadata.js.map +1 -0
- package/dist/providers/provider.d.ts +114 -9
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js +3 -20
- package/dist/providers/provider.js.map +1 -1
- package/dist/schedules/index.d.ts +94 -13
- package/dist/schedules/index.d.ts.map +1 -1
- package/dist/schedules/index.js +66 -12
- package/dist/schedules/index.js.map +1 -1
- package/dist/server/audit-context.d.ts +29 -0
- package/dist/server/audit-context.d.ts.map +1 -0
- package/dist/server/audit-context.js +44 -0
- package/dist/server/audit-context.js.map +1 -0
- package/dist/server/context.d.ts +141 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +39 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/contract-like.d.ts +1 -1
- package/dist/server/contract-like.d.ts.map +1 -1
- package/dist/server/contract-like.js +1 -1
- package/dist/server/contract-like.js.map +1 -1
- package/dist/server/health.d.ts +2 -2
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/hooks/auth.d.ts +89 -65
- package/dist/server/hooks/auth.d.ts.map +1 -1
- package/dist/server/hooks/auth.js +84 -55
- package/dist/server/hooks/auth.js.map +1 -1
- package/dist/server/hooks/cors.d.ts +1 -1
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/errors.d.ts +2 -2
- package/dist/server/hooks/errors.d.ts.map +1 -1
- package/dist/server/hooks/errors.js +2 -2
- package/dist/server/hooks/errors.js.map +1 -1
- package/dist/server/hooks/idempotency.d.ts +78 -0
- package/dist/server/hooks/idempotency.d.ts.map +1 -0
- package/dist/server/hooks/idempotency.js +154 -0
- package/dist/server/hooks/idempotency.js.map +1 -0
- package/dist/server/hooks/index.d.ts +8 -7
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js +6 -5
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/hooks/logging.d.ts +2 -2
- package/dist/server/hooks/logging.d.ts.map +1 -1
- package/dist/server/hooks/logging.js +1 -1
- package/dist/server/hooks/logging.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +25 -7
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +47 -12
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/hooks.d.ts +1 -1
- package/dist/server/hooks.d.ts.map +1 -1
- package/dist/server/hooks.js +1 -1
- package/dist/server/hooks.js.map +1 -1
- package/dist/server/http.d.ts +84 -6
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/index.d.ts +36 -12
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +24 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts +108 -0
- package/dist/server/instrumentation.d.ts.map +1 -0
- package/dist/server/instrumentation.js +297 -0
- package/dist/server/instrumentation.js.map +1 -0
- package/dist/server/openapi.d.ts +3 -3
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/openapi.js +1 -1
- package/dist/server/openapi.js.map +1 -1
- package/dist/server/providers/index.d.ts +3 -3
- package/dist/server/providers/index.d.ts.map +1 -1
- package/dist/server/providers/index.js +3 -3
- package/dist/server/providers/index.js.map +1 -1
- package/dist/server/providers/loadProviderConfig.d.ts +2 -2
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
- package/dist/server/providers/loadProviderConfig.js +2 -2
- package/dist/server/providers/loadProviderConfig.js.map +1 -1
- package/dist/server/request-context.d.ts +67 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/request-context.js +79 -0
- package/dist/server/request-context.js.map +1 -0
- package/dist/server/server-context.d.ts +38 -0
- package/dist/server/server-context.d.ts.map +1 -0
- package/dist/server/server-context.js +38 -0
- package/dist/server/server-context.js.map +1 -0
- package/dist/server/server.d.ts +148 -35
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +482 -145
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +2 -2
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -2
- package/dist/server/types.js.map +1 -1
- package/dist/server/use-case-route.d.ts +263 -0
- package/dist/server/use-case-route.d.ts.map +1 -0
- package/dist/server/use-case-route.js +77 -0
- package/dist/server/use-case-route.js.map +1 -0
- package/dist/server-only.d.ts +8 -0
- package/dist/server-only.d.ts.map +1 -0
- package/dist/server-only.js +8 -0
- package/dist/server-only.js.map +1 -0
- package/dist/tasks/index.d.ts +139 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +98 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/testing/index.d.ts +611 -5
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +434 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/index.d.ts +89 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +101 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/uploads/client.d.ts +278 -0
- package/dist/uploads/client.d.ts.map +1 -0
- package/dist/uploads/client.js +428 -0
- package/dist/uploads/client.js.map +1 -0
- package/dist/uploads/index.d.ts +361 -0
- package/dist/uploads/index.d.ts.map +1 -0
- package/dist/uploads/index.js +543 -0
- package/dist/uploads/index.js.map +1 -0
- package/package.json +34 -3
- package/src/application/index.ts +193 -10
- package/src/client/client.ts +148 -150
- package/src/client/error-messages.ts +35 -0
- package/src/client/index.ts +12 -4
- package/src/client/types.ts +44 -5
- package/src/client-only.ts +7 -0
- package/src/config/index.ts +6 -6
- package/src/contracts/catalog-errors.ts +115 -0
- package/src/contracts/contract-builder.ts +39 -76
- package/src/contracts/contract-group.ts +33 -68
- package/src/contracts/contract-like.ts +1 -1
- package/src/contracts/index.ts +24 -11
- package/src/contracts/openapi-meta.ts +55 -0
- package/src/contracts/path-template.ts +2 -2
- package/src/contracts/schema-shape.ts +75 -0
- package/src/contracts/success-status.ts +68 -0
- package/src/contracts/types.ts +32 -5
- package/src/contracts/utils.ts +5 -2
- package/src/domain/events.ts +6 -2
- package/src/domain/index.ts +3 -3
- package/src/errors/catalog.ts +9 -1
- package/src/errors/http.ts +11 -1
- package/src/errors/index.ts +4 -4
- package/src/errors/response.ts +4 -1
- package/src/events/index.ts +12 -26
- package/src/idempotency/index.ts +5 -3
- package/src/jobs/index.ts +340 -29
- package/src/notifications/index.ts +17 -27
- package/src/openapi/index.ts +73 -38
- package/src/openapi/schema-introspector.ts +68 -17
- package/src/outbox/index.ts +151 -6
- package/src/ports/audit.ts +120 -11
- package/src/ports/auth.ts +132 -0
- package/src/ports/events.ts +2 -2
- package/src/ports/index.ts +104 -35
- package/src/ports/policy.ts +50 -3
- package/src/ports/testing.ts +2220 -33
- package/src/ports/unbound.ts +64 -0
- package/src/ports/unit-of-work.ts +6 -2
- package/src/providers/index.ts +16 -3
- package/src/providers/instrumentation.ts +93 -8
- package/src/providers/metadata.ts +234 -0
- package/src/providers/provider.ts +168 -9
- package/src/schedules/index.ts +173 -23
- package/src/server/audit-context.ts +45 -0
- package/src/server/context.ts +224 -0
- package/src/server/contract-like.ts +1 -1
- package/src/server/health.ts +2 -2
- package/src/server/hooks/auth.ts +175 -158
- package/src/server/hooks/cors.ts +1 -1
- package/src/server/hooks/errors.ts +7 -4
- package/src/server/hooks/idempotency.ts +263 -0
- package/src/server/hooks/index.ts +15 -12
- package/src/server/hooks/logging.ts +3 -3
- package/src/server/hooks/rate-limit.ts +85 -17
- package/src/server/hooks.ts +1 -1
- package/src/server/http.ts +112 -6
- package/src/server/index.ts +63 -12
- package/src/server/instrumentation.ts +470 -0
- package/src/server/openapi.ts +4 -4
- package/src/server/providers/index.ts +6 -3
- package/src/server/providers/loadProviderConfig.ts +4 -4
- package/src/server/request-context.ts +116 -0
- package/src/server/server-context.ts +44 -0
- package/src/server/server.ts +1045 -229
- package/src/server/types.ts +2 -2
- package/src/server/use-case-route.ts +430 -0
- package/src/server-only.ts +7 -0
- package/src/tasks/index.ts +275 -0
- package/src/testing/index.ts +1153 -6
- package/src/tracing/index.ts +176 -0
- package/src/uploads/client.ts +861 -0
- package/src/uploads/index.ts +1071 -0
- package/dist/ports/mailer.d.ts +0 -6
- package/dist/ports/mailer.d.ts.map +0 -1
- package/dist/ports/mailer.js +0 -2
- package/dist/ports/mailer.js.map +0 -1
- package/dist/ports/schedules.d.ts +0 -9
- package/dist/ports/schedules.d.ts.map +0 -1
- package/dist/ports/schedules.js +0 -2
- package/dist/ports/schedules.js.map +0 -1
package/src/ports/testing.ts
CHANGED
|
@@ -1,4 +1,44 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
MemoryIdempotencyEntry,
|
|
3
|
+
MemoryIdempotencyStore,
|
|
4
|
+
} from "../idempotency/index.js";
|
|
5
|
+
import type {
|
|
6
|
+
MemoryMailDelivery,
|
|
7
|
+
NormalizedMailMessage,
|
|
8
|
+
} from "../mail/index.js";
|
|
9
|
+
import type { MemoryNotificationDelivery } from "../notifications/index.js";
|
|
10
|
+
import type {
|
|
11
|
+
DrainOutboxResult,
|
|
12
|
+
OutboxErrorInfo,
|
|
13
|
+
OutboxMessage,
|
|
14
|
+
OutboxMessageKind,
|
|
15
|
+
OutboxMessageStatus,
|
|
16
|
+
} from "../outbox/index.js";
|
|
17
|
+
import type {
|
|
18
|
+
ProviderInstrumentationEventInput,
|
|
19
|
+
ProviderInstrumentationPort,
|
|
20
|
+
} from "../providers/index.js";
|
|
21
|
+
import type {
|
|
22
|
+
InferSchedulePayload,
|
|
23
|
+
ScheduleDef,
|
|
24
|
+
ScheduleRunnerPort,
|
|
25
|
+
ScheduleRunOptions,
|
|
26
|
+
} from "../schedules/index.js";
|
|
27
|
+
import {
|
|
28
|
+
type ActivityActor,
|
|
29
|
+
type ActivityMetadata,
|
|
30
|
+
type ActivityMetadataValue,
|
|
31
|
+
type ActivityResource,
|
|
32
|
+
type ActivityTenant,
|
|
33
|
+
type AuditLogEntry,
|
|
34
|
+
type AuditOutcome,
|
|
35
|
+
createAnonymousActor,
|
|
36
|
+
createServiceActor,
|
|
37
|
+
createSystemActor,
|
|
38
|
+
createTenant,
|
|
39
|
+
createUserActor,
|
|
40
|
+
} from "./audit.js";
|
|
41
|
+
import type { EventBusPort, JobDef, JobDispatcherPort } from "./events.js";
|
|
2
42
|
import {
|
|
3
43
|
type CreateGateOptions,
|
|
4
44
|
createGate,
|
|
@@ -8,51 +48,1653 @@ import {
|
|
|
8
48
|
type PolicyDefinition,
|
|
9
49
|
type PolicyMapFromDefinitions,
|
|
10
50
|
type PolicySubjectArgs,
|
|
11
|
-
} from "./policy";
|
|
51
|
+
} from "./policy.js";
|
|
52
|
+
import type {
|
|
53
|
+
StorageObject,
|
|
54
|
+
StoragePort,
|
|
55
|
+
StorageVisibility,
|
|
56
|
+
} from "./storage.js";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A recorded event entry from the recording event bus.
|
|
60
|
+
*/
|
|
61
|
+
export interface RecordedEvent {
|
|
62
|
+
name: string;
|
|
63
|
+
payload: unknown;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Expected fields for a recorded event assertion.
|
|
68
|
+
*/
|
|
69
|
+
export interface RecordedEventExpectation {
|
|
70
|
+
/**
|
|
71
|
+
* Expected event name.
|
|
72
|
+
*/
|
|
73
|
+
name?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Expected event payload. Object values are matched as partial objects.
|
|
76
|
+
*/
|
|
77
|
+
payload?: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a recording event bus for testing.
|
|
82
|
+
*
|
|
83
|
+
* This bus records all published events for later assertion,
|
|
84
|
+
* but does not support subscription (throws if called).
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* const { bus, events } = createRecordingEventBus();
|
|
89
|
+
*
|
|
90
|
+
* // Inject bus into your use case
|
|
91
|
+
* await createUser({ ports: { eventBus: bus } });
|
|
92
|
+
*
|
|
93
|
+
* // Assert on recorded events
|
|
94
|
+
* expect(events).toHaveLength(1);
|
|
95
|
+
* expect(events[0].name).toBe("user.registered");
|
|
96
|
+
* expect(events[0].payload).toEqual({ userId: "123", email: "test@example.com" });
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export function createRecordingEventBus(): {
|
|
100
|
+
bus: EventBusPort;
|
|
101
|
+
events: RecordedEvent[];
|
|
102
|
+
} {
|
|
103
|
+
const events: RecordedEvent[] = [];
|
|
104
|
+
|
|
105
|
+
const bus: EventBusPort = {
|
|
106
|
+
publish(event, payload) {
|
|
107
|
+
events.push({ name: event.name, payload });
|
|
108
|
+
},
|
|
109
|
+
subscribe() {
|
|
110
|
+
throw new Error("Not implemented for recording bus");
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return { bus, events };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A job dispatch captured by `createRecordingJobDispatcher(...)`.
|
|
119
|
+
*/
|
|
120
|
+
export interface RecordedJobDispatch {
|
|
121
|
+
/**
|
|
122
|
+
* Dispatched job name.
|
|
123
|
+
*/
|
|
124
|
+
name: string;
|
|
125
|
+
/**
|
|
126
|
+
* Job definition supplied to the dispatcher.
|
|
127
|
+
*/
|
|
128
|
+
job: JobDef;
|
|
129
|
+
/**
|
|
130
|
+
* Payload supplied to the dispatcher.
|
|
131
|
+
*/
|
|
132
|
+
payload: unknown;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Expected fields for a recorded job dispatch assertion.
|
|
137
|
+
*/
|
|
138
|
+
export interface RecordedJobDispatchExpectation {
|
|
139
|
+
/**
|
|
140
|
+
* Expected job name.
|
|
141
|
+
*/
|
|
142
|
+
name?: string;
|
|
143
|
+
/**
|
|
144
|
+
* Expected job payload. Object values are matched as partial objects.
|
|
145
|
+
*/
|
|
146
|
+
payload?: unknown;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create a recording job dispatcher for tests.
|
|
151
|
+
*
|
|
152
|
+
* The dispatcher records dispatch intent without running the job handler. Use
|
|
153
|
+
* this when a use case or listener should enqueue work but the test does not
|
|
154
|
+
* need to execute that work inline.
|
|
155
|
+
*
|
|
156
|
+
* @returns A job dispatcher plus its captured dispatches.
|
|
157
|
+
*/
|
|
158
|
+
export function createRecordingJobDispatcher(): {
|
|
159
|
+
jobs: JobDispatcherPort;
|
|
160
|
+
dispatchedJobs: RecordedJobDispatch[];
|
|
161
|
+
} {
|
|
162
|
+
const dispatchedJobs: RecordedJobDispatch[] = [];
|
|
163
|
+
|
|
164
|
+
const jobs: JobDispatcherPort = {
|
|
165
|
+
dispatch(job, payload) {
|
|
166
|
+
dispatchedJobs.push({ name: job.name, job, payload });
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return { jobs, dispatchedJobs };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A schedule run captured by `createRecordingScheduleRunner(...)`.
|
|
175
|
+
*/
|
|
176
|
+
export interface RecordedScheduleRun {
|
|
177
|
+
/**
|
|
178
|
+
* Schedule name.
|
|
179
|
+
*/
|
|
180
|
+
name: string;
|
|
181
|
+
/**
|
|
182
|
+
* Schedule definition supplied to the runner.
|
|
183
|
+
*/
|
|
184
|
+
schedule: ScheduleDef;
|
|
185
|
+
/**
|
|
186
|
+
* Payload supplied to the runner, when present.
|
|
187
|
+
*/
|
|
188
|
+
payload?: unknown;
|
|
189
|
+
/**
|
|
190
|
+
* Run ID supplied by the provider or test, when present.
|
|
191
|
+
*/
|
|
192
|
+
id?: string;
|
|
193
|
+
/**
|
|
194
|
+
* Provider or app source label, when present.
|
|
195
|
+
*/
|
|
196
|
+
source?: string;
|
|
197
|
+
/**
|
|
198
|
+
* Scheduled timestamp supplied to the runner, when present.
|
|
199
|
+
*/
|
|
200
|
+
scheduledAt?: ScheduleRunOptions["scheduledAt"];
|
|
201
|
+
/**
|
|
202
|
+
* Triggered timestamp supplied to the runner, when present.
|
|
203
|
+
*/
|
|
204
|
+
triggeredAt?: ScheduleRunOptions["triggeredAt"];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Expected fields for a recorded schedule run assertion.
|
|
209
|
+
*/
|
|
210
|
+
export interface RecordedScheduleRunExpectation {
|
|
211
|
+
/**
|
|
212
|
+
* Expected schedule name.
|
|
213
|
+
*/
|
|
214
|
+
name?: string;
|
|
215
|
+
/**
|
|
216
|
+
* Expected schedule payload. Object values are matched as partial objects.
|
|
217
|
+
*/
|
|
218
|
+
payload?: unknown;
|
|
219
|
+
/**
|
|
220
|
+
* Expected run ID.
|
|
221
|
+
*/
|
|
222
|
+
id?: string;
|
|
223
|
+
/**
|
|
224
|
+
* Expected source label.
|
|
225
|
+
*/
|
|
226
|
+
source?: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Expected fields for a provider instrumentation event assertion.
|
|
231
|
+
*/
|
|
232
|
+
export interface ProviderInstrumentationEventExpectation {
|
|
233
|
+
/**
|
|
234
|
+
* Expected instrumentation event type.
|
|
235
|
+
*/
|
|
236
|
+
type?: ProviderInstrumentationEventInput["type"];
|
|
237
|
+
/**
|
|
238
|
+
* Expected event ID.
|
|
239
|
+
*/
|
|
240
|
+
id?: string;
|
|
241
|
+
/**
|
|
242
|
+
* Expected ISO timestamp.
|
|
243
|
+
*/
|
|
244
|
+
timestamp?: string;
|
|
245
|
+
/**
|
|
246
|
+
* Expected request correlation ID.
|
|
247
|
+
*/
|
|
248
|
+
requestId?: string;
|
|
249
|
+
/**
|
|
250
|
+
* Expected trace ID.
|
|
251
|
+
*/
|
|
252
|
+
traceId?: string;
|
|
253
|
+
/**
|
|
254
|
+
* Expected span ID.
|
|
255
|
+
*/
|
|
256
|
+
spanId?: string;
|
|
257
|
+
/**
|
|
258
|
+
* Expected parent span ID.
|
|
259
|
+
*/
|
|
260
|
+
parentSpanId?: string;
|
|
261
|
+
/**
|
|
262
|
+
* Expected traceparent header value.
|
|
263
|
+
*/
|
|
264
|
+
traceparent?: string;
|
|
265
|
+
/**
|
|
266
|
+
* Expected watcher name.
|
|
267
|
+
*/
|
|
268
|
+
watcher?: string;
|
|
269
|
+
/**
|
|
270
|
+
* Expected provider name. Matches `providerName` on provider lifecycle events
|
|
271
|
+
* and `details.providerName` on provider instrumentation events.
|
|
272
|
+
*/
|
|
273
|
+
providerName?: string;
|
|
274
|
+
/**
|
|
275
|
+
* Expected structured details. Object values are matched as partial objects.
|
|
276
|
+
*/
|
|
277
|
+
details?: unknown;
|
|
278
|
+
/**
|
|
279
|
+
* Expected request method.
|
|
280
|
+
*/
|
|
281
|
+
method?: string;
|
|
282
|
+
/**
|
|
283
|
+
* Expected request path.
|
|
284
|
+
*/
|
|
285
|
+
path?: string;
|
|
286
|
+
/**
|
|
287
|
+
* Expected contract name.
|
|
288
|
+
*/
|
|
289
|
+
contractName?: string;
|
|
290
|
+
/**
|
|
291
|
+
* Expected status. This matches request status codes as well as job, outbox,
|
|
292
|
+
* and schedule status strings.
|
|
293
|
+
*/
|
|
294
|
+
status?: unknown;
|
|
295
|
+
/**
|
|
296
|
+
* Expected duration in milliseconds.
|
|
297
|
+
*/
|
|
298
|
+
durationMs?: number;
|
|
299
|
+
/**
|
|
300
|
+
* Expected human-readable summary.
|
|
301
|
+
*/
|
|
302
|
+
summary?: string;
|
|
303
|
+
/**
|
|
304
|
+
* Expected error message.
|
|
305
|
+
*/
|
|
306
|
+
message?: string;
|
|
307
|
+
/**
|
|
308
|
+
* Expected stack trace.
|
|
309
|
+
*/
|
|
310
|
+
stack?: string;
|
|
311
|
+
/**
|
|
312
|
+
* Expected use-case name on error events.
|
|
313
|
+
*/
|
|
314
|
+
useCaseName?: string;
|
|
315
|
+
/**
|
|
316
|
+
* Expected use-case or custom event name.
|
|
317
|
+
*/
|
|
318
|
+
name?: string;
|
|
319
|
+
/**
|
|
320
|
+
* Expected use-case kind.
|
|
321
|
+
*/
|
|
322
|
+
kind?: "command" | "query";
|
|
323
|
+
/**
|
|
324
|
+
* Expected use-case phase.
|
|
325
|
+
*/
|
|
326
|
+
phase?: "start" | "end" | "error";
|
|
327
|
+
/**
|
|
328
|
+
* Expected error summary.
|
|
329
|
+
*/
|
|
330
|
+
error?: string;
|
|
331
|
+
/**
|
|
332
|
+
* Expected event bus event name.
|
|
333
|
+
*/
|
|
334
|
+
eventName?: string;
|
|
335
|
+
/**
|
|
336
|
+
* Expected job name.
|
|
337
|
+
*/
|
|
338
|
+
jobName?: string;
|
|
339
|
+
/**
|
|
340
|
+
* Expected outbox message ID.
|
|
341
|
+
*/
|
|
342
|
+
messageId?: string;
|
|
343
|
+
/**
|
|
344
|
+
* Expected outbox message kind.
|
|
345
|
+
*/
|
|
346
|
+
messageKind?: "event" | "job";
|
|
347
|
+
/**
|
|
348
|
+
* Expected outbox message name.
|
|
349
|
+
*/
|
|
350
|
+
messageName?: string;
|
|
351
|
+
/**
|
|
352
|
+
* Expected schedule name.
|
|
353
|
+
*/
|
|
354
|
+
scheduleName?: string;
|
|
355
|
+
/**
|
|
356
|
+
* Expected schedule cron expression.
|
|
357
|
+
*/
|
|
358
|
+
cron?: string;
|
|
359
|
+
/**
|
|
360
|
+
* Expected schedule time zone.
|
|
361
|
+
*/
|
|
362
|
+
timezone?: string;
|
|
363
|
+
/**
|
|
364
|
+
* Expected provider lifecycle action.
|
|
365
|
+
*/
|
|
366
|
+
action?: "setup" | "start" | "stop";
|
|
367
|
+
/**
|
|
368
|
+
* Expected custom event label.
|
|
369
|
+
*/
|
|
370
|
+
label?: string;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Source accepted by provider instrumentation assertion helpers.
|
|
375
|
+
*/
|
|
376
|
+
export type ProviderInstrumentationAssertionSource =
|
|
377
|
+
| readonly ProviderInstrumentationEventInput[]
|
|
378
|
+
| {
|
|
379
|
+
/**
|
|
380
|
+
* Recorded provider instrumentation events.
|
|
381
|
+
*/
|
|
382
|
+
readonly events: readonly ProviderInstrumentationEventInput[];
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Create a recording schedule runner for tests.
|
|
387
|
+
*
|
|
388
|
+
* The runner records schedule run intent without executing the schedule
|
|
389
|
+
* handler. Use `createInlineScheduleRunner(...)` when the test should run the
|
|
390
|
+
* handler.
|
|
391
|
+
*
|
|
392
|
+
* @returns A schedule runner plus its captured runs.
|
|
393
|
+
*/
|
|
394
|
+
export function createRecordingScheduleRunner(): {
|
|
395
|
+
runner: ScheduleRunnerPort;
|
|
396
|
+
runs: RecordedScheduleRun[];
|
|
397
|
+
} {
|
|
398
|
+
const runs: RecordedScheduleRun[] = [];
|
|
399
|
+
|
|
400
|
+
const runner: ScheduleRunnerPort = {
|
|
401
|
+
async run<S extends ScheduleDef>(
|
|
402
|
+
schedule: S,
|
|
403
|
+
options: ScheduleRunOptions<InferSchedulePayload<S>> = {},
|
|
404
|
+
) {
|
|
405
|
+
runs.push({
|
|
406
|
+
name: schedule.name,
|
|
407
|
+
schedule,
|
|
408
|
+
...(Object.hasOwn(options, "payload")
|
|
409
|
+
? { payload: options.payload }
|
|
410
|
+
: {}),
|
|
411
|
+
...(options.id !== undefined ? { id: options.id } : {}),
|
|
412
|
+
...(options.source !== undefined ? { source: options.source } : {}),
|
|
413
|
+
...(options.scheduledAt !== undefined
|
|
414
|
+
? { scheduledAt: options.scheduledAt }
|
|
415
|
+
: {}),
|
|
416
|
+
...(options.triggeredAt !== undefined
|
|
417
|
+
? { triggeredAt: options.triggeredAt }
|
|
418
|
+
: {}),
|
|
419
|
+
});
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return { runner, runs };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Create a provider instrumentation port for tests.
|
|
428
|
+
*
|
|
429
|
+
* The port records every event and can optionally disable specific watchers.
|
|
430
|
+
* Use the returned `events` array with provider instrumentation assertion
|
|
431
|
+
* helpers.
|
|
432
|
+
*
|
|
433
|
+
* @returns A provider instrumentation port plus its captured events.
|
|
434
|
+
*/
|
|
435
|
+
export function createRecordingProviderInstrumentation(
|
|
436
|
+
options: {
|
|
437
|
+
enabledWatchers?: readonly string[];
|
|
438
|
+
disabledWatchers?: readonly string[];
|
|
439
|
+
} = {},
|
|
440
|
+
): {
|
|
441
|
+
instrumentation: ProviderInstrumentationPort;
|
|
442
|
+
events: ProviderInstrumentationEventInput[];
|
|
443
|
+
} {
|
|
444
|
+
const events: ProviderInstrumentationEventInput[] = [];
|
|
445
|
+
const enabledWatchers = options.enabledWatchers
|
|
446
|
+
? new Set(options.enabledWatchers)
|
|
447
|
+
: null;
|
|
448
|
+
const disabledWatchers = new Set(options.disabledWatchers ?? []);
|
|
449
|
+
|
|
450
|
+
const instrumentation: ProviderInstrumentationPort = {
|
|
451
|
+
record(event) {
|
|
452
|
+
events.push(event);
|
|
453
|
+
},
|
|
454
|
+
isWatcherEnabled(name) {
|
|
455
|
+
if (disabledWatchers.has(name)) return false;
|
|
456
|
+
return enabledWatchers ? enabledWatchers.has(name) : true;
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
return { instrumentation, events };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Options for creating a test user actor.
|
|
465
|
+
*/
|
|
466
|
+
export interface CreateTestUserActorOptions
|
|
467
|
+
extends Omit<ActivityActor, "type" | "id" | "metadata"> {
|
|
468
|
+
/**
|
|
469
|
+
* Optional role stored as `actor.metadata.role`.
|
|
470
|
+
*/
|
|
471
|
+
role?: string;
|
|
472
|
+
/**
|
|
473
|
+
* Additional redaction-safe actor metadata.
|
|
474
|
+
*/
|
|
475
|
+
metadata?: ActivityMetadata;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Options for creating a test actor that represents impersonated user access.
|
|
480
|
+
*/
|
|
481
|
+
export interface CreateTestImpersonatedUserActorOptions
|
|
482
|
+
extends CreateTestUserActorOptions {
|
|
483
|
+
/**
|
|
484
|
+
* Stable ID for the actor performing the impersonation.
|
|
485
|
+
*/
|
|
486
|
+
impersonatorId: string;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Options for creating a test tenant.
|
|
491
|
+
*/
|
|
492
|
+
export type CreateTestTenantOptions = Omit<ActivityTenant, "id">;
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Context fields commonly shared by Beignet tests that exercise audit,
|
|
496
|
+
* authorization, route hooks, and use cases.
|
|
497
|
+
*/
|
|
498
|
+
export interface TestActivityContext {
|
|
499
|
+
/**
|
|
500
|
+
* Actor under test.
|
|
501
|
+
*/
|
|
502
|
+
actor: ActivityActor;
|
|
503
|
+
/**
|
|
504
|
+
* Tenant/account/workspace scope under test.
|
|
505
|
+
*/
|
|
506
|
+
tenant?: ActivityTenant;
|
|
507
|
+
/**
|
|
508
|
+
* Stable request ID for assertions.
|
|
509
|
+
*/
|
|
510
|
+
requestId: string;
|
|
511
|
+
/**
|
|
512
|
+
* Optional trace ID for assertions.
|
|
513
|
+
*/
|
|
514
|
+
traceId?: string;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Options for creating a test activity context.
|
|
519
|
+
*/
|
|
520
|
+
export interface CreateTestActivityContextOptions {
|
|
521
|
+
/**
|
|
522
|
+
* Actor under test.
|
|
523
|
+
*
|
|
524
|
+
* @default createTestUserActor()
|
|
525
|
+
*/
|
|
526
|
+
actor?: ActivityActor;
|
|
527
|
+
/**
|
|
528
|
+
* Tenant under test. Pass `null` to omit tenant context.
|
|
529
|
+
*
|
|
530
|
+
* @default createTestTenant()
|
|
531
|
+
*/
|
|
532
|
+
tenant?: ActivityTenant | null;
|
|
533
|
+
/**
|
|
534
|
+
* Request ID to expose on the context.
|
|
535
|
+
*
|
|
536
|
+
* @default "test-request"
|
|
537
|
+
*/
|
|
538
|
+
requestId?: string;
|
|
539
|
+
/**
|
|
540
|
+
* Trace ID to expose on the context.
|
|
541
|
+
*
|
|
542
|
+
* @default "test-trace"
|
|
543
|
+
*/
|
|
544
|
+
traceId?: string;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Create a predictable user actor for tests.
|
|
549
|
+
*
|
|
550
|
+
* Use this when authorization or audit assertions need a stable actor shape
|
|
551
|
+
* without repeating the `ActivityActor` object in every test.
|
|
552
|
+
*
|
|
553
|
+
* @param id - Stable user ID for the test actor.
|
|
554
|
+
* @param options - Optional display name, role, and metadata.
|
|
555
|
+
* @returns A user actor with `type: "user"`.
|
|
556
|
+
*/
|
|
557
|
+
export function createTestUserActor(
|
|
558
|
+
id = "user_test",
|
|
559
|
+
options: CreateTestUserActorOptions = {},
|
|
560
|
+
): ActivityActor {
|
|
561
|
+
const { role, metadata, ...actorOptions } = options;
|
|
562
|
+
const mergedMetadata = mergeRoleMetadata(metadata, role);
|
|
563
|
+
|
|
564
|
+
return createUserActor(id, {
|
|
565
|
+
...actorOptions,
|
|
566
|
+
...(mergedMetadata ? { metadata: mergedMetadata } : {}),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Create a predictable user actor for tests that exercise impersonation.
|
|
572
|
+
*
|
|
573
|
+
* The returned actor remains the effective user, with `metadata.impersonatorId`
|
|
574
|
+
* recording who initiated the impersonated access.
|
|
575
|
+
*
|
|
576
|
+
* @param id - Stable user ID being impersonated.
|
|
577
|
+
* @param options - Impersonator ID plus optional display name, role, and metadata.
|
|
578
|
+
* @returns A user actor with impersonation metadata.
|
|
579
|
+
*/
|
|
580
|
+
export function createTestImpersonatedUserActor(
|
|
581
|
+
id: string,
|
|
582
|
+
options: CreateTestImpersonatedUserActorOptions,
|
|
583
|
+
): ActivityActor {
|
|
584
|
+
const { impersonatorId, ...actorOptions } = options;
|
|
585
|
+
|
|
586
|
+
return createTestUserActor(id, {
|
|
587
|
+
...actorOptions,
|
|
588
|
+
metadata: {
|
|
589
|
+
...actorOptions.metadata,
|
|
590
|
+
impersonatorId,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Create a predictable anonymous actor for tests.
|
|
597
|
+
*
|
|
598
|
+
* @param options - Optional display name or metadata.
|
|
599
|
+
* @returns An anonymous actor with `type: "anonymous"`.
|
|
600
|
+
*/
|
|
601
|
+
export function createTestAnonymousActor(
|
|
602
|
+
options: Omit<ActivityActor, "type"> = {},
|
|
603
|
+
): ActivityActor {
|
|
604
|
+
return createAnonymousActor(options);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Create a predictable service actor for tests.
|
|
609
|
+
*
|
|
610
|
+
* @param id - Stable service ID.
|
|
611
|
+
* @param options - Optional display name or metadata.
|
|
612
|
+
* @returns A service actor with `type: "service"`.
|
|
613
|
+
*/
|
|
614
|
+
export function createTestServiceActor(
|
|
615
|
+
id = "service_test",
|
|
616
|
+
options: Omit<ActivityActor, "type" | "id"> = {},
|
|
617
|
+
): ActivityActor {
|
|
618
|
+
return createServiceActor(id, options);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Create a predictable system actor for tests.
|
|
623
|
+
*
|
|
624
|
+
* @param id - Stable system actor ID.
|
|
625
|
+
* @param options - Optional display name or metadata.
|
|
626
|
+
* @returns A system actor with `type: "system"`.
|
|
627
|
+
*/
|
|
628
|
+
export function createTestSystemActor(
|
|
629
|
+
id = "system_test",
|
|
630
|
+
options: Omit<ActivityActor, "type" | "id"> = {},
|
|
631
|
+
): ActivityActor {
|
|
632
|
+
return createSystemActor(id, options);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Create a predictable tenant for tests.
|
|
637
|
+
*
|
|
638
|
+
* @param id - Stable tenant ID.
|
|
639
|
+
* @param options - Optional slug or metadata.
|
|
640
|
+
* @returns A tenant descriptor.
|
|
641
|
+
*/
|
|
642
|
+
export function createTestTenant(
|
|
643
|
+
id = "tenant_test",
|
|
644
|
+
options: CreateTestTenantOptions = {},
|
|
645
|
+
): ActivityTenant {
|
|
646
|
+
return createTenant(id, options);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Create the activity fields commonly copied onto app test contexts.
|
|
651
|
+
*
|
|
652
|
+
* @param options - Optional actor, tenant, request ID, and trace ID overrides.
|
|
653
|
+
* @returns Stable activity context fields for a test.
|
|
654
|
+
*/
|
|
655
|
+
export function createTestActivityContext(
|
|
656
|
+
options: CreateTestActivityContextOptions = {},
|
|
657
|
+
): TestActivityContext {
|
|
658
|
+
return {
|
|
659
|
+
actor: options.actor ?? createTestUserActor(),
|
|
660
|
+
...(options.tenant === null
|
|
661
|
+
? {}
|
|
662
|
+
: { tenant: options.tenant ?? createTestTenant() }),
|
|
663
|
+
requestId: options.requestId ?? "test-request",
|
|
664
|
+
traceId: options.traceId ?? "test-trace",
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Expected audit fields used by audit assertion helpers.
|
|
670
|
+
*/
|
|
671
|
+
export interface AuditLogEntryExpectation {
|
|
672
|
+
/**
|
|
673
|
+
* Expected action name.
|
|
674
|
+
*/
|
|
675
|
+
action?: string;
|
|
676
|
+
/**
|
|
677
|
+
* Expected actor fields.
|
|
678
|
+
*/
|
|
679
|
+
actor?: Partial<ActivityActor>;
|
|
680
|
+
/**
|
|
681
|
+
* Convenience matcher for `entry.actor.id`.
|
|
682
|
+
*/
|
|
683
|
+
actorId?: string;
|
|
684
|
+
/**
|
|
685
|
+
* Convenience matcher for `entry.actor.type`.
|
|
686
|
+
*/
|
|
687
|
+
actorType?: ActivityActor["type"];
|
|
688
|
+
/**
|
|
689
|
+
* Expected tenant fields.
|
|
690
|
+
*/
|
|
691
|
+
tenant?: Partial<ActivityTenant>;
|
|
692
|
+
/**
|
|
693
|
+
* Convenience matcher for `entry.tenant.id`.
|
|
694
|
+
*/
|
|
695
|
+
tenantId?: string;
|
|
696
|
+
/**
|
|
697
|
+
* Expected resource fields.
|
|
698
|
+
*/
|
|
699
|
+
resource?: Partial<ActivityResource>;
|
|
700
|
+
/**
|
|
701
|
+
* Convenience matcher for `entry.resource.id`.
|
|
702
|
+
*/
|
|
703
|
+
resourceId?: string;
|
|
704
|
+
/**
|
|
705
|
+
* Convenience matcher for `entry.resource.type`.
|
|
706
|
+
*/
|
|
707
|
+
resourceType?: string;
|
|
708
|
+
/**
|
|
709
|
+
* Expected audit outcome.
|
|
710
|
+
*/
|
|
711
|
+
outcome?: AuditOutcome;
|
|
712
|
+
/**
|
|
713
|
+
* Convenience matcher for `entry.metadata.severity`.
|
|
714
|
+
*/
|
|
715
|
+
severity?: ActivityMetadataValue;
|
|
716
|
+
/**
|
|
717
|
+
* Expected request ID.
|
|
718
|
+
*/
|
|
719
|
+
requestId?: string;
|
|
720
|
+
/**
|
|
721
|
+
* Expected trace ID.
|
|
722
|
+
*/
|
|
723
|
+
traceId?: string;
|
|
724
|
+
/**
|
|
725
|
+
* Expected metadata fields. Object values are matched as partial objects.
|
|
726
|
+
*/
|
|
727
|
+
metadata?: ActivityMetadata;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Expected mail delivery fields used by mail assertion helpers.
|
|
732
|
+
*/
|
|
733
|
+
export interface MailDeliveryExpectation {
|
|
734
|
+
/**
|
|
735
|
+
* Expected memory delivery ID.
|
|
736
|
+
*/
|
|
737
|
+
id?: string;
|
|
738
|
+
/**
|
|
739
|
+
* Expected subject.
|
|
740
|
+
*/
|
|
741
|
+
subject?: string;
|
|
742
|
+
/**
|
|
743
|
+
* Expected recipients.
|
|
744
|
+
*/
|
|
745
|
+
to?: NormalizedMailMessage["to"];
|
|
746
|
+
/**
|
|
747
|
+
* Expected sender.
|
|
748
|
+
*/
|
|
749
|
+
from?: NormalizedMailMessage["from"];
|
|
750
|
+
/**
|
|
751
|
+
* Expected text body.
|
|
752
|
+
*/
|
|
753
|
+
text?: string;
|
|
754
|
+
/**
|
|
755
|
+
* Expected HTML body.
|
|
756
|
+
*/
|
|
757
|
+
html?: string;
|
|
758
|
+
/**
|
|
759
|
+
* Expected message headers.
|
|
760
|
+
*/
|
|
761
|
+
headers?: Record<string, string>;
|
|
762
|
+
/**
|
|
763
|
+
* Expected normalized message fields.
|
|
764
|
+
*/
|
|
765
|
+
message?: Partial<NormalizedMailMessage>;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Expected notification delivery fields used by notification assertion helpers.
|
|
770
|
+
*/
|
|
771
|
+
export interface NotificationDeliveryExpectation {
|
|
772
|
+
/**
|
|
773
|
+
* Expected memory delivery ID.
|
|
774
|
+
*/
|
|
775
|
+
id?: string;
|
|
776
|
+
/**
|
|
777
|
+
* Expected notification name.
|
|
778
|
+
*/
|
|
779
|
+
notificationName?: string;
|
|
780
|
+
/**
|
|
781
|
+
* Expected parsed payload. Object values are matched as partial objects.
|
|
782
|
+
*/
|
|
783
|
+
payload?: unknown;
|
|
784
|
+
/**
|
|
785
|
+
* Expected selected channels.
|
|
786
|
+
*/
|
|
787
|
+
channels?: readonly string[];
|
|
788
|
+
/**
|
|
789
|
+
* Expected delivery metadata. Object values are matched as partial objects.
|
|
790
|
+
*/
|
|
791
|
+
metadata?: Record<string, unknown>;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Expected storage object fields used by storage assertion helpers.
|
|
796
|
+
*/
|
|
797
|
+
export interface StorageObjectExpectation {
|
|
798
|
+
/**
|
|
799
|
+
* Object key to look up.
|
|
800
|
+
*/
|
|
801
|
+
key: string;
|
|
802
|
+
/**
|
|
803
|
+
* Expected size in bytes.
|
|
804
|
+
*/
|
|
805
|
+
size?: number;
|
|
806
|
+
/**
|
|
807
|
+
* Expected content type.
|
|
808
|
+
*/
|
|
809
|
+
contentType?: string;
|
|
810
|
+
/**
|
|
811
|
+
* Expected cache-control value.
|
|
812
|
+
*/
|
|
813
|
+
cacheControl?: string;
|
|
814
|
+
/**
|
|
815
|
+
* Expected storage metadata.
|
|
816
|
+
*/
|
|
817
|
+
metadata?: Record<string, string>;
|
|
818
|
+
/**
|
|
819
|
+
* Expected visibility.
|
|
820
|
+
*/
|
|
821
|
+
visibility?: StorageVisibility;
|
|
822
|
+
/**
|
|
823
|
+
* Expected text body. Cannot be combined with `bytes`.
|
|
824
|
+
*/
|
|
825
|
+
text?: string;
|
|
826
|
+
/**
|
|
827
|
+
* Expected object bytes. Cannot be combined with `text`.
|
|
828
|
+
*/
|
|
829
|
+
bytes?: Uint8Array;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Source accepted by outbox message assertion helpers.
|
|
834
|
+
*
|
|
835
|
+
* Use a `MemoryOutboxPort` or a snapshot returned by a test adapter. Durable
|
|
836
|
+
* SQL adapters should expose app-owned snapshots rather than widening the
|
|
837
|
+
* production `OutboxPort` read surface.
|
|
838
|
+
*/
|
|
839
|
+
export type OutboxMessageAssertionSource =
|
|
840
|
+
| readonly OutboxMessage[]
|
|
841
|
+
| {
|
|
842
|
+
/**
|
|
843
|
+
* Current outbox message snapshots.
|
|
844
|
+
*/
|
|
845
|
+
readonly messages: readonly OutboxMessage[];
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Expected outbox message fields used by outbox assertion helpers.
|
|
850
|
+
*/
|
|
851
|
+
export interface OutboxMessageExpectation {
|
|
852
|
+
/**
|
|
853
|
+
* Expected message ID.
|
|
854
|
+
*/
|
|
855
|
+
id?: string;
|
|
856
|
+
/**
|
|
857
|
+
* Expected message kind.
|
|
858
|
+
*/
|
|
859
|
+
kind?: OutboxMessageKind;
|
|
860
|
+
/**
|
|
861
|
+
* Expected event or job name.
|
|
862
|
+
*/
|
|
863
|
+
name?: string;
|
|
864
|
+
/**
|
|
865
|
+
* Expected JSON payload. Object values are matched as partial objects.
|
|
866
|
+
*/
|
|
867
|
+
payload?: unknown;
|
|
868
|
+
/**
|
|
869
|
+
* Expected delivery status.
|
|
870
|
+
*/
|
|
871
|
+
status?: OutboxMessageStatus;
|
|
872
|
+
/**
|
|
873
|
+
* Expected claim attempt count.
|
|
874
|
+
*/
|
|
875
|
+
attempts?: number;
|
|
876
|
+
/**
|
|
877
|
+
* Expected maximum delivery attempts.
|
|
878
|
+
*/
|
|
879
|
+
maxAttempts?: number;
|
|
880
|
+
/**
|
|
881
|
+
* Expected delivery timestamp.
|
|
882
|
+
*/
|
|
883
|
+
deliveredAt?: Date | null;
|
|
884
|
+
/**
|
|
885
|
+
* Expected serialized delivery error. Object values are matched as partial
|
|
886
|
+
* objects. Pass `null` to assert no error has been recorded.
|
|
887
|
+
*/
|
|
888
|
+
lastError?: Partial<OutboxErrorInfo> | null;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Expected fields for one outbox drain result assertion.
|
|
893
|
+
*/
|
|
894
|
+
export interface OutboxDrainResultExpectation {
|
|
895
|
+
/**
|
|
896
|
+
* Expected claimed count.
|
|
897
|
+
*/
|
|
898
|
+
claimed?: number;
|
|
899
|
+
/**
|
|
900
|
+
* Expected delivered count.
|
|
901
|
+
*/
|
|
902
|
+
delivered?: number;
|
|
903
|
+
/**
|
|
904
|
+
* Expected retried count.
|
|
905
|
+
*/
|
|
906
|
+
retried?: number;
|
|
907
|
+
/**
|
|
908
|
+
* Expected dead-lettered count.
|
|
909
|
+
*/
|
|
910
|
+
deadLettered?: number;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Source accepted by idempotency entry assertion helpers.
|
|
915
|
+
*
|
|
916
|
+
* Use a `MemoryIdempotencyStore` or a snapshot array from an app-owned adapter.
|
|
917
|
+
* Durable SQL adapters should expose app-owned snapshots rather than widening
|
|
918
|
+
* the production `IdempotencyPort` read surface.
|
|
919
|
+
*/
|
|
920
|
+
export type IdempotencyEntryAssertionSource =
|
|
921
|
+
| readonly MemoryIdempotencyEntry[]
|
|
922
|
+
| Pick<MemoryIdempotencyStore, "entries">;
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Expected idempotency entry fields used by idempotency assertion helpers.
|
|
926
|
+
*/
|
|
927
|
+
export interface IdempotencyEntryExpectation {
|
|
928
|
+
/**
|
|
929
|
+
* Expected operation namespace.
|
|
930
|
+
*/
|
|
931
|
+
namespace?: string;
|
|
932
|
+
/**
|
|
933
|
+
* Expected client-provided idempotency key.
|
|
934
|
+
*/
|
|
935
|
+
key?: string;
|
|
936
|
+
/**
|
|
937
|
+
* Expected normalized scope key.
|
|
938
|
+
*/
|
|
939
|
+
scopeKey?: string;
|
|
940
|
+
/**
|
|
941
|
+
* Expected request fingerprint.
|
|
942
|
+
*/
|
|
943
|
+
fingerprint?: string;
|
|
944
|
+
/**
|
|
945
|
+
* Expected reservation status.
|
|
946
|
+
*/
|
|
947
|
+
status?: MemoryIdempotencyEntry["status"];
|
|
948
|
+
/**
|
|
949
|
+
* Expected replay result. Object values are matched as partial objects.
|
|
950
|
+
*/
|
|
951
|
+
result?: unknown;
|
|
952
|
+
/**
|
|
953
|
+
* Expected reservation timestamp.
|
|
954
|
+
*/
|
|
955
|
+
reservedAt?: Date;
|
|
956
|
+
/**
|
|
957
|
+
* Expected completion timestamp.
|
|
958
|
+
*/
|
|
959
|
+
completedAt?: Date;
|
|
960
|
+
/**
|
|
961
|
+
* Expected expiration timestamp, or `null` when no expiration is set.
|
|
962
|
+
*/
|
|
963
|
+
expiresAt?: Date | null;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Find the first audit entry matching the expected fields.
|
|
968
|
+
*
|
|
969
|
+
* @param entries - Audit entries captured by a memory or fake audit port.
|
|
970
|
+
* @param expectation - Partial audit fields to match.
|
|
971
|
+
* @returns The first matching entry, or `undefined`.
|
|
972
|
+
*/
|
|
973
|
+
export function findAuditEntry(
|
|
974
|
+
entries: readonly AuditLogEntry[],
|
|
975
|
+
expectation: AuditLogEntryExpectation,
|
|
976
|
+
): AuditLogEntry | undefined {
|
|
977
|
+
return entries.find((entry) => auditEntryMatches(entry, expectation));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Assert that an audit entry exists and return the matching entry.
|
|
982
|
+
*
|
|
983
|
+
* The helper throws a plain `Error`, so it works with Bun, Vitest, Jest, and
|
|
984
|
+
* other test runners.
|
|
985
|
+
*
|
|
986
|
+
* @param entries - Audit entries captured by a memory or fake audit port.
|
|
987
|
+
* @param expectation - Partial audit fields to match.
|
|
988
|
+
* @returns The matching audit entry.
|
|
989
|
+
* @throws Error when no entry matches.
|
|
990
|
+
*/
|
|
991
|
+
export function assertAuditEntry(
|
|
992
|
+
entries: readonly AuditLogEntry[],
|
|
993
|
+
expectation: AuditLogEntryExpectation,
|
|
994
|
+
): AuditLogEntry {
|
|
995
|
+
const entry = findAuditEntry(entries, expectation);
|
|
996
|
+
if (entry) return entry;
|
|
997
|
+
|
|
998
|
+
throw new Error(
|
|
999
|
+
`Expected audit entry matching ${formatAuditExpectation(expectation)}, but only found ${entries.length} entr${
|
|
1000
|
+
entries.length === 1 ? "y" : "ies"
|
|
1001
|
+
}.`,
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Assert that no audit entry matches the expected fields.
|
|
1007
|
+
*
|
|
1008
|
+
* @param entries - Audit entries captured by a memory or fake audit port.
|
|
1009
|
+
* @param expectation - Partial audit fields to reject.
|
|
1010
|
+
* @throws Error when a matching entry exists.
|
|
1011
|
+
*/
|
|
1012
|
+
export function assertNoAuditEntry(
|
|
1013
|
+
entries: readonly AuditLogEntry[],
|
|
1014
|
+
expectation: AuditLogEntryExpectation,
|
|
1015
|
+
): void {
|
|
1016
|
+
const entry = findAuditEntry(entries, expectation);
|
|
1017
|
+
if (!entry) return;
|
|
1018
|
+
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
`Expected no audit entry matching ${formatAuditExpectation(expectation)}, but found ${entry.action}.`,
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Find the first recorded event matching the expected fields.
|
|
1026
|
+
*
|
|
1027
|
+
* @param events - Events captured by `createRecordingEventBus(...)`.
|
|
1028
|
+
* @param expectation - Partial event fields to match.
|
|
1029
|
+
* @returns The first matching event, or `undefined`.
|
|
1030
|
+
*/
|
|
1031
|
+
export function findRecordedEvent(
|
|
1032
|
+
events: readonly RecordedEvent[],
|
|
1033
|
+
expectation: RecordedEventExpectation,
|
|
1034
|
+
): RecordedEvent | undefined {
|
|
1035
|
+
return events.find((event) => recordedEventMatches(event, expectation));
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Assert that a recorded event exists and return the matching event.
|
|
1040
|
+
*
|
|
1041
|
+
* @param events - Events captured by `createRecordingEventBus(...)`.
|
|
1042
|
+
* @param expectation - Partial event fields to match.
|
|
1043
|
+
* @returns The matching event.
|
|
1044
|
+
* @throws Error when no event matches.
|
|
1045
|
+
*/
|
|
1046
|
+
export function assertRecordedEvent(
|
|
1047
|
+
events: readonly RecordedEvent[],
|
|
1048
|
+
expectation: RecordedEventExpectation,
|
|
1049
|
+
): RecordedEvent {
|
|
1050
|
+
const event = findRecordedEvent(events, expectation);
|
|
1051
|
+
if (event) return event;
|
|
1052
|
+
|
|
1053
|
+
throw new Error(
|
|
1054
|
+
`Expected recorded event matching ${formatTestingExpectation(expectation)}, but only found ${events.length} ${pluralize(
|
|
1055
|
+
"event",
|
|
1056
|
+
events.length,
|
|
1057
|
+
)}.`,
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Assert that no recorded event matches the expected fields.
|
|
1063
|
+
*
|
|
1064
|
+
* @param events - Events captured by `createRecordingEventBus(...)`.
|
|
1065
|
+
* @param expectation - Partial event fields to reject.
|
|
1066
|
+
* @throws Error when a matching event exists.
|
|
1067
|
+
*/
|
|
1068
|
+
export function assertNoRecordedEvent(
|
|
1069
|
+
events: readonly RecordedEvent[],
|
|
1070
|
+
expectation: RecordedEventExpectation,
|
|
1071
|
+
): void {
|
|
1072
|
+
const event = findRecordedEvent(events, expectation);
|
|
1073
|
+
if (!event) return;
|
|
1074
|
+
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`Expected no recorded event matching ${formatTestingExpectation(expectation)}, but found ${event.name}.`,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Find the first recorded job dispatch matching the expected fields.
|
|
1082
|
+
*
|
|
1083
|
+
* @param jobs - Job dispatches captured by `createRecordingJobDispatcher(...)`.
|
|
1084
|
+
* @param expectation - Partial job fields to match.
|
|
1085
|
+
* @returns The first matching dispatch, or `undefined`.
|
|
1086
|
+
*/
|
|
1087
|
+
export function findDispatchedJob(
|
|
1088
|
+
jobs: readonly RecordedJobDispatch[],
|
|
1089
|
+
expectation: RecordedJobDispatchExpectation,
|
|
1090
|
+
): RecordedJobDispatch | undefined {
|
|
1091
|
+
return jobs.find((job) => recordedJobMatches(job, expectation));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Assert that a job was dispatched and return the matching dispatch.
|
|
1096
|
+
*
|
|
1097
|
+
* @param jobs - Job dispatches captured by `createRecordingJobDispatcher(...)`.
|
|
1098
|
+
* @param expectation - Partial job fields to match.
|
|
1099
|
+
* @returns The matching dispatch.
|
|
1100
|
+
* @throws Error when no dispatch matches.
|
|
1101
|
+
*/
|
|
1102
|
+
export function assertDispatchedJob(
|
|
1103
|
+
jobs: readonly RecordedJobDispatch[],
|
|
1104
|
+
expectation: RecordedJobDispatchExpectation,
|
|
1105
|
+
): RecordedJobDispatch {
|
|
1106
|
+
const job = findDispatchedJob(jobs, expectation);
|
|
1107
|
+
if (job) return job;
|
|
1108
|
+
|
|
1109
|
+
throw new Error(
|
|
1110
|
+
`Expected dispatched job matching ${formatTestingExpectation(expectation)}, but only found ${jobs.length} ${pluralize(
|
|
1111
|
+
"job",
|
|
1112
|
+
jobs.length,
|
|
1113
|
+
)}.`,
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Assert that no job dispatch matches the expected fields.
|
|
1119
|
+
*
|
|
1120
|
+
* @param jobs - Job dispatches captured by `createRecordingJobDispatcher(...)`.
|
|
1121
|
+
* @param expectation - Partial job fields to reject.
|
|
1122
|
+
* @throws Error when a matching dispatch exists.
|
|
1123
|
+
*/
|
|
1124
|
+
export function assertNoDispatchedJob(
|
|
1125
|
+
jobs: readonly RecordedJobDispatch[],
|
|
1126
|
+
expectation: RecordedJobDispatchExpectation,
|
|
1127
|
+
): void {
|
|
1128
|
+
const job = findDispatchedJob(jobs, expectation);
|
|
1129
|
+
if (!job) return;
|
|
1130
|
+
|
|
1131
|
+
throw new Error(
|
|
1132
|
+
`Expected no dispatched job matching ${formatTestingExpectation(expectation)}, but found ${job.name}.`,
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Find the first recorded schedule run matching the expected fields.
|
|
1138
|
+
*
|
|
1139
|
+
* @param runs - Runs captured by `createRecordingScheduleRunner(...)`.
|
|
1140
|
+
* @param expectation - Partial schedule fields to match.
|
|
1141
|
+
* @returns The first matching schedule run, or `undefined`.
|
|
1142
|
+
*/
|
|
1143
|
+
export function findScheduleRun(
|
|
1144
|
+
runs: readonly RecordedScheduleRun[],
|
|
1145
|
+
expectation: RecordedScheduleRunExpectation,
|
|
1146
|
+
): RecordedScheduleRun | undefined {
|
|
1147
|
+
return runs.find((run) => recordedScheduleRunMatches(run, expectation));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Find the first provider instrumentation event matching the expected fields.
|
|
1152
|
+
*
|
|
1153
|
+
* @param source - Recording instrumentation result or event snapshot array.
|
|
1154
|
+
* @param expectation - Partial event fields to match.
|
|
1155
|
+
* @returns The first matching event, or `undefined`.
|
|
1156
|
+
*/
|
|
1157
|
+
export function findProviderInstrumentationEvent(
|
|
1158
|
+
source: ProviderInstrumentationAssertionSource,
|
|
1159
|
+
expectation: ProviderInstrumentationEventExpectation,
|
|
1160
|
+
): ProviderInstrumentationEventInput | undefined {
|
|
1161
|
+
return getProviderInstrumentationEvents(source).find((event) =>
|
|
1162
|
+
providerInstrumentationEventMatches(event, expectation),
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Assert that a provider instrumentation event exists and return it.
|
|
1168
|
+
*
|
|
1169
|
+
* @param source - Recording instrumentation result or event snapshot array.
|
|
1170
|
+
* @param expectation - Partial event fields to match.
|
|
1171
|
+
* @returns The matching event.
|
|
1172
|
+
* @throws Error when no event matches.
|
|
1173
|
+
*/
|
|
1174
|
+
export function assertProviderInstrumentationEvent(
|
|
1175
|
+
source: ProviderInstrumentationAssertionSource,
|
|
1176
|
+
expectation: ProviderInstrumentationEventExpectation,
|
|
1177
|
+
): ProviderInstrumentationEventInput {
|
|
1178
|
+
const event = findProviderInstrumentationEvent(source, expectation);
|
|
1179
|
+
if (event) return event;
|
|
1180
|
+
|
|
1181
|
+
const events = getProviderInstrumentationEvents(source);
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
`Expected provider instrumentation event matching ${formatTestingExpectation(
|
|
1184
|
+
expectation,
|
|
1185
|
+
)}, but only found ${events.length} ${pluralize("event", events.length)}.`,
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Assert that no provider instrumentation event matches the expected fields.
|
|
1191
|
+
*
|
|
1192
|
+
* @param source - Recording instrumentation result or event snapshot array.
|
|
1193
|
+
* @param expectation - Partial event fields to reject.
|
|
1194
|
+
* @throws Error when a matching event exists.
|
|
1195
|
+
*/
|
|
1196
|
+
export function assertNoProviderInstrumentationEvent(
|
|
1197
|
+
source: ProviderInstrumentationAssertionSource,
|
|
1198
|
+
expectation: ProviderInstrumentationEventExpectation,
|
|
1199
|
+
): void {
|
|
1200
|
+
const event = findProviderInstrumentationEvent(source, expectation);
|
|
1201
|
+
if (!event) return;
|
|
1202
|
+
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
`Expected no provider instrumentation event matching ${formatTestingExpectation(
|
|
1205
|
+
expectation,
|
|
1206
|
+
)}, but found ${event.type}.`,
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Assert that a schedule run was recorded and return the matching run.
|
|
1212
|
+
*
|
|
1213
|
+
* @param runs - Runs captured by `createRecordingScheduleRunner(...)`.
|
|
1214
|
+
* @param expectation - Partial schedule fields to match.
|
|
1215
|
+
* @returns The matching schedule run.
|
|
1216
|
+
* @throws Error when no run matches.
|
|
1217
|
+
*/
|
|
1218
|
+
export function assertScheduleRun(
|
|
1219
|
+
runs: readonly RecordedScheduleRun[],
|
|
1220
|
+
expectation: RecordedScheduleRunExpectation,
|
|
1221
|
+
): RecordedScheduleRun {
|
|
1222
|
+
const run = findScheduleRun(runs, expectation);
|
|
1223
|
+
if (run) return run;
|
|
1224
|
+
|
|
1225
|
+
throw new Error(
|
|
1226
|
+
`Expected schedule run matching ${formatTestingExpectation(expectation)}, but only found ${runs.length} ${pluralize(
|
|
1227
|
+
"run",
|
|
1228
|
+
runs.length,
|
|
1229
|
+
)}.`,
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Assert that no schedule run matches the expected fields.
|
|
1235
|
+
*
|
|
1236
|
+
* @param runs - Runs captured by `createRecordingScheduleRunner(...)`.
|
|
1237
|
+
* @param expectation - Partial schedule fields to reject.
|
|
1238
|
+
* @throws Error when a matching run exists.
|
|
1239
|
+
*/
|
|
1240
|
+
export function assertNoScheduleRun(
|
|
1241
|
+
runs: readonly RecordedScheduleRun[],
|
|
1242
|
+
expectation: RecordedScheduleRunExpectation,
|
|
1243
|
+
): void {
|
|
1244
|
+
const run = findScheduleRun(runs, expectation);
|
|
1245
|
+
if (!run) return;
|
|
1246
|
+
|
|
1247
|
+
throw new Error(
|
|
1248
|
+
`Expected no schedule run matching ${formatTestingExpectation(expectation)}, but found ${run.name}.`,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Find the first mail delivery matching the expected fields.
|
|
1254
|
+
*
|
|
1255
|
+
* @param deliveries - Deliveries captured by `createMemoryMailer(...)`.
|
|
1256
|
+
* @param expectation - Partial delivery or message fields to match.
|
|
1257
|
+
* @returns The first matching delivery, or `undefined`.
|
|
1258
|
+
*/
|
|
1259
|
+
export function findMailDelivery(
|
|
1260
|
+
deliveries: readonly MemoryMailDelivery[],
|
|
1261
|
+
expectation: MailDeliveryExpectation,
|
|
1262
|
+
): MemoryMailDelivery | undefined {
|
|
1263
|
+
return deliveries.find((delivery) =>
|
|
1264
|
+
mailDeliveryMatches(delivery, expectation),
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Assert that a mail delivery exists and return the matching delivery.
|
|
1270
|
+
*
|
|
1271
|
+
* @param deliveries - Deliveries captured by `createMemoryMailer(...)`.
|
|
1272
|
+
* @param expectation - Partial delivery or message fields to match.
|
|
1273
|
+
* @returns The matching delivery.
|
|
1274
|
+
* @throws Error when no delivery matches.
|
|
1275
|
+
*/
|
|
1276
|
+
export function assertMailDelivery(
|
|
1277
|
+
deliveries: readonly MemoryMailDelivery[],
|
|
1278
|
+
expectation: MailDeliveryExpectation,
|
|
1279
|
+
): MemoryMailDelivery {
|
|
1280
|
+
const delivery = findMailDelivery(deliveries, expectation);
|
|
1281
|
+
if (delivery) return delivery;
|
|
1282
|
+
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
`Expected mail delivery matching ${formatTestingExpectation(expectation)}, but only found ${deliveries.length} ${pluralize(
|
|
1285
|
+
"delivery",
|
|
1286
|
+
deliveries.length,
|
|
1287
|
+
)}.`,
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Assert that no mail delivery matches the expected fields.
|
|
1293
|
+
*
|
|
1294
|
+
* @param deliveries - Deliveries captured by `createMemoryMailer(...)`.
|
|
1295
|
+
* @param expectation - Partial delivery or message fields to reject.
|
|
1296
|
+
* @throws Error when a matching delivery exists.
|
|
1297
|
+
*/
|
|
1298
|
+
export function assertNoMailDelivery(
|
|
1299
|
+
deliveries: readonly MemoryMailDelivery[],
|
|
1300
|
+
expectation: MailDeliveryExpectation,
|
|
1301
|
+
): void {
|
|
1302
|
+
const delivery = findMailDelivery(deliveries, expectation);
|
|
1303
|
+
if (!delivery) return;
|
|
1304
|
+
|
|
1305
|
+
throw new Error(
|
|
1306
|
+
`Expected no mail delivery matching ${formatTestingExpectation(expectation)}, but found ${delivery.message.subject}.`,
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Find the first notification delivery matching the expected fields.
|
|
1312
|
+
*
|
|
1313
|
+
* @param deliveries - Deliveries captured by `createMemoryNotificationPort(...)`.
|
|
1314
|
+
* @param expectation - Partial delivery fields to match.
|
|
1315
|
+
* @returns The first matching delivery, or `undefined`.
|
|
1316
|
+
*/
|
|
1317
|
+
export function findNotificationDelivery(
|
|
1318
|
+
deliveries: readonly MemoryNotificationDelivery[],
|
|
1319
|
+
expectation: NotificationDeliveryExpectation,
|
|
1320
|
+
): MemoryNotificationDelivery | undefined {
|
|
1321
|
+
return deliveries.find((delivery) =>
|
|
1322
|
+
notificationDeliveryMatches(delivery, expectation),
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Assert that a notification delivery exists and return the matching delivery.
|
|
1328
|
+
*
|
|
1329
|
+
* @param deliveries - Deliveries captured by `createMemoryNotificationPort(...)`.
|
|
1330
|
+
* @param expectation - Partial delivery fields to match.
|
|
1331
|
+
* @returns The matching delivery.
|
|
1332
|
+
* @throws Error when no delivery matches.
|
|
1333
|
+
*/
|
|
1334
|
+
export function assertNotificationDelivery(
|
|
1335
|
+
deliveries: readonly MemoryNotificationDelivery[],
|
|
1336
|
+
expectation: NotificationDeliveryExpectation,
|
|
1337
|
+
): MemoryNotificationDelivery {
|
|
1338
|
+
const delivery = findNotificationDelivery(deliveries, expectation);
|
|
1339
|
+
if (delivery) return delivery;
|
|
1340
|
+
|
|
1341
|
+
throw new Error(
|
|
1342
|
+
`Expected notification delivery matching ${formatTestingExpectation(expectation)}, but only found ${deliveries.length} ${pluralize(
|
|
1343
|
+
"delivery",
|
|
1344
|
+
deliveries.length,
|
|
1345
|
+
)}.`,
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Assert that no notification delivery matches the expected fields.
|
|
1351
|
+
*
|
|
1352
|
+
* @param deliveries - Deliveries captured by `createMemoryNotificationPort(...)`.
|
|
1353
|
+
* @param expectation - Partial delivery fields to reject.
|
|
1354
|
+
* @throws Error when a matching delivery exists.
|
|
1355
|
+
*/
|
|
1356
|
+
export function assertNoNotificationDelivery(
|
|
1357
|
+
deliveries: readonly MemoryNotificationDelivery[],
|
|
1358
|
+
expectation: NotificationDeliveryExpectation,
|
|
1359
|
+
): void {
|
|
1360
|
+
const delivery = findNotificationDelivery(deliveries, expectation);
|
|
1361
|
+
if (!delivery) return;
|
|
1362
|
+
|
|
1363
|
+
throw new Error(
|
|
1364
|
+
`Expected no notification delivery matching ${formatTestingExpectation(expectation)}, but found ${delivery.notificationName}.`,
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Assert that a storage object exists and optionally matches metadata/body
|
|
1370
|
+
* expectations.
|
|
1371
|
+
*
|
|
1372
|
+
* This helper works against any `StoragePort`, not only memory storage.
|
|
1373
|
+
*
|
|
1374
|
+
* @param storage - Storage port under test.
|
|
1375
|
+
* @param expectation - Object key and expected fields.
|
|
1376
|
+
* @returns The matching object metadata.
|
|
1377
|
+
* @throws Error when the object is missing or does not match.
|
|
1378
|
+
*/
|
|
1379
|
+
export async function assertStorageObject(
|
|
1380
|
+
storage: StoragePort,
|
|
1381
|
+
expectation: StorageObjectExpectation,
|
|
1382
|
+
): Promise<StorageObject> {
|
|
1383
|
+
if (expectation.text !== undefined && expectation.bytes !== undefined) {
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
`Expected storage object "${expectation.key}" to check either text or bytes, not both.`,
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const object =
|
|
1390
|
+
expectation.text !== undefined || expectation.bytes !== undefined
|
|
1391
|
+
? await storage.get(expectation.key)
|
|
1392
|
+
: await storage.stat(expectation.key);
|
|
1393
|
+
|
|
1394
|
+
if (!object) {
|
|
1395
|
+
throw new Error(`Expected storage object "${expectation.key}" to exist.`);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (!storageObjectMatches(object, expectation)) {
|
|
1399
|
+
throw new Error(
|
|
1400
|
+
`Expected storage object "${expectation.key}" to match ${formatTestingExpectation(
|
|
1401
|
+
omitStorageBodyExpectation(expectation),
|
|
1402
|
+
)}.`,
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
if (expectation.text !== undefined) {
|
|
1407
|
+
const actualText = hasStorageTextReader(object)
|
|
1408
|
+
? await object.text()
|
|
1409
|
+
: undefined;
|
|
1410
|
+
if (actualText !== expectation.text) {
|
|
1411
|
+
throw new Error(
|
|
1412
|
+
`Expected storage object "${expectation.key}" text to match ${JSON.stringify(
|
|
1413
|
+
expectation.text,
|
|
1414
|
+
)}.`,
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
} else if (expectation.bytes !== undefined) {
|
|
1418
|
+
const actualBytes = hasStorageBytesReader(object)
|
|
1419
|
+
? await object.bytes()
|
|
1420
|
+
: undefined;
|
|
1421
|
+
if (!uint8ArrayMatches(actualBytes, expectation.bytes)) {
|
|
1422
|
+
throw new Error(
|
|
1423
|
+
`Expected storage object "${expectation.key}" bytes to match.`,
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
return object;
|
|
1429
|
+
}
|
|
12
1430
|
|
|
13
1431
|
/**
|
|
14
|
-
*
|
|
1432
|
+
* Assert that a storage object does not exist.
|
|
1433
|
+
*
|
|
1434
|
+
* @param storage - Storage port under test.
|
|
1435
|
+
* @param key - Object key expected to be absent.
|
|
1436
|
+
* @throws Error when the object exists.
|
|
15
1437
|
*/
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
1438
|
+
export async function assertNoStorageObject(
|
|
1439
|
+
storage: StoragePort,
|
|
1440
|
+
key: string,
|
|
1441
|
+
): Promise<void> {
|
|
1442
|
+
const exists = await storage.exists(key);
|
|
1443
|
+
if (!exists) return;
|
|
1444
|
+
|
|
1445
|
+
throw new Error(`Expected storage object "${key}" not to exist.`);
|
|
19
1446
|
}
|
|
20
1447
|
|
|
21
1448
|
/**
|
|
22
|
-
*
|
|
1449
|
+
* Find the first outbox message matching expected fields.
|
|
23
1450
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
1451
|
+
* @param source - Memory outbox or message snapshot array.
|
|
1452
|
+
* @param expectation - Partial message fields to match.
|
|
1453
|
+
* @returns The first matching message, or `undefined`.
|
|
1454
|
+
*/
|
|
1455
|
+
export function findOutboxMessage(
|
|
1456
|
+
source: OutboxMessageAssertionSource,
|
|
1457
|
+
expectation: OutboxMessageExpectation,
|
|
1458
|
+
): OutboxMessage | undefined {
|
|
1459
|
+
return getOutboxMessages(source).find((message) =>
|
|
1460
|
+
outboxMessageMatches(message, expectation),
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Find the first idempotency entry matching expected fields.
|
|
26
1466
|
*
|
|
27
|
-
* @
|
|
28
|
-
*
|
|
29
|
-
*
|
|
1467
|
+
* @param source - Memory idempotency store or entry snapshot array.
|
|
1468
|
+
* @param expectation - Partial entry fields to match.
|
|
1469
|
+
* @returns The first matching entry, or `undefined`.
|
|
1470
|
+
*/
|
|
1471
|
+
export function findIdempotencyEntry(
|
|
1472
|
+
source: IdempotencyEntryAssertionSource,
|
|
1473
|
+
expectation: IdempotencyEntryExpectation,
|
|
1474
|
+
): MemoryIdempotencyEntry | undefined {
|
|
1475
|
+
return getIdempotencyEntries(source).find((entry) =>
|
|
1476
|
+
idempotencyEntryMatches(entry, expectation),
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* Assert that an idempotency entry exists and return it.
|
|
30
1482
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
1483
|
+
* @param source - Memory idempotency store or entry snapshot array.
|
|
1484
|
+
* @param expectation - Partial entry fields to match.
|
|
1485
|
+
* @returns The matching entry.
|
|
1486
|
+
* @throws Error when no entry matches.
|
|
1487
|
+
*/
|
|
1488
|
+
export function assertIdempotencyEntry(
|
|
1489
|
+
source: IdempotencyEntryAssertionSource,
|
|
1490
|
+
expectation: IdempotencyEntryExpectation,
|
|
1491
|
+
): MemoryIdempotencyEntry {
|
|
1492
|
+
const entry = findIdempotencyEntry(source, expectation);
|
|
1493
|
+
if (entry) return entry;
|
|
1494
|
+
|
|
1495
|
+
const entries = getIdempotencyEntries(source);
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
`Expected idempotency entry matching ${formatTestingExpectation(
|
|
1498
|
+
expectation,
|
|
1499
|
+
)}, but only found ${entries.length} ${pluralize("entry", entries.length)}.`,
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* Assert that no idempotency entry matches expected fields.
|
|
33
1505
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* expect(events[0].payload).toEqual({ userId: "123", email: "test@example.com" });
|
|
38
|
-
* ```
|
|
1506
|
+
* @param source - Memory idempotency store or entry snapshot array.
|
|
1507
|
+
* @param expectation - Partial entry fields to reject.
|
|
1508
|
+
* @throws Error when a matching entry exists.
|
|
39
1509
|
*/
|
|
40
|
-
export function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
1510
|
+
export function assertNoIdempotencyEntry(
|
|
1511
|
+
source: IdempotencyEntryAssertionSource,
|
|
1512
|
+
expectation: IdempotencyEntryExpectation,
|
|
1513
|
+
): void {
|
|
1514
|
+
const entry = findIdempotencyEntry(source, expectation);
|
|
1515
|
+
if (!entry) return;
|
|
45
1516
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
53
|
-
};
|
|
1517
|
+
throw new Error(
|
|
1518
|
+
`Expected no idempotency entry matching ${formatTestingExpectation(
|
|
1519
|
+
expectation,
|
|
1520
|
+
)}, but found ${entry.namespace}:${entry.key}.`,
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
54
1523
|
|
|
55
|
-
|
|
1524
|
+
/**
|
|
1525
|
+
* Assert that a matching idempotency entry is still in progress.
|
|
1526
|
+
*
|
|
1527
|
+
* @param source - Memory idempotency store or entry snapshot array.
|
|
1528
|
+
* @param expectation - Entry fields to match.
|
|
1529
|
+
* @returns The matching in-progress entry.
|
|
1530
|
+
*/
|
|
1531
|
+
export function assertIdempotencyInProgress(
|
|
1532
|
+
source: IdempotencyEntryAssertionSource,
|
|
1533
|
+
expectation: Omit<IdempotencyEntryExpectation, "status"> = {},
|
|
1534
|
+
): MemoryIdempotencyEntry {
|
|
1535
|
+
return assertIdempotencyEntry(source, {
|
|
1536
|
+
...expectation,
|
|
1537
|
+
status: "in-progress",
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Assert that a matching idempotency entry completed.
|
|
1543
|
+
*
|
|
1544
|
+
* @param source - Memory idempotency store or entry snapshot array.
|
|
1545
|
+
* @param expectation - Entry fields to match.
|
|
1546
|
+
* @returns The matching completed entry.
|
|
1547
|
+
*/
|
|
1548
|
+
export function assertIdempotencyCompleted(
|
|
1549
|
+
source: IdempotencyEntryAssertionSource,
|
|
1550
|
+
expectation: Omit<IdempotencyEntryExpectation, "status"> = {},
|
|
1551
|
+
): MemoryIdempotencyEntry {
|
|
1552
|
+
return assertIdempotencyEntry(source, {
|
|
1553
|
+
...expectation,
|
|
1554
|
+
status: "completed",
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* Assert that an outbox message exists and return it.
|
|
1560
|
+
*
|
|
1561
|
+
* @param source - Memory outbox or message snapshot array.
|
|
1562
|
+
* @param expectation - Partial message fields to match.
|
|
1563
|
+
* @returns The matching message.
|
|
1564
|
+
* @throws Error when no message matches.
|
|
1565
|
+
*/
|
|
1566
|
+
export function assertOutboxMessage(
|
|
1567
|
+
source: OutboxMessageAssertionSource,
|
|
1568
|
+
expectation: OutboxMessageExpectation,
|
|
1569
|
+
): OutboxMessage {
|
|
1570
|
+
const message = findOutboxMessage(source, expectation);
|
|
1571
|
+
if (message) return message;
|
|
1572
|
+
|
|
1573
|
+
const messages = getOutboxMessages(source);
|
|
1574
|
+
throw new Error(
|
|
1575
|
+
`Expected outbox message matching ${formatTestingExpectation(expectation)}, but only found ${messages.length} ${pluralize(
|
|
1576
|
+
"message",
|
|
1577
|
+
messages.length,
|
|
1578
|
+
)}.`,
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/**
|
|
1583
|
+
* Assert that no outbox message matches expected fields.
|
|
1584
|
+
*
|
|
1585
|
+
* @param source - Memory outbox or message snapshot array.
|
|
1586
|
+
* @param expectation - Partial message fields to reject.
|
|
1587
|
+
* @throws Error when a matching message exists.
|
|
1588
|
+
*/
|
|
1589
|
+
export function assertNoOutboxMessage(
|
|
1590
|
+
source: OutboxMessageAssertionSource,
|
|
1591
|
+
expectation: OutboxMessageExpectation,
|
|
1592
|
+
): void {
|
|
1593
|
+
const message = findOutboxMessage(source, expectation);
|
|
1594
|
+
if (!message) return;
|
|
1595
|
+
|
|
1596
|
+
throw new Error(
|
|
1597
|
+
`Expected no outbox message matching ${formatTestingExpectation(expectation)}, but found ${message.kind} "${message.name}".`,
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/**
|
|
1602
|
+
* Assert that an outbox message is pending.
|
|
1603
|
+
*
|
|
1604
|
+
* @param source - Memory outbox or message snapshot array.
|
|
1605
|
+
* @param expectation - Message fields to match.
|
|
1606
|
+
* @returns The matching pending message.
|
|
1607
|
+
*/
|
|
1608
|
+
export function assertOutboxPending(
|
|
1609
|
+
source: OutboxMessageAssertionSource,
|
|
1610
|
+
expectation: Omit<OutboxMessageExpectation, "status"> = {},
|
|
1611
|
+
): OutboxMessage {
|
|
1612
|
+
return assertOutboxMessage(source, {
|
|
1613
|
+
...expectation,
|
|
1614
|
+
status: "pending",
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Assert that an outbox message was delivered.
|
|
1620
|
+
*
|
|
1621
|
+
* @param source - Memory outbox or message snapshot array.
|
|
1622
|
+
* @param expectation - Message fields to match.
|
|
1623
|
+
* @returns The matching delivered message.
|
|
1624
|
+
*/
|
|
1625
|
+
export function assertOutboxDelivered(
|
|
1626
|
+
source: OutboxMessageAssertionSource,
|
|
1627
|
+
expectation: Omit<OutboxMessageExpectation, "status"> = {},
|
|
1628
|
+
): OutboxMessage {
|
|
1629
|
+
return assertOutboxMessage(source, {
|
|
1630
|
+
...expectation,
|
|
1631
|
+
status: "delivered",
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* Assert that an outbox message is pending after at least one failed attempt.
|
|
1637
|
+
*
|
|
1638
|
+
* Use this for retry-scheduled assertions after `drainOutbox(...)` returns a
|
|
1639
|
+
* retried count.
|
|
1640
|
+
*
|
|
1641
|
+
* @param source - Memory outbox or message snapshot array.
|
|
1642
|
+
* @param expectation - Message fields to match.
|
|
1643
|
+
* @returns The matching retry-scheduled message.
|
|
1644
|
+
*/
|
|
1645
|
+
export function assertOutboxRetryScheduled(
|
|
1646
|
+
source: OutboxMessageAssertionSource,
|
|
1647
|
+
expectation: Omit<OutboxMessageExpectation, "status"> = {},
|
|
1648
|
+
): OutboxMessage {
|
|
1649
|
+
const message = assertOutboxMessage(source, {
|
|
1650
|
+
...expectation,
|
|
1651
|
+
status: "pending",
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
if (message.attempts < 1 || !message.lastError) {
|
|
1655
|
+
throw new Error(
|
|
1656
|
+
`Expected outbox message "${message.id}" to have a recorded failed attempt.`,
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
return message;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* Assert that an outbox message was dead-lettered.
|
|
1665
|
+
*
|
|
1666
|
+
* @param source - Memory outbox or message snapshot array.
|
|
1667
|
+
* @param expectation - Message fields to match.
|
|
1668
|
+
* @returns The matching dead-lettered message.
|
|
1669
|
+
*/
|
|
1670
|
+
export function assertOutboxDeadLettered(
|
|
1671
|
+
source: OutboxMessageAssertionSource,
|
|
1672
|
+
expectation: Omit<OutboxMessageExpectation, "status"> = {},
|
|
1673
|
+
): OutboxMessage {
|
|
1674
|
+
return assertOutboxMessage(source, {
|
|
1675
|
+
...expectation,
|
|
1676
|
+
status: "deadLettered",
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* Assert that a drain result matches expected counts.
|
|
1682
|
+
*
|
|
1683
|
+
* @param result - Result returned by `drainOutbox(...)`.
|
|
1684
|
+
* @param expectation - Partial count expectation.
|
|
1685
|
+
* @throws Error when any supplied count differs.
|
|
1686
|
+
*/
|
|
1687
|
+
export function assertOutboxDrainResult(
|
|
1688
|
+
result: DrainOutboxResult,
|
|
1689
|
+
expectation: OutboxDrainResultExpectation,
|
|
1690
|
+
): void {
|
|
1691
|
+
if (!outboxDrainResultMatches(result, expectation)) {
|
|
1692
|
+
throw new Error(
|
|
1693
|
+
`Expected outbox drain result matching ${formatTestingExpectation(
|
|
1694
|
+
expectation,
|
|
1695
|
+
)}, but received ${formatTestingExpectation(result)}.`,
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
56
1698
|
}
|
|
57
1699
|
|
|
58
1700
|
/**
|
|
@@ -279,3 +1921,548 @@ type DynamicGateInspect<TContext> = (
|
|
|
279
1921
|
ability: string,
|
|
280
1922
|
subject?: unknown,
|
|
281
1923
|
) => Promise<GateDecision>;
|
|
1924
|
+
|
|
1925
|
+
function mergeRoleMetadata(
|
|
1926
|
+
metadata: ActivityMetadata | undefined,
|
|
1927
|
+
role: string | undefined,
|
|
1928
|
+
): ActivityMetadata | undefined {
|
|
1929
|
+
if (!role) return metadata;
|
|
1930
|
+
|
|
1931
|
+
return {
|
|
1932
|
+
...metadata,
|
|
1933
|
+
role,
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function auditEntryMatches(
|
|
1938
|
+
entry: AuditLogEntry,
|
|
1939
|
+
expectation: AuditLogEntryExpectation,
|
|
1940
|
+
): boolean {
|
|
1941
|
+
if (expectation.action !== undefined && entry.action !== expectation.action) {
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
if (
|
|
1945
|
+
expectation.outcome !== undefined &&
|
|
1946
|
+
entry.outcome !== expectation.outcome
|
|
1947
|
+
) {
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
if (
|
|
1951
|
+
expectation.requestId !== undefined &&
|
|
1952
|
+
entry.requestId !== expectation.requestId
|
|
1953
|
+
) {
|
|
1954
|
+
return false;
|
|
1955
|
+
}
|
|
1956
|
+
if (
|
|
1957
|
+
expectation.traceId !== undefined &&
|
|
1958
|
+
entry.traceId !== expectation.traceId
|
|
1959
|
+
) {
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
if (
|
|
1963
|
+
expectation.actorId !== undefined &&
|
|
1964
|
+
entry.actor.id !== expectation.actorId
|
|
1965
|
+
) {
|
|
1966
|
+
return false;
|
|
1967
|
+
}
|
|
1968
|
+
if (
|
|
1969
|
+
expectation.actorType !== undefined &&
|
|
1970
|
+
entry.actor.type !== expectation.actorType
|
|
1971
|
+
) {
|
|
1972
|
+
return false;
|
|
1973
|
+
}
|
|
1974
|
+
if (
|
|
1975
|
+
expectation.tenantId !== undefined &&
|
|
1976
|
+
entry.tenant?.id !== expectation.tenantId
|
|
1977
|
+
) {
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
if (
|
|
1981
|
+
expectation.resourceId !== undefined &&
|
|
1982
|
+
entry.resource?.id !== expectation.resourceId
|
|
1983
|
+
) {
|
|
1984
|
+
return false;
|
|
1985
|
+
}
|
|
1986
|
+
if (
|
|
1987
|
+
expectation.resourceType !== undefined &&
|
|
1988
|
+
entry.resource?.type !== expectation.resourceType
|
|
1989
|
+
) {
|
|
1990
|
+
return false;
|
|
1991
|
+
}
|
|
1992
|
+
if (
|
|
1993
|
+
expectation.severity !== undefined &&
|
|
1994
|
+
entry.metadata?.severity !== expectation.severity
|
|
1995
|
+
) {
|
|
1996
|
+
return false;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
return (
|
|
2000
|
+
partialObjectMatches(entry.actor, expectation.actor) &&
|
|
2001
|
+
partialObjectMatches(entry.tenant, expectation.tenant) &&
|
|
2002
|
+
partialObjectMatches(entry.resource, expectation.resource) &&
|
|
2003
|
+
partialObjectMatches(entry.metadata, expectation.metadata)
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function recordedEventMatches(
|
|
2008
|
+
event: RecordedEvent,
|
|
2009
|
+
expectation: RecordedEventExpectation,
|
|
2010
|
+
): boolean {
|
|
2011
|
+
if (expectation.name !== undefined && event.name !== expectation.name) {
|
|
2012
|
+
return false;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
return expectation.payload === undefined
|
|
2016
|
+
? true
|
|
2017
|
+
: valueMatches(event.payload, expectation.payload);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function recordedJobMatches(
|
|
2021
|
+
job: RecordedJobDispatch,
|
|
2022
|
+
expectation: RecordedJobDispatchExpectation,
|
|
2023
|
+
): boolean {
|
|
2024
|
+
if (expectation.name !== undefined && job.name !== expectation.name) {
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
return expectation.payload === undefined
|
|
2029
|
+
? true
|
|
2030
|
+
: valueMatches(job.payload, expectation.payload);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
function recordedScheduleRunMatches(
|
|
2034
|
+
run: RecordedScheduleRun,
|
|
2035
|
+
expectation: RecordedScheduleRunExpectation,
|
|
2036
|
+
): boolean {
|
|
2037
|
+
if (expectation.name !== undefined && run.name !== expectation.name) {
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
if (expectation.id !== undefined && run.id !== expectation.id) {
|
|
2041
|
+
return false;
|
|
2042
|
+
}
|
|
2043
|
+
if (expectation.source !== undefined && run.source !== expectation.source) {
|
|
2044
|
+
return false;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
return expectation.payload === undefined
|
|
2048
|
+
? true
|
|
2049
|
+
: valueMatches(run.payload, expectation.payload);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
function getProviderInstrumentationEvents(
|
|
2053
|
+
source: ProviderInstrumentationAssertionSource,
|
|
2054
|
+
): readonly ProviderInstrumentationEventInput[] {
|
|
2055
|
+
return "events" in source ? source.events : source;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
function providerInstrumentationEventMatches(
|
|
2059
|
+
event: ProviderInstrumentationEventInput,
|
|
2060
|
+
expectation: ProviderInstrumentationEventExpectation,
|
|
2061
|
+
): boolean {
|
|
2062
|
+
const eventRecord = event as unknown as Record<string, unknown>;
|
|
2063
|
+
const expectationRecord = expectation as Record<string, unknown>;
|
|
2064
|
+
const directKeys = [
|
|
2065
|
+
"type",
|
|
2066
|
+
"id",
|
|
2067
|
+
"timestamp",
|
|
2068
|
+
"requestId",
|
|
2069
|
+
"traceId",
|
|
2070
|
+
"spanId",
|
|
2071
|
+
"parentSpanId",
|
|
2072
|
+
"traceparent",
|
|
2073
|
+
"watcher",
|
|
2074
|
+
"method",
|
|
2075
|
+
"path",
|
|
2076
|
+
"contractName",
|
|
2077
|
+
"status",
|
|
2078
|
+
"durationMs",
|
|
2079
|
+
"summary",
|
|
2080
|
+
"message",
|
|
2081
|
+
"stack",
|
|
2082
|
+
"useCaseName",
|
|
2083
|
+
"name",
|
|
2084
|
+
"kind",
|
|
2085
|
+
"phase",
|
|
2086
|
+
"error",
|
|
2087
|
+
"eventName",
|
|
2088
|
+
"jobName",
|
|
2089
|
+
"messageId",
|
|
2090
|
+
"messageKind",
|
|
2091
|
+
"messageName",
|
|
2092
|
+
"scheduleName",
|
|
2093
|
+
"cron",
|
|
2094
|
+
"timezone",
|
|
2095
|
+
"action",
|
|
2096
|
+
"label",
|
|
2097
|
+
] as const;
|
|
2098
|
+
|
|
2099
|
+
for (const key of directKeys) {
|
|
2100
|
+
if (
|
|
2101
|
+
Object.hasOwn(expectation, key) &&
|
|
2102
|
+
!valueMatches(eventRecord[key], expectationRecord[key])
|
|
2103
|
+
) {
|
|
2104
|
+
return false;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
if (
|
|
2109
|
+
expectation.providerName !== undefined &&
|
|
2110
|
+
providerNameFromInstrumentationEvent(event) !== expectation.providerName
|
|
2111
|
+
) {
|
|
2112
|
+
return false;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
return expectation.details === undefined
|
|
2116
|
+
? true
|
|
2117
|
+
: valueMatches(event.details, expectation.details);
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
function providerNameFromInstrumentationEvent(
|
|
2121
|
+
event: ProviderInstrumentationEventInput,
|
|
2122
|
+
): string | undefined {
|
|
2123
|
+
if (event.type === "provider") return event.providerName;
|
|
2124
|
+
if (!event.details || typeof event.details !== "object") return undefined;
|
|
2125
|
+
return (event.details as { providerName?: unknown }).providerName as
|
|
2126
|
+
| string
|
|
2127
|
+
| undefined;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function mailDeliveryMatches(
|
|
2131
|
+
delivery: MemoryMailDelivery,
|
|
2132
|
+
expectation: MailDeliveryExpectation,
|
|
2133
|
+
): boolean {
|
|
2134
|
+
if (expectation.id !== undefined && delivery.id !== expectation.id) {
|
|
2135
|
+
return false;
|
|
2136
|
+
}
|
|
2137
|
+
if (
|
|
2138
|
+
expectation.subject !== undefined &&
|
|
2139
|
+
delivery.message.subject !== expectation.subject
|
|
2140
|
+
) {
|
|
2141
|
+
return false;
|
|
2142
|
+
}
|
|
2143
|
+
if (
|
|
2144
|
+
expectation.to !== undefined &&
|
|
2145
|
+
!valueMatches(delivery.message.to, expectation.to)
|
|
2146
|
+
) {
|
|
2147
|
+
return false;
|
|
2148
|
+
}
|
|
2149
|
+
if (
|
|
2150
|
+
expectation.from !== undefined &&
|
|
2151
|
+
!valueMatches(delivery.message.from, expectation.from)
|
|
2152
|
+
) {
|
|
2153
|
+
return false;
|
|
2154
|
+
}
|
|
2155
|
+
if (
|
|
2156
|
+
expectation.text !== undefined &&
|
|
2157
|
+
delivery.message.text !== expectation.text
|
|
2158
|
+
) {
|
|
2159
|
+
return false;
|
|
2160
|
+
}
|
|
2161
|
+
if (
|
|
2162
|
+
expectation.html !== undefined &&
|
|
2163
|
+
delivery.message.html !== expectation.html
|
|
2164
|
+
) {
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
return (
|
|
2169
|
+
partialObjectMatches(delivery.message.headers, expectation.headers) &&
|
|
2170
|
+
partialObjectMatches(delivery.message, expectation.message)
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
function notificationDeliveryMatches(
|
|
2175
|
+
delivery: MemoryNotificationDelivery,
|
|
2176
|
+
expectation: NotificationDeliveryExpectation,
|
|
2177
|
+
): boolean {
|
|
2178
|
+
if (expectation.id !== undefined && delivery.id !== expectation.id) {
|
|
2179
|
+
return false;
|
|
2180
|
+
}
|
|
2181
|
+
if (
|
|
2182
|
+
expectation.notificationName !== undefined &&
|
|
2183
|
+
delivery.notificationName !== expectation.notificationName
|
|
2184
|
+
) {
|
|
2185
|
+
return false;
|
|
2186
|
+
}
|
|
2187
|
+
if (
|
|
2188
|
+
expectation.channels !== undefined &&
|
|
2189
|
+
!valueMatches(delivery.channels, expectation.channels)
|
|
2190
|
+
) {
|
|
2191
|
+
return false;
|
|
2192
|
+
}
|
|
2193
|
+
if (
|
|
2194
|
+
expectation.payload !== undefined &&
|
|
2195
|
+
!valueMatches(delivery.payload, expectation.payload)
|
|
2196
|
+
) {
|
|
2197
|
+
return false;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
return partialObjectMatches(delivery.metadata, expectation.metadata);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function storageObjectMatches(
|
|
2204
|
+
object: StorageObject,
|
|
2205
|
+
expectation: StorageObjectExpectation,
|
|
2206
|
+
): boolean {
|
|
2207
|
+
if (object.key !== expectation.key) return false;
|
|
2208
|
+
if (expectation.size !== undefined && object.size !== expectation.size) {
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
if (
|
|
2212
|
+
expectation.contentType !== undefined &&
|
|
2213
|
+
object.contentType !== expectation.contentType
|
|
2214
|
+
) {
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
if (
|
|
2218
|
+
expectation.cacheControl !== undefined &&
|
|
2219
|
+
object.cacheControl !== expectation.cacheControl
|
|
2220
|
+
) {
|
|
2221
|
+
return false;
|
|
2222
|
+
}
|
|
2223
|
+
if (
|
|
2224
|
+
expectation.visibility !== undefined &&
|
|
2225
|
+
object.visibility !== expectation.visibility
|
|
2226
|
+
) {
|
|
2227
|
+
return false;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
return partialObjectMatches(object.metadata, expectation.metadata);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
function getOutboxMessages(
|
|
2234
|
+
source: OutboxMessageAssertionSource,
|
|
2235
|
+
): readonly OutboxMessage[] {
|
|
2236
|
+
return "messages" in source ? source.messages : source;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function getIdempotencyEntries(
|
|
2240
|
+
source: IdempotencyEntryAssertionSource,
|
|
2241
|
+
): readonly MemoryIdempotencyEntry[] {
|
|
2242
|
+
if (Array.isArray(source)) return source;
|
|
2243
|
+
return (source as Pick<MemoryIdempotencyStore, "entries">).entries;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function idempotencyEntryMatches(
|
|
2247
|
+
entry: MemoryIdempotencyEntry,
|
|
2248
|
+
expectation: IdempotencyEntryExpectation,
|
|
2249
|
+
): boolean {
|
|
2250
|
+
if (
|
|
2251
|
+
expectation.namespace !== undefined &&
|
|
2252
|
+
entry.namespace !== expectation.namespace
|
|
2253
|
+
) {
|
|
2254
|
+
return false;
|
|
2255
|
+
}
|
|
2256
|
+
if (expectation.key !== undefined && entry.key !== expectation.key) {
|
|
2257
|
+
return false;
|
|
2258
|
+
}
|
|
2259
|
+
if (
|
|
2260
|
+
expectation.scopeKey !== undefined &&
|
|
2261
|
+
entry.scopeKey !== expectation.scopeKey
|
|
2262
|
+
) {
|
|
2263
|
+
return false;
|
|
2264
|
+
}
|
|
2265
|
+
if (
|
|
2266
|
+
expectation.fingerprint !== undefined &&
|
|
2267
|
+
entry.fingerprint !== expectation.fingerprint
|
|
2268
|
+
) {
|
|
2269
|
+
return false;
|
|
2270
|
+
}
|
|
2271
|
+
if (expectation.status !== undefined && entry.status !== expectation.status) {
|
|
2272
|
+
return false;
|
|
2273
|
+
}
|
|
2274
|
+
if (
|
|
2275
|
+
expectation.result !== undefined &&
|
|
2276
|
+
!valueMatches(entry.result, expectation.result)
|
|
2277
|
+
) {
|
|
2278
|
+
return false;
|
|
2279
|
+
}
|
|
2280
|
+
if (
|
|
2281
|
+
expectation.reservedAt !== undefined &&
|
|
2282
|
+
!valueMatches(entry.reservedAt, expectation.reservedAt)
|
|
2283
|
+
) {
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
if (
|
|
2287
|
+
expectation.completedAt !== undefined &&
|
|
2288
|
+
!valueMatches(entry.completedAt, expectation.completedAt)
|
|
2289
|
+
) {
|
|
2290
|
+
return false;
|
|
2291
|
+
}
|
|
2292
|
+
if (
|
|
2293
|
+
expectation.expiresAt !== undefined &&
|
|
2294
|
+
!valueMatches(entry.expiresAt, expectation.expiresAt)
|
|
2295
|
+
) {
|
|
2296
|
+
return false;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
return true;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
function outboxMessageMatches(
|
|
2303
|
+
message: OutboxMessage,
|
|
2304
|
+
expectation: OutboxMessageExpectation,
|
|
2305
|
+
): boolean {
|
|
2306
|
+
if (expectation.id !== undefined && message.id !== expectation.id) {
|
|
2307
|
+
return false;
|
|
2308
|
+
}
|
|
2309
|
+
if (expectation.kind !== undefined && message.kind !== expectation.kind) {
|
|
2310
|
+
return false;
|
|
2311
|
+
}
|
|
2312
|
+
if (expectation.name !== undefined && message.name !== expectation.name) {
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2315
|
+
if (
|
|
2316
|
+
expectation.status !== undefined &&
|
|
2317
|
+
message.status !== expectation.status
|
|
2318
|
+
) {
|
|
2319
|
+
return false;
|
|
2320
|
+
}
|
|
2321
|
+
if (
|
|
2322
|
+
expectation.attempts !== undefined &&
|
|
2323
|
+
message.attempts !== expectation.attempts
|
|
2324
|
+
) {
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2327
|
+
if (
|
|
2328
|
+
expectation.maxAttempts !== undefined &&
|
|
2329
|
+
message.maxAttempts !== expectation.maxAttempts
|
|
2330
|
+
) {
|
|
2331
|
+
return false;
|
|
2332
|
+
}
|
|
2333
|
+
if (
|
|
2334
|
+
expectation.deliveredAt !== undefined &&
|
|
2335
|
+
!valueMatches(message.deliveredAt, expectation.deliveredAt)
|
|
2336
|
+
) {
|
|
2337
|
+
return false;
|
|
2338
|
+
}
|
|
2339
|
+
if (
|
|
2340
|
+
expectation.lastError !== undefined &&
|
|
2341
|
+
!valueMatches(message.lastError, expectation.lastError)
|
|
2342
|
+
) {
|
|
2343
|
+
return false;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
return expectation.payload === undefined
|
|
2347
|
+
? true
|
|
2348
|
+
: valueMatches(message.payload, expectation.payload);
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function outboxDrainResultMatches(
|
|
2352
|
+
result: DrainOutboxResult,
|
|
2353
|
+
expectation: OutboxDrainResultExpectation,
|
|
2354
|
+
): boolean {
|
|
2355
|
+
if (
|
|
2356
|
+
expectation.claimed !== undefined &&
|
|
2357
|
+
result.claimed !== expectation.claimed
|
|
2358
|
+
) {
|
|
2359
|
+
return false;
|
|
2360
|
+
}
|
|
2361
|
+
if (
|
|
2362
|
+
expectation.delivered !== undefined &&
|
|
2363
|
+
result.delivered !== expectation.delivered
|
|
2364
|
+
) {
|
|
2365
|
+
return false;
|
|
2366
|
+
}
|
|
2367
|
+
if (
|
|
2368
|
+
expectation.retried !== undefined &&
|
|
2369
|
+
result.retried !== expectation.retried
|
|
2370
|
+
) {
|
|
2371
|
+
return false;
|
|
2372
|
+
}
|
|
2373
|
+
if (
|
|
2374
|
+
expectation.deadLettered !== undefined &&
|
|
2375
|
+
result.deadLettered !== expectation.deadLettered
|
|
2376
|
+
) {
|
|
2377
|
+
return false;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
return true;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function hasStorageTextReader(
|
|
2384
|
+
object: StorageObject,
|
|
2385
|
+
): object is StorageObject & { text(): Promise<string> } {
|
|
2386
|
+
return typeof (object as { text?: unknown }).text === "function";
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function hasStorageBytesReader(
|
|
2390
|
+
object: StorageObject,
|
|
2391
|
+
): object is StorageObject & { bytes(): Promise<Uint8Array> } {
|
|
2392
|
+
return typeof (object as { bytes?: unknown }).bytes === "function";
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
function omitStorageBodyExpectation(
|
|
2396
|
+
expectation: StorageObjectExpectation,
|
|
2397
|
+
): Omit<StorageObjectExpectation, "bytes" | "text"> {
|
|
2398
|
+
const { bytes: _bytes, text: _text, ...metadataExpectation } = expectation;
|
|
2399
|
+
return metadataExpectation;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
function partialObjectMatches(
|
|
2403
|
+
actual: unknown,
|
|
2404
|
+
expected: Record<string, unknown> | undefined,
|
|
2405
|
+
): boolean {
|
|
2406
|
+
if (!expected) return true;
|
|
2407
|
+
if (!actual || typeof actual !== "object") return false;
|
|
2408
|
+
|
|
2409
|
+
const actualRecord = actual as Record<string, unknown>;
|
|
2410
|
+
|
|
2411
|
+
for (const [key, expectedValue] of Object.entries(expected)) {
|
|
2412
|
+
if (!valueMatches(actualRecord[key], expectedValue)) return false;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
return true;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
function valueMatches(actual: unknown, expected: unknown): boolean {
|
|
2419
|
+
if (actual instanceof Date || expected instanceof Date) {
|
|
2420
|
+
return (
|
|
2421
|
+
actual instanceof Date &&
|
|
2422
|
+
expected instanceof Date &&
|
|
2423
|
+
actual.getTime() === expected.getTime()
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
if (actual instanceof Uint8Array || expected instanceof Uint8Array) {
|
|
2428
|
+
return uint8ArrayMatches(actual, expected);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
if (
|
|
2432
|
+
expected &&
|
|
2433
|
+
typeof expected === "object" &&
|
|
2434
|
+
!Array.isArray(expected) &&
|
|
2435
|
+
actual &&
|
|
2436
|
+
typeof actual === "object" &&
|
|
2437
|
+
!Array.isArray(actual)
|
|
2438
|
+
) {
|
|
2439
|
+
return partialObjectMatches(actual, expected as Record<string, unknown>);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
if (Array.isArray(expected) || Array.isArray(actual)) {
|
|
2443
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
return Object.is(actual, expected);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
function uint8ArrayMatches(actual: unknown, expected: unknown): boolean {
|
|
2450
|
+
if (!(actual instanceof Uint8Array) || !(expected instanceof Uint8Array)) {
|
|
2451
|
+
return false;
|
|
2452
|
+
}
|
|
2453
|
+
if (actual.byteLength !== expected.byteLength) return false;
|
|
2454
|
+
|
|
2455
|
+
return actual.every((byte, index) => byte === expected[index]);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
function pluralize(word: string, count: number): string {
|
|
2459
|
+
return count === 1 ? word : `${word}s`;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
function formatTestingExpectation(expectation: unknown): string {
|
|
2463
|
+
return JSON.stringify(expectation);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
function formatAuditExpectation(expectation: AuditLogEntryExpectation): string {
|
|
2467
|
+
return formatTestingExpectation(expectation);
|
|
2468
|
+
}
|