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
ДомКлик закрыт 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).
91 lines
4.9 KiB
Python
91 lines
4.9 KiB
Python
"""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
|