node: testing protected apis with supertest

2021-12-22

 | 

~3 min read

 | 

547 words

I previously wrote about testing node APIs with Supertest, but what about a protected route? One which requires a valid JWT associated with a user in the database?

That’s what I’ll walk through solving today.

First, let’s set the stage with a quick preview of the server:

app.ts
import express from "express"
import { protect } from "./middleware/protect"
import { authRouter } from "./auth"
import { todoRouter } from "./todos"

export const app = express()

/** Routes */
app.use("/auth", authRouter)

/** Protected routes */
app.use(protect)
app.use("/todo", todoRouter)

In this server, /auth isn’t protected, but /todo is. protect is a piece of middleware that verifies the request has valid JWT and is associated with a user.

How might you test this? Let’s say we want to test fetching a user’s todos. Since the route is protected, we’ll need:

  1. to make sure we have a user and
  2. have a valid JWT for that user, so that
  3. we can submit the JWT on the authentication header of each request (which is where the middleware is expecting to find it)

We can accomplish this using supertest thanks to the set method.

Let’s build this out.

todo.test.ts
import request from 'supertest'
import { app } from "../app"

describe('/todos', () => {
    test('GET /', async () => {
    await request(app)
      .get('/api/todo')
      .expect(200)
      .then((response: any) => {
        expect(response.body).toEqual([])
      })
  })
}

This is our test before we added the middleware. Now that the middleware’s in place, this test fails as the request is returning a 401!

Now, let’s add a new user before test (before each just to make sure each test is as isolated of a unit as possible).

todo.test.ts
import request from 'supertest'
import { app } from "../app"
import { TodoModel } from "./todos.schema"
import { UserModel } from "../users/users.schema"

+ describe('/todos', () => {
+   let jwt: string;
+
+   beforeEach(async () => {
+     await UserModel.deleteMany()
+     jwt = await (await request(app).post("/api/auth/signup").send(userInput))
+       .body.data
+   })

  test('GET /', async () => {
    await request(app)
      .get('/api/todo')
      .expect(200)
      .then((response: any) => {
        expect(response.body).toEqual([])
      })
  })
}

Here we have our cleanup of any users. We’ve also created a jwt string that’s accessible in all of our tests, which is assigned to the response of a call to our /api/auth/signup.

I debated whether this was appropriate and ultimately decided that it was. The alternative is to directly create a record in the database and then manually generating the JWT - replicating logic that’s handled by the /signup service.

You may have noticed that the test hasn’t changed yet, so it will still be failing. To get it to pass, we can take the final step and set the header:

todo.test.ts
import request from 'supertest'
import { app } from "../app"
import { TodoModel } from "./todos.schema"
import { UserModel } from "../users/users.schema"

describe('/todos', () => {
  let jwt: string;
  beforeEach(async () => {
    await UserModel.deleteMany()
    jwt = await (await request(app).post("/api/auth/signup").send(userInput))
      .body.data
  })

  test('GET /', async () => {
    await request(app)
      .get('/api/todo')
      .expect(200)
+       .set({ authorization: `Bearer ${jwt}` })
      .then((response: any) => {
        expect(response.body).toEqual([])
      })
  })
}

With that done, our test now passes as the JWT is valid and will get pulled off in the middleware en route to testing the route’s controllers and services!



Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!