Request path contains unescaped characters, in Mocha/chai testing - javascript

Given is my code to test a post request with an Authorization header set to the jwt token and a path parameter passed in to the post path, i.e. the id:5ee9b12ab08b6c3c58375a6d
Is there a better way to do this?
const expect = require("expect");
const request = require("request");
const chai = require("chai");
let chaiHttp = require("chai-http");
let server = require("../app");
let should = chai.should();
chai.use(chaiHttp);
describe("Admin Priveleges", () => {
describe("/Update Status", () => {
it("Update membership and registration status", (done) => {
chai
.request(server)
.post("/api​/v2​/user​/update-status​/5ee9b12ab08b6c3c58375a6d")
.set('Authorization', "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ZWU5YjEzN2IwOGI2YzNjNTgzNzVhNmUiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE1OTI0NjEyMjIyMDgsIm5hbWUiOiJVdGthcnNoIFNocml2YXN0YXZhIiwiaWF0IjoxNTkyMzc0ODIyfQ.M53gRzIppbhhLSCf9bD6xcdXfITiD1jUOzTlDqHK3is")
.send({
membership_status: "active",
registration_status: "pending_approval",
status_comment: "Good going"
})
.end((err, res) => {
if (err) throw err;
if (should) console.log("****Status Updated Successfully****");
res.should.have.status(200);
done();
});
}).timeout(30000);
});
});
So this is my testing code and each time I run this for testing I get the following error:
How can I solve this error?

When I copy-pasted you 'url' in web console, it showed the actual string with the otherwise 'invisible' .
Test:
context('Should not show "Request path contains unescaped characters, in Mocha/chai testing"', function() {
it('POST /api/v2/user/update-status/5ee9b12ab08b6c3c58375a6d', function(done) {
chai
.request(server)
.post('/api/v2/user/update-status/5ee9b12ab08b6c3c58375a6d')
// .post('/api​/v2​/user​/update-status​/5ee9b12ab08b6c3c58375a6d') // with the 'hidden dots'
.set('Authorization', "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ZWU5YjEzN2IwOGI2YzNjNTgzNzVhNmUiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE1OTI0NjEyMjIyMDgsIm5hbWUiOiJVdGthcnNoIFNocml2YXN0YXZhIiwiaWF0IjoxNTkyMzc0ODIyfQ.M53gRzIppbhhLSCf9bD6xcdXfITiD1jUOzTlDqHK3is")
.send({
membership_status: "active",
registration_status: "pending_approval",
status_comment: "Good going"
})
.end((err, res) => {
// if (err) throw err;
// if (should) console.log("****Status Updated Successfully****");
// res.should.have.status(200);
expect(res).to.have.status(404);
done();
});
});
})

Related

OPTIONS request inconsistency (in deployment)?

Introduction
So, I'm using the MERN stack (with Heroku + Netlify), and I'm having some really strange consistency problems with how the DELETE request is being handled. I've tried countless solutions in the last
three days trying to get this to work and none of them have worked. A lot of these solutions have
come from stack overflow, so if you want to direct me to another post, the chance is that I've already seen it. I've scoured every part of the web and making this post is my last resort.
The Problem
So, when I make a delete request, I'm getting the per-usual OPTIONS request since I'm sending a token in a custom header of the request ('x-auth-token'). The OPTIONS request always resolves with a 204, meaning that everything should be alright. However, afterward, there is no DELETE request like there should be. This, in essence, is my problem. I've checked my Heroku logs, and all I can see is the OPTIONS request, and nothing else.
Inconsistencies?
So this is where I've been very confused. The thing is, that sometimes it DOES work. And other routes I use in my API (like login, and creating a new post) work, even though I'm using the same middleware.
Every time it works, I get the OPTIONS request and then the DELETE request (with a 200 status) like I would expect to happen.
If you want an example of a re-creatable scenario:
I create X number posts after logging in and getting a valid token, then I can see those posts rendering in the posts listing on my home page. I then navigate one of the posts and delete it by clicking and then a confirmation button. I automatically get redirected to the next post in the list. I repeat this till I get to the last post. I delete that post, and since there are no more posts left, I get redirected to the posts listing which is... not empty! The last post I tried deleting is still there.
Keep in mind, that the DELETE requests all get sent in exactly the same way, so I'm pretty sure this isn't a front-end issue, so no need to poke around in the code there. I've logged everything and debugged, and it's 100% consistent with what I would expect.
(The create post doesn't redirect, while the delete post does? I don't see how this would effect anythign as the DELETE request gets sent as per usual... Though maybe a solution lies within this fact.)
Solutions I've tried
Cors
First off, you might already be rushing to your keyboard to tell me that this is a CORS issue. I thought the same thing yesterday, but I'm not so sure now. I've tried messing with all the config settings possible in CORS to get this to work. Since my two websites are on different domains, then CORS verifies the requests. I've already added my front-end website to a whitelist, and all other requests are going through properly, so no problem there. I've tried adding an allowHeaders option in the config, but it didn't do anything more than the default setting. I've also added 'OPTIONS' to the allowed methods in the config, still nothing. I'm also using app.use(cors({config})). I'll include some code later to see some more of this in detail.
Debugging
I've basically tested things out by inserting console.logs everywhere and discovered that neither the middleware, the options route (I tried making an options route with same route url), or the original post route get executed when the OPTIONS request doesn't result in a DELETE request.
Static Server
This is maybe where some of my inexperience shows (this is my first Web project). I saw some solutions telling that a static server is needed. So I tried setting up a static server, but I didn't see any results. So I'm not too sure what this accomplished.
Async and Await
I was just trying things at this point, so I made all my routes async to see if it would do anything. It didn't.
Others
I've also messed around with environment variables and dotenv, and other stuff I can't remember. I think everything here should already be sufficient information understand the situation.
Code
index.js
const express = require('express');
require("dotenv").config({ path: "variables.env" });
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const routes = require("./routes/router");
const cors = require("cors");
const morgan = require('morgan')
const app = express();
const whitelist = [
process.env.ORIGIN
];
app.use(
cors({
origin: function (origin, callback) {
if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
console.log(origin);
callback(new Error("Not allowed by CORS"));
}
}, //frontend server localhost:3000
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
credentials: true, // enable set cookie
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(morgan('dev'));
mongoose.connect(process.env.MONGODB_URL, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
console.log('connected to db');
});
const userSchema = mongoose.Schema({
name: String,
password: String
});
// Routes
// TODO: make seperate routers/routes
app.use("/", routes);
// Serve static assets if in production
if (process.env.NODE_ENV === 'production') {
// Set static folder
app.use(express.static('client/build'));
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
});
}
// TODO: set up custom port in future
app.listen(process.env.PORT, () => console.log(`Server listening at http://localhost:${process.env.PORT}`));
// Callback functions?
router.js
const express = require('express');
const router = express.Router();
const Post = require('../models/Post');
const User = require('../models/User');
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const auth = require('../middleware/auth');
const adminAuth = require('../middleware/adminAuth');
const cors = require("cors");
require("dotenv").config({ path: "variables.env" });
// import 'moment'
// second onwards are handlers => triggers like the post body then next() to go to the next handler
router.post('/api/add_post', adminAuth, async (req, res, next) => {
try{
newPost = new Post({
title: req.body.title,
body: req.body.body,
author: req.body.author,
created: req.body.created,
});
const savedPost = await newPost.save();
if (!savedUser) throw Error('Something went wrong saving the post');
res.send(savedPost);
} catch (e) {
res.status(400).json({ msg: e.message });
}
});
router.delete('/api/delete_post/:id', adminAuth, async (req, res, next) => {
// timeout?
// console.log(req.body);
try{
const id = req.params.id;
if(!id) throw Error('Invalid ID');
const post = await Post.findById(id);
if (!post) throw Error('Post doesn\'t exist');
const removed = await post.remove();
if(!removed) throw Error('Problem with deleting the post');
res.status(200).json({ success: true });
} catch(e) {
console.log("Error: ", e.message);
res.status(400).json({ msg: e.message, success: false });
}
});
// TODO : UPDATE for async soon
router.post('/api/update_post', adminAuth, async (req, res, next) => {
const id = req.body._id;
test_post_data = {
title: req.body.title,
body: req.body.body,
author: req.body.author,
modified: req.body.modified,
};
console.log(test_post_data, id);
Post.updateOne({ _id: id }, test_post_data, (err) => {
if(err) return next(err);
return res.status(200);
});
});
router.get('/api/get_posts', async (req, res, next) => {
try{
const posts = await Post.find();
if(!posts) throw Error('Error with fetching the posts')
res.send(posts.reverse());
} catch (e) {
res.status(400).json({ msg: e.message });
}
});
router.get('/api/get_chapter/:id', async (req, res, next) => {
try{
const id = req.params.id;
const post = await Post.findOne({_id: id})
if(!post) throw Error('No post was found')
res.send(post);
} catch(e) {
res.status(400).json({ msg: e.message })
}
});
// User routes
// TODO : make in seperate file
router.post('/api/user/register', async (req, res) => {
const { name, email, password } = req.body;
// Simple validation
if (!name || !email || !password) {
return res.status(400).json({ msg: 'Please enter all fields' });
}
try {
const user = await User.findOne({ email });
if (user) throw Error('User already exists');
const salt = await bcrypt.genSalt(10);
if (!salt) throw Error('Something went wrong with bcrypt');
const hash = await bcrypt.hash(password, salt);
if (!hash) throw Error('Something went wrong hashing the password');
const newUser = new User({
name,
email,
password: hash,
admin: false
});
const savedUser = await newUser.save();
if (!savedUser) throw Error('Something went wrong saving the user');
// TODO : check up on expires stuff : 3600 = 1 hr
const token = jwt.sign({ id: savedUser._id, admin: savedUser.admin }, process.env.JWT_SECRET, {
expiresIn: 3600
});
res.status(200).json({
token,
user: {
id: savedUser.id,
name: savedUser.name,
email: savedUser.email,
admin: savedUser.admin
}
});
} catch (e) {
res.status(400).json({ error: e.message });
}
});
router.post('/api/user/login', async (req, res) => {
const { name, password } = req.body;
// Simple validation
if (!name || !password) {
return res.status(400).json({ msg: 'Please enter all fields' });
}
try {
// Check for existing user
const user = await User.findOne({ name });
if (!user) throw Error('User Does not exist');
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) throw Error('Invalid credentials');
const token = jwt.sign({ id: user._id, admin: user.admin }, process.env.JWT_SECRET, { expiresIn: 3600 });
if (!token) throw Error('Couldnt sign the token');
res.status(200).json({
token,
user: {
id: user._id,
name: user.name,
email: user.email,
admin: user.admin
}
});
} catch (e) {
res.status(400).json({ msg: e.message });
}
});
module.exports = router;
adminAuth.js
const jwt = require('jsonwebtoken')
require("dotenv").config({ path: "variables.env" });
module.exports = (req, res, next) => {
console.log(req.header('x-auth-token'));
const token = req.header('x-auth-token');
// Check for token
if (!token)
return res.status(401).json({ msg: 'No token, authorizaton denied' });
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log('decoded:', decoded);
if(!decoded.admin)
return res.status(401).json({ msg: 'Not an admin, authorization denied' });
// Add user from payload
// console.log('decoded:', decoded);
req.user = decoded;
next();
} catch (e) {
res.status(400).json({ msg: 'Token is not valid' });
}
};
Link for request examples, and Heroku log since Stackoverflow says it's spam:
https://gist.github.com/macklinhrw/b2fec97642882ba406c49cce3e195c39
Edit
I pasted the Chrome request and response headers into the gist at the bottom, but there was no response data to go along with either.
I've debugged a little using this to check the difference and I discovered that with delete action that ends up working, the red (canceled) request has headers, while the non-working is completely empty (filled with 'provisional headers' if that means anything).
I couldn't copy-paste the request headers into the gist for the working red (canceled) one. But, I pasted everything that I thought could possibly be useful from chrome, hopefully it helps.
Also, I didn't see any DELETE requests when I was using the Chrome network tool, and I was seeing them on the other tool. Not sure if it matters, probably just a config option somewhere.
So, I haven't found an exact answer, but I've found a workaround.
As it turns out, it might have something to do with axios, and I've been searching for the wrong things in the last 3 days.
This thread helped me: https://github.com/axios/axios/issues/1428
I've added an e.preventDefault() to the onClick method I use for the delete button.
This fixed the problem, but doesn't redirect (I use href={link}), so I'm going to add a conditional render for react-router to redirect the page. I don't know of a better method so maybe give me some ideas. I'll edit if I have further troubles.

Unit testing using Mocha with Express Rest API

I'm learning unit testing. So far I was able to run simple tests like "Add two numbers and test if they are above 0", but I want to build a REST API using TDD. So far I have this:
My routes/index.js file:
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function (req, res, next) {
res.send({val: true});
});
module.exports = router;
My index.test.js file:
var mocha = require('mocha');
var assert = require('chai').assert;
var index = require('../routes/index');
describe('Index methods', () => {
it('Returns true', done => {
index
.get('http://localhost:3000')
.end(function (res) {
expect(res.status).to.equal(200);
done();
})
})
});
I user a tutorial to do this, but when I try to run this I get:
TypeError: index.get(...).end is not a function
So I'm guessing there is something wrong, but have no idea what. That's my first day learning TDD so if you see anything stupid please let me know.
Doing this:
it('Returns true', done => {
var resp = index.get('http://localhost:3000/');
assert.equal(resp.val === true);
done();
})
Also results in an error:
AssertionError: expected false to equal undefined
1. Install the dev dependencies for mocha
chai: assertion library for node and browser,
chai-http: HTTP Response assertions for the Chai Assertion Library.
2. You need to export your server,
'use strict';
/*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */
const express = require('express');
const app = express();
//...
const config = require('config');
const port = process.env.PORT || config.PORT || 3000;
//....
app.listen(port);
console.log('Listening on port ' + port);
module.exports = app;
3. Write your tests as:
If your test script is users.spec.js,it should start by:
// During the rest the en variable is set to test
/* global describe it beforeEach */
process.env.NODE_ENV = 'test';
const User = require('../app/models/user');
// Require the dev-dependencies
const chai = require('chai');
const chaiHttp = require('chai-http');
// You need to import your server
const server = require('../server');
const should = chai.should();
// Set up the chai Http assertion library
chai.use(chaiHttp);
// Your tests
describe('Users', () => {
beforeEach((done) => {
User.remove({}, (err) => {
done();
});
});
/**
* Test the GET /api/users
*/
describe('GET /api/users', () => {
it('it should GET all the users', (done) => {
chai.request(server)
.get('/api/users')
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a('array');
res.body.length.should.be.eql(0);
done();
});
});
});
// More test...
});
You can take a look at my repository, Github - Book Store REST API
const chai = require('chai');
const expect = require('chai').expect;
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
first install chai
it('Returns true', done => {
return chai.request(index)
.get('/')
.then(function (res) {
expect(res.status).to.equal(200);
done();
})
})
var mocha = require('mocha');
var assert = require('chai').assert;
var index = require('./index');
var req = require('supertest');
describe('Index methods', () => {
it('Returns true', done => {
req(index)
.get('/')
.end(function (res) {
expect(res.status).to.equal(200);
done();
})
})
});
also in your terminal type npm i supertest --save-dev
simple test case to check if the server is running properly.
const chai = require('chai'),
chaiHttp = require('chai-http'),
server = require('../app'),
faker = require('faker'),
should = chai.should();
chai.use(chaiHttp);
describe('Init', function () {
it('check app status', function (done) {
chai.request(server).get('/').end((err, res) => {
should.not.exist(err);
res.should.have.status(200);
done();
})
});
});
Test Cases for get API
describe('/Get API test', function () {
it('Check the api without user id parameter', function (done) {
chai.request(server).get('/post-list').end((err, res) => {
should.not.exist(err);
res.should.have.status(401);
res.body.should.be.a('object');
res.body.should.have.property('message');
res.body.should.have.property('message').eql('User Id parameter is missing');
done();
})
});
it('Check the api with user id. Success', function (done) {
chai.request(server).get('/post-list?user_id=1').end((err, res) => {
should.not.exist(err);
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('userId');
res.body.should.have.property('title');
res.body.should.have.property('body');
done();
})
});
});
Test Case for Post API
describe('/POST API test', function () {
it('Check the api without parameters . failure case', function (done) {
chai.request(server).post('/submit-data').send({}).end((err, res) => {
should.not.exist(err);
res.should.have.status(401);
res.body.should.be.a('object');
res.body.should.have.property('message');
res.body.should.have.property('message').eql('Mandatory params are missing!');
done();
})
});
it('Check the API with valid parameters. Success', function (done) {
chai.request(server).post('/submit-data').send({name:faker.name.firstName(),email:faker.internet.email()}).end((err, res) => {
should.not.exist(err);
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('message');
res.body.should.have.property('message').eql('data saved successfully');
done();
})
});
});
Add test cases as per the different case available in your API.
Find here the basic terminologies and complete sample application to proceed: https://medium.com/#pankaj.itdeveloper/basics-about-writing-tests-in-nodejs-api-application-4e17a1677834

Post Method with Chai (Node.js)

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

How to force error branch in jasmine-node test

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)
// ...

Mocha Testing a post function

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

Categories