@othree.io/chisel-forge 1.0.1

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/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # @othree.io/chisel-forge
2
+
3
+ Integration testing utilities for the [Chisel](https://www.npmjs.com/package/@othree.io/chisel) CQRS/Event Sourcing framework. Provides a fluent API to send commands against deployed AWS infrastructure and validate triggered events and aggregate state.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @othree.io/chisel-forge
9
+ ```
10
+
11
+ ### Peer Dependencies
12
+
13
+ - `@othree.io/awsome` ^4.0.0
14
+ - `@othree.io/chisel` ^5.0.0
15
+ - `@othree.io/optional` ^2.3.2
16
+ - `vitest` ^3.2.4
17
+
18
+ ## Quick Start~
19
+
20
+ ```typescript
21
+ import { when } from '@othree.io/chisel-forge'
22
+ import { Empty } from '@othree.io/optional'
23
+
24
+ type PersonState = Readonly<{
25
+ name: string
26
+ lastName: string
27
+ }>
28
+
29
+ const configuration = {
30
+ boundedContext: 'Person',
31
+ commandHandlerArn: 'arn:aws:lambda:us-east-1:123456789:function:PersonCommandHandler',
32
+ eventsTable: 'PersonEvents',
33
+ topicArn: 'arn:aws:sns:us-east-1:123456789:PersonEventsTopic',
34
+ testName: 'CreatePerson',
35
+ subscriptionAwaitTime: 3000,
36
+ loadStateLambdaArn: Empty(),
37
+ }
38
+
39
+ it('should create a person', async () => {
40
+ await when<PersonState>(configuration)({
41
+ type: 'CreatePerson',
42
+ body: { name: 'Chuck', lastName: 'Norris' },
43
+ })
44
+ .expectState({ name: 'Chuck', lastName: 'Norris' })
45
+ .expectEvent({
46
+ contextId: expect.any(String),
47
+ type: 'PersonCreated',
48
+ body: { name: 'Chuck', lastName: 'Norris' },
49
+ })
50
+ .toPass()
51
+ }, 30000)
52
+ ```
53
+
54
+ ## Command Handler Modes
55
+
56
+ The `when()` function supports three command delivery modes, determined by the configuration type.
57
+
58
+ ### Synchronous Lambda
59
+
60
+ Invokes the command handler Lambda directly and receives the `CommandResult` in the response.
61
+
62
+ ```typescript
63
+ const configuration = {
64
+ boundedContext: 'Person',
65
+ commandHandlerArn: 'arn:aws:lambda:...:PersonCommandHandler',
66
+ eventsTable: 'PersonEvents',
67
+ topicArn: 'arn:aws:sns:...:PersonEventsTopic',
68
+ testName: 'CreatePerson',
69
+ subscriptionAwaitTime: 3000,
70
+ loadStateLambdaArn: Empty(),
71
+ }
72
+ ```
73
+
74
+ ### Async SQS
75
+
76
+ Sends the command to an SQS queue. The command handler processes it asynchronously.
77
+
78
+ ```typescript
79
+ const configuration = {
80
+ boundedContext: 'Person',
81
+ commandQueueUrl: 'https://sqs.us-east-1.amazonaws.com/.../PersonCommandQueue.fifo',
82
+ commandDLQUrl: 'https://sqs.us-east-1.amazonaws.com/.../PersonCommandDLQ.fifo',
83
+ eventsTable: 'PersonEvents',
84
+ topicArn: 'arn:aws:sns:...:PersonEventsTopic.fifo',
85
+ testName: 'CreatePerson',
86
+ subscriptionAwaitTime: 3000,
87
+ loadStateLambdaArn: Empty(),
88
+ extractMessageGroupId: (command) => command.contextId,
89
+ extractMessageDeduplicationId: (command) => `${command.type}-${command.contextId}`,
90
+ }
91
+ ```
92
+
93
+ ### Async SNS
94
+
95
+ Sends the command to an SNS topic. The command handler subscribes and processes it asynchronously.
96
+
97
+ ```typescript
98
+ const configuration = {
99
+ boundedContext: 'Person',
100
+ commandTopicArn: 'arn:aws:sns:...:PersonCommandTopic.fifo',
101
+ commandDLQUrl: 'https://sqs.us-east-1.amazonaws.com/.../PersonCommandDLQ.fifo',
102
+ eventsTable: 'PersonEvents',
103
+ topicArn: 'arn:aws:sns:...:PersonEventsTopic.fifo',
104
+ testName: 'CreatePerson',
105
+ subscriptionAwaitTime: 3000,
106
+ loadStateLambdaArn: Empty(),
107
+ extractMessageGroupId: (command) => command.contextId,
108
+ extractMessageDeduplicationId: (command) => `${command.type}-${command.contextId}`,
109
+ }
110
+ ```
111
+
112
+ ## Fluent API
113
+
114
+ ### `when<State>(configuration)(command)`
115
+
116
+ Entry point. Accepts a configuration and returns a function that takes a `Command` and returns a test builder.
117
+
118
+ ### `.and(getCommand)`
119
+
120
+ Chains a subsequent command. The `getCommand` callback receives the `contextId` from the first command's result as an `Optional<string>`.
121
+
122
+ ```typescript
123
+ await when<PersonState>(configuration)({
124
+ type: 'CreatePerson',
125
+ body: { name: 'Chuck', lastName: 'Norris' },
126
+ })
127
+ .and((contextId) => ({
128
+ contextId: contextId.get(),
129
+ type: 'UpdatePerson',
130
+ body: { name: 'Bruce', lastName: 'Lee' },
131
+ }))
132
+ .expectState({ name: 'Bruce', lastName: 'Lee' })
133
+ .toPass()
134
+ ```
135
+
136
+ ### `.expectEvent(event)`
137
+
138
+ Asserts that a specific `ChiselEvent` was triggered. Can be called multiple times for multiple expected events.
139
+
140
+ ```typescript
141
+ .expectEvent({
142
+ contextId: expect.any(String),
143
+ type: 'PersonCreated',
144
+ body: { name: 'Chuck', lastName: 'Norris' },
145
+ })
146
+ ```
147
+
148
+ ### `.expectState(state)`
149
+
150
+ Asserts the final aggregate state matches the expected value. When `loadStateLambdaArn` is configured, also validates the state returned by the load-state Lambda.
151
+
152
+ ### `.toPass()`
153
+
154
+ Executes the command(s), validates events and state, and returns the `CommandResult` array. Automatically cleans up temporary SQS queues, subscriptions, and events.
155
+
156
+ ### `.toFail(errorMsg?)`
157
+
158
+ Asserts the command fails with a `CommandExecutionError`. Optionally validates the error message.
159
+
160
+ ```typescript
161
+ const error = await when<PersonState>(configuration)({
162
+ type: 'CreatePerson',
163
+ body: { name: '', lastName: '' },
164
+ }).toFail('Validation failed')
165
+
166
+ expect(error.name).toEqual('CommandExecutionError')
167
+ ```
168
+
169
+ ## AWS Credentials
170
+
171
+ Pass optional AWS credentials as the second argument to `when()`:
172
+
173
+ ```typescript
174
+ import { fromSSO } from '@aws-sdk/credential-providers'
175
+
176
+ const credentials = fromSSO({ profile: 'my-profile' })
177
+
178
+ await when<PersonState>(configuration, credentials)({
179
+ type: 'CreatePerson',
180
+ body: { name: 'Chuck', lastName: 'Norris' },
181
+ }).toPass()
182
+ ```
183
+
184
+ ## Low-Level API
185
+
186
+ For advanced use cases, the low-level functions are available under the `lowLevel` namespace:
187
+
188
+ ```typescript
189
+ import { lowLevel } from '@othree.io/chisel-forge'
190
+
191
+ const {
192
+ when,
193
+ runAndValidate,
194
+ runAndValidateAsync,
195
+ cleanUp,
196
+ createQueueAndSubscribe,
197
+ receiveMessages,
198
+ } = lowLevel
199
+ ```
200
+
201
+ These accept explicit dependency objects, making them fully testable in isolation.
202
+
203
+ ## How It Works
204
+
205
+ 1. Creates a temporary SQS queue and subscribes it to the bounded context's SNS events topic
206
+ 2. Sends the command(s) to the command handler (Lambda, SQS, or SNS depending on config)
207
+ 3. Receives triggered event messages from the temporary queue
208
+ 4. Validates events and state against expectations
209
+ 5. Cleans up: deletes the temporary queue, unsubscribes from the topic, and removes events from the events table
210
+
211
+ ## Configuration Reference
212
+
213
+ | Property | Type | Required | Description |
214
+ |---|---|---|---|
215
+ | `boundedContext` | `string` | All | Bounded context name (matched against event message attributes) |
216
+ | `topicArn` | `string` | All | SNS topic ARN where the bounded context publishes events |
217
+ | `eventsTable` | `string` | All | DynamoDB events table name (for cleanup) |
218
+ | `subscriptionAwaitTime` | `number` | All | Milliseconds to wait after subscribing before sending commands |
219
+ | `testName` | `string` | All | Used to name the temporary SQS queue |
220
+ | `loadStateLambdaArn` | `Optional<string>` | All | Lambda ARN for loading aggregate state (optional validation) |
221
+ | `commandHandlerArn` | `string` | Sync | Lambda ARN for the command handler |
222
+ | `commandQueueUrl` | `string` | Async SQS | SQS queue URL for commands |
223
+ | `commandTopicArn` | `string` | Async SNS | SNS topic ARN for commands |
224
+ | `commandDLQUrl` | `string` | Async | SQS DLQ URL for failed commands |
225
+ | `extractMessageGroupId` | `(command) => string` | Async | FIFO message group ID extractor (optional) |
226
+ | `extractMessageDeduplicationId` | `(command) => string` | Async | FIFO deduplication ID extractor (optional) |
227
+
228
+ ## Scripts
229
+
230
+ ```bash
231
+ npm test # Run tests
232
+ npm run build # Compile TypeScript
233
+ npm run docs # Generate JSDoc documentation
234
+ ```
@@ -0,0 +1,13 @@
1
+ import { TestConfiguration } from './utils';
2
+ import { Command } from '@othree.io/chisel';
3
+ import { AwsCredentialIdentity, Provider } from '@aws-sdk/types';
4
+ export * as lowLevel from './utils';
5
+ /**
6
+ * Initializes a testing environment for Chisel framework.
7
+ * @function
8
+ * @param {ChiselTestConfiguration} configuration - The configuration for the Chisel test.
9
+ * @param {TestConfiguration} [credentials] - The optional AWS credentials.
10
+ * @returns A function that initializes a testing environment and returns a low-level when function.
11
+ */
12
+ export declare const when: <State>(configuration: TestConfiguration, credentials?: AwsCredentialIdentity | Provider<AwsCredentialIdentity>) => (command: Command) => import("./utils").CommandChiselTest<State>;
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAWH,iBAAiB,EAEpB,MAAM,SAAS,CAAA;AAIhB,OAAO,EAAC,OAAO,EAAwC,MAAM,mBAAmB,CAAA;AAGhF,OAAO,EAAC,qBAAqB,EAAE,QAAQ,EAAC,MAAM,gBAAgB,CAAA;AAG9D,OAAO,KAAK,QAAQ,MAAM,SAAS,CAAA;AAEnC;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,EAAE,eAAe,iBAAiB,EAAE,cAAc,qBAAqB,GAAG,QAAQ,CAAC,qBAAqB,CAAC,qEAiIlI,CAAA"}
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.when = exports.lowLevel = void 0;
37
+ const utils_1 = require("./utils");
38
+ const client_lambda_1 = require("@aws-sdk/client-lambda");
39
+ const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
40
+ const awsome_1 = require("@othree.io/awsome");
41
+ const client_sqs_1 = require("@aws-sdk/client-sqs");
42
+ const client_sns_1 = require("@aws-sdk/client-sns");
43
+ const optional_1 = require("@othree.io/optional");
44
+ exports.lowLevel = __importStar(require("./utils"));
45
+ /**
46
+ * Initializes a testing environment for Chisel framework.
47
+ * @function
48
+ * @param {ChiselTestConfiguration} configuration - The configuration for the Chisel test.
49
+ * @param {TestConfiguration} [credentials] - The optional AWS credentials.
50
+ * @returns A function that initializes a testing environment and returns a low-level when function.
51
+ */
52
+ const when = (configuration, credentials) => {
53
+ const sqsClient = new client_sqs_1.SQSClient({
54
+ credentials: credentials
55
+ });
56
+ const createQueueFn = awsome_1.sqs.createQueue({ client: sqsClient });
57
+ const deleteQueueFn = awsome_1.sqs.deleteQueue({ client: sqsClient });
58
+ const snsClient = new client_sns_1.SNSClient({
59
+ credentials: credentials
60
+ });
61
+ const snsConfiguration = {
62
+ BatchSize: 10,
63
+ Parallelism: 10,
64
+ TopicArn: configuration.topicArn
65
+ };
66
+ const subscribeSqsFn = awsome_1.sns.subscribeSqs({ snsClient, sqsClient, configuration: snsConfiguration });
67
+ const unsubscribeFn = awsome_1.sns.unsubscribe({ snsClient });
68
+ const dynamoClient = new client_dynamodb_1.DynamoDBClient({
69
+ credentials: credentials
70
+ });
71
+ const dynamoConfiguration = {
72
+ TableName: configuration.eventsTable
73
+ };
74
+ const deleteByFn = awsome_1.dynamo.deleteBy({ dynamoDb: dynamoClient, configuration: dynamoConfiguration });
75
+ const queryFn = awsome_1.dynamo.query({ dynamoDb: dynamoClient, configuration: dynamoConfiguration });
76
+ const getByFn = awsome_1.dynamo.getBy({ query: queryFn });
77
+ const cleanUpFn = (0, utils_1.cleanUp)({
78
+ getBy: getByFn,
79
+ deleteBy: deleteByFn,
80
+ deleteQueue: deleteQueueFn,
81
+ unsubscribe: unsubscribeFn,
82
+ });
83
+ const loadState = configuration.loadStateLambdaArn.map(lambdaArn => {
84
+ const lambdaClient = new client_lambda_1.LambdaClient({
85
+ credentials: credentials
86
+ });
87
+ return awsome_1.lambda.query({ lambda: lambdaClient, functionName: lambdaArn });
88
+ }).orElse(async (payload) => (0, optional_1.Empty)());
89
+ const createQueueAndSubscribeFn = (0, utils_1.createQueueAndSubscribe)({
90
+ now: () => new Date().getTime(),
91
+ createQueue: createQueueFn,
92
+ cleanUp: cleanUpFn,
93
+ subscribeSqs: subscribeSqsFn,
94
+ });
95
+ const receiveMessagesFn = (0, utils_1.receiveMessages)({
96
+ sqsClient: sqsClient,
97
+ receive: awsome_1.sqs.receiveMessages,
98
+ });
99
+ if ((0, utils_1.isChiselTestConfiguration)(configuration)) {
100
+ const lambdaClient = new client_lambda_1.LambdaClient({
101
+ credentials: credentials
102
+ });
103
+ const commandHandlerClient = awsome_1.lambda.command({ lambda: lambdaClient, functionName: configuration.commandHandlerArn });
104
+ return (0, utils_1.when)((0, utils_1.runAndValidate)({
105
+ sendCommand: commandHandlerClient,
106
+ createQueueAndSubscribe: createQueueAndSubscribeFn,
107
+ receiveMessages: receiveMessagesFn,
108
+ cleanUp: cleanUpFn,
109
+ configuration,
110
+ loadState,
111
+ }));
112
+ }
113
+ else {
114
+ const receiveDLQMessages = awsome_1.sqs.receiveMessages({
115
+ client: sqsClient,
116
+ configuration: {
117
+ QueueUrl: configuration.commandDLQUrl,
118
+ maxNumberOfMessages: 10,
119
+ waitTimeSeconds: 5,
120
+ autoAcknowledgeMessages: true,
121
+ },
122
+ transformMessage: (message) => {
123
+ return JSON.parse((0, optional_1.Optional)(message.Body).orElse('{}'));
124
+ },
125
+ });
126
+ if ((0, utils_1.isAsyncSqsChiselTestConfiguration)(configuration)) {
127
+ const sendCommand = awsome_1.sqs.send({
128
+ client: sqsClient,
129
+ configuration: { QueueUrl: configuration.commandQueueUrl },
130
+ withMessageGroupId: configuration.extractMessageGroupId,
131
+ withDeduplicationId: configuration.extractMessageDeduplicationId,
132
+ });
133
+ return (0, utils_1.when)((0, utils_1.runAndValidateAsync)({
134
+ sendCommand,
135
+ createQueueAndSubscribe: createQueueAndSubscribeFn,
136
+ receiveMessages: receiveMessagesFn,
137
+ cleanUp: cleanUpFn,
138
+ receiveErrorMessages: receiveDLQMessages,
139
+ configuration,
140
+ loadState,
141
+ }));
142
+ }
143
+ else {
144
+ const sendCommand = awsome_1.sns.send({
145
+ client: snsClient,
146
+ configuration: {
147
+ TopicArn: configuration.commandTopicArn,
148
+ Parallelism: 10,
149
+ BatchSize: 10,
150
+ },
151
+ withMessageGroupId: configuration.extractMessageGroupId,
152
+ withMessageDeduplicationId: configuration.extractMessageDeduplicationId,
153
+ });
154
+ return (0, utils_1.when)((0, utils_1.runAndValidateAsync)({
155
+ sendCommand,
156
+ createQueueAndSubscribe: createQueueAndSubscribeFn,
157
+ receiveMessages: receiveMessagesFn,
158
+ cleanUp: cleanUpFn,
159
+ receiveErrorMessages: receiveDLQMessages,
160
+ configuration,
161
+ loadState,
162
+ }));
163
+ }
164
+ }
165
+ };
166
+ exports.when = when;
167
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChiselTestConfiguration,\n cleanUp,\n createQueueAndSubscribe,\n DLQMessage,\n isAsyncSqsChiselTestConfiguration,\n isChiselTestConfiguration,\n LoadStateQuery,\n receiveMessages,\n runAndValidate,\n runAndValidateAsync,\n TestConfiguration,\n when as lowLevelWhen\n} from './utils'\nimport {LambdaClient} from '@aws-sdk/client-lambda'\nimport {DynamoDBClient} from '@aws-sdk/client-dynamodb'\nimport {lambda, sns, sqs, dynamo} from '@othree.io/awsome'\nimport {Command, CommandResult, InternalTriggeredEvent} from '@othree.io/chisel'\nimport {Message, SQSClient} from '@aws-sdk/client-sqs'\nimport {SNSClient} from '@aws-sdk/client-sns'\nimport {AwsCredentialIdentity, Provider} from '@aws-sdk/types'\nimport {Empty, Optional} from '@othree.io/optional'\n\nexport * as lowLevel from './utils'\n\n/**\n * Initializes a testing environment for Chisel framework.\n * @function\n * @param {ChiselTestConfiguration} configuration - The configuration for the Chisel test.\n * @param {TestConfiguration} [credentials] - The optional AWS credentials.\n * @returns A function that initializes a testing environment and returns a low-level when function.\n */\nexport const when = <State>(configuration: TestConfiguration, credentials?: AwsCredentialIdentity | Provider<AwsCredentialIdentity>) => {\n const sqsClient = new SQSClient({\n credentials: credentials\n })\n\n const createQueueFn = sqs.createQueue({client: sqsClient})\n const deleteQueueFn = sqs.deleteQueue({client: sqsClient})\n\n const snsClient = new SNSClient({\n credentials: credentials\n })\n\n const snsConfiguration: sns.SNSConfiguration = {\n BatchSize: 10,\n Parallelism: 10,\n TopicArn: configuration.topicArn\n }\n\n const subscribeSqsFn = sns.subscribeSqs({snsClient, sqsClient, configuration: snsConfiguration})\n\n const unsubscribeFn = sns.unsubscribe({snsClient})\n\n const dynamoClient = new DynamoDBClient({\n credentials: credentials\n })\n\n const dynamoConfiguration: dynamo.DynamoConfiguration = {\n TableName: configuration.eventsTable\n }\n\n const deleteByFn = dynamo.deleteBy({dynamoDb: dynamoClient, configuration: dynamoConfiguration})\n\n const queryFn = dynamo.query<InternalTriggeredEvent>({dynamoDb: dynamoClient, configuration: dynamoConfiguration})\n const getByFn = dynamo.getBy<InternalTriggeredEvent>({query: queryFn})\n\n const cleanUpFn = cleanUp({\n getBy: getByFn,\n deleteBy: deleteByFn,\n deleteQueue: deleteQueueFn,\n unsubscribe: unsubscribeFn,\n })\n\n const loadState = configuration.loadStateLambdaArn.map(lambdaArn => {\n const lambdaClient = new LambdaClient({\n credentials: credentials\n })\n return lambda.query<LoadStateQuery, State>({lambda: lambdaClient, functionName: lambdaArn})\n }).orElse(async (payload: LoadStateQuery) => Empty())\n\n const createQueueAndSubscribeFn = createQueueAndSubscribe({\n now: () => new Date().getTime(),\n createQueue: createQueueFn,\n cleanUp: cleanUpFn,\n subscribeSqs: subscribeSqsFn,\n })\n const receiveMessagesFn = receiveMessages({\n sqsClient: sqsClient,\n receive: sqs.receiveMessages,\n })\n\n if (isChiselTestConfiguration(configuration)) {\n const lambdaClient = new LambdaClient({\n credentials: credentials\n })\n const commandHandlerClient = lambda.command<Command, CommandResult<State>>({lambda: lambdaClient, functionName: configuration.commandHandlerArn})\n\n return lowLevelWhen(runAndValidate<State>({\n sendCommand: commandHandlerClient,\n createQueueAndSubscribe: createQueueAndSubscribeFn,\n receiveMessages: receiveMessagesFn,\n cleanUp: cleanUpFn,\n configuration,\n loadState,\n }))\n } else {\n const receiveDLQMessages = sqs.receiveMessages<DLQMessage>({\n client: sqsClient,\n configuration: {\n QueueUrl: configuration.commandDLQUrl,\n maxNumberOfMessages: 10,\n waitTimeSeconds: 5,\n autoAcknowledgeMessages: true,\n },\n transformMessage: (message: Message) => {\n return JSON.parse(Optional(message.Body).orElse('{}')) as DLQMessage\n },\n })\n\n if (isAsyncSqsChiselTestConfiguration(configuration)) {\n const sendCommand = sqs.send<Command>({\n client: sqsClient,\n configuration: {QueueUrl: configuration.commandQueueUrl},\n withMessageGroupId: configuration.extractMessageGroupId,\n withDeduplicationId: configuration.extractMessageDeduplicationId,\n })\n\n return lowLevelWhen(runAndValidateAsync<State>({\n sendCommand,\n createQueueAndSubscribe: createQueueAndSubscribeFn,\n receiveMessages: receiveMessagesFn,\n cleanUp: cleanUpFn,\n receiveErrorMessages: receiveDLQMessages,\n configuration,\n loadState,\n }))\n } else {\n const sendCommand = sns.send<Command>({\n client: snsClient,\n configuration: {\n TopicArn: configuration.commandTopicArn,\n Parallelism: 10,\n BatchSize: 10,\n },\n withMessageGroupId: configuration.extractMessageGroupId,\n withMessageDeduplicationId: configuration.extractMessageDeduplicationId,\n })\n\n return lowLevelWhen(runAndValidateAsync<State>({\n sendCommand,\n createQueueAndSubscribe: createQueueAndSubscribeFn,\n receiveMessages: receiveMessagesFn,\n cleanUp: cleanUpFn,\n receiveErrorMessages: receiveDLQMessages,\n configuration,\n loadState,\n }))\n }\n }\n\n}\n"]}
@@ -0,0 +1,233 @@
1
+ import { Command, CommandResult, InternalTriggeredEvent, ChiselEvent } from '@othree.io/chisel';
2
+ import { Optional } from '@othree.io/optional';
3
+ import { sqs, sns, dynamo } from '@othree.io/awsome';
4
+ import { Message, SQSClient } from '@aws-sdk/client-sqs';
5
+ /**
6
+ * Custom error class for representing errors that occur during command execution.
7
+ * @class
8
+ * @extends {Error}
9
+ */
10
+ export declare class CommandExecutionError extends Error {
11
+ static ERROR: string;
12
+ command: Command;
13
+ constructor(command: Command, message?: string);
14
+ }
15
+ /**
16
+ * Represents an error message related to a command placed in a dead-letter queue (DLQ).
17
+ * @typedef DLQMessage
18
+ */
19
+ export type DLQMessage = Readonly<{
20
+ request: Command;
21
+ error: Readonly<{
22
+ name: string;
23
+ message: string;
24
+ stack?: string;
25
+ }>;
26
+ }>;
27
+ /**
28
+ * Represents a query for loading the state using a context identifier.
29
+ * @typedef LoadStateQuery
30
+ */
31
+ export type LoadStateQuery = Readonly<{
32
+ contextId: string;
33
+ }>;
34
+ /**
35
+ * Represents a Chisel test builder that configures expected events and states for a command.
36
+ * @typedef ChiselTest
37
+ * @template State - The type representing the state.
38
+ */
39
+ export type ChiselTest<State> = Readonly<{
40
+ /**
41
+ * Specifies an expected event that should be produced by the command.
42
+ * @param {ChiselEvent} event - The expected event.
43
+ * @returns {ChiselTest<State>} The ChiselTest instance with the expected event.
44
+ */
45
+ expectEvent: (event: ChiselEvent) => ChiselTest<State>;
46
+ /**
47
+ * Specifies an expected state that the system should be in after the command execution.
48
+ * @param {State} state - The expected state.
49
+ * @returns {ChiselTest<State>} The ChiselTest instance with the expected state.
50
+ */
51
+ expectState: (state: State) => ChiselTest<State>;
52
+ /**
53
+ * Executes the command and validates the expected results.
54
+ * @returns {Promise<CommandResult<State>>} A promise that resolves to the command(s) results.
55
+ */
56
+ toPass: () => Promise<Array<CommandResult<State>>>;
57
+ }>;
58
+ export type GetCommand = (contextId: Optional<string>) => Command;
59
+ /**
60
+ * A specialized extension of the `ChiselTest` type tailored for testing command execution.
61
+ * @typedef CommandChiselTest
62
+ * @template State - The type representing the state used in the test.
63
+ */
64
+ export type CommandChiselTest<State> = Readonly<{
65
+ and: (getCommand: GetCommand) => CommandChiselTest<State>;
66
+ toFail: (errorMsg?: string) => Promise<Error>;
67
+ }> & ChiselTest<State>;
68
+ /**
69
+ * Creates a Chisel test builder based on a given run and validate function.
70
+ * @template State - The type representing the state.
71
+ * @param {RunAndValidate<State>} runAndValidate - The function to run and validate a command.
72
+ * @returns A function that accepts a command and returns a ChiselTest instance.
73
+ */
74
+ export declare const when: <State>(runAndValidate: RunAndValidate<State>) => (command: Command) => CommandChiselTest<State>;
75
+ /**
76
+ * Represents the configuration for all types of tests
77
+ * @typedef BaseTestConfiguration
78
+ */
79
+ export type BaseTestConfiguration = Readonly<{
80
+ boundedContext: string;
81
+ topicArn: string;
82
+ eventsTable: string;
83
+ subscriptionAwaitTime: number;
84
+ testName: string;
85
+ loadStateLambdaArn: Optional<string>;
86
+ }>;
87
+ /**
88
+ * Configuration options for a Chisel test.
89
+ * @typedef ChiselTestConfiguration
90
+ */
91
+ export type ChiselTestConfiguration = Readonly<{
92
+ commandHandlerArn: string;
93
+ }> & BaseTestConfiguration;
94
+ /**
95
+ * Configuration options for an Async SQS Chisel test.
96
+ * @typedef AsyncSqsChiselTestConfiguration
97
+ */
98
+ export type AsyncSqsChiselTestConfiguration = Readonly<{
99
+ commandQueueUrl: string;
100
+ extractMessageGroupId?: (command: Command) => string;
101
+ extractMessageDeduplicationId?: (command: Command) => string;
102
+ commandDLQUrl: string;
103
+ }> & BaseTestConfiguration;
104
+ /**
105
+ * Configuration options for an Async SNS Chisel test.
106
+ * @typedef AsyncSnsChiselTestConfiguration
107
+ */
108
+ export type AsyncSnsChiselTestConfiguration = Readonly<{
109
+ commandTopicArn: string;
110
+ extractMessageGroupId?: (command: Command) => string;
111
+ extractMessageDeduplicationId?: (command: Command) => string;
112
+ commandDLQUrl: string;
113
+ }> & BaseTestConfiguration;
114
+ /**
115
+ * Represents the possible configurations for different types of tests.
116
+ * @typedef {ChiselTestConfiguration | AsyncSqsChiselTestConfiguration | AsyncSnsChiselTestConfiguration} TestConfiguration
117
+ */
118
+ export type TestConfiguration = ChiselTestConfiguration | AsyncSqsChiselTestConfiguration | AsyncSnsChiselTestConfiguration;
119
+ /**
120
+ * Determines if the configuration is an ChiselTestConfiguration
121
+ * @param configuration Test configuration
122
+ * @returns {boolean} True is configuration is of type ChiselTestConfiguration
123
+ */
124
+ export declare const isChiselTestConfiguration: (configuration: TestConfiguration) => configuration is ChiselTestConfiguration;
125
+ /**
126
+ * Determines if the configuration is an AsyncSqsChiselTestConfiguration
127
+ * @param configuration Test configuration
128
+ * @returns {boolean} True is configuration is of type AsyncSqsChiselTestConfiguration
129
+ */
130
+ export declare const isAsyncSqsChiselTestConfiguration: (configuration: TestConfiguration) => configuration is AsyncSqsChiselTestConfiguration;
131
+ /**
132
+ * Represents a Chisel event message containing attributes, an event, and a state.
133
+ * @typedef ChiselEventMessage
134
+ * @template State - The type representing the state.
135
+ */
136
+ export type ChiselEventMessage<State> = Readonly<{
137
+ messageAttributes: Readonly<{
138
+ bc: string;
139
+ eventType: string;
140
+ }>;
141
+ event: InternalTriggeredEvent;
142
+ state: State;
143
+ }>;
144
+ /**
145
+ * Function type for cleanup operations including deleting a queue, events, and data.
146
+ * @typedef {Function} CleanUp
147
+ */
148
+ export type CleanUp = <State>(queue: SubscribedSqsQueue | sqs.SqsQueue, maybeCommandResults: Optional<Array<CommandResult<State>>>) => Promise<true>;
149
+ /**
150
+ * Performs cleanup operations including deleting a queue, events, and data.
151
+ * @function
152
+ * @param deps - Dependencies for cleanup operations.
153
+ * @returns {CleanUp} A function that performs cleanup operations.
154
+ */
155
+ export declare const cleanUp: (deps: {
156
+ getBy: (keys: dynamo.Keys) => Promise<Optional<Array<InternalTriggeredEvent>>>;
157
+ deleteBy: (keys: dynamo.Keys) => Promise<Optional<boolean>>;
158
+ deleteQueue: (request: sqs.DeleteQueueRequest) => Promise<Optional<boolean>>;
159
+ unsubscribe: (subscription: sns.Subscription) => Promise<Optional<true>>;
160
+ }) => CleanUp;
161
+ /**
162
+ * Representation of a queue subscribed to a topic
163
+ * @typedef SubscribedSqsQueue
164
+ */
165
+ export type SubscribedSqsQueue = Readonly<{
166
+ subscriptionArn: string;
167
+ }> & sqs.SqsQueue;
168
+ /**
169
+ * Creates and subscribes to an SQS queue.
170
+ * @typedef {Function} CreateQueueAndSubscribe
171
+ */
172
+ export type CreateQueueAndSubscribe = (configuration: TestConfiguration) => Promise<SubscribedSqsQueue | sqs.SqsQueue>;
173
+ /**
174
+ * Creates and subscribes to an SQS queue.
175
+ * @function
176
+ * @param deps - Dependencies for queue creation and subscription.
177
+ * @returns {CreateQueueAndSubscribe} A function that creates and subscribes to an SQS queue.
178
+ */
179
+ export declare const createQueueAndSubscribe: (deps: {
180
+ now: () => number;
181
+ createQueue: (request: sqs.CreateQueueRequest) => Promise<Optional<sqs.SqsQueue>>;
182
+ cleanUp: CleanUp;
183
+ subscribeSqs: (request: sns.SubscribeQueueRequest) => Promise<Optional<sns.Subscription>>;
184
+ }) => CreateQueueAndSubscribe;
185
+ /**
186
+ * Asynchronous function for receiving messages from an SQS queue.
187
+ * @function
188
+ * @param deps - Dependencies for receiving messages.
189
+ * @returns A function that receives messages from a specified queue URL.
190
+ */
191
+ export declare const receiveMessages: (deps: {
192
+ sqsClient: SQSClient;
193
+ receive: <T>(deps: Readonly<{
194
+ client: SQSClient;
195
+ configuration: sqs.ReceiveMessagesConfiguration;
196
+ transformMessage: (message: Message) => T;
197
+ }>) => () => Promise<Optional<Array<T>>>;
198
+ }) => (queueUrl: string) => Promise<Array<Message>>;
199
+ /**
200
+ * A function that runs a command, validates the results, and performs cleanup operations.
201
+ * @typedef {Function} RunAndValidate
202
+ */
203
+ export type RunAndValidate<State> = (command: Command, getCommands: Array<GetCommand>, maybeExpectedState: Optional<State>, expectedEvents: Array<ChiselEvent>) => Promise<Array<CommandResult<State>>>;
204
+ /**
205
+ * Runs a command, validates the results, and performs cleanup operations.
206
+ * @function
207
+ * @param deps - Dependencies for running and validating commands.
208
+ * @returns {RunAndValidate<State>} The run and validate function.
209
+ */
210
+ export declare const runAndValidate: <State>(deps: {
211
+ sendCommand: (command: Command) => Promise<Optional<CommandResult<State>>>;
212
+ createQueueAndSubscribe: CreateQueueAndSubscribe;
213
+ receiveMessages: (queueUrl: string) => Promise<Array<Message>>;
214
+ cleanUp: CleanUp;
215
+ configuration: ChiselTestConfiguration;
216
+ loadState: (query: LoadStateQuery) => Promise<Optional<State>>;
217
+ }) => RunAndValidate<State>;
218
+ /**
219
+ * Sends a command to a queue and validates the command events and state.
220
+ * @function
221
+ * @param deps - Dependencies for running and validating async commands.
222
+ * @returns {RunAndValidate<State>} The run and validate function.
223
+ */
224
+ export declare const runAndValidateAsync: <State>(deps: {
225
+ sendCommand: (command: Command) => Promise<Optional<boolean>>;
226
+ createQueueAndSubscribe: CreateQueueAndSubscribe;
227
+ receiveMessages: (queueUrl: string) => Promise<Array<Message>>;
228
+ cleanUp: CleanUp;
229
+ receiveErrorMessages: () => Promise<Optional<Array<DLQMessage>>>;
230
+ configuration: AsyncSqsChiselTestConfiguration | AsyncSnsChiselTestConfiguration;
231
+ loadState: (query: LoadStateQuery) => Promise<Optional<State>>;
232
+ }) => RunAndValidate<State>;
233
+ //# sourceMappingURL=utils.d.ts.map