Search code examples
node.jschaisinonaws-sdk-jsaws-secrets-manager

Sinon mock is calling real AWS Secrets Manager


I am trying to mock the getSecretValue method of AWS Secrets Manager.

It is not working - instead of calling the sinon mock, it calls the real AWS function.

Example test:

'use strict';

const sinon = require( 'sinon' );
const AWS = require( 'aws-sdk' );
const secretsManager = new AWS.SecretsManager();
const unitUnderTest = require( '../modules/myUnitUnderTest' );
const { expect } = require( 'chai' );

describe( 'Failing example for Stack Overflow', async () => {

    afterEach( () => {
        sinon.restore();
    } );

    it( 'mocks the call to AWS Secret Manager successfully', async () => {

        sinon.stub( secretsManager, 'getSecretValue' ).returns( Promise.resolve( {SecretString: { publicKey: 'secretUsername', privateKey: 'secretPassword' }} ) );
        const key = 'key';
        const username = null;
        const password = null;
        await unitUnderTest( key, username, password );
        expect( username ).to.equal( 'secretUsername' );
        expect( password ).to.equal( 'secretPassword' );
    } );
} );

Example function:

const AWS = require( 'aws-sdk' );
const secretsManager = new AWS.SecretsManager();

module.exports = async ( keyId, username, password ) => {
        await getSecret( keyId )
            .then( secret => {
                username = secret.publicKey;
                password = secret.privateKey;
            } )
            .catch(
                err => {
                    logger.error( err );
                }
            );
};

const getSecret = (keyId ) => {
    return new Promise( ( resolve, reject ) => {
        secretsManager.getSecretValue( {
            SecretId: keyId
        }, ( err, data ) => {
            if ( err ) {
                reject( err );
            } else {
                resolve( JSON.parse( data.SecretString ) );
            }
        } );
    } );
};

Expected behaviour:

Sinon mocks AWS Secrets Manager

Actual behaviour:

Sinon doesn't work, AWS is called for real and I get errors in my logs proving that the real AWS SDK has been invoked/has tried to read a secret out of an AWS Secrets Manager account for my default profile on my machine:

Failed to obtain the secret: ConfigError: Missing region in config

I am at my wits end with this. Why is sinon not working?


Solution

  • Since you are instantiating AWS.SecretsManager at module scope, you need to stub the AWS.SecretsManager class before requiring your module.

    E.g.

    main.js:

    const AWS = require('aws-sdk');
    const secretsManager = new AWS.SecretsManager();
    
    module.exports = async (keyId) => {
      return getSecret(keyId)
        .then((secret) => {
          const username = secret.publicKey;
          const password = secret.privateKey;
          return { username, password };
        })
        .catch((err) => {
          console.error(err);
        });
    };
    
    const getSecret = (keyId) => {
      return new Promise((resolve, reject) => {
        secretsManager.getSecretValue(
          {
            SecretId: keyId,
          },
          (err, data) => {
            if (err) {
              reject(err);
            } else {
              resolve(JSON.parse(data.SecretString));
            }
          },
        );
      });
    };
    

    main.test.js:

    const AWS = require('aws-sdk');
    const sinon = require('sinon');
    const { expect } = require('chai');
    
    describe('67322634', () => {
      afterEach(() => {
        sinon.restore();
      });
      it('should get secret value', async () => {
        const data = {
          SecretString: JSON.stringify({ publicKey: 'secretUsername', privateKey: 'secretPassword' }),
        };
        const secretsManagerStub = {
          getSecretValue: sinon.stub().callsFake((params, callback) => {
            callback(null, data);
          }),
        };
        const SecretsManagerStub = sinon.stub(AWS, 'SecretsManager').returns(secretsManagerStub);
        const main = require('./main');
        const { username, password } = await main('1');
        expect(username).to.equal('secretUsername');
        expect(password).to.equal('secretPassword');
        sinon.assert.calledOnce(SecretsManagerStub);
        sinon.assert.calledOnceWithExactly(
          secretsManagerStub.getSecretValue,
          {
            SecretId: '1',
          },
          sinon.match.func,
        );
      });
    });
    

    unit test result:

      67322634
        ✓ should get secret value (1472ms)
    
    
      1 passing (1s)
    
    ----------|---------|----------|---------|---------|-------------------
    File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    ----------|---------|----------|---------|---------|-------------------
    All files |   85.71 |       50 |   83.33 |   85.71 |                   
     main.js  |   85.71 |       50 |   83.33 |   85.71 | 12,24             
    ----------|---------|----------|---------|---------|-------------------