test(rate-limit): add unit tests for rate-limiting

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson
2026-05-13 11:58:56 +02:00
committed by Philip Molares
parent 5c0f39376c
commit cdf66b00c5
+120
View File
@@ -0,0 +1,120 @@
/*
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { describe, expect, it } from '@jest/globals';
import type { errorResponseBuilderContext } from '@fastify/rate-limit';
import { Mock } from 'ts-mockery';
import type { FastifyRequest } from 'fastify';
import type { CompleteRequest } from '../api/utils/request.type';
import type { SecurityConfig } from '../config/security.config';
import {
buildRateLimitResponse,
generateRateLimitKey,
getMaxLimitByRequestWithSecurityConfig,
getTimeWindowByRequestWithSecurityConfig,
} from './rate-limiting';
import type { SessionState } from '../sessions/session-state';
describe('rate limiting', () => {
const securityConfig = Mock.of<SecurityConfig>({
rateLimit: {
publicApi: { max: 150, window: 300 },
authenticated: { max: 600, window: 300 },
unauthenticated: { max: 100, window: 300 },
auth: { max: 20, window: 600 },
},
});
function createMockedRequest(options: {
url: string;
routeUrl?: string;
ip?: string;
userId?: number | null;
}): FastifyRequest {
return Mock.of<CompleteRequest>({
url: options.url,
ip: options.ip ?? '203.0.113.10',
routeOptions: options.routeUrl ? { url: options.routeUrl } : undefined,
session: Mock.of<SessionState>({
userId: options.userId ?? null,
}),
}) as FastifyRequest;
}
it('generates a user based key for authenticated requests', () => {
const request = createMockedRequest({ url: '/api/v2/notes', userId: 42 });
expect(generateRateLimitKey(request)).toBe('user:42');
});
it('falls back to the ip based key for unauthenticated requests', () => {
const request = createMockedRequest({ url: '/api/v2/notes', ip: '198.51.100.5' });
expect(generateRateLimitKey(request)).toBe('ip:198.51.100.5');
});
it('uses the routeUrl if provided (test with authenticated v2 request)', () => {
const request = createMockedRequest({ url: '/ignored', routeUrl: '/api/v2/notes', userId: 7 });
expect(getTimeWindowByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(300000);
expect(getMaxLimitByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(150);
});
it('uses the public api limits for authenticated v2 requests', () => {
const request = createMockedRequest({ url: '/api/v2/notes', userId: 7 });
expect(getTimeWindowByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(300000);
expect(getMaxLimitByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(150);
});
it('uses the unauthenticated limits for unauthenticated v2 requests', () => {
const request = createMockedRequest({ url: '/api/v2/notes' });
expect(getTimeWindowByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(300000);
expect(getMaxLimitByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(100);
});
it('uses the authenticated private api limits for authenticated requests', () => {
const request = createMockedRequest({ url: '/api/private/notes', userId: 7 });
expect(getTimeWindowByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(300000);
expect(getMaxLimitByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(600);
});
it('never rate limits logout requests', () => {
const request = createMockedRequest({ url: '/api/private/auth/logout', userId: 7 });
expect(getTimeWindowByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(0);
expect(getMaxLimitByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(Infinity);
});
it('uses auth limits for auth endpoints', () => {
const request = createMockedRequest({ url: '/api/private/auth/login' });
expect(getTimeWindowByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(600000);
expect(getMaxLimitByRequestWithSecurityConfig(securityConfig)(request, 'key')).toBe(20);
});
it('returns infinity when the configured max is zero', () => {
const request = createMockedRequest({ url: '/api/private/auth/login' });
const config = Mock.of<SecurityConfig>({
rateLimit: {
publicApi: { max: 150, window: 300 },
authenticated: { max: 600, window: 300 },
unauthenticated: { max: 100, window: 300 },
auth: { max: 0, window: 600 },
},
});
expect(getMaxLimitByRequestWithSecurityConfig(config)(request, 'key')).toBe(Infinity);
});
it('builds the expected rate limit response', () => {
const context = Mock.of<errorResponseBuilderContext>({
after: '10 seconds',
ttl: 2500,
});
expect(buildRateLimitResponse(Mock.of<FastifyRequest>({}), context)).toEqual({
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later (10 seconds).',
expiresIn: 2500,
});
});
});