Mocking serverless-mysql using sinon - node.js

I am trying to test an AWS lambda function (node.js) created using AWS SAM. My function uses the npm module serverless-mysql to connect to Aurora. The following are the relevant parts of my lambda function:
const connection = require('serverless-mysql')({
config: {
host : process.env.DB_HOST,
user : process.env.DB_USER,
password : process.env.DB_PASSWORD
}
});
exports.lambdaHandler = async (event, context) => {
try {
const name = event.pathParameters.name;
const rows = await connection.query('SELECT * FROM users WHERE name = ?', [name]);
await connection.end()
const user = rows[0];
return {
'statusCode': 200,
'body': JSON.stringify({
firstName: user.first_name,
lastName: user.last_name,
bk: user.bk,
team: user.current_team
})
}
} catch (err) {
console.log(err);
return err;
}
};
I am trying to tests this by mocking the serverless-mysql dependency, but I am currently unable to do so. My test looks like this:
const app = require('../../app.js');
const chai = require('chai');
const sinon = require('sinon');
const expect = chai.expect;
const event = {
pathParameters: {
name: 'johndoe'
}
}
var context;
var successConnectionObject = {
connect: function() {
return Promise.resolve();
},
query: function(sqlQuery, params) {
return Promise.resolve('');
},
end: function() {}
}
var mysql = require('serverless-mysql');
var stub = sinon.stub(mysql, 'connect').returns(successConnectionObject);
describe('Tests', function () {
it('verifies successful response', async () => {
const result = await app.lambdaHandler(event, context);
expect(result).to.be.an('object');
expect(result.statusCode).to.equal(200);
mock.verify();
mock.restore();
});
});
However, this returns the following error:
TypeError: Cannot stub non-existent own property connect
I believe this is because mysql is not instantiated. So I replaced the line var mysql = require('serverless-mysql'); with:
var mysql = require('serverless-mysql')();
Unfortunately, this results in the following error:
AssertionError: expected [Error: Error: self signed certificate in certificate chain] to be an object
So it seems the connect() method of the real serverless-mysql module is called.
How can I correctly mock serverless-mysql using sinon?

require('serverless-mysql') returns a function that returns a different value each time it is called, so mocking the properties on the result of one call won't affect the returned value of a different call.
That means you have to mock the function itself, which means mocking the entire module.
sinon doesn't provide a way to mock an entire module so you'll have to use something else for that part.
Here is a working test using proxyquire to mock the serverless-mysql module:
const chai = require('chai');
const sinon = require('sinon');
const expect = chai.expect;
const proxyquire = require('proxyquire');
const event = {
pathParameters: {
name: 'johndoe'
}
}
var context;
var successConnectionObject = {
connect: function () {
return Promise.resolve();
},
query: function (sqlQuery, params) {
return Promise.resolve([{
first_name: 'first',
last_name: 'last',
bk: 'bk',
current_team: 'team'
}]);
},
end: function () { }
}
const stub = sinon.stub().returns(successConnectionObject);
const app = proxyquire('../../app.js', { 'serverless-mysql': stub });
describe('Tests', function () {
it('verifies successful response', async () => {
const result = await app.lambdaHandler(event, context);
expect(result).to.be.an('object'); // Success!
expect(result.statusCode).to.equal(200); // Success!
sinon.assert.calledWithExactly(stub, {
config: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD
}
}); // Success!
});
});

Related

Mocking function to unit test Serverless Lambda

I am really struggling to understand unit testing within a Serverless Application. So I obviously have my handler, and I have a single Lambda function
const responses = require('../utils/jsonResponse');
const someConnector = require('../services/connectToService/connectToService');
module.exports = async (event) => {
const connectionParams = {
//some env variables
};
try {
const token = await someConnector.connectToService(connectionParams);
return responses.status(token, 200);
} catch (e) {
return responses.status(
`Issue connecting to service - ${e.message}`,
500,
);
}
};
So this Lambda function is pretty straight forward, gets some environment variables, and awaits a response from a service. It then returns the response.
So I have already done integration tests for this which is fine, but now I wanted to do a Unit test. I wanted to test this function in isolation, so essentially I want to mock connectToService to return my own responses.
So I came up with the following
require('dotenv').config();
const { expect } = require('chai');
const sinon = require('sinon');
let sandbox = require("sinon").createSandbox();
const LambdaTester = require('lambda-tester');
const handler = require('../../../handler');
const msConnector = require('../../../services/connectToService/connectToService');
describe('Testing handler', async (done) => {
describe('endpoint someEndpoint returns 200', () => {
it('Should resolve with 200', async () => {
before(() => {
sandbox = sinon.createSandbox();
sandbox.stub(msConnector, 'connectToService').resolves('some-token');
});
afterEach(() => {
sandbox.restore();
});
await LambdaTester(handler.someEndpoint)
.expectResult((result) => {
console.log(result);
expect(result.statusCode).to.equal(200);
});
});
});
done();
});
msConnector is the filename of the service, connectToService is the function name. What I want to do is not invoke this function, but return some-token when my Lambda calls it.
However, I have the console.log, and what I get from that is the real token, not some-token.
This tells me that the mocked function is really being called and executed and returning the real value.
So how can I mock this to make sure it returns some-token?
Thanks
Service function
const { DOMParser } = require('#xmldom/xmldom');
const axios = require('axios');
const { loginRequest } = require('./xml/login');
const connectToService = async (connectionParams) => {
//this injects config details into XML
const xmlRequest = loginRequest(
connectionParams.username,
connectionParams.password,
connectionParams.url,
);
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': xmlRequest.length,
},
};
const token = await axios
.post(connectionParams.msHost, xmlRequest, config)
.then((res) => {
const dom = new DOMParser().parseFromString(res.data, 'text/xml');
if (
dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0)
) {
return dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0).firstChild.nodeValue;
}
throw new Error('Invalid Username/Password');
})
.catch((err) => {
throw new Error(`Error making connection - ${err.message}`);
});
return token;
};
module.exports = {
connectToService,
};
The function connectToService may be not same copy between you mocked and called.
Because you overwrote a new object by module.exports = .... This causes you probably get different object for each require.
Try to do the below approach sharing the same object for all require.
const { DOMParser } = require('#xmldom/xmldom');
const axios = require('axios');
const { loginRequest } = require('./xml/login');
const connectToService = async (connectionParams) => {
//this injects config details into XML
const xmlRequest = loginRequest(
connectionParams.username,
connectionParams.password,
connectionParams.url,
);
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': xmlRequest.length,
},
};
const token = await axios
.post(connectionParams.msHost, xmlRequest, config)
.then((res) => {
const dom = new DOMParser().parseFromString(res.data, 'text/xml');
if (
dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0)
) {
return dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0).firstChild.nodeValue;
}
throw new Error('Invalid Username/Password');
})
.catch((err) => {
throw new Error(`Error making connection - ${err.message}`);
});
return token;
};
module.exports.connectToService = connectToService;

sinon stub for lambda function which is inside another lambda

Am writing unit test case for my code, as am calling another lambda function inside my lambda am not sure how to mock the inner lambda value, so because of this my test case is getting timed out. Attaching my code below
Test case file
"use strict";
const sinon = require("sinon");
const AWS = require("aws-sdk");
const expect = require("chai").expect;
const models = require("common-lib").models;
const { Organization } = models;
const DATA_CONSTANTS = require("./data/deleteOrganization");
const wrapper = require("../../admin/deleteOrganization");
const sandbox = sinon.createSandbox();
describe("Start Test updateOrganization", () => {
beforeEach(() => {
sandbox.stub(Organization, "update").resolves([1]);
});
afterEach(async () => {
sandbox.restore();
});
it("Test 03: Test to check success returned by handler", async () => {
const mLambda = {
invoke: sinon.stub().returnsThis(),
promise: sinon.stub(),
};
const response = await wrapper.handler(
DATA_CONSTANTS.API_REQUEST_OBJECT_FOR_200
);
console.log({ response });
expect(response.statusCode).to.be.equal(200);
const body = JSON.parse(response.body);
expect(body.message).to.be.equal("Updated successfully");
});
});
Code function
exports.handler = asyncHandler(async (event) => {
InitLambda("userService-deleteOrganization", event);
const { id } = event.pathParameters;
if (isEmpty(id)) {
return badRequest({
message: userMessages[1021],
});
}
try {
const orgrepo = getRepo(Organization);
const [rowsUpdated] = await orgrepo.update(
{ isDeleted: true },
{ org_id: id }
);
if (!rowsUpdated) {
return notFound({
message: userMessages[1022],
});
}
const lambda = new AWS.Lambda({
region: process.env.region,
});
await lambda
.invoke({
FunctionName:
"user-service-" + process.env.stage + "-deleteOrganizationDetail",
InvocationType: "Event",
Payload: JSON.stringify({
pathParameters: { id },
headers: event.headers,
}),
})
.promise();
return success({
message: userMessages[1023],
});
} catch (err) {
log.error(err);
return failure({
error: err,
message: err.message,
});
}
});
It seems that you are not properly stubbing the AWS.Lambda object.
try this,
const sinon = require("sinon");
const AWS = require("aws-sdk");
const expect = require("chai").expect;
const models = require("common-lib").models;
const { Organization } = models;
const DATA_CONSTANTS = require("./data/deleteOrganization");
const wrapper = require("../../admin/deleteOrganization");
const sandbox = sinon.createSandbox();
describe("Start Test updateOrganization", () => {
beforeEach(() => {
sandbox.stub(Organization, "update").resolves([1]);
});
afterEach(async () => {
sandbox.restore();
});
it("Test 03: Test to check success returned by handler", async () => {
const mLambda = { invoke: sinon.stub().returnsThis(), promise: sinon.stub() };
// you missed the below line
sinon.stub(AWS, 'Lambda').callsFake(() => mLambda);
const response = await wrapper.handler(
DATA_CONSTANTS.API_REQUEST_OBJECT_FOR_200
);
console.log({ response });
expect(response.statusCode).to.be.equal(200);
const body = JSON.parse(response.body);
expect(body.message).to.be.equal("Updated successfully");
sinon.assert.calledOnce(AWS.Lambda);
sinon.assert.calledWith(mLambda.invoke, {});
sinon.assert.calledOnce(mLambda.promise);
});
});
I can see that,
You are writing entire logic inside your handler function. This makes it less testable.
To overcome this you can write your code in such a way that is divided into small functions, which are easy to mock in test case files or testable independently. Handler function should only make call to those functions and return the result to the caller.
for Eg.
Lambda Handler:
exports.lambdaHandler = async (event) => {
// do some init work here
const lambdaInvokeResponse = await exports.invokeLambda(params);
}
exports.invokeLambda = async (params) {
const response = await lambda.invoke(params).promise();
return response;
}
test cases:
it('My Test Case - Success', async () => {
const result = await app.lambdaHandler(event);
const invikeLambdaResponse = {
// some dummy response
};
sinon.replace(app, 'invokeLambda', sinon.fake.returns(invikeLambdaResponse ));
});
This is now mocking the only lambda invoke part.
You can mock all the external calls like this (dynamodb, invoke, sns, etc.)
You can set spy and check if the called method is called as per desired arguments

How to Test Dynamo DB code Node Js ( Mocha - chai )

I'm trying to write a unit test using sinon promise support. I'm using DocumentClient.
My code looks like this:
const docClient = new AWS.DynamoDB.DocumentClient({ region: "us-east-2" });
const create = async (data, tableName, connectionData) => {
try {
const creditLine = {
...data,
id: uuidV4(),
};
const params = {
TableName: tableName,
Item: creditLine,
};
await docClient.put(params).promise();
return Responses._201(creditLine, connectionData);
} catch (e) {
return handleError(e, connectionData);
}
};
I cannot to prove the function create cause i can get into docCliente.put

Testing: Ensure a method is called in AWS NodeJS Lamda function

I have a NodeJS lambda function that I run on AWS. I want to write a simple test for the .handler function.
CODE
Here is the index.js code:
// importing dependencies
var mySQLWriter = require('./mySQLWriterService');
exports.handler = function(event, context, callback) {
console.log('Printing out JSON.stringify(event): ');
console.log(JSON.stringify(event));
event.Records.forEach((record) => {
if (record.eventName === 'INSERT') {
console.log('We have an INSERT happening.');
mySQLWriter(record, callback);
}
});
};
I want to write a simple test that would pass if mySQLWriter is called.
Using Mocha and Chai and with help from dashmud below, I have attempted to do this, but it isn't working, here is my indexTests.js code:
const chai = require('chai');
const expect = chai.expect;
const spies = require('chai-spies');
chai.use(spies);
const appStart = require('../index');
const mySQLWriter = require('../mySQLWriterService');
describe('lambda function', () => {
it('should call the mySQLWriter() function', () => {
const spy = chai.spy.on(mySQLWriter, 'mySQLWriter');
let event = {
Records: [
{
eventName: 'INSERT',
dynamodb: {
NewImage: {
DeviceId: { S: 'Device 000' },
TimeStamp: { S: '2018-03-20T11:15:31.668Z' },
Accuracy: { S: '5' },
Latitude: { S: '53.639645' },
Longitude: { S: '-1.782491' },
Speed: { S: '1' },
}
}
}
]
};
const context = {};
appStart.handler(event, context, () => {
expect(spy).to.have.been.called();
done();
})
});
});
When I run the test, I get:
Removed chai and used sinon instead
//const chai = require('chai');
//const expect = chai.expect;
const sinon = require('sinon');
//chai.use(sinon);
const SQLWriter = require('./mysqlwriterservice.js');
const appStart = require('./sinonsqlwriter');
describe('lambda function', () => {
it('should call the mySQLWriter() function', () => {
const spy = sinon.spy(SQLWriter, 'mySQLWriter');
let event = {
Records: [
{
eventName: 'INSERT',
dynamodb: {
NewImage: {
DeviceId: { S: 'Device 000' },
TimeStamp: { S: '2018-03-20T11:15:31.668Z' },
Accuracy: { S: '5' },
Latitude: { S: '53.639645' },
Longitude: { S: '-1.782491' },
Speed: { S: '1' },
}
}
}
]
};
const context = {};
appStart.handler(event, context, () => {
console.log("Call count"+spy.callCount)
//expect(spy).to.have.been.called();
})
});
});
// sinonsqlwriter.js
// importing dependencies
const SQLWriter = require('./mysqlwriterservice.js');
exports.handler = function(event, context, callback) {
console.log('Printing out JSON.stringify(event): ');
console.log(JSON.stringify(event));
event.Records.forEach((record) => {
if (record.eventName === 'INSERT') {
console.log('We have an INSERT happening.');
SQLWriter.mySQLWriter(record, callback);
SQLWriter.mySQLWriter(record, callback);
}
});
callback();
};
// mysqlwriterservice.js
I used the code from your shared link. Below is the updated answer:
Looks like you're not calling the handler function, so try changing
appStart()
to
appStart.handler(someEvent)
Your appStart is just a module. You need to call the handler inside your appStart (index.js) and do your assertions inside the callback.
const chai = require('chai');
const expect = chai.expect;
const spies = require('chai-spies');
chai.use(spies);
const appStart = require('../index');
describe('lambda function', () => {
it('should call the mySQLWriter() function', done => {
const spy = chai.spy.on(mySQLWriter, 'mySQLWriter');
const event = {};
const context = {};
appStart.handler(event, context, () => {
expect(spy).to.have.been.called();
done();
})
});
});
Update based on comment and updated question:
Based on your screenshot, it seems like mySQLWriterService.js exports an object with a mySQLWriter function.
This will not work.
var mySQLWriter = require('./mySQLWriterService');
I think is should be like this:
const mySQLWriter = require('./mySQLWriterService').mysqlWriter;
(I'm not 100% sure as you did not include the code inside your mySQLWriterService.js.)
P.S. Don't use var. Ever.

stub nodejs promise in chain to return error

I'm new to using promises in nodejs and also in testing them. I have managed to test the individual modules separately, but when it comes to testing the chain of promises, I am having some trouble. I tried following the examples found here and on the npm page for sinon-as-promised but don't seem to managed to control the flow and trigger the error in the first promise of the chain.
I am using mocha, chai and sinon for my tests with sinon-as-promised and chai-as-promised.
I am trying to test this module:
'use strict';
var mySQS = require('./modules/sqs/sqs-manager');
var sWebHook = require('./modules/webhooks/shopify/webhooks');
var main = {};
main.manageShopifyWebhook = function (params, callback) {
sWebHook.verify(params.srcHmac, params.rawBody, params.shopName.split('.myshopify.com')[0], params.productId)
.then(function(data) {
var body = {
"params": {
"productId": data.productId,
"shopName": data.shopName
},
"job": "call-update-item"
};
mySQS.create_Queue(body)
.then(mySQS.send_Message)
.then(function(result) {
callback(null, result);
})
.catch(function(error) {
callback(error, null);
});
});
};
module.exports = main;
This is the sWebHook module I want to trigger the reject callback in the main flow:
'use strict';
var crypto = require('crypto');
var nconf = require('../../../../config/nconfig');
var webHookManager = {};
webHookManager.verify = function (srcHmac, rawBody, shopName, productId) {
return new Promise(function (resolve, reject) {
rawBody = new Buffer(rawBody, 'base64');
var sharedSecret = nconf.get('SHOPIFY_CLIENT_SECRET');
var digest = crypto.createHmac('SHA256', sharedSecret).update(rawBody).digest('base64');
console.log('***** CALCULATED DIGEST *****');
console.log(digest);
console.log('***** HMAC FROM SHOPIFY *****');
console.log(srcHmac);
if (digest !== srcHmac) {
console.log('Hello');
var customError = new Error('Unauthorized: HMAC Not Verified');
reject(customError);
return false;
}
var newEvent = {
shopName: shopName,
productId: productId
};
console.log('!! WEBHOOK VERIFIED !!');
resolve(newEvent);
});
};
module.exports = webHookManager;
And these are my tests so far (which do not work):
'use strict';
var chai = require('chai');
var sinonChai = require("sinon-chai");
var expect = chai.expect;
var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
var sinon = require('sinon');
chai.use(sinonChai);
var proxyquire = require('proxyquire').noCallThru();
var AWS = require('mock-aws');
describe('MAIN', function() {
require('sinon-as-promised');
var testedModule,
sWebHookStub,
sqsQueueStub,
sqsSendMsgStub,
callbackSpy,
fakeDataObj;
before(function() {
sWebHookStub = sinon.stub();
sqsQueueStub = sinon.stub();
sqsSendMsgStub = sinon.stub();
callbackSpy = sinon.spy();
fakeDataObj = {
srcHmac: '12345',
rawBody: 'helloworld',
shopName: 'mario-test.myshopify.com',
productId: '6789'
};
testedModule = proxyquire('../lib/main', {
'./modules/webhooks/shopify/webhooks': {
'verify': sWebHookStub
},
'./modules/sqs/sqs-manager': {
'create_Queue': sqsQueueStub,
'send_Message': sqsSendMsgStub
}
});
});
it('calling shopifyVeriWebhook returns an error', function() {
var fakeError = new Error('Error verifying webhook');
sWebHookStub.rejects(fakeError);
testedModule.manageShopifyWebhook(fakeDataObj, function() {
callbackSpy.apply(null, arguments);
});
expect(callbackSpy).has.been.called.and.calledWith(fakeError, null);
});
});
So, I ended up figuring out how to test chains of promises using sinon. For the following main module (Note: the other modules all return promises):
'use strict';
var mySQS = require('./modules/sqs/sqs-manager');
var sWebHook = require('./modules/webhooks/shopify/webhooks');
var main = {};
//#params {object} params
//#params {string} params.srcHmac
//#params {string} params.rawBody
//#params {string} params.shopName - <shop-name.myshopify.com>
//#params {string} params.productId
main.manageShopifyWebhook = function (params) {
return new Promise(function(resolve, reject) {
sWebHook.verify(params.srcHmac, params.rawBody, params.shopName.split('.myshopify.com')[0], params.productId)
.then(function(data) {
var body = {
"params": {
"productId": data.productId,
"shopName": data.shopName
},
"job": "call-update-item"
};
return mySQS.create_Queue(body);
})
.then(mySQS.send_Message)
.then(resolve)
.catch(function(err) {
reject(err);
});
});
};
module.exports = main;
The secret is to manually resolve or reject the promises and write the expectation within the callback functions of the then or catch methods (just as we would do if we were writing tests for async code using done). And we then trigger the method we want to test, saving its value to a variable. Like so:
'use strict';
var chai = require('chai');
var sinonChai = require("sinon-chai");
var expect = chai.expect;
var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
require('sinon-as-promised');
var sinon = require('sinon');
chai.use(sinonChai);
var proxyquire = require('proxyquire').noCallThru();
describe('MAIN', function() {
require('sinon-as-promised');
var testedModule,
sWebHookStub,
sqsQueueStub,
sqsSendMsgStub,
callbackSpy,
fakeDataObj;
before(function() {
sWebHookStub = sinon.stub();
sqsQueueStub = sinon.stub();
sqsSendMsgStub = sinon.stub();
callbackSpy = sinon.spy();
fakeDataObj = {
srcHmac: '12345',
rawBody: 'helloworld',
shopName: 'mario-test.myshopify.com',
productId: '6789'
};
testedModule = proxyquire('../lib/main', {
'./modules/webhooks/shopify/webhooks': {
'verify': sWebHookStub
},
'./modules/sqs/sqs-manager': {
'create_Queue': sqsQueueStub,
'send_Message': sqsSendMsgStub
}
});
});
it('calling shopifyVeriWebhook returns an error when trying to VERIFY WEBHOOK', function() {
var fakeError = new Error('Error verifying webhook');
sWebHookStub.rejects(fakeError)().catch(function(error) {
expect(shopifyWebhook).to.eventually.equal(error);
});
var shopifyWebhook = testedModule.manageShopifyWebhook(fakeDataObj);
});
it('calling shopifyVeriWebhook returns an error when trying to CREATE SQS QUEUE', function() {
var fakeBody = {
"params": {
"productId": '1234',
"shopName": 'name'
},
"job": "call-update-item"
};
var fakeError = new Error('Error creating sqs queue');
sWebHookStub.resolves(fakeBody)().then(function(result) {
sqsQueueStub.rejects(fakeError)().catch(function(error) {
expect(shopifyWebhook).to.eventually.equal(error);
});
});
var shopifyWebhook = testedModule.manageShopifyWebhook(fakeDataObj);
});
it('calling shopifyVeriWebhook returns an error when trying to SEND SQS MESSAGE', function() {
var fakeData = {
queueUrl: '5678',
payLoad: '{"message": "Hello World"'
};
var fakeBody = {
"params": {
"productId": '1234',
"shopName": 'name'
},
"job": "call-update-item"
};
var fakeError = new Error('Error sending sqs message');
sWebHookStub.resolves(fakeBody)().then(function(result) {
sqsQueueStub.resolves(fakeData)().then(function(result) {
sqsSendMsgStub.rejects(fakeError)().catch(function(error) {
expect(shopifyWebhook).to.eventually.equal(error);
});
});
});
var shopifyWebhook = testedModule.manageShopifyWebhook(fakeDataObj);
});
it('calling shopifyVeriWebhook is SUCCESSFUL', function() {
var fakeData = {
queueUrl: '5678',
payLoad: '{"message": "Hello World"'
};
var fakeBody = {
"params": {
"productId": '1234',
"shopName": 'name'
},
"job": "call-update-item"
};
var fakeResponse = {
'message': 'success'
};
sWebHookStub.resolves(fakeBody)().then(function(result) {
sqsQueueStub.resolves(fakeData)().then(function(result) {
sqsSendMsgStub.resolves(fakeResponse)().then(function(result) {
expect(shopifyWebhook).to.eventually.equal(result);
});
});
});
var shopifyWebhook = testedModule.manageShopifyWebhook(fakeDataObj);
});
});
Bonus sample - I needed to run my code on aws lambda, and therefore needed to have a final callback. So I had the main entry point to my code in a file called lambda.js:
'use strict';
var main = require('./lib/main');
//Verifies shopify webhooks
//#params {object} event
//#params {string} event.srcHmac
//#params {string} event.rawBody
//#params {string} event.shopName - <shop-name.myshopify.com>
//#params {string} event.productId
exports.shopifyVerifyWebHook = function (event, context, callback) {
console.log('---- EVENT ----');
console.log(event);
main.manageShopifyWebhook(event)
.then(function(result) {
callback(null, result);
})
.catch(function(err) {
callback(err, null);
});
};
And for this I needed to control the result of the promises and make sure the callback was called with either an error or a success message.
The premiss is the same.
describe('LAMBDA', function() {
var testedModule,
mainShopStub,
callbackSpy,
mainModule,
fakeEvent;
before(function() {
callbackSpy = sinon.spy();
fakeEvent = {
srcHmac: '12345',
rawBody: 'helloworld',
shopName: 'mario-test.myshopify.com',
productId: '6789'
};
testedModule = require('../lambda');
mainModule = require('../lib/main');
mainShopStub = sinon.stub(mainModule, 'manageShopifyWebhook');
});
after(function() {
mainShopStub.restore();
});
it('calling shopifyVerifyWebHook returns an error', function() {
var fakeError = new Error('Error running lambda');
mainShopStub.rejects(fakeError);
mainShopStub().catch(function (error) {
expect(callbackSpy).has.been.called.and.calledWith(error, null);
});
testedModule.shopifyVerifyWebHook(fakeEvent, {}, function() {
callbackSpy.apply(null, arguments);
});
});
it('calling shopifyVerifyWebHook return a data object', function() {
var fakeObj = {message: 'success'};
mainShopStub.resolves(fakeObj);
mainShopStub().then(function (result) {
expect(callbackSpy).has.been.called.and.calledWith(null, result);
});
testedModule.shopifyVerifyWebHook(fakeEvent, {}, function() {
expected.resolves(fakeObj);
callbackSpy.apply(null, arguments);
});
});
});
Before getting into how to test multiple promises and validating errors, there is a much larger problem with your code.
manageShopifyWebhook() is constructed using an anti-pattern of promises which is, you're using a callback structure to return your promise value instead of returning your promise directly. If you do this, you're taking away a large benefit of promises, direct chain for error handling. Furthermore, you won't be able to use sinon-as-promised and chai-as-promised since they expect a Promise/thenable to be returned.
However, this is a rather quick fix in your code, by simply returning the promise created by sWebHook.verify():
main.manageShopifyWebhook = function (params) {
// Return the promise directly
// the final return will be returned to the original caller of manageShopifyWebhook
return sWebHook.verify(params.srcHmac, params.rawBody, params.shopName.split('.myshopify.com')[0], params.productId)
.then(function(data) {
var body = {
"params": {
"productId": data.productId,
"shopName": data.shopName
},
"job": "call-update-item"
};
return mySQS.create_Queue(body);
})
.then(mySQS.send_Message)
.then(function(result) {
return result;
})
.catch(function(err) {
// In reality you can let error propagate out here
// if you don't need to do anything special with it and let
// the promise just return the error directly
// I've only done this so we can return 'Error Verifying Webhook' as an error from the promise returned by manageShopifyWebhook()
return Promise.reject(new Error('Error verifying webook'));
});
});
};
Now that manageShopfiyWebhook() is returning a promise, you can use the two as-promised test libraries.
For chai-as-promised you need to convert your expect() to look for a promise using the chain eventually and then you can use rejectedWith() to validate the Error/Error Message.
To validate multiple promises tests you can use Promise.all() and pass in all your promise returning assertions and return the outcome of Promise.all() to your mocha it().
I don't use sinon but the above should've given you enough direction to figure out how to use this pattern with sinon-as-promised as well since it will work for any Promise returning testing library.
it('calling shopifyVeriWebhook returns an error', function() {
var fakeError = new Error('Error verifying webhook');
let shopifyWebhook = testedModule.manageShopifyWebhook(fakeDataObj);
return Promise.all([
expect(shopifyWebhook).to.eventually.be.rejectedWith(fakeError);
]);
});

Resources