"""Per-city SERP location URL builders (#12 fix EKB-leak в oblast city-sweep). Проблема: avito/cian/yandex провайдеры хардкодили ЕКБ-локацию (путь /ekaterinburg/, CIAN_EKB_REGION_ID, _EKB_RGID) мимо anchor'а oblast-города → city-sweep целевого города (напр. Н.Тагил) фактически скрейпил ЕКБ, n_analogs=0. Фикс — новые kw-параметры (avito: target_city_slug уже был #2487, теперь и в ПУТИ URL; cian: city_region_id; yandex: city_rgid), все дефолт None → ЕКБ-поведение byte-identical прежнему (back-compat critical). No network / no DB — чистые URL-билдеры на stub-конфиге. """ from __future__ import annotations import os from types import SimpleNamespace from urllib.parse import parse_qs, urlparse os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") from scraper_kit.providers.avito.serp import NOVOSTROYKA_SLUG, AvitoScraper from scraper_kit.providers.cian.serp import CIAN_EKB_REGION_ID, CianScraper from scraper_kit.providers.yandex.serp import _EKB_RGID, YandexRealtyScraper _STUB_CFG = SimpleNamespace() def _query(url: str) -> dict[str, list[str]]: return parse_qs(urlparse(url).query) # ── Avito: city-slug в ПУТЬ URL (не только SERP-фильтр) ──────────────────────── def test_avito_web_url_uses_target_city_slug_in_path() -> None: s = AvitoScraper(_STUB_CFG, target_city_slug="nizhniy_tagil") # type: ignore[arg-type] url = s._build_web_url(57.910, 59.980, radius_km=2, page=1) assert "/nizhniy_tagil/" in url assert "/ekaterinburg/" not in url def test_avito_citywide_url_uses_target_city_slug_in_path() -> None: s = AvitoScraper(_STUB_CFG, target_city_slug="kamensk-uralskiy") # type: ignore[arg-type] url = s._build_citywide_url(page=2) assert "/kamensk-uralskiy/" in url assert "/ekaterinburg/" not in url def test_avito_newbuilding_url_uses_target_city_slug_in_path() -> None: s = AvitoScraper(_STUB_CFG, target_city_slug="verhnyaya_pyshma") # type: ignore[arg-type] url = s._build_newbuilding_url(page=1) assert "/verhnyaya_pyshma/" in url assert "/ekaterinburg/" not in url assert NOVOSTROYKA_SLUG in url def test_avito_rooms_url_uses_target_city_slug_in_path() -> None: s = AvitoScraper(_STUB_CFG, target_city_slug="serov") # type: ignore[arg-type] url = s._build_rooms_url("2k", page=1) assert "/serov/" in url assert "/ekaterinburg/" not in url def test_avito_urls_default_to_ekaterinburg_when_no_target_city_slug() -> None: """Back-compat: target_city_slug=None → путь /ekaterinburg/ (ЕКБ full_load/pipeline).""" s = AvitoScraper(_STUB_CFG, target_city_slug=None) # type: ignore[arg-type] assert "/ekaterinburg/" in s._build_web_url(56.84, 60.60, radius_km=1, page=1) assert "/ekaterinburg/" in s._build_citywide_url(page=1) assert "/ekaterinburg/" in s._build_newbuilding_url(page=1) assert "/ekaterinburg/" in s._build_rooms_url("2k", page=1) # ── Cian: city_region_id → region= (subdomain НЕ трогается) ──────────────────── def test_cian_build_url_uses_city_region_id() -> None: s = CianScraper(_STUB_CFG, city_region_id=4886) # type: ignore[arg-type] url = s._build_url() assert _query(url)["region"] == ["4886"] assert urlparse(url).hostname == "ekb.cian.ru" def test_cian_build_url_defaults_to_ekb_region_when_no_city_region_id() -> None: """Back-compat: city_region_id=None → region=CIAN_EKB_REGION_ID (4743).""" s = CianScraper(_STUB_CFG, city_region_id=None) # type: ignore[arg-type] url = s._build_url() assert _query(url)["region"] == [str(CIAN_EKB_REGION_ID)] assert urlparse(url).hostname == "ekb.cian.ru" # ── Yandex: city_rgid → rgid= ─────────────────────────────────────────────────── def test_yandex_build_url_uses_city_rgid() -> None: s = YandexRealtyScraper(_STUB_CFG, city_rgid=328286) # type: ignore[arg-type] url = s._build_url() assert _query(url)["rgid"] == ["328286"] def test_yandex_build_url_defaults_to_ekb_rgid_when_no_city_rgid() -> None: """Back-compat: city_rgid=None → rgid=_EKB_RGID (559132).""" s = YandexRealtyScraper(_STUB_CFG, city_rgid=None) # type: ignore[arg-type] url = s._build_url() assert _query(url)["rgid"] == [str(_EKB_RGID)]