I'm testing the controller logic behind API endpoints in my node server with jasmine-node. Here is what this controller logic typically looks like:
var getSummary = function(req, res) {
var playerId = req.params.playerId;
db.players.getAccountSummary(playerId, function(err, summary) {
if (err) {
logger.warn('Error while retrieving summary for player %d.', playerId, err);
return res.status(500).json({
message: err.message || 'Error while retrieving summary.',
success: false
});
} else {
res.json({success: true, summary: summary});
}
});
};
Below is how I successfully test the else block:
describe('GET /api/players/:playerId/summary', function() {
it('should return an object summarizing the player account', function(done) {
request
.get('/api/players/' + playerId + '/summary')
.set('Content-Type', 'application/json')
.set('cookie', cookie)
.expect(200)
.expect('Content-Type', /json/)
.end(function(err, res) {
expect(err).toBeNull(err ? err.message : null);
expect(res.body.success).toBe(true);
expect(res.body.summary).toBeDefined();
done();
});
});
});
This works nicely, but leaves me with poor branch coverage as the if block is never tested. My question is, how do I force the error block to run in a test? Can I mock a response which is set to return an error so that I can test the correct warning is logged and correct data is passed back?
It depends on your tests. If you only want to unit test, spies are the way to go.
You can just stub your db response. Be aware that in this case the database is not called though. It's just simulated.
const db = require('./yourDbModel');
spyOn(db.players, 'getAccountSummary').and.callFake(function(id, cb) {
cb(new Error('database error');
});
request
.get('/api/players/' + playerId + '/summary')
.set('Content-Type', 'application/json')
.set('cookie', cookie)
.expect(500)
// ...
If you want functional/integration tests, you need to call your request simply with wrong data, for example a players id that doesn't exist in your database.
request
.get('/api/players/i_am_no_player/summary')
.set('Content-Type', 'application/json')
.set('cookie', cookie)
.expect(500)
// ...
Related
I am trying to write a small API logger as an Express middleware. The logger collects various pieces of information from the req and res, then saves a JSON file to disk that can be read later.
This is my current function to store the logs.
function store(req, res, next) {
init();
const log = {
request_url: req.hostname,
request_body: req.body,
request_method: req.method,
request_headers: req.headers,
api_endpoint: req.baseUrl,
timestamp: moment().format('x')
};
res.on('finish', () => {
log.response_body = res.body;
log.response_status = res.statusCode;
global.enoch_logs.push(log);
fs.writeFile(`./logs/${ moment().format('x') }.json`, JSON.stringify(log), (err) => (err) ? console.log(err) : null);
});
next();
}
The problem is that res.body is always empty. I have tried a few different methods to capture the response body but nothing seems to work.
Where am I going wrong?
I am having trouble making my unit test pass and I think it is because it is posting using the wrong type of data. My route:
let upload = multer({
storage: storage
});
router.route('/')
.post(upload.any('image'), function (req, res, next) {
let memory = new Memory();
if (req.files.length === 0) {
Object.assign(memory, req.body);
} else {
Object.assign(memory, req.body, {'image': req.file.secure_url});
}
memory.save(function (err) {
if (err) {
return res.send(err);
}
res.json({message: 'Memory Created', memory});
});
})
As you can see my route uses multer which accepts form-data as input. However, in my Chai test:
it('it should not post an item without a location field', (done) => {
let formData = new FormData();
formData.append('description', "First time we met");
formData.append('image','n/a');
chai.request(server)
.post('/api/memory')
.set('Accept', 'application/form-data')
.send(formData)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('errors');
res.body.errors.should.have.property('location');
res.body.errors.location.should.have.property('kind').eql('required');
done();
});
I am using Chai's send method but this test just freezes and gives me no response. So I tried using postman and if I send data using x-www-form-urlencoded it feezes but if I send data using form-data it works fine. So I suspect I am sending data in x-www-form-urlencded using Chai. How do I fix this? (Note: I tried using .set('Accept', 'application/form-data'))
Simply use .field()
describe('/POST item', () => {
it('it should not post an item without a location field', (done) => {
chai.request(server)
.post('/api/memory')
.set('Accept', 'application/form-data')
.field('description', "First time we met")
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('errors');
res.body.errors.should.have.property('location');
res.body.errors.location.should.have.property('kind').eql('required');
done();
});
});
In my ReactJS project, I am currently running the server with NodeJS and ExpressJS, and connecting to the MongoDB using MongoClient. I have a login API endpoint set up that accepts a request with user's username and password. And if a user is not found, should catch the error and respond with an error (status(500)) to the front-end.
But rather than responding to the front-end with an json error, the server gets crashed. I have tried everything to figure out why but still no luck.
How can I fix the following error? Any guidance or insight would be greatly appreciated, and will upvote and accept the answer.
I intentionally made a request with a username and a password ({ username: 'iopsert', password: 'vser'}) that does not exist in the database.
Here is the login endpoint:
//login endpoint
app.post('/api/login/', function(req, res) {
console.log('Req body in login ', req.body)
console.log('THIS IS WHAT WAS PASSED IN+++++', req._id)
db.collection('users').findOne({username: req.body.username}, function(err, user) {
console.log('User found ')
if(err) {
console.log('THIS IS ERROR RESPONSE')
// Would like to send this json as an error response to the front-end
res.status(500).send({
error: 'This is error response',
success: false,
})
}
if(user.password === req.body.password) {
console.log('Username and password are correct')
res.status(500).send({
username: req.body.username,
success: true,
user: user,
})
} else {
res.status(500).send({
error: 'Credentials are wrong',
success: false,
})
}
})
And here is the terminal error log:
Req body in login { username: 'iopsert', password: 'vset' }
THIS IS WHAT WAS PASSED IN+++++ undefined
User found
/Users/John/practice-project/node_modules/mongodb/lib/utils.js:98
process.nextTick(function() { throw err; });
^
TypeError: Cannot read property 'password' of null
at /Users/John/practice-project/server/server.js:58:12
at handleCallback (/Users/John/practice-project/node_modules/mongodb/lib/utils.js:96:12)
at /Users/John/practice-project/node_modules/mongodb/lib/collection.js:1395:5
at handleCallback (/Users/John/practice-project/node_modules/mongodb/lib/utils.js:96:12)
at /Users/John/practice-project/node_modules/mongodb/lib/cursor.js:675:5
at handleCallback (/Users/John/practice-project/node_modules/mongodb-core/lib/cursor.js:165:5)
at setCursorNotified (/Users/John/practice-project/node_modules/mongodb-core/lib/cursor.js:505:3)
at /Users/John/practice-project/node_modules/mongodb-core/lib/cursor.js:578:16
at queryCallback (/Users/John/practice-project/node_modules/mongodb-core/lib/cursor.js:226:18)
at /Users/John/practice-project/node_modules/mongodb-core/lib/connection/pool.js:430:18
And /Users/John/practice-project/node_modules/mongodb/lib/utils.js:98 is referring to the following:
var handleCallback = function(callback, err, value1, value2) {
try {
if(callback == null) return;
if(value2) return callback(err, value1, value2);
return callback(err, value1);
} catch(err) {
process.nextTick(function() { throw err; });
return false;
}
return true;
}
EDIT
Here are everything that's being imported to the server:
"use strict"
var express = require('express');
var path = require('path');
var config = require('../webpack.config.js');
var webpack = require('webpack');
var webpackDevMiddleware = require('webpack-dev-middleware');
var webpackHotMiddleware = require('webpack-hot-middleware');
var bodyParser = require('body-parser');
var MongoClient = require('mongodb').MongoClient;
var ObjectId = require('mongodb').ObjectID;
const jwt = require('jsonwebtoken')
var app = express();
var db;
var compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, {noInfo: true, publicPath: config.output.publicPath}));
app.use(webpackHotMiddleware(compiler));
app.use(express.static('dist'));
app.use(bodyParser.json());
And this is how the request is made and error is caught:
loginUser(creds) {
var request = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(creds),
}
fetch(`http://localhost:3000/api/login`, request)
.then(res => res.json())
.then(user => {
console.log(user);
console.log('Successful')
})
.catch(err => {
console.log('Error is', err)
})
},
It looks to me like the error is being thrown on this line because user is not defined.
if(user.password === req.body.password) {...}
Take a harder look at your console statements.
1. Req body in login { username: 'iopsert', password: 'vset' }
2. THIS IS WHAT WAS PASSED IN+++++ undefined
3. User found
4. /Users/John/practice-project/node_modules/mongodb/lib/utils.js:98
5. process.nextTick(function() { throw err; });
^
6. TypeError: Cannot read property 'password' of null
7. at /Users/John/practice-project/server/server.js:58:12
Line 2 shows that req._id is undefined
Your User found statement is printed before you check if there is an error or if the user actually exists, so it isn't representative of there actually being a user.
Line 6 shows that the error is being thrown because you're trying to read a property of password from a null object.
I'd recommend modifying your login logic to look more like this:
//login endpoint
app.post('/api/login/', function(req, res) {
console.log('Performing login with req.body=');
console.log(JSON.stringify(req.body, null, 4));
// check for username
if (!req.body.username) {
return res.status(401).send({message: 'No username'});
}
// find user with username
db.collection('users').findOne({username: req.body.username}, function(err, user) {
// handle error
if(err) {
console.log('Error finding user.');
return res.status(500).send({message: 'Error finding user.'});
}
// check for user
if (!user) {
console.log('No user.');
return res.status(500).send({message: 'No user.'});
}
console.log('User found.');
// check password
if(user.password !== req.body.password) {
console.log('Wrong password.');
return res.status(401).send({message: 'Wrong password.'});
}
// return user info
return res.status(200).send(user);
});
Some final thoughts:
Make sure to handle the error (if it exists) and check that user exists before proceeding.
Always include return in your return res.status(...).send(...) statements, otherwise the subsequent code will execute.
It's generally not a good idea to save passwords as simple strings. Work toward encrypting them. Look at passport or bcrypt.
Hope this helps.
Hmmm just double checking if I'm making some silly error, however doesn't seem like it. I just want this test to pass but it keeps giving me a timeout error. This module should work, it is sending mail correctly, but mocha keeps giving a timeout.
// describe('POST /api/mail', function() {
// it('should successfully send mail', function(done) {
// request(app)
// .post('/api/mail')
// .send(form)
// .expect(200)
// .end(function(err, res) {
// if (err) return done(err);
// done();
// });
// });
// });
This is the actual function being tested
'use strict';
var transporter = require('./transporter.js').transporter;
exports.sendMail = function(req, res){
// setup e-mail data with unicode symbols
var mailOptions = {
from: req.body.name + ' <'+req.body.email+'>',
to: 'test#gmail.com',
subject: req.body.subject,
text: req.body.message
};
// send mail with defined transport object
transporter.sendMail(mailOptions, function(err, info){
if(err){
res.status(400); //error
}else{
res.status(200); //success
}
});
};
I think Mocha is waiting for sendMail result via callback. I have a similar sendMail, using nodemailer.js, in an application:
function send(fr, to, sj, msg, callback){
//...
var transport = nodemailer.createTransport();
console.log("Message content: "+msg);
transport.sendMail({from:fr, to:to, subject: sj, text: "\r\n\r\n" + msg},
function(err,response){
if(err){
callback(err);
}else{
callback(response);
}
});
};
In my test:
describe('when this example is tested',function(done){
it('should be sending an email', function(done){
mailSender.sendMail('test#test.es', 'Test', 'Test text', function(sent){
sent.should.be.ok;
done();
});
});
You get the sent in your callback and then Mocha can reach the done() method to indicate the test has finished.
Also, you can use Supertest to test your endpoint. It should be something like this:
it('should return 200 on /api/mail', function(done) {
supertest('http://localhost:3000').post('/api/mail').expect(200)
.end(
function(err, res) {
if (err) {
return done(err);
}
done();
});
});
I have a main express route that once navigated to, should prompt a download of the specified file in res.download:
app.get('/download/:fileid', (req, res) => {
var connection = mysql.createConnection({
host : 'localhost',
user : 'root',
password : '',
database : 'filesystem'
});
connection.connect(() => {
console.log("Connection to database established");
});
connection.query(`SELECT * from files where fileid = '${req.params.fileid}'`, (error, results, fields) => {
if (error) throw error;
else if(results.length === 0){
console.log("empty");
}
else{
res.download(`./uploads/PFSk5x7bg.png`, 'PFSk5x7bg.png', (err) => {
console.log(err);
})
}
});
connection.end();
})
However, when accessed, it simply displays the image on the webpage, and when I check the response headers, the content disposition header is missing. I have tried manually setting it with res.setheader, but still does not work.
I tried making another route just for testing:
app.get('/test', (req, res) => {
res.download(`./uploads/PFSk5x7bg.png`, 'PFSk5x7bg.png', (err) => {
console.log(err);
})
})
and it works.... the content disposition header is there, the browser actually downloads the file.
Is anyone able to understand what I am doing wrong with my /download route?
***UPDATE:
I changed the name of the /download/:fileid route to /test/:fileid and it worked. I then changed it BACK to /download/:fileid and it now works...
does anyone have any idea what could of caused this?
You have a few errors there, which can screw your debugging.
You have misplaced your connection.end() statement which will be called immediately after connection.query(). You should move it inside of connection.query's callback.
Using relative paths instead of abolute ones. Try changing './uploads/PFSk5x7bg.png' to __dirname + '/uploads/PFSk5x7bg.png'
Not using res.end() in the request where necessary may confuse browser's representation of response of your software.
if (error) {
res.end();
throw error;
}else if(results.length === 0){
res.end();
console.log("empty");
}
else{
res.download(`./uploads/PFSk5x7bg.png`, 'PFSk5x7bg.png', (err) => {
console.log(err);
})
}