Testing secure endpoints with integration testing

Testing APIs is a non-functional requirement for successful APIs, it is part of the definition of done. Securing APIs is also a non-functional requirement.

Creating an integration test on a secure endpoint that uses session-based secure cookies can be a challenge! This post will step through the approach we took to create these integration tests even while secured by session-based cookies.

Integration Tests are test that verify a collection of components properly work together to perform a process. A common integration test is from the API Endpoint to the Service Layer.

Here is our server setup:

server.js

const express = require('express')
const session = require('express-session')
const app = express()
const auth = require('./middleware/auth')

app.use(session({
  secret: 'jack russell',
  resave: false,
  saveUinitialized: true,
  cookie: { secure: true }
}))

app.use(auth.check)

app.post('/login', (req, res) => {
  req.session.user = { name: req.query.name }
  res.status(201).json({ ok: true })
})

app.get('/movies', (req, res) => {
  res.status(200).json(['Ghostbusters', 'Grounhog Day', 'What about Bob?', 'Stripes', 'Caddyshack'])
})

app.get('/logout', (req, res) => {
  res.session.user = null
  res.json({ ok: true })
})

if (!module.parent) {
  app.listen(3000)
}

module.exports = app

middleware/auth.js

exports.check = function (req, res, next) {
  if (req.path !== '/login' && req.session.user) {
    next()
  } else {
    res.status(401).json({ message: 'not authorized' })
  }
}

So we have a very simple API that returns a list of movies, but you have to be logged in to the API to get the movies’ list.

Let’s write a test!

test/movies.js

const test = require('tape')
const testServer = require('@twilson63/test-server')
const fetch = require('node-fetch')

const app = require('../server')

test('List Movies', async (t) => {
  t.plan(1)
  const server = testServer(app)
  const result = await fetch(server.url + '/movies').then(r => r.json())
  t.deepEqual(result, ['Ghostbusters', 'Grounhog Day', 'What about Bob?', 'Stripes', 'Caddyshack'])

  server.close()
})

Great! Our test is simple, but it should serve our purpose. Let’s give it a run:

node test/movies_test.js

not ok 1 should be strictly equal
  ---
    operator: equal
    expected: |-
      ['Ghostbusters', 'Grounhog Day', 'What about Bob?', 'Stripes', 'Caddyshack']
    actual: |-
      { message: 'not authorized' }

Oops, what is the problem?

So we can’t test our endpoint, because it is secured, so how do we work around this issue? We can use sinon 's stub feature.

const test = require('tape')
const testServer = require('@twilson63/test-server')
const fetch = require('node-fetch')
const sinon = require('sinon')

const auth = require('../middleware/auth')
sinon.stub(auth, 'check').callsFake(function (req, res, next) {
  req.user = 'bob'
  next()
})

const app = require('../server')

test('List Movies', async (t) => {
  t.plan(1)
  const server = testServer(app)
  const result = await fetch(server.url + '/movies').then(r => r.json())
  t.deepEqual(result, ['Ghostbusters', 'Grounhog Day', 'What about Bob?', 'Stripes', 'Caddyshack'])

  server.close()
})

The result

TAP version 13
# List Movies
ok 1 should be deeply equivalent

1..1
# tests 1
# pass 1

# ok

What is sinon.js?

Sinon is a standalone mocking library that enables you to spy, stub, and mock code for your application, you can check it out at https://sinonjs.org/ — I would recommend not to overuse Sinon, but it can come in handy for issues like this one.

Summary

Testing around security can be tricky mocking tools like sinon come in handy to create tests that focus on testing your code.

The full example is available here: https://github.com/hyper63/testing-secure-endpoints

21