All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 5m3s
ДВА ПОСЛЕДСТВИЯ #3436, обнаруженные на прогоне против прода. 1. ДЕПЛОЙ МЕРЫ БЫЛ ЗАБЛОКИРОВАН. В #3436 политика конфиденциальности получила раздел про cookie, то есть новую редакцию, и `PRIVACY_APPROVAL` во фронте стал «№ 2 от 10 сентября 2026 г.». Бэкендовая `_CONSENT_POLICY_VERSION` осталась на «2026-08-13», а между ними стоит гейт `test_consent_text_frontend_sync.py` — он и упал. Job `test` в deploy-tradein.yml падает → `deploy` пропускается по своему `needs.test.result != 'failure'` → прод остался на старом образе фронта, при том что Caddy обновился отдельным пайплайном. Внешне это выглядело как «задеплоилось наполовину»: UTM на редиректе со слэшем починился, а noindex и robots.txt — нет. Гейт сработал ровно как задуман: версия согласия обязана указывать на ту редакцию документа, которую человек реально видел, иначе снимок согласия в trade_in_leads.consent_policy_version подписан не тем документом. Правим версию, а не тест. Согласия, собранные до 10.09, остаются с "2026-08-13" — в этом и смысл хранить версию per-row. 2. СМОУК ЖДАЛ 404 ТАМ, ГДЕ ПРОД ОТВЕЧАЕТ 401. Проверка «карта сайта МЕРЫ не просачивается через B2B-домен» ожидала 404 от allowlist'а site-блока, но корень gendsgn.ru закрыт пилотным basic_auth, и гейт отвечает 401 РАНЬШЕ, чем запрос доходит до allowlist'а. Проверка была написана без прогона против прода — это честно отмечено в её же комментарии — и упала на первом же запуске. Заведён `check_any`: PASS на любом из перечисленных кодов. Здесь допустимы 401 и 404 — оба означают проверяемое («наружу этого адреса нет»), а какой рубеж ответил первым, к предмету проверки отношения не имеет. Жёсткое ожидание к тому же сломалось бы при снятии пилотного гейта. Красная строка осталась там, где ей место: 200 означал бы реальную течь. Проверено: `pytest tests/test_consent_text_frontend_sync.py` — 6 passed; полный сьют бэкенда МЕРЫ локально 5737 passed; `bash -n` на смоуке чист; `check_any` прогнан против живого gendsgn.ru — PASS на фактическом 401. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiUFZ3rmTNpp3DRajUo8KQ
390 lines
30 KiB
Bash
390 lines
30 KiB
Bash
#!/usr/bin/env bash
|
||
# Регресс-тест публичного B2C-периметра МЕРА (ЭТАП 1 плана B2C-запуска).
|
||
#
|
||
# Проверяет инварианты периметра (см. корневой Caddyfile):
|
||
# 1. meraocenka.ru отдаёт 200 анонимно (публичный лэндинг).
|
||
# 1b. Длинный адрес /trade-in/mera-public/privacy отдаёт 301 на короткий
|
||
# (у страницы один канонический адрес, старые ссылки не ломаются).
|
||
# 1c. Короткие адреса /oferta, /refund, /privacy отдают 200 — эти URL
|
||
# напечатаны внутри самих юридических документов и уходят эквайеру.
|
||
# 1d. /estimate отдаёт 200 — экран проверки, куда ведут все кнопки лэндинга.
|
||
# 1e. Длинные адреса поддерева отдают 301 на короткие (включая ГОЛЫЙ
|
||
# /trade-in/mera-public — прежний матчер его не ловил, «Главная» в подвале
|
||
# вела в 404).
|
||
# 2d. Публичный API /api/public/mera/* доступен анонимно, а /api/v1/* на
|
||
# публичном домене по-прежнему 404.
|
||
# 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.
|
||
# 5b. www-формы всех трёх доменов МЕРА — тоже 301 (без своих site-блоков
|
||
# Caddy не выпускает сертификат, и клиент видит обрыв TLS, а не редирект).
|
||
#
|
||
# ВАЖНО: проверки 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_any: PASS, если код ответа — ЛЮБОЙ из перечисленных.
|
||
#
|
||
# Нужен там, где проверяемое свойство — «этого адреса наружу нет», а каким
|
||
# именно отказом это выражено, зависит от того, какой рубеж ответил первым.
|
||
# На gendsgn.ru таких рубежа два: пилотный basic_auth (401) стоит выше
|
||
# allowlist'а site-блока (404), и какой сработает — вопрос порядка директив, а
|
||
# не безопасности. Фиксировать один конкретный код значило бы ронять смоук при
|
||
# снятии пилотного гейта, то есть при изменении, которое к предмету проверки
|
||
# отношения не имеет.
|
||
check_any() {
|
||
local desc="$1" url="$2"
|
||
shift 2
|
||
local code expected="$*"
|
||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$url" 2>/dev/null)
|
||
for want in "$@"; do
|
||
if [ "$code" = "$want" ]; then
|
||
echo "PASS: $desc ($url -> $code)"
|
||
return
|
||
fi
|
||
done
|
||
echo "FAIL: $desc ($url -> got '${code:-<no response>}', expected one of: $expected)"
|
||
fail=1
|
||
}
|
||
|
||
check_post() {
|
||
local desc="$1" url="$2" body="$3" expected="$4" reject="${5:-}"
|
||
local out code head_and_body
|
||
# -i: заголовки попадают в вывод вместе с телом — по ним отличаем ответ
|
||
# приложения от заглушки Caddy (см. $reject у вызова payments/notify).
|
||
out=$(curl -s -i -w '\n%{http_code}' --max-time 15 \
|
||
-X POST -H 'Content-Type: application/json' -d "$body" "$url" 2>/dev/null)
|
||
code=${out##*$'\n'}
|
||
head_and_body=${out%$'\n'*}
|
||
if [ "$code" != "$expected" ]; then
|
||
echo "FAIL: $desc ($url -> got '${code:-<no response>}', expected $expected)"
|
||
fail=1
|
||
elif [ -n "$reject" ] && printf '%s' "$head_and_body" | grep -qEi "$reject"; then
|
||
echo "FAIL: $desc ($url -> $code, но ответ от заглушки окна деплоя, а не от приложения)"
|
||
fail=1
|
||
else
|
||
echo "PASS: $desc ($url -> $code)"
|
||
fi
|
||
}
|
||
|
||
# check_redirect_location: сверяет буквальный заголовок Location редиректа,
|
||
# а не только код ответа — нужен там, где регресс не меняет код (301
|
||
# остаётся 301), а меняет ТОЛЬКО цель (баг 10.09.2026 у @meraShortSlash в
|
||
# apps.caddy: query терялась молча, код ответа был зелёным всю дорогу).
|
||
# curl без -L (редирект не проходим), -D - дампит заголовки в stdout.
|
||
#
|
||
# ФОРМА LOCATION НЕ ФИКСИРУЕТСЯ. Caddy отдаёт цель редиректа так, как её
|
||
# собрала директива: для `redir * {uri}` это относительный путь
|
||
# (`/articles?utm_source=vc`), а для веток с явным хостом — абсолютный URL
|
||
# (`https://meraocenka.ru/articles?utm_source=vc`). Обе формы валидны по
|
||
# RFC 7231, браузер разрешает относительную сам, и переписывание одной ветки
|
||
# конфига на другую — не регресс, ради которого стоит ронять смоук. Поэтому
|
||
# сверяется ХВОСТ: путь с query, с какого бы префикса Location ни начинался.
|
||
# Ровно это и есть предмет проверки — что query дожила до цели.
|
||
check_redirect_location() {
|
||
local desc="$1" url="$2" expected_suffix="$3"
|
||
local location
|
||
location=$(curl -s -o /dev/null -D - --max-time 15 "$url" 2>/dev/null \
|
||
| grep -i '^location:' | tr -d '\r' | sed 's/^[Ll]ocation: *//')
|
||
case "$location" in
|
||
"$expected_suffix"|*"$expected_suffix")
|
||
echo "PASS: $desc ($url -> Location: $location)"
|
||
;;
|
||
*)
|
||
echo "FAIL: $desc ($url -> got Location '${location:-<none>}', expected ending with '$expected_suffix')"
|
||
fail=1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
echo "== МЕРА B2C perimeter smoke (ЭТАП 1) =="
|
||
|
||
# 1. Публичный домен отдаёт 200 анонимно.
|
||
check "meraocenka.ru root — public 200" "$BASE_MERA/" 200
|
||
|
||
# 1b. Подстраница лэндинга по ДЛИННОМУ адресу теперь отдаёт 301 на короткий, а
|
||
# не 200: с 15.08.2026 у публичной страницы один канонический адрес.
|
||
# Проверка осталась именно здесь, потому что раньше она сторожила
|
||
# доступность обязательного по 152-ФЗ документа — теперь сторожит, что при
|
||
# переходе на короткие адреса длинные не превратились в 404 (тогда бы
|
||
# сломались уже разосланные ссылки).
|
||
check "meraocenka.ru длинная privacy — 301 на короткую" "$BASE_MERA/trade-in/mera-public/privacy" 301
|
||
|
||
# 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
|
||
|
||
# 1d. Экран проверки квартиры — короткий адрес, на который ведут все кнопки
|
||
# лэндинга. Отвалится handle — кнопки «Проверить» станут ссылками в 404.
|
||
check "meraocenka.ru/estimate — public 200" "$BASE_MERA/estimate" 200
|
||
|
||
# 1d2. Страница «МЕРА для бизнеса» — короткий адрес, на который ведут пункт
|
||
# шапки и подвала. До 31.08.2026 они вели в закрытый контур и приводили
|
||
# человека на форму входа; отвалится handle — вернётся 404 вместо неё.
|
||
check "meraocenka.ru/business — public 200" "$BASE_MERA/business" 200
|
||
|
||
# 1d3. robots.txt / sitemap.xml — первое, что запрашивает поисковый краулер
|
||
# при индексации. robots.txt отдаётся ОТДЕЛЬНЫМ handle (Next-конвенция:
|
||
# файл в корне app/, а не под mera-public/), sitemap.xml — той же строкой
|
||
# что и остальные @meraPages (см. комментарии в apps.caddy). Отвалится
|
||
# любой из handle — сайт не проиндексируется вовсе.
|
||
check "meraocenka.ru/robots.txt — public 200" "$BASE_MERA/robots.txt" 200
|
||
check "meraocenka.ru/sitemap.xml — public 200" "$BASE_MERA/sitemap.xml" 200
|
||
|
||
# 1e. Длинные адреса поддерева отдают 301 на короткие: у страницы один
|
||
# канонический адрес, а старые ссылки и закладки продолжают работать.
|
||
# ГОЛЫЙ /trade-in/mera-public — регресс на баг 15.08.2026: прежний матчер
|
||
# `/trade-in/mera-public/*` эту форму не ловил, и ссылка «Главная» в
|
||
# подвале v3 вела в 404.
|
||
check "meraocenka.ru длинный корень — 301 на /" "$BASE_MERA/trade-in/mera-public" 301
|
||
check "meraocenka.ru длинная оферта — 301 на /oferta" "$BASE_MERA/trade-in/mera-public/oferta" 301
|
||
|
||
# 1f. Регресс-тест на баг 10.09.2026 (замер на живом проде): редирект со
|
||
# слэшем терял query-строку — статьи публикуются с UTM-метками, а
|
||
# мессенджеры и автолинкификаторы дописывают слэш к скопированной ссылке,
|
||
# то есть именно этот трафик терял атрибуцию молча (код ответа
|
||
# оставался 301, поэтому предыдущая версия смоука проблему не ловила).
|
||
# Проверяем сам факт 301 (уже покрыт проверками check выше по коду) И
|
||
# буквальный Location — только вторая половина ловит регресс.
|
||
check_redirect_location "meraocenka.ru/articles/?utm_source=vc — query сохраняется на редиректе" \
|
||
"$BASE_MERA/articles/?utm_source=vc" "/articles?utm_source=vc"
|
||
|
||
# 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
|
||
|
||
# 2c-bis. Внутри разрешённой статики закрыто поддерево ЧУЖИХ маршрутов (#3324):
|
||
# App Router кладёт код постранично в chunks/app/<маршрут>/, и до этой
|
||
# правки аноним скачивал с публичного домена бандлы /admin, /team,
|
||
# /scrapers — с именами внутренних ручек внутри.
|
||
#
|
||
# КОД 404 ЗДЕСЬ НЕДОСТАТОЧЕН: несуществующий чанк Next тоже отдаёт 404,
|
||
# поэтому проверка не отличила бы «Caddy отсёк» от «Caddy проксировал, а
|
||
# файла нет» — и осталась бы зелёной после отката матчера. Отличаем по
|
||
# ТЕЛУ: `respond 404` Caddy пустой (0 байт), 404 от Next — непустой
|
||
# (замер на проде 02.09.2026: 9 байт).
|
||
check_caddy_404() {
|
||
local desc="$1" url="$2"
|
||
local out code size
|
||
out=$(curl -s -o /dev/null -w '%{http_code} %{size_download}' --max-time 15 "$url" 2>/dev/null)
|
||
code=${out%% *}
|
||
size=${out##* }
|
||
if [ "$code" = "404" ] && [ "$size" = "0" ]; then
|
||
echo "PASS: $desc ($url -> 404, пустое тело = отсёк Caddy)"
|
||
else
|
||
echo "FAIL: $desc ($url -> got '${out:-<no response>}', expected '404 0')"
|
||
fail=1
|
||
fi
|
||
}
|
||
|
||
check_caddy_404 "meraocenka.ru — чанки /admin не раздаются" \
|
||
"$BASE_MERA/trade-in/_next/static/chunks/app/admin/page-smoke.js"
|
||
check_caddy_404 "meraocenka.ru — чанки /admin/analytics не раздаются" \
|
||
"$BASE_MERA/trade-in/_next/static/chunks/app/admin/analytics/page-smoke.js"
|
||
check_caddy_404 "meraocenka.ru — чанки /team не раздаются" \
|
||
"$BASE_MERA/trade-in/_next/static/chunks/app/team/page-smoke.js"
|
||
|
||
# Обратная сторона того же матчера: статика САМОГО лэндинга обязана остаться
|
||
# живой. Без этой строки «починка» вида «404 на весь chunks/app/» выглядела бы
|
||
# успешной, а публичный сайт молча остался бы без JS.
|
||
layout_chunk=$(curl -s --max-time 15 "$BASE_MERA/" 2>/dev/null \
|
||
| grep -o '/trade-in/_next/static/chunks/app/layout-[^"]*\.js' | head -1)
|
||
if [ -z "$layout_chunk" ]; then
|
||
# Пустая строка вместо пути дала бы запрос к корню и зелёную проверку ни о
|
||
# чём — поэтому это FAIL, а не «пропустим».
|
||
echo "FAIL: не нашёл layout-чанк в HTML лэндинга (сам лэндинг сломан?)"
|
||
fail=1
|
||
else
|
||
check "meraocenka.ru — корневой layout-чанк лэндинга жив (200)" "$BASE_MERA$layout_chunk" 200
|
||
fi
|
||
|
||
# 2d. Публичный API МЕРЫ (#2911). Ровно две ручки под /api/public/mera/*
|
||
# доступны анонимно на обоих доменах; ВЕСЬ /api/v1/* на публичном домене
|
||
# по-прежнему 404.
|
||
#
|
||
# Пара проверок ниже неразделима: первая доказывает, что форма вообще
|
||
# работает, вторая — что новый handle не расширил периметр до
|
||
# `/trade-in/api/*`. Зелёная только первая = API открыт целиком и тест это
|
||
# пропустил (ровно та ошибка, ради которой в Caddyfile выбран отдельный
|
||
# префикс, а не поимённый проброс v1-путей).
|
||
# ПРО ВНЕШНЮЮ ЗАВИСИМОСТЬ (#2917). Опасение «упадёт DaData — покраснеет
|
||
# смоук без всякого регресса» проверено по коду и оказалось у́же, чем
|
||
# звучит: `geocoder.suggest` — это цепочка «кадастровый тир → DaData →
|
||
# Nominatim → []», и КАЖДЫЙ внешний тир обёрнут в `except Exception`
|
||
# (services/geocoder.py). Отказ, квота и 5xx провайдера дают пустой список
|
||
# и HTTP 200 — проверка остаётся зелёной. Покраснеть она может только если
|
||
# провайдер ВИСНЕТ дольше 15 с (--max-time у curl), то есть на зависании,
|
||
# а не на отказе. Ослаблять ожидание не стали: 200 здесь проверяет
|
||
# открытость пути анониму, ради которой проверка и написана.
|
||
check_post "meraocenka.ru public suggest — 200 anonymous" "$BASE_MERA/trade-in/api/public/mera/suggest" '{"q":"Малышева"}' 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=test" 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_post "gendsgn.ru public suggest — 200 anonymous" "$BASE_MAIN/trade-in/api/public/mera/suggest" '{"q":"Малышева"}' 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
|
||
# Путь именно `/api/v1/trade-in/history`: роутер trade_in подключён в main.py с
|
||
# префиксом `/api/v1/trade-in`, а сама ручка объявлена как `@router.get("/history")`.
|
||
# До 05.09.2026 здесь стоял несуществующий `/api/v1/history` — проверка была
|
||
# зелёной только потому, что guard отвечал 401 на ЛЮБОЙ путь; после #3352
|
||
# несуществующий путь даёт 404, и проверка честно покраснела.
|
||
check "trade-in /api/v1/trade-in/history — 401 anonymous (чужие оценки)" "$BASE_MAIN/trade-in/api/v1/trade-in/history" 401
|
||
# #3360: admin-префикс анониму отвечает 404, а НЕ 401. Периметр здесь не срезан
|
||
# (Caddy-блок /trade-in/api/* стоит выше basic_auth-снипета, и срезать нельзя —
|
||
# admin-UI зовёт эти же пути из браузера), поэтому существование ручки прячет сам
|
||
# guard. Пара путей взята намеренно разная: `/proxies` — РЕАЛЬНЫЙ роут
|
||
# (app/api/v1/admin.py), `/users` — несуществующий. Одинаковый код на обоих и
|
||
# означает, что перебором имён admin-API снаружи ничего не узнать; 401 на первом
|
||
# = регресс маскировки (app/core/rbac.py::_unauthenticated).
|
||
check "trade-in /api/v1/admin/proxies — 404 anonymous (существующая ручка скрыта)" \
|
||
"$BASE_MAIN/trade-in/api/v1/admin/proxies" 404
|
||
check "trade-in /api/v1/admin/users — 404 anonymous (несуществующая — тот же ответ)" \
|
||
"$BASE_MAIN/trade-in/api/v1/admin/users" 404
|
||
|
||
# 3b. sitemap.xml публичного лэндинга МЕРЫ не должен просачиваться через
|
||
# закрытый B2B-контур gendsgn.ru — корень gendsgn.ru принадлежит Site
|
||
# Finder, у него нет своего route на /sitemap.xml под МЕРУ, allowlist-by-
|
||
# default этого site-блока (как и у meraocenka.ru) отдаёт 404 на всё
|
||
# незаявленное.
|
||
#
|
||
# ДВА ДОПУСТИМЫХ КОДА, А НЕ ОДИН. Первая редакция этой проверки ждала 404 и
|
||
# упала на первом же прогоне против прода: корень gendsgn.ru закрыт
|
||
# пилотным basic_auth, и гейт отвечает 401 РАНЬШЕ, чем запрос доходит до
|
||
# allowlist'а. Оба ответа означают ровно проверяемое — карты сайта МЕРЫ на
|
||
# B2B-домене нет; какой именно рубеж сработал первым, к предмету проверки
|
||
# отношения не имеет, а вот снятие пилотного гейта (оно запланировано)
|
||
# переключит 401 на 404 и уронило бы жёсткое ожидание на пустом месте.
|
||
# Красная строка здесь — только 200: он означал бы, что sitemap реально
|
||
# отдаётся из закрытого контура.
|
||
#
|
||
# Если у Site Finder когда-нибудь появится СВОЙ /sitemap.xml, ожидание
|
||
# придётся пересмотреть на «200 с другим содержимым».
|
||
check_any "gendsgn.ru/sitemap.xml — must NOT serve MERA sitemap (закрытый контур)" "$BASE_MAIN/sitemap.xml" 401 404
|
||
|
||
# 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
|
||
|
||
# 5b. www-формы — та же проверка, отдельным блоком: их отсутствие проявляется
|
||
# НЕ как 404, а как обрыв TLS (curl вернёт пустой код). До 27.08 site-блоков
|
||
# под www.mera* не было, A-записи при этом существовали, и «www.» в адресной
|
||
# строке давал «сайт недоступен». Держим проверку, чтобы регресс был виден
|
||
# здесь, а не в жалобе клиента.
|
||
check "www.meraocenka.ru — 301 to canonical" "https://www.meraocenka.ru/" 301
|
||
check "www.merahome.ru — 301 to canonical" "https://www.merahome.ru/" 301
|
||
check "www.meraotsenka.ru — 301 to canonical" "https://www.meraotsenka.ru/" 301
|
||
|
||
# 6. Платёжный периметр. Заведён в PR-D2 как канарейка «ожидания обновляют
|
||
# вместе с тем PR, который путь открывает». Канарейка отработала: PR-D3
|
||
# (#3231, роутер) смержен 29.08 и внёс notify в `_PUBLIC_PATHS`, ожидание
|
||
# здесь обновлено тем же днём — но уже ПОСЛЕ красного деплоя, а не вместе с
|
||
# ним. Кто будет делать PR-D4 — правьте ожидания в одном PR с ним.
|
||
#
|
||
# Состояние на 29.08, чего ждём сейчас:
|
||
# - meraocenka.ru вообще не проксирует /trade-in/api/* (allowlist-by-default,
|
||
# см. проверку 2) — 404 от Caddy, до бэкенда не доходит. Это ВСЁ ЕЩЁ
|
||
# канарейка: PR-D4 не смержен, 404 должен держаться;
|
||
# - gendsgn.ru: `checkout` по-прежнему требует X-Authenticated-User → 401
|
||
# анониму (он не публичный и не станет им);
|
||
# - gendsgn.ru: `notify` — публичный по построению (вебхук банка), см.
|
||
# комментарий у самой проверки ниже.
|
||
check "meraocenka.ru payments/notify — must 404 (Caddy не проксирует, PR-D4)" \
|
||
"$BASE_MERA/trade-in/api/v1/trade-in/payments/notify" 404
|
||
check "meraocenka.ru payments/checkout — must 404 (Caddy не проксирует, PR-D4)" \
|
||
"$BASE_MERA/trade-in/api/v1/trade-in/payments/checkout" 404
|
||
# notify — ЕДИНСТВЕННЫЙ платёжный путь, который PR-D3 (#3231) открыл анониму
|
||
# осознанно: это вебхук банка, он обязан быть достижим без наших заголовков, и
|
||
# потому внесён в `_PUBLIC_PATHS`. Канарейка выше отработала как задумано — 401
|
||
# здесь стал бы теперь признаком ПОЛОМКИ (банк получил бы отказ), а не защиты.
|
||
#
|
||
# Проверяем POST и ждём 503: это строже прежнего 401 — утверждает сразу, что
|
||
# маршрут существует (не 404 — старый образ) И что приём платежей выключен
|
||
# (`payments_enabled=False`). 200 здесь означал бы, что флаг включили, не тронув
|
||
# этот смоук. GET → 405 закрепляет, что путь принимает только POST.
|
||
#
|
||
# С #3274 голый код 503 больше не доказывает ничего: Caddy сам отдаёт 503 на
|
||
# окне деплоя (`handle_errors` -> caddy/deploy-window.caddy.snippet), и тогда
|
||
# запрос до приложения не дошёл вовсе. Отличаем по признакам заглушки —
|
||
# заголовок `Retry-After: 30` и `"error":"service_unavailable"` в теле; у
|
||
# приложения тело `{"detail":"payments are disabled"}` и никакого Retry-After.
|
||
# Совпал код, но пришла заглушка → FAIL, а не молчаливый PASS.
|
||
check_post "trade-in payments/notify — 503 anonymous (маршрут есть, приём выключен)" \
|
||
"$BASE_MAIN/trade-in/api/v1/trade-in/payments/notify" '{}' 503 \
|
||
'service_unavailable|^retry-after: *30'
|
||
check "trade-in payments/notify — 405 на GET (только POST)" \
|
||
"$BASE_MAIN/trade-in/api/v1/trade-in/payments/notify" 405
|
||
check "trade-in payments/checkout — 401 anonymous (не публичный по построению)" \
|
||
"$BASE_MAIN/trade-in/api/v1/trade-in/payments/checkout" 401
|
||
|
||
echo "========================================"
|
||
if [ "$fail" -eq 0 ]; then
|
||
echo "ALL CHECKS PASSED"
|
||
else
|
||
echo "SOME CHECKS FAILED — see FAIL lines above"
|
||
fi
|
||
|
||
exit "$fail"
|