I want to create an Express Middleware to perform a basic check if the user/password pair in the authorization header exists in a JSON file (educational purpose). I added it on a very simple unit converter app.
The problem is that I receive a 403 instead of the resource when the username/password are correct.
I found that when I perform a request, the Promise.then in the middleware is executed before the Promise is fulfilled in my function findUserByCredentials. See an illustration of the problem in the third code snippet below.
index.js
const express = require('express')
const app = express()
const port = process.env.PORT || 3000
const findUserByCredentials = require("./lib/find-user");
app.use(function (req, res, next) {
if (req.headers) {
let header = req.headers.authorization || '';
let [type, payload] = header.split(' ');
if (type === 'Basic') {
let credentials = Buffer.from(payload, 'base64').toString('ascii');
let [username, password] = credentials.split(':');
findUserByCredentials({username, password}).then(() => {
console.log("next")
next();
}).catch(() => {
console.log("403")
res.sendStatus(403);
});
}
} else {
next();
}
});
app.get('/inchtocm', (req, res) => {
const cm = parseFloat(req.query.inches) * 2.54;
res.send({"unit": "cm", "value": cm});
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
module.exports = app;
./lib/find-user.js
const bcrypt = require('bcrypt');
const jsonfile = require('../users.json');
let findUserByCredentials = () => (object) => {
const username = object.username;
const password = object.password;
return new Promise((resolve, reject) => {
jsonfile.forEach(user => {
if (user.username === username) {
bcrypt.compare(password, user.password).then((buffer) => {
if (buffer) {
console.log("resolve")
resolve();
} else {
console.log("reject")
reject();
}
});
}
});
reject();
});
};
module.exports = findUserByCredentials();
Server console after sending a request
Example app listening at http://localhost:3000
403
resolve
How can I force Express to wait for the first Promise to finish before performing the second operation ?
To have a better control over the order of your promises and also have a less nested code, you should use the async/await syntax. You can read more about it here.
What it essentially does is lets you...well, await for an async operation to finish before proceeding. If you use await before something that returns a Promise (like your findUserByCredentials), it will assign to the variable what you resolve your promise with, for example:
const myPromise = () => {
return new Promise(resolve => resolve(3));
}
const myFunc = async () => {
const number = await myPromise();
console.log(number); // Output: 3
}
I would rephrase your findUserByCredentials function like so:
const bcrypt = require('bcrypt');
const jsonfile = require('../users.json');
const findUserByCredentials = async (object) => {
const username = object.username;
const password = object.password;
// This assumes there's only a single unique user with a specific username
const potentialUser = jsonfile.find(user => user.username === username);
if (!potentialUser) {
throw new Error('wrong credentials')
}
const passHashCompare = await bcrypt.compare(password, potentialUser.password);
if (!passHashCompare) {
throw new Error('wrong credentials')
}
};
module.exports = findUserByCredentials;
This way it's less nested, more readable and works in the order you need.
You can go even further with this principle and make your middleware (the function you're passing to app.use) an async function as well and use the await keyword instead of .then() and .catch()
I would switch the code to use the latest features of the javascript language related to asynchronous code async/await in that way you can have better control of your execution flow.
I will modify your code in the following way:
Firstly for the findUserByCredentials function:
const bcrypt = require('bcrypt');
const jsonfile = require('../users.json');
const findUserByCredentials = async (object) => {
const username = object.username;
const password = object.password;
const user = jsonfile.find(user => user.username == username);
if (user)
await bcrypt.compareSync(user.password, password);
return false;
};
module.exports = findUserByCredentials;
Secondly for the index.js
const express = require('express')
const app = express()
const port = process.env.PORT || 3000
const findUserByCredentials = require("./lib/find-user");
app.use(async (req, res, next) => {
if (req.headers) {
let header = req.headers.authorization || '';
let [type, payload] = header.split(' ');
if (type === 'Basic') {
const credentials = Buffer.from(payload, 'base64').toString('ascii');
const [username, password] = credentials.split(':');
const result = await findUserByCredentials({username, password})
if(result) {
return next()
}
return res.sendStatus(403);
}
return res.sendStatus(403);
}
return next();
});
app.get('/inchtocm', (req, res) => {
const cm = parseFloat(req.query.inches) * 2.54;
res.send({"unit": "cm", "value": cm});
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
module.exports = app;
It's very important learn how to use asynchronous code in Nodejs because it's a single thread and if it's not used correctly you can block it and your performance of your app won't be optimal, also async/await is syntactical sugar of javascript languages which allows to write clean asynchronous code, try to use the latest things of the specification of language because they are created to ease our lives.
Related
This is the filesPost controllers file. Here I fetch all the datas from MongoDB as well I push datas there. The function works very well without logging in console the userInfo but when I try to log it in console, it gives the error that userInfo.toObject({getters: true}) is not a function. I have tried toJSON() but I got the same error.
const { validationResult } = require("express-validator");
const HttpError = require("../models/http-error");
const File = require("../models/file");
const User = require("../models/user");
const getFilesByUserId = async (req, res, next) => {
const userId = req.params.uid;
let filePosts;
try {
filePosts = await File.find({ creator: userId });
} catch (err) {
return next(new HttpError("Fetching failed, please try again.", 500));
}
const userInfo = User.findById(userId);
if (!filePosts || filePosts.length === 0) {
return next(
new HttpError("Could not find files for the provided user id.", 404)
);
}
console.log(userInfo.toObject({ getters: true }));
res.json({
filePosts: filePosts.map((file) => file.toObject({ getters: true })),
});
};
In your async function, your code continues even though the call to User.findById(userId) isn't complete. You can use an await statement to ensure that the function has run before your code continues.
Your code should work if you change const userInfo = User.findById(userId); to
const userInfo = await User.findById(userId);
For more information, here is the async/await documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
Just needed to add await.
const userInfo = await User.findById(userId);
I'm stuck on some code and I do not understand why. I'm determined not to copy and paste but to understand what Im doing (and doing wrong).
The problem is where I want to make the new Url object in the post route. It makes the the object but does not wait for the function getHighestShort to resolve its promise and always returns shortMax as 1.
If I console log on some code lines, I can see that the code in the if statement in the getHighestShort function is resolved after making and saving the Url object and therefore the Url object will always have short_url as 1.
I would really appreciate some hints in the right direction, because I thought the await before the new Url would make that line of code hold up the rest of the code.
-42 year old self learner- So this post might sound a bit all over the place :)
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser')
const mongoose = require('mongoose')
const Url = require('./schemas/urlModel');
mongoose.connect('mongodb://localhost:27017/urlshortener', { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('connection open'))
.catch((err) => console.log(err))
const app = express();
// Basic Configuration
const port = process.env.PORT || 3000;
app.use(cors());
app.use('/public', express.static(`${process.cwd()}/public`));
app.use(bodyParser.urlencoded({ extended: false }))
app.get('/', function(req, res) {
res.sendFile(process.cwd() + '/views/index.html');
});
// Your first API endpoint
//find input url in db and return the long version
app.get('/api/shorturl/:input', async (req, res) => {
try {
const {input} = req.params
// const shortUrlNr = parseInt(input)
console.log(input)
console.log(Url.findOne( {short_url: `${input}`} ))
const found = await Url.findOne( {short_url: `${input}`} )
res.redirect(`https://${found.full_url}`)
} catch (err) {
console.log(err)
console.log(res.status)
}
})
app.post('/api/shorturl/new', async (req, res, next) => {
try {
const { url } = await req.body
//check for valid url, if not respond with json object as asked in tests.
if (!validURL(url)) {
return res.json({error: 'invalid url'})
} else {
//find url in db and get json response with the corresponding object.
//If it is already in the db, than redirect to this website as asked in tests.
const foundUrl = await Url.findOne( {full_url: `${url}`} )
if (foundUrl !== null) {
res.redirect(`https://${foundUrl.full_url}`)
} else {
//if the url is not there, we''ll create a new entry with that url.
const newUrl = await new Url( {full_url: `${url}`, short_url: `${getHighestShort()}`} )
await newUrl.save()
res.json(newUrl)
}
}
} catch(err) {
console.log('catch error post request')
return next(err)
}
})
app.listen(port, function() {
console.log(`Listening on port ${port}`);
});
function validURL(str) {
var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
'(\\#[-a-z\\d_]*)?$','i'); // fragment locator
return !!pattern.test(str);
}
function getHighestShort() {
let shortMax = 1
//make an array from the collection with short values sorted descending
//and than pick the first one which should be the highest value.
//if the array is empty just return 1 as it is the first one
Url.find({}).sort({short_url: 1}).exec()
.then(res => {
if (res.length !== 0) {
shortMax = res[0].short_url + 1
}
})
.catch(err => {
console.log('catch err server.js function getHighestShort')
console.log(err)
})
return shortMax
}
model
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const urlSchema = new Schema(
{
full_url: {
type: String,
required: false
},
short_url: Number
}
)
const Url = mongoose.model('Url', urlSchema)
module.exports = Url
The await operators waits on a promise, for it to work like you expect you need it to be used on one.
For example in the line:
const { url } = await req.body
await is pointless here because req.body is not a promise, it's an object. so it will be the same as doing:
const { url } = req.body
Now why isn't your code working? Let's start by understanding getHighestShort, does it return a promise? ( spoiler alert it doesn't ).
So first we need to make it return a promise ( and then we will have to wait for that promise to resolve ).
Step 1, make getHighestShort return a promise:
function getHighestShort() {
let shortMax = 1
//make an array from the collection with short values sorted descending
//and than pick the first one which should be the highest value.
//if the array is empty just return 1 as it is the first one
return new Promise((resolve, reject) => {
Url.find({}).sort({short_url: 1}).exec()
.then(res => {
if (res.length !== 0) {
shortMax = res[0].short_url + 1;
}
resolve(shortMax)
})
.catch(err => {
console.log('catch err server.js function getHighestShort')
console.log(err)
// you decide logic here
resolve(shortMax)
})
})
}
Now I personally recommend you re-write this a little to make it a bit cleaner, something like so:
async function getHighestShort() {
let shortMax = 1
try {
//make an array from the collection with short values sorted descending
//and than pick the first one which should be the highest value.
//if the array is empty just return 1 as it is the first one
const results = await Url.find({}).sort({short_url: 1}).limit(1).exec()
if (res.length !== 0) {
shortMax = res[0].short_url + 1
}
return shortMax
} catch (e) {
console.log('catch err server.js function getHighestShort')
console.log(err)
return shortMax
}
}
Notice I added limit(1), there is no need to read the entire collection on each call.
I want to add that you have some wrong assumptions here about the atomicity of the operations, what happens if 2 requests are processed at the same time? you will have 2 urls with the same short_url.
I will leave this problem for you to handle on your own.
Step 2, now that getHighestShort returns a promise. we need to wait on it:
const short_url = await getHighestShort();
const newUrl = await new Url( {full_url: `${url}`, short_url: short_url} )
This is confusing me a lot, because I'm repeating the code structure I have on other parts of my project (and it works there).
This is the part where it doesn't work:
/utils/socketQueries.js
const db = require('../db')
exports.addMessage = async (message) => {
console.log('add message func', message)
try {
/* const res = await db.query('select 1', (err, res) => {
console.log(res)
})*/
const res = await db.query('select 1') // this doesn't work either
return res
} catch (error) { console.error('adding message error', error) }
}
This is called from /socketserver.js
exports.socketServer = (wss) => {
let socket_id = new Map()
wss.on("connection", (ws, req) => {
ws.on('message', (data) => { //messages from the client
parseMessage(ws, socket_id, data)
})
})
...
this is how I set up the db connection in db.js
require('dotenv').config()
const { Pool } = require('pg')
const connectionString = process.env.DBCONNSTRING
const db = new Pool({
connectionString: connectionString,
})
module.exports = db;
and an example of a working query function would be /api/health.js
const db = require('../db');
exports.check = async (req, res) => {
let response = {
apiResponsive: true
}
try {
dbResponsive = await db.query(`select 1`)
if (dbResponsive.rows[0]) response = {dbResponsive: true, ...response}
else response = {dbResponsive: true, ...response} }
catch(error) {
console.error('error on db health check', error)
response = {dbResponsive: 'error while checking', ...response}
}
res.json(response)
}
I've been trying different ways to run the query: .then, callback, async/await and it always returns either the unresolved promise or undefined - it never throws an error.
Can anyone sort out whats the (likely very basic) error I'm missing?
EDIT: Trying out the answers proposed I noticed that if I try an INSERT query, nothing is inserted on the DB. regardless of what I do with what db.query returns, the query should run.
I write a test with jest to test one of my middleware.
const asyncAll = (req, res, next) => {
const queue = [
service.exchangeLongTimeToken(req),
service.retrieveUserInfo(req),
];
Promise.all(queue).then((values) => {
res.locals.auth = values[0];
res.locals.user = values[1];
next();
}).catch((err) => {
next(err)
});
};
The test file is like this:
const httpMocks = require('node-mocks-http');
const testData = require('../../testdata/data.json');
describe('Test asyncAll', () => {
let spy1 = {};
let spy2 = {};
const mockNext = jest.fn();
afterEach(() => {
mockNext.mockReset();
spy1.mockRestore();
spy2.mockRestore();
});
test('Should call next() with no error when no error with 2 requests', () => {
spy1 = jest.spyOn(service, 'exchangeLongTimeToken').mockImplementation((url) => {
return Promise.resolve(testData.fbLongTimeToken);
});
spy2 = jest.spyOn(service, 'retrieveUserInfo').mockImplementation((url) => {
return Promise.resolve(testData.fbUserInfo);
});
const request = httpMocks.createRequest();
const response = httpMocks.createResponse();
asyncAll(request, response, mockNext);
expect(spy1).toBeCalled();
expect(spy2).toBeCalled();
expect(mockNext).toBeCalled();
expect(mockNext).toBeCalledWith();
expect(mockNext.mock.calls.length).toBe(1);
});
}
The error is like this:
Error: expect(jest.fn()).toBeCalled()
Expected mock function to have been called.
at Object.<anonymous> (tests/backend/unit/fblogin/asyncAll.test.js:39:26)
Which reflects the line:
expect(mockNext).toBeCalled();
Why it doesn't get called?
I read the documents about jest, it says I need to return the promise in order to test the value. But the asyncAll() doesn't return a promise, instead, it consumes a promise, how to deal with this?
You have to notify Jest about the promises you create in the test, have a look at the docs on this topic:
test('Should call next() with no error when no error with 2 requests', async() => {
const p1 = Promise.resolve(testData.fbLongTimeToken);
const p2 = Promise.resolve(testData.fbUserInfo);
spy1 = jest.spyOn(service, 'exchangeLongTimeToken').mockImplementation((url) => {
return p1
});
spy2 = jest.spyOn(service, 'retrieveUserInfo').mockImplementation((url) => {
return p2
});
const request = httpMocks.createRequest();
const response = httpMocks.createResponse();
asyncAll(request, response, mockNext);
await Promise.all([p1,p2])
expect(spy1).toBeCalled();
expect(spy2).toBeCalled();
expect(mockNext).toBeCalled();
expect(mockNext).toBeCalledWith();
expect(mockNext.mock.calls.length).toBe(1);
});
I build express app, there is a route A use many middlewares:
// fblogin.js
const saveUser = require('./middlewares').saveUser;
const issueJWT = require('./middlewares').issueJWT;
const exchangeLongTimeToken = (a) => { //return promise to call API };
const retrieveUserInfo = (b) => { //return promise to call API };
const service = {
exchangeLongTimeToken,
retrieveUserInfo,
};
const asyncAll = (req, res) => {
// use Promise.all() to get service.exchangeLongTimeToken
// and service.retrieveUserInfo
};
router.post('/', [asyncAll, saveUser, issueJWT], (req, res) => {
//some logic;
});
module.exports = { router, service };
And this is my middlewares.js:
const saveUser = (req, res, next) => { //save user };
const issueJWT = (req, res, next) => { //issue jwt };
module.exports = { saveUser, issueJWT };
It works well. But got problem when I tried to write the test.
This is my test, I use mocha, chai, supertest and sinon:
const sinon = require('sinon');
const middlewares = require('../../../../src/routes/api/auth/shared/middlewares');
const testData = require('../../testdata/data.json');
let app = require('../../../../src/config/expressapp').setupApp();
const request = require('supertest');
let service = require('../../../../src/routes/api/auth/facebook/fblogin').service;
describe('I want to test', () => {
context('Let me test', function () {
const testReq = {name: 'verySad'};
beforeEach(() => {
sinon.stub(middlewares, 'saveUser').callsFake((req, res, next)=>{
console.log(req);
});
sinon.stub(service, 'exchangeLongTimeToken').callsFake((url) => {
return Promise.resolve(testData.fbLongTimeToken);
});
sinon.stub(service, 'retrieveUserInfo').callsFake((url) => {
return Promise.resolve(testData.fbUserInfo);
});
});
it('Should return 400 when bad signedRequest', () => {
return request(app).post(facebookAPI).send(testReq).then((response) => {
response.status.should.be.equal(400);
});
});
});
What is the problem
You could see that there are 3 stubs, 1 for middlewares.saveUser and 2 for services.XXXX which is in the same file of the route.
The problem is that, the 2 stubs works while the 1 for middlewares.saveUser not work, always trigger the original one.
I think it maybe that when I call the setupApp(), the express will load all the routers it needs, so mock it afterwards won't have a effect, but it
is strange that route.service could be mocked...
How to get the stub work?
The only way to get it work, is to put the stub at the top of test file, just after that middleware require.
I tried:
1. Use 3rd party modules like proxyquire, rewire
2. Use node's own delete require.cache[middlewares] and 'app' and re-require them.
3. Many other tricks.
4. Use jest's mock, but still only works if I put it at the top of the file.
What is the way of solving this problem without putting the stub at the top of the test file? Thanks!
The solution in the question is a bit restricted, since the mock has polluted the whole test suites.
I end up by doing this, The logic is simple, we still need to mock the saveUser first, but then we require all the other variables into the test function rather than require them at the top of the file, more flexible this time. And I add a checkIfTheStubWorks method to check if the stub works, to make sure the whole test works.
const middlewares = require('../../../../src/routes/api/auth/shared/middlewares');
const removeEmptyProperty = require('../../../../src/utils/utils').removeEmptyProperty;
let app;
let service;
let request;
/*
* The reason we need this is:
* If the mock not works,
* the whole test is meaningless
*/
const checkIfTheStubWorks = () => {
expect(spy1).toHaveBeenCalled();
expect(spy2).toHaveBeenCalled();
expect(spy3).toHaveBeenCalled();
};
const loadAllModules = () => {
service = require('../../../../src/routes/api/auth/facebook/fblogin').service;
app = require('../../../../src/config/expressapp').setupApp();
request = require('supertest')(app);
};
describe('Mock response from facebook', () => {
let spy1 = {};
let spy2 = {};
let spy3 = {};
const testReq = testData.fbShortToken;
beforeAll(() => {
spy1 = jest.spyOn(middlewares, 'saveUser').mockImplementation((req, res, next) => {
userToSaveOrUpdate = removeEmptyProperty(res.locals.user);
next();
});
// It must be load here, in this order,
// otherwise, the above mock won't work!
loadAllModules();
spy2 = jest.spyOn(service, 'exchangeLongTimeToken').mockImplementation((url) => {
// mock it
});
spy3 = jest.spyOn(service, 'retrieveUserInfo').mockImplementation((url) => {
// mock it
});
});
afterAll(() => {
spy1.mockRestore();
spy2.mockRestore();
spy3.mockRestore();
});
test('Return a JWT should have same length as facebook one', async () => {
const response = await request.post(facebookAPI).send(testReq);
// assert this, assert that
checkIfTheStubWorks();
});
});