dotteddice
dotteddice

Reputation: 331

Mocking Secrets Manager module for JavaScript jest unit tests

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.

Upvotes: 11

Views: 17187

Answers (3)

Prem prakash
Prem prakash

Reputation: 81

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);
    });
});

Upvotes: 1

user3006381
user3006381

Reputation: 2885

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.

Upvotes: 3

dotteddice
dotteddice

Reputation: 331

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');
            }
        }
      }
    };
  }
}});

Upvotes: 17

Related Questions