gendesign/tradein-mvp/backend/tests/test_impersonate_single_source.py
bot-backend cf3d4850e2
Some checks failed
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) Failing after 1m3s
fix(tradein/scrapers): три прод-пути остались на chrome120, пока kit ушёл на chrome146
#3034 свёл impersonate к единственной константе внутри scraper_kit и поднял
профиль до chrome146. Сторож литерала сканирует только пакет, а его докстрока
объявила остальное «отдельным периметром вне scope», сославшись на #2361 F4a.
Периметр не спящий — он ходит в сеть каждый день, а #2361 к тому моменту был
закрыт, то есть отсылка вела в никуда.

На chrome120 оставались:

  app/services/cian_session.py:164        верификация куки Циана
  app/services/yandex_address_backfill.py:153   бэкфилл адресов
  app/tasks/yandex_detail_backfill.py:303       detail-бэкфилл

Разрыв в 31 мажорную версию живёт в TLS-отпечатке (JA3/JA4), а не в строке
User-Agent, поэтому сменой прокси он не лечится.

ПРО ЦИАН ОТДЕЛЬНО. По #2673 оценка Циана мертва с 29 июня — «куки протухли,
ни одной новой строки 37 дней». Путь, которым проверяется живость этих куки,
всё это время представлялся площадке браузером двухлетней давности. Причину
этим не объявляю: утверждаю, что при таком отпечатке отличить «куки протухли»
от «нас узнали по рукопожатию» нечем.

ТЕСТ ЗАКРЕПЛЯЛ ДЕФЕКТ. test_cian_session прибивал chrome120 гвоздём: подъём
профиля в kit ронял бы этот тест, а «починкой» выглядел бы возврат к
устаревшему профилю. Теперь тест сверяется с DEFAULT_IMPERSONATE.

Сторож литерала расширен на backend/app — без этого периметр возвращается
молча, что уже один раз и произошло. Намеренно НЕ входят tests/fixtures/**
(номер профиля там — часть записи о том, чем снят фикстур-HTML) и scripts/**
(разовые инструменты, в прод-путях не участвуют).

Исторические замеры в комментариях сохранены как замеры: «curl_cffi с
kit-профилем (на момент замера — Chrome 120)» вместо переписывания истории.

Проверено: сканер сторожа на дереве даёт ноль нарушителей, на подсаженном
литерале краснеет; все изменённые модули компилируются.

Refs #3148
2026-08-27 18:18:05 +03:00

82 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Единственный источник правды для curl_cffi impersonate-профиля (#3034).
До этого фикса `impersonate="chrome120"` был захардкожен литералом в 9+ местах
scraper_kit (`providers/avito/{serp,detail,imv,houses}.py`,
`orchestration/pipeline.py` x4, `providers/cian/valuation.py`,
`providers/yandex/valuation.py`) — обновить TLS-профиль значило найти и
поправить их все вручную, и один пропущенный литерал молча гонял бы устаревший
профиль. Теперь единственный источник — `DEFAULT_IMPERSONATE`
(`providers/_base.py`); любой caller обязан читать константу.
ПОЧЕМУ ГРЕП, А НЕ AST. Инвариант — «строка `chrome120` не встречается нигде в
дереве» (ни в коде, ни в docstring/комментарии) — буквально то, что нужно
поймать: сменится профиль — не должно остаться ни одного места, которое надо
будет вспомнить и найти руками. AST увидел бы только строковые константы в
выражениях (kwarg-значения, docstring), но не поймал бы упоминание в
`#`-комментарии — а именно в комментарии литерал незаметнее всего переживёт
следующий рефактор.
Область — ДВЕ директории:
* `packages/scraper-kit/src/scraper_kit/` — пакет, где живёт `DEFAULT_IMPERSONATE`
и все providers;
* `backend/app/` — прод-код бэкенда. Добавлен после того, как выяснилось, что
первая волна #3034 вычистила kit, а три живых прод-пути остались на
`chrome120`: верификация куки Циана (`services/cian_session.py`) и два
яндексовых бэкфилла. Оговорка «legacy-периметр вне scope» разошлась с фактом:
периметр не спит, он ходит в сеть каждый день, и та же ссылка (#2361 F4a)
к тому моменту была закрыта, то есть отсылать было уже некуда.
НЕ входят: `tests/fixtures/**` (там номер профиля — часть записи о том, чем
снят фикстур-HTML, историю переписывать нельзя) и `scripts/**` (разовые
инструменты, в прод-путях не участвуют).
"""
from __future__ import annotations
from pathlib import Path
_BACKEND_ROOT = Path(__file__).resolve().parents[1]
_KIT_SRC = _BACKEND_ROOT.parent / "packages" / "scraper-kit" / "src" / "scraper_kit"
_APP_SRC = _BACKEND_ROOT / "app"
_SCAN_ROOTS = (_KIT_SRC, _APP_SRC)
_BANNED_LITERAL = "chrome120"
def test_scan_area_exists() -> None:
"""Обе области сканирования живы — иначе сторож зелен вхолостую (путь съехал)."""
for root in _SCAN_ROOTS:
assert root.is_dir(), f"область сканирования съехала: {root}"
def test_detector_actually_detects(tmp_path: Path) -> None:
"""Сторож обязан уметь краснеть на литерале — иначе он зелен вхолостую."""
probe = tmp_path / "probe.py"
probe.write_text('impersonate = "chrome120"\n', encoding="utf-8")
assert _BANNED_LITERAL in probe.read_text(encoding="utf-8")
clean = tmp_path / "clean.py"
clean.write_text(
"from scraper_kit.providers._base import DEFAULT_IMPERSONATE\n", encoding="utf-8"
)
assert _BANNED_LITERAL not in clean.read_text(encoding="utf-8")
def test_default_impersonate_is_the_only_chrome_profile_literal() -> None:
"""Ни один файл scraper_kit не содержит строку "chrome120" — ни в коде, ни в
docstring/комментарии. Единственное разрешённое место для конкретного номера
профиля — значение `DEFAULT_IMPERSONATE` в `providers/_base.py`.
"""
offenders = [
str(path.relative_to(_BACKEND_ROOT.parent))
for root in _SCAN_ROOTS
for path in sorted(root.rglob("*.py"))
if _BANNED_LITERAL in path.read_text(encoding="utf-8")
]
assert offenders == [], (
f"{offenders}: литерал {_BANNED_LITERAL!r} обходит DEFAULT_IMPERSONATE "
"(providers/_base.py) — единственный источник правды для impersonate-"
"профиля. Прочитай константу вместо хардкода строки."
)