import { Request, Response } from 'express'; import { DyFM_Error } from '@futdevpro/fsm-dynamo'; import { DyNTS_RateLimit_Middleware } from './rate-limit.middleware'; /** Test-only — minimal Request mock-ot ad vissza. */ const mockReq = (overrides: Partial<{ ip: string; path: string; headers: Record }> = {}): Request => { return { ip: overrides.ip ?? '127.0.0.1', path: overrides.path ?? '/api/test', headers: overrides.headers ?? {}, } as unknown as Request; }; /** Test-only — Response stub a setHeader-rel. */ const mockRes = (): Response & { _headers: Record } => { const headers: Record = {}; return { _headers: headers, setHeader: function(name: string, value: string): void { headers[name] = value; }, } as unknown as Response & { _headers: Record }; }; describe('| DyNTS_RateLimit_Middleware', (): void => { let svc: DyNTS_RateLimit_Middleware; beforeEach((): void => { svc = DyNTS_RateLimit_Middleware.getInstance(); svc._resetForTesting(); }); afterEach((): void => { svc._resetForTesting(); }); describe('| singleton', (): void => { it('| ugyanazt az instance-t adja vissza', (): void => { const a: DyNTS_RateLimit_Middleware = DyNTS_RateLimit_Middleware.getInstance(); const b: DyNTS_RateLimit_Middleware = DyNTS_RateLimit_Middleware.getInstance(); expect(a).toBe(b); }); }); describe('| check() — default limit', (): void => { it('| atengedi az elso request-et', async (): Promise => { let thrown: any = null; try { await svc.check(mockReq(), mockRes()); } catch (e) { thrown = e; } expect(thrown).toBeNull(); }); it('| 429 amikor a limit-et eleri', async (): Promise => { svc.configure({ defaultLimit: 3, defaultWindowMs: 10000 }); // 3 sikeres request a limit-en belul await svc.check(mockReq(), mockRes()); await svc.check(mockReq(), mockRes()); await svc.check(mockReq(), mockRes()); // 4. request: 429 let thrown: any = null; try { await svc.check(mockReq(), mockRes()); } catch (e) { thrown = e; } expect(thrown).not.toBeNull(); expect(DyFM_Error.getErrorStatus(thrown)).toBe(429); expect(DyFM_Error.getErrorCode(thrown)).toContain('DyNTS-RL-LIMIT'); }); it('| kulonbozo IP-k kulon vodorbe kerulnek', async (): Promise => { svc.configure({ defaultLimit: 2, defaultWindowMs: 10000 }); await svc.check(mockReq({ ip: '1.1.1.1' }), mockRes()); await svc.check(mockReq({ ip: '1.1.1.1' }), mockRes()); // 1.1.1.1 most a limit-en van; 2.2.2.2 meg friss let thrown: any = null; try { await svc.check(mockReq({ ip: '2.2.2.2' }), mockRes()); } catch (e) { thrown = e; } expect(thrown).toBeNull(); }); it('| kulonbozo endpoint-ok kulon vodorbe kerulnek', async (): Promise => { svc.configure({ defaultLimit: 2, defaultWindowMs: 10000 }); await svc.check(mockReq({ path: '/api/a' }), mockRes()); await svc.check(mockReq({ path: '/api/a' }), mockRes()); // /api/a most a limit-en van; /api/b meg friss let thrown: any = null; try { await svc.check(mockReq({ path: '/api/b' }), mockRes()); } catch (e) { thrown = e; } expect(thrown).toBeNull(); }); }); describe('| response headers', (): void => { it('| beallitja az X-RateLimit-* header-eket sikeres request-nel', async (): Promise => { svc.configure({ defaultLimit: 10, defaultWindowMs: 60000 }); const res = mockRes(); await svc.check(mockReq(), res); expect(res._headers['X-RateLimit-Limit']).toBe('10'); expect(res._headers['X-RateLimit-Remaining']).toBe('9'); expect(res._headers['X-RateLimit-Reset']).toBeDefined(); }); it('| beallitja a Retry-After header-t 429-nel', async (): Promise => { svc.configure({ defaultLimit: 1, defaultWindowMs: 10000 }); await svc.check(mockReq(), mockRes()); const res = mockRes(); let thrown: any = null; try { await svc.check(mockReq(), res); } catch (e) { thrown = e; } expect(thrown).not.toBeNull(); expect(res._headers['Retry-After']).toBeDefined(); expect(res._headers['X-RateLimit-Remaining']).toBe('0'); }); it('| nem allit be header-t ha responseHeaders=false', async (): Promise => { svc.configure({ defaultLimit: 10, defaultWindowMs: 60000, responseHeaders: false }); const res = mockRes(); await svc.check(mockReq(), res); expect(res._headers['X-RateLimit-Limit']).toBeUndefined(); }); }); describe('| setPolicyForKey() — subscription-tier-up', (): void => { it('| egyedi limit a kulcsra felulhatja a default-ot', async (): Promise => { svc.configure({ defaultLimit: 2, defaultWindowMs: 10000, keyExtractor: (req: Request): string => (req.headers['x-api-key'] as string) ?? req.ip ?? 'anon', }); svc.setPolicyForKey('premium-key', { limit: 10, windowMs: 10000 }); // premium-key 10-et kap for (let i: number = 0; i < 10; i++) { await svc.check(mockReq({ headers: { 'x-api-key': 'premium-key' } }), mockRes()); } let thrown: any = null; try { await svc.check(mockReq({ headers: { 'x-api-key': 'premium-key' } }), mockRes()); } catch (e) { thrown = e; } expect(thrown).not.toBeNull(); expect(DyFM_Error.getErrorStatus(thrown)).toBe(429); }); it('| clearPolicyForKey visszaallit a default-ra', async (): Promise => { svc.configure({ defaultLimit: 1, defaultWindowMs: 10000, keyExtractor: (req: Request): string => (req.headers['x-api-key'] as string) ?? 'anon', }); svc.setPolicyForKey('k1', { limit: 5, windowMs: 10000 }); svc.clearPolicyForKey('k1'); // most a default 1-es limit ervenyes await svc.check(mockReq({ headers: { 'x-api-key': 'k1' } }), mockRes()); let thrown: any = null; try { await svc.check(mockReq({ headers: { 'x-api-key': 'k1' } }), mockRes()); } catch (e) { thrown = e; } expect(thrown).not.toBeNull(); }); }); describe('| setPolicyForEndpoint() — per-endpoint limit', (): void => { it('| egyedi limit az endpointra felulhatja a default-ot, mas endpoint nem erintett', async (): Promise => { svc.configure({ defaultLimit: 2, defaultWindowMs: 10000 }); svc.setPolicyForEndpoint('/webhook', { limit: 5, windowMs: 10000 }); // /webhook 5-ot kap a default 2 helyett for (let i: number = 0; i < 5; i++) { await svc.check(mockReq({ path: '/webhook' }), mockRes()); } let webhookThrown: any = null; try { await svc.check(mockReq({ path: '/webhook' }), mockRes()); } catch (e) { webhookThrown = e; } expect(webhookThrown).not.toBeNull(); expect(DyFM_Error.getErrorStatus(webhookThrown)).toBe(429); // egy masik endpoint tovabbra is a default 2-t kapja (a per-endpoint policy izolalt) await svc.check(mockReq({ path: '/admin/refund' }), mockRes()); await svc.check(mockReq({ path: '/admin/refund' }), mockRes()); let refundThrown: any = null; try { await svc.check(mockReq({ path: '/admin/refund' }), mockRes()); } catch (e) { refundThrown = e; } expect(refundThrown).not.toBeNull(); }); it('| clearPolicyForEndpoint visszaallit a default-ra', async (): Promise => { svc.configure({ defaultLimit: 1, defaultWindowMs: 10000 }); svc.setPolicyForEndpoint('/webhook', { limit: 5, windowMs: 10000 }); svc.clearPolicyForEndpoint('/webhook'); await svc.check(mockReq({ path: '/webhook' }), mockRes()); let thrown: any = null; try { await svc.check(mockReq({ path: '/webhook' }), mockRes()); } catch (e) { thrown = e; } expect(thrown).not.toBeNull(); }); it('| precedencia: per-kulcs policy felulirja a per-endpoint policy-t', async (): Promise => { svc.configure({ defaultLimit: 2, defaultWindowMs: 10000, keyExtractor: (req: Request): string => (req.headers['x-api-key'] as string) ?? req.ip ?? 'anon', }); svc.setPolicyForEndpoint('/webhook', { limit: 5, windowMs: 10000 }); svc.setPolicyForKey('throttled-key', { limit: 1, windowMs: 10000 }); // a kulcs-policy (1) gyoz az endpoint-policy (5) felett await svc.check(mockReq({ path: '/webhook', headers: { 'x-api-key': 'throttled-key' } }), mockRes()); let thrown: any = null; try { await svc.check(mockReq({ path: '/webhook', headers: { 'x-api-key': 'throttled-key' } }), mockRes()); } catch (e) { thrown = e; } expect(thrown).not.toBeNull(); }); }); describe('| keyExtractor override', (): void => { it('| custom keyExtractor felulhatja a default IP-alapu-t', async (): Promise => { svc.configure({ defaultLimit: 1, defaultWindowMs: 10000, keyExtractor: (req: Request): string => (req.headers['x-api-key'] as string) ?? 'anon', }); // ugyanaz a IP, kulonbozo api-key — kulon limit await svc.check(mockReq({ ip: '1.1.1.1', headers: { 'x-api-key': 'k1' } }), mockRes()); await svc.check(mockReq({ ip: '1.1.1.1', headers: { 'x-api-key': 'k2' } }), mockRes()); // k1 most a limit-en, k2 meg friss; ujabb k1 → 429 let thrown: any = null; try { await svc.check(mockReq({ ip: '1.1.1.1', headers: { 'x-api-key': 'k1' } }), mockRes()); } catch (e) { thrown = e; } expect(thrown).not.toBeNull(); }); }); describe('| GC', (): void => { it('| runGc eltavolitja az ures kulcsokat', async (): Promise => { svc.configure({ defaultLimit: 10, defaultWindowMs: 10 }); // 10ms-os window await svc.check(mockReq(), mockRes()); expect(svc.getConfig().trackedStorageKeys).toBe(1); // varjunk meg amig a window lejar await new Promise((resolve: () => void): void => { setTimeout(resolve, 50); }); svc.runGc(); expect(svc.getConfig().trackedStorageKeys).toBe(0); }); }); describe('| getConfig() diagnosztika', (): void => { it('| visszaadja az aktualis allapotot', (): void => { svc.configure({ defaultLimit: 50, defaultWindowMs: 30000 }); svc.setPolicyForKey('a', { limit: 100, windowMs: 60000 }); svc.setPolicyForKey('b', { limit: 200, windowMs: 60000 }); const cfg = svc.getConfig(); expect(cfg.defaultLimit).toBe(50); expect(cfg.defaultWindowMs).toBe(30000); expect(cfg.activeKeyPolicies).toBe(2); }); }); });