9

I'm having trouble getting the AWS Secrets Manager module mocked for the jest unit tests... The part it errors on is the .promise(). When I remove that, the code doesn't work for the real Secrets Manager so I think it needs to stay there. How do I mock the getSecretData function so that getSecretData.promise() will work for the mock?

Here is the SecretsManager.js code:

import AWS from 'aws-sdk';

export class SecretsManager {
  constructor() {
    AWS.config.update({
      region: 'us-east-1',
    });
    this.secretsManager = new AWS.SecretsManager();
  }

  async getSecretData(secretName) {
    try {
      const response = await this.secretsManager.getSecretValue({
        SecretId: secretName,
      }).promise();
      const secretString = response.SecretString;
      const parsedSecret = JSON.parse(secretString);
      return parsedSecret;
    } catch (e) {
      console.log('Failed to get data from AWS Secrets Manager.');
      console.log(e);
      throw new Error('Unable to retrieve data.');
    }
  }
}

Here is the SecretsManager.test.js code:

import { SecretsManager } from '../utils/SecretsManager';

jest.mock('aws-sdk', () => {
  return {
    config: {
      update(val) {

      },
    },
    SecretsManager: function () {
      return {
        async getSecretValue({
          SecretId: secretName
        }) {
          return {
            promise: function () {
              return {
                 UserName: 'test',
                 Password: 'password',
              };
            }
          };
        }
      };
    }
  }

});


describe('SecretsManager.js', () => {
  describe('Given I have a valid secret name', () => {
    describe('When I send a request for test_creds', () => {
      it('Then the correct data is returned.', async () => {
        const mockReturnValue = {
          UserName: 'test',
          Password: 'password',
        };
        const logger = getLogger();
        const secretManager = new SecretsManager();
        const result = await secretManager.getSecretData('test_creds');
        expect(result).toEqual(mockReturnValue)
      });
    });
    describe('When I send a request without data', () => {
      it('Then an error is thrown.', async () => {
      const secretManager = new SecretsManager();
      await expect(secretManager.getSecretData()).rejects.toThrow();
      });
    });
  });
});

This is the error I get when running the tests:

 this.secretsManager.getSecretValue(...).promise is not a function

Any suggestions or pointers are greatly appreciated!
Thank you for looking at my post.

dotteddice
  • 271
  • 1
  • 2
  • 9

3 Answers3

13

I finally got it to work... figures it'd happen shortly after posting the question, but instead of deleting the post I'll share how I changed the mock to make it work incase it helps anyone else.

Note: This is just the updated mock, the tests are the same as in the question above.

// I added this because it's closer to how AWS returns data for real.
const mockSecretData = {
  ARN: 'x',
  Name: 'test_creds',
  VersionId: 'x',
  SecretString: '{"UserName":"test","Password":"password"}',
  VersionStages: ['x'],
  CreatedDate: 'x'
}

jest.mock('aws-sdk', () => {
  return {
    config: {
      update(val) {
      },
    },
    SecretsManager: function () {
      return {
        getSecretValue: function ( { SecretId } ) {
          {
           // Adding function above to getSecretValue: is what made the original ".promise() is not a function" error go away.

            if (SecretId === 'test_creds') {
              return {
                promise: function () {
                  return mockSecretData;
                }
              };
            } else {
              throw new Error('mock error');
            }
        }
      }
    };
  }
}});
dotteddice
  • 271
  • 1
  • 2
  • 9
  • 2
    If you downvote, please leave a comment explaining why so I can understand what the problem is. – dotteddice Aug 20 '20 at 21:57
  • Could you please clarify on what makes the new solution to work. I can see that in both (old and new) the `getSecretValue()` function returns an object with `promise` field, so why does it fail in old version but passes in the new one? Btw, is it the first or the second test that breaks? – Simon Oct 09 '20 at 22:22
  • This was helpful. Any idea how we can mock PutSecretValue or CreateSecret ? – Varun Dec 16 '21 at 15:08
3

I ran into this issue as well. There may be a more elegant way to handle this that also allows for greater control and assertion, but I haven't found one. Note that the in-test option may work better with newer versions of Jest.

I personally solved this issue by making use of manual mocks and a custom mock file for aws-sdk. In your case, it would look something like the following:

# app_root/__tests__/__mocks__/aws-sdk.js

const exampleResponse = {
  ARN: 'x',
  Name: 'test_creds',
  VersionId: 'x',
  SecretString: '{"UserName":"test","Password":"password"}',
  VersionStages: ['x'],
  CreatedDate: 'x'
};
const mockPromise = jest.fn().mockResolvedValue(exampleResponse);
const getSecretValue = jest.fn().mockReturnValue({ promise: mockPromise });
function SecretsManager() { this.getSecretValue = getSecretValue };
const AWS = { SecretsManager };

module.exports = AWS;

Then in your test file:

// ... imports

jest.mock('aws-sdk');

// ... your tests

So, in a nutshell:

  • Instead of mocking directly in your test file, you're handing mocking control to a mock file, which Jest knows to look for in the __mocks__ directory.
  • You create a mock constructor for the SecretsManager in the mock file
  • SecretsManager returns an instance with the mock function getSecretValue
  • getSecretValue returns a mock promise
  • the mock promise returns the exampleResponse

Bada boom, bada bing. You can read more here.

user3006381
  • 2,735
  • 3
  • 23
  • 32
1

I ran into a same issue, I have tried to solve as below. It worked perfectly in my case.

Terminalsecret.ts

import AWS from 'aws-sdk';    
AWS.config.update({
  region: "us-east-1",
});    
const client = new AWS.SecretsManager();
export class Secret {
  constructor(){}
  async getSecret(secretName: string) {    
    let secret: any;
    const data = await client.getSecretValue({ SecretId: secretName).promise();
    if ('SecretString' in data) {
      secret = data.SecretString;
    } else {
      const buff = Buffer.alloc(data.SecretBinary as any, 'base64');
      secret = buff.toString('ascii');
    }
    const secretParse = JSON.parse(secret);
    return secretParse[secretName];
  }
}

Terminalsecret.test.ts

import { SecretsManager as fakeSecretsManager } from 'aws-sdk';
import { Secret } from './terminalSecret';

jest.mock('aws-sdk');
const setup = () => {
    const mockGetSecretValue = jest.fn();
    fakeSecretsManager.prototype.getSecretValue = mockGetSecretValue;
    return { mockGetSecretValue };
};

describe('success', () => {
    it('should call getSecretValue with the argument', async () => {
        const { mockGetSecretValue } = setup();
        mockGetSecretValue.mockReturnValueOnce({
            promise: async () => ({ SecretString: '{"userName": "go-me"}' })
        });
         const fakeName = 'userName';
         const terminalSecretMock: TerminalSecret = new TerminalSecret()
         terminalSecretMock.getTerminalSecret(fakeName);
         expect(mockGetSecretValue).toHaveBeenCalledTimes(1);
    });
});