From 6f00596e33d319d224e9f2f5e1e6016a68242609 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 30 Sep 2026 15:31:09 +0700 Subject: [PATCH] =?UTF-8?q?fix(api):=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BE=D0=B1=20=D0=BE=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=D0=B0=D1=85=20=D0=BE=D0=B1=D1=89=D0=B5=D0=B3=D0=BE=20GRE?= =?UTF-8?q?-=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 2 +- backend/src/routes/filters-gre-catalog.test.ts | 15 +++++++++++++++ backend/src/routes/filters.ts | 12 +++--------- backend/src/services/gre-catalog-poll.test.ts | 17 +++++++++++++++++ backend/src/services/gre-catalog-poll.ts | 14 ++++++++++++++ docs/ux-ui-progress.md | 9 ++++++++- docs/ux-ui-stage-9.md | 7 +++++++ 7 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 backend/src/routes/filters-gre-catalog.test.ts create mode 100644 backend/src/services/gre-catalog-poll.test.ts create mode 100644 backend/src/services/gre-catalog-poll.ts diff --git a/backend/package.json b/backend/package.json index 1e9f01c9..70bcdaf8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,7 +22,7 @@ "test:config-sync": "tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts", "test:ros-file": "tsx src/services/ros-file-contents.test.ts", "test:backups": "tsx src/services/s3-backup-client.test.ts", - "test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts", + "test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts", "test": "npm run test:alert-engine && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:live-maps && npm run test:config-sync && npm run test:ros-file", "test:geoip": "tsx src/services/traffic-flow-geoip.test.ts" }, diff --git a/backend/src/routes/filters-gre-catalog.test.ts b/backend/src/routes/filters-gre-catalog.test.ts new file mode 100644 index 00000000..e9c9f77e --- /dev/null +++ b/backend/src/routes/filters-gre-catalog.test.ts @@ -0,0 +1,15 @@ +import assert from "node:assert/strict" +import Fastify from "fastify" +import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod" +import filtersRoutes from "./filters.js" +const app = Fastify() +app.setValidatorCompiler(validatorCompiler) +app.setSerializerCompiler(serializerCompiler) +await app.register(filtersRoutes, { prefix: "/api" }) +for (const id of ["srv1", "0", "1abc", "-1"]) { + const response = await app.inject({ method: "GET", url: "/api/filters/gre-tunnels?serverId=" + encodeURIComponent(id) }) + assert.equal(response.statusCode, 400) + assert.equal(response.json().error, "Некорректный serverId GRE-каталога") +} +await app.close() +console.log("GRE invalid selected context: HTTP 400 without DB/RouterOS passed") diff --git a/backend/src/routes/filters.ts b/backend/src/routes/filters.ts index 55b5cf9a..e829ed32 100644 --- a/backend/src/routes/filters.ts +++ b/backend/src/routes/filters.ts @@ -4,6 +4,7 @@ import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod" import { db } from "../db/index.js" import { filterRules, recursiveRoutes, servers } from "../db/schema.js" import { MikrotikClient } from "../services/mikrotik.js" +import { pollGreCatalog } from "../services/gre-catalog-poll.js" import { parseDbServerId } from "../utils/server-id.js" import { appendEvent } from "../modules/events/service/events-service.js" import { managedComment } from "../managed-markers.js" @@ -464,6 +465,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => { app.get("/filters/gre-tunnels", async (req, reply) => { const q = req.query as { serverId?: string | number } const sid = parseDbServerId(q.serverId) + if (q.serverId !== undefined && sid === null) return reply.status(400).send({ error: "Некорректный serverId GRE-каталога" }) const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true)) if (sid !== null) { @@ -478,15 +480,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => { } } - const results = await Promise.all(enabledServers.map(async (server) => { - try { - const remote = await fetchServerFilters(server) - return remote.tunnels - } catch { - return [] as LiveGreTunnel[] - } - })) - return reply.send({ tunnels: results.flat() }) + return reply.send(await pollGreCatalog(enabledServers, fetchServerFilters)) }) /** Без serverId — cache для дашборда. С serverId — live с CHR, cache fallback. */ diff --git a/backend/src/services/gre-catalog-poll.test.ts b/backend/src/services/gre-catalog-poll.test.ts new file mode 100644 index 00000000..7a91e807 --- /dev/null +++ b/backend/src/services/gre-catalog-poll.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert/strict" +import { pollGreCatalog } from "./gre-catalog-poll.js" +const devices = [{ id: 1, name: "Первый" }, { id: 2, name: "Второй" }] +const mixed = await pollGreCatalog(devices, async device => { + if (device.id === 2) throw new Error("Таймаут тест") + return { tunnels: [{ id: "*1", serverId: "1" }] } +}) +assert.deepEqual(mixed, { tunnels: [{ id: "*1", serverId: "1" }], failures: [{ serverId: "2", serverName: "Второй", error: "Таймаут тест" }] }) +assert.deepEqual(await pollGreCatalog(devices, async () => ({ tunnels: [] })), { tunnels: [], failures: [] }) +const failed = await pollGreCatalog(devices, async () => { throw new Error("Недоступно") }) +assert.equal(failed.tunnels.length, 0) +assert.deepEqual(failed.failures.map(f => f.serverId), ["1", "2"]) +const foreign = await pollGreCatalog([devices[0]], async () => ({ tunnels: [{ serverId: "2" }] })) +assert.equal(foreign.tunnels.length, 0) +assert.equal(foreign.failures[0].serverId, "1") +assert.deepEqual(await pollGreCatalog([], async () => { throw new Error("Не должен вызываться") }), { tunnels: [], failures: [] }) +console.log("gre catalog: partial/empty/failed/foreign contexts passed") diff --git a/backend/src/services/gre-catalog-poll.ts b/backend/src/services/gre-catalog-poll.ts new file mode 100644 index 00000000..9f3ee250 --- /dev/null +++ b/backend/src/services/gre-catalog-poll.ts @@ -0,0 +1,14 @@ +export async function pollGreCatalog( + devices: readonly S[], read: (device: S) => Promise<{ tunnels: T[] }>, +): Promise<{ tunnels: T[]; failures: Array<{ serverId: string; serverName: string; error: string }> }> { + const results = await Promise.all(devices.map(async device => { + try { + const { tunnels } = await read(device) + if (!Array.isArray(tunnels) || tunnels.some(t => t.serverId !== String(device.id))) throw new Error("Чужой контекст GRE-каталога") + return { tunnels, failures: [] } + } catch (error) { + return { tunnels: [], failures: [{ serverId: String(device.id), serverName: device.name, error: error instanceof Error ? error.message : String(error) }] } + } + })) + return { tunnels: results.flatMap(r => r.tunnels), failures: results.flatMap(r => r.failures) } +} diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 8bf85567..fe27ffeb 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -25,12 +25,19 @@ | 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже | | 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже | | 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 | -| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.55 — IPsec/GRE/VXLAN, Firewall и операции Filters | +| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.56 — IPsec/GRE/VXLAN, Firewall и операции Filters | | 10 | Маршрутизация, диагностика, карта и терминал | Не начат | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | +## Блок 9.56 — ошибки общего GRE-каталога (2026-09-30) + +- Общий GET /filters/gre-tunnels больше не превращает ошибку устройства в пустой успешный каталог: возвращает tunnels успешных устройств и failures с serverId/name/error. Опрос вынесен в тестируемый pollGreCatalog; чужой контекст строки считается отказом исходного устройства. Выборный успешный/503 путь сохранён. +- Явный невалидный serverId получает HTTP 400 до БД и не переключает запрос на все устройства. Нет изменений сетевого применения. Дополнительное поле failures совместимо с прежними потребителями, но карта сети/оповещения пока его игнорируют — их состояния входят в этапы 10/11 и не приняты этим блоком. +- Unit опроса проверил mixed/empty/all-failed/foreign/нет устройств; Fastify inject с production Zod compiler проверил четыре невалидных ID без запросов DB/RouterOS. TypeScript/backend build и lint новых файлов прошли. Тесты добавлены в test:live-maps. HTTP успех/503 с настоящей БД остаются внешней приёмкой. +- Далее — состояния/схемы/частичные отказы экрана GRE (он использует /gre/tunnels, уже имеющий failures), затем его readiness и контекст. Этап 9 открыт; локальный коммит без push. + ## Блок 9.55 — согласованность ответов IPsec/VXLAN (2026-09-30) - IPsec дополнительно отклоняет чужой serverId вложенных peer/pool/shared-mode-config, повторные peer/сертификаты и отказ с пустым serverId. VXLAN отклоняет повторные failures, одновременные tunnel/failure одного устройства, несогласованные arpMode/arpProxy и повторные ID inventory. Прежние схемы payload и контекст выборного IPsec ответа сохранены. diff --git a/docs/ux-ui-stage-9.md b/docs/ux-ui-stage-9.md index 105e360c..2c0536bd 100644 --- a/docs/ux-ui-stage-9.md +++ b/docs/ux-ui-stage-9.md @@ -307,3 +307,10 @@ Live-копирование читает актуальный целевой rul - Противоречивый ответ целиком считается ошибкой чтения и не заменяет последние подтверждённые строки; вложенный объект не переносится под другой сервер. Старые ответы без arpMode совместимы. - 24 E2E прошли: расширенные повреждённые/чужие payload IPsec и регрессия ссылок/сбоев/гонок VXLAN. TypeScript и lint четырёх файлов чистые. Backend в этом блоке не менялся; последний backend build — 9.54, frontend — 9.52. - Далее — ошибки общего GRE-каталога и его потребители, затем экраны VXLAN/сводная регрессия этапа 9. Реальные RouterOS/JWT/БД не проверены. Локальный коммит без push. + +## Блок 9.56 — ошибки общего GRE-каталога (2026-09-30) + +- Общий GET /filters/gre-tunnels больше не превращает ошибку устройства в пустой успешный каталог: возвращает tunnels успешных устройств и failures с serverId/name/error. Опрос вынесен в тестируемый pollGreCatalog; чужой контекст строки считается отказом исходного устройства. Выборный успешный/503 путь сохранён. +- Явный невалидный serverId получает HTTP 400 до БД и не переключает запрос на все устройства. Нет изменений сетевого применения. Дополнительное поле failures совместимо с прежними потребителями, но карта сети/оповещения пока его игнорируют — их состояния входят в этапы 10/11 и не приняты этим блоком. +- Unit опроса проверил mixed/empty/all-failed/foreign/нет устройств; Fastify inject с production Zod compiler проверил четыре невалидных ID без запросов DB/RouterOS. TypeScript/backend build и lint новых файлов прошли. Тесты добавлены в test:live-maps. HTTP успех/503 с настоящей БД остаются внешней приёмкой. +- Далее — состояния/схемы/частичные отказы экрана GRE (он использует /gre/tunnels, уже имеющий failures), затем его readiness и контекст. Этап 9 открыт; локальный коммит без push.