简体   繁体   中英

Mocking express-rate-limit for unit testing

I have implemented 'express-rate-limit' into my application as a middleware for specific endpoints. When it comes to unit testing I am finding trouble when attempting to stub this middleware to appropriately control the responses.

For example, I have a route that allows 3 requests every 15 minutes, however, I have 10 unit tests for this route. The first 3 tests pass as expected and the following 7 return a '429 Too Many Requests' response.

The 'express-rate-limit' library seems to be highly suggested for rate limiting, however, I cannot find any information regarding how it would be utilized in a testing environment.

The following shows the simplified implementation attempt.

ratelimit.js

exports.createUser = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minute
    max: 3,
    message: 'Too many requests',
    statusCode: 429
})

route.js

const rateLimits = require('./ratelimit')

let incorrect = function (res, msg) {
    res.status(401)
    res.send({
        status: 'err',
        payload: {
            msg: msg
        }
    })
}

router.post('/create', rateLimits.createUser, (req, res) => {
    if (!req.body.email) return incorrect(res, 'Email not provided')
    userController
        .createUser(req.body.email)
        .then(user => {
            if (!user) incorrect(res, 'User not created')
            else correct(res)
        })
        .catch(() => incorrect(res, 'Internal Error'))
})

test.js

This should stub the rate limit to only allow a single request to be made, meaning the second test should fail. However the second test will pass as this is never called (rate limit is still 3 as per the 'real' implementation)

const rateLimits = require('./ratelimit')
const userController = require('./usercontroller')
let server
let limiter

describe('Users', () => {
  before(() => {
    limiter = sinon.stub(rateLimits, 'createUser').callsFake(() => rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minute
      max: 1,
      message: 'Too many requests',
      statusCode: 429
  }))
    server = require('../app')
  })
  

  it('should return an error for invalid email', done => {
    chai.request(server)
    .post('/users/create')
    .send({
      email: 'notanemail'
    })
    .end((err, res) => {
      res.should.have.status(401)
      res.body.status.should.equal('err')
      done()
    })
  })

  it('should return an error for missing parameter', done => {
    chai.request(server)
    .post('/users/create')
    .send({})
    .end((err, res) => {
      res.should.have.status(401)
      res.body.status.should.equal('err')
      done()
    })
  })

})

This question Stubbing Out Middleware highlighted that the App itself should be initialized after the stubbing has been executed to ensure that is loaded correctly. This does not appear to work, nor does the standard.implementation where the import is created at the top of the file.

Alternatively, for testing purposes, I have attempted to log some text to the console on execution to ensure that the stub function is called. The function appears to never be called as the log is never printed to console.

  limiter = sinon.stub(rateLimits, 'createUser').callsFake(() => console.log("I was executed"))

Another failed alternative I have attempted is to call the express-rate-limit module directly with

const rateLimit = require('express-rate-limit')

limiter = sinon.stub(rateLimit.prototype, 'constructor').callsFake(() => rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minute
      max: 1,
      message: 'Too many requests',
      statusCode: 429
  }))

This question is 4 months old and hasn't received a response so I'll do my best to explain it now that I've worked through and got it working.

So say you are using express-rate-limit like so:

middleware.js

const rateLimit = require('express-rate-limit')

const limiter = rateLimit({
  windowMs: 60 * 60 * 1000 * 24, // 24 hour window
  max: 5, // start blocking after 5 requests
  message:
    'Too many requests sent from this IP, please try again after an hour'
})

module.exports = {
   limiter
}

app.js

const middleware = require('./middleware')

router.post('/my-rate-limited-api', middleware.limiter, async (req, res) => {
  try {
    const info = await doSomething('my args', req.body)
    res.json(info)
  } catch (err) {
    res.status(err.status || 400).send(`error: ${err.message}`)
  }
})

To be able to test the things inside of this route, you'll need to stub limiter.

In your test file, make sure to include an IP in the req. And in your response, stub

req = {
      method: 'POST',
      ip: '12.12.123.123',
      url: '/my-rate-limited-api',
      body: [{ id: '123' }]
    }

and

res.headers = {}

and

res.setHeader = (x, y) => { res.headers[x] = y }

beforeEach(function () {
    const middleware = require('./middleware')
    this.sinon.stub(middleware, 'limiter').callsFake(function (req, res, next) {
      return next()
    })
    res = new Promise(function (resolve, reject) {
      doneResolve = this.resolve = resolve
      doneReject = this.reject = reject
    })
    res.headers = {}
    res.setHeader = (x, y) => { res.headers[x] = y }

    req = {
      method: 'POST',
      ip: '12.12.123.123',
      url: '/my-rate-limited-api',
      body: [{ id: '123' }]
    }

    next = this.sinon.spy(function createDeferredNext () {
      let resolve
      const promise = new Promise(function () {
        resolve = arguments[0]
      })
      const fn = (arg) => resolve(arg)
      fn.then = promise.then.bind(promise)
      return fn
    })
  })

From there, you'll be able to test your function however you like.


  it('can test without rate limits', function () {
    router(req, res, next)
    return res
      .then(ret => {
        expect(res.statusCode).to.eql(200)
      })
  })

The best approach is to check if your application is running on a test environment and apply some conditional check logic for the max param. say yours is 5 requests per 15 minutes window. You should set the max key to a higher value than or equal to the number of requests in the test environment. In this case, I have used a larger number for the test environmentnt which is 100.

export const apiLimiter = rateLimit({
 windowMs: 15 * 60 * 1000, // 15 minutes
 max: process.env.NODE_ENV === "test" ? 100 : 3, // 3 requests per window
 onLimitReached: function (req, res /*, next*/) {
   throw new TooManyRequestsError();
 },
 handler: function (req, res /*, next*/) {
   throw new TooManyRequestsError();
 },

});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM