139 lines
3.2 KiB
TypeScript
139 lines
3.2 KiB
TypeScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import type { NextFunction, Request, Response } from 'express'
|
|
|
|
import { createRateLimitMiddleware, resetRateLimitBucketsForTest } from './rate-limit.js'
|
|
|
|
test('createRateLimitMiddleware allows requests within the window', () => {
|
|
resetRateLimitBucketsForTest()
|
|
|
|
const limiter = createRateLimitMiddleware({
|
|
scope: 'test:allow',
|
|
windowMs: 60_000,
|
|
max: 2,
|
|
})
|
|
const req = createMockRequest()
|
|
const res = createMockResponse()
|
|
let nextCount = 0
|
|
const next: NextFunction = () => {
|
|
nextCount += 1
|
|
}
|
|
|
|
limiter(req, res, next)
|
|
limiter(req, res, next)
|
|
|
|
assert.equal(nextCount, 2)
|
|
assert.equal(res.statusCode, 200)
|
|
})
|
|
|
|
test('createRateLimitMiddleware blocks requests over the limit', () => {
|
|
resetRateLimitBucketsForTest()
|
|
|
|
const limiter = createRateLimitMiddleware({
|
|
scope: 'test:block',
|
|
windowMs: 60_000,
|
|
max: 1,
|
|
})
|
|
const req = createMockRequest()
|
|
const res = createMockResponse()
|
|
let nextCount = 0
|
|
const next: NextFunction = () => {
|
|
nextCount += 1
|
|
}
|
|
|
|
limiter(req, res, next)
|
|
limiter(req, res, next)
|
|
|
|
assert.equal(nextCount, 1)
|
|
assert.equal(res.statusCode, 429)
|
|
assert.equal(res.headers['Retry-After'], '60')
|
|
assert.equal(res.body.errorCode, 'rate_limited')
|
|
})
|
|
|
|
test('createRateLimitMiddleware supports custom limit responses', () => {
|
|
resetRateLimitBucketsForTest()
|
|
|
|
const limiter = createRateLimitMiddleware({
|
|
scope: 'test:custom',
|
|
windowMs: 60_000,
|
|
max: 1,
|
|
onLimit: (_req, res) => {
|
|
res.status(200).json({ code: 429, message: 'limited' })
|
|
},
|
|
})
|
|
const req = createMockRequest()
|
|
const res = createMockResponse()
|
|
|
|
limiter(req, res, () => {})
|
|
limiter(req, res, () => {})
|
|
|
|
assert.equal(res.statusCode, 200)
|
|
assert.deepEqual(res.body, { code: 429, message: 'limited' })
|
|
})
|
|
|
|
test('createRateLimitMiddleware can cap an IP across different accounts', () => {
|
|
resetRateLimitBucketsForTest()
|
|
|
|
const limiter = createRateLimitMiddleware({
|
|
scope: 'test:ip-wide',
|
|
windowMs: 60_000,
|
|
max: 2,
|
|
})
|
|
const first = createMockRequest()
|
|
const second = createMockRequest()
|
|
const third = createMockRequest()
|
|
const response = createMockResponse()
|
|
let nextCount = 0
|
|
const next: NextFunction = () => {
|
|
nextCount += 1
|
|
}
|
|
|
|
limiter(first, response, next)
|
|
limiter(second, response, next)
|
|
limiter(third, response, next)
|
|
|
|
assert.equal(nextCount, 2)
|
|
assert.equal(response.statusCode, 429)
|
|
})
|
|
|
|
function createMockRequest(): Request {
|
|
return {
|
|
ip: '127.0.0.1',
|
|
originalUrl: '/test',
|
|
headers: {},
|
|
socket: {
|
|
remoteAddress: '127.0.0.1',
|
|
},
|
|
} as Request
|
|
}
|
|
|
|
function createMockResponse(): Response & {
|
|
statusCode: number
|
|
headers: Record<string, string>
|
|
body: any
|
|
} {
|
|
const res = {
|
|
statusCode: 200,
|
|
headers: {} as Record<string, string>,
|
|
body: null as any,
|
|
setHeader(name: string, value: string) {
|
|
this.headers[name] = value
|
|
return this
|
|
},
|
|
status(statusCode: number) {
|
|
this.statusCode = statusCode
|
|
return this
|
|
},
|
|
json(body: any) {
|
|
this.body = body
|
|
return this
|
|
},
|
|
}
|
|
|
|
return res as Response & {
|
|
statusCode: number
|
|
headers: Record<string, string>
|
|
body: any
|
|
}
|
|
}
|