gendesign/tradein-mvp/browser/test_server_recycle.py
bot-backend 09bdd7888b
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 19s
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / browser-tests (pull_request) Successful in 1m10s
CI Trade-In / backend-tests (pull_request) Successful in 5m23s
fix(tradein/domclick): перезапуск браузера на карточку уничтожал пропуск QRATOR (#3212)
ДомКлик закрыт QRATOR с proof-of-work: первый запрос отдаёт 401 и заглушку с
задачей, браузер её решает, дёргает /__qrator/validate и получает пропуск в куках
(qrator_jsid2 + qrator_jsr). Пропуск живёт в cookie jar, то есть в browser
context'е — а прод брал каждую карточку в новом контексте, выбрасывая его.
Повторная валидация с того же IP получала 403 и страницу bot-mitigation.

Замер (прод, прод-прокси, по 6 карточек):
  общий контекст, одна вкладка       6/6, validate не вызывался ни разу
  общий контекст, вкладка на карточку 6/6, validate не вызывался ни разу
  новый контекст на карточку (= прод) 1/6, validate = [304, 403] на каждой
  боевой путь /fetch с reuse_context  6/6 при 0 перезапусков и 1 контексте

Что правится:
  * снят код-дефолт domclick=1 из #3205: перезапуск процесса гарантированно
    уничтожает контекст, то есть лечил симптом, который сам же и создавал.
    Ручка per-provider и domclick как отдельный провайдер остаются;
  * сброс контекста в бэкфилле был на КАЖДЫЙ блок — стал один раз за прогон.
    Это и объясняет провал #3193: сброс выбрасывал пропуск, следующий фетч
    блокировался гарантированно, что снова вызывало сброс. Приёмка тогда дала
    ровно 1 успех из 10;
  * снята неверная формулировка «отказ, а не челлендж» из #3204/#3205 —
    26 624 байта это РЕЗУЛЬТАТ проваленного PoW, а не статика вместо него.
    Ошибка вышла из метода: HTML читали на 4.5-й секунде и не смотрели в сеть.

Замер 6/6, которым обосновывали #3205, был испорчен: в логах сайдкара после
каждой страницы стоит «recycle threshold (1) достигнут, перезапуск браузера».

Тесты: два кодировали domclick=1 — переписаны через подставной словарь, чтобы
уровень «код-дефолт поставщика» продолжал проверяться, а не исчез вместе с
записью. Тест сброса требует ровно одну попытку за прогон.
159 passed (сайдкар), 4972 passed / 37 skipped (backend).
2026-08-29 14:09:23 +03:00

91 lines
4.9 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.

"""test_server_recycle.py — юниты для per-provider recycle_pages threshold (#3205).
Проверяет ``_resolve_recycle_pages``:
1. per-provider env перебивает глобальный env;
2. глобальный env перебивает код-дефолт per-provider;
3. невалидное (не int) значение → фолбэк, без исключения;
4. значение <1 → приводится к 1;
5. код-дефолт per-provider перебивает общий фолбэк (#3212: словарь
_RECYCLE_PAGES_DEFAULT_BY_PROVIDER сейчас ПУСТ — domclick=1 снят, потому что
перезапуск процесса уничтожал пропуск QRATOR; уровень приоритета остался и
проверяется через подставной словарь, а не через живого поставщика);
6. все поставщики без env → общий фолбэк 15.
camoufox/Playwright НЕ поднимается: тестируется чистая функция резолвера.
Запуск (из tradein-mvp/browser/)::
python -m pytest test_server_recycle.py -q
"""
from __future__ import annotations
import importlib.util
from pathlib import Path
# server.py — не пакет (отдельный сервис без __init__/pyproject). Грузим по пути.
_SERVER_PATH = Path(__file__).resolve().parent / "server.py"
_spec = importlib.util.spec_from_file_location("tradein_browser_server", _SERVER_PATH)
assert _spec is not None and _spec.loader is not None
server = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(server)
def test_per_provider_env_overrides_global() -> None:
"""BROWSER_RECYCLE_PAGES_DOMCLICK=3 + глобал=10 → domclick=3."""
fake_env = {"BROWSER_RECYCLE_PAGES": "10", "BROWSER_RECYCLE_PAGES_DOMCLICK": "3"}
assert server._resolve_recycle_pages("domclick", fake_env) == 3
def test_global_env_overrides_code_default(monkeypatch) -> None:
"""Нет per-provider env, но задан глобальный → перебивает код-дефолт поставщика."""
monkeypatch.setattr(server, "_RECYCLE_PAGES_DEFAULT_BY_PROVIDER", {"domclick": 2})
fake_env = {"BROWSER_RECYCLE_PAGES": "7"}
assert server._resolve_recycle_pages("domclick", fake_env) == 7
def test_invalid_per_provider_env_falls_back() -> None:
"""Невалидный (не int) per-provider env → фолбэк на глобальный уровень."""
fake_env = {"BROWSER_RECYCLE_PAGES": "9", "BROWSER_RECYCLE_PAGES_CIAN": "abc"}
assert server._resolve_recycle_pages("cian", fake_env) == 9
def test_invalid_global_env_falls_back_to_code_default(monkeypatch) -> None:
"""Невалидный глобальный env → фолбэк на код-дефолт per-provider (без исключения)."""
monkeypatch.setattr(server, "_RECYCLE_PAGES_DEFAULT_BY_PROVIDER", {"domclick": 2})
fake_env = {"BROWSER_RECYCLE_PAGES": "not-a-number"}
assert server._resolve_recycle_pages("domclick", fake_env) == 2
assert server._resolve_recycle_pages("cian", fake_env) == 15
def test_value_below_one_clamped_to_one() -> None:
"""Явный env <1 недопустим → приводится к 1, а не проваливается дальше по цепочке."""
assert server._resolve_recycle_pages("avito", {"BROWSER_RECYCLE_PAGES_AVITO": "0"}) == 1
assert server._resolve_recycle_pages("cian", {"BROWSER_RECYCLE_PAGES": "-5"}) == 1
def test_code_default_overrides_fallback(monkeypatch) -> None:
"""Код-дефолт поставщика (когда он есть) сильнее общего фолбэка 15."""
monkeypatch.setattr(server, "_RECYCLE_PAGES_DEFAULT_BY_PROVIDER", {"domclick": 2})
assert server._resolve_recycle_pages("domclick", {}) == 2
assert server._resolve_recycle_pages("cian", {}) == 15
def test_domclick_is_not_restarted_per_page() -> None:
"""#3212: у domclick НЕТ код-дефолта 1 — перезапуск на карточку уничтожал пропуск.
Замер (прод, прод-прокси, 6 карточек): общий контекст без перезапуска — 6/6;
новый контекст на карточку — 1/6. Порог 1 гарантировал второе, а не первое.
"""
assert "domclick" not in server._RECYCLE_PAGES_DEFAULT_BY_PROVIDER
assert server._resolve_recycle_pages("domclick", {}) == 15
def test_all_providers_without_env_default_to_fifteen() -> None:
"""Без env все поставщики живут на общем фолбэке 15."""
for provider in ("avito", "cian", "yandex", "domclick"):
assert server._resolve_recycle_pages(provider, {}) == 15
def test_generic_without_env_defaults_to_fifteen() -> None:
assert server._resolve_recycle_pages("generic", {}) == 15