AWS Lambda unable to call external https endpoint - node.js
We are working on an Alexa skill and it will need to reach out to external REST API's to get data. I'm having a really hard time getting this to work in our lambda function for some reason. I'm also having a hard time determining if the problem is in my node.js code not using callback's correctly or if it's in the VPC settings for my function. Here is my code, I've stripped out the non-essential stuff.
/* eslint-disable func-names */
/* eslint quote-props: ["error", "consistent"]*/
/**
* This sample demonstrates a simple skill built with the Amazon Alexa Skills
* nodejs skill development kit.
* This sample supports multiple lauguages. (en-US, en-GB, de-DE).
* The Intent Schema, Custom Slots and Sample Utterances for this skill, as well
* as testing instructions are located at https://github.com/alexa/skill-sample-nodejs-fact
**/
'use strict';
const Alexa = require('alexa-sdk');
const APP_ID = undefined; // TODO replace with your app ID (OPTIONAL).
const https = require('https');
const handlers = {
'LaunchRequest': function () {
this.emit('GetFact');
},
'GetNewFactIntent': function () {
this.emit('GetFact');
},
'maintenanceIntent': function () {
console.log('inside maintenanceIntent');
var options = {
host: 'api.forismatic.com',
path: '/api/1.0/?method=getQuote&lang=en&format=text',
method: 'GET'
};
getQuote(options, function (quote){
if(quote === ''){
console.log("No quote");
//speechOutput = "Please try again later";
}
else{console.log(quote)}
//self.emit(':tellWithCard', speechOutput, SKILL_NAME, text);
});
// Create speech output
// Place holder
var randomFact = 'Test Fact';
const speechOutput = randomFact;
this.emit(':tellWithCard', speechOutput, 'test skill name', randomFact);
},
'AMAZON.HelpIntent': function () {
const speechOutput = this.t('HELP_MESSAGE');
const reprompt = this.t('HELP_MESSAGE');
this.emit(':ask', speechOutput, reprompt);
},
'AMAZON.CancelIntent': function () {
this.emit(':tell', this.t('STOP_MESSAGE'));
},
'AMAZON.StopIntent': function () {
this.emit(':tell', this.t('STOP_MESSAGE'));
},
};
exports.handler = function (event, context, callback) {
const alexa = Alexa.handler(event, context);
alexa.APP_ID = APP_ID;
// To enable string internationalization (i18n) features, set a resources object.
//alexa.resources = languageStrings;
alexa.registerHandlers(handlers);
alexa.execute();
};
function getQuote(options, callback){
var text = '';
console.log("in getquote");
https.get(options, function(res) {
console.error("Got response: " + res.statusCode);
res.on("data", function(chunk) {
console.error("BODY: " + chunk);
text = '' + chunk;
return callback(text);
});
}).on('error', function(e) {
text = 'error' + e.message;
console.error("Got error: " + e.message);
});
}
Now when I invoke the maintenanceIntent this is what I see in the logs.
{"timestamp":1508426249817,"message":"START RequestId: 9f66123e-b4e0-11e7-baac-1bfb01d2abc8 Version: $LATEST","logStream":"2017/10/19/[$LATEST]0e048ab2fc5441cda8007e4a1963bf02","logGroup":"/aws/lambda/factDemo","requestID":"9f66123e-b4e0-11e7-baac-1bfb01d2abc8"}
{"timestamp":1508426250256,"message":"Warning: Application ID is not set","logStream":"2017/10/19/[$LATEST]0e048ab2fc5441cda8007e4a1963bf02","logGroup":"/aws/lambda/factDemo","requestID":"9f66123e-b4e0-11e7-baac-1bfb01d2abc8"}
{"timestamp":1508426250256,"message":"inside maintenanceIntent","logStream":"2017/10/19/[$LATEST]0e048ab2fc5441cda8007e4a1963bf02","logGroup":"/aws/lambda/factDemo","requestID":"9f66123e-b4e0-11e7-baac-1bfb01d2abc8"}
{"timestamp":1508426250256,"message":"in getquote","logStream":"2017/10/19/[$LATEST]0e048ab2fc5441cda8007e4a1963bf02","logGroup":"/aws/lambda/factDemo","requestID":"9f66123e-b4e0-11e7-baac-1bfb01d2abc8"}
{"timestamp":1508426250256,"message":"END RequestId: 9f66123e-b4e0-11e7-baac-1bfb01d2abc8","logStream":"2017/10/19/[$LATEST]0e048ab2fc5441cda8007e4a1963bf02","logGroup":"/aws/lambda/factDemo","requestID":"9f66123e-b4e0-11e7-baac-1bfb01d2abc8"}
{"timestamp":1508426250256,"message":"REPORT RequestId: 9f66123e-b4e0-11e7-baac-1bfb01d2abc8\tDuration: 378.28 ms\tBilled Duration: 400 ms \tMemory Size: 128 MB\tMax Memory Used: 33 MB\t","logStream":"2017/10/19/[$LATEST]0e048ab2fc5441cda8007e4a1963bf02","logGroup":"/aws/lambda/factDemo","requestID":"9f66123e-b4e0-11e7-baac-1bfb01d2abc8"}
So I can see it's actually calling the getQuote function. I'm not seeing any errors or success messages at all. I thought maybe I wasn't using callbacks correctly (node isn't my normal development language) but I've actually pulled code straight from an Amazon example on GitHub and I couldn't even get that to work. (This code is very similar to it except it's a little shorter.)
If I strip this down and run it locally through the node, it works fine.
As far as the networking stuff, I followed this guide: https://gist.github.com/reggi/dc5f2620b7b4f515e68e46255ac042a7
. I've also tried Amazon guides but at this point, I'm not even sure how I would check internet connectivity or if this is even the problem.
Any help to get on the right track would be greatly appreciated!
--EDIT--
I've changed my code as so. This comes straight from the alexa-cookbook at https://raw.githubusercontent.com/alexa/alexa-cookbook/master/external-calls/httpsGet/src/index.js
/* eslint-disable func-names */
/* eslint quote-props: ["error", "consistent"]*/
/**
* This sample demonstrates a simple skill built with the Amazon Alexa Skills
* nodejs skill development kit.
* This sample supports multiple lauguages. (en-US, en-GB, de-DE).
* The Intent Schema, Custom Slots and Sample Utterances for this skill, as well
* as testing instructions are located at https://github.com/alexa/skill-sample-nodejs-fact
**/
'use strict';
const Alexa = require('alexa-sdk');
const APP_ID = undefined; // TODO replace with your app ID (OPTIONAL).
const https = require('https');
const handlers = {
'LaunchRequest': function () {
this.emit('GetFact');
},
'GetNewFactIntent': function () {
this.emit('GetFact');
},
'maintenanceIntent': function () {
console.log('inside maintenanceIntent');
var myRequest = 'Florida';
httpsGet(myRequest, (myResult) => {
console.log("sent : " + myRequest);
console.log("received : " + myResult);
this.response.speak('The population of ' + myRequest + ' is ' + myResult);
this.emit(':responseReady');
}
);
// Create speech output
// Place holder
var randomFact = 'Test Fact';
const speechOutput = randomFact;
this.emit(':tellWithCard', speechOutput, 'test skill name', randomFact);
},
'AMAZON.HelpIntent': function () {
const speechOutput = this.t('HELP_MESSAGE');
const reprompt = this.t('HELP_MESSAGE');
this.emit(':ask', speechOutput, reprompt);
},
'AMAZON.CancelIntent': function () {
this.emit(':tell', this.t('STOP_MESSAGE'));
},
'AMAZON.StopIntent': function () {
this.emit(':tell', this.t('STOP_MESSAGE'));
},
};
exports.handler = function (event, context, callback) {
console.log("exports handler");
const alexa = Alexa.handler(event, context);
alexa.APP_ID = APP_ID;
// To enable string internationalization (i18n) features, set a resources object.
//alexa.resources = languageStrings;
alexa.registerHandlers(handlers);
alexa.execute();
console.log("post execute");
};
function httpsGet(myData, callback) {
// GET is a web service request that is fully defined by a URL string
// Try GET in your browser:
// https://cp6gckjt97.execute-api.us-east-1.amazonaws.com/prod/stateresource?usstate=New%20Jersey
console.log("in");
console.log(myData);
// Update these options with the details of the web service you would like to call
var options = {
host: 'cp6gckjt97.execute-api.us-east-1.amazonaws.com',
port: 443,
path: '/prod/stateresource?usstate=' + encodeURIComponent(myData),
method: 'GET',
// if x509 certs are required:
// key: fs.readFileSync('certs/my-key.pem'),
// cert: fs.readFileSync('certs/my-cert.pem')
};
var req = https.request(options, res => {
res.setEncoding('utf8');
var returnData = "";
console.log("request");
res.on('data', chunk => {
console.log("data");
returnData = returnData + chunk;
});
res.on('end', () => {
console.log("end");
// we have now received the raw return data in the returnData variable.
// We can see it in the log output via:
// console.log(JSON.stringify(returnData))
// we may need to parse through it to extract the needed data
var pop = JSON.parse(returnData).population;
callback(pop); // this will execute whatever function the caller defined, with one argument
});
});
console.log("req.end");
req.end();
}
Same idea but slightly different execution of getting the result from the endpoint. This is the log output.
{"timestamp":1508434982754,"message":"START RequestId: f4a39351-b4f4-11e7-a563-fbf7599fa72f Version: $LATEST","logStream":"2017/10/19/[$LATEST]3252e394be9b4a229c3a0d042deffbf8","logGroup":"/aws/lambda/factDemo","requestID":"f4a39351-b4f4-11e7-a563-fbf7599fa72f"}
{"timestamp":1508434982887,"message":"exports handler","logStream":"2017/10/19/[$LATEST]3252e394be9b4a229c3a0d042deffbf8","logGroup":"/aws/lambda/factDemo","requestID":"f4a39351-b4f4-11e7-a563-fbf7599fa72f"}
{"timestamp":1508434982887,"message":"Warning: Application ID is not set","logStream":"2017/10/19/[$LATEST]3252e394be9b4a229c3a0d042deffbf8","logGroup":"/aws/lambda/factDemo","requestID":"f4a39351-b4f4-11e7-a563-fbf7599fa72f"}
{"timestamp":1508434982887,"message":"inside maintenanceIntent","logStream":"2017/10/19/[$LATEST]3252e394be9b4a229c3a0d042deffbf8","logGroup":"/aws/lambda/factDemo","requestID":"f4a39351-b4f4-11e7-a563-fbf7599fa72f"}
{"timestamp":1508434982887,"message":"in","logStream":"2017/10/19/[$LATEST]3252e394be9b4a229c3a0d042deffbf8","logGroup":"/aws/lambda/factDemo","requestID":"f4a39351-b4f4-11e7-a563-fbf7599fa72f"}
{"timestamp":1508434982887,"message":"Florida","logStream":"2017/10/19/[$LATEST]3252e394be9b4a229c3a0d042deffbf8","logGroup":"/aws/lambda/factDemo","requestID":"f4a39351-b4f4-11e7-a563-fbf7599fa72f"}
{"timestamp":1508434983307,"message":"req.end","logStream":"2017/10/19/[$LATEST]3252e394be9b4a229c3a0d042deffbf8","logGroup":"/aws/lambda/factDemo","requestID":"f4a39351-b4f4-11e7-a563-fbf7599fa72f"}
{"timestamp":1508434983309,"message":"post execute","logStream":"2017/10/19/[$LATEST]3252e394be9b4a229c3a0d042deffbf8","logGroup":"/aws/lambda/factDemo","requestID":"f4a39351-b4f4-11e7-a563-fbf7599fa72f"}
{"timestamp":1508434983367,"message":"END RequestId: f4a39351-b4f4-11e7-a563-fbf7599fa72f","logStream":"2017/10/19/[$LATEST]3252e394be9b4a229c3a0d042deffbf8","logGroup":"/aws/lambda/factDemo","requestID":"f4a39351-b4f4-11e7-a563-fbf7599fa72f"}
{"timestamp":1508434983367,"message":"REPORT RequestId: f4a39351-b4f4-11e7-a563-fbf7599fa72f\tDuration: 608.20 ms\tBilled Duration: 700 ms \tMemory Size: 128 MB\tMax Memory Used: 35 MB\t","logStream":"2017/10/19/[$LATEST]3252e394be9b4a229c3a0d042deffbf8","logGroup":"/aws/lambda/factDemo","requestID":"f4a39351-b4f4-11e7-a563-fbf7599fa72f"}
I've tried this in the VPC and out of the VPC with the same result.
So this is solved thanks to this question: Node JS callbacks with Alexa skill
Particurlarly, this line
The :tell function will call the lambda callback and terminate the execution of the lambda function.
Moving the this.emit outside the httpsGet into it like so fixed the issue.
httpsGet(myRequest, (myResult) => {
console.log("sent : " + myRequest);
console.log("received : " + myResult);
this.response.speak('The population of ' + myRequest + ' is ' + myResult);
this.emit(':responseReady');
// Create speech output
// Place holder
var randomFact = 'Test Fact';
const speechOutput = randomFact;
this.emit(':tellWithCard', speechOutput, 'test skill name', randomFact);
}
);
Related
Using Express to get geoCoding from google API
I am having difficulty getting my LatLon look up to work - I have read Get Google Maps Geocoding JSON from Express - but that just says use HTTP...and I have read the docs on http/https - but I'm still getting an error. Here is my code - so calling myUrl/LatLon should give me the Google API response - or at least that is what I want... const https = require('https'); router.get( '/LatLon', ( res ) => {console.log('Here getting https'); const googleKey = '---'; const address = '1600 Amphitheatre Parkway, Mountain View, CA'; const options = new URL('https://maps.googleapis.com/maps/api/geocode/json?address=' + address + '&key=' + googleKey); const req = https.request(options, (res) => { res.on('data', (chunk) => { console.log(`BODY: ${chunk}`); }); res.on('end', () => { console.log('No more data in response.'); }); }); req.on('error', (e) => { console.error(`problem with request: ${e.message}`); }); req.write(); req.end(); }); I get this error - TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be one of type string or Buffer. Received type undefined at write_ (_http_outgoing.js:595:11) at ClientRequest.write (_http_outgoing.js:567:10) Any help would be greatly appreciated - I have tried about 4 variations on using "get" or "https"...
I found node-geocoder - and it worked great... Basically I did this, it is 'generalized code', non-functional; but you'll get the idea. A bunch of checks and compares went into it so I am not hitting API's when I do not need to. var NodeGeocoder = require('node-geocoder'); var options = { provider: process.env.GEOCODING_PROVIDER, httpAdapter: 'https', apiKey: process.env.GEOCODING_KEY, formatter: null }; var geocoder = NodeGeocoder(options); collection.getExistingId( req.params.id, ( err, record ) => { const existingAddress = addresstoString(record.address); const newAddress = addresstoString(newRecord.address); if ( !compareAddresses(existingAddress,newAddress) ) { geocoder.geocode(newAddress, function(err, geocode) { let coords = []; // mongoDB wants [Long,Lat] coords[0] = geocode[0].longitude; coords[1] = geocode[0].latitude; // set existingAddress.COORDINATES = coords // save the record }); } });
Lambda#Edge when triggered Dynamodb giving 503 Error
I am trying to invoke Lambda through cloudfront viewer request . Here is my Lambda code 'use strict'; const AWS = require("aws-sdk"); const docClient = new AWS.DynamoDB.DocumentClient(); exports.handler = (event, context, callback) => { /* Get request */ const request = event.Records[0].cf.request; const requestbody = Buffer.from(request.body.data, 'base64').toString(); const data = JSON.parse(requestbody); const Id = data.Name; console.log(Id); /* Generate body for response */ const body = '<html>\n' + '<head><title>Hello From Lambda#Edge</title></head>\n' + '<body>\n' + '<h1>You clicked more than 10 Times </h1>\n' + '</body>\n' + '</html>'; var params = { TableName: "Test", ProjectionExpression: "#V,#N", KeyConditionExpression: "#N = :v1", ExpressionAttributeNames: { "#N" : "Name", "#V" : "Value" }, ExpressionAttributeValues: { ":v1": Id } }; var querydb = docClient.query(params).promise(); querydb.then(function(data) { console.log(data.Items[0].Value); if(data.Items[0].Value >= 11){ const response = { status: '200', body: body, }; callback(null, response); }else { callback(null,request); } }).catch(function(err) { console.log(err); }); }; When i triggered the same lambda through console it is giving correct response. But when i deployed through Cloudfront it is giving 503 Error. But i had tried the same code withcode Dynamodb Client it worked perfectly fine. Here is the working one 'use strict'; const AWS = require("aws-sdk"); const docClient = new AWS.DynamoDB.DocumentClient(); exports.handler = (event, context, callback) => { /* Get request */ const request = event.Records[0].cf.request; const requestbody = Buffer.from(request.body.data, 'base64').toString(); const data = JSON.parse(requestbody); /* Generate body for response */ const body = '<html>\n' + '<head><title>Hello From Lambda#Edge</title></head>\n' + '<body>\n' + '<h1>You clicked more than 10 Times </h1>\n' + '</body>\n' + '</html>'; if(data.Value >= 10){ const response = { status: '200', body: body, }; callback(null, response); } else { callback(null, request); } }; I had given full dynamodb permissions to the lambda#edge. Any help is appreciated Thanks
Where have you specified region for DyanamoDB? It is possible that Lambda#Edge is executing in a region where your DDB table is missing. Have a look at AWS doc on region's order of precedence. You can also look at this L#E workshop code and documentation for more details on calling DDB. On a side note: A viewer facing Lambda function, making a call to a cross region dynamodb table will have negative effects on your latency. Not sure about your use case but see if it is possible to move this call to an origin facing event or make async call to ddb.
alexa implement CanFulfillIntentRequest in node.js
Alexa has released CanFulfillIntentRequest feature or Name-free Interaction for custom skills recently. I am trying to implement it in my existing skill which uses alexa-sdk. Please find my code below: 'use strict'; const Alexa = require('alexa-sdk'); var handlers = { 'LaunchRequest': function() { var speechOutput = "You can ask me to read out quotes from Steve Jobs"; var repromptText = "Sorry I didnt understand"; this.emit(':tell', speechOutput, repromptText); }, 'RandomQuote': function() { let data = getQuoteFunction(); const author = data[0]; const quote = data[1]; let cardTitle = "Quotation from author"; let cardContent = "Actual quote"; let speechOutput = "Actual quote"; // Speak out the output along with card information this.emit(':tellWithCard', speechOutput, cardTitle, cardContent); } } exports.handler = function (event, context, callback) { const alexa = Alexa.handler(event, context, callback); alexa.registerHandlers(handlers); alexa.execute(); }; Do we need to add handler for CanFulfillIntentRequest, the way I did for other handlers ? for example: var handlers = { 'LaunchRequest': function() { }, 'RandomQuote': function() { }, 'CanFulfillIntentRequest': function() { //code to handle } } Is this feature only available in ASK SDK v2 for Node.js ? Can we implement it in skill developed using alexa-sdk. Could anyone please let me know ? Thanks
Alexa skill which can read Facebook posts using AWS Lambda, Node.js and the Alexa Skills Kit
I am developing an Alexa skill using AWS Lambda, Node.js and the Alexa Skills Kit.I am using a forked from skill-sample-nodejs-fact project & successfully deployed & tested the sample fact project .Now I am trying to modify that code to read posts on some Facebook feeds.First I tried to develop some node application which can read posts & it was successful.Please find below code for your reference.I used fb module - https://www.npmjs.com/package/fb const FB = require('fb'); FB.setAccessToken('abc'); const query='cnninternational/posts'; FB.api(query, function (res) { if(!res || res.error) { console.log(!res ? 'error occurred' : res.error); return; } console.log(res); }); Next, I tried to integrate above code block into the lambda function.Unfortunately, I was unable, to read Facebook posts using these codes.Please find those code blocks in the below panel .Also, I checked cloudwatch logs as well.I can see the "GetNewsIntent", but I didn't see "fb-init" , "fb-error" or "fb-exit"entries in logs.Surprisingly, no error in logs as well.I would much appreciate it if someone can help to solve that issue. 'use strict'; const Alexa = require('alexa-sdk'); const FB = require('fb'); const APP_ID = 'abc'; const SKILL_NAME = 'test'; const GET_FACT_MESSAGE = "Here's your news: "; const STOP_MESSAGE = 'Goodbye!'; exports.handler = function(event, context, callback) { var alexa = Alexa.handler(event, context); alexa.appId = APP_ID; alexa.registerHandlers(handlers); alexa.execute(); }; const handlers = { 'LaunchRequest': function () { this.emit('GetNewsIntent'); }, 'GetNewsIntent': function () { console.log('GetNewsIntent'); const speechOutput = GET_FACT_MESSAGE; const query='cnninternational/posts'; FB.setAccessToken('abc'); FB.api(query, function (res) { console.log('fb-init'); if(!res || res.error) { console.log(!res ? 'error occurred' : res.error); console.log('fb-error'); return; } console.log(res); speechOutput = speechOutput + res; console.log('fb-exit'); }); this.response.cardRenderer(SKILL_NAME, speechOutput); this.response.speak(speechOutput); this.emit(':responseReady'); }, 'AMAZON.StopIntent': function () { this.response.speak(STOP_MESSAGE); this.emit(':responseReady'); }, };
Have you implemented account linking? You should be using event.session.user.accessToken for the parameter to setAccessToken().
I have removed this.response.cardRenderer , this.response.speak & changed the code bit.It's working fine now.Please find the below code snippet which can be used to read posts on the BBC Facebook page. var accessToken = ''; exports.handler = function(event, context, callback) { var alexa = Alexa.handler(event, context); alexa.appId = APP_ID; alexa.registerHandlers(handlers); alexa.execute(); }; const handlers = { 'NewSession': function() { var welcomeMessage = "Welcome to Athena"; welcomeMessage = welcomeMessage +"<break time=\"1s\"/>"+ "<audio src='https://s3.amazonaws.com/my-ssml-samples/Flourish.mp3' />"+"<break time=\"1s\"/>"; welcomeMessage += HELP_MESSAGE; accessToken = this.event.session.user.accessToken; if (accessToken) { FB.setAccessToken(accessToken); this.emit(':ask', welcomeMessage, HELP_REPROMPT); } else { // If we don't have an access token, we close down the skill. this.emit(':tellWithLinkAccountCard', "This skill requires you to link a Facebook account. Seems like you are not linked to a Facebook Account. Please link a valid Facebook account and try again."); } }, 'LaunchRequest': function () { this.emit('NewSession'); }, 'ReadBbcNewsFacebookPostsIntent': function () { var alexa = this; FB.api("bbcnews/posts", function (response) { if (response && !response.error) { if (response.data) { var output = "Here are recent posts" + "<break time=\"1s\"/>"; var max = 5; for (var i = 0; i < response.data.length; i++) { if (i < max) { output += "<break time=\"1s\"/>" + "Post " + (i + 1) + response.data[i].message.replace(/(?:https?|ftp):\/\/[\n\S]+/g, '') + ". "; } } alexa.emit(':ask', output+ ", What would you like to do next?",HELP_MESSAGE); } else { // REPORT PROBLEM WITH PARSING DATA } } else { // Handle errors here. console.log(response.error); this.emit(':tell', EMPTY_ACCESS_TOKEN_MESSAGE, TRY_AGAIN_MESSAGE); } }); } };
How do I expect a function to be called with specific args with sinon, mocha, and chai in nodejs?
I have been having a problem trying to make sure Q.ninvoke is called with the args I am passing in. I am new to testing with Sinon, Mocha and Chai. I have been trying everything I have found online for 2 days now and I still cant get my test pass. What am I doing wrong? This is my code under test. var cuid = require('cuid'); var fs = require('fs'); var Q = require('q'); var AWS = require('aws-sdk'); var S3 = new AWS.S3(); module.exports = { initialize: initialize }; function initialize(options) { return Q.nfcall(fs.readFile, options.path).then(function (file) { var fileParams = { Bucket: options.bucket, Key: options.name, Body: file, ContentType: options.contentType }; return Q.ninvoke(S3, 'upload', fileParams).then(function(data){ return data.Location; }); }); } Here is my test. describe.only('when a file is read successfully', function() { var spy; beforeEach(function() { spy = chai.spy.on(Q, 'ninvoke'); sinon.stub(Q, 'nfcall').withArgs(fs.readFile, fileParams.path).returns(Q.resolve(file)); }); it('Q.ninvoke should be called with args', function() { UploadCommand.initialize(fileParams) expect(spy).to.have.been.called.with(S3, 'upload', params); }); }); This is the error I am getting. 1) UploadCommand .initialize when a file is read successfully Q.ninvoke should be called with args: AssertionError: expected { Spy } to have been called with [ Array(3) ]
try this: var cuid = require('cuid'); var fs = require('fs'); var Q = require('q'); var AWS = require('aws-sdk'); var S3 = new AWS.S3(); module.exports = { initialize: initialize }; function initialize(options) { return Q.nfcall(fs.readFile, options.path).then(function (file) { var fileParams = { Bucket: options.bucket, Key: options.name, Body: file, ContentType: options.contentType }; return Q.ninvoke(S3, 'upload', fileParams); }); } note in particular that you should return a promise from your initialize function. then in the test: describe.only('when a file is read successfully', function() { var spy; beforeEach(function() { spy = chai.spy.on(Q, 'ninvoke'); sinon.stub(Q, 'nfcall').withArgs(fs.readFile,fileParams.path).returns(Q.resolve(file)); }); it('Q.ninvoke should be called with args', function(done) { UploadCommand.initialize(fileParams).then(function(data) { expect(spy).to.have.been.called.with(S3, 'upload', params); done(); }); }); }); a couple of other things to note, in your main application code, you will also want to chain your initialize function to a 'then' function, and in the body of that then function is where the rest of your application code should go. also, the 'done' callback is the way you tell mocha that it is an asynchronous test.
Mike I was able to get it working finally thanks to you. I really appreciate it! Here is the final test. describe.only('when a file is read successfully', function() { beforeEach(function() { sinon.stub(Q, 'nfcall').withArgs(fs.readFile, fileParams.path).returns(Q.resolve(file)); sinon.stub(Q, 'ninvoke').withArgs(S3, 'upload', params).returns(Q.resolve('url')); chai.spy.on(Q, 'ninvoke') }); it('Q.ninvoke should be called with args', function(done) { UploadCommand.initialize(fileParams).then(function(data) { expect(Q.ninvoke).to.have.been.called.with(S3, 'upload', params); done(); }); }); });
You can use sinon to stub as shown below let yourFunctionStub yourFunctionStub= sinon.stub(yourClass, "yourFunction"); yourFunctionStub.withArgs(arg1, arg2, arg3).returns(resultYouWant); if this a promise you can return ....returns(Promise.resolve(resultYouWant)); if for argument you are not clear you can you sinon.match.any Hope this helps you. :)