import Fastify from 'fastify' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { build403Html, 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() if (originalIps === undefined) { delete process.env.SITE_ACCESS_IPS } else { 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('build403Html показывает "не определён" когда IP не передан', () => { const html = build403Html() expect(html).toContain('не определён') expect(html).toContain('Любимый Креатив') }) it('build403Html показывает переданный IP', () => { const html = build403Html('9.9.9.9') expect(html).toContain('9.9.9.9') expect(html).not.toContain('не определён') }) it('build403Html с пустой строкой показывает "не определён"', () => { const html = build403Html('') expect(html).toContain('не определён') }) 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) }) it('пропускает запрос с IP в CIDR-диапазоне /24', async () => { process.env.SITE_ACCESS_IPS = '192.168.1.0/24' const res = await app.inject({ method: 'GET', url: '/test', remoteAddress: '192.168.1.100', }) expect(res.statusCode).toBe(200) }) it('блокирует запрос с IP вне CIDR-диапазона', async () => { process.env.SITE_ACCESS_IPS = '192.168.1.0/24' const res = await app.inject({ method: 'GET', url: '/test', remoteAddress: '10.0.0.1', }) expect(res.statusCode).toBe(403) }) it('пропускает IP в CIDR /32 (эквивалент одного IP)', async () => { process.env.SITE_ACCESS_IPS = '10.0.0.5/32' const res = await app.inject({ method: 'GET', url: '/test', remoteAddress: '10.0.0.5', }) expect(res.statusCode).toBe(200) }) it('блокирует IP рядом с CIDR /32', async () => { process.env.SITE_ACCESS_IPS = '10.0.0.5/32' const res = await app.inject({ method: 'GET', url: '/test', remoteAddress: '10.0.0.6', }) expect(res.statusCode).toBe(403) }) it('пропускает любой IP в CIDR /0', async () => { process.env.SITE_ACCESS_IPS = '0.0.0.0/0' const res = await app.inject({ method: 'GET', url: '/test', remoteAddress: '1.2.3.4', }) expect(res.statusCode).toBe(200) }) it('поддерживает микс точных IP и CIDR-диапазонов', async () => { process.env.SITE_ACCESS_IPS = '1.2.3.4,10.0.0.0/24' const res1 = await app.inject({ method: 'GET', url: '/test', remoteAddress: '1.2.3.4', }) expect(res1.statusCode).toBe(200) const res2 = await app.inject({ method: 'GET', url: '/test', remoteAddress: '10.0.0.50', }) expect(res2.statusCode).toBe(200) const res3 = await app.inject({ method: 'GET', url: '/test', remoteAddress: '9.9.9.9', }) expect(res3.statusCode).toBe(403) }) it('IPv6-mapped адрес в CIDR-диапазоне', async () => { process.env.SITE_ACCESS_IPS = '192.168.1.0/24' const res = await app.inject({ method: 'GET', url: '/test', remoteAddress: '::ffff:192.168.1.50', }) expect(res.statusCode).toBe(200) }) })