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.
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');
}
}
}
};
}
}});
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.
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);
});
});
Related
**Edit: Re-written with a simple example that works first:
So I've got a test file and 2 modules.
moduleA has a dependency, moduleB
// moduleA.js
const ModuleB = require('./moduleB');
function functionA() {
return 20 + ModuleB.functionB();
};
module.exports = { functionA };
// moduleB.js
const functionB = () => {
return 10;
}
module.exports = { functionB }
My test file stubs out functionB (returned from moduleB) using proxyquire:
const sinon = require('sinon');
const proxyquire = require('proxyquire');
describe('Unit Tests', function() {
it('should work', () => {
const mockedFn = sinon.stub();
mockedFn.returns(30);
const copyModuleA = proxyquire('./moduleA', {
'./moduleB': {
functionB: mockedFn
}
});
console.log(copyModuleA.functionA());
})
});
So it outputs 50 (stubbed functionB 30 + functionA 20)
Now I'm trying to take this example into my code:
moduleA in this case is a file called validation.js. It is dependent on moduleB, in this case a sequelize model, Person, with the function I want to mock: findOne
validation.js exports module.exports = { validateLogin };, a function that calls validate, which returns a function that uses Person.findOne()
So in my mind, as with the simple example, I need to create a stub, point to the validation module in proxyquire, and reference the dependency and its findOne function. Like this:
const stubbedFindOne = sinon.stub();
stubbedFindOne.resolves();
validationModule = proxyquire('../../utils/validation', {
'../models/Person': {
findOne: stubbedFindOne
}
});
This should stub Person.findOne in validation.js. But it doesn't seem to. And I have no idea why.
let validationModule;
describe('Unit Tests', () => {
before(() => {
const stubbedFindOne = sinon.stub();
stubbedFindOne.resolves();
validationModule = proxyquire('../../utils/validation', {
'../models/Person': {
findOne: stubbedFindOne
}
});
})
it.only('should return 422 if custom email validation fails', async() => {
const wrongEmailReq = { body: {email: 'nik#hotmail.com'} };
const res = {
statusCode: 500,
status: (code) => {this.statusCode = code; return this},
};
const validationFn = validationModule.validateLogin();
const wrongEmail = await validationFn(wrongEmailReq, res, ()=>{});
expect(wrongEmail.errors[0].msg).to.be.equal('Custom Authorisation Error');
return;
})
And this is my validation.js file:
const Person = require('../models/Person');
// parallel processing
const validate = validations => {
return async (req, res, next) => {
await Promise.all(validations.map(validation => validation.run(req)));
const errors = validationResult(req);
if (errors.isEmpty()) {
return next();
}
const error = new Error();
error.message = process.env.NODE_ENV === 'development'? 'Validation Failed':'Error';
error.statusCode = !errors.isEmpty()? 422:500;
error.errors = errors.array({onlyFirstError: true});
next(error);
return error;
};
};
const validateLogin = () => {
const validations = [
body('email')
.isString()
// snip
.custom(async (value, {req}) => {
try{
const person = await Person.findOne({ where: { email: value } });
if(!person) return Promise.reject('Custom Authorisation Error');
} catch(err) {
throw err;
}
})
.trim(),
];
return validate(validations);
}
module.exports = {
validateLogin
};
So the code in both the small sample and my app is correct, apart from how I stub the function. It shouldn't resolve or reject anything (I tried both out of desperation). It should return null in order to satisfy the conditional rather than jump to the catch block:
try{
const person = await Person.findOne({ where: { email: value } });
if(!person) return Promise.reject('Custom Authorisation Error');
} catch(err) {
throw err;
}
Hope the simple example helps someone else with proxyquire though
I'm new to nextjs and I'm creating API on next.js to perform db update using the pg-promise. However, it always hit the WARNING: Creating a duplicate database object for the same connection on console when the app is calling the API.
I tried browsing the docs but couldn't find a solution. I also tried solution (update-2) mentioned on stackoverflow page below, but the warning still exists.
Where should I initialize pg-promise
I think the problem is on the method I used to set the columnset. However I can't find proper way to do it. How should I fix it with pg-promise ?
Db setting code:
import ConfigEnv from 'utils/configuration';
import * as pgLib from 'pg-promise';
const initOptions = {
capSQL: true,
};
const pgp = require('pg-promise')(initOptions);
interface IDatabaseScope {
db: pgLib.IDatabase<any>;
pgp: pgLib.IMain;
}
export function createSingleton<T>(name: string, create: () => T): T {
const s = Symbol.for(name);
let scope = (global as any)[s];
if (!scope) {
scope = {...create()};
(global as any)[s] = scope;
}
return scope;
}
export function getDB(): IDatabaseScope {
return createSingleton<IDatabaseScope>('my-app-db-space', () => {
return {
db: pgp(ConfigEnv.pgp),
pgp
};
});
}
API code:
import {getDB} from 'db/pgpdb';
const {db, pgp} = getDB();
const cs = new pgp.helpers.ColumnSet([
'?detail_id',
'age',
'name'
// 'last_modified_date',
], {
table: 'user_detail',
})
export default async (req, res) => {
try {
// generating the update query where it is needed:
const update = pgp.helpers.update(req.body.content, cs) + ` WHERE v.detail_id = t.detail_id`;
// executing the query
await db
.none(update)
.then(() => {
return res.status(200).end();
})
.catch((error) => {
console.log('error', error);
return res.status(500).send(error);
});
} catch (error) {
console.log(error);
}
};
i am initializing a node js app with crucial data for the app to work from a database in index.js.
index.ts
import {getInitialData} from 'initData.ts';
export let APP_DATA: AppData;
export const initializeAppData = async () => {
try {
APP_DATA = (await getInitialData()) as AppData;
if (process.env.NODE_ENV !== 'test') {
initializeMongoose();
startServer();
}
} catch (error) {
console.log(error);
}
};
initData.ts
let dbName: string = 'initialData';
if (process.env.NODE_ENV === 'test') {
dbName = 'testDb';
}
const uri = `${process.env.MONGODB_URI}/?maxPoolSize=20&w=majority`;
export async function getInitialData() {
const client = new MongoClient(uri);
try {
await client.connect();
const database = client.db(dbName);
const configCursor = database
.collection('config')
.find({}, { projection: { _id: 0 } });
const config = await configCursor.toArray();
const aaoCursor = database
.collection('aao')
.find({}, { projection: { _id: 0 } });
const aao = await aaoCursor.toArray();
return { config, aao };
} catch {
(err: Error) => console.log(err);
} finally {
await client.close();
}
}
I'm using this array in another file and import it there.
missionCreateHandler
import { APP_DATA } from '../index';
export const addMissionResources = (
alarmKeyword: AlarmKeyword,
newMission: MissionDocument
) => {
const alarmKeywordObject = APP_DATA?.aao.find(
(el) => Object.keys(el)[0] === alarmKeyword
);
const resourceCommand = Object.values(alarmKeywordObject!);
resourceCommand.forEach((el) => {
Object.entries(el).forEach(([key, value]) => {
for (let ii = 1; ii <= value; ii++) {
newMission.resources?.push({
initialType: key,
status: 'unarranged',
});
}
});
});
};
I'm setting up a mongodb-memory-server in globalSetup.ts for Jest and copy the relevant data to the database from json-files.
globalSetup.ts
export = async function globalSetup() {
const instance = await MongoMemoryServer.create({
instance: { dbName: 'testDb' },
});
const uri = instance.getUri();
(global as any).__MONGOINSTANCE = instance;
process.env.MONGODB_URI = uri.slice(0, uri.lastIndexOf('/'));
process.env.JWT_SECRET = 'testSECRET';
const client = new MongoClient(
`${process.env.MONGODB_URI}/?maxPoolSize=20&w=majority`
);
try {
await client.connect();
const database = client.db('testDb');
database.createCollection('aao');
//#ts-ignore
await database.collection('aao').insertMany(aao['default']);
} catch (error) {
console.log(error);
} finally {
await client.close();
}
};
missionCreateHandler.test.ts
test('it adds the correct mission resources to the array', async () => {
const newMission = await Mission.create({
address: {
street: 'test',
houseNr: 23,
},
alarmKeyword: 'R1',
});
const expected = {
initialType: 'rtw',
status: 'unarranged',
};
addMissionResources('R1', newMission);
expect(newMission.resources[0].initialType).toEqual(expected.initialType);
expect(newMission.resources[0].status).toEqual(expected.status);
});
When runing the test, i get an 'TypeError: Cannot convert undefined or null to object at Function.values ()'. So it seems that the APP_DATA object is not set. I checked that the mongodb-memory-server is set up correctly and feed with the needed data.
When i hardcode the content of APP_DATA in index.ts, the test runs without problems.
So my questions are: How is the best practice to set up initial data in a node js app and where to store it (global object, simple variable and import it in the files where needed)? How can the test successfully run, or is my code just untestable?
Thank you!
In my calendar.spec.js, I have:
const { google } = require('googleapis')
const googleCalendar = google.calendar('v3')
...
before(() => {
sinon.stub(googleCalendar.calendarList, 'list').resolves({ data: true })
})
after(() => {
googleCalendar.calendarList.list.restore()
})
In my calendar.js, I have:
const { google } = require('googleapis')
const googleCalendar = google.calendar('v3')
let { data } = await googleCalendar.calendarList.list({
auth: oauth2Client
})
But it doesn't appear to be stubbed. It goes ahead and tries to connect to Google Calendar. What am I doing wrong?
You can mock the entire googleapis module with mock-require.
const mock = require('mock-require');
mock('googleapis', {
google: {
calendar: () => ({
calendarList: {
list: () => {
return Promise.resolve({
data: {
foo: 'bar'
}
});
}
}
})
}
});
Once you mocked it, your module will consume the mocked module instead of the original so you can test it. So if you module is exposing a method that calls the API, something like that:
exports.init = async () => {
const { google } = require('googleapis');
const googleCalendar = google.calendar('v3');
let { data } = await googleCalendar.calendarList.list({
auth: 'auth'
});
return data;
}
The test will be
describe('test', () => {
it('should call the api and console the output', async () => {
const result = await init();
assert.isTrue(result.foo === 'bar');
});
});
Here is a small repo to play with it: https://github.com/moshfeu/mock-google-apis
Trying to write a unittest for the below module in /utility/sqsThing.js. However I'm having diffuculty mocking the sqs.sendMessage method. Anyone know how I should go about this. I'm using the sinon library, and mocha for running the tests.
The function that I'm trying to unittest utility/sqsThing.js:
const AWS = require('aws-sdk');
AWS.config.update({ region: 'us-east-1' });
const sqs = new AWS.SQS({ apiVersion: '2012-11-05' });
const outputQueURL = 'https:awsUrl';
const SQSOutputSender = (results) => {
const params = {
MessageBody: JSON.stringify(results),
QueueUrl: outputQueURL,
};
// Method that I want to mock
sqs.sendMessage(params, function (err, data) {
if (err) {
console.log('Error');
} else {
console.log('Success', data.MessageId);
}
});
};
My attempt at mocking the sqs.sendMessage method in a unittest sqsThingTest.js:
const sqsOutputResultSender = require('../utility/sqsThing');
const AWS = require('aws-sdk');
const sqs = new AWS.SQS({ apiVersion: '2012-11-05' });
const mochaccino = require('mochaccino');
const { expect } = mochaccino;
const sinon = require('sinon');
describe('SQS thing test', function() {
beforeEach(function () {
sinon.stub(sqs, 'sendMessage').callsFake( function() { return 'test' });
});
afterEach(function () {
sqs.sendMessage.restore();
});
it('sqsOutputResultSender.SQSOutputSender', function() {
// Where the mock substitution should occur
const a = sqsOutputResultSender.SQSOutputSender('a');
expect(a).toEqual('test');
})
});
Running this unittest with mocha tests/unit/sqsThingTest.js however I get:
AssertionError: expected undefined to deeply equal 'test'.
info: Error AccessDenied: Access to the resource https://sqs.us-east-1.amazonaws.com/ is denied..
It looks like the mock did not replace the aws api call. Anyone know how I can mock sqs.SendMessage in my test?
You could use rewire js it is a library that lets you inject mocked properties into your module you want to test.
Your require statement would look something like this:
var rewire = require("rewire");
var sqsOutputResultSender = rewire('../utility/sqsThing');
Rewire will allow you to mock everything in the top-level scope of you sqsThing.js file.
Also you need to return the value of sqs.sendMessage this will remove the issue expected undefined to deeply equal 'test'
Your original file would look the same just with a return statement.
//utility/sqsThing.js
const AWS = require('aws-sdk');
AWS.config.update({ region: 'us-east-1' });
const sqs = new AWS.SQS({ apiVersion: '2012-11-05' });
const outputQueURL = 'https:awsUrl';
const SQSOutputSender = (results) => {
const params = {
MessageBody: JSON.stringify(results),
QueueUrl: outputQueURL,
};
// Method that I want to mock
return sqs.sendMessage(params, function (err, data) {
if (err) {
console.log('Error');
} else {
console.log('Success', data.MessageId);
}
});
};
You would then write your unit test as follows:
//sqsThingTest.js
var rewire = require("rewire");
var sqsOutputResultSender = rewire('../utility/sqsThing');
const mochaccino = require('mochaccino');
const { expect } = mochaccino;
const sinon = require('sinon');
describe('SQS thing test', function() {
beforeEach(function () {
sqsOutputResultSender.__set__("sqs", {
sendMessage: function() { return 'test' }
});
});
it('sqsOutputResultSender.SQSOutputSender', function() {
// Where the mock substitution should occur
const a = sqsOutputResultSender.SQSOutputSender('a');
expect(a).toEqual('test');
})
});
This example returns an object with a property of sendMessage but this could be replaces with a spy.
Rewire Docs
Try moving the declaration of sqsOutputResultSender after you have stubbed the sendmessage function
var sqsOutputResultSender;
const AWS = require('aws-sdk');
const sqs = new AWS.SQS({ apiVersion: '2012-11-05' });
const mochaccino = require('mochaccino');
const { expect } = mochaccino;
const sinon = require('sinon');
describe('SQS thing test', function() {
beforeEach(function () {
sinon.stub(sqs, 'sendMessage').callsFake( function() { return 'test' });
sqsOutputResultSender = require('../utility/sqsThing');
});
afterEach(function () {
sqs.sendMessage.restore();
});
it('sqsOutputResultSender.SQSOutputSender', function() {
// Where the mock substitution should occur
const a = sqsOutputResultSender.SQSOutputSender('a');
expect(a).toEqual('test');
})
});