From 2b2bf2364a9570475b830a7da2f355543e85d26f Mon Sep 17 00:00:00 2001 From: svozza Date: Thu, 10 Sep 2026 20:08:53 +0000 Subject: [PATCH 1/4] chore(testing): replace AWS SDK Vitest matcher dependency Implement toReceiveCommandWith locally with typed inputs, partial matching, and failure diagnostics. Remove the dependency and update documentation and test configuration. Closes #5692 --- docs/features/parameters.md | 4 +- .../parameters/testingYourCodeClientMock.ts | 1 - package-lock.json | 22 +-- packages/event-handler/tests/tsconfig.json | 7 +- packages/idempotency/tests/tsconfig.json | 7 +- packages/kafka/tests/tsconfig.json | 7 +- packages/logger/tests/tsconfig.json | 7 +- packages/metrics/tests/tsconfig.json | 7 +- packages/parameters/tests/tsconfig.json | 7 +- packages/testing/package.json | 4 +- packages/testing/src/setupEnv.ts | 10 +- packages/testing/src/toReceiveCommandWith.ts | 54 ++++++ .../types/toReceiveCommandWith.test-d.ts | 29 +++ .../tests/unit/toReceiveCommandWith.test.ts | 170 ++++++++++++++++++ packages/validation/tests/tsconfig.json | 7 +- 15 files changed, 306 insertions(+), 37 deletions(-) create mode 100644 packages/testing/src/toReceiveCommandWith.ts create mode 100644 packages/testing/tests/types/toReceiveCommandWith.test-d.ts create mode 100644 packages/testing/tests/unit/toReceiveCommandWith.test.ts diff --git a/docs/features/parameters.md b/docs/features/parameters.md index e7f16500af..df1aadc0a9 100644 --- a/docs/features/parameters.md +++ b/docs/features/parameters.md @@ -522,10 +522,10 @@ A similar pattern can be applied also to any of the built-in provider classes - --8<-- "examples/snippets/parameters/testingYourCodeProvidersHandler.ts" ``` -For when you want to mock the AWS SDK v3 client directly, we recommend using the [`aws-sdk-client-mock`](https://www.npmjs.com/package/aws-sdk-client-mock) and [`aws-sdk-client-mock-vitest`](https://www.npmjs.com/package/aws-sdk-client-mock-vitest) libraries. This is useful when you want to test how your code behaves when the AWS SDK v3 client throws an error or a specific response. +For when you want to mock the AWS SDK v3 client directly, we recommend using the [`aws-sdk-client-mock`](https://www.npmjs.com/package/aws-sdk-client-mock) library. This is useful when you want to test how your code behaves when the AWS SDK v3 client throws an error or a specific response. === "handler.test.ts" - ```typescript hl_lines="2-7 12 16 21-28" + ```typescript hl_lines="2-7 11 15 20-27" --8<-- "examples/snippets/parameters/testingYourCodeClientMock.ts" ``` diff --git a/examples/snippets/parameters/testingYourCodeClientMock.ts b/examples/snippets/parameters/testingYourCodeClientMock.ts index 38e3416215..ca835cae2c 100644 --- a/examples/snippets/parameters/testingYourCodeClientMock.ts +++ b/examples/snippets/parameters/testingYourCodeClientMock.ts @@ -6,7 +6,6 @@ import { import { mockClient } from 'aws-sdk-client-mock'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { handler } from './testingYourCodeFunctionsHandler.js'; -import 'aws-sdk-client-mock-vitest'; describe('Function tests', () => { const client = mockClient(SecretsManagerClient); diff --git a/package-lock.json b/package-lock.json index 82696a5852..ef9006297a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4309,24 +4309,6 @@ "tslib": "^2.1.0" } }, - "node_modules/aws-sdk-client-mock-vitest": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/aws-sdk-client-mock-vitest/-/aws-sdk-client-mock-vitest-7.1.0.tgz", - "integrity": "sha512-Qjl5cHdTysOfsq7ac0UFchQKW/A4NbVL4uhU1FcPmawAQQ8Bt6xsTJA//gWCkZO9+hHQxcCJfpmWbSYm02po1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "^4.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@smithy/types": ">=3.5.0", - "aws-sdk-client-mock": ">=2.2.0", - "vitest": ">=3.2.0" - } - }, "node_modules/aws-xray-sdk-core": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.12.0.tgz", @@ -8715,9 +8697,9 @@ "promise-retry": "^2.0.1" }, "devDependencies": { + "@smithy/types": "^4.18.0", "@types/promise-retry": "^1.1.6", - "aws-sdk-client-mock": "^4.1.0", - "aws-sdk-client-mock-vitest": "^7.1.0" + "aws-sdk-client-mock": "^4.1.0" } }, "packages/tracer": { diff --git a/packages/event-handler/tests/tsconfig.json b/packages/event-handler/tests/tsconfig.json index 39f442212e..6b03de0ead 100644 --- a/packages/event-handler/tests/tsconfig.json +++ b/packages/event-handler/tests/tsconfig.json @@ -4,5 +4,10 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] + "include": [ + "../../testing/src/setupEnv.ts", + "../../testing/src/toReceiveCommandWith.ts", + "../src/**/*", + "./**/*" + ] } diff --git a/packages/idempotency/tests/tsconfig.json b/packages/idempotency/tests/tsconfig.json index 39f442212e..6b03de0ead 100644 --- a/packages/idempotency/tests/tsconfig.json +++ b/packages/idempotency/tests/tsconfig.json @@ -4,5 +4,10 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] + "include": [ + "../../testing/src/setupEnv.ts", + "../../testing/src/toReceiveCommandWith.ts", + "../src/**/*", + "./**/*" + ] } diff --git a/packages/kafka/tests/tsconfig.json b/packages/kafka/tests/tsconfig.json index daf72b90df..c1fb36205a 100644 --- a/packages/kafka/tests/tsconfig.json +++ b/packages/kafka/tests/tsconfig.json @@ -5,5 +5,10 @@ "noEmit": true, "resolveJsonModule": true }, - "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] + "include": [ + "../../testing/src/setupEnv.ts", + "../../testing/src/toReceiveCommandWith.ts", + "../src/**/*", + "./**/*" + ] } diff --git a/packages/logger/tests/tsconfig.json b/packages/logger/tests/tsconfig.json index 39f442212e..6b03de0ead 100644 --- a/packages/logger/tests/tsconfig.json +++ b/packages/logger/tests/tsconfig.json @@ -4,5 +4,10 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] + "include": [ + "../../testing/src/setupEnv.ts", + "../../testing/src/toReceiveCommandWith.ts", + "../src/**/*", + "./**/*" + ] } diff --git a/packages/metrics/tests/tsconfig.json b/packages/metrics/tests/tsconfig.json index 39f442212e..6b03de0ead 100644 --- a/packages/metrics/tests/tsconfig.json +++ b/packages/metrics/tests/tsconfig.json @@ -4,5 +4,10 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] + "include": [ + "../../testing/src/setupEnv.ts", + "../../testing/src/toReceiveCommandWith.ts", + "../src/**/*", + "./**/*" + ] } diff --git a/packages/parameters/tests/tsconfig.json b/packages/parameters/tests/tsconfig.json index 39f442212e..6b03de0ead 100644 --- a/packages/parameters/tests/tsconfig.json +++ b/packages/parameters/tests/tsconfig.json @@ -4,5 +4,10 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] + "include": [ + "../../testing/src/setupEnv.ts", + "../../testing/src/toReceiveCommandWith.ts", + "../src/**/*", + "./**/*" + ] } diff --git a/packages/testing/package.json b/packages/testing/package.json index 85d6a5af66..f0cabdcc02 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -137,8 +137,8 @@ "promise-retry": "^2.0.1" }, "devDependencies": { + "@smithy/types": "^4.18.0", "@types/promise-retry": "^1.1.6", - "aws-sdk-client-mock": "^4.1.0", - "aws-sdk-client-mock-vitest": "^7.1.0" + "aws-sdk-client-mock": "^4.1.0" } } diff --git a/packages/testing/src/setupEnv.ts b/packages/testing/src/setupEnv.ts index b00e9c7e9b..42a075ec08 100644 --- a/packages/testing/src/setupEnv.ts +++ b/packages/testing/src/setupEnv.ts @@ -1,8 +1,8 @@ +import { expect, vi } from 'vitest'; import { - type CustomMatcher, + type CommandMatcher, toReceiveCommandWith, -} from 'aws-sdk-client-mock-vitest'; -import { expect, vi } from 'vitest'; +} from './toReceiveCommandWith.js'; expect.extend({ toReceiveCommandWith }); @@ -234,7 +234,7 @@ expect.addEqualityTesters([ declare module 'vitest' { // biome-ignore lint/suspicious/noExplicitAny: vitest typings expect an any type - interface Assertion extends CustomMatcher { + interface Assertion extends CommandMatcher { /** * Asserts that the logger function has been called with the expected log message * during any call. @@ -360,7 +360,7 @@ declare module 'vitest' { expected: Record ): void; } - interface AsymmetricMatchersContaining extends CustomMatcher {} + interface AsymmetricMatchersContaining extends CommandMatcher {} } // Set up environment variables for testing diff --git a/packages/testing/src/toReceiveCommandWith.ts b/packages/testing/src/toReceiveCommandWith.ts new file mode 100644 index 0000000000..e5c18703b0 --- /dev/null +++ b/packages/testing/src/toReceiveCommandWith.ts @@ -0,0 +1,54 @@ +import type { MetadataBearer } from '@smithy/types'; +import type { AwsCommand, AwsStub } from 'aws-sdk-client-mock'; +import { expect, type MatcherResult, type MatcherState } from 'vitest'; + +/** + * Describes the AWS command matcher available in test assertions. + */ +interface CommandMatcher { + /** + * Asserts that at least one call to the command contains the expected input. + * + * @param command - The AWS SDK command constructor + * @param expected - The expected subset of the command input + */ + toReceiveCommandWith( + command: new (input: Input) => AwsCommand, + expected: Partial> + ): void; +} + +/** + * Matches recorded AWS SDK command inputs using Vitest's partial object matching. + * + * @param received - The mocked AWS SDK client + * @param command - The AWS SDK command constructor + * @param expected - The expected subset of the command input + */ +function toReceiveCommandWith< + Input extends object, + Output extends MetadataBearer, +>( + this: MatcherState, + received: AwsStub, + command: new (input: Input) => AwsCommand, + expected: Partial +): MatcherResult { + const inputs = received + .commandCalls(command) + .map((call) => call.args[0].input); + const pass = inputs.some((input) => + this.equals(input, expect.objectContaining(expected)) + ); + + return { + pass, + message: () => + `Expected ${received.clientName()} ${command.name} ${this.isNot ? 'not ' : ''}to receive input containing ${this.utils.printExpected(expected)}\nReceived inputs (call count: ${inputs.length}): ${this.utils.printReceived(inputs)}`, + actual: inputs, + expected, + }; +} + +export type { CommandMatcher }; +export { toReceiveCommandWith }; diff --git a/packages/testing/tests/types/toReceiveCommandWith.test-d.ts b/packages/testing/tests/types/toReceiveCommandWith.test-d.ts new file mode 100644 index 0000000000..418ccd2716 --- /dev/null +++ b/packages/testing/tests/types/toReceiveCommandWith.test-d.ts @@ -0,0 +1,29 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { expect, expectTypeOf, it } from 'vitest'; +import type { CommandMatcher } from '../../src/toReceiveCommandWith.js'; +import '../../src/setupEnv.js'; + +it('infers the expected input from the command constructor', () => { + // Prepare + const assertion = expect({}); + + // Act & Assess + expectTypeOf(assertion).toExtend(); + expectTypeOf( + assertion.toReceiveCommandWith(DescribeStacksCommand, { + StackName: 'stack', + }) + ).toBeVoid(); + assertion.toReceiveCommandWith(DescribeStacksCommand, {}); + assertion.toReceiveCommandWith(DescribeStacksCommand, { + StackName: expect.any(String), + }); + assertion.toReceiveCommandWith(DescribeStacksCommand, { + // @ts-expect-error StackName must be a string + StackName: 123, + }); + assertion.toReceiveCommandWith(DescribeStacksCommand, { + // @ts-expect-error UnknownField is not a DescribeStacks input + UnknownField: 'value', + }); +}); diff --git a/packages/testing/tests/unit/toReceiveCommandWith.test.ts b/packages/testing/tests/unit/toReceiveCommandWith.test.ts new file mode 100644 index 0000000000..932b467e04 --- /dev/null +++ b/packages/testing/tests/unit/toReceiveCommandWith.test.ts @@ -0,0 +1,170 @@ +import { + CloudFormationClient, + CreateStackCommand, + DeleteStackCommand, + DescribeStacksCommand, +} from '@aws-sdk/client-cloudformation'; +import { mockClient } from 'aws-sdk-client-mock'; +import { afterAll, beforeEach, describe, expect, it } from 'vitest'; +import '../../src/setupEnv.js'; + +describe('toReceiveCommandWith', () => { + const client = new CloudFormationClient({}); + const clientMock = mockClient(client); + + beforeEach(() => { + clientMock.reset(); + clientMock.resolves({}); + }); + + afterAll(() => { + clientMock.restore(); + client.destroy(); + }); + + it.each([ + { StackName: 'my-stack', NextToken: 'next' }, + { StackName: 'my-stack' }, + { StackName: expect.stringContaining('stack') }, + {}, + ])('matches the expected input %j', async (expected) => { + // Prepare + const command = new DescribeStacksCommand({ + StackName: 'my-stack', + NextToken: 'next', + }); + + // Act + await client.send(command); + + // Assess + expect(clientMock).toReceiveCommandWith(DescribeStacksCommand, expected); + }); + + it('matches nested asymmetric matchers', async () => { + // Prepare + const command = new CreateStackCommand({ + StackName: 'my-stack', + Tags: [{ Key: 'service', Value: 'my-service' }], + }); + + // Act + await client.send(command); + + // Assess + expect(clientMock).toReceiveCommandWith(CreateStackCommand, { + Tags: expect.arrayContaining([ + expect.objectContaining({ Value: expect.stringContaining('service') }), + ]), + }); + }); + + it('requires nested objects to match unless an asymmetric matcher is used', async () => { + // Prepare + const command = new CreateStackCommand({ + StackName: 'my-stack', + Parameters: [{ ParameterKey: 'service', ParameterValue: 'my-service' }], + }); + + // Act + await client.send(command); + + // Assess + expect(clientMock).not.toReceiveCommandWith(CreateStackCommand, { + Parameters: [{ ParameterKey: 'service' }], + }); + }); + + it('finds a matching input among multiple calls', async () => { + // Prepare + const stackNames = ['first', 'matching', 'last']; + + // Act + for (const StackName of stackNames) { + await client.send(new DescribeStacksCommand({ StackName })); + } + + // Assess + expect(clientMock).toReceiveCommandWith(DescribeStacksCommand, { + StackName: 'matching', + }); + }); + + it('ignores matching inputs sent to a different command', async () => { + // Prepare + const input = { StackName: 'my-stack' }; + + // Act + await client.send(new DeleteStackCommand(input)); + + // Assess + expect(clientMock).not.toReceiveCommandWith(DescribeStacksCommand, input); + expect(() => + expect(clientMock).toReceiveCommandWith(DescribeStacksCommand, input) + ).toThrow('Received inputs (call count: 0):'); + }); + + it('reports the expected input and recorded inputs on a mismatch', async () => { + // Prepare + const command = new DescribeStacksCommand({ StackName: 'actual-stack' }); + + // Act + await client.send(command); + + // Assess + expect(clientMock).not.toReceiveCommandWith(DescribeStacksCommand, { + StackName: 'expected-stack', + }); + expect(() => + expect(clientMock).toReceiveCommandWith(DescribeStacksCommand, { + StackName: 'expected-stack', + }) + ).toThrow( + /CloudFormationClient DescribeStacksCommand to receive input containing.*expected-stack.*\nReceived inputs \(call count: 1\):.*actual-stack/s + ); + }); + + it('reports no calls for an unused client', () => { + // Prepare + const expected = { StackName: 'my-stack' }; + + // Act + const assertCommand = () => + expect(clientMock).toReceiveCommandWith(DescribeStacksCommand, expected); + + // Assess + expect(assertCommand).toThrow('Received inputs (call count: 0):'); + expect(clientMock).not.toReceiveCommandWith( + DescribeStacksCommand, + expected + ); + }); + + it('reports a matching call when a negated assertion fails', async () => { + // Prepare + const input = { StackName: 'my-stack' }; + + // Act + await client.send(new DescribeStacksCommand(input)); + + // Assess + expect(() => + expect(clientMock).not.toReceiveCommandWith(DescribeStacksCommand, input) + ).toThrow( + /DescribeStacksCommand not to receive input containing.*my-stack/s + ); + }); + + it('supports asymmetric command assertions', async () => { + // Prepare + const input = { StackName: 'my-stack' }; + + // Act + await client.send(new DescribeStacksCommand(input)); + + // Assess + expect({ client: clientMock }).toEqual({ + client: expect.toReceiveCommandWith(DescribeStacksCommand, input), + }); + }); +}); diff --git a/packages/validation/tests/tsconfig.json b/packages/validation/tests/tsconfig.json index 39f442212e..6b03de0ead 100644 --- a/packages/validation/tests/tsconfig.json +++ b/packages/validation/tests/tsconfig.json @@ -4,5 +4,10 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] + "include": [ + "../../testing/src/setupEnv.ts", + "../../testing/src/toReceiveCommandWith.ts", + "../src/**/*", + "./**/*" + ] } From 454d18f2e92f1954898c26d5c31252e89c79d5e7 Mon Sep 17 00:00:00 2001 From: svozza Date: Thu, 10 Sep 2026 20:16:32 +0000 Subject: [PATCH 2/4] chore(testing): preserve tsconfig include formatting --- packages/event-handler/tests/tsconfig.json | 7 +------ packages/idempotency/tests/tsconfig.json | 7 +------ packages/kafka/tests/tsconfig.json | 7 +------ packages/logger/tests/tsconfig.json | 7 +------ packages/metrics/tests/tsconfig.json | 7 +------ packages/parameters/tests/tsconfig.json | 7 +------ packages/validation/tests/tsconfig.json | 7 +------ 7 files changed, 7 insertions(+), 42 deletions(-) diff --git a/packages/event-handler/tests/tsconfig.json b/packages/event-handler/tests/tsconfig.json index 6b03de0ead..902bf042d4 100644 --- a/packages/event-handler/tests/tsconfig.json +++ b/packages/event-handler/tests/tsconfig.json @@ -4,10 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": [ - "../../testing/src/setupEnv.ts", - "../../testing/src/toReceiveCommandWith.ts", - "../src/**/*", - "./**/*" - ] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] } diff --git a/packages/idempotency/tests/tsconfig.json b/packages/idempotency/tests/tsconfig.json index 6b03de0ead..902bf042d4 100644 --- a/packages/idempotency/tests/tsconfig.json +++ b/packages/idempotency/tests/tsconfig.json @@ -4,10 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": [ - "../../testing/src/setupEnv.ts", - "../../testing/src/toReceiveCommandWith.ts", - "../src/**/*", - "./**/*" - ] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] } diff --git a/packages/kafka/tests/tsconfig.json b/packages/kafka/tests/tsconfig.json index c1fb36205a..9885e0b2f8 100644 --- a/packages/kafka/tests/tsconfig.json +++ b/packages/kafka/tests/tsconfig.json @@ -5,10 +5,5 @@ "noEmit": true, "resolveJsonModule": true }, - "include": [ - "../../testing/src/setupEnv.ts", - "../../testing/src/toReceiveCommandWith.ts", - "../src/**/*", - "./**/*" - ] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] } diff --git a/packages/logger/tests/tsconfig.json b/packages/logger/tests/tsconfig.json index 6b03de0ead..902bf042d4 100644 --- a/packages/logger/tests/tsconfig.json +++ b/packages/logger/tests/tsconfig.json @@ -4,10 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": [ - "../../testing/src/setupEnv.ts", - "../../testing/src/toReceiveCommandWith.ts", - "../src/**/*", - "./**/*" - ] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] } diff --git a/packages/metrics/tests/tsconfig.json b/packages/metrics/tests/tsconfig.json index 6b03de0ead..902bf042d4 100644 --- a/packages/metrics/tests/tsconfig.json +++ b/packages/metrics/tests/tsconfig.json @@ -4,10 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": [ - "../../testing/src/setupEnv.ts", - "../../testing/src/toReceiveCommandWith.ts", - "../src/**/*", - "./**/*" - ] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] } diff --git a/packages/parameters/tests/tsconfig.json b/packages/parameters/tests/tsconfig.json index 6b03de0ead..902bf042d4 100644 --- a/packages/parameters/tests/tsconfig.json +++ b/packages/parameters/tests/tsconfig.json @@ -4,10 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": [ - "../../testing/src/setupEnv.ts", - "../../testing/src/toReceiveCommandWith.ts", - "../src/**/*", - "./**/*" - ] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] } diff --git a/packages/validation/tests/tsconfig.json b/packages/validation/tests/tsconfig.json index 6b03de0ead..902bf042d4 100644 --- a/packages/validation/tests/tsconfig.json +++ b/packages/validation/tests/tsconfig.json @@ -4,10 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": [ - "../../testing/src/setupEnv.ts", - "../../testing/src/toReceiveCommandWith.ts", - "../src/**/*", - "./**/*" - ] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] } From 984489330f10d38ae64ebe38d06975d3829715da Mon Sep 17 00:00:00 2001 From: svozza Date: Thu, 10 Sep 2026 20:19:36 +0000 Subject: [PATCH 3/4] refactor(testing): group AWS SDK matchers in a shared module --- packages/event-handler/tests/tsconfig.json | 2 +- packages/idempotency/tests/tsconfig.json | 2 +- packages/kafka/tests/tsconfig.json | 2 +- packages/logger/tests/tsconfig.json | 2 +- packages/metrics/tests/tsconfig.json | 2 +- packages/parameters/tests/tsconfig.json | 2 +- .../src/{toReceiveCommandWith.ts => awsSdkMatchers.ts} | 6 +++--- packages/testing/src/setupEnv.ts | 9 +++------ .../testing/tests/types/toReceiveCommandWith.test-d.ts | 4 ++-- packages/validation/tests/tsconfig.json | 2 +- 10 files changed, 15 insertions(+), 18 deletions(-) rename packages/testing/src/{toReceiveCommandWith.ts => awsSdkMatchers.ts} (93%) diff --git a/packages/event-handler/tests/tsconfig.json b/packages/event-handler/tests/tsconfig.json index 902bf042d4..521f7eb8e5 100644 --- a/packages/event-handler/tests/tsconfig.json +++ b/packages/event-handler/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] } diff --git a/packages/idempotency/tests/tsconfig.json b/packages/idempotency/tests/tsconfig.json index 902bf042d4..521f7eb8e5 100644 --- a/packages/idempotency/tests/tsconfig.json +++ b/packages/idempotency/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] } diff --git a/packages/kafka/tests/tsconfig.json b/packages/kafka/tests/tsconfig.json index 9885e0b2f8..3a34706bc1 100644 --- a/packages/kafka/tests/tsconfig.json +++ b/packages/kafka/tests/tsconfig.json @@ -5,5 +5,5 @@ "noEmit": true, "resolveJsonModule": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] } diff --git a/packages/logger/tests/tsconfig.json b/packages/logger/tests/tsconfig.json index 902bf042d4..521f7eb8e5 100644 --- a/packages/logger/tests/tsconfig.json +++ b/packages/logger/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] } diff --git a/packages/metrics/tests/tsconfig.json b/packages/metrics/tests/tsconfig.json index 902bf042d4..521f7eb8e5 100644 --- a/packages/metrics/tests/tsconfig.json +++ b/packages/metrics/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] } diff --git a/packages/parameters/tests/tsconfig.json b/packages/parameters/tests/tsconfig.json index 902bf042d4..521f7eb8e5 100644 --- a/packages/parameters/tests/tsconfig.json +++ b/packages/parameters/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] } diff --git a/packages/testing/src/toReceiveCommandWith.ts b/packages/testing/src/awsSdkMatchers.ts similarity index 93% rename from packages/testing/src/toReceiveCommandWith.ts rename to packages/testing/src/awsSdkMatchers.ts index e5c18703b0..8d5f7a6412 100644 --- a/packages/testing/src/toReceiveCommandWith.ts +++ b/packages/testing/src/awsSdkMatchers.ts @@ -3,9 +3,9 @@ import type { AwsCommand, AwsStub } from 'aws-sdk-client-mock'; import { expect, type MatcherResult, type MatcherState } from 'vitest'; /** - * Describes the AWS command matcher available in test assertions. + * Describes the AWS SDK matchers available in test assertions. */ -interface CommandMatcher { +interface AwsSdkMatchers { /** * Asserts that at least one call to the command contains the expected input. * @@ -50,5 +50,5 @@ function toReceiveCommandWith< }; } -export type { CommandMatcher }; +export type { AwsSdkMatchers }; export { toReceiveCommandWith }; diff --git a/packages/testing/src/setupEnv.ts b/packages/testing/src/setupEnv.ts index 42a075ec08..5a9adca49f 100644 --- a/packages/testing/src/setupEnv.ts +++ b/packages/testing/src/setupEnv.ts @@ -1,8 +1,5 @@ import { expect, vi } from 'vitest'; -import { - type CommandMatcher, - toReceiveCommandWith, -} from './toReceiveCommandWith.js'; +import { type AwsSdkMatchers, toReceiveCommandWith } from './awsSdkMatchers.js'; expect.extend({ toReceiveCommandWith }); @@ -234,7 +231,7 @@ expect.addEqualityTesters([ declare module 'vitest' { // biome-ignore lint/suspicious/noExplicitAny: vitest typings expect an any type - interface Assertion extends CommandMatcher { + interface Assertion extends AwsSdkMatchers { /** * Asserts that the logger function has been called with the expected log message * during any call. @@ -360,7 +357,7 @@ declare module 'vitest' { expected: Record ): void; } - interface AsymmetricMatchersContaining extends CommandMatcher {} + interface AsymmetricMatchersContaining extends AwsSdkMatchers {} } // Set up environment variables for testing diff --git a/packages/testing/tests/types/toReceiveCommandWith.test-d.ts b/packages/testing/tests/types/toReceiveCommandWith.test-d.ts index 418ccd2716..17f546d66e 100644 --- a/packages/testing/tests/types/toReceiveCommandWith.test-d.ts +++ b/packages/testing/tests/types/toReceiveCommandWith.test-d.ts @@ -1,6 +1,6 @@ import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; import { expect, expectTypeOf, it } from 'vitest'; -import type { CommandMatcher } from '../../src/toReceiveCommandWith.js'; +import type { AwsSdkMatchers } from '../../src/awsSdkMatchers.js'; import '../../src/setupEnv.js'; it('infers the expected input from the command constructor', () => { @@ -8,7 +8,7 @@ it('infers the expected input from the command constructor', () => { const assertion = expect({}); // Act & Assess - expectTypeOf(assertion).toExtend(); + expectTypeOf(assertion).toExtend(); expectTypeOf( assertion.toReceiveCommandWith(DescribeStacksCommand, { StackName: 'stack', diff --git a/packages/validation/tests/tsconfig.json b/packages/validation/tests/tsconfig.json index 902bf042d4..521f7eb8e5 100644 --- a/packages/validation/tests/tsconfig.json +++ b/packages/validation/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/toReceiveCommandWith.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] } From 6f7c3e08c26374b53a997647f66cbb92a9257cde Mon Sep 17 00:00:00 2001 From: svozza Date: Thu, 10 Sep 2026 21:15:23 +0000 Subject: [PATCH 4/4] refactor(tests): register AWS matcher alongside existing matchers --- packages/event-handler/tests/tsconfig.json | 2 +- packages/idempotency/tests/tsconfig.json | 2 +- packages/kafka/tests/tsconfig.json | 2 +- packages/logger/tests/tsconfig.json | 2 +- packages/metrics/tests/tsconfig.json | 2 +- packages/parameters/tests/tsconfig.json | 2 +- packages/testing/src/awsSdkMatchers.ts | 54 ------------------- packages/testing/src/setupEnv.ts | 50 +++++++++++++++-- .../types/toReceiveCommandWith.test-d.ts | 2 - packages/validation/tests/tsconfig.json | 2 +- 10 files changed, 53 insertions(+), 67 deletions(-) delete mode 100644 packages/testing/src/awsSdkMatchers.ts diff --git a/packages/event-handler/tests/tsconfig.json b/packages/event-handler/tests/tsconfig.json index 521f7eb8e5..39f442212e 100644 --- a/packages/event-handler/tests/tsconfig.json +++ b/packages/event-handler/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] } diff --git a/packages/idempotency/tests/tsconfig.json b/packages/idempotency/tests/tsconfig.json index 521f7eb8e5..39f442212e 100644 --- a/packages/idempotency/tests/tsconfig.json +++ b/packages/idempotency/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] } diff --git a/packages/kafka/tests/tsconfig.json b/packages/kafka/tests/tsconfig.json index 3a34706bc1..daf72b90df 100644 --- a/packages/kafka/tests/tsconfig.json +++ b/packages/kafka/tests/tsconfig.json @@ -5,5 +5,5 @@ "noEmit": true, "resolveJsonModule": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] } diff --git a/packages/logger/tests/tsconfig.json b/packages/logger/tests/tsconfig.json index 521f7eb8e5..39f442212e 100644 --- a/packages/logger/tests/tsconfig.json +++ b/packages/logger/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] } diff --git a/packages/metrics/tests/tsconfig.json b/packages/metrics/tests/tsconfig.json index 521f7eb8e5..39f442212e 100644 --- a/packages/metrics/tests/tsconfig.json +++ b/packages/metrics/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] } diff --git a/packages/parameters/tests/tsconfig.json b/packages/parameters/tests/tsconfig.json index 521f7eb8e5..39f442212e 100644 --- a/packages/parameters/tests/tsconfig.json +++ b/packages/parameters/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] } diff --git a/packages/testing/src/awsSdkMatchers.ts b/packages/testing/src/awsSdkMatchers.ts deleted file mode 100644 index 8d5f7a6412..0000000000 --- a/packages/testing/src/awsSdkMatchers.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { MetadataBearer } from '@smithy/types'; -import type { AwsCommand, AwsStub } from 'aws-sdk-client-mock'; -import { expect, type MatcherResult, type MatcherState } from 'vitest'; - -/** - * Describes the AWS SDK matchers available in test assertions. - */ -interface AwsSdkMatchers { - /** - * Asserts that at least one call to the command contains the expected input. - * - * @param command - The AWS SDK command constructor - * @param expected - The expected subset of the command input - */ - toReceiveCommandWith( - command: new (input: Input) => AwsCommand, - expected: Partial> - ): void; -} - -/** - * Matches recorded AWS SDK command inputs using Vitest's partial object matching. - * - * @param received - The mocked AWS SDK client - * @param command - The AWS SDK command constructor - * @param expected - The expected subset of the command input - */ -function toReceiveCommandWith< - Input extends object, - Output extends MetadataBearer, ->( - this: MatcherState, - received: AwsStub, - command: new (input: Input) => AwsCommand, - expected: Partial -): MatcherResult { - const inputs = received - .commandCalls(command) - .map((call) => call.args[0].input); - const pass = inputs.some((input) => - this.equals(input, expect.objectContaining(expected)) - ); - - return { - pass, - message: () => - `Expected ${received.clientName()} ${command.name} ${this.isNot ? 'not ' : ''}to receive input containing ${this.utils.printExpected(expected)}\nReceived inputs (call count: ${inputs.length}): ${this.utils.printReceived(inputs)}`, - actual: inputs, - expected, - }; -} - -export type { AwsSdkMatchers }; -export { toReceiveCommandWith }; diff --git a/packages/testing/src/setupEnv.ts b/packages/testing/src/setupEnv.ts index 5a9adca49f..f83c2cd5d7 100644 --- a/packages/testing/src/setupEnv.ts +++ b/packages/testing/src/setupEnv.ts @@ -1,7 +1,6 @@ -import { expect, vi } from 'vitest'; -import { type AwsSdkMatchers, toReceiveCommandWith } from './awsSdkMatchers.js'; - -expect.extend({ toReceiveCommandWith }); +import type { MetadataBearer } from '@smithy/types'; +import type { AwsCommand, AwsStub } from 'aws-sdk-client-mock'; +import { expect, type MatcherResult, vi } from 'vitest'; // Mock console methods to prevent output during tests vi.spyOn(console, 'error').mockReturnValue(); @@ -11,6 +10,33 @@ vi.spyOn(console, 'info').mockReturnValue(); vi.spyOn(console, 'log').mockReturnValue(); expect.extend({ + /** + * Matches recorded AWS SDK command inputs using Vitest's partial object matching. + * + * @param received - The mocked AWS SDK client + * @param command - The AWS SDK command constructor + * @param expected - The expected subset of the command input + */ + toReceiveCommandWith( + received: AwsStub, + command: new (input: Input) => AwsCommand, + expected: Partial + ): MatcherResult { + const inputs = received + .commandCalls(command) + .map((call) => call.args[0].input); + const pass = inputs.some((input) => + this.equals(input, expect.objectContaining(expected)) + ); + + return { + pass, + message: () => + `Expected ${received.clientName()} ${command.name} ${this.isNot ? 'not ' : ''}to receive input containing ${this.utils.printExpected(expected)}\nReceived inputs (call count: ${inputs.length}): ${this.utils.printReceived(inputs)}`, + actual: inputs, + expected, + }; + }, toHaveLogged(received, expected) { const calls = received.mock.calls; const messages = new Array(calls.length); @@ -229,6 +255,22 @@ expect.addEqualityTesters([ }, ]); +/** + * Describes the AWS SDK matchers available in test assertions. + */ +interface AwsSdkMatchers { + /** + * Asserts that at least one call to the command contains the expected input. + * + * @param command - The AWS SDK command constructor + * @param expected - The expected subset of the command input + */ + toReceiveCommandWith( + command: new (input: Input) => AwsCommand, + expected: Partial> + ): void; +} + declare module 'vitest' { // biome-ignore lint/suspicious/noExplicitAny: vitest typings expect an any type interface Assertion extends AwsSdkMatchers { diff --git a/packages/testing/tests/types/toReceiveCommandWith.test-d.ts b/packages/testing/tests/types/toReceiveCommandWith.test-d.ts index 17f546d66e..290b886d44 100644 --- a/packages/testing/tests/types/toReceiveCommandWith.test-d.ts +++ b/packages/testing/tests/types/toReceiveCommandWith.test-d.ts @@ -1,6 +1,5 @@ import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; import { expect, expectTypeOf, it } from 'vitest'; -import type { AwsSdkMatchers } from '../../src/awsSdkMatchers.js'; import '../../src/setupEnv.js'; it('infers the expected input from the command constructor', () => { @@ -8,7 +7,6 @@ it('infers the expected input from the command constructor', () => { const assertion = expect({}); // Act & Assess - expectTypeOf(assertion).toExtend(); expectTypeOf( assertion.toReceiveCommandWith(DescribeStacksCommand, { StackName: 'stack', diff --git a/packages/validation/tests/tsconfig.json b/packages/validation/tests/tsconfig.json index 521f7eb8e5..39f442212e 100644 --- a/packages/validation/tests/tsconfig.json +++ b/packages/validation/tests/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "../../", "noEmit": true }, - "include": ["../../testing/src/setupEnv.ts", "../../testing/src/awsSdkMatchers.ts", "../src/**/*", "./**/*"] + "include": ["../../testing/src/setupEnv.ts", "../src/**/*", "./**/*"] }