"""#2616: POST /api/v1/admin/scrape (source=avito) прокидывает proxy_provider в AvitoScraper. Root cause (issue #2613/#2616): `AvitoScraper(config, delay_provider=get_scraper_delay)` в `scrape_around` не передавал `proxy_provider=proxy_provider`, хотя переменная уже вычислена в scope и передаётся в CianScraper/YandexRealtyScraper на соседних строках. Браузерный путь Avito (SCRAPER_FETCH_MODE=browser) в результате всегда шёл через мёртвый `BROWSER_PROXY_AVITO` env-прокси вместо пула. Offline: db/AvitoScraper/save_listings мокаются, NO live network/DB. """ from __future__ import annotations import os from typing import Any, ClassVar from unittest.mock import MagicMock, patch os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") import pytest from fastapi import FastAPI from fastapi.testclient import TestClient @pytest.fixture def client() -> TestClient: from app.api.v1 import admin as admin_module from app.core.db import get_db app = FastAPI() app.include_router(admin_module.router, prefix="/api/v1/admin") def fake_db() -> Any: yield MagicMock() app.dependency_overrides[get_db] = fake_db return TestClient(app) class _RecordingScraper: """Recording async-context-manager: пишет init-kwargs в общий список, fetch_around возвращает пустой список лотов (save_listings ниже мокнут, до БД не доходим).""" calls: ClassVar[list[dict[str, Any]]] = [] def __init__(self, config: Any, **kwargs: Any) -> None: type(self).calls.append({"config": config, **kwargs}) async def __aenter__(self) -> _RecordingScraper: return self async def __aexit__(self, *_args: object) -> None: return None async def fetch_around(self, *_args: Any, **_kwargs: Any) -> list[Any]: return [] def test_scrape_avito_passes_proxy_provider(client: TestClient) -> None: """source=['avito'] → AvitoScraper(config, ..., proxy_provider=). Falsification: если admin.py перестанет передавать proxy_provider= в AvitoScraper(...), calls[-1] не будет содержать ключ 'proxy_provider' со значением sentinel (либо будет None) — assert падает на VALUE (или KeyError на .get None-fallback), не на TypeError (recorder принимает **kwargs, сигнатуру не проверяет). """ sentinel = object() _RecordingScraper.calls = [] with ( patch("app.api.v1.admin.AvitoScraper", _RecordingScraper), patch("app.api.v1.admin._kit_proxy_provider", return_value=sentinel), patch("app.api.v1.admin.save_listings", return_value=(0, 0)), ): r = client.post( "/api/v1/admin/scrape", json={"lat": 56.8332, "lon": 60.5944, "radius_m": 1000, "sources": ["avito"]}, ) assert r.status_code == 200 assert len(_RecordingScraper.calls) == 1 assert _RecordingScraper.calls[0].get("proxy_provider") is sentinel def test_scrape_avito_no_pool_default_none(client: TestClient) -> None: """_kit_proxy_provider() → None (pool off, ship-dark default) → AvitoScraper получает proxy_provider=None — поведение прежнее. """ _RecordingScraper.calls = [] with ( patch("app.api.v1.admin.AvitoScraper", _RecordingScraper), patch("app.api.v1.admin._kit_proxy_provider", return_value=None), patch("app.api.v1.admin.save_listings", return_value=(0, 0)), ): r = client.post( "/api/v1/admin/scrape", json={"lat": 56.8332, "lon": 60.5944, "radius_m": 1000, "sources": ["avito"]}, ) assert r.status_code == 200 assert len(_RecordingScraper.calls) == 1 assert _RecordingScraper.calls[0].get("proxy_provider") is None