From 28c748ea02be2a93e635d993fbbda51f297caf4e Mon Sep 17 00:00:00 2001 From: "dan.castillo" Date: Tue, 20 Aug 2024 21:34:18 -0400 Subject: [PATCH] chore: migrate from tap to node:test and c8 --- .taprc | 3 - package.json | 4 +- test/github-issues/issue-207.test.js | 118 +- test/github-issues/issue-215.test.js | 66 +- test/github-issues/issue-284.test.js | 66 +- test/global-rate-limit.test.js | 1441 +++++++-------- test/local-store-close.test.js | 7 +- test/not-found-handler-rate-limited.test.js | 133 +- test/redis-rate-limit.test.js | 613 +++++++ test/route-rate-limit.test.js | 1824 ++++++++++--------- 10 files changed, 2439 insertions(+), 1836 deletions(-) delete mode 100644 .taprc create mode 100644 test/redis-rate-limit.test.js diff --git a/.taprc b/.taprc deleted file mode 100644 index f22a32eb..00000000 --- a/.taprc +++ /dev/null @@ -1,3 +0,0 @@ -jobs: 1 -files: - - test/**/*.test.js diff --git a/package.json b/package.json index 297b8509..a3be59c2 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint:fix": "standard --fix", "redis": "docker run -p 6379:6379 --name rate-limit-redis -d --rm redis", "test": "npm run test:unit && npm run test:typescript", - "test:unit": "tap", + "test:unit": "c8 --100 node --test", "test:typescript": "tsd" }, "repository": { @@ -32,12 +32,12 @@ "@fastify/pre-commit": "^2.1.0", "@sinonjs/fake-timers": "^11.2.2", "@types/node": "^22.0.0", + "c8": "^10.1.2", "fastify": "^5.0.0-alpha.3", "ioredis": "^5.4.1", "knex": "^3.1.0", "sqlite3": "^5.1.7", "standard": "^17.1.0", - "tap": "20.0.3", "tsd": "^0.31.1" }, "dependencies": { diff --git a/test/github-issues/issue-207.test.js b/test/github-issues/issue-207.test.js index c7d0fdd8..0ca42c92 100644 --- a/test/github-issues/issue-207.test.js +++ b/test/github-issues/issue-207.test.js @@ -1,37 +1,33 @@ 'use strict' -const FakeTimers = require('@sinonjs/fake-timers') -const t = require('tap') -const test = t.test +const { test, mock } = require('node:test') const Fastify = require('fastify') const rateLimit = require('../../index') -t.beforeEach(t => { - t.context.clock = FakeTimers.install() -}) - -t.afterEach(t => { - t.context.clock.uninstall() -}) - -test('issue #207 - when continueExceeding is true and the store is local then it should reset the rate-limit', async t => { +test('issue #207 - when continueExceeding is true and the store is local then it should reset the rate-limit', async (t) => { + const clock = mock.timers + clock.enable() const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: { - rateLimit: { - max: 1, - timeWindow: 5000, - continueExceeding: true + fastify.get( + '/', + { + config: { + rateLimit: { + max: 1, + timeWindow: 5000, + continueExceeding: true + } } + }, + async () => { + return 'hello!' } - }, async () => { - return 'hello!' - }) + ) const firstOkResponse = await fastify.inject({ url: '/', @@ -42,7 +38,7 @@ test('issue #207 - when continueExceeding is true and the store is local then it method: 'GET' }) - t.context.clock.tick(3000) + clock.tick(3000) const secondRateLimitWithResettingTheRateLimitTimer = await fastify.inject({ url: '/', @@ -50,7 +46,7 @@ test('issue #207 - when continueExceeding is true and the store is local then it }) // after this the total time passed is 6s which WITHOUT `continueExceeding` the next request should be OK - t.context.clock.tick(3000) + clock.tick(3000) const thirdRateLimitWithResettingTheRateLimitTimer = await fastify.inject({ url: '/', @@ -58,29 +54,67 @@ test('issue #207 - when continueExceeding is true and the store is local then it }) // After this the rate limiter should allow for new requests - t.context.clock.tick(5000) + clock.tick(5000) const okResponseAfterRateLimitCompleted = await fastify.inject({ url: '/', method: 'GET' }) - t.equal(firstOkResponse.statusCode, 200) - - t.equal(firstRateLimitResponse.statusCode, 429) - t.equal(firstRateLimitResponse.headers['x-ratelimit-limit'], '1') - t.equal(firstRateLimitResponse.headers['x-ratelimit-remaining'], '0') - t.equal(firstRateLimitResponse.headers['x-ratelimit-reset'], '5') - - t.equal(secondRateLimitWithResettingTheRateLimitTimer.statusCode, 429) - t.equal(secondRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-limit'], '1') - t.equal(secondRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-remaining'], '0') - t.equal(secondRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-reset'], '5') - - t.equal(thirdRateLimitWithResettingTheRateLimitTimer.statusCode, 429) - t.equal(thirdRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-limit'], '1') - t.equal(thirdRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-remaining'], '0') - t.equal(thirdRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-reset'], '5') - - t.equal(okResponseAfterRateLimitCompleted.statusCode, 200) + t.assert.deepStrictEqual(firstOkResponse.statusCode, 200) + + t.assert.deepStrictEqual(firstRateLimitResponse.statusCode, 429) + t.assert.deepStrictEqual( + firstRateLimitResponse.headers['x-ratelimit-limit'], + '1' + ) + t.assert.deepStrictEqual( + firstRateLimitResponse.headers['x-ratelimit-remaining'], + '0' + ) + t.assert.deepStrictEqual( + firstRateLimitResponse.headers['x-ratelimit-reset'], + '5' + ) + + t.assert.deepStrictEqual( + secondRateLimitWithResettingTheRateLimitTimer.statusCode, + 429 + ) + t.assert.deepStrictEqual( + secondRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-limit'], + '1' + ) + t.assert.deepStrictEqual( + secondRateLimitWithResettingTheRateLimitTimer.headers[ + 'x-ratelimit-remaining' + ], + '0' + ) + t.assert.deepStrictEqual( + secondRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-reset'], + '5' + ) + + t.assert.deepStrictEqual( + thirdRateLimitWithResettingTheRateLimitTimer.statusCode, + 429 + ) + t.assert.deepStrictEqual( + thirdRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-limit'], + '1' + ) + t.assert.deepStrictEqual( + thirdRateLimitWithResettingTheRateLimitTimer.headers[ + 'x-ratelimit-remaining' + ], + '0' + ) + t.assert.deepStrictEqual( + thirdRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-reset'], + '5' + ) + + t.assert.deepStrictEqual(okResponseAfterRateLimitCompleted.statusCode, 200) + clock.reset(0) }) diff --git a/test/github-issues/issue-215.test.js b/test/github-issues/issue-215.test.js index eba1c53d..b14ef84e 100644 --- a/test/github-issues/issue-215.test.js +++ b/test/github-issues/issue-215.test.js @@ -1,36 +1,32 @@ 'use strict' -const FakeTimers = require('@sinonjs/fake-timers') -const t = require('tap') -const test = t.test +const { test, mock } = require('node:test') const Fastify = require('fastify') const rateLimit = require('../../index') -t.beforeEach(t => { - t.context.clock = FakeTimers.install() -}) - -t.afterEach(t => { - t.context.clock.uninstall() -}) - -test('issue #215 - when using local store, 2nd user should not be rate limited when the time window is passed for the 1st user', async t => { +test('issue #215 - when using local store, 2nd user should not be rate limited when the time window is passed for the 1st user', async (t) => { t.plan(5) + const clock = mock.timers + clock.enable() const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: { - rateLimit: { - max: 1, - timeWindow: 5000, - continueExceeding: false + fastify.get( + '/', + { + config: { + rateLimit: { + max: 1, + timeWindow: 5000, + continueExceeding: false + } } - } - }, async () => 'hello!') + }, + async () => 'hello!' + ) const user1FirstRequest = await fastify.inject({ url: '/', @@ -39,7 +35,7 @@ test('issue #215 - when using local store, 2nd user should not be rate limited w }) // Waiting for the time to pass to make the 2nd user start in a different start point - t.context.clock.tick(3000) + clock.tick(3000) const user2FirstRequest = await fastify.inject({ url: '/', @@ -54,7 +50,7 @@ test('issue #215 - when using local store, 2nd user should not be rate limited w }) // After this the total time passed for the 1st user is 6s and for the 2nd user only 3s - t.context.clock.tick(3000) + clock.tick(3000) const user2ThirdRequestAndShouldStillBeRateLimited = await fastify.inject({ url: '/', @@ -63,7 +59,7 @@ test('issue #215 - when using local store, 2nd user should not be rate limited w }) // After this the total time passed for the 2nd user is 5.1s - he should not be rate limited - t.context.clock.tick(2100) + clock.tick(2100) const user2OkResponseAfterRateLimitCompleted = await fastify.inject({ url: '/', @@ -71,11 +67,21 @@ test('issue #215 - when using local store, 2nd user should not be rate limited w remoteAddress: '2.2.2.2' }) - t.equal(user1FirstRequest.statusCode, 200) - t.equal(user2FirstRequest.statusCode, 200) - - t.equal(user2SecondRequestAndShouldBeRateLimited.statusCode, 429) - t.equal(user2ThirdRequestAndShouldStillBeRateLimited.statusCode, 429) - - t.equal(user2OkResponseAfterRateLimitCompleted.statusCode, 200) + t.assert.deepStrictEqual(user1FirstRequest.statusCode, 200) + t.assert.deepStrictEqual(user2FirstRequest.statusCode, 200) + + t.assert.deepStrictEqual( + user2SecondRequestAndShouldBeRateLimited.statusCode, + 429 + ) + t.assert.deepStrictEqual( + user2ThirdRequestAndShouldStillBeRateLimited.statusCode, + 429 + ) + + t.assert.deepStrictEqual( + user2OkResponseAfterRateLimitCompleted.statusCode, + 200 + ) + clock.reset() }) diff --git a/test/github-issues/issue-284.test.js b/test/github-issues/issue-284.test.js index 121dee43..c19b1e64 100644 --- a/test/github-issues/issue-284.test.js +++ b/test/github-issues/issue-284.test.js @@ -1,20 +1,12 @@ 'use strict' -const FakeTimers = require('@sinonjs/fake-timers') -const t = require('tap') -const test = t.test +const { test, mock } = require('node:test') const Fastify = require('fastify') const rateLimit = require('../../index') -t.beforeEach(t => { - t.context.clock = FakeTimers.install() -}) - -t.afterEach(t => { - t.context.clock.uninstall() -}) - -test("issue #284 - don't set the reply code automatically", async t => { +test("issue #284 - don't set the reply code automatically", async (t) => { + const clock = mock.timers + clock.enable() const fastify = Fastify() await fastify.register(rateLimit, { @@ -22,23 +14,27 @@ test("issue #284 - don't set the reply code automatically", async t => { }) fastify.setErrorHandler((err, req, res) => { - t.equal(res.statusCode, 200) - t.equal(err.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(err.statusCode, 429) res.redirect('/') }) - fastify.get('/', { - config: { - rateLimit: { - max: 1, - timeWindow: 5000, - continueExceeding: true + fastify.get( + '/', + { + config: { + rateLimit: { + max: 1, + timeWindow: 5000, + continueExceeding: true + } } + }, + async () => { + return 'hello!' } - }, async () => { - return 'hello!' - }) + ) const firstOkResponse = await fastify.inject({ url: '/', @@ -50,19 +46,29 @@ test("issue #284 - don't set the reply code automatically", async t => { }) // After this the rate limiter should allow for new requests - t.context.clock.tick(5000) + clock.tick(5000) const okResponseAfterRateLimitCompleted = await fastify.inject({ url: '/', method: 'GET' }) - t.equal(firstOkResponse.statusCode, 200) + t.assert.deepStrictEqual(firstOkResponse.statusCode, 200) - t.equal(firstRateLimitResponse.statusCode, 302) - t.equal(firstRateLimitResponse.headers['x-ratelimit-limit'], '1') - t.equal(firstRateLimitResponse.headers['x-ratelimit-remaining'], '0') - t.equal(firstRateLimitResponse.headers['x-ratelimit-reset'], '5') + t.assert.deepStrictEqual(firstRateLimitResponse.statusCode, 302) + t.assert.deepStrictEqual( + firstRateLimitResponse.headers['x-ratelimit-limit'], + '1' + ) + t.assert.deepStrictEqual( + firstRateLimitResponse.headers['x-ratelimit-remaining'], + '0' + ) + t.assert.deepStrictEqual( + firstRateLimitResponse.headers['x-ratelimit-reset'], + '5' + ) - t.equal(okResponseAfterRateLimitCompleted.statusCode, 200) + t.assert.deepStrictEqual(okResponseAfterRateLimitCompleted.statusCode, 200) + clock.reset(0) }) diff --git a/test/global-rate-limit.test.js b/test/global-rate-limit.test.js index 78dcc7c8..7509e1bd 100644 --- a/test/global-rate-limit.test.js +++ b/test/global-rate-limit.test.js @@ -1,18 +1,15 @@ 'use strict' -const t = require('tap') -const test = t.test -const Redis = require('ioredis') +const { test, mock } = require('node:test') const Fastify = require('fastify') const rateLimit = require('../index') -const FakeTimers = require('@sinonjs/fake-timers') -const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) -const REDIS_HOST = '127.0.0.1' +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) -test('Basic', async t => { +test('Basic', async (t) => { t.plan(15) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: 1000 }) @@ -22,43 +19,49 @@ test('Basic', async t => { res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second' + }, + JSON.parse(res.payload) + ) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + clock.reset() }) -test('With text timeWindow', async t => { +test('With text timeWindow', async (t) => { t.plan(15) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: '1s' }) @@ -68,43 +71,49 @@ test('With text timeWindow', async t => { res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second' + }, + JSON.parse(res.payload) + ) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + clock.reset() }) -test('With function timeWindow', async t => { +test('With function timeWindow', async (t) => { t.plan(15) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: (_, __) => 1000 }) @@ -114,45 +123,49 @@ test('With function timeWindow', async t => { res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second' + }, + JSON.parse(res.payload) + ) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + clock.reset() }) -test('When passing NaN to the timeWindow property then the timeWindow should be the default value - 60 seconds', async t => { +test('When passing NaN to the timeWindow property then the timeWindow should be the default value - 60 seconds', async (t) => { t.plan(5) - - t.context.clock = FakeTimers.install() - + const clock = mock.timers + clock.enable(0) const defaultTimeWindowInSeconds = '60' const fastify = Fastify() @@ -164,31 +177,33 @@ test('When passing NaN to the timeWindow property then the timeWindow should be res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-reset'], defaultTimeWindowInSeconds) + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual( + res.headers['x-ratelimit-reset'], + defaultTimeWindowInSeconds + ) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) // Wait for almost 60s to make sure the time limit is right - t.context.clock.tick(55 * 1000) + clock.tick(55 * 1000) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) // Wait for the seconds that left until the time limit reset - t.context.clock.tick(5 * 1000) + clock.tick(5 * 1000) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + clock.reset() }) -test('With ips allowList, allowed ips should not result in rate limiting', async t => { +test('With ips allowList, allowed ips should not result in rate limiting', async (t) => { t.plan(3) const fastify = Fastify() await fastify.register(rateLimit, { @@ -202,16 +217,16 @@ test('With ips allowList, allowed ips should not result in rate limiting', async let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) }) -test('With ips allowList, not allowed ips should result in rate limiting', async t => { +test('With ips allowList, not allowed ips should result in rate limiting', async (t) => { t.plan(3) const fastify = Fastify() await fastify.register(rateLimit, { @@ -225,16 +240,16 @@ test('With ips allowList, not allowed ips should result in rate limiting', async let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('With ips whitelist', async t => { +test('With ips whitelist', async (t) => { t.plan(3) const fastify = Fastify() await fastify.register(rateLimit, { @@ -248,25 +263,27 @@ test('With ips whitelist', async t => { let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) }) -test('With function allowList', async t => { +test('With function allowList', async (t) => { t.plan(18) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: '2s', - keyGenerator () { return 42 }, + keyGenerator () { + return 42 + }, allowList: function (req, key) { - t.ok(req.headers) - t.equal(key, 42) + t.assert.ok(req.headers) + t.assert.deepStrictEqual(key, 42) return req.headers['x-my-header'] !== undefined } }) @@ -284,36 +301,38 @@ test('With function allowList', async t => { let res res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('With async/await function allowList', async t => { +test('With async/await function allowList', async (t) => { t.plan(18) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: '2s', - keyGenerator () { return 42 }, + keyGenerator () { + return 42 + }, allowList: async function (req, key) { await sleep(1) - t.ok(req.headers) - t.equal(key, 42) + t.assert.ok(req.headers) + t.assert.deepStrictEqual(key, 42) return req.headers['x-my-header'] !== undefined } }) @@ -331,32 +350,32 @@ test('With async/await function allowList', async t => { let res res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('With onExceeding option', async t => { +test('With onExceeding option', async (t) => { t.plan(5) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: '2s', onExceeding: function (req, key) { - if (req && key) t.pass('onExceeding called') + if (req && key) t.assert.ok('onExceeding called') } }) @@ -365,23 +384,23 @@ test('With onExceeding option', async t => { let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('With onExceeded option', async t => { +test('With onExceeded option', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: '2s', onExceeded: function (req, key) { - if (req && key) t.pass('onExceeded called') + if (req && key) t.assert.ok('onExceeded called') } }) @@ -390,23 +409,24 @@ test('With onExceeded option', async t => { let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('With onBanReach option', async t => { +test('With onBanReach option', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, ban: 1, onBanReach: function (req) { - t.pass('onBanReach called') + // onBanReach called + t.assert.ok(req) } }) @@ -415,195 +435,25 @@ test('With onBanReach option', async t => { let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - - res = await fastify.inject('/') - t.equal(res.statusCode, 429) - - res = await fastify.inject('/') - t.equal(res.statusCode, 403) -}) - -test('With redis store', async t => { - t.plan(19) - const fastify = Fastify() - const redis = new Redis({ host: REDIS_HOST }) - await fastify.register(rateLimit, { - max: 2, - timeWindow: 1000, - redis - }) - - fastify.get('/', async (req, reply) => 'hello!') - - let res - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '1') - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - - await sleep(100) - - res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) - - // Not using fake timers here as we use an external Redis that would not be effected by this - await sleep(1100) - - res = await fastify.inject('/') - - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '1') - - await redis.flushall() - await redis.quit() -}) - -test('With redis store (ban)', async t => { - t.plan(19) - const fastify = Fastify() - const redis = new Redis({ host: REDIS_HOST }) - await fastify.register(rateLimit, { - max: 1, - ban: 1, - timeWindow: 1000, - redis - }) - - fastify.get('/', async (req, reply) => 'hello!') - - let res - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - - res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - - res = await fastify.inject('/') - t.equal(res.statusCode, 403) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 403, - error: 'Forbidden', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) - - // Not using fake timers here as we use an external Redis that would not be effected by this - await sleep(1100) - - res = await fastify.inject('/') - - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - - await redis.flushall() - await redis.quit() -}) - -test('Skip on redis error', async t => { - t.plan(9) - const fastify = Fastify() - const redis = new Redis({ host: REDIS_HOST }) - await fastify.register(rateLimit, { - max: 2, - timeWindow: 1000, - redis, - skipOnError: true - }) - - fastify.get('/', async (req, reply) => 'hello!') - - let res - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - - await redis.flushall() - await redis.quit() - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '2') + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '2') -}) - -test('Throw on redis error', async t => { - t.plan(5) - const fastify = Fastify() - const redis = new Redis({ host: REDIS_HOST }) - await fastify.register(rateLimit, { - max: 2, - timeWindow: 1000, - redis, - skipOnError: false - }) - - fastify.get('/', async (req, reply) => 'hello!') - - let res - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - - await redis.flushall() - await redis.quit() + t.assert.deepStrictEqual(res.statusCode, 429) res = await fastify.inject('/') - t.equal(res.statusCode, 500) - t.equal(res.body, '{"statusCode":500,"error":"Internal Server Error","message":"Connection is closed."}') + t.assert.deepStrictEqual(res.statusCode, 403) }) -test('With keyGenerator', async t => { +test('With keyGenerator', async (t) => { t.plan(19) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: 1000, keyGenerator (req) { - t.equal(req.headers['my-custom-header'], 'random-value') + t.assert.deepStrictEqual(req.headers['my-custom-header'], 'random-value') return req.headers['my-custom-header'] } }) @@ -621,45 +471,51 @@ test('With keyGenerator', async t => { let res res = await fastify.inject(payload) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject(payload) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject(payload) - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) - - t.context.clock.tick(1100) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second' + }, + JSON.parse(res.payload) + ) - res = await fastify.inject(payload) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + clock.tick(1100) - t.context.clock.uninstall() + res = await fastify.inject(payload) + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + clock.reset() }) -test('With async/await keyGenerator', async t => { +test('With async/await keyGenerator', async (t) => { + t.plan(16) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, timeWindow: 1000, keyGenerator: async function (req) { await sleep(1) - t.equal(req.headers['my-custom-header'], 'random-value') + t.assert.deepStrictEqual(req.headers['my-custom-header'], 'random-value') return req.headers['my-custom-header'] } }) @@ -677,32 +533,38 @@ test('With async/await keyGenerator', async t => { let res res = await fastify.inject(payload) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject(payload) - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second' + }, + JSON.parse(res.payload) + ) await sleep(1100) res = await fastify.inject(payload) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') }) -test('With CustomStore', async t => { +test('With CustomStore', async (t) => { t.plan(15) function CustomStore (options) { @@ -713,11 +575,13 @@ test('With CustomStore', async t => { CustomStore.prototype.incr = function (key, cb) { const timeWindow = this.options.timeWindow this.current++ - cb(null, { current: this.current, ttl: timeWindow - (this.current * 1000) }) + cb(null, { current: this.current, ttl: timeWindow - this.current * 1000 }) } CustomStore.prototype.child = function (routeOptions) { - const store = new CustomStore(Object.assign(this.options, routeOptions.config.rateLimit)) + const store = new CustomStore( + Object.assign(this.options, routeOptions.config.rateLimit) + ) return store } @@ -734,34 +598,40 @@ test('With CustomStore', async t => { res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '9') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '9') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '8') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '8') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '7') - t.equal(res.headers['retry-after'], '7') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 10 seconds' - }, JSON.parse(res.payload)) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '7') + t.assert.deepStrictEqual(res.headers['retry-after'], '7') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 10 seconds' + }, + JSON.parse(res.payload) + ) }) -test('does not override the onRequest', async t => { +test('does not override the onRequest', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { @@ -769,20 +639,24 @@ test('does not override the onRequest', async t => { timeWindow: 1000 }) - fastify.get('/', { - onRequest: function (req, reply, next) { - t.pass('onRequest called') - next() - } - }, async (req, reply) => 'hello!') + fastify.get( + '/', + { + onRequest: function (req, reply, next) { + t.assert.ok('onRequest called') + next() + } + }, + async (req, reply) => 'hello!' + ) const res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') }) -test('does not override the onRequest as an array', async t => { +test('does not override the onRequest as an array', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { @@ -790,26 +664,32 @@ test('does not override the onRequest as an array', async t => { timeWindow: 1000 }) - fastify.get('/', { - onRequest: [function (req, reply, next) { - t.pass('onRequest called') - next() - }] - }, async (req, reply) => 'hello!') + fastify.get( + '/', + { + onRequest: [ + function (req, reply, next) { + t.assert.ok('onRequest called') + next() + } + ] + }, + async (req, reply) => 'hello!' + ) const res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') }) -test('variable max', async t => { +test('variable max', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { max: (req, key) => { - t.pass() + t.assert.ok(req) return +req.headers['secret-max'] }, timeWindow: 1000 @@ -819,17 +699,17 @@ test('variable max', async t => { const res = await fastify.inject({ url: '/', headers: { 'secret-max': 50 } }) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '50') - t.equal(res.headers['x-ratelimit-remaining'], '49') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '50') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '49') }) -test('variable max contenders', async t => { +test('variable max contenders', async (t) => { t.plan(7) const fastify = Fastify() await fastify.register(rateLimit, { keyGenerator: (req) => req.headers['api-key'], - max: (req, key) => key === 'pro' ? 3 : 2, + max: (req, key) => (key === 'pro' ? 3 : 2), timeWindow: 10000 }) @@ -847,11 +727,11 @@ test('variable max contenders', async t => { for (const item of requestSequence) { const res = await fastify.inject({ url: item.url, headers: item.headers }) - t.equal(res.statusCode, item.status) + t.assert.deepStrictEqual(res.statusCode, item.status) } }) -test('when passing NaN to max variable then it should use the default max - 1000', async t => { +test('when passing NaN to max variable then it should use the default max - 1000', async (t) => { t.plan(2002) const defaultMax = 1000 @@ -866,18 +746,19 @@ test('when passing NaN to max variable then it should use the default max - 1000 for (let i = 0; i < defaultMax; i++) { const res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1000') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000') } const res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['x-ratelimit-limit'], '1000') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000') }) -test('hide rate limit headers', async t => { +test('hide rate limit headers', async (t) => { t.plan(14) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, @@ -896,34 +777,50 @@ test('hide rate limit headers', async t => { res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.notOk(res.headers['x-ratelimit-limit'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - t.notOk(res.headers['retry-after'], 'the header must be missing') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-limit'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['retry-after'], + 'the header must be missing' + ) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') - t.context.clock.uninstall() + clock.reset() }) -test('hide rate limit headers on exceeding', async t => { +test('hide rate limit headers on exceeding', async (t) => { t.plan(14) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, @@ -941,35 +838,56 @@ test('hide rate limit headers on exceeding', async t => { res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.notOk(res.headers['x-ratelimit-limit'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - - res = await fastify.inject('/') - - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.not(res.headers['x-ratelimit-reset'], undefined) - t.equal(res.headers['retry-after'], '1') - - t.context.clock.tick(1100) - - res = await fastify.inject('/') - - t.equal(res.statusCode, 200) - t.notOk(res.headers['x-ratelimit-limit'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.notStrictEqual( + res.headers['x-ratelimit-limit'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + + res = await fastify.inject('/') + + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.notStrictEqual(res.headers['x-ratelimit-reset'], undefined) + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + + clock.tick(1100) + + res = await fastify.inject('/') + + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.notStrictEqual( + res.headers['x-ratelimit-limit'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + clock.reset() }) -test('hide rate limit headers at all times', async t => { +test('hide rate limit headers at all times', async (t) => { t.plan(14) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, @@ -993,33 +911,65 @@ test('hide rate limit headers at all times', async t => { res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.notOk(res.headers['x-ratelimit-limit'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - - res = await fastify.inject('/') - - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.notOk(res.headers['x-ratelimit-limit'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - t.notOk(res.headers['retry-after'], 'the header must be missing') - - t.context.clock.tick(1100) - - res = await fastify.inject('/') - - t.equal(res.statusCode, 200) - t.notOk(res.headers['x-ratelimit-limit'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.notStrictEqual( + res.headers['x-ratelimit-limit'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + + res = await fastify.inject('/') + + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-limit'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['retry-after'], + 'the header must be missing' + ) + + clock.tick(1100) + + res = await fastify.inject('/') + + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.notStrictEqual( + res.headers['x-ratelimit-limit'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + clock.reset() }) -test('With ban', async t => { +test('With ban', async (t) => { t.plan(3) const fastify = Fastify() await fastify.register(rateLimit, { @@ -1032,44 +982,49 @@ test('With ban', async t => { let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) res = await fastify.inject('/') - t.equal(res.statusCode, 403) + t.assert.deepStrictEqual(res.statusCode, 403) }) -test('stops fastify lifecycle after onRequest and before preValidation', async t => { +test('stops fastify lifecycle after onRequest and before preValidation', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, timeWindow: 1000 }) let preValidationCallCount = 0 - fastify.get('/', { - preValidation: function (req, reply, next) { - t.pass('preValidation called only once') - preValidationCallCount++ - next() - } - }, - async (req, reply) => 'hello!') + fastify.get( + '/', + { + preValidation: function (req, reply, next) { + t.assert.ok('preValidation called only once') + preValidationCallCount++ + next() + } + }, + async (req, reply) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(preValidationCallCount, 1) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual(preValidationCallCount, 1) }) -test('With enabled IETF Draft Spec', async t => { +test('With enabled IETF Draft Spec', async (t) => { t.plan(16) - t.context.clock = FakeTimers.install() + + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, @@ -1089,45 +1044,58 @@ test('With enabled IETF Draft Spec', async t => { res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['ratelimit-limit'], '2') - t.equal(res.headers['ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['ratelimit-limit'], '2') - t.equal(res.headers['ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['ratelimit-limit'], '2') - t.equal(res.headers['ratelimit-remaining'], '0') - t.equal(res.headers['ratelimit-reset'], res.headers['retry-after']) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0') + t.assert.deepStrictEqual( + res.headers['ratelimit-reset'], + res.headers['retry-after'] + ) const { ttl, ...payload } = JSON.parse(res.payload) - t.equal(res.headers['retry-after'], '' + Math.floor(ttl / 1000)) - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second' - }, payload) + t.assert.deepStrictEqual( + res.headers['retry-after'], + '' + Math.floor(ttl / 1000) + ) + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second' + }, + payload + ) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['ratelimit-limit'], '2') - t.equal(res.headers['ratelimit-remaining'], '1') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '1') + clock.reset() }) -test('hide IETF draft spec headers', async t => { +test('hide IETF draft spec headers', async (t) => { t.plan(14) - t.context.clock = FakeTimers.install() + + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, @@ -1147,35 +1115,51 @@ test('hide IETF draft spec headers', async t => { res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['ratelimit-limit'], '1') - t.equal(res.headers['ratelimit-remaining'], '0') - t.equal(res.headers['ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['ratelimit-reset'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.notOk(res.headers['ratelimit-limit'], 'the header must be missing') - t.notOk(res.headers['ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['ratelimit-reset'], 'the header must be missing') - t.notOk(res.headers['retry-after'], 'the header must be missing') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.notStrictEqual( + res.headers['ratelimit-limit'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['ratelimit-reset'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['retry-after'], + 'the header must be missing' + ) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['ratelimit-limit'], '1') - t.equal(res.headers['ratelimit-remaining'], '0') - t.equal(res.headers['ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['ratelimit-reset'], '1') - t.context.clock.uninstall() + clock.reset() }) -test('afterReset and Rate Limit remain the same when enableDraftSpec is enabled', async t => { +test('afterReset and Rate Limit remain the same when enableDraftSpec is enabled', async (t) => { t.plan(13) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, @@ -1187,32 +1171,34 @@ test('afterReset and Rate Limit remain the same when enableDraftSpec is enabled' const res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['ratelimit-limit'], '1') - t.equal(res.headers['ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0') - t.context.clock.tick(500) + clock.tick(500) await retry('10') - t.context.clock.tick(1000) + clock.tick(1000) await retry('9') async function retry (timeLeft) { const res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['ratelimit-limit'], '1') - t.equal(res.headers['ratelimit-remaining'], '0') - t.equal(res.headers['ratelimit-reset'], timeLeft) - t.equal(res.headers['ratelimit-reset'], res.headers['retry-after']) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['ratelimit-reset'], timeLeft) + t.assert.deepStrictEqual( + res.headers['ratelimit-reset'], + res.headers['retry-after'] + ) } - - t.context.clock.uninstall() + clock.reset() }) -test('Before async in "max"', async t => { +test('Before async in "max"', async (t) => { const fastify = Fastify() - await await fastify.register(rateLimit, { + await fastify.register(rateLimit, { keyGenerator: (req) => req.headers['api-key'], max: async (req, key) => requestSequence(key), timeWindow: 10000 @@ -1220,10 +1206,10 @@ test('Before async in "max"', async t => { await fastify.get('/', async (req, res) => 'hello') - const requestSequence = async (key) => await key === 'pro' ? 5 : 2 + const requestSequence = async (key) => ((await key) === 'pro' ? 5 : 2) }) -test('exposeHeadRoutes', async t => { +test('exposeHeadRoutes', async (t) => { const fastify = Fastify({ exposeHeadRoutes: true }) @@ -1243,16 +1229,36 @@ test('exposeHeadRoutes', async t => { method: 'HEAD' }) - t.equal(res.statusCode, 200, 'GET: Response status code') - t.equal(res.headers['x-ratelimit-limit'], '10', 'GET: x-ratelimit-limit header (global rate limit)') - t.equal(res.headers['x-ratelimit-remaining'], '9', 'GET: x-ratelimit-remaining header (global rate limit)') - - t.equal(resHead.statusCode, 200, 'HEAD: Response status code') - t.equal(resHead.headers['x-ratelimit-limit'], '10', 'HEAD: x-ratelimit-limit header (global rate limit)') - t.equal(resHead.headers['x-ratelimit-remaining'], '8', 'HEAD: x-ratelimit-remaining header (global rate limit)') + t.assert.deepStrictEqual(res.statusCode, 200, 'GET: Response status code') + t.assert.deepStrictEqual( + res.headers['x-ratelimit-limit'], + '10', + 'GET: x-ratelimit-limit header (global rate limit)' + ) + t.assert.deepStrictEqual( + res.headers['x-ratelimit-remaining'], + '9', + 'GET: x-ratelimit-remaining header (global rate limit)' + ) + + t.assert.deepStrictEqual( + resHead.statusCode, + 200, + 'HEAD: Response status code' + ) + t.assert.deepStrictEqual( + resHead.headers['x-ratelimit-limit'], + '10', + 'HEAD: x-ratelimit-limit header (global rate limit)' + ) + t.assert.deepStrictEqual( + resHead.headers['x-ratelimit-remaining'], + '8', + 'HEAD: x-ratelimit-remaining header (global rate limit)' + ) }) -test('When continue exceeding is on (Local)', async t => { +test('When continue exceeding is on (Local)', async (t) => { const fastify = Fastify() await fastify.register(rateLimit, { @@ -1272,168 +1278,15 @@ test('When continue exceeding is on (Local)', async t => { method: 'GET' }) - t.equal(first.statusCode, 200) - - t.equal(second.statusCode, 429) - t.equal(second.headers['x-ratelimit-limit'], '1') - t.equal(second.headers['x-ratelimit-remaining'], '0') - t.equal(second.headers['x-ratelimit-reset'], '5') -}) - -test('When continue exceeding is on (Redis)', async t => { - const fastify = Fastify() - const redis = new Redis({ host: REDIS_HOST }) - - await fastify.register(rateLimit, { - redis, - max: 1, - timeWindow: 5000, - continueExceeding: true - }) - - fastify.get('/', async (req, reply) => 'hello!') - - const first = await fastify.inject({ - url: '/', - method: 'GET' - }) - const second = await fastify.inject({ - url: '/', - method: 'GET' - }) - - t.equal(first.statusCode, 200) - - t.equal(second.statusCode, 429) - t.equal(second.headers['x-ratelimit-limit'], '1') - t.equal(second.headers['x-ratelimit-remaining'], '0') - t.equal(second.headers['x-ratelimit-reset'], '5') - - await redis.flushall() - await redis.quit() -}) - -test('Redis with continueExceeding should not always return the timeWindow as ttl', async t => { - t.plan(19) - const fastify = Fastify() - const redis = new Redis({ host: REDIS_HOST }) - await fastify.register(rateLimit, { - max: 2, - timeWindow: 3000, - continueExceeding: true, - redis - }) - - fastify.get('/', async (req, reply) => 'hello!') - - let res - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '3') - - // After this sleep, we should not see `x-ratelimit-reset === 3` anymore - await sleep(1000) - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '2') - - res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '3') - t.equal(res.headers['retry-after'], '3') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 3 seconds' - }, JSON.parse(res.payload)) - - // Not using fake timers here as we use an external Redis that would not be effected by this - await sleep(1000) - - res = await fastify.inject('/') - - t.equal(res.statusCode, 429) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '3') - - await redis.flushall() - await redis.quit() -}) - -test('When use a custom nameSpace', async t => { - const fastify = Fastify() - const redis = new Redis({ host: REDIS_HOST }) - - await fastify.register(rateLimit, { - max: 2, - timeWindow: 1000, - redis, - nameSpace: 'my-namespace:', - keyGenerator: (req) => req.headers['x-my-header'] - }) - - fastify.get('/', async (req, reply) => 'hello!') - - const allowListHeader = { - method: 'GET', - url: '/', - headers: { - 'x-my-header': 'custom name space' - } - } + t.assert.deepStrictEqual(first.statusCode, 200) - let res - - res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '1') - - res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - - res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) - - // Not using fake timers here as we use an external Redis that would not be effected by this - await sleep(1100) - - res = await fastify.inject(allowListHeader) - - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '1') - - await redis.flushall() - await redis.quit() + t.assert.deepStrictEqual(second.statusCode, 429) + t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5') }) -test('on preHandler hook', async t => { +test('on preHandler hook', async (t) => { const fastify = Fastify() await fastify.register(rateLimit, { @@ -1446,7 +1299,7 @@ test('on preHandler hook', async t => { }) fastify.decorateRequest('userId', '') - fastify.addHook('preHandler', async req => { + fastify.addHook('preHandler', async (req) => { const { userId } = req.query if (userId) { req.userId = userId @@ -1455,7 +1308,7 @@ test('on preHandler hook', async t => { fastify.get('/', async (req, reply) => 'fastify is awesome !') - const send = userId => { + const send = (userId) => { let query if (userId) { query = { userId } @@ -1472,16 +1325,19 @@ test('on preHandler hook', async t => { const fourth = await send('123') const fifth = await send('234') - t.equal(first.statusCode, 200) - t.equal(second.statusCode, 429) - t.equal(third.statusCode, 200) - t.equal(fourth.statusCode, 429) - t.equal(fifth.statusCode, 200) + t.assert.deepStrictEqual(first.statusCode, 200) + t.assert.deepStrictEqual(second.statusCode, 429) + t.assert.deepStrictEqual(third.statusCode, 200) + t.assert.deepStrictEqual(fourth.statusCode, 429) + t.assert.deepStrictEqual(fifth.statusCode, 200) }) -test('ban directly', async t => { +test('ban directly', async (t) => { t.plan(15) - t.context.clock = FakeTimers.install() + + const clock = mock.timers + clock.enable(0) + const fastify = Fastify() await fastify.register(rateLimit, { max: 2, ban: 0, timeWindow: '1s' }) @@ -1491,88 +1347,105 @@ test('ban directly', async t => { res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/') - t.equal(res.statusCode, 403) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 403, - error: 'Forbidden', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) + t.assert.deepStrictEqual(res.statusCode, 403) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 403, + error: 'Forbidden', + message: 'Rate limit exceeded, retry in 1 second' + }, + JSON.parse(res.payload) + ) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + clock.reset() }) -test('wrong timewindow', async t => { +test('wrong timewindow', async (t) => { t.plan(15) - t.context.clock = FakeTimers.install() + + const clock = mock.timers + clock.enable(0) + const fastify = Fastify() await fastify.register(rateLimit, { max: 2, ban: 0, timeWindow: '1s' }) - fastify.get('/', { - config: { - rateLimit: { - timeWindow: -5 + fastify.get( + '/', + { + config: { + rateLimit: { + timeWindow: -5 + } } - } - }, async (req, reply) => 'hello!') + }, + async (req, reply) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/') - t.equal(res.statusCode, 403) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '60') - t.same({ - statusCode: 403, - error: 'Forbidden', - message: 'Rate limit exceeded, retry in 1 minute' - }, JSON.parse(res.payload)) + t.assert.deepStrictEqual(res.statusCode, 403) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '60') + t.assert.deepStrictEqual( + { + statusCode: 403, + error: 'Forbidden', + message: 'Rate limit exceeded, retry in 1 minute' + }, + JSON.parse(res.payload) + ) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 403) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 403) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + clock.reset() }) diff --git a/test/local-store-close.test.js b/test/local-store-close.test.js index e1308d58..6985bf9f 100644 --- a/test/local-store-close.test.js +++ b/test/local-store-close.test.js @@ -1,11 +1,10 @@ 'use strict' -const t = require('tap') -const test = t.test +const { test } = require('node:test') const Fastify = require('fastify') const rateLimit = require('../index') -test('Fastify close on local store', async t => { +test('Fastify close on local store', async (t) => { t.plan(1) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: 1000 }) @@ -15,5 +14,5 @@ test('Fastify close on local store', async t => { done() }) await fastify.close() - t.equal(counter, 2) + t.assert.deepStrictEqual(counter, 2) }) diff --git a/test/not-found-handler-rate-limited.test.js b/test/not-found-handler-rate-limited.test.js index 165876bd..0dad6f6f 100644 --- a/test/not-found-handler-rate-limited.test.js +++ b/test/not-found-handler-rate-limited.test.js @@ -1,103 +1,114 @@ 'use strict' -const t = require('tap') -const test = t.test +const { test } = require('node:test') const Fastify = require('fastify') const rateLimit = require('../index') -test('Set not found handler can be rate limited', async t => { +test('Set not found handler can be rate limited', async (t) => { t.plan(18) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: 1000 }) - t.ok(fastify.rateLimit) - - fastify.setNotFoundHandler({ - preHandler: fastify.rateLimit() - }, function (request, reply) { - t.pass('Error handler has been called') - reply.status(404).send(new Error('Not found')) - }) + t.assert.ok(fastify.rateLimit) + + fastify.setNotFoundHandler( + { + preHandler: fastify.rateLimit() + }, + function (request, reply) { + t.assert.ok('Error handler has been called') + reply.status(404).send(new Error('Not found')) + } + ) let res res = await fastify.inject('/not-found') - t.equal(res.statusCode, 404) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.statusCode, 404) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') res = await fastify.inject('/not-found') - t.equal(res.statusCode, 404) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.statusCode, 404) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') res = await fastify.inject('/not-found') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - t.equal(res.headers['retry-after'], '1') - t.same(JSON.parse(res.payload), { + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual(JSON.parse(res.payload), { statusCode: 429, error: 'Too Many Requests', message: 'Rate limit exceeded, retry in 1 second' }) }) -test('Set not found handler can be rate limited with specific options', async t => { +test('Set not found handler can be rate limited with specific options', async (t) => { t.plan(28) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: 1000 }) - t.ok(fastify.rateLimit) - - fastify.setNotFoundHandler({ - preHandler: fastify.rateLimit({ - max: 4, - timeWindow: 2000 - }) - }, function (request, reply) { - t.pass('Error handler has been called') - reply.status(404).send(new Error('Not found')) - }) + t.assert.ok(fastify.rateLimit) + + fastify.setNotFoundHandler( + { + preHandler: fastify.rateLimit({ + max: 4, + timeWindow: 2000 + }) + }, + function (request, reply) { + t.assert.ok('Error handler has been called') + reply.status(404).send(new Error('Not found')) + } + ) let res res = await fastify.inject('/not-found') - t.equal(res.statusCode, 404) - t.equal(res.headers['x-ratelimit-limit'], '4') - t.equal(res.headers['x-ratelimit-remaining'], '3') - t.equal(res.headers['x-ratelimit-reset'], '2') + t.assert.deepStrictEqual(res.statusCode, 404) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '3') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2') res = await fastify.inject('/not-found') - t.equal(res.statusCode, 404) - t.equal(res.headers['x-ratelimit-limit'], '4') - t.equal(res.headers['x-ratelimit-remaining'], '2') - t.equal(res.headers['x-ratelimit-reset'], '2') + t.assert.deepStrictEqual(res.statusCode, 404) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2') res = await fastify.inject('/not-found') - t.equal(res.statusCode, 404) - t.equal(res.headers['x-ratelimit-limit'], '4') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '2') + t.assert.deepStrictEqual(res.statusCode, 404) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2') res = await fastify.inject('/not-found') - t.equal(res.statusCode, 404) - t.equal(res.headers['x-ratelimit-limit'], '4') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '2') + t.assert.deepStrictEqual(res.statusCode, 404) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2') res = await fastify.inject('/not-found') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '4') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '2') - t.equal(res.headers['x-ratelimit-reset'], '2') - t.same(JSON.parse(res.payload), { + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2') + t.assert.deepStrictEqual(JSON.parse(res.payload), { statusCode: 429, error: 'Too Many Requests', message: 'Rate limit exceeded, retry in 2 seconds' diff --git a/test/redis-rate-limit.test.js b/test/redis-rate-limit.test.js new file mode 100644 index 00000000..dd289c17 --- /dev/null +++ b/test/redis-rate-limit.test.js @@ -0,0 +1,613 @@ +'use strict' + +const { test, describe } = require('node:test') +const Redis = require('ioredis') +const Fastify = require('fastify') +const rateLimit = require('../index') + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) + +const REDIS_HOST = '127.0.0.1' + +describe('Global rate limit', () => { + test('With redis store', async (t) => { + t.plan(21) + const fastify = Fastify() + const redis = await new Redis({ host: REDIS_HOST }) + await fastify.register(rateLimit, { + max: 2, + timeWindow: 1000, + redis + }) + + fastify.get('/', async (req, reply) => 'hello!') + + let res + + res = await fastify.inject('/') + t.assert.strictEqual(res.statusCode, 200) + t.assert.ok(res) + t.assert.strictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + await sleep(100) + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second' + }, + JSON.parse(res.payload) + ) + + // Not using fake timers here as we use an external Redis that would not be effected by this + await sleep(1100) + + res = await fastify.inject('/') + + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + await redis.flushall() + await redis.quit() + }) + + test('With redis store (ban)', async (t) => { + t.plan(19) + const fastify = Fastify() + const redis = await new Redis({ host: REDIS_HOST }) + await fastify.register(rateLimit, { + max: 1, + ban: 1, + timeWindow: 1000, + redis + }) + + fastify.get('/', async (req, reply) => 'hello!') + + let res + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 403) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 403, + error: 'Forbidden', + message: 'Rate limit exceeded, retry in 1 second' + }, + JSON.parse(res.payload) + ) + + // Not using fake timers here as we use an external Redis that would not be effected by this + await sleep(1100) + + res = await fastify.inject('/') + + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + await redis.flushall() + await redis.quit() + }) + + test('Skip on redis error', async (t) => { + t.plan(9) + const fastify = Fastify() + const redis = await new Redis({ host: REDIS_HOST }) + await fastify.register(rateLimit, { + max: 2, + timeWindow: 1000, + redis, + skipOnError: true + }) + + fastify.get('/', async (req, reply) => 'hello!') + + let res + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + + await redis.flushall() + await redis.quit() + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2') + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2') + }) + + test('Throw on redis error', async (t) => { + t.plan(5) + const fastify = Fastify() + const redis = await new Redis({ host: REDIS_HOST }) + await fastify.register(rateLimit, { + max: 2, + timeWindow: 1000, + redis, + skipOnError: false + }) + + fastify.get('/', async (req, reply) => 'hello!') + + let res + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + + await redis.flushall() + await redis.quit() + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 500) + t.assert.deepStrictEqual( + res.body, + '{"statusCode":500,"error":"Internal Server Error","message":"Connection is closed."}' + ) + }) + + test('When continue exceeding is on (Redis)', async (t) => { + const fastify = Fastify() + const redis = await new Redis({ host: REDIS_HOST }) + + await fastify.register(rateLimit, { + redis, + max: 1, + timeWindow: 5000, + continueExceeding: true + }) + + fastify.get('/', async (req, reply) => 'hello!') + + const first = await fastify.inject({ + url: '/', + method: 'GET' + }) + const second = await fastify.inject({ + url: '/', + method: 'GET' + }) + + t.assert.deepStrictEqual(first.statusCode, 200) + + t.assert.deepStrictEqual(second.statusCode, 429) + t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5') + + await redis.flushall() + await redis.quit() + }) + + test('Redis with continueExceeding should not always return the timeWindow as ttl', async (t) => { + t.plan(19) + const fastify = Fastify() + const redis = await new Redis({ host: REDIS_HOST }) + await fastify.register(rateLimit, { + max: 2, + timeWindow: 3000, + continueExceeding: true, + redis + }) + + fastify.get('/', async (req, reply) => 'hello!') + + let res + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '3') + + // After this sleep, we should not see `x-ratelimit-reset === 3` anymore + await sleep(1000) + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2') + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '3') + t.assert.deepStrictEqual(res.headers['retry-after'], '3') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 3 seconds' + }, + JSON.parse(res.payload) + ) + + // Not using fake timers here as we use an external Redis that would not be effected by this + await sleep(1000) + + res = await fastify.inject('/') + + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '3') + + await redis.flushall() + await redis.quit() + }) + + test('When use a custom nameSpace', async (t) => { + const fastify = Fastify() + const redis = await new Redis({ host: REDIS_HOST }) + + await fastify.register(rateLimit, { + max: 2, + timeWindow: 1000, + redis, + nameSpace: 'my-namespace:', + keyGenerator: (req) => req.headers['x-my-header'] + }) + + fastify.get('/', async (req, reply) => 'hello!') + + const allowListHeader = { + method: 'GET', + url: '/', + headers: { + 'x-my-header': 'custom name space' + } + } + + let res + + res = await fastify.inject(allowListHeader) + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + res = await fastify.inject(allowListHeader) + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + res = await fastify.inject(allowListHeader) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second' + }, + JSON.parse(res.payload) + ) + + // Not using fake timers here as we use an external Redis that would not be effected by this + await sleep(1100) + + res = await fastify.inject(allowListHeader) + + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + await redis.flushall() + await redis.quit() + }) +}) + +describe('Route rate limit', () => { + test('With redis store', async t => { + t.plan(19) + const fastify = Fastify() + const redis = new Redis({ host: REDIS_HOST }) + await fastify.register(rateLimit, { + global: false, + redis + }) + + fastify.get('/', { + config: { + rateLimit: { + max: 2, + timeWindow: 1000 + }, + someOtherPlugin: { + someValue: 1 + } + } + }, async (req, reply) => 'hello!') + + let res + + res = await fastify.inject('/') + t.assert.strictEqual(res.statusCode, 200) + t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1') + + res = await fastify.inject('/') + t.assert.strictEqual(res.statusCode, 200) + t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1') + + res = await fastify.inject('/') + t.assert.strictEqual(res.statusCode, 429) + t.assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8') + t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1') + t.assert.strictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual({ + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second' + }, JSON.parse(res.payload)) + + // Not using fake timers here as we use an external Redis that would not be effected by this + await sleep(1100) + + res = await fastify.inject('/') + t.assert.strictEqual(res.statusCode, 200) + t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1') + + await redis.flushall() + await redis.quit() + }) + + test('Throw on redis error', async (t) => { + t.plan(6) + const fastify = Fastify() + const redis = new Redis({ host: REDIS_HOST }) + await fastify.register(rateLimit, { + redis, + global: false + }) + + fastify.get( + '/', + { + config: { + rateLimit: { + max: 2, + timeWindow: 1000, + skipOnError: false + } + } + }, + async (req, reply) => 'hello!' + ) + + let res + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + await redis.flushall() + await redis.quit() + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 500) + t.assert.deepStrictEqual( + res.body, + '{"statusCode":500,"error":"Internal Server Error","message":"Connection is closed."}' + ) + }) + + test('Skip on redis error', async (t) => { + t.plan(9) + const fastify = Fastify() + const redis = new Redis({ host: REDIS_HOST }) + await fastify.register(rateLimit, { + redis, + global: false + }) + + fastify.get( + '/', + { + config: { + rateLimit: { + max: 2, + timeWindow: 1000, + skipOnError: true + } + } + }, + async (req, reply) => 'hello!' + ) + + let res + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + + await redis.flushall() + await redis.quit() + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2') + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2') + }) + + test('When continue exceeding is on (Redis)', async (t) => { + const fastify = Fastify() + const redis = await new Redis({ host: REDIS_HOST }) + + await fastify.register(rateLimit, { + global: false, + redis + }) + + fastify.get( + '/', + { + config: { + rateLimit: { + timeWindow: 5000, + max: 1, + continueExceeding: true + } + } + }, + async (req, reply) => 'hello!' + ) + + const first = await fastify.inject({ + url: '/', + method: 'GET' + }) + const second = await fastify.inject({ + url: '/', + method: 'GET' + }) + + t.assert.deepStrictEqual(first.statusCode, 200) + + t.assert.deepStrictEqual(second.statusCode, 429) + t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5') + + await redis.flushall() + await redis.quit() + }) + + test('When continue exceeding is off under route (Redis)', async (t) => { + const fastify = Fastify() + const redis = await new Redis({ host: REDIS_HOST }) + + await fastify.register(rateLimit, { + global: false, + continueExceeding: true, + redis + }) + + fastify.get( + '/', + { + config: { + rateLimit: { + timeWindow: 5000, + max: 1, + continueExceeding: false + } + } + }, + async (req, reply) => 'hello!' + ) + + const first = await fastify.inject({ + url: '/', + method: 'GET' + }) + const second = await fastify.inject({ + url: '/', + method: 'GET' + }) + + await sleep(2000) + + const third = await fastify.inject({ + url: '/', + method: 'GET' + }) + + t.assert.deepStrictEqual(first.statusCode, 200) + + t.assert.deepStrictEqual(second.statusCode, 429) + t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5') + + t.assert.deepStrictEqual(third.statusCode, 429) + t.assert.deepStrictEqual(third.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(third.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(third.headers['x-ratelimit-reset'], '3') + + await redis.flushall() + await redis.quit() + }) +}) diff --git a/test/route-rate-limit.test.js b/test/route-rate-limit.test.js index 1da31e5e..db070b35 100644 --- a/test/route-rate-limit.test.js +++ b/test/route-rate-limit.test.js @@ -1,15 +1,8 @@ 'use strict' -const t = require('tap') -const test = t.test -const Redis = require('ioredis') +const { test, mock } = require('node:test') const Fastify = require('fastify') const rateLimit = require('../index') -const FakeTimers = require('@sinonjs/fake-timers') - -const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) - -const REDIS_HOST = '127.0.0.1' const defaultRouteConfig = { rateLimit: { @@ -21,19 +14,26 @@ const defaultRouteConfig = { } } -test('Basic', async t => { - t.plan(21) - t.context.clock = FakeTimers.install() +test('Basic', async (t) => { + t.plan(20) + + const clock = mock.timers + clock.enable(0) + const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: defaultRouteConfig - }, async (req, reply) => 'hello!') + fastify.get( + '/', + { + config: defaultRouteConfig + }, + async (req, reply) => 'hello!' + ) fastify.setErrorHandler(function (error, request, reply) { - t.pass('Error handler has been called') - t.equal(error.statusCode, 429) + // t.assert.ok('Error handler has been called') + t.assert.deepStrictEqual(error.statusCode, 429) reply.code(429) error.message += ' from error handler' reply.send(error) @@ -42,152 +42,174 @@ test('Basic', async t => { let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') // Ticking time to simulate time been passed, passing `shouldAdvanceTime: true` won't help as between the 2 requests // the event loop not reached the timer stage and is not able to run the `setInterval` that sinonjs/fake-timers use internally to update the time - t.context.clock.tick(1) + clock.tick(1) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') - t.context.clock.tick(500) + clock.tick(500) res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second from error handler' - }, JSON.parse(res.payload)) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second from error handler' + }, + JSON.parse(res.payload) + ) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') - t.context.clock.uninstall() + clock.reset() }) -test('With text timeWindow', async t => { +test('With text timeWindow', async (t) => { t.plan(15) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: { - rateLimit: { - max: 2, - timeWindow: '1s' - }, - someOtherPlugin: { - someValue: 1 + fastify.get( + '/', + { + config: { + rateLimit: { + max: 2, + timeWindow: '1s' + }, + someOtherPlugin: { + someValue: 1 + } } - } - }, async (req, reply) => 'hello!') + }, + async (req, reply) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') - t.same(JSON.parse(res.payload), { + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual(JSON.parse(res.payload), { statusCode: 429, error: 'Too Many Requests', message: 'Rate limit exceeded, retry in 1 second' }) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') - t.context.clock.uninstall() + clock.reset() }) -test('With function timeWindow', async t => { +test('With function timeWindow', async (t) => { t.plan(15) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: { - rateLimit: { - max: 2, - timeWindow: (_, __) => 1000 - }, - someOtherPlugin: { - someValue: 1 + fastify.get( + '/', + { + config: { + rateLimit: { + max: 2, + timeWindow: (_, __) => 1000 + }, + someOtherPlugin: { + someValue: 1 + } } - } - }, async (req, reply) => 'hello!') + }, + async (req, reply) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') - t.same(JSON.parse(res.payload), { + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual(JSON.parse(res.payload), { statusCode: 429, error: 'Too Many Requests', message: 'Rate limit exceeded, retry in 1 second' }) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') - t.context.clock.uninstall() + clock.reset() }) -test('With ips allowList', async t => { +test('With ips allowList', async (t) => { t.plan(3) const fastify = Fastify() await fastify.register(rateLimit, { @@ -195,38 +217,46 @@ test('With ips allowList', async t => { allowList: ['127.0.0.1'] }) - fastify.get('/', { - config: defaultRouteConfig - }, async (req, reply) => 'hello!') + fastify.get( + '/', + { + config: defaultRouteConfig + }, + async (req, reply) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) }) -test('With function allowList', async t => { +test('With function allowList', async (t) => { t.plan(18) const fastify = Fastify() await fastify.register(rateLimit, { global: false, keyGenerator: () => 42, allowList: (req, key) => { - t.ok(req.headers) - t.equal(key, 42) + t.assert.ok(req.headers) + t.assert.deepStrictEqual(key, 42) return req.headers['x-my-header'] !== undefined } }) - fastify.get('/', { - config: defaultRouteConfig - }, async (req, reply) => 'hello!') + fastify.get( + '/', + { + config: defaultRouteConfig + }, + async (req, reply) => 'hello!' + ) const allowListHeader = { method: 'GET', @@ -239,225 +269,110 @@ test('With function allowList', async t => { let res res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject(allowListHeader) - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('With onExceeding option', async t => { +test('With onExceeding option', async (t) => { t.plan(5) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: { - rateLimit: { - max: 2, - timeWindow: '2s', - onExceeding: function (req) { - t.pass('onExceeding called') + fastify.get( + '/', + { + config: { + rateLimit: { + max: 2, + timeWindow: '2s', + onExceeding: function (req) { + t.assert.ok('onExceeding called') + } } } - } - }, async (req, res) => 'hello!') + }, + async (req, res) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('With onExceeded option', async t => { +test('With onExceeded option', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: { - rateLimit: { - max: 2, - timeWindow: '2s', - onExceeded: function (req) { - t.pass('onExceeded called') + fastify.get( + '/', + { + config: { + rateLimit: { + max: 2, + timeWindow: '2s', + onExceeded: function (req) { + t.assert.ok('onExceeded called') + } } } - } - }, async (req, res) => 'hello!') - - let res - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - - res = await fastify.inject('/') - t.equal(res.statusCode, 429) -}) - -test('With redis store', async t => { - t.plan(19) - const fastify = Fastify() - const redis = new Redis({ host: REDIS_HOST }) - await fastify.register(rateLimit, { - global: false, - redis - }) - - fastify.get('/', { - config: defaultRouteConfig - }, async (req, reply) => 'hello!') - - let res - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '1') - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - - res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) - - // Not using fake timers here as we use an external Redis that would not be effected by this - await sleep(1100) - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '1') - - await redis.flushall() - await redis.quit() -}) - -test('Throw on redis error', async t => { - t.plan(6) - const fastify = Fastify() - const redis = new Redis({ host: REDIS_HOST }) - await fastify.register(rateLimit, { - redis, - global: false - }) - - fastify.get('/', { - config: { - rateLimit: { - max: 2, - timeWindow: 1000, - skipOnError: false - } - } - }, async (req, reply) => 'hello!') - - let res - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '1') - - await redis.flushall() - await redis.quit() - - res = await fastify.inject('/') - t.equal(res.statusCode, 500) - t.equal(res.body, '{"statusCode":500,"error":"Internal Server Error","message":"Connection is closed."}') -}) - -test('Skip on redis error', async t => { - t.plan(9) - const fastify = Fastify() - const redis = new Redis({ host: REDIS_HOST }) - await fastify.register(rateLimit, { - redis, - global: false - }) - - fastify.get('/', { - config: { - rateLimit: { - max: 2, - timeWindow: 1000, - skipOnError: true - } - } - }, async (req, reply) => 'hello!') + }, + async (req, res) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - - await redis.flushall() - await redis.quit() + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '2') + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '2') + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('With keyGenerator', async t => { +test('With keyGenerator', async (t) => { t.plan(19) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { global: false, keyGenerator: (req) => { - t.equal(req.headers['my-custom-header'], 'random-value') + t.assert.deepStrictEqual(req.headers['my-custom-header'], 'random-value') return req.headers['my-custom-header'] } }) - fastify.get('/', { - config: defaultRouteConfig - }, async (req, reply) => 'hello!') + fastify.get( + '/', + { + config: defaultRouteConfig + }, + async (req, reply) => 'hello!' + ) const payload = { method: 'GET', @@ -469,38 +384,44 @@ test('With keyGenerator', async t => { let res res = await fastify.inject(payload) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject(payload) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject(payload) - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 1 second' - }, JSON.parse(res.payload)) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 1 second' + }, + JSON.parse(res.payload) + ) - t.context.clock.tick(1100) + clock.tick(1100) res = await fastify.inject(payload) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') - t.context.clock.uninstall() + clock.reset() }) -test('no rate limit without settings', async t => { +test('no rate limit without settings', async (t) => { t.plan(3) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) @@ -508,100 +429,123 @@ test('no rate limit without settings', async t => { fastify.get('/', async (req, reply) => 'hello!') const res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], undefined) - t.equal(res.headers['x-ratelimit-remaining'], undefined) + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], undefined) + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], undefined) }) -test('no rate limit with bad rate-limit parameters', async t => { +test('no rate limit with bad rate-limit parameters', async (t) => { t.plan(1) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: 1000 }) try { - fastify.get('/', { - config: Object.assign({}, defaultRouteConfig, { rateLimit: () => { } }) - }, async (req, reply) => 'hello!') + fastify.get( + '/', + { + config: Object.assign({}, defaultRouteConfig, { rateLimit: () => {} }) + }, + async (req, reply) => 'hello!' + ) t.fail('should throw') } catch (err) { - t.equal(err.message, 'Unknown value for route rate-limit configuration') + t.assert.deepStrictEqual( + err.message, + 'Unknown value for route rate-limit configuration' + ) } }) -test('works with existing route config', async t => { +test('works with existing route config', async (t) => { t.plan(2) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: 1000 }) - fastify.get('/', { - config: defaultRouteConfig - }, async (req, reply) => 'hello!') + fastify.get( + '/', + { + config: defaultRouteConfig + }, + async (req, reply) => 'hello!' + ) await fastify.ready() const res = await fastify.inject('/') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') }) -test('With ban', async t => { +test('With ban', async (t) => { t.plan(3) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: { rateLimit: { max: 1, ban: 1 } } - }, async (req, reply) => 'hello!') + fastify.get( + '/', + { + config: { rateLimit: { max: 1, ban: 1 } } + }, + async (req, reply) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) res = await fastify.inject('/') - t.equal(res.statusCode, 403) + t.assert.deepStrictEqual(res.statusCode, 403) }) -test('route can disable the global limit', async t => { +test('route can disable the global limit', async (t) => { t.plan(3) const fastify = Fastify() await fastify.register(rateLimit, { max: 2, timeWindow: 1000 }) - fastify.get('/', { - config: Object.assign({}, defaultRouteConfig, { rateLimit: false }) - }, async (req, reply) => 'hello!') + fastify.get( + '/', + { + config: Object.assign({}, defaultRouteConfig, { rateLimit: false }) + }, + async (req, reply) => 'hello!' + ) const res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], undefined) - t.equal(res.headers['x-ratelimit-remaining'], undefined) + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], undefined) + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], undefined) }) -test('does not override onRequest', async t => { +test('does not override onRequest', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - onRequest: function (req, reply, next) { - t.pass('onRequest called') - next() + fastify.get( + '/', + { + onRequest: function (req, reply, next) { + t.assert.ok('onRequest called') + next() + }, + config: defaultRouteConfig }, - config: defaultRouteConfig - }, async (req, reply) => 'hello!') + async (req, reply) => 'hello!' + ) const res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') }) -test('onExceeding and onExceeded events', async t => { +test('onExceeding and onExceeded events', async (t) => { t.plan(11) let onExceedingCounter = 0 @@ -609,46 +553,50 @@ test('onExceeding and onExceeded events', async t => { const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: Object.assign({}, defaultRouteConfig, { - rateLimit: { - max: 2, - timeWindow: 1000, - onExceeding: function (req) { - // it will be executed 2 times - t.ok(req, 'req should be not null') - onExceedingCounter += 1 - }, - onExceeded: function (req) { - // it will be executed 2 times - t.ok(req, 'req should be not null') - onExceededCounter += 1 + fastify.get( + '/', + { + config: Object.assign({}, defaultRouteConfig, { + rateLimit: { + max: 2, + timeWindow: 1000, + onExceeding: function (req) { + // it will be executed 2 times + t.assert.ok(req, 'req should be not null') + onExceedingCounter += 1 + }, + onExceeded: function (req) { + // it will be executed 2 times + t.assert.ok(req, 'req should be not null') + onExceededCounter += 1 + } } - } - }) - }, async (req, reply) => 'hello!') + }) + }, + async (req, reply) => 'hello!' + ) const payload = { method: 'GET', url: '/' } let res res = await fastify.inject(payload) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject(payload) - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject(payload) - t.equal(res.statusCode, 429) - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') - t.equal(onExceedingCounter, 2) - t.equal(onExceededCounter, 1) + t.assert.deepStrictEqual(onExceedingCounter, 2) + t.assert.deepStrictEqual(onExceededCounter, 1) }) -test('custom error response', async t => { +test('custom error response', async (t) => { t.plan(12) const fastify = Fastify() await fastify.register(rateLimit, { @@ -660,41 +608,48 @@ test('custom error response', async t => { }) }) - fastify.get('/', { - config: { - rateLimit: { - max: 2, - timeWindow: 1000 + fastify.get( + '/', + { + config: { + rateLimit: { + max: 2, + timeWindow: 1000 + } } - } - }, async (req, reply) => 'hello!') + }, + async (req, reply) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') - t.same(JSON.parse(res.payload), { + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + t.assert.deepStrictEqual(JSON.parse(res.payload), { statusCode: 429, timeWindow: '1 second', limit: 2 }) }) -test('variable max contenders', async t => { +test('variable max contenders', async (t) => { t.plan(9) const fastify = Fastify() await fastify.register(rateLimit, { @@ -703,16 +658,24 @@ test('variable max contenders', async t => { timeWindow: 10000 }) - fastify.get('/', { - config: { - rateLimit: { - keyGenerator: (req) => req.headers['api-key'], - max: (req, key) => key === 'pro' ? 3 : 2 + fastify.get( + '/', + { + config: { + rateLimit: { + keyGenerator: (req) => req.headers['api-key'], + max: (req, key) => (key === 'pro' ? 3 : 2) + } } - } - }, async (req, reply) => 'hello') + }, + async (req, reply) => 'hello' + ) - fastify.get('/limit', { config: { rateLimit: {} } }, async (req, res) => 'limited') + fastify.get( + '/limit', + { config: { rateLimit: {} } }, + async (req, res) => 'limited' + ) const requestSequence = [ { headers: { 'api-key': 'pro' }, status: 200, url: '/' }, @@ -728,27 +691,31 @@ test('variable max contenders', async t => { for (const item of requestSequence) { const res = await fastify.inject({ url: item.url, headers: item.headers }) - t.equal(res.statusCode, item.status) + t.assert.deepStrictEqual(res.statusCode, item.status) } }) -// TODO this test gets extremely flaky because of setTimeout -// rewrite using https://www.npmjs.com/package/@sinonjs/fake-timers -test('limit reset per Local storage', { skip: true }, async t => { +// // TODO this test gets extremely flaky because of setTimeout +// // rewrite using https://www.npmjs.com/package/@sinonjs/fake-timers +test('limit reset per Local storage', { skip: true }, async (t) => { t.plan(12) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: { - rateLimit: { - max: 1, - timeWindow: 4000 + fastify.get( + '/', + { + config: { + rateLimit: { + max: 1, + timeWindow: 4000 + } } + }, + (req, reply) => { + reply.send('hello!') } - }, (req, reply) => { - reply.send('hello!') - }) + ) setTimeout(doRequest.bind(null, 4), 0) setTimeout(doRequest.bind(null, 3), 1000) @@ -760,14 +727,15 @@ test('limit reset per Local storage', { skip: true }, async t => { function doRequest (resetValue) { fastify.inject('/', (err, res) => { t.error(err) - t.equal(res.headers['x-ratelimit-reset'], resetValue) + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], resetValue) }) } }) -test('hide rate limit headers', async t => { +test('hide rate limit headers', async (t) => { t.plan(14) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, @@ -780,50 +748,67 @@ test('hide rate limit headers', async t => { } }) - fastify.get('/', { - config: { - rateLimit: { - timeWindow: 1000, - addHeaders: { - 'x-ratelimit-limit': true, // this must override the global one - 'x-ratelimit-remaining': false, - 'x-ratelimit-reset': false, - 'retry-after': false + fastify.get( + '/', + { + config: { + rateLimit: { + timeWindow: 1000, + addHeaders: { + 'x-ratelimit-limit': true, // this must override the global one + 'x-ratelimit-remaining': false, + 'x-ratelimit-reset': false, + 'retry-after': false + } } } - } - }, async (req, res) => 'hello') + }, + async (req, res) => 'hello' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - - res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '1') - t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - t.notOk(res.headers['retry-after'], 'the header must be missing') - - t.context.clock.tick(1100) - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '1') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1') + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.notStrictEqual( + res.headers['x-ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['retry-after'], + 'the header must be missing' + ) + + clock.tick(1100) + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepEqual(res.headers['x-ratelimit-reset'], '1') + + clock.reset() }) -test('hide rate limit headers on exceeding', async t => { +test('hide rate limit headers on exceeding', async (t) => { t.plan(14) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, @@ -835,49 +820,69 @@ test('hide rate limit headers on exceeding', async t => { } }) - fastify.get('/', { - config: { - rateLimit: { - timeWindow: 1000, - addHeadersOnExceeding: { - 'x-ratelimit-limit': true, // this must override the global one - 'x-ratelimit-remaining': false, - 'x-ratelimit-reset': false + fastify.get( + '/', + { + config: { + rateLimit: { + timeWindow: 1000, + addHeadersOnExceeding: { + 'x-ratelimit-limit': true, // this must override the global one + 'x-ratelimit-remaining': false, + 'x-ratelimit-reset': false + } } } - } - }, async (req, res) => 'hello') + }, + async (req, res) => 'hello' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - - res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.not(res.headers['x-ratelimit-reset'], undefined) - t.equal(res.headers['retry-after'], '1') - - t.context.clock.tick(1100) - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.notStrictEqual( + res.headers['x-ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.notStrictEqual(res.headers['x-ratelimit-reset'], undefined) + t.assert.deepStrictEqual(res.headers['retry-after'], '1') + + clock.tick(1100) + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.notStrictEqual( + res.headers['x-ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + + clock.reset() }) -test('hide rate limit headers at all times', async t => { +test('hide rate limit headers at all times', async (t) => { t.plan(14) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { max: 1, @@ -895,53 +900,81 @@ test('hide rate limit headers at all times', async t => { } }) - fastify.get('/', { - config: { - rateLimit: { - timeWindow: 1000, - addHeaders: { - 'x-ratelimit-limit': true, // this must override the global one - 'x-ratelimit-remaining': false, - 'x-ratelimit-reset': false, - 'retry-after': false - }, - addHeadersOnExceeding: { - 'x-ratelimit-limit': false, - 'x-ratelimit-remaining': true, // this must override the global one - 'x-ratelimit-reset': false + fastify.get( + '/', + { + config: { + rateLimit: { + timeWindow: 1000, + addHeaders: { + 'x-ratelimit-limit': true, // this must override the global one + 'x-ratelimit-remaining': false, + 'x-ratelimit-reset': false, + 'retry-after': false + }, + addHeadersOnExceeding: { + 'x-ratelimit-limit': false, + 'x-ratelimit-remaining': true, // this must override the global one + 'x-ratelimit-reset': false + } } } - } - }, async (req, res) => 'hello') + }, + async (req, res) => 'hello' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.notOk(res.headers['x-ratelimit-limit'], 'the header must be missing') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - - res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '1') - t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - t.notOk(res.headers['retry-after'], 'the header must be missing') - - t.context.clock.tick(1100) - - res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.notOk(res.headers['x-ratelimit-limit'], 'the header must be missing') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing') - - t.context.clock.uninstall() + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.notStrictEqual( + res.headers['x-ratelimit-limit'], + 'the header must be missing' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.notStrictEqual( + res.headers['x-ratelimit-remaining'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + t.assert.notStrictEqual( + res.headers['retry-after'], + 'the header must be missing' + ) + + clock.tick(1100) + + res = await fastify.inject('/') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.notStrictEqual( + res.headers['x-ratelimit-limit'], + 'the header must be missing' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.notStrictEqual( + res.headers['x-ratelimit-reset'], + 'the header must be missing' + ) + + clock.reset() }) -test('global timeWindow when not set in routes', async t => { +test('global timeWindow when not set in routes', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { @@ -949,26 +982,34 @@ test('global timeWindow when not set in routes', async t => { timeWindow: 6000 }) - fastify.get('/six', { - config: { rateLimit: { max: 6 } } - }, async (req, res) => 'hello!') + fastify.get( + '/six', + { + config: { rateLimit: { max: 6 } } + }, + async (req, res) => 'hello!' + ) - fastify.get('/four', { - config: { rateLimit: { max: 4, timeWindow: 4000 } } - }, async (req, res) => 'hello!') + fastify.get( + '/four', + { + config: { rateLimit: { max: 4, timeWindow: 4000 } } + }, + async (req, res) => 'hello!' + ) let res res = await fastify.inject('/six') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-reset'], '6') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '6') res = await fastify.inject('/four') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-reset'], '4') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '4') }) -test('timeWindow specified as a string', async t => { +test('timeWindow specified as a string', async (t) => { t.plan(9) function CustomStore (options) { this.options = options @@ -978,7 +1019,7 @@ test('timeWindow specified as a string', async t => { CustomStore.prototype.incr = function (key, cb) { const timeWindow = this.options.timeWindow this.current++ - cb(null, { current: this.current, ttl: timeWindow - (this.current * 1000) }) + cb(null, { current: this.current, ttl: timeWindow - this.current * 1000 }) } CustomStore.prototype.child = function (routeOptions) { @@ -992,29 +1033,33 @@ test('timeWindow specified as a string', async t => { store: CustomStore }) - fastify.get('/', { - config: { rateLimit: { max: 2, timeWindow: '10 seconds' } } - }, async (req, res) => 'hello!') + fastify.get( + '/', + { + config: { rateLimit: { max: 2, timeWindow: '10 seconds' } } + }, + async (req, res) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '9') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '9') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '8') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '8') res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('With CustomStore', async t => { +test('With CustomStore', async (t) => { t.plan(15) function CustomStore (options) { this.options = options @@ -1024,7 +1069,7 @@ test('With CustomStore', async t => { CustomStore.prototype.incr = function (key, cb) { const timeWindow = this.options.timeWindow this.current++ - cb(null, { current: this.current, ttl: timeWindow - (this.current * 1000) }) + cb(null, { current: this.current, ttl: timeWindow - this.current * 1000 }) } CustomStore.prototype.child = function (routeOptions) { @@ -1040,70 +1085,84 @@ test('With CustomStore', async t => { store: CustomStore }) - fastify.get('/', { - config: { rateLimit: { max: 2, timeWindow: 10000 } } - }, async (req, res) => 'hello!') + fastify.get( + '/', + { + config: { rateLimit: { max: 2, timeWindow: 10000 } } + }, + async (req, res) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '1') - t.equal(res.headers['x-ratelimit-reset'], '9') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '9') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '8') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '8') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '2') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '7') - t.equal(res.headers['retry-after'], '7') - t.same({ - statusCode: 429, - error: 'Too Many Requests', - message: 'Rate limit exceeded, retry in 10 seconds' - }, JSON.parse(res.payload)) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '7') + t.assert.deepStrictEqual(res.headers['retry-after'], '7') + t.assert.deepStrictEqual( + { + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded, retry in 10 seconds' + }, + JSON.parse(res.payload) + ) }) -test('stops fastify lifecycle after onRequest and before preValidation', async t => { +test('stops fastify lifecycle after onRequest and before preValidation', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) let preValidationCallCount = 0 - fastify.get('/', { - config: { - rateLimit: { - max: 1, - timeWindow: 1000 + fastify.get( + '/', + { + config: { + rateLimit: { + max: 1, + timeWindow: 1000 + } + }, + preValidation: function (req, reply, next) { + t.assert.ok('preValidation called only once') + preValidationCallCount++ + next() } }, - preValidation: function (req, reply, next) { - t.pass('preValidation called only once') - preValidationCallCount++ - next() - } - }, async (req, res) => 'hello!') + async (req, res) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(preValidationCallCount, 1) + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual(preValidationCallCount, 1) }) -test('avoid double onRequest', async t => { +test('avoid double onRequest', async (t) => { t.plan(3) const fastify = Fastify() @@ -1115,7 +1174,7 @@ test('avoid double onRequest', async t => { max: 1, timeWindow: 1000, keyGenerator: (req) => { - t.pass('keyGenerator called only once') + t.assert.ok('keyGenerator called only once') keyGeneratorCallCount++ return req.ip @@ -1131,11 +1190,11 @@ test('avoid double onRequest', async t => { url: '/test', method: 'GET' }) - t.equal(res.statusCode, 200) - t.equal(keyGeneratorCallCount, 1) + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(keyGeneratorCallCount, 1) }) -test('Allow multiple different rate limiter registrations', async t => { +test('Allow multiple different rate limiter registrations', async (t) => { t.plan(16) const fastify = Fastify() @@ -1158,31 +1217,37 @@ test('Allow multiple different rate limiter registrations', async t => { let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') res = await fastify.inject('/test') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/test') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['retry-after'], '1') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['retry-after'], '1') }) -test('With enable IETF draft spec', async t => { +test('With enable IETF draft spec', async (t) => { t.plan(4) const fastify = Fastify() await fastify.register(rateLimit, { @@ -1190,31 +1255,39 @@ test('With enable IETF draft spec', async t => { enableDraftSpec: true }) - fastify.get('/', { - config: defaultRouteConfig - }, async (req, res) => 'hello!') + fastify.get( + '/', + { + config: defaultRouteConfig + }, + async (req, res) => 'hello!' + ) const res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['ratelimit-limit'], '2') - t.equal(res.headers['ratelimit-remaining'], '1') - t.equal(res.headers['ratelimit-reset'], '1') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '2') + t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '1') + t.assert.deepStrictEqual(res.headers['ratelimit-reset'], '1') }) -test('per route rate limit', async t => { +test('per route rate limit', async (t) => { const fastify = Fastify({ exposeHeadRoutes: true }) await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: { - rateLimit: { - max: 10, - timeWindow: 1000 + fastify.get( + '/', + { + config: { + rateLimit: { + max: 10, + timeWindow: 1000 + } } - } - }, async (req, reply) => 'hello!') + }, + async (req, reply) => 'hello!' + ) const res = await fastify.inject({ url: '/', @@ -1226,23 +1299,48 @@ test('per route rate limit', async t => { method: 'HEAD' }) - t.equal(res.statusCode, 200, 'GET: Response status code') - t.equal(res.headers['x-ratelimit-limit'], '10', 'GET: x-ratelimit-limit header (per route limit)') - t.equal(res.headers['x-ratelimit-remaining'], '9', 'GET: x-ratelimit-remaining header (per route limit)') - - t.equal(resHead.statusCode, 200, 'HEAD: Response status code') - t.equal(resHead.headers['x-ratelimit-limit'], '10', 'HEAD: x-ratelimit-limit header (per route limit)') - t.equal(resHead.headers['x-ratelimit-remaining'], '9', 'HEAD: x-ratelimit-remaining header (per route limit)') + t.assert.deepStrictEqual(res.statusCode, 200, 'GET: Response status code') + t.assert.deepStrictEqual( + res.headers['x-ratelimit-limit'], + '10', + 'GET: x-ratelimit-limit header (per route limit)' + ) + t.assert.deepStrictEqual( + res.headers['x-ratelimit-remaining'], + '9', + 'GET: x-ratelimit-remaining header (per route limit)' + ) + + t.assert.deepStrictEqual( + resHead.statusCode, + 200, + 'HEAD: Response status code' + ) + t.assert.deepStrictEqual( + resHead.headers['x-ratelimit-limit'], + '10', + 'HEAD: x-ratelimit-limit header (per route limit)' + ) + t.assert.deepStrictEqual( + resHead.headers['x-ratelimit-remaining'], + '9', + 'HEAD: x-ratelimit-remaining header (per route limit)' + ) }) -test('Allow custom timeWindow in preHandler', async t => { +test('Allow custom timeWindow in preHandler', async (t) => { t.plan(23) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) fastify.register((fastify, options, done) => { - fastify.get('/default', { - config: { rateLimit: { max: 1, timeWindow: '10 seconds' } } - }, async (req, reply) => 'Global rateLimiter should limit this with 60seconds timeWindow') + fastify.get( + '/default', + { + config: { rateLimit: { max: 1, timeWindow: '10 seconds' } } + }, + async (req, reply) => + 'Global rateLimiter should limit this with 60seconds timeWindow' + ) fastify.route({ method: 'GET', url: '/2', @@ -1275,93 +1373,67 @@ test('Allow custom timeWindow in preHandler', async t => { }) let res = await fastify.inject('/2') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/2') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '120') - t.equal(res.headers['retry-after'], '120') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '120') + t.assert.deepStrictEqual(res.headers['retry-after'], '120') res = await fastify.inject('/3') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/3') - t.equal(res.statusCode, 429) - t.equal(res.headers['content-type'], 'application/json; charset=utf-8') - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') - t.equal(res.headers['x-ratelimit-reset'], '180') - t.equal(res.headers['retry-after'], '180') + t.assert.deepStrictEqual(res.statusCode, 429) + t.assert.deepStrictEqual( + res.headers['content-type'], + 'application/json; charset=utf-8' + ) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '180') + t.assert.deepStrictEqual(res.headers['retry-after'], '180') res = await fastify.inject('/default') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1') - t.equal(res.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0') res = await fastify.inject('/default') - t.equal(res.headers['retry-after'], '10') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.headers['retry-after'], '10') + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('When continue exceeding is on (Local)', async t => { +test('When continue exceeding is on (Local)', async (t) => { const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - config: { - rateLimit: { - max: 1, - timeWindow: 5000, - continueExceeding: true - } - } - }, async (req, reply) => 'hello!') - - const first = await fastify.inject({ - url: '/', - method: 'GET' - }) - const second = await fastify.inject({ - url: '/', - method: 'GET' - }) - - t.equal(first.statusCode, 200) - - t.equal(second.statusCode, 429) - t.equal(second.headers['x-ratelimit-limit'], '1') - t.equal(second.headers['x-ratelimit-remaining'], '0') - t.equal(second.headers['x-ratelimit-reset'], '5') -}) - -test('When continue exceeding is on (Redis)', async t => { - const fastify = Fastify() - const redis = await new Redis({ host: REDIS_HOST }) - - await fastify.register(rateLimit, { - global: false, - redis - }) - - fastify.get('/', { - config: { - rateLimit: { - timeWindow: 5000, - max: 1, - continueExceeding: true + fastify.get( + '/', + { + config: { + rateLimit: { + max: 1, + timeWindow: 5000, + continueExceeding: true + } } - } - }, async (req, reply) => 'hello!') + }, + async (req, reply) => 'hello!' + ) const first = await fastify.inject({ url: '/', @@ -1372,120 +1444,75 @@ test('When continue exceeding is on (Redis)', async t => { method: 'GET' }) - t.equal(first.statusCode, 200) - - t.equal(second.statusCode, 429) - t.equal(second.headers['x-ratelimit-limit'], '1') - t.equal(second.headers['x-ratelimit-remaining'], '0') - t.equal(second.headers['x-ratelimit-reset'], '5') + t.assert.deepStrictEqual(first.statusCode, 200) - await redis.flushall() - await redis.quit() + t.assert.deepStrictEqual(second.statusCode, 429) + t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1') + t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0') + t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5') }) -test('When continue exceeding is off under route (Redis)', async t => { +test('should consider routes allow list', async (t) => { const fastify = Fastify() - const redis = await new Redis({ host: REDIS_HOST }) - await fastify.register(rateLimit, { - global: false, - continueExceeding: true, - redis + global: false }) - fastify.get('/', { - config: { - rateLimit: { - timeWindow: 5000, - max: 1, - continueExceeding: false + fastify.get( + '/', + { + config: { + rateLimit: { allowList: ['127.0.0.1'], max: 2, timeWindow: 10000 } } + }, + (req, reply) => { + reply.send('hello!') } - }, async (req, reply) => 'hello!') - - const first = await fastify.inject({ - url: '/', - method: 'GET' - }) - const second = await fastify.inject({ - url: '/', - method: 'GET' - }) - - await sleep(2000) - - const third = await fastify.inject({ - url: '/', - method: 'GET' - }) - - t.equal(first.statusCode, 200) - - t.equal(second.statusCode, 429) - t.equal(second.headers['x-ratelimit-limit'], '1') - t.equal(second.headers['x-ratelimit-remaining'], '0') - t.equal(second.headers['x-ratelimit-reset'], '5') - - t.equal(third.statusCode, 429) - t.equal(third.headers['x-ratelimit-limit'], '1') - t.equal(third.headers['x-ratelimit-remaining'], '0') - t.equal(third.headers['x-ratelimit-reset'], '3') - - await redis.flushall() - await redis.quit() -}) - -test('should consider routes allow list', async t => { - const fastify = Fastify() - await fastify.register(rateLimit, { - global: false - }) - - fastify.get('/', { - config: { rateLimit: { allowList: ['127.0.0.1'], max: 2, timeWindow: 10000 } } - }, (req, reply) => { - reply.send('hello!') - }) + ) let res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) }) -test('on preValidation hook', async t => { +test('on preValidation hook', async (t) => { const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/quero', { - config: { - rateLimit: { - max: 1, - timeWindow: 10000, - hook: 'preValidation', - keyGenerator (req) { - return req.userId || req.ip + fastify.get( + '/quero', + { + config: { + rateLimit: { + max: 1, + timeWindow: 10000, + hook: 'preValidation', + keyGenerator (req) { + return req.userId || req.ip + } } } - } - }, async (req, reply) => 'fastify is awesome !') + }, + async (req, reply) => 'fastify is awesome !' + ) fastify.decorateRequest('userId', '') - fastify.addHook('preParsing', async req => { + fastify.addHook('preParsing', async (req) => { const { userId } = req.query if (userId) { req.userId = userId } }) - const send = userId => { + const send = (userId) => { let query if (userId) { query = { userId } @@ -1502,14 +1529,14 @@ test('on preValidation hook', async t => { const fourth = await send('123') const fifth = await send('234') - t.equal(first.statusCode, 200) - t.equal(second.statusCode, 429) - t.equal(third.statusCode, 200) - t.equal(fourth.statusCode, 429) - t.equal(fifth.statusCode, 200) + t.assert.deepStrictEqual(first.statusCode, 200) + t.assert.deepStrictEqual(second.statusCode, 429) + t.assert.deepStrictEqual(third.statusCode, 200) + t.assert.deepStrictEqual(fourth.statusCode, 429) + t.assert.deepStrictEqual(fifth.statusCode, 200) }) -test('on undefined hook should use onRequest-hook', async t => { +test('on undefined hook should use onRequest-hook', async (t) => { t.plan(2) const fastify = Fastify() @@ -1518,23 +1545,27 @@ test('on undefined hook should use onRequest-hook', async t => { }) fastify.addHook('onRoute', function (routeOptions) { - t.equal(routeOptions.preHandler, undefined) - t.equal(routeOptions.onRequest.length, 1) + t.assert.deepStrictEqual(routeOptions.preHandler, undefined) + t.assert.deepStrictEqual(routeOptions.onRequest.length, 1) }) - fastify.get('/', { - exposeHeadRoute: false, - config: { - rateLimit: { - max: 1, - timeWindow: 10000, - hook: 'onRequest' + fastify.get( + '/', + { + exposeHeadRoute: false, + config: { + rateLimit: { + max: 1, + timeWindow: 10000, + hook: 'onRequest' + } } - } - }, async (req, reply) => 'fastify is awesome !') + }, + async (req, reply) => 'fastify is awesome !' + ) }) -test('on rateLimitHook should not be set twice on HEAD', async t => { +test('on rateLimitHook should not be set twice on HEAD', async (t) => { const fastify = Fastify() await fastify.register(rateLimit, { @@ -1542,164 +1573,197 @@ test('on rateLimitHook should not be set twice on HEAD', async t => { }) fastify.addHook('onRoute', function (routeOptions) { - t.equal(routeOptions.preHandler, undefined) - t.equal(routeOptions.onRequest.length, 1) + t.assert.deepStrictEqual(routeOptions.preHandler, undefined) + t.assert.deepStrictEqual(routeOptions.onRequest.length, 1) }) - fastify.get('/', { - exposeHeadRoute: true, - config: { - rateLimit: { - max: 1, - timeWindow: 10000, - hook: 'onRequest' + fastify.get( + '/', + { + exposeHeadRoute: true, + config: { + rateLimit: { + max: 1, + timeWindow: 10000, + hook: 'onRequest' + } } - } - }, async (req, reply) => 'fastify is awesome !') - - fastify.head('/explicit-head', { - config: { - rateLimit: { - max: 1, - timeWindow: 10000, - hook: 'onRequest' + }, + async (req, reply) => 'fastify is awesome !' + ) + + fastify.head( + '/explicit-head', + { + config: { + rateLimit: { + max: 1, + timeWindow: 10000, + hook: 'onRequest' + } } - } - }, async (req, reply) => 'fastify is awesome !') - - fastify.head('/explicit-head-2', { - exposeHeadRoute: true, - config: { - rateLimit: { - max: 1, - timeWindow: 10000, - hook: 'onRequest' + }, + async (req, reply) => 'fastify is awesome !' + ) + + fastify.head( + '/explicit-head-2', + { + exposeHeadRoute: true, + config: { + rateLimit: { + max: 1, + timeWindow: 10000, + hook: 'onRequest' + } } - } - }, async (req, reply) => 'fastify is awesome !') + }, + async (req, reply) => 'fastify is awesome !' + ) }) -test("child's allowList should not crash the app", async t => { +test("child's allowList should not crash the app", async (t) => { const fastify = Fastify() await fastify.register(rateLimit, { global: false, allowList: () => false }) - fastify.get('/', { - config: { rateLimit: { allowList: ['127.0.0.1'], max: 2, timeWindow: 10000 } } - }, (req, reply) => { - reply.send('hello!') - }) + fastify.get( + '/', + { + config: { + rateLimit: { allowList: ['127.0.0.1'], max: 2, timeWindow: 10000 } + } + }, + (req, reply) => { + reply.send('hello!') + } + ) let res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) }) -test("child's allowList function should not crash and should override parent", async t => { +test("child's allowList function should not crash and should override parent", async (t) => { const fastify = Fastify() await fastify.register(rateLimit, { global: false, allowList: ['127.0.0.1'] }) - fastify.get('/', { - config: { rateLimit: { allowList: () => false, max: 2, timeWindow: 10000 } } - }, (req, reply) => { - reply.send('hello!') - }) + fastify.get( + '/', + { + config: { + rateLimit: { allowList: () => false, max: 2, timeWindow: 10000 } + } + }, + (req, reply) => { + reply.send('hello!') + } + ) let res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('rateLimit decorator should work when a property other than timeWindow is modified', async t => { +test('rateLimit decorator should work when a property other than timeWindow is modified', async (t) => { const fastify = Fastify() await fastify.register(rateLimit, { global: false, allowList: (req, key) => false }) - fastify.get('/', { - onRequest: fastify.rateLimit({ - allowList: ['127.0.0.1'], max: 1 - }) - }, (req, reply) => { - reply.send('hello!') - }) + fastify.get( + '/', + { + onRequest: fastify.rateLimit({ + allowList: ['127.0.0.1'], + max: 1 + }) + }, + (req, reply) => { + reply.send('hello!') + } + ) let res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject('/') - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject({ path: '/', remoteAddress: '1.1.1.1' }) - t.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.statusCode, 200) res = await fastify.inject({ path: '/', remoteAddress: '1.1.1.1' }) - t.equal(res.statusCode, 429) + t.assert.deepStrictEqual(res.statusCode, 429) }) -test('With NaN in subroute config', async t => { +test('With NaN in subroute config', async (t) => { t.plan(12) - t.context.clock = FakeTimers.install() + const clock = mock.timers + clock.enable(0) const fastify = Fastify() await fastify.register(rateLimit, { global: false }) - fastify.get('/', { - - config: { - rateLimit: { - max: NaN + fastify.get( + '/', + { + config: { + rateLimit: { + max: NaN + } } - } - }, async (req, reply) => 'hello!') + }, + async (req, reply) => 'hello!' + ) let res res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1000') - t.equal(res.headers['x-ratelimit-remaining'], '999') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '999') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1000') - t.equal(res.headers['x-ratelimit-remaining'], '998') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '998') res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1000') - t.equal(res.headers['x-ratelimit-remaining'], '997') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '997') - t.context.clock.tick(70000) + clock.tick(70000) res = await fastify.inject('/') - t.equal(res.statusCode, 200) - t.equal(res.headers['x-ratelimit-limit'], '1000') - t.equal(res.headers['x-ratelimit-remaining'], '999') + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000') + t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '999') - t.context.clock.uninstall() + clock.reset() })