i want to perform some validation and addition to the request on specific parameter for a route and all nested routes.
My REST structure is /room/:room/messages
In my main.js
const roomRoute = require('roomroute.js');
const messageRoute = require('messageroute.js');
app.use('/room',roomRoute);
app.use('/room/:room/messages',messageRoute);
in roomroute.js
const express = require('express');
const router = express.Router();
router.param('room', function (req,res,next,id) {
// Just for demo
var room = {
id: id,
title: 'Testroom'
};
req.room = room;
next();
});
router.get('/:room, function (req,res) {
// Display room to console
console.log(req.room);
res.sendStatus(200).end();
});
module.exports = router;
In messageroute.js
const express = require('express');
const router = express.Router({ mergeParams:true });
router.get('/', function(req,res) {
console.log(req.room); // Not working
});
module.exports = router;
When i do a get to a room eg. /room/1234 then req.room is displayed to the console, which actually is want i want.
But when i do a get to eg. /room/1234/messages the route.param('room'... of the parent is not executed, but instead just the get of the messageroute.js.
Is there a way to achieve that the param is evaluated for the parent route and also for all nested routes ?
Thank you,
Stefan
This looks like a misunderstanding of how nested routers work. In your example you seem to be looking to share a param across roomroute and messageroute, however, both of those routers have no relation to eachother.
Routers become nested when they're supplied as middleware to another router - you have an example of this already....app is a router in itself and you nest both roomroute and messageroute into it. So based on your current setup, if you want to share param('room') across both these routes you will need to configure it at app level i.e.
main.js
const roomRoute = require('roomroute.js');
const messageRoute = require('messageroute.js');
app.param('room', function (req,res,next,id) {
// Just for demo
var room = {
id: id,
title: 'Testroom'
};
req.room = room;
next();
});
app.use('/room', roomRoute);
app.use('/room/:room/messages', messageRoute);
roomroute.js
const router = express.Router({ mergeParams: true });
router.get('/:room', ...);
messageroute.js
const router = express.Router({ mergeParams: true });
router.get('/', ...);
Related
I'll go in detail on what I really want my server to do.
So basically, I'm creating a project in which the user register and then logs in.
The routes are like - localhost:3000/login and localhost:3000/register
Now when the user logs in using their credentials (POST) , it should send them to localhost:3000/home which is unique for every person. I don't want to send the data like we do for templating engines but I want to make that data accessible across all routes so that I can use it whenever I like.
What I'm having trouble with is that when the person logs in , their data gets stored in a session (which contains their name and other user data) which as of now is not sharable between routes. The session gets stored for the /login route and I'm unable to use it for the /home route or for any other route for that matter.
Is there any way that I can use save a session every time a person logs in (using POST) and make that session data available across all my routes ?
server.js
var express = require('express');
const path = require('path');
const fs = require('fs');
const session = require('express-session');
const { v4: uuidv4 } = require('uuid');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public/auth-pages')));
app.use(express.json());
var publicPath = __dirname+"/public"
var authPagesPath = publicPath+"/auth-pages"
var jsonPath = __dirname+"/json"
var usersFile = jsonPath+"/users.json"
var testVar = 1;
app.set('view engine', 'pug')
// have to be on top
app.use(logger)
app.get('/',(req,res) => {
res.sendFile('index.html',{root:publicPath})
})
app.get('/home',(req,res) => {
var data = {}
res.render('home',data)
})
app.get('/profile',(req,res) => {
var data = {}
res.render('profile',data)
})
app.get('/login',(req,res) => {
res.sendFile('login.html',{root:authPagesPath})
})
app.get('/register',(req,res) => {
res.sendFile('register.html',{root:authPagesPath})
})
app.post('/register',(req,res) => {
var data = req.body;
if (register(data)){
res.send({
register:"success"
})
}
else {
res.send({
register:"fail"
})
}
})
app.post('/login',(req,res) => {
var data = req.body;
fs.readFile(usersFile,(error,fullData) => {
var fullData = JSON.parse(fullData);
allUsernames = Object.keys(fullData);
if (!allUsernames.includes(data.username)){
res.send({
login:"fail",
reason:"invalid-username"
})
}
else if (fullData[data.username]['pwd'] != data.pwd){
res.send({
login:"fail",
reason:"wrong-pwd"
})
}
else {
res.send({
login:"success",
user:fullData[data.username]
})
// session storage
req.session.user = {
username:data.username,
id:fullData[data.username]['id']
}
console.log(req.session)
}
})
})
app.get('/test',(req,res) => {
testVar += 1;
res.send(""+testVar);
})
function register(data){
fs.readFile(usersFile,(err,fullData) => {
var fullData = JSON.parse(fullData)
if (Object.keys(fullData).includes(data.username)){
console.log('username taken')
}
else {
fullData[data.username] = {
id:uuidv4(),
pwd:data.pwd
}
fullData = JSON.stringify(fullData,null,4)
fs.writeFile(usersFile,fullData,error => {
})
}
})
return true;
}
You can use cookies to keep user data between routes.
If you dont want to store the whole data in the browser,
you can keep the user id in cookies and store the whole data in a repository object.
for example:
you can create a class that will store the state of the logged in users and can be reachable between routes.
the class should be instantiate and you should export its object.
( this solution keeps the state in memory and will be lost when service restarts/shutdown. to make the state available after restarts you can store the state in db (redis, mongo etc...)).
repo class:
class MyRepository {
constuctor() {
this.users = {};
}
set(user) {
this.users[user.id] = user;
}
get(userId) {
return this.users[userId];
}
}
let repo = new MyRepository();
module.exports = repo;
route:
const express = require('express');
const router = express.Router();
const repo = require('myrepository.js'); // this line will get you the object with all logged in users already
router.post('/login', (req, res, next) => {
// check if user logged in and get the user id (to myUserId);
user = ...
logged = ...
if (logged) {
res.cookie('userId', user.id)
repo.set(user)
}
});
route.get('/home', (req, res, next) => {
let userId = req.cookies.userId // get user id from cookie
let user = repo.get(userId);
});
You can do this by having some sort of a class that holds user's data. Please be aware that this solution is not scalable and you are designing a single point of failure.
For instance, if you have multiple servers running your application with a load balancer that routes requests to your servers. Let's say Server A creates an object from a class for User A. In the second or third request, presume that User A's request gets routed to Server B, which has not created an object that holds User A's data. This leads to scalability and even perhaps inconsistency issues.
Let's say I have the following routes:
// routes.js
import PhotoRoutes from './photoRoutes';
const UserBaseRoute = Router();
UserBaseRoute.use('/api/:userId', PhotoRoutes);
// photoRoutes.js
const PhotoRoute = Router();
PhotoRoute.get('/', (req, res) => {
console.log(req.params);
res.end();
});
export default PhotoRoute;
When I hit /api/123/ I expect to use {"userId: 123}' But I don't. Why is the :userId defined in the baseRoute not passed up?
This is a issue related to nested router.
You need to set the mergeParams of child router as true to access params from parent router.
So try following code:
const PhotoRoute = Router({mergeParams: true});
BTW, this option came with Express version 4.5. For more details, refer to the API document
I'm new to mongoose so I apologise for incorrect uses of terminology.
I have a routes file as detailed below
const express = require('express');
const router = express.Router();
const passport = require('passport');
const controller = require('./clubController');
const authGuard = passport.authenticate('jwt', { session: false });
const verifyUser = require('./clubController').verifyUser;
const isSiteAdmin = require('./clubController').isSiteAdmin;
router.param('id', controller.params);
router.route('/')
.post(authGuard, controller.newClub)
.get(controller.allPublicClubs);
router.route('/:id')
.put(authGuard, verifyUser(), controller.editClub)
.get(controller.getClub);
router.route('/private')
.get(controller.allPrivateClubs);
module.exports = router;
controller.params
exports.params = function(req, res, next, id) {
Club.findById(id)
.populate('creator teams', '-password -email -role')
.exec()
.then(function(club) {
if (!club) {
return res.status(404).send({ msg: 'No Club exists with that ID' });
} else {
req.club = club;
next();
}
}, function(err) {
// error handling
next(err);
});
};
controller.params is being fired when I make a get request to /private. To my understanding, the params middleware I have setup should only be fired when a called route is using an id parameter.
The value for the id argument in controller.params is set as private which is the route.
The error I receive is detailed below
CastError: Cast to ObjectId failed for value "private" at path "_id" for model "club"
This was working fine yesterday, no idea what changed that it now does not work.
I solved the issue by moving
router.route('/:id')
.put(authGuard, verifyUser(), controller.editClub)
.get(controller.getClub);
To the bottom of all the routes. Was strange as I had the same order before and it worked fine
I'm new to Node and Express and I'm trying to unit test my routes/controllers. I've separated my routes from my controllers. How do I go about testing my routes?
config/express.js
var app = express();
// middleware, etc
var router = require('../app/router')(app);
app/router/index.js
module.exports = function(app) {
app.use('/api/books', require('./routes/books'));
};
app/router/routes/books.js
var controller = require('../../api/controllers/books');
var express = require('express');
var router = express.Router();
router.get('/', controller.index);
module.exports = router;
app/api/controllers/books.js
// this is just an example controller
exports.index = function(req, res) {
return res.status(200).json('ok');
};
app/tests/api/routes/books.test.js
var chai = require('chai');
var should = chai.should();
var sinon = require('sinon');
describe('BookRoute', function() {
});
If you just want to unit test the route's presence and its method, you can do something like this:
auth.router.js
import { Router } from 'express';
const router = Router();
router.post('/signup', signupValidation, signupUser);
router.post('/login', loginValidation, loginUser);
router.post('/reset', resetValidation, setPasswordReset);
export default router;
auth.router.spec.js
test('has routes', () => {
const routes = [
{ path: '/signup', method: 'post' },
{ path: '/login', method: 'post' },
{ path: '/reset', method: 'post' },
]
it.each(routes)('`$method` exists on $path', (route) => {
expect(router.stack.some((s) => Object.keys(s.route.methods).includes(route.method))).toBe(true)
expect(router.stack.some((s) => s.route.path === route.path)).toBe(true)
})
Note: The use of $variables in the example test name will only work with Jest ^27.0.0
Edit: Thanks to Keith Yeh for his suggestion to put this into an each() statement. I have updated the code accordingly & the old code is below:
auth.router.spec.js (OLD)
import router from '../auth.router';
test('has routes', () => {
const routes = [
{ path: '/signup', method: 'post' },
{ path: '/login', method: 'post' },
{ path: '/reset', method: 'post' }
]
routes.forEach((route) => {
const match = router.stack.find(
(s) => s.route.path === route.path && s.route.methods[route.method]
);
expect(match).toBeTruthy();
});
});
Code:
config/express.js
var app = express();
// middleware, etc
var router = require('../app/router')(app);
module.exports = app;
app/tests/api/routes/books.test.js
var chai = require('chai');
var should = chai.should();
var sinon = require('sinon');
var request = require('supertest');
var app = require('config/express');
describe('BookRoute', function() {
request(app)
.get('/api/books')
.expect('Content-Type', /json/)
.expect('Content-Length', '4')
.expect(200, "ok")
.end(function(err, res){
if (err) throw err;
});
});
Considerations:
If your server requires an initial state at the beginning of a set of tests (because you're executing calls which mutate server state), you'll need to write a function that will return a freshly configured app and the beginning of each group of tests. There is an NPM library: https://github.com/bahmutov/really-need that will allow you to require a freshly instantiated version of your server.
This is interesting because you've separated out your controllers from your routers. The other StackOverflow article mentioned in the comments is a good way to test your controllers, I think. The thing to keep in mind with unit tests is what are you testing exactly. You shouldn't need to write tests to test the express library because presumably it has its own unit tests. So you just need to test your calls to the library. So for the books route, you just need to test this one line of code:
router.get('/', controller.index);
I looked around to see if there was an obvious way to get a list of routes from the express library, but I didn't see one. You can probably just look at the library itself and check its internals to see if you set a route correctly. Another option though is to mock it up and just check that you are calling it correctly.
This is going to get pretty complicated because you need to mock up a some fundamental parts of Javascript in order to test this one line of code. Here's how I did it:
describe('BookRoute', function() {
it("should route / to books controller index", function() {
var controller = require('../../../api/controllers/books');
var orig_this = this;
var orig_load = require('module')._load;
var router = jasmine.createSpyObj('Router', ['get']);
var express = jasmine.createSpyObj('express', ['Router']);
express.Router.and.returnValues(router);
spyOn(require('module'), '_load').and.callFake(function() {
if (arguments[0] == 'express') {
return express;
} else {
return orig_load.apply(orig_this, arguments);
}
});
require("../../../router/routes/books");
expect(router.get).toHaveBeenCalledWith('/', controller.index);
});
});
What's going on here is I used Jasmine's spyOn function to spyOn the _load function in module.js which is what handles all of the require calls. This is so that when we require the books router and it calls require('express') we can return our express SpyObj that we created with jasmine.createSpyObj. Once we have replaced express with our spy object, then we can have it return our Router SpyObj which will let us spy on router.get. Then we can check to make sure it is called with '/' and controller.index.
This could probably be made into some sort of utility if you wanted to use this a lot.
I usually avoid a lot of this thing by using a more object oriented approach and either I'm passing around some object everywhere that I can mock for tests or you could use some kind of dependency injection like Angular uses.
I found this blog incredibly insightful when testing my own servers endpoints.
In the blog he addresses:
How to use the endpoint testing library supertest.
How to programmatically spin up and tear down an express server with your needed routes before and after each endpoint test. (he also explains why you would want to do this).
How to avoid a common gotcha, require caching your modules required in your unit tests, leading to unintended consequences.
Hope this helps. Good luck and if you have any further questions let me know.
I'm having an issue getting Sinon's stub to work correctly for me. When I stub list on retro and the test runs, app.get('/retro', retro.list) is executing the original function retro.list instead of the stub. Since this happens, the test fails because the stub's callCount is 0.
I'm more familiar with coffeescript and I have stubbed things in the same way. Is there something I'm not understanding about Javascript's scoping, or how the require('../routes/retro') works, or is retro is not the same in app.js and test.js.
Much thanks for the help and code below.
test.js:
var request = require('supertest')
, retro = require('../routes/retro')
, app = require('../app')
, sinon = require('sinon');
require('should');
describe('GET /retro', function() {
// less involved, but maybe stupid to test
it('should call retro.list', function(done) {
var stub = sinon.stub(retro, 'list');
request(app)
.get('/retro')
.end(function(err, res){
stub.callCount.should.equal(1);
if (err) return done(err);
done();
})
})
})
app.js:
var express = require('express')
, config = require('./config')
, routes = require('./routes')
, retro = require('./routes/retro');
var app = express();
config(app);
app.get('/', routes.index);
app.get('/retro', retro.list);
module.exports = app;
retro.js:
var retro = {
list: function(req, res){
console.log('actual called');
res.send("respond with a resource");
}
}
module.exports = retro;
You'll likely need to create your stubs before requiring/creating the app.
var request = require('supertest')
, sinon = require('sinon')
, retro = require('../routes/retro');
var stubRetroList = sinon.stub(retro, 'list');
var app = require('../app');
// ...
stubRetroList.callCount.should.equal(1);
This allows retro.list to be updated before it's passed to the route:
app.get('/retro', retro.list);
The issue is probably because retro.list isn't being passed-by-reference (pointer), but is rather a reference that's passed-by-value (copied). So, though sinon.stub() is altering retro.list, it wouldn't affect the copy that the '/retro' route already had.
I faced the same issue and the accepted answer (while true) was of no help. Turns out in order for sinon stubbing to work the stubbed method cannot be used in the same module. In other words stubbing a module endpoint will only stub the module endpoint and not the internal usage of the function referenced by module.exports.
Explained via an example:
module.js
const express = require('express')
const router = express.Router()
router.get('/', function (req, res) {
res.status(200).json(list())
})
function list() {
return ['something']
}
module.exports = {
router: router,
list: list
}
module.spec.js
// This stub will not work
sinon.stub(module, 'list').callsFake(() => ['something else'])
To make it work you have separate what you want to stub into its own module and use it that way:
sub_module.js
function list() {
return ['something']
}
module.exports = {
list: list
}
Now sub_module.list() can be stubbed.
(OP defines a method in place so this is not an issue for him)