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
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).
137 lines
5.7 KiB
Python
137 lines
5.7 KiB
Python
"""#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"
|