fix(mera): версия согласия ПДн отстала от новой редакции политики + смоук ждал не тот код
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
This commit is contained in:
bot-backend 2026-09-10 17:43:03 +03:00
parent aba07b581f
commit 2220df8741
2 changed files with 49 additions and 8 deletions

View file

@ -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:-<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
@ -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/* перехватывается

View file

@ -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 рядом