"""#2698 — домовая оценка Авито ходила в сайдкар без прокси пула, и причина отказа терялась. Два независимых дефекта одного пути (backfill_house_imv → BrowserFetcher → POST /fetch-json → tradein-browser): 1. `BrowserFetcher(source="avito", endpoint=...)` конструировался БЕЗ proxy_provider/use_pool/environment — единственный avito-путь без проводки пула (avito_city_sweep её подключает, orchestration/pipeline.py). Тело POST уходило без "proxy", сайдкар брал env-прокси SCRAPER_PROXY_URL = узел пула id=1 (provider_affinity='domclick'), который `proxy_pool.acquire('avito')` не выдал бы никогда. Прод 03.07-05.08: 35 отказов из 35 попыток в каждом прогоне при живом сайдкаре и работающих в те же дни sweep'ах. 2. Причина отказа приходила в теле ответа сайдкара ({"error": "browser unavailable (proxy may be down)"} / "Page.goto: NS_ERROR_PROXY_BAD_GATEWAY"), а `resp.raise_for_status()` её выбрасывал — в houses.imv_error_reason 34 дня лежал голый код статуса. Сеть/БД/камуфокс замоканы. """ from __future__ import annotations import os from typing import Any, ClassVar from unittest.mock import AsyncMock, MagicMock import httpx import pytest os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") from scraper_kit.browser_fetcher import BrowserFetcher from app.services import house_imv_backfill as hib # ── (1) проводка пула на пути домовой оценки ───────────────────────────────── class _CapturingFetcher: 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 async def test_backfill_browser_fetcher_gets_proxy_pool_wiring( monkeypatch: pytest.MonkeyPatch, ) -> None: """Прод-случай #2698: без этих трёх аргументов сайдкар уходил на env-прокси.""" _CapturingFetcher.captured = {} monkeypatch.setattr(hib.settings, "avito_imv_use_browser_fetcher", True) monkeypatch.setattr(hib.settings, "use_proxy_pool_browser", True) monkeypatch.setattr(hib.settings, "environment", "production") monkeypatch.setattr(hib, "BrowserFetcher", _CapturingFetcher) monkeypatch.setattr(hib, "_process_one_house", AsyncMock(return_value="ok")) db = MagicMock() db.execute.return_value.mappings.return_value.all.return_value = [ {"id": 1, "address": "ЕКБ, ул. X, 1", "full_address": None, "lat": 56.8, "lon": 60.6} ] await hib.backfill_house_imv(db, batch_size=1) captured = _CapturingFetcher.captured assert captured["source"] == "avito" assert captured["proxy_provider"] is not None, "без провайдера пул не подключится" assert captured["use_pool"] is True, "флаг пула должен доезжать до фетчера" # #2616 шаг 1: иначе прод-отказ «пул пуст» мёртв и мы молча уходим на env-прокси. assert captured["environment"] == "production" # ── (2) причина отказа сайдкара доезжает до вызывающего ────────────────────── def _client_returning(status: int, body: dict[str, Any], url: str) -> MagicMock: resp = httpx.Response(status, json=body, request=httpx.Request("POST", url)) client = MagicMock() client.post = AsyncMock(return_value=resp) client.aclose = AsyncMock(return_value=None) return client async def test_fetch_json_error_carries_sidecar_reason() -> None: """503 сайдкара: в тексте ошибки должна быть ПРИЧИНА, а не только код статуса.""" endpoint = "http://tradein-browser:3000" async with BrowserFetcher(source="avito", endpoint=endpoint) as bf: bf._client = _client_returning( # type: ignore[assignment] 503, {"error": "browser unavailable (proxy may be down)"}, f"{endpoint}/fetch-json" ) with pytest.raises(httpx.HTTPStatusError) as exc_info: await bf.fetch_json("https://www.avito.ru/web/1/coords/by_address?address=X") message = str(exc_info.value) assert "browser unavailable (proxy may be down)" in message assert "503" in message async def test_fetch_error_carries_sidecar_reason() -> None: """Тот же инвариант для /fetch — общий helper, а не заплатка на одном вызове.""" endpoint = "http://tradein-browser:3000" async with BrowserFetcher(source="avito", endpoint=endpoint) as bf: bf._client = _client_returning( # type: ignore[assignment] 500, {"error": "Error: Page.goto: NS_ERROR_PROXY_BAD_GATEWAY"}, f"{endpoint}/fetch" ) with pytest.raises(httpx.HTTPStatusError) as exc_info: await bf.fetch("https://www.avito.ru/evaluation/realty") assert "NS_ERROR_PROXY_BAD_GATEWAY" in str(exc_info.value)