From 2220df874104ea2c7a83e6999f5ba23a666635ca Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 10 Sep 2026 17:43:03 +0300 Subject: [PATCH] =?UTF-8?q?fix(mera):=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=81=D0=BE=D0=B3=D0=BB=D0=B0=D1=81=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=9F=D0=94=D0=BD=20=D0=BE=D1=82=D1=81=D1=82=D0=B0=D0=BB=D0=B0?= =?UTF-8?q?=20=D0=BE=D1=82=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B9=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B0=D0=BA=D1=86=D0=B8=D0=B8=20=D0=BF=D0=BE=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D0=B8=D0=BA=D0=B8=20+=20=D1=81=D0=BC=D0=BE=D1=83=D0=BA?= =?UTF-8?q?=20=D0=B6=D0=B4=D0=B0=D0=BB=20=D0=BD=D0=B5=20=D1=82=D0=BE=D1=82?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ДВА ПОСЛЕДСТВИЯ #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 Claude-Session: https://claude.ai/code/session_01CiUFZ3rmTNpp3DRajUo8KQ --- scripts/smoke-mera-perimeter.sh | 44 +++++++++++++++++++++++--- tradein-mvp/backend/app/api/v1/lead.py | 13 ++++++-- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/scripts/smoke-mera-perimeter.sh b/scripts/smoke-mera-perimeter.sh index 700d0072..2df297be 100644 --- a/scripts/smoke-mera-perimeter.sh +++ b/scripts/smoke-mera-perimeter.sh @@ -51,6 +51,30 @@ check() { 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:-}', 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 @@ -278,11 +302,21 @@ check "trade-in /api/v1/admin/users — 404 anonymous (несуществующ # закрытый B2B-контур gendsgn.ru — корень gendsgn.ru принадлежит Site # Finder, у него нет своего route на /sitemap.xml под МЕРУ, allowlist-by- # default этого site-блока (как и у meraocenka.ru) отдаёт 404 на всё -# незаявленное. NB: не проверено вживую на проде (нет approval на prod- -# доступ в рамках этой сессии) — если у Site Finder когда-нибудь появится -# СВОЙ /sitemap.xml, ожидание 404 придётся пересмотреть на 200-с-другим- -# содержимым. -check "gendsgn.ru/sitemap.xml — must NOT serve MERA sitemap (закрытый контур)" "$BASE_MAIN/sitemap.xml" 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/* перехватывается diff --git a/tradein-mvp/backend/app/api/v1/lead.py b/tradein-mvp/backend/app/api/v1/lead.py index a0eb307a..653fd141 100644 --- a/tradein-mvp/backend/app/api/v1/lead.py +++ b/tradein-mvp/backend/app/api/v1/lead.py @@ -53,12 +53,19 @@ _PHONE_MAX_DIGITS = 15 # только в audit-лог (#2497 TODO, теперь закрыт). # # Значение = дата утверждения политики (PRIVACY_APPROVAL в frontend/src/app/ -# mera-public/content.ts: «приказом директора № 1 от 13 августа 2026 г.» → -# "2026-08-13"), а не дата этого коммита — версия обязана указывать на редакцию +# mera-public/content.ts: «приказом директора № 2 от 10 сентября 2026 г.» → +# "2026-09-10"), а не дата этого коммита — версия обязана указывать на редакцию # ДОКУМЕНТА, на который согласие фактически ссылается (чекбокс теперь линкует # именно на /mera-public/privacy). test_consent_text_frontend_sync.py проверяет # это соответствие автоматически, так что рассинхронизация здесь падает в CI. -_CONSENT_POLICY_VERSION = "2026-08-13" +# +# Редакция № 2 от 10.09.2026: в политику добавлен раздел 9 «Файлы cookie и +# веб-аналитика» (на публичный сайт поставлены Яндекс.Метрика и GA4), прежний +# раздел «Контакты» стал десятым. Согласия, собранные до этой даты, ссылаются +# на редакцию № 1 и остаются с версией "2026-08-13" — в этом и смысл хранить +# версию per-row: снимок согласия должен указывать на тот документ, который +# человек видел, а не на текущий. +_CONSENT_POLICY_VERSION = "2026-09-10" # Снимок точного текста согласия, который видит пользователь при отправке лида # (ПЛОСКИЙ текст — без разметки ссылки на политику, которая в LeadForm.tsx рядом -- 2.45.3