Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support creating regional mockClients #158

Closed
Zordrak opened this issue May 4, 2023 · 2 comments · Fixed by #164
Closed

Support creating regional mockClients #158

Zordrak opened this issue May 4, 2023 · 2 comments · Fixed by #164
Labels
enhancement New feature or request

Comments

@Zordrak
Copy link

Zordrak commented May 4, 2023

I am trying to create jest tests for a lambda function that iterates through a list of regions and performs actions in each region - but expecting that different regions may return different results.

I cannot find a way to get more than one mock configured that would each respond to clients constructed with different region parameters.

For example:

const mock = new AwsClientMock(SecurityHubClient);

mock.onAnyClient((client) => {
  if (client.region === 'us-east-1') {
    return Promise.reject(new Error('SecurityHubServiceException'));
  } else {
    return Promise.resolve({ standards: ['CIS AWS Foundations Benchmark'] });
  }
});

or

let mock: Record<string, AwsStub<any, any>>;
const regions = [ 'eu-west-1', 'us-east-1' ];

regions.forEach((region) => {
  mock[region] = mockClient(SecurityHubClient, {region});
}

mock['us-east-1'].on(GetEnabledStandardsCommand).rejects(new SecurityHubServiceException({
  message: 'The service is temporarily unavailable',
  name: 'ServiceUnavailableException',
  $fault: 'server',
  $metadata: {
    httpStatusCode: 503,
    requestId: 'abc123',
  },
});

mock['eu-west-1'].on(GetEnabledStandardsCommand).resolves({
  $metadata: {
    httpStatusCode: 200,
    requestId: 'def456',
  },
  StandardsSubscriptions: [
    {
      StandardsSubscriptionArn:
        'arn:aws:securityhub:eu-west-1::standards/aws-foundational-security-best-practices/v/1.0.0',
      StandardsArn: 'arn:aws:securityhub:eu-west-1::standards/aws-foundational-security-best-practices',
      StandardsInput: {},
      StandardsStatus: 'READY',
    },
  ],
});

or

const mock = mockClient(SecurityHubClient);

mock.withClientParameters({region: 'us-east-1'}).on(GetEnabledStandardsCommand).rejects(new SecurityHubServiceException({
  message: 'The service is temporarily unavailable',
  name: 'ServiceUnavailableException',
  $fault: 'server',
  $metadata: {
    httpStatusCode: 503,
    requestId: 'abc123',
  },
});

mock.withClientParameters({region: 'eu-west-1'}).on(GetEnabledStandardsCommand).resolves({
  $metadata: {
    httpStatusCode: 200,
    requestId: 'def456',
  },
  StandardsSubscriptions: [
    {
      StandardsSubscriptionArn:
        'arn:aws:securityhub:eu-west-1::standards/aws-foundational-security-best-practices/v/1.0.0',
      StandardsArn: 'arn:aws:securityhub:eu-west-1::standards/aws-foundational-security-best-practices',
      StandardsInput: {},
      StandardsStatus: 'READY',
    },
  ],
});
@Zordrak Zordrak added the enhancement New feature or request label May 4, 2023
@m-radzikowski
Copy link
Owner

I've been thinking about it a little.

One way to achieve it right now is to mock client instances instead of classes (prototypes). So that works:

import {PublishCommand, SNSClient} from '@aws-sdk/client-sns';
import {mockClient} from 'aws-sdk-client-mock';

it('works', async () => {
    const snsEU = new SNSClient({region: 'eu-west-1'});
    const snsUS = new SNSClient({region: 'us-east-1'});

    const snsEUMock = mockClient(snsEU);
    const snsUSMock = mockClient(snsUS);

    snsEUMock.on(PublishCommand).resolves({MessageId: 'EU'});
    snsUSMock.on(PublishCommand).resolves({MessageId: 'US'});

    const outputEU = await snsEU.send(new PublishCommand({TopicArn: '', Message: ''}));
    const outputUS = await snsUS.send(new PublishCommand({TopicArn: '', Message: ''}));

    expect(outputEU.MessageId).toBe('EU');
    expect(outputUS.MessageId).toBe('US');
});

The drawback is that you need to access client instances in the tests, while they are often created inline in code that you are testing. The solution is to move Clients creation to a separate module (file) that you can mock, providing there your own instances of the client to the tested code during tests.


That being said... I think I found a better solution that I can integrate into the library, but I need to test it more before publishing it.

Because of how mocks work - we use Sinon to mock just the send() method on the Client object or Client prototype - it's not possible to make a mock for a single Client region. I would like the solution from your third example, but it's not possible without changing how the mock is attached. And needless to say, changing that would be risky / breaking change / laborious / all of the above.

However, I found a way to provide the Client instance to the callsFake() method. This is similar to your first example:

it('provides the Client used to send the Command', async () => {
    const snsMock = mockClient(SNSClient);

    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 outputEU1 = await snsEU.send(new PublishCommand({TopicArn: '', Message: 'EU 1'}));
    const outputEU2 = await snsEU.send(new PublishCommand({TopicArn: '', Message: 'EU 2'}));
    const outputUS1 = await snsUS.send(new PublishCommand({TopicArn: '', Message: 'US 1'}));
    const outputEU3 = await snsEU.send(new PublishCommand({TopicArn: '', Message: 'EU 3'}));

    expect(outputEU1.MessageId).toBe('eu');
    expect(outputEU2.MessageId).toBe('eu');
    expect(outputUS1.MessageId).toBe('us');
    expect(outputEU3.MessageId).toBe('eu');
});

The Client is obtainable with the getClient() function, which is provided as a new, second argument to the callsFake() callback. Then you can access config and region from the Client.

I will probably publish it as a beta version soon, but can't give an exact ETA yet.

@m-radzikowski
Copy link
Owner

I've just released v2.2.0-beta.0. You can access the client in callsFake() like in my example above:

    snsMock.on(PublishCommand).callsFake(async (input, getClient) => {
        const client = getClient();
        const region = await client.config.region();
        return {MessageId: region.substring(0, 2)};
    });

You can test it if you want.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants