From 5fdf49658f4fd0d31da82ce8774f22d160684306 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 23 May 2026 11:06:57 +0500 Subject: [PATCH] test: add ip-gate plugin tests --- server/src/plugins/__tests__/ip-gate.test.js | 154 +++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 server/src/plugins/__tests__/ip-gate.test.js diff --git a/server/src/plugins/__tests__/ip-gate.test.js b/server/src/plugins/__tests__/ip-gate.test.js new file mode 100644 index 0000000..78871f3 --- /dev/null +++ b/server/src/plugins/__tests__/ip-gate.test.js @@ -0,0 +1,154 @@ +import Fastify from 'fastify' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { registerIpGate } from '../ip-gate.js' + +function buildApp() { + const app = Fastify({ logger: false, trustProxy: true }) + app.get('/test', async () => ({ ok: true })) + app.get('/api/webhooks/yookassa', async () => ({ ok: true })) + app.get('/api/auth/oauth/vk/callback', async () => ({ ok: true })) + app.get('/api/auth/oauth/yandex/callback', async () => ({ ok: true })) + app.get('/api/admin/notifications/telegram/webhook', async () => ({ ok: true })) + return app +} + +describe('registerIpGate', () => { + let app + const originalIps = process.env.SITE_ACCESS_IPS + + beforeEach(async () => { + app = buildApp() + await registerIpGate(app) + await app.ready() + }) + + afterEach(async () => { + await app.close() + process.env.SITE_ACCESS_IPS = originalIps + }) + + it('пропускает запрос если SITE_ACCESS_IPS не задан', async () => { + delete process.env.SITE_ACCESS_IPS + const res = await app.inject({ method: 'GET', url: '/test', remoteAddress: '1.2.3.4' }) + expect(res.statusCode).toBe(200) + expect(res.json()).toEqual({ ok: true }) + }) + + it('пропускает запрос с разрешённого IP', async () => { + process.env.SITE_ACCESS_IPS = '1.2.3.4,5.6.7.8' + const res = await app.inject({ method: 'GET', url: '/test', remoteAddress: '1.2.3.4' }) + expect(res.statusCode).toBe(200) + }) + + it('пропускает запрос с IPv6-mapped разрешённого IP', async () => { + process.env.SITE_ACCESS_IPS = '1.2.3.4' + const res = await app.inject({ method: 'GET', url: '/test', remoteAddress: '::ffff:1.2.3.4' }) + expect(res.statusCode).toBe(200) + }) + + it('блокирует запрос с неразрешённого IP (403)', async () => { + process.env.SITE_ACCESS_IPS = '1.2.3.4' + const res = await app.inject({ method: 'GET', url: '/test', remoteAddress: '9.9.9.9' }) + expect(res.statusCode).toBe(403) + expect(res.headers['content-type']).toMatch(/text\/html/) + expect(res.body).toContain('Любимый Креатив') + expect(res.body).toContain('9.9.9.9') + }) + + it('403-страница показывает IP по умолчанию (127.0.0.1) когда remoteAddress не указан', async () => { + process.env.SITE_ACCESS_IPS = '1.2.3.4' + const res = await app.inject({ method: 'GET', url: '/test' }) + expect(res.statusCode).toBe(403) + expect(res.body).toContain('127.0.0.1') + }) + + it('пропускает исключённые пути с любым IP (webhook yookassa)', async () => { + process.env.SITE_ACCESS_IPS = '1.2.3.4' + const res = await app.inject({ + method: 'GET', + url: '/api/webhooks/yookassa', + remoteAddress: '9.9.9.9', + }) + expect(res.statusCode).toBe(200) + }) + + it('пропускает исключённые пути с любым IP (vk callback)', async () => { + process.env.SITE_ACCESS_IPS = '1.2.3.4' + const res = await app.inject({ + method: 'GET', + url: '/api/auth/oauth/vk/callback', + remoteAddress: '9.9.9.9', + }) + expect(res.statusCode).toBe(200) + }) + + it('пропускает исключённые пути с любым IP (yandex callback)', async () => { + process.env.SITE_ACCESS_IPS = '1.2.3.4' + const res = await app.inject({ + method: 'GET', + url: '/api/auth/oauth/yandex/callback', + remoteAddress: '9.9.9.9', + }) + expect(res.statusCode).toBe(200) + }) + + it('пропускает исключённые пути с любым IP (telegram webhook)', async () => { + process.env.SITE_ACCESS_IPS = '1.2.3.4' + const res = await app.inject({ + method: 'GET', + url: '/api/admin/notifications/telegram/webhook', + remoteAddress: '9.9.9.9', + }) + expect(res.statusCode).toBe(200) + }) + + it('корректно тримит пробелы в списке IP', async () => { + process.env.SITE_ACCESS_IPS = ' 1.2.3.4 , 5.6.7.8 ' + const res = await app.inject({ + method: 'GET', + url: '/test', + remoteAddress: '5.6.7.8', + }) + expect(res.statusCode).toBe(200) + }) + + it('нормализует IPv6-mapped адреса в whitelist', async () => { + process.env.SITE_ACCESS_IPS = '::ffff:1.2.3.4' + const res = await app.inject({ + method: 'GET', + url: '/test', + remoteAddress: '1.2.3.4', + }) + expect(res.statusCode).toBe(200) + }) + + it('пропускает если после трима список IP пуст', async () => { + process.env.SITE_ACCESS_IPS = ' , , ' + const res = await app.inject({ + method: 'GET', + url: '/test', + remoteAddress: '9.9.9.9', + }) + expect(res.statusCode).toBe(200) + }) + + it('путь с query-параметрами проверяется корректно', async () => { + process.env.SITE_ACCESS_IPS = '1.2.3.4' + const res = await app.inject({ + method: 'GET', + url: '/test?foo=bar', + remoteAddress: '9.9.9.9', + }) + expect(res.statusCode).toBe(403) + }) + + it('исключённый путь с query-параметрами тоже пропускается', async () => { + process.env.SITE_ACCESS_IPS = '1.2.3.4' + const res = await app.inject({ + method: 'GET', + url: '/api/webhooks/yookassa?foo=bar', + remoteAddress: '9.9.9.9', + }) + expect(res.statusCode).toBe(200) + }) +})