feat(api): rate-limit, honeypot, коды ошибок

rateLimit.ts: 5 запросов / 10 минут на IP в памяти, фиксированное окно,
чистка устаревших записей по ходу запросов, 429 с Retry-After. IP из
X-Real-IP только от loopback-сокета (плюс TRUSTED_PROXIES для шлюза
docker), иначе адрес сокета; в базу IP не пишем. Honeypot → 201 без
записи и уведомления. isPaused() → 503 на время восстановления базы.
Тесты на каждый код ответа из таблицы плана.
This commit is contained in:
2026-10-17 15:41:04 +03:00
parent c20e40c333
commit ea0535001c
6 changed files with 203 additions and 9 deletions
+22
View File
@@ -99,6 +99,28 @@ describe('POST /api/lead', () => {
expect(db.prepare('SELECT count(*) AS n FROM leads').get()).toEqual({ n: 0 });
});
it('honeypot: 201 как на обычную заявку, ничего не пишет, notifier молчит', async () => {
const { post, db, events } = setup();
const res = await post(validBody({ company: 'ООО Спам' }));
await flush();
expect(res.status).toBe(201);
expect(await res.json()).toEqual({ ok: true, duplicate: false });
expect(db.prepare('SELECT count(*) AS n FROM leads').get()).toEqual({ n: 0 });
expect(events).toEqual([]);
});
it('503 unavailable: приём на паузе (восстановление базы)', async () => {
let paused = true;
const { post, db } = setup({ isPaused: () => paused });
const res = await post(validBody());
expect(res.status).toBe(503);
expect(await res.json()).toEqual({ ok: false, error: 'unavailable' });
expect(db.prepare('SELECT count(*) AS n FROM leads').get()).toEqual({ n: 0 });
paused = false;
expect((await post(validBody())).status).toBe(201);
});
it('500 server: исключение при записи', async () => {
const { post, db } = setup();
db.close();