Testing Express.js authorization middleware with mocking requests
Express.js, which is the most popular framework for Node.js, has a rich ecosystem for middleware functions.
1. Middleware functions
Middleware are functions, which are called before a route handler function is invoked. For example, if we have a private endpoint that provides access to content only available to subscribed clients, we’ll want to check if the user is authorized and authenticated when they type the URL in the browser.
For cases like this, we can create middleware functions, and all we have to do is just to call them before the route handler:
const express = require('express')
const app = express()
// ... code
app.get('/private', authorize, (req, res, next) => {
// ... this will execute when the /private endpoint is hit
})
Lots of pre-build middleware libraries are available for Express, and we can also write our own ones. More information on middleware can be found on the Express.js website here and here. I also wrote a post on Express middleware, so I won’t deep dive on the topic here again.
2. Setup and code
The app I’ll use for this demonstration is the same as in this post. The full code can be downloaded from the GitHub repository of the project.
2.1. server.js
The relevant part of the code belongs to the /private
endpoint. As it was stated above, private endpoints should not be accessed by anyone, and some sort of check is necessary to ensure that the user has the rights to get the content on the page.
The relevant code snippet has the following simple logic:
// server.js
app.get('/private', authorize, (req, res, next) => {
res.send({ message: 'The private endpoint is only allowed for authenticated users' })
})
There’s nothing fancy here. We simply send an object with the message
property in the response.
2.2. authorize middleware
The authorize
middleware function performs the check to see if the user is allowed to access the content.
The function lives in its own module, and it looks like this:
// authorization.js
const AuthorizationError = require('./errors')
const authorize = (req, res, next) => {
if (!(req.headers && req.headers.authorization)) {
throw new AuthorizationError('Not authorized')
}
next()
}
module.exports = {
authorize
}
It checks if the request header has the authorization
property. If so, it calls the next
function, which passes the control to the next function in the flow. In our case, this will be the route handler described in 2.1., which adds the object with the message
property to the response.
If, however, the request doesn’t contain the authorization
header, it means that the user is not allowed to access the private content, i.e. they are not logged in or they don’t have the necessary permissions. In this case, authorize
will throw an error, and the execution will continue with the global error handler (in server.js
).
2.3. AuthorizationError
authorize
throws a custom error called AuthorizationError
. Having a custom error is not compulsory at all and we could throw a regular Error
instead. In fact, AuthorizationError
itself inherits from the Error
object:
// errors.js
class AuthorizationError extends Error {
constructor(message) {
super(message)
this.name = 'AuthorizationError'
this.httpStatusCode = 401
}
}
module.exports = AuthorizationError
The advantage of having a custom AuthorizationError
is that we can customize the error message, and decide what properties we want to add to the error object. In this case, it has a hardcoded httpStatusCode
property and the name
of the error.
3. mock-express-request
Now, we want to test the authorize
middleware function.
authorize
has two possible outcomes: either throws an error (if no authorization
header is present), or calls next
. Therefore, we will create two test cases, one for each scenario.
So we have to watch the req
object (the client request) for the presence of the authorization
header. Because the goal of the test is to see how the authorize
function works, it’s a good idea to isolate it from the rest of the logic.
This means that we can mock the request object, and focus on the authorize
middleware.
There is a great tool that mocks http requests called mock-express-request.
The library is very easy to use. All we have to do is to instantiate the MockExpressRequest
class, and then add the required request properties: method, URL, headers, cookies etc.
The instance of the MockExpressRequest
will replace the req
(Request
) object in the authorize
middleware function.
Let’s see how!
4. Test cases
In order for the tests to work, we’ll need Mocha and Chai testing tools. Please refer to this post if you are unsure how to set them up in the project folder.
Now that the libraries are set up, we can have a look at the test cases themselves:
// authorization.spec.js
const { expect } = require('chai')
const MockExpressRequest = require('mock-express-request')
const sinon = require('sinon')
const { authorize } = require('../authorization')
const AuthorizationError = require('../errors')
describe('#authorize', () => {
it('when authorization header is not present, it throws an error', () => {
const request = new MockExpressRequest()
expect(() => authorize(request)).to.throw(AuthorizationError)
})
it('does not throw error if the authorization header is set up', () => {
const request = new MockExpressRequest({
headers: {
Authorization: '123qwe'
}
})
const resSpy = sinon.spy()
const nextSpy = sinon.spy()
expect(() => authorize(request, resSpy, nextSpy)).to.not.throw()
expect(nextSpy.called).to.be.true
})
})
4.1. The sad path
In the first test case, we don’t provide the authorization
header, and we expect
that an AuthorizationError
will be thrown.
request
is the instance of the MockExpressRequest
class, and this time we don’t need any arguments.
4.2. The happy path
The second test case is meant to examine the scenario when the header contains the authorization
property. The argument of the MockExpressRequest
instance is an object which has the headers
property.
We can make use of the great spy, stub and mock library, Sinon, and create a spy on the response
object and the next
function.
In this case, we don’t expect any errors, but we expect that the next
function (which is replaced by the nextSpy
stub) to be called.
5. Conclusion
Authorization middleware functions are small but important parts of any applications that have endpoints with restricted access.
Testing these functions should be part of the test coverage of the application.
mock-express-request
is one library that can be used to create mock requests, and it’s a useful part of our testing toolkit.
Thanks for reading and see you next time.