gendesign/tradein-mvp/backend/tests/test_3197_domclick_proxy_pool_wiring.py
bot-backend fd95c962bb
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
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 11s
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 4m53s
fix(tradein/domclick): backfill ходил в сайдкар мимо прокси-пула (#3197 ч.1)
BrowserFetcher(source="domclick", reuse_context=True) конструировался без
proxy_provider/use_pool/environment -- тела POST /fetch не несли "proxy",
сайдкар брал свой env-прокси, и прогон шёл мимо пула целиком: ни выбора узла
по affinity, ни scrape_proxy_source_bans, ни ротации при блоке. Тот же дефект
уже чинили на avito_detail_backfill/house_imv_backfill (#2698) -- этот call
site оставался последним непочиненным. environment обязателен: без него
отказ «пул пуст» на этом пути мёртв (#2616 шаг 1). reuse_context=True
сохранён без изменений.

Заодно поправлен устаревший комментарий над конструктором: ссылался на
scrape_proxies.provider_affinity='domclick' и миграцию 173 -- на проде
такого больше нет (миграция 253 сняла резервацию узла, #2800), все четыре
включённых узла (id 1/9/10/11) имеют provider_affinity='any'.

test_3118_domclick_warm_context.py обновлён под новую сигнатуру вызова
(assert_called_once_with -> точечная проверка нужных kwargs).
2026-08-29 15:26:14 +03:00

137 lines
5.7 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.

"""#3197 (часть 1, Домклик) — backfill ходил в сайдкар мимо прокси-пула.
`BrowserFetcher(source="domclick", endpoint=..., reuse_context=True)` конструировался
БЕЗ `proxy_provider`/`use_pool`/`environment` — единственные три аргумента, которые
кладут "proxy" в тело POST /fetch (см. scraper_kit.browser_fetcher.BrowserFetcher.
_acquire_lease). Без них сайдкар брал свой env-прокси (`SCRAPER_PROXY_URL`), и прогон
шёл мимо пула целиком: ни выбора узла, ни `scrape_proxy_source_bans`, ни ротации при
блоке. Тот же дефект уже чинили на соседних путях — avito_detail_backfill (#2698,
см. test_2698_imv_sidecar_wiring.py) и house_imv_backfill (#2698) — этот call site
оставался последним непочиненным.
Сеть/БД/камуфокс замоканы; в сеть тест не ходит.
"""
from __future__ import annotations
import os
from datetime import UTC, datetime, timedelta
from typing import Any, ClassVar
from unittest.mock import AsyncMock, MagicMock, patch
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
import pytest
from app.tasks import domclick_detail_backfill as dcb
_RUNS = "app.tasks.domclick_detail_backfill.runs_mod"
_SESSION_SVC = "app.tasks.domclick_detail_backfill.domclick_session_svc"
_SLEEP = "app.tasks.domclick_detail_backfill.asyncio.sleep"
_FETCH = "app.tasks.domclick_detail_backfill.fetch_detail"
_SAVE = "app.tasks.domclick_detail_backfill.save_detail_enrichment"
class _CapturingFetcher:
"""Зеркалит _CapturingFetcher из test_2698_imv_sidecar_wiring.py."""
captured: ClassVar[dict[str, Any]] = {}
def __init__(self, **kwargs: Any) -> None:
_CapturingFetcher.captured = kwargs
async def __aenter__(self) -> _CapturingFetcher:
return self
async def __aexit__(self, *_: object) -> None:
return None
def request_context_reset(self) -> None:
return None
def _make_snapshot(n: int) -> list[dict]:
return [
{"id": i + 1, "source_url": f"https://ekaterinburg.domclick.ru/card/sale__flat__{i + 1}"}
for i in range(n)
]
def _mock_db(snapshot: list[dict]) -> MagicMock:
db = MagicMock()
sel = MagicMock()
sel.mappings.return_value.all.return_value = snapshot
db.execute.return_value = sel
return db
def _mock_session_svc() -> MagicMock:
svc = MagicMock()
svc.load_session.return_value = None
svc.COOKIE_EXPIRY_WARN_DAYS = 5
svc.session_expires_at.return_value = datetime.now(tz=UTC) + timedelta(days=30)
return svc
@pytest.mark.asyncio
async def test_browser_fetcher_gets_proxy_pool_wiring(monkeypatch: pytest.MonkeyPatch) -> None:
"""use_pool/proxy_provider/environment доезжают до BrowserFetcher из конфига."""
_CapturingFetcher.captured = {}
monkeypatch.setattr(dcb.settings, "use_proxy_pool_browser", True)
monkeypatch.setattr(dcb.settings, "environment", "production")
snapshot = _make_snapshot(1)
db = _mock_db(snapshot)
with (
patch(_SESSION_SVC, _mock_session_svc()),
patch(_RUNS, MagicMock()),
patch.object(dcb, "BrowserFetcher", _CapturingFetcher),
patch(_FETCH, AsyncMock(return_value=MagicMock())),
patch(_SAVE, return_value=True),
patch(_SLEEP, new_callable=AsyncMock),
):
await dcb.run_domclick_detail_backfill(
db, run_id=3197, params={"batch_size": 1, "budget_sec": 3600}
)
captured = _CapturingFetcher.captured
assert captured["source"] == "domclick"
assert captured["proxy_provider"] is not None, "без провайдера пул не подключится"
assert captured["use_pool"] is True, "флаг пула должен доезжать до фетчера из конфига"
# #2616 шаг 1: без него отказ «пул пуст» на этом пути мёртв.
assert captured["environment"] == "production"
# #3118: reuse_context остаётся включённым — QRATOR прогрев тратится один раз
# на весь прогон, а не на каждую карточку.
assert captured["reuse_context"] is True
@pytest.mark.asyncio
async def test_browser_fetcher_use_pool_follows_config_off(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""use_proxy_pool_browser=False (дефолт) -> use_pool=False, provider всё равно передан."""
_CapturingFetcher.captured = {}
monkeypatch.setattr(dcb.settings, "use_proxy_pool_browser", False)
monkeypatch.setattr(dcb.settings, "environment", "dev")
snapshot = _make_snapshot(1)
db = _mock_db(snapshot)
with (
patch(_SESSION_SVC, _mock_session_svc()),
patch(_RUNS, MagicMock()),
patch.object(dcb, "BrowserFetcher", _CapturingFetcher),
patch(_FETCH, AsyncMock(return_value=MagicMock())),
patch(_SAVE, return_value=True),
patch(_SLEEP, new_callable=AsyncMock),
):
await dcb.run_domclick_detail_backfill(
db, run_id=3197, params={"batch_size": 1, "budget_sec": 3600}
)
captured = _CapturingFetcher.captured
# proxy_provider/use_pool передаются ВМЕСТЕ (см. providers/_base.py::acquire) —
# при use_pool=False провайдер игнорируется, но параметр не должен пропадать из
# проводки: это то же самое call-site для dev и для прода.
assert captured["proxy_provider"] is not None
assert captured["use_pool"] is False
assert captured["environment"] == "dev"