gendesign/tradein-mvp/backend/tests/test_impersonate_single_source.py
bot-backend fe628a5e17 fix(tradein/scraper-kit): actualize avito fingerprint - impersonate/Sec-Fetch-Site/referer/cookie-check
Живой замер браузера 2026-08-21 разошёлся с тем, что шлёт прогретая сессия:

- impersonate="chrome120" был захардкожен в 9+ местах (avito/{serp,detail,imv,
  houses}.py, pipeline.py x4, cian/valuation.py, yandex/valuation.py) вместо
  единой DEFAULT_IMPERSONATE (providers/_base.py). Обновление до chrome146
  (макс. доступный профиль curl_cffi 0.15.0; alias "chrome" НЕ используется -
  едет сам при апгрейде библиотеки без ревью) теперь меняется в одном месте.
  Guard-тест backend/tests/test_impersonate_single_source.py грепает всё
  дерево scraper_kit на литерал "chrome120".

- DOCUMENT_HEADERS ставил Sec-Fetch-Site="none" на уровне сессии, а Referer
  добавлялся per-request (curl_cffi мёржит per-request headers поверх
  session-level) - живой Referer с "none" рядом не бывает у настоящего
  Chrome. Добавлен referer_headers() (Sec-Fetch-Site="cross-site") для
  caller'ов с чужедоменным Referer; avito warm-up (yandex/ya.ru -> avito)
  теперь его использует. Внутренний avito-search -> avito-detail Referer
  (fetch_detail, same-origin) НЕ тронут - отдельный явный комментарий почему.

- Referer прогрева заменён с yandex.ru на ya.ru (живой переход из выдачи
  даёт короткий домен). ysclid сознательно не добавлен - значение выпускает
  Яндекс, подделка хуже отсутствия.

- warm_up_session/research_in_session считали прогрев успешным по HTTP-
  статусу и отсутствию firewall-маркеров, не проверяя антибот-cookies
  (__zzatw-*/cfidsw-*, сняты с живого браузера) - detail-батч на такой
  "прогретой" сессии сжигал прокси на обречённых 403. Теперь поднимают
  AvitoWarmupCookiesMissingError (наследник AvitoBlockedError - существующие
  except-блоки/ban_kind/proxy-ротация в pipeline и backfill ловят без
  изменений).

Полный backend pytest suite зелёный (4672 passed), ruff check чист.

Refs #3034
2026-08-21 17:02:53 +03:00

67 lines
4 KiB
Python
Raw Permalink 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. Legacy-дубликаты вне kit (например
`app/services/cian_session.py`, ещё не мигрированный на kit, #2361 F4a) сюда
намеренно не входят — отдельный, более крупный периметр вне scope #3034.
"""
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"
_BANNED_LITERAL = "chrome120"
def test_scan_area_exists() -> None:
"""Область сканирования жива — иначе сторож зелен вхолостую (путь съехал)."""
assert _KIT_SRC.is_dir(), f"область сканирования съехала: {_KIT_SRC}"
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(_KIT_SRC))
for path in sorted(_KIT_SRC.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-"
"профиля. Прочитай константу вместо хардкода строки."
)