All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m49s
Первый шаг к отдельному B2C-интерфейсу оценки на meraocenka.ru: домен получает собственную поверхность бэкенда вместо того, чтобы тянуть куски закрытого контура. ## Отдельный префикс, а не проброс кусков /api/v1/* На meraocenka.ru действует allowlist-by-default. Открыть там API можно было двумя способами: перечислить нужные v1-пути поимённо — или завести префикс, под которым по определению не лежит ничего закрытого. Выбран второй: при первом одна опечатка в матчере (`/trade-in/api/*` вместо точного пути) открывает наружу весь v1 — ~20 ручек, включая PDF расчётов, фотографии и админку. Цена ошибки, а не удобство. Добавить сюда приватную ручку теперь нужно СПЕЦИАЛЬНО — положив файл в app/api/public/. Случайно нельзя. ## Ноль записей в БД Обе ручки только читают: /coverage — один SELECT, /suggest — прокси автокомплита. Это условие, при котором публичная форма работает ДО контура согласия 152-ФЗ (#2895: сегодня адрес физлица попадает в trade_in_estimates раньше согласия, а пути удаления в бэкенде нет). Платный расчёт, который писать будет, открывается только вместе с ним. ## Делегирование, а не копии Обе ручки вызывают те же функции, что обслуживают закрытый контур (v1.geocode.suggest_addresses, v1.trade_in.coverage_probe). Разбор #2894 показал, чем кончается вторая копия когорты: проба отвечает «данные есть» там, где платный расчёт видит ноль. Публичный ответ переиспользует CoverageProbeResponse — на нём уже стоит гейт «ни одного price-подобного поля». ## Бюджеты Общего 300/60с мало: /suggest через DaData-тир — платный внешний вызов, абуз стоит денег. Свои per-IP окна: 40/мин на подсказки (человек с debounce'ом тратит единицы на адрес), 15/мин на пробу. ## Проверено 11 тестов, из них структурные: набор ручек под /api/public проверяется на РАВЕНСТВО (третья, добавленная без правки теста, роняет сборку) и сверяется с rbac._PUBLIC_PATHS в обе стороны — чтобы не осталось открытого пути-призрака. Рядом висит закрытый маршрут-двойник: без него «аноним получает 200» одинаково зелёный и когда исключение точечное, и когда auth-гейт снят целиком. Мутации: убрать пути из rbac._PUBLIC_PATHS → 7 failed / 4 passed снять бюджет с /coverage → 2 failed / 9 passed откат → 11 passed Плюс 72 passed на связке rbac + coverage + version, `caddy validate` = Valid configuration, ruff чист. Смоук периметра дополнен парой, которую нельзя разделять: публичные ручки отвечают 200 анонимно И /trade-in/api/v1/* на этом домене по-прежнему 404. Зелёная только первая проверка = API открыт целиком, а тест этого не заметил. Refs #2894, #2895
144 lines
9.7 KiB
Bash
144 lines
9.7 KiB
Bash
#!/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:-<no response>}', expected $expected)"
|
||
fail=1
|
||
fi
|
||
}
|
||
|
||
check_post() {
|
||
local desc="$1" url="$2" body="$3" expected="$4"
|
||
local code
|
||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \
|
||
-X POST -H 'Content-Type: application/json' -d "$body" "$url" 2>/dev/null)
|
||
if [ "$code" = "$expected" ]; then
|
||
echo "PASS: $desc ($url -> $code)"
|
||
else
|
||
echo "FAIL: $desc ($url -> got '${code:-<no response>}', 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
|
||
|
||
# 2d. Публичный API МЕРЫ (#2911). Ровно две ручки под /api/public/mera/*
|
||
# доступны анонимно на обоих доменах; ВЕСЬ /api/v1/* на публичном домене
|
||
# по-прежнему 404.
|
||
#
|
||
# Пара проверок ниже неразделима: первая доказывает, что форма вообще
|
||
# работает, вторая — что новый handle не расширил периметр до
|
||
# `/trade-in/api/*`. Зелёная только первая = API открыт целиком и тест это
|
||
# пропустил (ровно та ошибка, ради которой в Caddyfile выбран отдельный
|
||
# префикс, а не поимённый проброс v1-путей).
|
||
check "meraocenka.ru public suggest — 200 anonymous" "$BASE_MERA/trade-in/api/public/mera/suggest?q=%D0%9C%D0%B0%D0%BB%D1%8B%D1%88%D0%B5%D0%B2%D0%B0" 200
|
||
check_post "meraocenka.ru public coverage — 200 anonymous" "$BASE_MERA/trade-in/api/public/mera/coverage" '{"lat":56.838,"lon":60.597,"rooms":2,"area_m2":54}' 200
|
||
check "meraocenka.ru v1 geocode — must stay 404" "$BASE_MERA/trade-in/api/v1/geocode/suggest?q=%D0%9C%D0%B0%D0%BB%D1%8B%D1%88%D0%B5%D0%B2%D0%B0" 404
|
||
check "meraocenka.ru v1 coverage — must stay 404" "$BASE_MERA/trade-in/api/v1/trade-in/coverage" 404
|
||
|
||
# Тот же публичный путь на gendsgn.ru: страницу лэндинга открывают и оттуда
|
||
# (QA за basic_auth), поэтому URL у формы один на оба домена. Здесь он
|
||
# проходит через уже существующий `handle /trade-in/api/*` — проверка ловит
|
||
# регресс в rbac._PUBLIC_PATHS (стало бы 401), а не в Caddyfile.
|
||
check "gendsgn.ru public suggest — 200 anonymous" "$BASE_MAIN/trade-in/api/public/mera/suggest?q=%D0%9C%D0%B0%D0%BB%D1%8B%D1%88%D0%B5%D0%B2%D0%B0" 200
|
||
|
||
# 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"
|