nodejs - test failing but callback being called - node.js

I have a module which I export and which has a method editHeroImage which I am trying to test using mocha, chai and sinon. The modules has two objects that are passed as arguments, connection and queries. These are mySql objects, one containing the connection to the database and the other the query strings which are defined in their separate modules. The expObj which I am exporting and trying to test is a "helper" module.
I have successfully tested other methods of this module in the same way I am trying to test this method, but, however when I run into methods which use the async module for some reason, my tests no longer behave as expected. I wonder if I am missing something in this particular case, because I have tested other modules and methods which also use async and have not come across this behaviour.
When I run the tests, it logs "HELLO!" as expected but the assertion that the callbackSpy has been called, fails.
I am losing my mind here! Please help! What is going on? Could there be contamination between test suits?
Method under test:
expObj.editHeroImage = function(connection, queries, postId, postData, callback) {
async.waterfall([
function(next) {
var qString = queries.getSinglePostById();
connection.query(qString, [postId], function(err, results) {
if (err) {
return next(err);
}
if (!results.length) {
console.log('NO POST FOUND WITH ID ' + postId);
return callback();
}
next(null, results[0].hero_image);
});
},
function(heroImageId, next) {
if (!heroImageId) {
console.log('HERO IMAGE IS NEW - NEXT TICK!');
return next();
}
// Delete resized images of hero image
var queryStr = queries.deleteResizedImages();
var resizedVals = [heroImageId];
connection.query(queryStr, resizedVals, function(err) {
if (err) {
return callback(err);
}
console.log('DELETED RESIZED IMAGES OF HERO IMAGE ' + heroImageId);
var qString = queries.updateHeroImagePath();
var values = [postData.hero_image, heroImageId];
return connection.query(qString, values, function(err, results) {
if (err) {
return next(err);
}
console.log('UPDATED HERO IMAGE ' + heroImageId + ' WITH PATH ' + postData.hero_image);
next('break');
});
});
},
function addHeroImage(next) {
var qString = queries.insertImage();
var values = [postData.hero_image, postId];
connection.query(qString, values, function(err, results) {
if (err) {
return next(err);
}
next(null, results.insertId);
});
},
function addHeroImagePathToPost(heroImageId, next) {
var qString = queries.saveHeroImageId();
var values = [heroImageId, postId];
connection.query(qString, values, function(err) {
if (err) {
return next(err);
}
next();
});
}
], function(err) {
if (err && err !== 'break') {
return callback(err);
}
console.log('HELLO!');
callback(null);
});
};
Test, with set-up:
'use strict';
var chai = require('chai');
var sinonChai = require("sinon-chai");
var proxyquire = require('proxyquire');
var sinon = require('sinon');
chai.use(sinonChai);
var expect = chai.expect;
describe('HELPERS', function() {
var testedModule,
callbackSpy,
fakeConnectionObj,
fakeQueriesObj,
fakePost,
fakeSnakeCaseObj,
queryStub,
connectionStub,
manageStub,
fakeCamelCaseObj;
beforeEach(function() {
fakePost = {};
fakeConnectionObj = {};
fakeQueriesObj = {
getPostIdFromImage: function() {},
insertResizedImages: function() {},
createPost: function() {},
getPostImages: function() {},
getPostsAlternativesImages: function() {},
getSinglePostById: function() {},
getAllImages: function() {},
insertImage: function() {},
deleteMainImage: function() {},
deleteResizedImages: function() {},
updateHeroImagePath: function() {},
saveHeroImageId: function() {}
};
afterEach(function() {
queryStub.resetBehavior();
});
fakeSnakeCaseObj = {
sub_title: '123',
hero_image: '456'
};
fakeCamelCaseObj = {
subTitle: '123',
heroImage: '456'
};
callbackSpy = sinon.spy();
queryStub = sinon.stub();
manageStub = sinon.stub();
connectionStub = {query: queryStub};
testedModule = proxyquire('./../../../../lib/modules/mySql/workers/helpers', {
'./../../../factories/notification-service': {
select: function() {
return {manageSns: manageStub};
}
}
});
});
it('edits hero image', function() {
var _post = {
id: '123',
title: 'vf',
sub_title: 'vf',
slug: 'vf',
reading_time: 4,
created_at: '123',
published_at: '123',
deleted_on: false,
hero_image: 'hero_image_path'
};
var _postId = '123';
queryStub.onCall(0).callsArgWith(2, null, [{hero_image: '55'}]);
queryStub.onCall(1).callsArgWith(2, null);
queryStub.onCall(2).callsArgWith(2, null);
testedModule.editHeroImage(connectionStub, fakeQueriesObj, _postId, _post, function() {
console.log(arguments); // --> {'0': null} as expected
callbackSpy.apply(null, arguments);
});
expect(callbackSpy).has.been.calledWith(null);
});
});

Your assertion is probably executing before your async function has returned.
There are a number of ways to ensure your async functions have finished executing. The cleanest is to format your mocha test differently.
describe('...', function () {
var callbackSpy;
before(function () {
var _post = {
id: '123',
title: 'vf',
sub_title: 'vf',
slug: 'vf',
reading_time: 4,
created_at: '123',
published_at: '123',
deleted_on: false,
hero_image: 'hero_image_path'
};
var _postId = '123';
queryStub.onCall(0).callsArgWith(2, null, [{
hero_image: '55'
}]);
queryStub.onCall(1).callsArgWith(2, null);
queryStub.onCall(2).callsArgWith(2, null);
return testedModule.editHeroImage(connectionStub, fakeQueriesObj, _postId, _post, function () {
console.log(arguments); // --> {'0': null} as expected
callbackSpy.apply(null, arguments);
});
});
it('edits hero image', function () {
expect(callbackSpy).has.been.calledWith(null);
});
});
Notice that I have wrapped your assertion in a describe block so we can use before. Your actual logic for setting up stubs and executing the class has been moved to the before block and a return added, this ensures the async function is complete before moving on to your assertions.
Your other tests may have passed, but they will also be susceptible to this and it is purely a timing issue.

Indeed #Varedis was right about it being a timing issue. However using your suggestion of wrapping the assertion in a describe bloack and using the before function to set-up the test resulted in my stubs no longer working correctly. However taking your suggestion about timing into account I managed to solve the issue by using the done callback within my test suit. By keeping the set-up I made a slight change and my tests suddenly passed:
it('edits hero image', function(done) {
var _post = {
id: '123',
title: 'vf',
sub_title: 'vf',
slug: 'vf',
reading_time: 4,
created_at: '123',
published_at: '123',
deleted_on: false,
hero_image: 'hero_image_path'
};
var _postId = '123';
queryStub.onCall(0).callsArgWith(2, null, [{hero_image: '55'}]);
queryStub.onCall(1).callsArgWith(2, null);
queryStub.onCall(2).callsArgWith(2, null);
testedModule.editHeroImage(connectionStub, fakeQueriesObj, _postId, _post, function() {
callbackSpy.apply(null, arguments);
expect(callbackSpy).has.been.calledWith(null);
expect(callbackSpy).has.not.been.calledWith('FDgdjghg');
done();
});
});

Related

Expressjs does not execute sequencially and function return does not work

I am new to node/express js, and trying to execute the following. The control executes the lines after function call "var nextVersion =getNextContractVersion(cid)", even before the function returns a response. As a result the value for newVersion is not updated to Contract object. Also, function getNextContractVersion(cid) returns undefined, unlike the updated nextVersion.
How do i fix this behavior, please suggest. Also, is the right way of invoking function?
// Package imports
const express = require('express');
var router = express.Router();
const mongoose = require('mongoose');
//Local imports
var { Customer } = require('../models/customer');
var { Contract } = require('../models/contract');
router.put('/:cid/contracts', (req, res) => {
var cid = req.params.cid;
var nextVersion =getNextContractVersion(cid);
var contract;
if (validateCustomerId(cid)) {
req.body.contract.forEach((item) => {
contract = new Contract({
customerID: cid,
startDate: item.startDate,
endDate: item.endDate,
conditions: item.conditions,
price: item.price,
author: item.author,
version: nextVersion
});
});
contract.save((err, docs) => {
if (!err) {
Customer.findOneAndUpdate({ customerID: cid }, { $push: { contract: contract } },
{ safe: true, upsert: true, new: true }).populate({ path: 'contract' }).exec((err1, docs1) => {
if (!err1) {
res.send(docs1).status(200);
} else {
console.log('Error is adding a new contract:' + JSON.stringify(err1, undefined, 2));
}
});
} else {
console.log('Error is updating a new customer:' + JSON.stringify(err, undefined, 2));
}
});
} else {
res.status(400).send('Bad Request - Invalid input!')
}
});
function getNextContractVersion(cid) {
var nextVersion=1;
Contract.findOne({ customerID: cid }).sort({version: 'descending'}).exec((err, doc) => {
if (!err && doc != null) {
var currentVersion = parseInt(doc.version);
nextVersion = currentVersion + 1;
}
});
return nextVersion;
}
You are mixing synchronous and asynchronous code.
Contract.findOne({ customerID: cid }).sort({version: 'descending'}).exec((err, doc) => {
if (!err && doc != null) {
var currentVersion = parseInt(doc.version);
nextVersion = currentVersion + 1;
}
});
The above code effectively says "Go to the database, find one of these objects and whenever in the future that is done, run this code that's in the exec block."
One of the ways to reason about asynchronous code from a synchronous mindset is that of promises.
Here's a semi pseudo implementation:
router.put('/:cid/contracts', (req, res) => {
var cid = req.params.cid;
return getTheMostRecentContract(cid)
.then(function(oldContract){
var nextVersion = oldContract.version +1;
if(!validateCustomerId(cid)){
return res.status(400).send('Bad Request - Invalid input!');
}
var contract;
var savePromises = [];
req.body.contract.forEach((item) => {
contract = new Contract({
customerID: cid,
startDate: item.startDate,
endDate: item.endDate,
conditions: item.conditions,
price: item.price,
author: item.author,
version: nextVersion
});
savePromises.push(contract.save());
});
return Promise.all(savePromises);
})
.then(function(resultOfAllSavePromises){
//rest of code here
}).catch(function(error){
console.log('Error is updating a new customer:' + JSON.stringify(err, undefined, 2));
return res.status(400);
})
});
function getTheMostRecentContract(cid) {
return Contract.findOne({ customerID: cid }).sort({version: 'descending'});
}
As a matter of practice though, have the database control your auto-increment values. This code won't work in a high traffic environment.

TypeError in Node.js application

I've copied this example program from Node.js textbook:
var MongoClient = require('mongodb').MongoClient;
var website = {
url: 'http://www.google.com',
visits: 0
};
var findKey = {
url: 'www.google.com'
}
MongoClient.connect('mongodb://127.0.0.1:27017/demo', { useNewUrlParser: true }, function(err, client) {
var db = client.db('demo');
if(err) throw err;
var collection = db.collection('websites');
collection.insert(website, function(err, docs) {
var done = 0;
function onDone(err) {
done++;
if(done < 4) return;
collection.find(findKey).toArray(function(err, results) {
console.log('Visits:', results[0].visits);
//cleanup
collection.drop(function() {
client.close();
});
});
}
var incrementVisits = {
'$inc': {
'visits': 1
}
};
collection.update(findKey, incrementVisits, onDone);
collection.update(findKey, incrementVisits, onDone);
collection.update(findKey, incrementVisits, onDone);
collection.update(findKey, incrementVisits, onDone);
});
});
It throws this error when I run it:
/Users/me/Documents/Beginning NodeJS/node_modules/mongodb/lib/utils.js:132
throw err;
^
TypeError: Cannot read property 'visits' of undefined
at /Users/me/Documents/Beginning NodeJS/update/2update.js:26:43
at result (/Users/me/Documents/Beginning NodeJS/node_modules/mongodb/lib/utils.js:414:17)
at executeCallback (/Users/me/Documents/Beginning NodeJS/node_modules/mongodb/lib/utils.js:406:9)
at handleCallback (/Users/me/Documents/Beginning NodeJS/node_modules/mongodb/lib/utils.js:128:55)
at self.close (/Users/me/Documents/Beginning NodeJS/node_modules/mongodb/lib/cursor.js:905:60)
at handleCallback (/Users/me/Documents/Beginning NodeJS/node_modules/mongodb/lib/utils.js:128:55)
at completeClose (/Users/me/Documents/Beginning NodeJS/node_modules/mongodb/lib/cursor.js:1044:14)
at Cursor.close (/Users/me/Documents/Beginning NodeJS/node_modules/mongodb/lib/cursor.js:1057:10)
at /Users/me/Documents/Beginning NodeJS/node_modules/mongodb/lib/cursor.js:905:21
at handleCallback (/Users/me/Documents/Beginning NodeJS/node_modules/mongodb-core/lib/cursor.js:199:5)
I can't see whats wrong here but the textbook is a few years old and I've already had issues where the code was out of date and wouldn't work so I want to check if that is the case here.
It's a pretty horrible example you are following, but basically there are errors there essentially compounded from http:///www.google.com which is created as the value in the document is different to www.google.com, therefore you don't get a result and it's undefined when trying to read a property from an empty array.
The basic corrections would be to fix that, and actually use findOneAndUpdate() in all cases, since that will atomically return a document.
var MongoClient = require('mongodb').MongoClient;
var website = {
url: 'http://www.google.com',
visits: 0
};
var findKey = {
url: 'http://www.google.com'
}
MongoClient.connect('mongodb://127.0.0.1:27017/demo', { useNewUrlParser: true }, function(err, client) {
var db = client.db('demo');
if(err) throw err;
var collection = db.collection('websites');
collection.findOneAndUpdate(
findKey, website, { upsert: true },function(err, doc) {
var done = 0;
function onDone(err,doc) {
done++;
console.log("Visits: %s", doc.value.visits);
if (done >= 4) {
collection.drop(function(err) {
client.close();
});
}
}
var incrementVisits = {
'$inc': {
'visits': 1
}
};
var options = { returnOriginal: false };
collection.findOneAndUpdate(findKey, incrementVisits, options, onDone);
collection.findOneAndUpdate(findKey, incrementVisits, options, onDone);
collection.findOneAndUpdate(findKey, incrementVisits, options, onDone);
collection.findOneAndUpdate(findKey, incrementVisits, options, onDone);
});
});
Note those "four" calls at the end do not resolve immediately. These simply queue up async functions and there is no guaranteed order to their execution.
But the script will return:
Visits: 1
Visits: 2
Visits: 3
Visits: 4
A much better and "modern" example would instead be:
const { MongoClient } = require("mongodb");
const uri = "mongodb://localhost:27017/";
const options = { useNewUrlParser: true };
const website = {
url: 'http://www.google.com',
visits: 0
};
const findKey = { url: 'http://www.google.com' };
(async function() {
try {
const client = await MongoClient.connect(uri,options);
const db = client.db('demo');
const collection = db.collection('websites');
await collection.insertOne(website);
var times = 4;
while (times--) {
let doc = await collection.findOneAndUpdate(
findKey,
{ $inc: { visits: 1 } },
{ returnOriginal: false },
);
console.log("Visits: %s", doc.value.visits);
}
await collection.drop();
client.close();
} catch(e) {
console.error(e);
} finally {
process.exit();
}
})()
Since we actually await each call executed in the while loop, we guarantee that these are actually executed sequentially. We also await everything, so the code is clean and ordered and we can just hang up the database connection when everything is done, without waiting on callbacks to resolve or other methods.
It seems you Mongo instance returns some kind of error, which makes the results parameter undefined. So, check for errors in the line before (which you should do anyway, but maybe with a more sophisticated error handling):
collection.find(findKey).toArray(function(err, results) {
// this is added
if( err ) {
console.log( err );
return;
}
console.log('Visits:', results[0].visits);
//cleanup
collection.drop(function() {
client.close();
});
});
Instead of
console.log('Visits:', results[0].visits);
Try printing out :
console.log('Visits:', results[0]);
so that from results[0] you can check if there exits a property 'visits'

Backbone and Express: concatinating (duplicating) routes on res.redirect

I have an action where I need to update MongoDB entry including _id field, which requires deleting old entry and making a new one, here is server side:
exports.update = function(req, res, next){
var outcome = [];
outcome.previousId = req.params.id;
outcome.newId = req.body.name;
var getPreviousRecord = function(callback) {
req.app.db.models.AccountGroup
.findOne({ _id: req.params.id })
.lean()
.exec(function(err, accountGroups) {
if (err) {
return callback(err, null);
}
outcome.accountGroups = accountGroups;
return callback(null, 'done');
});
};
var makeNewRecord = function(callback) {
var permissions = outcome.accountGroups.permissions;
var fieldsToSet = {
_id: outcome.newId.toLowerCase(),
name: outcome.newId,
permissions: permissions
};
req.app.db.models.AccountGroup
.create(fieldsToSet, function(err, record) {
if (err) {
return callback(err, null);
}
outcome.record = record;
return callback(null, 'done');
});
};
var deletePreviousRecord = function() {
req.app.db.models.AccountGroup
.findByIdAndRemove(outcome.previousId)
.exec(function(err) {
if (err) {
return next(err);
}
res.redirect('admin/account-groups/' + outcome.newId + '/');
});
};
var asyncFinally = function(err) {
if (err) {
return next(err);
}
};
require('async').series([getPreviousRecord, makeNewRecord, deletePreviousRecord], asyncFinally);
};
It works fine, but I can't make this work normally on the front-end, it returns me both old route and a new route, for example:
PUT /admin/account-groups/customers22/admin/account-groups/Customers2233/ 404 213.749 ms - 31
where customers22 is old _id and customers2233 is new _id. If I navigate from another page to new entry it gets route normally.
On client side:
(function() {
'use strict';
app = app || {};
app.Details = Backbone.Model.extend({
idAttribute: '_id',
defaults: {
success: false,
errors: [],
errfor: {},
name: ''
},
url: function() {
return '/admin/account-groups/'+ app.mainView.model.id +'/';
},
parse: function(response) {
if (response.accountGroup) {
app.mainView.model.set(response.accountGroup);
delete response.accountGroup;
}
return response;
}
});
app.DetailsView = Backbone.View.extend({
el: '#details',
events: {
'click .btn-update': 'update'
},
template: Handlebars.compile( $('#tmpl-details').html() ),
initialize: function() {
this.model = new app.Details();
this.syncUp();
this.listenTo(app.mainView.model, 'change', this.syncUp);
this.listenTo(this.model, 'sync', this.render);
this.render();
},
syncUp: function() {
this.model.set({
_id: app.mainView.model.id,
name: app.mainView.model.get('name')
});
},
render: function() {
this.$el.html(this.template( this.model.attributes ));
for (var key in this.model.attributes) {
if (this.model.attributes.hasOwnProperty(key)) {
this.$el.find('[name="'+ key +'"]').val(this.model.attributes[key]);
}
}
},
update: function() {
this.model.save({
name: this.$el.find('[name="name"]').val()
});
}
});
app.MainView = Backbone.View.extend({
el: '.page .container',
initialize: function() {
app.mainView = this;
this.model = new app.AccountGroup( JSON.parse( unescape($('#data-record').html()) ) );
// ...
app.detailsView = new app.DetailsView();
}
});
$(document).ready(function() {
app.mainView = new app.MainView();
});
}());
It probably requires to trigger both model.save and model.destroy or prevent URL being used. Any advice on how to do it is appreciated, thank you.
Edit
Just a typo mistake here that is not related to the question, recklessly checking routes, see as cancelled
I believe the problem is here:
res.redirect('admin/account-groups/' + outcome.newId + '/');
That's a relative path so it'll be appended onto the current URL. I suspect you want something like this:
res.redirect('/admin/account-groups/' + outcome.newId + '/');

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

Jade template doesn't get the data passed from express

I know this questions has almost the same title but the issue is different.
I'm using Jade template engine v.1.11.0 built into latest Keystone.js release. In a controller, I query the data with two view.on('init') callbacks. First callback only queries one record and always passes. The second sometimes don't.
category.js
var keystone = require('keystone');
var async = require('async');
exports = module.exports = function (req, res) {
var view = new keystone.View(req, res);
var locals = res.locals;
// Init locals
locals.section = 'category';
locals.filters = {
category: req.params.category
};
locals.data = {
sections: [],
category: {}
};
// Load current category
view.on('init', function (next) {
var q = keystone.list('Category').model.findOne({
key: locals.filters.category
});
q.exec(function (err, result) {
locals.data.category = result;
locals.section = locals.data.category.name.toLowerCase();
next(err);
});
});
// Load sections
view.on('init', function (next) {
var q = keystone.list('Section').model.find().where('category').in([locals.data.category]).sort('sortOrder').exec(function(err, results) {
if (err || !results.length) {
return next(err);
}
async.each(results, function(section, next) {
keystone.list('Article').model.find().where('section').in([section.id]).sort('sortOrder').exec(function(err, articles){
var s = section;
if (articles.length) {
s.articles = articles;
locals.data.sections.push(s);
} else {
locals.data.sections.push(s);
}
});
}, function(err) {
console.log(locals.data.sections);
});
next(err);
});
});
view.render('category');
};
In my view, I should always get this passed:
sections: { _id: 574b909b43ff68163ed86bf2, publicTitle: 'Title 1', key: 'name-1', sortOrder: 3, name: 'Name 1', __v: 0, category: 574b8960947f45f034ac89b4, text: '', image: {} }
category: { _id: 574b8960947f45f034ac89b4, key: 'blabla', sortOrder: 1, name: 'Blabla', __v: 0, image: {} }
But 60% of the time, I get this:
sections:
category: { _id: 574b8960947f45f034ac89b4, key: 'johndoe', sortOrder: 1, name: 'JohnDoe', __v: 0, image: {} }
Strange thing is, If I go to another category, which has more sections and like 30 articles, I get sections 90% of the time, but still missing them 10%. This persists in both development and production.
Is some "next()" firing too early? I can't see where I messed up.
Alright. After some reading on async I managed to get it to work properly using async.parallel.
var keystone = require('keystone');
var async = require('async');
exports = module.exports = function (req, res) {
var view = new keystone.View(req, res);
var locals = res.locals;
// Init locals
locals.section = 'category';
locals.filters = {
category: req.params.category
};
locals.data = {
sections: [],
category: {}
};
// Load current category
view.on('init', function (next) {
var q = keystone.list('Category').model.findOne({
key: locals.filters.category
});
q.exec(function (err, result) {
locals.data.category = result;
locals.section = locals.data.category.name.toLowerCase();
getChildrenRecords(locals.data.category, next);
});
});
function getChildrenRecords(category, next){
var q = keystone.list('Section').model.find().where('category').in([category]).sort('sortOrder').exec(function(err, results) {
if (err || !results.length) {
return next(err);
}
callItems = [];
for(var i = 0; i < results.length; i++) {
var data = results[i];
callItems.push(makeCallbackFunction(data));
}
function makeCallbackFunction(section) {
return function (callback) {
keystone.list('Article').model.find().where('section').in([section.id]).sort('sortOrder').exec(function(err, articles){
if (err) return callback(err);
if (articles.length) { section.articles = articles; }
locals.data.sections.push(section);
callback();
});
};
}
async.parallel(callItems, function(err, results){
next(err);
});
});
}
// Render the view
view.render('category');
};

Resources