diff --git a/packages/aws-sdk-client-mock-jest/src/jestMatchers.ts b/packages/aws-sdk-client-mock-jest/src/jestMatchers.ts index 7493c04..0657053 100644 --- a/packages/aws-sdk-client-mock-jest/src/jestMatchers.ts +++ b/packages/aws-sdk-client-mock-jest/src/jestMatchers.ts @@ -57,16 +57,16 @@ interface AwsSdkJestMockBaseMatchers extends Record { /** * Asserts {@link AwsStub Aws Client Mock} received a {@link command} as defined specific {@link call} * number with matchin {@link input} - * + * * @param call call number to assert * @param command aws-sdk command constructor - * @param input + * @param input */ toHaveReceivedNthSpecificCommandWith( call: number, command: new (input: TCmdInput) => AwsCommand, - input: Partial - ): R; + input: Partial, + ): R; } interface AwsSdkJestMockAliasMatchers { @@ -126,16 +126,16 @@ interface AwsSdkJestMockAliasMatchers { /** * Asserts {@link AwsStub Aws Client Mock} received a {@link command} as defined specific {@link call} * number with matchin {@link input} - * + * * @param call call number to assert * @param command aws-sdk command constructor - * @param input + * @param input */ toReceiveNthSpecificCommandWith( call: number, command: new (input: TCmdInput) => AwsCommand, - input: Partial - ): R; + input: Partial, + ): R; } /** @@ -172,7 +172,7 @@ declare global { } } -type ClientMock = AwsStub; +type ClientMock = AwsStub; type AnyCommand = AwsCommand; type AnySpyCall = SinonSpyCall<[AnyCommand]>; type MessageFunctionParams = { diff --git a/packages/aws-sdk-client-mock/src/awsClientStub.ts b/packages/aws-sdk-client-mock/src/awsClientStub.ts index 0c9ad67..3449da8 100644 --- a/packages/aws-sdk-client-mock/src/awsClientStub.ts +++ b/packages/aws-sdk-client-mock/src/awsClientStub.ts @@ -2,10 +2,10 @@ import {Client, Command, MetadataBearer} from '@aws-sdk/types'; import {match, SinonSpyCall, SinonStub} from 'sinon'; import {mockClient} from './mockClient'; -export type AwsClientBehavior> = - TClient extends Client ? Behavior : never; +export type AwsClientBehavior = + TClient extends Client ? Behavior : never; -export interface Behavior { +export interface Behavior { /** * Allows specifying the behavior for any Command with given input (parameters). @@ -29,7 +29,7 @@ export interface Behavior(input?: Partial, strict?: boolean): Behavior; + onAnyCommand(input?: Partial, strict?: boolean): Behavior; /** * Allows specifying the behavior for a given Command type and its input (parameters). @@ -42,14 +42,14 @@ export interface Behavior( command: new (input: TCmdInput) => AwsCommand, input?: Partial, strict?: boolean, - ): Behavior; + ): Behavior; /** * Sets a successful response that will be returned from any `Client#send()` invocation. * * @param response Content to be returned */ - resolves(response: CommandResponse): AwsStub; + resolves(response: CommandResponse): AwsStub; /** * Sets a successful response that will be returned from one `Client#send()` invocation. @@ -67,7 +67,7 @@ export interface Behavior): Behavior; + resolvesOnce(response: CommandResponse): Behavior; /** * Sets a failure response that will be returned from any `Client#send()` invocation. @@ -75,7 +75,7 @@ export interface Behavior; + rejects(error?: string | Error | AwsError): AwsStub; /** * Sets a failure response that will be returned from one `Client#send()` invocation. @@ -94,14 +94,14 @@ export interface Behavior; + rejectsOnce(error?: string | Error | AwsError): Behavior; /** * Sets a function that will be called on any `Client#send()` invocation. * * @param fn Function taking Command input and returning result */ - callsFake(fn: (input: any) => any): AwsStub; // TODO Types + callsFake(fn: (input: any, getClient: () => Client) => any): AwsStub; // TODO Types /** * Sets a function that will be called on any `Client#send()` invocation. @@ -119,7 +119,7 @@ export interface Behavior any): Behavior; // TODO Types + callsFakeOnce(fn: (input: any, getClient: () => Client) => any): Behavior; // TODO Types } @@ -133,8 +133,8 @@ export interface Behavior> = - TClient extends Client ? AwsStub : never; +export type AwsClientStub = + TClient extends Client ? AwsStub : never; /** * Wrapper on the mocked `Client#send()` method, @@ -144,7 +144,7 @@ export type AwsClientStub> = * * To define resulting variable type easily, use {@link AwsClientStub}. */ -export class AwsStub implements Behavior { +export class AwsStub implements Behavior { /** * Underlying `Client#send()` method Sinon stub. @@ -154,7 +154,7 @@ export class AwsStub impl public send: SinonStub<[AwsCommand], Promise>; constructor( - private client: Client, + private client: Client, send: SinonStub<[AwsCommand], Promise>, ) { this.send = send; @@ -168,7 +168,7 @@ export class AwsStub impl /** * Resets stub. It will replace the stub with a new one, with clean history and behavior. */ - reset(): AwsStub { + reset(): AwsStub { /* sinon.stub.reset() does not remove the fakes which in some conditions can break subsequent stubs, * so instead of calling send.reset(), we recreate the stub. * See: https://github.com/sinonjs/sinon/issues/1572 @@ -180,7 +180,7 @@ export class AwsStub impl } /** Resets stub's calls history. */ - resetHistory(): AwsStub { + resetHistory(): AwsStub { this.send.resetHistory(); return this; } @@ -214,7 +214,7 @@ export class AwsStub impl commandCalls, TCmdInput extends TCmd extends AwsCommand ? TIn : never, TCmdOutput extends TCmd extends AwsCommand ? TOut : never, - >( + >( commandType: new (input: TCmdInput) => TCmd, input?: Partial, strict?: boolean, @@ -227,17 +227,17 @@ export class AwsStub impl }); } - onAnyCommand(input?: Partial, strict = false): CommandBehavior { + onAnyCommand(input?: Partial, strict = false): CommandBehavior { const cmdStub = this.send.withArgs(this.createInputMatcher(input, strict)); return new CommandBehavior(this, cmdStub); } on( command: new (input: TCmdInput) => AwsCommand, input?: Partial, strict = false, - ): CommandBehavior { + ): CommandBehavior { const matcher = match.instanceOf(command).and(this.createInputMatcher(input, strict)); const cmdStub = this.send.withArgs(matcher); - return new CommandBehavior(this, cmdStub); + return new CommandBehavior(this, cmdStub); } private createInputMatcher(input?: Partial, strict = false) { @@ -246,32 +246,32 @@ export class AwsStub impl : match.any; } - resolves(response: CommandResponse): AwsStub { + resolves(response: CommandResponse): AwsStub { return this.onAnyCommand().resolves(response); } - resolvesOnce(response: CommandResponse): CommandBehavior { + resolvesOnce(response: CommandResponse): CommandBehavior { return this.onAnyCommand().resolvesOnce(response); } - rejects(error?: string | Error | AwsError): AwsStub { + rejects(error?: string | Error | AwsError): AwsStub { return this.onAnyCommand().rejects(error); } - rejectsOnce(error?: string | Error | AwsError): CommandBehavior { + rejectsOnce(error?: string | Error | AwsError): CommandBehavior { return this.onAnyCommand().rejectsOnce(error); } - callsFake(fn: (input: any) => any): AwsStub { + callsFake(fn: (input: any, getClient: () => Client) => any): AwsStub { return this.onAnyCommand().callsFake(fn); } - callsFakeOnce(fn: (input: any) => any): CommandBehavior { + callsFakeOnce(fn: (input: any, getClient: () => Client) => any): CommandBehavior { return this.onAnyCommand().callsFakeOnce(fn); } } -export class CommandBehavior implements Behavior { +export class CommandBehavior implements Behavior { /** * Counter to simulate chainable `resolvesOnce()` and similar `*Once()` methods with Sinon `Stub#onCall()`. @@ -279,38 +279,45 @@ export class CommandBehavior this.send.thisValues[this.send.thisValues.length - 1] as Client; + constructor( - private clientStub: AwsStub, + private clientStub: AwsStub, private send: SinonStub<[AwsCommand], unknown>, ) { } - onAnyCommand(input?: Partial, strict?: boolean): Behavior { + onAnyCommand(input?: Partial, strict?: boolean): Behavior { return this.clientStub.onAnyCommand(input, strict); } on( command: new (input: TCmdInput) => AwsCommand, input?: Partial, strict = false, - ): CommandBehavior { + ): CommandBehavior { return this.clientStub.on(command, input, strict); } - resolves(response: CommandResponse): AwsStub { + resolves(response: CommandResponse): AwsStub { this.send.resolves(response); return this.clientStub; } - resolvesOnce(response: CommandResponse): CommandBehavior { + resolvesOnce(response: CommandResponse): CommandBehavior { this.send = this.send.onCall(this.nextChainableCallNumber++).resolves(response); return this; } - rejects(error?: string | Error | AwsError): AwsStub { + rejects(error?: string | Error | AwsError): AwsStub { this.send.rejects(CommandBehavior.normalizeError(error)); return this.clientStub; } - rejectsOnce(error?: string | Error | AwsError): CommandBehavior { + rejectsOnce(error?: string | Error | AwsError): CommandBehavior { this.send.onCall(this.nextChainableCallNumber++).rejects(CommandBehavior.normalizeError(error)); return this; } @@ -327,13 +334,13 @@ export class CommandBehavior any): AwsStub { - this.send.callsFake(cmd => fn(cmd.input)); + callsFake(fn: (input: any, getClient: () => Client) => any): AwsStub { + this.send.callsFake(cmd => fn(cmd.input, this.getClient)); return this.clientStub; } - callsFakeOnce(fn: (input: any) => any): CommandBehavior { - this.send.onCall(this.nextChainableCallNumber++).callsFake(cmd => fn(cmd.input)); + callsFakeOnce(fn: (input: any, getClient: () => Client) => any): CommandBehavior { + this.send.onCall(this.nextChainableCallNumber++).callsFake(cmd => fn(cmd.input, this.getClient)); return this; } } diff --git a/packages/aws-sdk-client-mock/src/mockClient.ts b/packages/aws-sdk-client-mock/src/mockClient.ts index 128ce05..a72af09 100644 --- a/packages/aws-sdk-client-mock/src/mockClient.ts +++ b/packages/aws-sdk-client-mock/src/mockClient.ts @@ -9,9 +9,9 @@ import {AwsClientStub, AwsStub} from './awsClientStub'; * @param client `Client` type or instance to replace the method * @return Stub allowing to configure Client's behavior */ -export const mockClient = ( - client: InstanceOrClassType>, -): AwsClientStub> => { +export const mockClient = ( + client: InstanceOrClassType>, +): AwsClientStub> => { const instance = isClientInstance(client) ? client : client.prototype; const send = instance.send; @@ -22,7 +22,7 @@ export const mockClient = ], Promise>; // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return new AwsStub(instance, sendStub); + return new AwsStub(instance, sendStub); }; type ClassType = { diff --git a/packages/aws-sdk-client-mock/test/mockClient.test.ts b/packages/aws-sdk-client-mock/test/mockClient.test.ts index 4bd3872..01de73e 100644 --- a/packages/aws-sdk-client-mock/test/mockClient.test.ts +++ b/packages/aws-sdk-client-mock/test/mockClient.test.ts @@ -643,6 +643,131 @@ describe('chained behaviors', () => { }); }); +describe('Client configuration-dependent behavior', () => { + it('provides the Client used to send the Command', async () => { + const snsEU = new SNSClient({region: 'eu-west-1'}); + const snsUS = new SNSClient({region: 'us-east-1'}); + + snsMock.on(PublishCommand).callsFake(async (input, getClient) => { + const client = getClient(); + const region = await client.config.region(); + return {MessageId: region.substring(0, 2)}; + }); + + const outputs = await Promise.all([ + snsEU.send(publishCmd1), + snsEU.send(publishCmd1), + snsUS.send(publishCmd1), + snsEU.send(publishCmd1), + ]); + + expect(outputs).toStrictEqual([ + {MessageId: 'eu'}, + {MessageId: 'eu'}, + {MessageId: 'us'}, + {MessageId: 'eu'}, + ]); + }); + + it('provides the Client used to send the Command once', async () => { + const snsEU = new SNSClient({region: 'eu-west-1'}); + const snsUS = new SNSClient({region: 'us-east-1'}); + + snsMock.on(PublishCommand) + .callsFakeOnce(async (input, getClient) => { + const client = getClient(); + const region = await client.config.region(); + return {MessageId: region.substring(0, 2)}; + }) + .callsFakeOnce(async (input, getClient) => { + const client = getClient(); + const region = await client.config.region(); + return {MessageId: region.substring(0, 2).toUpperCase()}; + }); + + const outputs = await Promise.all([ + snsEU.send(publishCmd1), + snsUS.send(publishCmd1), + ]); + + expect(outputs).toStrictEqual([ + {MessageId: 'eu'}, + {MessageId: 'US'}, + ]); + }); + + it('provides the Client used to send any Command', async () => { + const snsEU = new SNSClient({region: 'eu-west-1'}); + const snsUS = new SNSClient({region: 'us-east-1'}); + + snsMock + .onAnyCommand() + .callsFake(async (input, getClient) => { + const client = getClient(); + const region = await client.config.region(); + return {MessageId: region.substring(0, 2)}; + }); + + const outputs = await Promise.all([ + snsEU.send(publishCmd1), + snsUS.send(publishCmd1), + ]); + + expect(outputs).toStrictEqual([ + {MessageId: 'eu'}, + {MessageId: 'us'}, + ]); + }); + + it('provides the Client to the default behavior', async () => { + const snsEU = new SNSClient({region: 'eu-west-1'}); + const snsUS = new SNSClient({region: 'us-east-1'}); + + snsMock + .callsFake(async (input, getClient) => { + const client = getClient(); + const region = await client.config.region(); + return {MessageId: region.substring(0, 2)}; + }); + + const outputs = await Promise.all([ + snsEU.send(publishCmd1), + snsUS.send(publishCmd1), + ]); + + expect(outputs).toStrictEqual([ + {MessageId: 'eu'}, + {MessageId: 'us'}, + ]); + }); + + it('provides correct Client among multiple calls', async () => { + const snsEU = new SNSClient({region: 'eu-west-1'}); + const snsUS = new SNSClient({region: 'us-east-1'}); + + snsMock + .on(PublishCommand, publishCmd1.input).resolves({MessageId: uuid1}) + .on(PublishCommand, publishCmd2.input) + .callsFake(async (input, getClient) => { + const client = getClient(); + const region = await client.config.region(); + return {MessageId: region.substring(0, 2)}; + }); + + const outputs = await Promise.all([ + snsEU.send(publishCmd1), + snsEU.send(publishCmd2), + snsUS.send(publishCmd2), + ]); + + expect(outputs).toStrictEqual([ + {MessageId: uuid1}, + {MessageId: 'eu'}, + {MessageId: 'us'}, + ]); + }); +}); + const resolveImmediately = (x: T): Promise => new Promise(resolve => { setTimeout(() => { resolve(x);