Skip to content

Commit

Permalink
test: FORMS-914 rate limiter unit tests (bcgov#1300)
Browse files Browse the repository at this point in the history
* test: FORMS-914 rate limiter unit tests

* Undid unnecessary change

* switched rate limit strings to template literals

* changed if clause formatting for consistency

* tests missing async keyword
  • Loading branch information
WalterMoar authored Feb 29, 2024
1 parent 168b3a4 commit 9d345a2
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 2 deletions.
4 changes: 2 additions & 2 deletions app/src/forms/common/middleware/rateLimiter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ const apiKeyRateLimiter = rateLimit({

limit: config.get('server.rateLimit.public.max'),

// Skip Bearer token auth so that CHEFS app users are not limited.
skip: (req) => req.headers && req.headers.authorization && !req.headers.authorization.startsWith('Basic '),
// Skip everything except Basic auth so that CHEFS app users are not limited.
skip: (req) => !req.headers?.authorization || !req.headers.authorization.startsWith('Basic '),

// Use the latest draft of the IETF standard for rate limiting headers.
standardHeaders: 'draft-7',
Expand Down
144 changes: 144 additions & 0 deletions app/tests/unit/forms/common/middleware/rateLimiter.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
const { getMockReq, getMockRes } = require('@jest-mock/express');
const uuid = require('uuid');

const { apiKeyRateLimiter } = require('../../../../../src/forms/common/middleware');

const rateLimit = 7;
const rateWindowSeconds = 11;

jest.mock('config', () => {
return {
get: jest.fn((key) => {
if (key === 'server.rateLimit.public.max') {
return rateLimit;
}

if (key === 'server.rateLimit.public.windowMs') {
return rateWindowSeconds * 1000;
}
}),
};
});

// Headers for Draft 7 of the standard.
const rateLimitName = 'RateLimit';
const rateLimitValue = `limit=${rateLimit}, remaining=${rateLimit - 1}, reset=${rateWindowSeconds}`;
const rateLimitPolicyName = 'RateLimit-Policy';
const rateLimitPolicyValue = `${rateLimit};w=${rateWindowSeconds}`;

const ipAddress = '1.2.3.4';

const formId = uuid.v4();
const secret = uuid.v4();
const basicToken = Buffer.from(`${formId}:${secret}`).toString('base64');

const bearerToken = Math.random().toString(36).substring(2);

beforeEach(() => {
// Reset the rate limiting to be able to call the rate limiter multiple times
apiKeyRateLimiter.resetKey(ipAddress);
});

describe('apiKeyRateLimiter', () => {
it('rate limits basic auth', async () => {
const req = getMockReq({
headers: {
authorization: 'Basic ' + basicToken,
},
ip: ipAddress,
});
req.app.get = jest.fn().mockReturnValue();
const { res, next } = getMockRes();

await apiKeyRateLimiter(req, res, next);

expect(res.setHeader).toHaveBeenCalledTimes(2);
// These also test that the rate limiter uses our custom config values.
expect(res.setHeader).toHaveBeenNthCalledWith(1, rateLimitPolicyName, rateLimitPolicyValue);
expect(res.setHeader).toHaveBeenNthCalledWith(2, rateLimitName, rateLimitValue);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
});

describe('skips rate limiting for', () => {
test('no headers', async () => {
const req = getMockReq({
ip: ipAddress,
});
req.app.get = jest.fn().mockReturnValue();
const { res, next } = getMockRes();

await apiKeyRateLimiter(req, res, next);

expect(res.setHeader).toHaveBeenCalledTimes(0);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
});

test('no authorization header', async () => {
const req = getMockReq({
headers: {},
ip: ipAddress,
});
req.app.get = jest.fn().mockReturnValue();
const { res, next } = getMockRes();

await apiKeyRateLimiter(req, res, next);

expect(res.setHeader).toHaveBeenCalledTimes(0);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
});

test('empty authorization header', async () => {
const req = getMockReq({
headers: {
authorization: '',
},
ip: ipAddress,
});
req.app.get = jest.fn().mockReturnValue();
const { res, next } = getMockRes();

await apiKeyRateLimiter(req, res, next);

expect(res.setHeader).toHaveBeenCalledTimes(0);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
});

test('unexpected authorization type', async () => {
const req = getMockReq({
headers: {
authorization: Math.random().toString(36).substring(2),
},
ip: ipAddress,
});
req.app.get = jest.fn().mockReturnValue();
const { res, next } = getMockRes();

await apiKeyRateLimiter(req, res, next);

expect(res.setHeader).toHaveBeenCalledTimes(0);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
});

test('bearer auth', async () => {
const req = getMockReq({
headers: {
authorization: 'Bearer ' + bearerToken,
},
ip: ipAddress,
});
req.app.get = jest.fn().mockReturnValue();
const { res, next } = getMockRes();

await apiKeyRateLimiter(req, res, next);

expect(res.setHeader).toHaveBeenCalledTimes(0);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
});
});
});

0 comments on commit 9d345a2

Please sign in to comment.