#!/usr/bin/env bash # Регресс-тест публичного B2C-периметра МЕРА (ЭТАП 1 плана B2C-запуска). # # Проверяет инварианты периметра (см. корневой Caddyfile): # 1. meraocenka.ru отдаёт 200 анонимно (публичный лэндинг). # 1b. Подстраница лэндинга /trade-in/mera-public/privacy отдаёт 200 — # политика ПДн, на которую ссылается футер. # 1c. Короткие адреса /oferta, /refund, /privacy отдают 200 — эти URL # напечатаны внутри самих юридических документов и уходят эквайеру. # 2. meraocenka.ru/v2 и /trade-in/v2, /trade-in/api/* (B2B-пути) отдают 404 — # allowlist-by-default, НЕ были случайно проброшены на B2B-дерево # tradein-frontend. Проверяются обе формы — с basePath-префиксом и без. # 3. trade-in API (/me, /history, /admin/*) отдаёт 401 анониму — данные B2B # закрыты. Именно API, а не страница: см. комментарий у проверки ниже. # 4. gendsgn.ru/api/v1/admin/* отдаёт 401 анониму (gate Site Finder). # 5. merahome.ru и meraotsenka.ru отдают 301 на канонический meraocenka.ru. # # ВАЖНО: проверки 1 и 2 требуют, чтобы DNS A-record meraocenka.ru → IP VPS # уже существовал И деплой прошёл (сертификат Let's Encrypt выпущен). Пока # записи нет — они ожидаемо падают (DNS resolution failure / TLS handshake # failure), это НЕ регресс периметра gendsgn.ru. Проверки 3 и 4 не зависят от # DNS нового домена и обязаны быть зелёными всегда. # # Запуск вручную: # bash scripts/smoke-mera-perimeter.sh # Запуск в CI: .forgejo/workflows/perimeter-smoke.yml (workflow_dispatch + daily cron). set -uo pipefail BASE_MERA="${SMOKE_MERA_BASE:-https://meraocenka.ru}" BASE_MAIN="${SMOKE_MAIN_BASE:-https://gendsgn.ru}" fail=0 check() { local desc="$1" url="$2" expected="$3" local code code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$url" 2>/dev/null) if [ "$code" = "$expected" ]; then echo "PASS: $desc ($url -> $code)" else echo "FAIL: $desc ($url -> got '${code:-}', expected $expected)" fail=1 fi } echo "== МЕРА B2C perimeter smoke (ЭТАП 1) ==" # 1. Публичный домен отдаёт 200 анонимно. check "meraocenka.ru root — public 200" "$BASE_MERA/" 200 # 1b. Подстраница лэндинга (политика ПДн) доступна — на неё ссылается футер. # Путь приезжает с basePath: next/link + basePath=/trade-in эмитит именно # /trade-in/mera-public/privacy. Если этот handle выпадет из Caddyfile, # обязательный по 152-ФЗ документ станет недоступен с публичной страницы. check "meraocenka.ru privacy — public 200" "$BASE_MERA/trade-in/mera-public/privacy" 200 # 1c. Короткие адреса юридических документов. Это НЕ дубль проверки 1b: именно # эти три URL напечатаны внутри самих документов и уходят в заявку # эквайеру — если rewrite выпадет из Caddyfile, оферта будет ссылаться на # 404, и заявку завернут. Проверяем все три поимённо, потому что и в # Caddyfile они перечислены поимённо (allowlist, не шаблон). check "meraocenka.ru/oferta — public 200" "$BASE_MERA/oferta" 200 check "meraocenka.ru/refund — public 200" "$BASE_MERA/refund" 200 check "meraocenka.ru/privacy — public 200" "$BASE_MERA/privacy" 200 # 2. B2B-путь на публичном домене — 404 (allowlist-by-default), не 200/401. check "meraocenka.ru/v2 — B2B path must 404" "$BASE_MERA/v2" 404 # 2b. Те же B2B-пути в basePath-форме — 404. Это регресс-тест именно на # matcher `handle /trade-in/mera-public/*`: расширь его случайно до # `/trade-in/*` — и B2B-дерево уедет наружу через публичный домен, а # проверка 2 (/v2 без префикса) этого НЕ заметит. check "meraocenka.ru/trade-in/v2 — B2B path must 404" "$BASE_MERA/trade-in/v2" 404 check "meraocenka.ru/trade-in/api/* — must 404 (не проксируем API)" "$BASE_MERA/trade-in/api/v1/me" 404 # 2c. Статика проксируется ТОЛЬКО из _next/static/*. Оптимизатор картинок # /_next/image на лэндинге не нужен (next/image там не импортируется) и # наружу не открыт — иначе аноним получил бы CPU-нагрузку по запросу. # Ловит расширение матчера обратно до `/trade-in/_next/*`. check "meraocenka.ru/_next/image — must 404 (не открываем оптимизатор)" "$BASE_MERA/trade-in/_next/image?url=%2Ftest.png&w=64&q=75" 404 # 3. B2B-данные trade-in по-прежнему закрыты анониму. # # ВНИМАНИЕ: проверять СТРАНИЦУ (/trade-in/v2) больше нельзя — она отдаёт 200. # После #2555/#2558 trade-in ушёл с Caddy basic_auth на собственный логин: # страница рендерится анониму, а RouteGuard уже на клиенте уводит на /login. # Гейт данных переехал на API — там и проверяем, иначе тест зелёный при # открытом наружу бэкенде. check "trade-in /api/v1/me — 401 anonymous" "$BASE_MAIN/trade-in/api/v1/me" 401 check "trade-in /api/v1/history — 401 anonymous (чужие оценки)" "$BASE_MAIN/trade-in/api/v1/history" 401 check "trade-in /api/v1/admin/* — 401 anonymous" "$BASE_MAIN/trade-in/api/v1/admin/users" 401 # 4. gendsgn.ru/api/v1/admin/* отдаёт 401 анониму (auth gate стоит ДО роутинга # в FastAPI — конкретный путь неважен, любой /api/v1/admin/* перехватывается # на уровне Caddy до бэкенда). check "gendsgn.ru/api/v1/admin/* — 401 anonymous" "$BASE_MAIN/api/v1/admin/users" 401 # 5. Домены-спутники ведут на канонический (301, без следования редиректу — # curl без -L, поэтому ждём именно код редиректа, а не 200 конечной страницы). # Как и проверки 1-2, требуют DNS + выпущенного сертификата. check "merahome.ru — 301 to canonical" "https://merahome.ru/" 301 check "meraotsenka.ru — 301 to canonical" "https://meraotsenka.ru/" 301 echo "========================================" if [ "$fail" -eq 0 ]; then echo "ALL CHECKS PASSED" else echo "SOME CHECKS FAILED — see FAIL lines above" fi exit "$fail"