"""Offline smoke for scrape_pipeline. Mocks scrapers + DB session.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from app.services.scrape_pipeline import ( PipelineCounters, PipelineResult, run_avito_pipeline, ) from app.services.scrapers.base import ScrapedLot def _make_lot( item_id: str, house_url: str | None = None, price: int = 5_000_000, ) -> ScrapedLot: return ScrapedLot( source="avito", source_url=f"https://www.avito.ru/ekaterinburg/kvartiry/test_{item_id}", source_id=item_id, price_rub=price, address="Test address", house_source="avito" if house_url else None, house_ext_id=house_url.rstrip("/").split("/")[-1] if house_url else None, house_url=house_url, listing_segment="vtorichka", ) def test_counters_dataclass_default() -> None: c = PipelineCounters() assert c.lots_fetched == 0 assert c.unique_houses == 0 assert c.errors == [] @pytest.mark.asyncio async def test_pipeline_search_failure_graceful() -> None: """Если AvitoScraper.fetch_around raises — pipeline возвращает PipelineResult с errors, не падает наружу.""" mock_db = MagicMock() mock_scraper = MagicMock() mock_scraper.__aenter__ = AsyncMock(return_value=mock_scraper) mock_scraper.__aexit__ = AsyncMock(return_value=None) mock_scraper.fetch_around = AsyncMock(side_effect=RuntimeError("test net error")) with patch("app.services.scrape_pipeline.AvitoScraper", return_value=mock_scraper): result = await run_avito_pipeline( mock_db, 56.8, 60.6, 1000, enrich_houses=False, enrich_detail_top_n=0 ) assert isinstance(result, PipelineResult) assert result.counters.lots_fetched == 0 assert len(result.counters.errors) == 1 assert "search:" in result.counters.errors[0] @pytest.mark.asyncio async def test_pipeline_group_by_house_unique_set() -> None: """3 lots с house_url'ами (2 одинаковых, 1 уникальный) → unique_houses=2.""" mock_db = MagicMock() lots = [ _make_lot("1", "https://www.avito.ru/catalog/houses/ekb/test/100"), _make_lot("2", "https://www.avito.ru/catalog/houses/ekb/test/100"), # дубль _make_lot("3", "https://www.avito.ru/catalog/houses/ekb/other/200"), _make_lot("4", None), # без дома ] mock_scraper = MagicMock() mock_scraper.__aenter__ = AsyncMock(return_value=mock_scraper) mock_scraper.__aexit__ = AsyncMock(return_value=None) mock_scraper.fetch_around = AsyncMock(return_value=lots) with ( patch("app.services.scrape_pipeline.AvitoScraper", return_value=mock_scraper), patch("app.services.scrape_pipeline.save_listings", return_value=(4, 0)), ): result = await run_avito_pipeline( mock_db, 56.8, 60.6, 1000, enrich_houses=False, enrich_detail_top_n=0 ) assert result.counters.lots_fetched == 4 assert result.counters.lots_inserted == 4 assert result.counters.unique_houses == 2