├── .all-contributorsrc
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── .release-it.json
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── __tests__
└── eventbridge.test.ts
├── babel.config.js
├── img
└── logo.png
├── jest.config.js
├── jest.setup.js
├── package-lock.json
├── package.json
├── src
├── assertions
│ ├── index.ts
│ ├── toContainItemWithValues
│ │ └── index.ts
│ ├── toContainUser
│ │ └── index.ts
│ ├── toExistAsS3Bucket
│ │ └── index.ts
│ ├── toExistInDynamoTable
│ │ └── index.ts
│ ├── toHaveCompletedExecutionWithStatus
│ │ └── index.ts
│ ├── toHaveContentEqualTo
│ │ └── index.ts
│ ├── toHaveContentTypeEqualTo
│ │ └── index.ts
│ ├── toHaveEvent
│ │ └── index.ts
│ ├── toHaveEventWithSource
│ │ └── index.ts
│ ├── toHaveObjectWithNameEqualTo
│ │ └── index.ts
│ ├── toMatchStateMachineOutput
│ │ └── index.ts
│ └── utils
│ │ ├── errorTypesCheckers.ts
│ │ ├── globalTypeChecker.ts
│ │ └── index.ts
├── helpers
│ ├── cognito.ts
│ ├── eventBridge.ts
│ ├── general.ts
│ ├── index.ts
│ ├── stepFunctions.ts
│ └── utils
│ │ ├── loadArg.ts
│ │ └── removeUndefinedMessages.ts
├── index.ts
└── utils
│ └── testResult.ts
├── tsconfig.json
├── yarn-error.log
└── yarn.lock
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "BenEllerby",
10 | "name": "Ben Ellerby",
11 | "avatar_url": "https://avatars1.githubusercontent.com/u/11080984?v=4",
12 | "profile": "https://medium.com/serverless-transformation",
13 | "contributions": [
14 | "code",
15 | "doc",
16 | "ideas",
17 | "design",
18 | "review"
19 | ]
20 | },
21 | {
22 | "login": "hamilton-s",
23 | "name": "Sarah Hamilton",
24 | "avatar_url": "https://avatars.githubusercontent.com/hamilton-s",
25 | "profile": "https://github.com/hamilton-s",
26 | "contributions": [
27 | "code",
28 | "doc"
29 | ]
30 | },
31 | {
32 | "login": "aghwi",
33 | "name": "Alexander White",
34 | "avatar_url": "https://avatars.githubusercontent.com/agwhi",
35 | "profile": "https://github.com/agwhi",
36 | "contributions": [
37 | "code"
38 | ]
39 | },
40 | {
41 | "login": "joelhamiltondev",
42 | "name": "Joel Hamilton",
43 | "avatar_url": "https://avatars.githubusercontent.com/joelhamiltondev",
44 | "profile": "https://github.com/joelhamiltondev",
45 | "contributions": [
46 | "code"
47 | ]
48 | }
49 | ],
50 | "contributorsPerLine": 7,
51 | "projectName": "sls-test-tools",
52 | "projectOwner": "BenEllerby",
53 | "repoType": "github",
54 | "repoHost": "https://github.com",
55 | "skipCi": true,
56 | "commitConvention": "none"
57 | }
58 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | "eslint:recommended",
4 | "plugin:import/recommended",
5 | "plugin:prettier/recommended",
6 | ],
7 | rules: {
8 | curly: ["error", "all"],
9 | eqeqeq: ["error", "smart"],
10 | "import/no-duplicates": "off",
11 | "import/no-extraneous-dependencies": [
12 | "error",
13 | {
14 | devDependencies: true,
15 | optionalDependencies: false,
16 | peerDependencies: false,
17 | },
18 | ],
19 | "no-shadow": [
20 | "error",
21 | {
22 | hoist: "all",
23 | },
24 | ],
25 | "prefer-const": "error",
26 | "sort-imports": [
27 | "error",
28 | {
29 | ignoreCase: true,
30 | ignoreDeclarationSort: true,
31 | ignoreMemberSort: false,
32 | memberSyntaxSortOrder: ["none", "all", "multiple", "single"],
33 | },
34 | ],
35 | "padding-line-between-statements": [
36 | "error",
37 | {
38 | blankLine: "always",
39 | prev: "*",
40 | next: "return",
41 | },
42 | ],
43 | complexity: ["error", 8],
44 | "max-lines": ["error", 300],
45 | "max-depth": ["error", 3],
46 | "max-params": ["error", 2],
47 | },
48 |
49 | root: true,
50 | plugins: ["import"],
51 | env: {
52 | es6: true,
53 | node: true,
54 | },
55 | parserOptions: { ecmaVersion: 2021 },
56 | overrides: [
57 | {
58 | files: ["**/*.ts?(x)"],
59 | settings: { "import/resolver": { typescript: {} } },
60 | extends: [
61 | "plugin:@typescript-eslint/recommended",
62 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
63 | "plugin:import/typescript",
64 | "plugin:prettier/recommended",
65 | ],
66 | parser: "@typescript-eslint/parser",
67 | parserOptions: {
68 | project: "tsconfig.json",
69 | },
70 | rules: {
71 | "@typescript-eslint/explicit-module-boundary-types": "error",
72 | "@typescript-eslint/prefer-optional-chain": "error",
73 | "no-shadow": "off",
74 | "@typescript-eslint/no-shadow": "error",
75 | "@typescript-eslint/prefer-nullish-coalescing": "error",
76 | "@typescript-eslint/strict-boolean-expressions": "error",
77 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
78 | "@typescript-eslint/no-unnecessary-condition": "error",
79 | "@typescript-eslint/no-unnecessary-type-arguments": "error",
80 | "no-unused-vars": "off",
81 | "@typescript-eslint/no-unused-vars": [
82 | "error",
83 | { argsIgnorePattern: "^_$", varsIgnorePattern: "^_$" },
84 | ],
85 | "@typescript-eslint/prefer-string-starts-ends-with": "error",
86 | "@typescript-eslint/switch-exhaustiveness-check": "error",
87 | "@typescript-eslint/ban-ts-comment": [
88 | "error",
89 | {
90 | "ts-ignore": "allow-with-description",
91 | minimumDescriptionLength: 10,
92 | },
93 | ],
94 | },
95 | },
96 | ],
97 | };
98 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | build
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "auto"
3 | }
4 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "github": {
3 | "release": true
4 | },
5 | "hooks": {
6 | "after:my-plugin:bump": "./bin/my-script.sh",
7 | "after:bump": "yarn build",
8 | "after:git:release": "echo After git push, before github release",
9 | "after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.codeActionsOnSave": {
4 | "source.fixAll.eslint": true
5 | },
6 | "eslint.validate": ["javascript"]
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Theodo UK
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
sls-test-tools
3 |

4 |
5 | Custom Jest Assertions for Serverless Projects
6 |
7 |
8 |
9 |
10 |
11 | `sls-test-tools` provides a range of utilities, setup, teardown and assertions to make it easier to write effective and high quality integration tests for Serverless Architectures on AWS.
12 |
13 | **🚧 This is in an alpha state while we trial a few initial assertions and get feedback on the approach and structure. 🚧**
14 |
15 | **⚠️ AWS resources will be created (SQS Queue, EventBridge Rule...) if the EventBridge module is used. Although there is clear setup and teardown we do not advise running this on production environments currently. ⚠️**
16 |
17 | ## Installation
18 |
19 | With npm:
20 |
21 | ```sh
22 | npm install --save-dev sls-test-tools
23 | ```
24 |
25 | With yarn:
26 |
27 | ```sh
28 | yarn add -D sls-test-tools
29 | ```
30 |
31 | ## Maintenance
32 |
33 | sls-test-tools is currently being actively maintained, yet is in alpha. Your feedback is very welcome.
34 |
35 | ## Assertions:
36 |
37 | ### EventBridge
38 |
39 | ```
40 | expect(eventBridgeEvents).toHaveEvent();
41 |
42 | expect(eventBridgeEvents).toHaveEventWithSource("order.created");
43 | ```
44 |
45 | ### S3
46 |
47 | Note: these async assertions require "await"
48 |
49 | ```
50 | await expect("BUCKET NAME").toHaveS3ObjectWithNameEqualTo("FILE NAME");
51 | ```
52 |
53 | ```
54 | await expect("BUCKET NAME").toExistAsS3Bucket();
55 | ```
56 |
57 | ```
58 | await expect({
59 | bucketName: "BUCKET_NAME",
60 | objectName: "FILE NAME",
61 | }).toHaveContentTypeEqualTo("CONTENT_TYPE");;
62 | ```
63 |
64 | where CONTENT_TYPE are [standards MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)
65 |
66 | ```
67 | await expect({
68 | bucketName: "BUCKET_NAME",
69 | objectName: "FILE NAME",
70 | }).toHaveContentEqualTo("CONTENT");
71 | ```
72 |
73 | ### Step Functions
74 |
75 | Note: these assertions also require "await"
76 |
77 | ```
78 | await expect("STATE_MACHINE_NAME").toHaveCompletedExecutionWithStatus("STATUS");
79 | await expect("STATE_MACHINE_NAME").toMatchStateMachineOutput({EXPECTED_OUTPUT}));
80 | ```
81 |
82 | ### DynamoDB
83 |
84 | Note: these assertions also require "await"
85 |
86 | ```
87 | await expect("TABLENAME").toContainItemWithValues({[field]: value});
88 | await expect({PK: pk,
89 | SK: sk,
90 | }).toExistInDynamoTable('TABLENAME');
91 | ```
92 |
93 | ### Cognito
94 |
95 | Note: this assertion also requires "await"
96 |
97 | ```
98 | await expect('USER_POOL_ID').toContainUser('USERNAME');
99 | ```
100 |
101 | ## Helpers
102 |
103 | ### General
104 |
105 | AWSClient - An AWS client with credentials set up
106 |
107 | ```
108 | getStackResources(stackName) - get information about a stack
109 | getOptions() - get options for making requests to AWS
110 | ```
111 |
112 | ### EventBridge
113 |
114 | An interface to the deployed EventBridge, allowing events to be injected and intercepted via an SQS queue and EventBridge rule.
115 |
116 | #### Static
117 |
118 | ```
119 | EventBridge.build(busName) - create a EventBridge instance to allow events to be injected and intercepted
120 | ```
121 |
122 | #### Instance
123 |
124 | ```
125 | eventBridge.publishEvent(source, detailType, detail, clear?) - publish an event to the bus
126 | eventBridge.getEvents(clear?) - get the events that have been sent to the bus
127 | eventBridge.clear() - clear old messages
128 | eventBridge.destroy() - remove infastructure used to track events
129 | ```
130 |
131 | ### Step Functions
132 |
133 | An interface to a deployed Step Function, with a function to execute a Step Function until its completion.
134 |
135 | #### Static
136 |
137 | ```
138 | StepFunctions.build() // create a Step Functions Client for executing existing state machines
139 | ```
140 |
141 | #### Instance
142 |
143 | ```
144 | stepFunctions.runExecution(stateMachineName, input) // executes state machine until completion
145 | ```
146 |
147 | ### Cognito
148 |
149 | ```
150 | await createAuthenticatedUser({
151 | clientId: "CLIENT_ID",
152 | userPoolId: "USER_POOL_ID",
153 | standardAttributes: ["email", "middle_name"], // works for all cognito standard user attributes
154 | customAttributes: [{hello: "string"}], // only works for customAttributes which have been explicitly defined in the user pool schema
155 | });
156 |
157 | await createUnauthenticatedUser({
158 | clientId: "CLIENT_ID",
159 | userPoolId: "USER_POOL_ID",
160 | confirmed: true,
161 | standardAttributes: ["email", "middle_name", "address", "birthdate"], // works for all cognito standard user attributes
162 | customAttributes: [{hello: "string"}], // only works for customAttributes which have been explicitly defined in the user pool schema
163 | });
164 | ```
165 |
166 | ## Running with `jest`
167 |
168 | ### Arguments
169 |
170 | - When running tests with `jest` using `sls-test-tools` matchers there are certain parameters needed for `sls-test-tools` to make assertions.
171 | - These are passed either as command line arguments, using quotation to match `jests` convention on test arguments, or by using environment variables. CLI arguments override environment variables.
172 |
173 | **Required**
174 |
175 | - `'--stack=my-service-dev'` or `process.env.CFN_STACK_NAME` - the CloudFormation stack name of the stack under test.
176 |
177 | **Optional**
178 |
179 | - `'--profile=[PROFILE NAME]'` or `process.env.AWS_PROFILE` (will default to `default`)
180 | - `'--region=[AWS Region]'` or `process.env.AWS_REGION` (will default to `eu-west-2`)
181 | - `'--keep=true'` or `process.env.SLS_TEST_TOOLS_KEEP` (will default to `false`) - keeps testing resources up to avoid creation throttles (e.g. SQS Queue created for EventBridge assertions)
182 | - `'--event-rule-name=[Event Bus Rule Name]'` - Custom Event bridge Rule name (will deafult to `test-${eventBridgeName}-rule`)
183 | - `'--queue-name=[SQS Queue Name]'` - Custom SQS Queue name (will deafult to `${eventBridgeName}-testing-queue`)
184 |
185 | - To avoid issues we recommend `--runInBand`
186 |
187 | ```
188 | import { AWSClient, EventBridge } from "sls-test-tools";
189 |
190 | const lambda = new AWSClient.Lambda()
191 | let eventBridge;
192 | const s3 = new AWSClient.S3()
193 |
194 | describe("Integration Testing Event Bridge", () => {
195 | beforeAll(async () => {
196 | eventBridge = await EventBridge.build("event-bridge")
197 | });
198 |
199 | afterAll(async () => {
200 | await eventBridge.destroy()
201 | });
202 |
203 | it("correctly publishes an event to the event bus when the lambda is invoked", async () => {
204 | const event = {
205 | body: JSON.stringify({
206 | filename: filename,
207 | }),
208 | };
209 |
210 | // Invoke Lambda Function
211 | const params = {
212 | FunctionName: "event-bridge-example-dev-service1",
213 | Payload: JSON.stringify(event),
214 | };
215 | await lambda.invoke(params).promise();
216 |
217 | const eventBridgeEvents = await eventBridge.getEvents()
218 | expect(eventBridgeEvents).toHaveEvent();
219 | expect(eventBridgeEvents).toHaveEventWithSource("order.created");
220 | });
221 |
222 | it("correctly generates a PDF when an order is created", async () => {
223 | const bucketName = example-bucket
224 | await eventBridge
225 | .publishEvent("order.created", "example", JSON.stringify({ filename: filename }));
226 |
227 | await sleep(5000); // wait 5 seconds to allow event to pass
228 |
229 | const params = {
230 | Bucket: bucketName,
231 | Key: filename,
232 | };
233 |
234 | // Assert that file was added to the S3 bucket
235 | await expect("example-dev-thumbnails-bucket").toHaveS3ObjectWithNameEqualTo(
236 | filename
237 | );
238 | });
239 | ```
240 |
241 | ## Contributors ✨
242 |
243 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
244 |
245 |
246 |
247 |
248 |
256 |
257 |
258 |
259 |
260 |
261 |
262 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
263 |
264 |
265 |
sls-test-tools
266 |

267 |
268 | Custom Jest Assertions for Serverless Projects
269 |
270 |
271 |
272 |
273 |
274 | `sls-test-tools` provides a range of utilities, setup, teardown and assertions to make it easier to write effective and high quality integration tests for Serverless Architectures on AWS.
275 |
276 | **🚧 This is in an alpha state while we trial a few initial assertions and get feedback on the approach and structure. 🚧**
277 |
278 | **⚠️ AWS resources will be created (SQS Queue, EventBridge Rule...) if the EventBridge module is used. Although there is clear setup and teardown we do not advise running this on production environments currently. ⚠️**
279 |
280 | ## Installation
281 |
282 | With npm:
283 |
284 | ```sh
285 | npm install --save-dev sls-test-tools
286 | ```
287 |
288 | With yarn:
289 |
290 | ```sh
291 | yarn add -D sls-test-tools
292 | ```
293 |
294 | ## Maintenance
295 |
296 | sls-test-tools is currently being actively maintained, yet is in alpha. Your feedback is very welcome.
297 |
298 | ## Assertions:
299 |
300 | ### EventBridge
301 |
302 | ```
303 | expect(eventBridgeEvents).toHaveEvent();
304 |
305 | expect(eventBridgeEvents).toHaveEventWithSource("order.created");
306 | ```
307 |
308 | ### S3
309 |
310 | Note: these async assertions require "await"
311 |
312 | ```
313 | await expect("BUCKET NAME").toHaveS3ObjectWithNameEqualTo("FILE NAME");
314 | ```
315 |
316 | ```
317 | await expect("BUCKET NAME").toExistAsS3Bucket();
318 | ```
319 |
320 | ```
321 | await expect({
322 | bucketName: "BUCKET_NAME",
323 | objectName: "FILE NAME",
324 | }).toHaveContentTypeEqualTo("CONTENT_TYPE");;
325 | ```
326 |
327 | where CONTENT_TYPE are [standards MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)
328 |
329 | ```
330 | await expect({
331 | bucketName: "BUCKET_NAME",
332 | objectName: "FILE NAME",
333 | }).toHaveContentEqualTo("CONTENT");
334 | ```
335 |
336 | ### Step Functions
337 |
338 | Note: these assertions also require "await"
339 |
340 | ```
341 | await expect("STATE_MACHINE_NAME").toHaveCompletedExecutionWithStatus("STATUS");
342 | await expect("STATE_MACHINE_NAME").toMatchStateMachineOutput({EXPECTED_OUTPUT}));
343 | ```
344 |
345 | ### DynamoDB
346 |
347 | Note: these assertions also require "await"
348 |
349 | ```
350 | await expect("TABLENAME").toContainItemWithValues({[field]: value});
351 | await expect({PK: pk,
352 | SK: sk,
353 | }).toExistInDynamoTable('TABLENAME');
354 | ```
355 |
356 | ### Cognito
357 |
358 | Note: this assertion also requires "await"
359 |
360 | ```
361 | await expect('USER_POOL_ID').toContainUser('USERNAME');
362 | ```
363 |
364 | ## Helpers
365 |
366 | ### General
367 |
368 | AWSClient - An AWS client with credentials set up
369 |
370 | ```
371 | getStackResources(stackName) - get information about a stack
372 | getOptions() - get options for making requests to AWS
373 | ```
374 |
375 | ### EventBridge
376 |
377 | An interface to the deployed EventBridge, allowing events to be injected and intercepted via an SQS queue and EventBridge rule.
378 |
379 | #### Static
380 |
381 | ```
382 | EventBridge.build(busName) - create a EventBridge instance to allow events to be injected and intercepted
383 | ```
384 |
385 | #### Instance
386 |
387 | ```
388 | eventBridge.publishEvent(source, detailType, detail) - publish an event to the bus
389 | eventBridge.getEvents() - get the events that have been sent to the bus
390 | eventBridge.clear() - clear old messages
391 | eventBridge.destroy() - remove infastructure used to track events
392 | ```
393 |
394 | ### Step Functions
395 |
396 | An interface to a deployed Step Function, with a function to execute a Step Function until its completion.
397 |
398 | #### Static
399 |
400 | ```
401 | StepFunctions.build() // create a Step Functions Client for executing existing state machines
402 | ```
403 |
404 | #### Instance
405 |
406 | ```
407 | stepFunctions.runExecution(stateMachineName, input) // executes state machine until completion
408 | ```
409 |
410 | ### Cognito
411 |
412 | ```
413 | await createAuthenticatedUser({
414 | clientId: "CLIENT_ID",
415 | userPoolId: "USER_POOL_ID",
416 | standardAttributes: ["email", "middle_name"], // works for all cognito standard user attributes
417 | customAttributes: ["hello"], // only works for customAttributes which have been explicitly defined in the user pool schema
418 | });
419 |
420 | await createUnauthenticatedUser({
421 | clientId: "CLIENT_ID",
422 | userPoolId: "USER_POOL_ID",
423 | confirmed: true,
424 | standardAttributes: ["email", "middle_name", "address", "birthdate"], // works for all cognito standard user attributes
425 | });
426 |
427 |
428 | ```
429 |
430 | ## Running with `jest`
431 |
432 | ### Arguments
433 |
434 | - When running tests with `jest` using `sls-test-tools` matchers there are certain parameters needed for `sls-test-tools` to make assertions.
435 | - These are passed either as command line arguments, using quotation to match `jests` convention on test arguments, or by using environment variables. CLI arguments override environment variables.
436 |
437 | **Required**
438 |
439 | - `'--stack=my-service-dev'` or `process.env.CFN_STACK_NAME` - the CloudFormation stack name of the stack under test.
440 |
441 | **Optional**
442 |
443 | - `'--profile=[PROFILE NAME]'` or `process.env.AWS_PROFILE` (will default to `default`)
444 | - `'--region=[AWS Region]'` or `process.env.AWS_REGION` (will default to `eu-west-2`)
445 | - `'--keep=true'` - keeps testing resources up to avoid creation throttles (e.g. SQS Queue created for EventBridge assertions)
446 |
447 | - To avoid issues we recommend `--runInBand`
448 |
449 | ```
450 | import { AWSClient, EventBridge } from "sls-test-tools";
451 |
452 | const lambda = new AWSClient.Lambda()
453 | let eventBridge;
454 | const s3 = new AWSClient.S3()
455 |
456 | describe("Integration Testing Event Bridge", () => {
457 | beforeAll(async () => {
458 | eventBridge = await EventBridge.build("event-bridge")
459 | });
460 |
461 | afterAll(async () => {
462 | await eventBridge.destroy()
463 | });
464 |
465 | it("correctly publishes an event to the event bus when the lambda is invoked", async () => {
466 | const event = {
467 | body: JSON.stringify({
468 | filename: filename,
469 | }),
470 | };
471 |
472 | // Invoke Lambda Function
473 | const params = {
474 | FunctionName: "event-bridge-example-dev-service1",
475 | Payload: JSON.stringify(event),
476 | };
477 | await lambda.invoke(params).promise();
478 |
479 | const eventBridgeEvents = await eventBridge.getEvents()
480 | expect(eventBridgeEvents).toHaveEvent();
481 | expect(eventBridgeEvents).toHaveEventWithSource("order.created");
482 | });
483 |
484 | it("correctly generates a PDF when an order is created", async () => {
485 | const bucketName = example-bucket
486 | await eventBridge
487 | .publishEvent("order.created", "example", JSON.stringify({ filename: filename }));
488 |
489 | await sleep(5000); // wait 5 seconds to allow event to pass
490 |
491 | const params = {
492 | Bucket: bucketName,
493 | Key: filename,
494 | };
495 |
496 | // Assert that file was added to the S3 bucket
497 | await expect("example-dev-thumbnails-bucket").toHaveS3ObjectWithNameEqualTo(
498 | filename
499 | );
500 | });
501 | ```
502 |
503 | ## Contributors ✨
504 |
505 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
506 |
507 |
508 |
509 |
510 |
517 |
518 |
519 |
520 |
521 |
522 |
523 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
524 |
--------------------------------------------------------------------------------
/__tests__/eventbridge.test.ts:
--------------------------------------------------------------------------------
1 | import { EventBridge as AWSEventBridge, SQS } from "aws-sdk";
2 | import EventBridge from "../src/helpers/eventBridge";
3 |
4 | describe("EventBridge assertions", () => {
5 | let awsEventBridgeClient: AWSEventBridge;
6 | let slsEventBridgeClient: EventBridge;
7 |
8 | beforeAll(async () => {
9 | awsEventBridgeClient = new AWSEventBridge({
10 | region: "eu-west-2",
11 | });
12 | await awsEventBridgeClient
13 | .createEventBus({
14 | Name: "TestEventBus",
15 | })
16 | .promise();
17 | slsEventBridgeClient = await EventBridge.build("TestEventBus");
18 | });
19 |
20 | test("toHaveEvent should pass if event is created", async () => {
21 | await slsEventBridgeClient.publishEvent(
22 | "TestSource",
23 | "TestDetailType",
24 | JSON.stringify({ Detail: "TestDetail" }),
25 | false
26 | );
27 | const events = await slsEventBridgeClient.getEvents();
28 | expect(events).toHaveEvent();
29 | });
30 |
31 | test("toHaveEvent should fail if event is not present", async () => {
32 | await slsEventBridgeClient.publishEvent(
33 | "TestSource",
34 | "TestDetailType",
35 | JSON.stringify({ Detail: "TestDetail" }),
36 | false
37 | );
38 | await new Promise((resolve) => setTimeout(resolve, 3000));
39 | await slsEventBridgeClient.clear();
40 | await new Promise((resolve) => setTimeout(resolve, 60000));
41 | const events = await slsEventBridgeClient.getEvents();
42 | expect(events).not.toHaveEvent();
43 | });
44 |
45 | test("toHaveEventWithSource should pass if event is created with correct source", async () => {
46 | await slsEventBridgeClient.publishEvent(
47 | "TestSource",
48 | "TestDetailType",
49 | JSON.stringify({ Detail: "TestDetail" }),
50 | false
51 | );
52 | const events = await slsEventBridgeClient.getEvents();
53 | expect(events).toHaveEventWithSource("TestSource");
54 | });
55 |
56 | test("toHaveEventWithSource should pass if event is created with wrong source", async () => {
57 | await slsEventBridgeClient.publishEvent(
58 | "TestSource1",
59 | "TestDetailType",
60 | JSON.stringify({ Detail: "TestDetail" }),
61 | false
62 | );
63 | const events = await slsEventBridgeClient.getEvents();
64 | expect(events).not.toHaveEventWithSource("TestSource");
65 | });
66 |
67 | test("toHaveEventWithSource should fail if event is not created", async () => {
68 | await slsEventBridgeClient.clear();
69 | await new Promise((resolve) => setTimeout(resolve, 60000));
70 | const events = await slsEventBridgeClient.getEvents();
71 | expect(events).not.toHaveEventWithSource("TestSource");
72 | });
73 |
74 | test("clear helper should delete events off queue", async () => {
75 | await slsEventBridgeClient.publishEvent(
76 | "TestSource1",
77 | "TestDetailType1",
78 | JSON.stringify({ Detail: "TestDetail1" }),
79 | false
80 | );
81 | await slsEventBridgeClient.publishEvent(
82 | "TestSource2",
83 | "TestDetailType2",
84 | JSON.stringify({ Detail: "TestDetail2" }),
85 | false
86 | );
87 | await new Promise((resolve) => setTimeout(resolve, 3000));
88 | await slsEventBridgeClient.clear();
89 | await new Promise((resolve) => setTimeout(resolve, 60000));
90 | const events = await slsEventBridgeClient.getEvents();
91 | expect(events?.Messages?.length).toBe(undefined);
92 | });
93 |
94 | test("destroy helper should delete queue, and eventbus rules & targets.", async () => {
95 | let rules;
96 | let targets;
97 | const busName = "destroyTestingEventBus";
98 | const testingAwsEventBridgeClient = new AWSEventBridge({
99 | region: "eu-west-2",
100 | });
101 | // creating event bus
102 | await testingAwsEventBridgeClient
103 | .createEventBus({
104 | Name: busName,
105 | })
106 | .promise();
107 | const testingSlsEventBridgeClient = await EventBridge.build(busName);
108 | // get rules and check that rule has been created
109 | rules = await testingSlsEventBridgeClient.eventBridgeClient
110 | ?.listRules({
111 | EventBusName: busName,
112 | })
113 | .promise();
114 | expect(rules?.Rules?.length).toBeGreaterThan(0);
115 | // get rule targets and see that target has been assigned to rule
116 | targets = await testingSlsEventBridgeClient.eventBridgeClient
117 | ?.listTargetsByRule({
118 | EventBusName: busName,
119 | Rule: testingSlsEventBridgeClient.ruleName || `test-${busName}-rule`,
120 | })
121 | .promise();
122 | expect(targets?.Targets?.length).toBeGreaterThan(0);
123 | const testingSqsClient = new SQS();
124 | let queueExists = true;
125 | // check that queue exists
126 | let attributes = (
127 | await testingSqsClient
128 | .getQueueAttributes({
129 | QueueUrl: slsEventBridgeClient.QueueUrl || "",
130 | })
131 | .promise()
132 | ).Attributes;
133 | attributes === undefined ? (queueExists = true) : (queueExists = false);
134 | expect(queueExists).toBe(true);
135 | // call destroy
136 | await testingSlsEventBridgeClient.destroy();
137 |
138 | // check that there are no more rules
139 | rules = await testingSlsEventBridgeClient.eventBridgeClient
140 | ?.listRules({
141 | EventBusName: busName,
142 | })
143 | .promise();
144 | expect(rules?.Rules?.length).toBe(0);
145 | // check that the queue no longer exists
146 | attributes = (
147 | await testingSqsClient
148 | .getQueueAttributes({
149 | QueueUrl: slsEventBridgeClient.QueueUrl || "",
150 | })
151 | .promise()
152 | ).Attributes;
153 | attributes === undefined ? (queueExists = false) : (queueExists = true);
154 | expect(queueExists).toBe(false);
155 | await testingAwsEventBridgeClient
156 | .deleteEventBus({ Name: busName })
157 | .promise();
158 | });
159 |
160 | afterAll(async () => {
161 | await awsEventBridgeClient
162 | .removeTargets({
163 | EventBusName: "TestEventBus",
164 | Rule: slsEventBridgeClient.ruleName || `test-TestEventBus-rule`,
165 | Ids: ["1"],
166 | })
167 | .promise();
168 | await awsEventBridgeClient
169 | .deleteRule({
170 | EventBusName: "TestEventBus",
171 | Name: slsEventBridgeClient.ruleName || `test-TestEventBus-rule`,
172 | })
173 | .promise();
174 | const sqsClient = new SQS();
175 | if (slsEventBridgeClient.QueueUrl === undefined) {
176 | throw new Error("QueueUrl is undefined");
177 | } else {
178 | await sqsClient
179 | .deleteQueue({
180 | QueueUrl: slsEventBridgeClient.QueueUrl || "",
181 | })
182 | .promise();
183 | }
184 | await awsEventBridgeClient
185 | .deleteEventBus({ Name: "TestEventBus" })
186 | .promise();
187 | });
188 | });
189 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const defaultPresets = [
2 | ["@babel/preset-typescript", { allowNamespaces: true }],
3 | ];
4 |
5 | const defaultIgnores = [
6 | /.*\/(.*\.|)test\.tsx?/,
7 | /bundle\.ts/,
8 | /node_modules/,
9 | /lib/,
10 | ];
11 |
12 | const presetsForESM = [
13 | [
14 | "@babel/preset-env",
15 | {
16 | modules: false,
17 | },
18 | ],
19 | ...defaultPresets,
20 | ];
21 | const presetsForCJS = [
22 | [
23 | "@babel/preset-env",
24 | {
25 | modules: "cjs",
26 | },
27 | ],
28 | ...defaultPresets,
29 | ];
30 | const plugins = [
31 | [
32 | "module-resolver",
33 | {
34 | root: ["./src"],
35 | extensions: [".ts"],
36 | },
37 | ],
38 | "@babel/plugin-transform-runtime",
39 | ];
40 |
41 | module.exports = {
42 | env: {
43 | cjs: {
44 | presets: presetsForCJS,
45 | },
46 | esm: {
47 | presets: presetsForESM,
48 | },
49 | },
50 | ignore: defaultIgnores,
51 | plugins,
52 | };
53 |
--------------------------------------------------------------------------------
/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleios-cloud/sls-test-tools/bcfd81153d4957ff1f16edc7b0a61e4cae68b81d/img/logo.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | setupFilesAfterEnv: ["./jest.setup.js", "sls-test-tools"],
3 | preset: "ts-jest",
4 | };
5 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | jest.setTimeout(190000);
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sls-test-tools",
3 | "version": "1.0.7",
4 | "description": "Custom Jest Assertions for Serverless Projects",
5 | "main": "lib/cjs/index.js",
6 | "module": "lib/esm/index.js",
7 | "types": "lib/types/index.d.ts",
8 | "directories": {
9 | "lib": "lib"
10 | },
11 | "repository": "git@github.com:aleios-cloud/sls-test-tools.git",
12 | "bugs": {
13 | "url": "https://github.com/aleios-cloud/sls-test-tools/issues"
14 | },
15 | "homepage": "https://github.com/aleios-cloud/sls-test-tools#readme",
16 | "author": "Aleios",
17 | "license": "MIT",
18 | "scripts": {
19 | "build": "rm -rf lib && yarn package:cjs && yarn package:esm && yarn package:types",
20 | "contributors:add": "all-contributors add",
21 | "contributors:generate": "all-contributors generate",
22 | "dev": "rm -rf lib && concurrently 'yarn:package:* --watch'",
23 | "lint": "eslint ./src",
24 | "fix": "yarn lint --fix",
25 | "release": "release-it",
26 | "package:cjs": "NODE_ENV=cjs yarn transpile --out-dir lib/cjs --source-maps",
27 | "package:esm": "NODE_ENV=esm yarn transpile --out-dir lib/esm --source-maps",
28 | "package:types": "ttsc",
29 | "transpile": "babel src --extensions .ts"
30 | },
31 | "dependencies": {
32 | "@types/chance": "^1.1.3",
33 | "aws-sdk": "^2.711.0",
34 | "chance": "^1.1.8",
35 | "import-all.macro": "^3.1.0",
36 | "json-schema-faker": "^0.5.0-rcv.42",
37 | "uuid": "^8.3.2",
38 | "yargs": "^17.3.1"
39 | },
40 | "devDependencies": {
41 | "@babel/cli": "^7.16.0",
42 | "@babel/core": "^7.16.0",
43 | "@babel/plugin-transform-runtime": "^7.16.0",
44 | "@babel/preset-env": "^7.16.0",
45 | "@babel/preset-typescript": "^7.16.0",
46 | "@types/jest": "^28.1.4",
47 | "@types/yargs": "^17.0.8",
48 | "@typescript-eslint/eslint-plugin": "^5.6.0",
49 | "@typescript-eslint/parser": "^5.6.0",
50 | "@zerollup/ts-transform-paths": "^1.7.18",
51 | "all-contributors-cli": "^6.20.0",
52 | "babel-plugin-macros": "^3.1.0",
53 | "babel-plugin-module-resolver": "^4.1.0",
54 | "concurrently": "^6.0.0",
55 | "eslint": "^8.4.1",
56 | "eslint-config-airbnb-base": "^14.2.1",
57 | "eslint-config-prettier": "^8.3.0",
58 | "eslint-import-resolver-typescript": "^2.5.0",
59 | "eslint-plugin-import": "^2.25.3",
60 | "eslint-plugin-prettier": "^4.0.0",
61 | "jest": "^28.1.2",
62 | "prettier": "^2.2.1",
63 | "release-it": "^14.12.4",
64 | "ts-jest": "^28.0.5",
65 | "ts-migrate": "^0.1.27",
66 | "ts-node": "^10.4.0",
67 | "ttypescript": "^1.5.12",
68 | "typescript": "^4.7.4"
69 | },
70 | "babel": {
71 | "presets": [
72 | [
73 | "@babel/preset-env",
74 | {
75 | "targets": {
76 | "node": true
77 | }
78 | }
79 | ]
80 | ],
81 | "plugins": [
82 | "macros"
83 | ]
84 | },
85 | "files": [
86 | "lib/**/*.js",
87 | "lib/**/*.ts"
88 | ],
89 | "keywords": [
90 | "aws",
91 | "dev",
92 | "EventBridge",
93 | "serverless",
94 | "sls",
95 | "testing",
96 | "tools",
97 | "theodo"
98 | ]
99 | }
100 |
--------------------------------------------------------------------------------
/src/assertions/index.ts:
--------------------------------------------------------------------------------
1 | import toExistAsS3Bucket from "./toExistAsS3Bucket";
2 | import toHaveContentEqualTo from "./toHaveContentEqualTo";
3 | import toHaveContentTypeEqualTo from "./toHaveContentTypeEqualTo";
4 | import toHaveEvent from "./toHaveEvent";
5 | import toHaveEventWithSource from "./toHaveEventWithSource";
6 | import toHaveObjectWithNameEqualTo from "./toHaveObjectWithNameEqualTo";
7 | import toExistInDynamoTable from "./toExistInDynamoTable";
8 | import toHaveCompletedExecutionWithStatus from "./toHaveCompletedExecutionWithStatus";
9 | import toContainItemWithValues from "./toContainItemWithValues";
10 | import toMatchStateMachineOutput from "./toMatchStateMachineOutput";
11 | import toContainUser from "./toContainUser";
12 |
13 | export default {
14 | ...toExistAsS3Bucket,
15 | ...toHaveContentEqualTo,
16 | ...toHaveContentTypeEqualTo,
17 | ...toHaveEvent,
18 | ...toHaveEventWithSource,
19 | ...toHaveObjectWithNameEqualTo,
20 | ...toExistInDynamoTable,
21 | ...toHaveCompletedExecutionWithStatus,
22 | ...toContainItemWithValues,
23 | ...toMatchStateMachineOutput,
24 | ...toContainUser,
25 | };
26 |
--------------------------------------------------------------------------------
/src/assertions/toContainItemWithValues/index.ts:
--------------------------------------------------------------------------------
1 | import { testResult, TestResultOutput } from "utils/testResult";
2 | import { AWSClient } from "helpers/general";
3 | import { region } from "../../helpers/general";
4 |
5 | export default {
6 | async toContainItemWithValues(
7 | tableName: string,
8 | values: { [key: string]: unknown }
9 | ): Promise {
10 | const docClient = new AWSClient.DynamoDB.DocumentClient({
11 | region: region,
12 | });
13 | const keys: { pk: unknown; sk?: unknown } = { pk: values["PK"] };
14 | if (values["SK"] !== undefined) {
15 | keys.sk = values["SK"];
16 | }
17 | const queryParams = {
18 | Key: keys,
19 | TableName: tableName,
20 | };
21 | let allMatched = true;
22 | let itemExists = true;
23 | try {
24 | const result = await docClient.get(queryParams).promise();
25 | Object.entries(values).forEach(([key, val]) => {
26 | if (result.Item !== undefined) {
27 | if (key in result.Item) {
28 | if (result.Item[key] !== val) {
29 | allMatched = false;
30 | }
31 | }
32 | } else {
33 | itemExists = false;
34 | }
35 | });
36 | if (!itemExists) {
37 | return testResult(`Item does not exist.`, false);
38 | } else if (!allMatched) {
39 | return testResult(`Some values do not match as expected.`, false);
40 | } else {
41 | return testResult("Item exists with expected values", true);
42 | }
43 | } catch (e: any) {
44 | console.log(e);
45 |
46 | return testResult("Item with specified keys does not exist.", false);
47 | }
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/src/assertions/toContainUser/index.ts:
--------------------------------------------------------------------------------
1 | import { AWSClient } from "helpers/general";
2 | import { CognitoIdentityServiceProvider } from "aws-sdk";
3 | import { testResult, TestResultOutput } from "utils/testResult";
4 |
5 | export default {
6 | async toContainUser(
7 | userPoolId: string,
8 | username: string
9 | ): Promise {
10 | const cognitoClient: CognitoIdentityServiceProvider = new AWSClient.CognitoIdentityServiceProvider();
11 | try {
12 | await cognitoClient
13 | .adminGetUser({
14 | UserPoolId: userPoolId,
15 | Username: username,
16 | })
17 | .promise();
18 |
19 | return testResult(
20 | `User with username ${username} exists in User Pool with Id ${userPoolId}`,
21 | true
22 | );
23 | } catch (e) {
24 | console.log(e);
25 |
26 | return testResult(
27 | `User does not exist in User Pool with Id ${userPoolId}`,
28 | false
29 | );
30 | }
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/src/assertions/toExistAsS3Bucket/index.ts:
--------------------------------------------------------------------------------
1 | import { AWSClient } from "helpers/general";
2 | import { testResult, TestResultOutput } from "utils/testResult";
3 | import { is404Error } from "../utils";
4 |
5 | export default {
6 | async toExistAsS3Bucket(bucketName: string): Promise {
7 | const s3 = new AWSClient.S3();
8 | const params = {
9 | Bucket: bucketName,
10 | };
11 |
12 | let message;
13 | try {
14 | await s3.headBucket(params).promise();
15 | message = `expected S3 bucket to exist with name ${bucketName}`;
16 |
17 | return testResult(message, true);
18 | } catch (error) {
19 | if (is404Error(error)) {
20 | message = `expected S3 bucket to exist with name ${bucketName} - not found`;
21 |
22 | return testResult(message, false);
23 | }
24 | throw error;
25 | }
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/src/assertions/toExistInDynamoTable/index.ts:
--------------------------------------------------------------------------------
1 | import { TestResultOutput } from "utils/testResult";
2 | import { AWSClient } from "helpers/general";
3 |
4 | export default {
5 | async toExistInDynamoTable(
6 | { PK, SK }: { PK: string; SK?: string },
7 | tableName: string
8 | ): Promise {
9 | const docClient = new AWSClient.DynamoDB.DocumentClient();
10 | if (SK === undefined) {
11 | const queryParams = {
12 | TableName: tableName,
13 | KeyConditionExpression: "#pk = :pk",
14 | ExpressionAttributeNames: {
15 | "#pk": "PK",
16 | },
17 | ExpressionAttributeValues: {
18 | ":pk": "PK",
19 | },
20 | Limit: 1,
21 | };
22 | const result = await docClient.query(queryParams).promise();
23 |
24 | return {
25 | message: () => `expected to find ${PK} in ${tableName}`,
26 | pass: result.Count === 1,
27 | };
28 | }
29 | const getParams = {
30 | TableName: tableName,
31 | Key: {
32 | PK,
33 | SK,
34 | },
35 | };
36 | const result = await docClient.get(getParams).promise();
37 |
38 | return {
39 | message: () => `expected to find ${PK} in ${tableName}`,
40 | pass: result.Item !== undefined,
41 | };
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/assertions/toHaveCompletedExecutionWithStatus/index.ts:
--------------------------------------------------------------------------------
1 | import { testResult, TestResultOutput } from "../../utils/testResult";
2 | import StepFunctions from "../../helpers/stepFunctions";
3 |
4 | import { AWSClient } from "../../helpers/general";
5 |
6 | export default {
7 | async toHaveCompletedExecutionWithStatus(
8 | stateMachineName: string,
9 | expectedStatus: string
10 | ): Promise {
11 | const stepFunctions = new AWSClient.StepFunctions();
12 | const stepFunctionsObject = await StepFunctions.build();
13 | // Helper to get stateMachine ARN from stateMachine name
14 | const smArn = await stepFunctionsObject.obtainStateMachineArn(
15 | stateMachineName
16 | );
17 |
18 | const listExecutionsParams = { stateMachineArn: smArn };
19 | // Get all executions of specified state machine
20 | const smExecutions = await stepFunctions
21 | .listExecutions(listExecutionsParams)
22 | .promise();
23 | // Get the latest execution (list ordered in reverse chronological)
24 | const latestExecution = smExecutions.executions[0];
25 | if (latestExecution.status === expectedStatus) {
26 | return testResult(
27 | `Execution status is ${expectedStatus}, as expected.`,
28 | true
29 | );
30 | }
31 |
32 | return testResult(
33 | `Execution status was ${latestExecution.status}, where it was expected to be ${expectedStatus}`,
34 | false
35 | );
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/src/assertions/toHaveContentEqualTo/index.ts:
--------------------------------------------------------------------------------
1 | import { AWSClient } from "helpers/general";
2 | import { testResult, TestResultOutput } from "utils/testResult";
3 | import { isNoSuchBucketError, isNoSuchKeyError } from "../utils";
4 |
5 | export default {
6 | // Import & use s3 type ?
7 | async toHaveContentEqualTo(
8 | { bucketName, objectName }: { bucketName: string; objectName: string },
9 | content: Record | string
10 | ): Promise {
11 | const s3 = new AWSClient.S3();
12 | const params = {
13 | Bucket: bucketName,
14 | Key: objectName,
15 | };
16 |
17 | let message;
18 | try {
19 | const object = await s3.getObject(params).promise();
20 | if (JSON.stringify(object.Body) === JSON.stringify(content)) {
21 | message = `expected ${objectName} to have content ${JSON.stringify(
22 | content
23 | )}`;
24 |
25 | return testResult(message, true);
26 | }
27 | const stringifiedObjectBody = object.Body?.toString();
28 | if (stringifiedObjectBody === undefined) {
29 | message = `expected ${objectName} to have content ${JSON.stringify(
30 | content
31 | )}, but content found was undefined`;
32 |
33 | return testResult(message, false);
34 | }
35 |
36 | message = `expected ${objectName} to have content ${JSON.stringify(
37 | content
38 | )}, but content found was ${stringifiedObjectBody}`;
39 |
40 | return testResult(message, false);
41 | } catch (error) {
42 | if (isNoSuchKeyError(error)) {
43 | message = `expected ${bucketName} to have object with name ${objectName} - not found`;
44 |
45 | return testResult(message, false);
46 | }
47 | if (isNoSuchBucketError(error)) {
48 | message = `expected ${bucketName} to exist - not found`;
49 |
50 | return testResult(message, false);
51 | }
52 | throw error;
53 | }
54 | },
55 | };
56 |
--------------------------------------------------------------------------------
/src/assertions/toHaveContentTypeEqualTo/index.ts:
--------------------------------------------------------------------------------
1 | import { AWSClient } from "helpers/general";
2 | import { testResult, TestResultOutput } from "utils/testResult";
3 | import { isNoSuchBucketError, isNoSuchKeyError } from "../utils";
4 |
5 | export default {
6 | async toHaveContentTypeEqualTo(
7 | { bucketName, objectName }: { bucketName: string; objectName: string },
8 | contentType: string
9 | ): Promise {
10 | const s3 = new AWSClient.S3();
11 | const params = {
12 | Bucket: bucketName,
13 | Key: objectName,
14 | };
15 |
16 | let message;
17 | try {
18 | const object = await s3.getObject(params).promise();
19 | if (object.ContentType === contentType) {
20 | message = `expected ${objectName} to have content type ${contentType}`;
21 |
22 | return testResult(message, true);
23 | }
24 | message = `expected ${objectName} to have content type ${contentType}, but content type found was ${
25 | object.ContentType ?? "undefined"
26 | }`;
27 |
28 | return testResult(message, false);
29 | } catch (error) {
30 | if (isNoSuchKeyError(error)) {
31 | message = `expected ${bucketName} to have object with name ${objectName} - not found`;
32 |
33 | return testResult(message, false);
34 | }
35 | if (isNoSuchBucketError(error)) {
36 | message = `expected ${bucketName} to exist - not found`;
37 |
38 | return testResult(message, false);
39 | }
40 | throw error;
41 | }
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/assertions/toHaveEvent/index.ts:
--------------------------------------------------------------------------------
1 | import { SQS } from "aws-sdk";
2 | import { testResult, TestResultOutput } from "utils/testResult";
3 |
4 | export default {
5 | toHaveEvent(eventBridgeEvents?: SQS.ReceiveMessageResult): TestResultOutput {
6 | if (
7 | eventBridgeEvents === undefined ||
8 | eventBridgeEvents.Messages === undefined ||
9 | eventBridgeEvents.Messages.length === 0
10 | ) {
11 | return testResult("no message intercepted from EventBridge Bus", false);
12 | }
13 |
14 | return testResult("expected to have message in EventBridge Bus", true);
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/assertions/toHaveEventWithSource/index.ts:
--------------------------------------------------------------------------------
1 | import { testResult, TestResultOutput } from "utils/testResult";
2 | import { SQS } from "aws-sdk";
3 |
4 | export default {
5 | toHaveEventWithSource(
6 | Events: SQS.ReceiveMessageResult,
7 | expectedSourceName: string
8 | ): TestResultOutput {
9 | let message;
10 |
11 | if (Events.Messages === undefined || Events.Messages.length < 1) {
12 | return testResult("There are no events present.", false);
13 | }
14 |
15 | const parsedBody = JSON.parse(Events.Messages[0].Body) as {
16 | source?: string;
17 | };
18 |
19 | if (parsedBody.source === expectedSourceName) {
20 | message = `expected sent event to have source ${expectedSourceName}`;
21 |
22 | return testResult(message, true);
23 | }
24 | message = `sent event source "${
25 | parsedBody.source ?? "undefined"
26 | }" does not match expected source "${expectedSourceName}"`;
27 |
28 | return testResult(message, false);
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/src/assertions/toHaveObjectWithNameEqualTo/index.ts:
--------------------------------------------------------------------------------
1 | import { AWSClient } from "helpers/general";
2 | import { testResult, TestResultOutput } from "utils/testResult";
3 | import { isNoSuchKeyError } from "../utils";
4 |
5 | export default {
6 | async toHaveS3ObjectWithNameEqualTo(
7 | bucketName: string,
8 | objectName: string
9 | ): Promise {
10 | const s3 = new AWSClient.S3();
11 | const params = {
12 | Bucket: bucketName,
13 | Key: objectName,
14 | };
15 |
16 | let message;
17 | try {
18 | await s3.getObject(params).promise();
19 | message = `expected ${bucketName} to have object with name ${objectName}`;
20 |
21 | return testResult(message, true);
22 | } catch (error) {
23 | if (isNoSuchKeyError(error)) {
24 | message = `expected ${bucketName} to have object with name ${objectName} - not found`;
25 |
26 | return testResult(message, false);
27 | }
28 | throw error;
29 | }
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/src/assertions/toMatchStateMachineOutput/index.ts:
--------------------------------------------------------------------------------
1 | import { testResult, TestResultOutput } from "../../utils/testResult";
2 | import { StepFunctions as AWSStepFunctions } from "aws-sdk";
3 | import StepFunctions from "../../helpers/stepFunctions";
4 |
5 | export default {
6 | async toMatchStateMachineOutput(
7 | stateMachineName: string,
8 | expectedOutput: unknown
9 | ): Promise {
10 | const stepFunctions = new AWSStepFunctions();
11 | const stepFunctionObject = await StepFunctions.build();
12 | // Helper to get stateMachine ARN from stateMachine name
13 | const smArn = await stepFunctionObject.obtainStateMachineArn(
14 | stateMachineName
15 | );
16 | // Helper to get latest execution ARN for given stateMachine
17 | const exArn = await stepFunctionObject.obtainExecutionArn(smArn);
18 |
19 | const executionResult = await stepFunctions
20 | .describeExecution({
21 | executionArn: exArn,
22 | })
23 | .promise();
24 |
25 | if (executionResult.status === "SUCCEEDED") {
26 | if (executionResult.output === expectedOutput) {
27 | return testResult(
28 | `Output is ${JSON.stringify(executionResult.output)} as expected`,
29 | true
30 | );
31 | } else {
32 | return testResult(
33 | `Expected output was ${JSON.stringify(
34 | expectedOutput
35 | )}, but output received was ${JSON.stringify(
36 | executionResult.output
37 | )}`,
38 | false
39 | );
40 | }
41 | }
42 |
43 | return testResult(
44 | "Step Function execution failed. Cannot verify output for failed executions.",
45 | false
46 | );
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/assertions/utils/errorTypesCheckers.ts:
--------------------------------------------------------------------------------
1 | type Error404 = { statusCode: 404 };
2 | export const is404Error = (error: unknown): error is Error404 =>
3 | (error as { statusCode?: number }).statusCode === 404;
4 |
5 | type ErrorNoSuchKey = { code: "NoSuchKey" };
6 | export const isNoSuchKeyError = (error: unknown): error is ErrorNoSuchKey =>
7 | (error as { code?: string }).code === "NoSuchKey";
8 |
9 | type ErrorNoSuchBucket = { code: "NoSuchBucket" };
10 | export const isNoSuchBucketError = (
11 | error: unknown
12 | ): error is ErrorNoSuchBucket =>
13 | (error as { code?: string }).code === "NoSuchBucket";
14 |
--------------------------------------------------------------------------------
/src/assertions/utils/globalTypeChecker.ts:
--------------------------------------------------------------------------------
1 | type GlobalWithExpectKey = { expect: any };
2 | export const isGlobalWithExpectKey = (
3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
4 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
5 | global: any
6 | ): global is GlobalWithExpectKey => "expect" in global;
7 |
--------------------------------------------------------------------------------
/src/assertions/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./errorTypesCheckers";
2 |
--------------------------------------------------------------------------------
/src/helpers/cognito.ts:
--------------------------------------------------------------------------------
1 | import { CognitoIdentityServiceProvider } from "aws-sdk";
2 | import { AWSClient } from "./general";
3 | import { Chance } from "chance";
4 | import { AttributeType } from "aws-sdk/clients/cognitoidentityserviceprovider";
5 | import jsf from "json-schema-faker";
6 |
7 | interface User {
8 | username: string;
9 | password: string;
10 | confirmed?: boolean | undefined;
11 | standardAttributes?: StandardAttributes;
12 | customAttributes?: { [attribute: string]: string };
13 | }
14 |
15 | interface CreateUserInput {
16 | clientId: string;
17 | userPoolId: string;
18 | confirmed: boolean;
19 | standardAttributes?: Array;
20 | customAttributes?: { [key: string]: unknown };
21 | }
22 |
23 | interface ConfirmUserInput {
24 | userPoolId: string;
25 | username: string;
26 | password: string;
27 | }
28 |
29 | interface Address {
30 | formatted: string;
31 | street_address: string;
32 | locality: string;
33 | region: string;
34 | postal_code: string;
35 | country: string;
36 | }
37 |
38 | interface StandardAttributes {
39 | address: string;
40 | birthdate: string;
41 | email: string;
42 | family_name: string;
43 | gender: string;
44 | given_name: string;
45 | locale: string;
46 | middle_name: string;
47 | name: string;
48 | nickname: string;
49 | phone_number: string;
50 | picture: string;
51 | preferred_username: string;
52 | profile: string;
53 | updated_at: string;
54 | website: string;
55 | zoneinfo: string;
56 | }
57 |
58 | const createUser = async (
59 | createUserInput: CreateUserInput,
60 | username: string
61 | ): Promise => {
62 | const cognitoClient: CognitoIdentityServiceProvider =
63 | new AWSClient.CognitoIdentityServiceProvider();
64 | const chance = new Chance();
65 | const password: string = chance.string({ length: 8 });
66 |
67 | const givenName = chance.first();
68 | const middleName = chance.first();
69 | const familyName = chance.last();
70 | const name = givenName + " " + middleName + " " + familyName;
71 |
72 | const country = chance.country();
73 | const streetAddress = chance.street();
74 | const locality = chance.city();
75 | const region = chance.province();
76 | const postalCode = chance.postcode();
77 |
78 | const formatted = [streetAddress, locality, region, postalCode, country].join(
79 | "\r\n"
80 | );
81 |
82 | const address: Address = {
83 | formatted: formatted,
84 | street_address: streetAddress,
85 | locality: locality,
86 | region: region,
87 | postal_code: postalCode,
88 | country: country,
89 | };
90 |
91 | const allStandardAttributes: StandardAttributes = {
92 | email: chance.email(),
93 | birthdate: chance.date().toISOString().split("T")[0],
94 | family_name: familyName,
95 | gender: chance.gender(),
96 | given_name: givenName,
97 | locale: chance.locale(),
98 | middle_name: middleName,
99 | name: name,
100 | nickname: chance.string(),
101 | phone_number: chance.phone(),
102 | picture: chance.url(),
103 | preferred_username: chance.string(),
104 | profile: chance.url(),
105 | website: chance.url(),
106 | zoneinfo: chance.string(),
107 | address: JSON.stringify(address),
108 | updated_at: String(chance.timestamp()),
109 | };
110 |
111 | const attributesArg: AttributeType[] = [];
112 | jsf.extend("chance", () => new Chance());
113 | if (createUserInput.customAttributes !== undefined) {
114 | Object.entries(createUserInput.customAttributes).forEach(([key, val]) => {
115 | attributesArg.push({
116 | Name: "custom:" + key,
117 | Value: jsf.generate({ type: val }),
118 | });
119 | });
120 | }
121 |
122 | createUserInput.standardAttributes?.forEach(
123 | (attribute: keyof StandardAttributes) => {
124 | attributesArg.push({
125 | Name: attribute,
126 | Value: allStandardAttributes[attribute],
127 | });
128 | }
129 | );
130 |
131 | try {
132 | const signUpParams: CognitoIdentityServiceProvider.Types.SignUpRequest = {
133 | ClientId: createUserInput.clientId,
134 | Username: username,
135 | Password: password,
136 | UserAttributes: attributesArg,
137 | };
138 | await cognitoClient.signUp(signUpParams).promise();
139 | } catch (e) {
140 | console.log(e);
141 | console.error(
142 | "Failed to create user. Please make sure the clientId is correct, and that the username is valid."
143 | );
144 | }
145 |
146 | return {
147 | username,
148 | password,
149 | };
150 | };
151 |
152 | const confirmUser = async (input: ConfirmUserInput): Promise => {
153 | const cognitoClient: CognitoIdentityServiceProvider =
154 | new AWSClient.CognitoIdentityServiceProvider();
155 |
156 | try {
157 | await cognitoClient
158 | .adminConfirmSignUp({
159 | UserPoolId: input.userPoolId,
160 | Username: input.username,
161 | })
162 | .promise();
163 | } catch (e) {
164 | console.error(
165 | "Failed to confirm sign up. Please make sure the user exists."
166 | );
167 | throw e;
168 | }
169 |
170 | return {
171 | username: input.username,
172 | password: input.password,
173 | confirmed: true,
174 | };
175 | };
176 |
177 | export const createUnauthenticatedUser = async (
178 | input: CreateUserInput
179 | ): Promise => {
180 | const chance = new Chance();
181 | const username: string = chance.email();
182 | const user: User = await createUser(input, username);
183 |
184 | if (input.confirmed) {
185 | return await confirmUser({
186 | userPoolId: input.userPoolId,
187 | username: username,
188 | password: user.password,
189 | });
190 | }
191 |
192 | return {
193 | username: username,
194 | password: user.password,
195 | confirmed: input.confirmed,
196 | };
197 | };
198 |
199 | export const createAuthenticatedUser = async (
200 | input: CreateUserInput
201 | ): Promise => {
202 | const cognitoClient: CognitoIdentityServiceProvider =
203 | new AWSClient.CognitoIdentityServiceProvider();
204 | const chance = new Chance();
205 | const username: string = chance.email();
206 |
207 | const user: User = await createUser(input, username);
208 |
209 | await confirmUser({
210 | userPoolId: input.userPoolId,
211 | username: username,
212 | password: user.password,
213 | });
214 |
215 | try {
216 | const auth: CognitoIdentityServiceProvider.InitiateAuthResponse =
217 | await cognitoClient
218 | .initiateAuth({
219 | AuthFlow: "USER_PASSWORD_AUTH",
220 | ClientId: input.clientId,
221 | AuthParameters: {
222 | USERNAME: user.username,
223 | PASSWORD: user.password,
224 | },
225 | })
226 | .promise();
227 |
228 | return {
229 | username,
230 | password: user.password,
231 | idToken: auth.AuthenticationResult?.IdToken,
232 | accessToken: auth.AuthenticationResult?.AccessToken,
233 | };
234 | } catch (e) {
235 | console.error(
236 | "Failed to authorize user - please make sure you're using the correct AuthFlow and that the user exists, and is confirmed."
237 | );
238 |
239 | throw e;
240 | }
241 | };
242 |
--------------------------------------------------------------------------------
/src/helpers/eventBridge.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-lines */
2 | import { AWSError, EventBridge as AWSEventBridge, SQS } from "aws-sdk";
3 | import { PromiseResult } from "aws-sdk/lib/request";
4 | import { AWSClient, region } from "./general";
5 | import { removeUndefinedMessages } from "./utils/removeUndefinedMessages";
6 |
7 | export default class EventBridge {
8 | QueueUrl: string | undefined;
9 | eventBridgeClient: AWSEventBridge | undefined;
10 | eventBridgeName: string | undefined;
11 | keep: boolean | undefined;
12 | ruleName: string | undefined;
13 | sqsClient: SQS | undefined;
14 | targetId: string | undefined;
15 |
16 | async init(eventBridgeName: string): Promise {
17 | this.eventBridgeClient = new AWSClient.EventBridge();
18 | this.eventBridgeName = eventBridgeName;
19 | this.ruleName = `test-${eventBridgeName}-rule`;
20 | this.targetId = "1";
21 |
22 | const keepArg = process.argv.filter((x) => x.startsWith("--keep="))[0];
23 | const keepArgEnabled = keepArg ? keepArg.split("=")[1] === "true" : false;
24 | const keepEnvVarEnabled = !!process.env.SLS_TEST_TOOLS_KEEP;
25 | this.keep = keepArgEnabled || keepEnvVarEnabled;
26 | const ruleNameArg = process.argv.filter((x) => x.startsWith("--event-rule-name="))[0];
27 | this.ruleName = ruleNameArg ? ruleNameArg.split("=")[1] : `test-${eventBridgeName}-rule`;
28 | const queueNameArg = process.argv.filter((x) => x.startsWith("--queue-name="))[0];
29 | const queueName = queueNameArg ? queueNameArg.split("=")[1] : `${eventBridgeName}-testing-queue`;
30 |
31 | this.sqsClient = new AWSClient.SQS();
32 | if (!this.keep) {
33 | console.info(
34 | "If running repeatedly add '--keep=true' to keep testing resources up to avoid creation throttles"
35 | );
36 | }
37 |
38 | const queueResult = await this.sqsClient
39 | .createQueue({
40 | QueueName: queueName,
41 | })
42 | .promise();
43 |
44 | this.QueueUrl = queueResult.QueueUrl;
45 |
46 | if (this.QueueUrl === undefined) {
47 | throw new Error("QueueUrl is undefined");
48 | }
49 | const accountId = this.QueueUrl.split("/")[3];
50 | const sqsArn = `arn:aws:sqs:${region}:${accountId}:${queueName}`;
51 | const pattern = {
52 | account: [`${accountId}`],
53 | };
54 |
55 | await this.eventBridgeClient
56 | .putRule({
57 | Name: this.ruleName,
58 | EventBusName: eventBridgeName,
59 | EventPattern: JSON.stringify(pattern),
60 | State: "ENABLED",
61 | })
62 | .promise();
63 |
64 | await this.eventBridgeClient
65 | .putTargets({
66 | EventBusName: eventBridgeName,
67 | Rule: this.ruleName,
68 | Targets: [
69 | {
70 | Arn: sqsArn,
71 | Id: this.targetId,
72 | },
73 | ],
74 | })
75 | .promise();
76 |
77 | const policy = {
78 | Version: "2008-10-17",
79 | Statement: [
80 | {
81 | Effect: "Allow",
82 | Principal: {
83 | Service: "events.amazonaws.com",
84 | },
85 | Action: "SQS:SendMessage",
86 | Resource: sqsArn,
87 | },
88 | ],
89 | };
90 |
91 | await this.sqsClient
92 | .setQueueAttributes({
93 | Attributes: {
94 | Policy: JSON.stringify(policy),
95 | },
96 | QueueUrl: this.QueueUrl,
97 | })
98 | .promise();
99 | }
100 |
101 | static async build(eventBridgeName: string): Promise {
102 | const eventBridge = new EventBridge();
103 | await eventBridge.init(eventBridgeName);
104 |
105 | return eventBridge;
106 | }
107 |
108 | // eslint-disable-next-line max-params
109 | async publishEvent(
110 | source: string | undefined,
111 | detailType: string | undefined,
112 | detail: string | undefined,
113 | clear?: boolean
114 | ): Promise> {
115 | if (this.eventBridgeClient === undefined) {
116 | throw new Error(
117 | "EventBridgeClient is undefined. You might have forgotten to use init()"
118 | );
119 | }
120 | const result = await this.eventBridgeClient
121 | .putEvents({
122 | Entries: [
123 | {
124 | EventBusName: this.eventBridgeName,
125 | Source: source,
126 | DetailType: detailType,
127 | Detail: detail,
128 | },
129 | ],
130 | })
131 | .promise();
132 |
133 | if (clear === undefined) {
134 | clear = true;
135 | }
136 | if (!clear) {
137 | return result;
138 | }
139 | await this.getEvents(); // need to clear this manual published event from the SQS observer queue.
140 |
141 | return result;
142 | }
143 |
144 | async getEvents(
145 | clear?: boolean | undefined
146 | ): Promise {
147 | if (this.QueueUrl === undefined) {
148 | throw new Error("QueueUrl is undefined");
149 | }
150 | // Long poll SQS queue
151 | const queueParams = {
152 | QueueUrl: this.QueueUrl,
153 | WaitTimeSeconds: 5,
154 | };
155 | if (this.sqsClient === undefined) {
156 | throw new Error(
157 | "SQSClient is undefined. You might have forgotten to use init()"
158 | );
159 | }
160 | const result = await this.sqsClient.receiveMessage(queueParams).promise();
161 |
162 | if (clear === undefined) {
163 | clear = true;
164 | }
165 |
166 | if (!clear) {
167 | return result;
168 | }
169 |
170 | const messageHandlers = removeUndefinedMessages(
171 | result.Messages?.map((message: SQS.Message) => ({
172 | Id: message.MessageId,
173 | ReceiptHandle: message.ReceiptHandle,
174 | }))
175 | );
176 |
177 | if (messageHandlers !== undefined && messageHandlers.length > 0) {
178 | await this.sqsClient
179 | .deleteMessageBatch({
180 | Entries: messageHandlers,
181 | QueueUrl: this.QueueUrl,
182 | })
183 | .promise();
184 | }
185 |
186 | return result;
187 | }
188 |
189 | async clear(): Promise {
190 | if (this.sqsClient === undefined) {
191 | throw new Error(
192 | "SQSClient is undefined. You might have forgotten to use init()"
193 | );
194 | }
195 | if (this.QueueUrl === undefined) {
196 | throw new Error("QueueUrl is undefined");
197 | }
198 | const result = await this.sqsClient
199 | .purgeQueue({
200 | QueueUrl: this.QueueUrl,
201 | })
202 | .promise();
203 |
204 | return result;
205 | }
206 |
207 | async destroy(): Promise {
208 | if (this.keep === undefined) {
209 | throw new Error(
210 | "keep is undefined. You might have forgotten to use init()"
211 | );
212 | }
213 | if (!this.keep) {
214 | if (this.sqsClient === undefined) {
215 | throw new Error(
216 | "SQSClient is undefined. You might have forgotten to use init()"
217 | );
218 | }
219 | if (this.QueueUrl === undefined) {
220 | throw new Error("QueueUrl is undefined");
221 | }
222 |
223 | await this.sqsClient
224 | .deleteQueue({
225 | QueueUrl: this.QueueUrl,
226 | })
227 | .promise();
228 |
229 | if (this.eventBridgeClient === undefined) {
230 | throw new Error(
231 | "EventBridgeClient is undefined. You might have forgotten to use init()"
232 | );
233 | }
234 |
235 | if (this.targetId === undefined) {
236 | throw new Error(
237 | "targetId is undefined. You might have forgotten to use init()"
238 | );
239 | }
240 | if (this.ruleName === undefined) {
241 | throw new Error(
242 | "ruleName is undefined. You might have forgotten to use init()"
243 | );
244 | }
245 | await this.eventBridgeClient
246 | .removeTargets({
247 | Ids: [this.targetId],
248 | Rule: this.ruleName,
249 | EventBusName: this.eventBridgeName,
250 | })
251 | .promise();
252 |
253 | await this.eventBridgeClient
254 | .deleteRule({
255 | Name: this.ruleName,
256 | EventBusName: this.eventBridgeName,
257 | })
258 | .promise();
259 | } else {
260 | await this.clear();
261 | }
262 |
263 | return true;
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/src/helpers/general.ts:
--------------------------------------------------------------------------------
1 | import AWS, { AWSError } from "aws-sdk";
2 | import { DescribeStacksOutput } from "aws-sdk/clients/cloudformation";
3 | import { PromiseResult } from "aws-sdk/lib/request";
4 | import { loadArg } from "./utils/loadArg";
5 |
6 | export const stackName = loadArg({
7 | cliArg: "stack",
8 | processEnvName: "CFN_STACK_NAME",
9 | });
10 |
11 | const profile = loadArg({
12 | cliArg: "profile",
13 | processEnvName: "AWS_PROFILE",
14 | defaultValue: "default",
15 | });
16 |
17 | export const region = loadArg({
18 | cliArg: "region",
19 | processEnvName: "AWS_REGION",
20 | defaultValue: "eu-west-2",
21 | });
22 |
23 | let creds;
24 |
25 | if (
26 | process.env.AWS_ACCESS_KEY_ID !== undefined &&
27 | process.env.AWS_SECRET_ACCESS_KEY !== undefined
28 | ) {
29 | creds = new AWS.Credentials({
30 | accessKeyId: process.env.AWS_ACCESS_KEY_ID,
31 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
32 | sessionToken: process.env.AWS_SESSION_TOKEN,
33 | });
34 | } else {
35 | creds = new AWS.SharedIniFileCredentials({
36 | profile,
37 | callback: (err) => {
38 | if (err) {
39 | console.error(`SharedIniFileCreds Error: ${err.name} - ${err.message}`);
40 | }
41 | },
42 | });
43 | }
44 |
45 | AWS.config.credentials = creds;
46 | AWS.config.region = region;
47 |
48 | export const AWSClient = AWS;
49 |
50 | const cloudformation = new AWSClient.CloudFormation();
51 |
52 | export const getStackResources = (
53 | stack: string | undefined
54 | ): Promise> =>
55 | cloudformation
56 | .describeStacks({ StackName: stack })
57 | .promise()
58 | .catch((error) => {
59 | console.error(error);
60 | });
61 |
62 | const apigateway = new AWSClient.APIGateway();
63 | let apiKey: string | null = null;
64 |
65 | interface GetOptionsOutput {
66 | method: string;
67 | headers: { "x-api-key": string | null; "Content-Type": string };
68 | }
69 |
70 | export const getOptions = async (): Promise => {
71 | if (apiKey === null) {
72 | const resources = await cloudformation
73 | .listStackResources({ StackName: stackName })
74 | .promise();
75 |
76 | const stackResourceSummaries = resources.StackResourceSummaries;
77 | if (stackResourceSummaries === undefined) {
78 | return;
79 | }
80 |
81 | const stackResourceSummary = stackResourceSummaries.find(
82 | (r) => r.ResourceType === "AWS::ApiGateway::ApiKey"
83 | );
84 |
85 | if (stackResourceSummary === undefined) {
86 | return;
87 | }
88 |
89 | const id = stackResourceSummary.PhysicalResourceId;
90 |
91 | if (id === undefined) {
92 | return;
93 | }
94 | const params = {
95 | apiKey: id,
96 | includeValue: true,
97 | };
98 |
99 | const data = await apigateway.getApiKey(params).promise();
100 | apiKey = data.value !== undefined ? data.value : null;
101 | }
102 |
103 | return {
104 | method: "POST",
105 | headers: {
106 | "x-api-key": apiKey,
107 | "Content-Type": "application/json",
108 | },
109 | };
110 | };
111 |
--------------------------------------------------------------------------------
/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export { default as EventBridge } from "./eventBridge";
2 | export { default as StepFunctions } from "./stepFunctions";
3 | export * from "./general";
4 | export * from "./cognito";
5 |
--------------------------------------------------------------------------------
/src/helpers/stepFunctions.ts:
--------------------------------------------------------------------------------
1 | import { StepFunctions as AWSStepFunctions } from "aws-sdk";
2 | import { AWSClient } from "./general";
3 |
4 | const API_POLLING_DELAY_MS = 1000;
5 |
6 | export default class StepFunctions {
7 | stepFunctions: AWSStepFunctions | undefined;
8 | allStateMachines: AWSStepFunctions.ListStateMachinesOutput | undefined;
9 |
10 | async init(): Promise {
11 | this.stepFunctions = new AWSClient.StepFunctions();
12 | this.allStateMachines = await this.stepFunctions
13 | .listStateMachines()
14 | .promise();
15 | }
16 |
17 | static async build(): Promise {
18 | const stepFunction = new StepFunctions();
19 | await stepFunction.init();
20 |
21 | return stepFunction;
22 | }
23 |
24 | async runExecution(
25 | stateMachineName: string,
26 | input: unknown
27 | ): Promise {
28 | if (this.allStateMachines === undefined) {
29 | throw new Error(
30 | "The list of state machines is undefined. You might have forgotten to run build()."
31 | );
32 | }
33 | const smList = this.allStateMachines.stateMachines.filter(
34 | (stateMachine: AWSStepFunctions.StateMachineListItem) =>
35 | stateMachine.name === stateMachineName
36 | );
37 | const stateMachineArn = smList[0].stateMachineArn;
38 | const executionParams = {
39 | stateMachineArn: stateMachineArn,
40 | input: JSON.stringify(input),
41 | };
42 | if (this.stepFunctions === undefined) {
43 | throw new Error(
44 | "The Step Functions client is undefined. You might have forgotten to run build()."
45 | );
46 | }
47 | const execution: AWSStepFunctions.StartExecutionOutput =
48 | await this.stepFunctions.startExecution(executionParams).promise();
49 | const listExecParams = { stateMachineArn: stateMachineArn };
50 | let executionList = await this.stepFunctions
51 | .listExecutions(listExecParams)
52 | .promise();
53 | // Poll until the given execution is no longer running
54 | while (
55 | executionList.executions.filter(
56 | (exec: AWSStepFunctions.ExecutionListItem) =>
57 | exec.executionArn === execution.executionArn &&
58 | exec.status === "RUNNING"
59 | ).length !== 0
60 | ) {
61 | executionList = await this.stepFunctions
62 | .listExecutions(listExecParams)
63 | .promise();
64 |
65 | // Wait before retrying to avoid throttle limits
66 | await new Promise((resolve) => setTimeout(resolve, API_POLLING_DELAY_MS));
67 | }
68 |
69 | return await this.stepFunctions
70 | .describeExecution({ executionArn: execution.executionArn })
71 | .promise();
72 | }
73 |
74 | async obtainStateMachineArn(stateMachineName: string): Promise {
75 | const listStateMachineParams = {};
76 | // Get all state machines
77 | if (this.stepFunctions === undefined) {
78 | throw new Error(
79 | "The Step Functions client is undefined. You might have forgotten to run build()."
80 | );
81 | }
82 | const allStateMachines = await this.stepFunctions
83 | .listStateMachines(listStateMachineParams)
84 | .promise();
85 | // Find state machine with specified name and get its arn
86 | const smList = allStateMachines.stateMachines.find(
87 | (stateMachine: AWSStepFunctions.StateMachineListItem) =>
88 | stateMachine.name === stateMachineName
89 | );
90 | if (smList == null) throw new Error("No matching state machine. ");
91 |
92 | return smList.stateMachineArn;
93 | }
94 |
95 | async obtainExecutionArn(StateMachineArn: string): Promise {
96 | const listExecParams = { stateMachineArn: StateMachineArn };
97 | if (this.stepFunctions == null) {
98 | throw new Error(
99 | "The Step Functions client is undefined. You might have forgotten to run build()."
100 | );
101 | }
102 |
103 | // Get all executions for this stateMachine
104 | const executionList = await this.stepFunctions
105 | .listExecutions(listExecParams)
106 | .promise();
107 |
108 | return executionList.executions[0].executionArn;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/helpers/utils/loadArg.ts:
--------------------------------------------------------------------------------
1 | import yargs from "yargs";
2 |
3 | const argv = yargs(process.argv).argv as Record;
4 |
5 | const isNonEmptyString = (arg: unknown): arg is string =>
6 | typeof arg === "string" && arg !== "";
7 |
8 | export const loadArg = ({
9 | cliArg,
10 | processEnvName,
11 | defaultValue,
12 | }: {
13 | cliArg: string;
14 | processEnvName: string;
15 | defaultValue?: string;
16 | }): string => {
17 | let arg = argv[cliArg];
18 |
19 | if (isNonEmptyString(arg)) {
20 | return arg;
21 | }
22 |
23 | arg = process.env[processEnvName];
24 |
25 | if (isNonEmptyString(arg)) {
26 | return arg;
27 | }
28 |
29 | if (defaultValue === undefined) {
30 | throw new Error(
31 | `--${cliArg} CLI argument or ${processEnvName} env var required.`
32 | );
33 | }
34 |
35 | return defaultValue;
36 | };
37 |
--------------------------------------------------------------------------------
/src/helpers/utils/removeUndefinedMessages.ts:
--------------------------------------------------------------------------------
1 | interface MessageHandler {
2 | Id: string | undefined;
3 | ReceiptHandle: string | undefined;
4 | }
5 | interface DefinedMessageHandler {
6 | Id: string;
7 | ReceiptHandle: string;
8 | }
9 |
10 | export const removeUndefinedMessages = (
11 | messageHandlers: MessageHandler[] | undefined
12 | ): DefinedMessageHandler[] | undefined =>
13 | messageHandlers?.filter(
14 | (messageHandler): messageHandler is DefinedMessageHandler =>
15 | messageHandler.Id !== undefined &&
16 | messageHandler.ReceiptHandle !== undefined
17 | );
18 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | /* eslint-disable @typescript-eslint/no-namespace */
3 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4 | /* eslint-disable @typescript-eslint/no-unsafe-call */
5 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */
6 | import matchers from "./assertions";
7 | import { isGlobalWithExpectKey } from "./assertions/utils/globalTypeChecker";
8 |
9 | if (isGlobalWithExpectKey(global)) {
10 | const jestExpect = global.expect;
11 |
12 | if (jestExpect !== undefined) {
13 | jestExpect.extend(matchers);
14 | } else {
15 | console.error("Unable to find Jest's global expect.");
16 | }
17 | }
18 |
19 | export * from "./helpers";
20 |
21 | declare global {
22 | namespace jest {
23 | interface Matchers {
24 | toExistAsS3Bucket(): Promise;
25 | toExistInDynamoTable(table: string): Promise;
26 | toHaveContentEqualTo(
27 | content: Record | string
28 | ): Promise;
29 | toHaveContentTypeEqualTo(contentType: string): Promise;
30 | toHaveEvent(): R;
31 | toHaveEventWithSource(expectedSourceName: string): R;
32 | toHaveS3ObjectWithNameEqualTo(objectName: string): Promise;
33 | toContainItemWithValues(values:{ [key: string]: unknown }): Promise;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/utils/testResult.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/prefer-default-export
2 |
3 | export interface TestResultOutput {
4 | message: () => string;
5 | pass: boolean;
6 | }
7 |
8 | export const testResult = (
9 | message: string,
10 | pass: boolean
11 | ): TestResultOutput => ({
12 | message: () => message,
13 | pass,
14 | });
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ESNext"],
4 | "skipLibCheck": true,
5 | "moduleResolution": "node",
6 | "noUnusedLocals": true,
7 | "noUnusedParameters": true,
8 | "removeComments": true,
9 | "sourceMap": true,
10 | "target": "ES2020",
11 | "strict": true,
12 | "baseUrl": "src",
13 | "composite": true,
14 | "plugins": [{ "transform": "@zerollup/ts-transform-paths" }],
15 | "emitDeclarationOnly": true,
16 | "outDir": "./lib/types",
17 | "rootDir": "./src",
18 | "esModuleInterop": true
19 | },
20 | "include": ["./src/**/*.ts"]
21 | }
22 |
--------------------------------------------------------------------------------