I have a module under test which uses https to PUT data to a response URL. Before doing so, it makes calls to the AWS SDK. I do not want to stub the calls that AWS SDK makes using https, but I do want to stub the call to https.post that my module under test uses (it's an AWS Lambda unit test if that matters).
Consider the following test code
describe('app', function () {
beforeEach(function () {
this.handler = require('../app').handler;
this.request = sinon.stub(https, 'request');
});
afterEach(function () {
https.request.restore();
});
describe('#handler()', function () {
it('should do something', function (done) {
var request = new PassThrough();
var write = sinon.spy(request, 'write');
this.request.returns(request);
var event = {...};
var context = {
done: function () {
assert(write.withArgs({...}).calledOnce);
done();
}
}
this.handler(event, context);
});
});
});
And my module under test (app.js)
var aws = require("aws-sdk");
var promise = require("promise");
exports.handler = function (event, context) {
var iam = new aws.IAM();
promise.denodeify(iam.getUser.bind(iam))().then(function (result) {
....
sendResponse(...);
}, function (err) {
...
});
};
// I only want to stub the use of https in THIS function, not the use of https by the AWS SDK itself
function sendResponse(event, context, responseStatus, responseData) {
var https = require("https");
var url = require("url");
var parsedUrl = url.parse(event.ResponseURL);
var options = {
...
};
var request = https.request(options, function (response) {
...
context.done();
});
request.on("error", function (error) {
...
context.done();
});
// write data to request body
request.write(...);
request.end();
}
How can I accomplish this?
You could use nock to mock specific HTTP/S requests, rather than function calls.
With nock, you can setup URL and request matchers that will allow requests through that don't match what you've defined.
Eg:
nock('https://www.something.com')
.post('/the-post-path-to-mock')
.reply(200, 'Mocked response!');
This would only intercept POST calls to https://www.something.com/the-post-path-to-mock, responding with a 200, and ignore other requests.
Nock also provides many options for mocking responses or accessing the original request data.
Related
I'm trying to make my own google action and I want to call an external api to get responses.
Here is my code:
const { conversation } = require('#assistant/conversation');
const functions = require('firebase-functions');
const app = conversation({debug:true});
const https = require('https');
app.handle('Tester', conv => {
// Implement your code here
conv.add("ok it works");
});
app.handle('Tester2', conv => {
// Implement your code here
let url = 'https://jsonplaceholder.typicode.com/users?_limit=2';
//const url = "https://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22";
http_req(url).then((message)=> {
console.log(message[0].name);
conv.add(message[0].name);
//return Promise.resolve();
});
});
function http_req(url) {
return new Promise((resolve, reject) => {
https.get(url, function(resp) {
var json = "";
resp.on("data", function(chunk) {
//console.log("received JSON response: " + chunk);
json += chunk;
});
resp.on("end", function() {
let jsonData = JSON.parse(json);
console.log(jsonData[0].name);
resolve(jsonData);
});
}).on("error", (err) => {
reject("Error: " + err.message);
});
});
}
exports.ActionsOnGoogleFulfillment = functions.https.onRequest(app);
The logs:
Error text:
Error: Response has already been sent. Is this being used in an async call that was not returned as a promise to the intent handler?
The problem is that the assistant won't say the conv.add(message[0].name); (obviusly it has a value)
Thanks in advance!
Thanks to a reddit user
https://www.reddit.com/r/GoogleAssistantDev/comments/lia5r4/make_http_get_from_fulfillment_in_nodejs/gn23hi3?utm_source=share&utm_medium=web2x&context=3
This error messages tells you just about all you need to know! Your
call to con.add() is indeed being used in an asynchronous call (the
callback chained to the Promise you created from http_req), and you
are indeed not returning that Promise.
Here's what's happening:
Google calls your 'Tester2' handler
You start an asynchronous HTTP request via http_req, encapsulated in a
Promise
Your function completes before the HTTP request does
Google sees that you are not returning anything from your handler and
assumes that you're done, so it sends the Response
The HTTP request finishes and its Promise resolves, calling your code
attached by the then() function
The simple solution here is to return the Promise created by your
http_req(...).then(...) code, so Google will know that you're not just
quite done, and it should wait for that Promise to resolve before
sending the Response.
If you can use async/await it becomes a bit clearer:
app.handle('Tester2', async conv => {
// Implement your code here
let url = 'https://jsonplaceholder.typicode.com/users?_limit=2';
//const url = "https://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22";
const message = await http_req(url);
console.log(message[0].name);
conv.add(message[0].name);
});
I would like to capture an async call made by "request".
The call I am looking to intercept is "https://api.ap.org/v2/yada/yada" .
I want to intercept this third party call to api.ap.org and redirect it to another service, say 127.0.0.1:3001.
I would also like to add headers during this intercept process.
I know how to intercept all calls made by the express js route via http-proxy, but this does not intercept calls made within nodejs itself.
router.get('/', function(req, res, next) {
request("https://api.ap.org/v2/yada/yada", {}, (err, data) => {
console.log('---- call made')
console.log(data);
});
res.render('index', { title: 'Express' });
});
UPDATE - from Estus
function patchedRequest(url, options, ...args) {
let newUrl = 'https://www.google.com/' // replace url with another one;
console.log('------ args');
console.log(url);
console.log(options);
if(url.match(/api\.ap\.org/).length){
options = {};
newUrl = 'http://127.0.0.1:3000/api'
}
return originalRequest(newUrl, options, ...args);
}
This allows me to intercept the call to the third party API and send it the service of my choosing.
Thanks Estus!
This can be done by mocking original request module.
This is roughly how cache-mangling libraries like proxyquire work:
patch-request.js
const originalRequest = require('request');
function patchedRequest(url, ...args) {
const newUrl = 'https://www.google.com/' // replace url with another one;
return originalRequest(newUrl, ...args);
}
Object.assign(patchedRequest, originalRequest);
for (const verb of 'get,head,options,post,put,patch,del,delete'.split(',')) {
patchedRequest[verb] = function (url, ...args) {
const newUrl = 'https://www.google.com/' // replace url with another one;
return originalRequest[verb](newUrl, ...args);
};
}
module.exports = require.cache[require.resolve('request')].exports = patchedRequest;
index.js
// patch request before it's required anywhere else
require('./patch-request');
// load the app that uses request
I am trying to call a rest API from Firebase function which servers as a fulfillment for Actions on Google.
I tried the following approach:
const { dialogflow } = require('actions-on-google');
const functions = require('firebase-functions');
const http = require('https');
const host = 'wwws.example.com';
const app = dialogflow({debug: true});
app.intent('my_intent_1', (conv, {param1}) => {
// Call the rate API
callApi(param1).then((output) => {
console.log(output);
conv.close(`I found ${output.length} items!`);
}).catch(() => {
conv.close('Error occurred while trying to get vehicles. Please try again later.');
});
});
function callApi (param1) {
return new Promise((resolve, reject) => {
// Create the path for the HTTP request to get the vehicle
let path = '/api/' + encodeURIComponent(param1);
console.log('API Request: ' + host + path);
// Make the HTTP request to get the vehicle
http.get({host: host, path: path}, (res) => {
let body = ''; // var to store the response chunks
res.on('data', (d) => { body += d; }); // store each response chunk
res.on('end', () => {
// After all the data has been received parse the JSON for desired data
let response = JSON.parse(body);
let output = {};
//copy required response attributes to output here
console.log(response.length.toString());
resolve(output);
});
res.on('error', (error) => {
console.log(`Error calling the API: ${error}`)
reject();
});
}); //http.get
}); //promise
}
exports.myFunction = functions.https.onRequest(app);
This is almost working. API is called and I get the data back. The problem is that without async/await, the function does not wait for the "callApi" to complete, and I get an error from Actions on Google that there was no response. After the error, I can see the console.log outputs in the Firebase log, so everything is working, it is just out of sync.
I tried using async/await but got an error which I think is because Firebase uses old version of node.js which does not support async.
How can I get around this?
Your function callApi returns a promise, but you don't return a promise in your intent handler. You should make sure you add the return so that the handler knows to wait for the response.
app.intent('my_intent_1', (conv, {param1}) => {
// Call the rate API
return callApi(param1).then((output) => {
console.log(output);
conv.close(`I found ${output.length} items!`);
}).catch(() => {
conv.close('Error occurred while trying to get vehicles. Please try again later.');
});
});
I've got a MEAN app and I'm trying to get tests to work on the node side. Async events are wrapped in promises, which are consumed in the controller. I failed at testing the controller :(
The controller I'm trying to test:
ProjectController.prototype.getAll = function(req, res, next) {
req.dic.subjectRepository
.getById(req.params.subjectId)
.then(function(subject) {
res.json(subject.projects);
}, function(err) {
return res.status(404).send('Subject does not exist.' + err);
});
};
The subjectRepository is our data source, which returns a promise (mpromise because under the hood we're using mongoose, but it shouldn't matter):
So in our test we tried mocking the request (we're injecting our dependency injection container from a middleware into the req) and response (the test succeeds if response.json() has been called with the subjects we tried to fetch) and our subjectRepository. We used bluebird (although I tried others out of frustration) to create fake promises for our mocked subjectRepository:
describe('SubjectController', function() {
'use strict';
var Promise = require('bluebird');
it('gets all existing subjects', function() {
// -------------------------------------
// subjectRepository Mock
var subjectRepository = {
getAll: function() {},
};
var subjectPromise = Promise.resolve([
{name: 'test'},
{name: 'test2'},
]);
spyOn(subjectRepository, 'getAll').andReturn(subjectPromise);
// -------------------------------------
// request mock
var req = {
dic: {
subjectRepository: subjectRepository,
},
};
// -------------------------------------
// response mock
var res = {
json: function() {},
send: function() {},
};
spyOn(res, 'json');
// -------------------------------------
// actual test
var subjectController = new (require('../../../private/controllers/SubjectController'))();
subjectController.getAll(req, res);
// this succeeds
expect(subjectRepository.getAll).toHaveBeenCalled();
// this fails
// expect(res.json).toHaveBeenCalled();
});
});
Question: How do I make the test run the expect() AFTER the promise succeeded?
Node v0.12
The code is on GitHub for anyone who's interested: https://github.com/mihaeu/fair-projects
And maybe I should mention that the controller is called from the router:
// router handles only routing
// and controller handles data between view and model (=MVC)
subjectRouter.get('/:subjectId', subjectController.get);
I got this to work by changing our controllers to hand down the promises, but I'm not sure this is what we want. Isn't there a way to get my approach to work?
it('gets all existing subjects', function(done) {
// ...
var subjectController = new (require('../../../private/controllers/SubjectController'))();
subjectController.getAll(req, res).then(function() {
expect(res.json).toHaveBeenCalledWith(testSubjects); // success
}).finally(done);
expect(subjectRepository.getAll).toHaveBeenCalled(); // success
}
Your code makes the mistake of mixing business logic with front facing routing.
If your getAll did not touch the request and response object, it would look something like this:
ProjectController.prototype.getAll = function(subjectId) {
return req.dic.subjectRepository.getById(subjectId).then(function(subject){
return subject.projects;
});
};
Now, it is no longer related to the request response life cycle or in charge of logic, testing it is trivial by:
it("does foo", function(){
// resolve to pass the test, reject otherwise, mocha or jasmine-as-promised
return controller.getAll(152).then(...)
});
That would make your actual handler look like:
app.get("/projects", function(req, res){
controller.getAll(req.params.subjectId).then(function(result){
res.json(result);
}, function(){
res.status(404).send("...");
});
});
Brand new to testing. Trying to figure out why mocha is passing this test when it should be failing.
var assert = require('assert');
var nock = require('nock');
var https = require('https');
describe('thing', function() {
describe('foo', function () {
it('makes the correct https call to API', function () {
nock('https://example.com')
.get('/foo')
.reply(404);
https.get('https://example.com/foo', function (response) {
console.log(response.statusCode); // returns 404
assert.equal(response.statusCode, 200); //passes
});
});
});
});
Mocha, just like any other [properly-written] Node.js module/app, runs asynchronously out of the box. Because your https call takes longer to execute than the entire Mocha test, Mocha never has a chance to perform its assertions before the process completes.
That said, Mocha tests also supports a callback that let you execute long-running activities before performing your assertions:
var assert = require('assert');
var nock = require('nock');
var https = require('https');
describe('thing', function() {
describe('foo', function () {
it('makes the correct https call to API', function (done) {
nock('https://example.com')
.get('/foo')
.reply(404);
https.get('https://example.com/foo', function (response) {
console.log(response.statusCode); // returns 404
assert.equal(response.statusCode, 200); //passes
done();
});
});
});
});