From 87ffb5d6215a3644c0863e9951c7a85f6cdb0372 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Mon, 24 Aug 2026 00:42:56 +0300 Subject: [PATCH] =?UTF-8?q?test(mera):=20=D1=87=D0=B8=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D1=8D=D1=84=D1=84=D0=B5=D0=BA=D1=82=D0=B8=D0=B2=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=20Caddy,=20?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B5=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D0=BD=D0=B5=D0=B2=D0=BE=D0=B9=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI поймал настоящую связь, которую разделение site-блоков порвало: два фронтовых теста читают Caddyfile как ИСТОЧНИК ПРАВДЫ о публичном периметре, а не дублируют список путей. public-perimeter.test.ts — ищет блок meraocenka.ru, сверяет, что каждый маршрут из PUBLIC_ROUTES раздаётся, и что у каждой страницы есть 301 с длинного адреса RouteGuard.publicPaths.ts — вытаскивает матчер @meraPages и сверяет с списком публичных путей в гварде Оба комментария прямо объясняют, зачем так: списки уже расходились дважды (15.08 и 16.08), причём страница в обоих случаях отдавала 200 и ломалась только после гидратации — ни один тест по коду ответа этого не видел. После переноса site-блоков в caddy/sites/*.caddy корневой файл этих блоков не содержит, и проверки стали бы пустыми. Тесты честно упали на своих же страховках («блок не найден — иначе проверки ниже пустые», «не нашли матчер @meraPages — проверка стала бы пустой»), а не тихо позеленели. Теперь оба читают эффективный конфиг: корневой Caddyfile + все caddy/sites/*.caddy. Все файлы берутся намеренно — `import caddy/sites/{$CADDY_SITES:*}` без переменной раскрывается в звёздочку, то есть это и есть конфиг по умолчанию. Проверено симуляцией ровно тех утверждений, на которых падал CI: срез между meraocenka.ru и merahome.ru = 10 458 символов (тест ждёт >500), матчер @meraPages найден, в нём 5 путей. Запустить vitest локально не вышло — фронтовые зависимости не установлены (node_modules/.bin пуст), так что окончательная проверка за CI. Refs #3059 --- .../__tests__/public-perimeter.test.ts | 22 ++++++++++++++++- .../__tests__/RouteGuard.publicPaths.test.ts | 24 +++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/public-perimeter.test.ts b/tradein-mvp/frontend/src/app/mera-public/__tests__/public-perimeter.test.ts index a570b49b..202df462 100644 --- a/tradein-mvp/frontend/src/app/mera-public/__tests__/public-perimeter.test.ts +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/public-perimeter.test.ts @@ -55,6 +55,26 @@ describe("палитра v3 доезжает до CSS", () => { }); }); +/** + * Эффективный конфиг Caddy = корневой Caddyfile + всё, что он импортирует из + * caddy/sites/*.caddy. С #3059 site-блоки живут в отдельных файлах (переезд на + * Selectel: на новом хосте поднимаются только apps, на Beget остаются infra), + * а корневой файл держит глобальные опции и два import'а. Читать один + * Caddyfile здесь больше нельзя — проверка стала бы пустой и молча зелёной. + * + * Берём ВСЕ файлы из caddy/sites/ намеренно: `import caddy/sites/{$CADDY_SITES:*}` + * без переменной раскрывается в звёздочку, то есть это и есть конфиг по умолчанию. + */ +function readEffectiveCaddyConfig(repoRoot: string): string { + const sitesDir = path.join(repoRoot, "caddy", "sites"); + const siteFiles = readdirSync(sitesDir) + .filter((f) => f.endsWith(".caddy")) + .sort() + .map((f) => readFileSync(path.join(sitesDir, f), "utf8")); + return [readFileSync(path.join(repoRoot, "Caddyfile"), "utf8"), ...siteFiles].join(" +"); +} + describe("короткие адреса публичного домена", () => { /** * Дефект 15.08.2026: подвал v3 вёл «Главную» на `/mera-public`, а Caddy на @@ -62,7 +82,7 @@ describe("короткие адреса публичного домена", () = * голый путь падал в catch-all 404. Страница, которой нет в allowlist * Caddyfile, недоступна независимо от того, что она собралась. */ - const caddyfile = readFileSync(path.join(REPO_ROOT, "Caddyfile"), "utf8"); + const caddyfile = readEffectiveCaddyConfig(REPO_ROOT); const meraBlock = caddyfile.slice( caddyfile.indexOf("meraocenka.ru {"), caddyfile.indexOf("merahome.ru {"), diff --git a/tradein-mvp/frontend/src/components/auth/__tests__/RouteGuard.publicPaths.test.ts b/tradein-mvp/frontend/src/components/auth/__tests__/RouteGuard.publicPaths.test.ts index 21eedf8c..4dc18692 100644 --- a/tradein-mvp/frontend/src/components/auth/__tests__/RouteGuard.publicPaths.test.ts +++ b/tradein-mvp/frontend/src/components/auth/__tests__/RouteGuard.publicPaths.test.ts @@ -1,4 +1,4 @@ -import { readFileSync } from "node:fs"; +import { readFileSync, readdirSync } from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; @@ -19,6 +19,26 @@ import { isPublicPath } from "../RouteGuard"; * ТОЧНЫМ совпадением. Наивное добавление его в префиксный список сделало бы * публичным всё приложение — `"/v2/admin".startsWith("/")` истинно. */ +/** + * Эффективный конфиг Caddy = корневой Caddyfile + всё, что он импортирует из + * caddy/sites/*.caddy. С #3059 site-блоки живут в отдельных файлах (переезд на + * Selectel: на новом хосте поднимаются только apps, на Beget остаются infra), + * а корневой файл держит глобальные опции и два import'а. Читать один + * Caddyfile здесь больше нельзя — проверка стала бы пустой и молча зелёной. + * + * Берём ВСЕ файлы из caddy/sites/ намеренно: `import caddy/sites/{$CADDY_SITES:*}` + * без переменной раскрывается в звёздочку, то есть это и есть конфиг по умолчанию. + */ +function readEffectiveCaddyConfig(repoRoot: string): string { + const sitesDir = path.join(repoRoot, "caddy", "sites"); + const siteFiles = readdirSync(sitesDir) + .filter((f) => f.endsWith(".caddy")) + .sort() + .map((f) => readFileSync(path.join(sitesDir, f), "utf8")); + return [readFileSync(path.join(repoRoot, "Caddyfile"), "utf8"), ...siteFiles].join(" +"); +} + describe("isPublicPath", () => { it("пропускает поддерево лэндинга (путь без basePath)", () => { expect(isPublicPath("/mera-public")).toBe(true); @@ -50,7 +70,7 @@ describe("isPublicPath", () => { it("совпадает с тем, что публичный домен реально раздаёт (Caddyfile)", () => { // __tests__ → auth → components → src → frontend → tradein-mvp → корень. const repoRoot = path.resolve(__dirname, "../../../../../.."); - const caddyfile = readFileSync(path.join(repoRoot, "Caddyfile"), "utf8"); + const caddyfile = readEffectiveCaddyConfig(repoRoot); const served = caddyfile .match(/@meraPages path ([^\n]+)/)?.[1] .split(/\s+/)