"""Гард гео-скоупа свипов: `region_code` 77/50 не должен молча собирать Екатеринбург. Контекст: планировщик читает регион из строки расписания (#3515, `scheduler._resolve_region_code`), но гео-скоуп свипа задаётся НЕ им — у avito/cian/ yandex это anchors города (`CITY_ANCHORS`/`CITY_LOCATIONS`, сегодня только Свердловская обл.), а у domclick — профиль региона в BFF-URL (DOMCLICK_GEO_PROFILES: 66/77/50; код без профиля — падение). Заведи мы строку `*_city_sweep_moscow` с `{"region_code": 77}` — прогон собрал бы ЕКБ и проштамповал его московским регионом, не показав ни одной аномалии в счётчиках. Гард превращает эту тихую порчу в падение до первого запроса. """ from __future__ import annotations import inspect from collections.abc import Callable from typing import Any import pytest from scraper_kit.orchestration.pipeline import ( _require_region_scope, run_avito_city_sweep, run_cian_city_sweep, run_domclick_city_sweep, run_yandex_city_sweep, ) from scraper_kit.providers.domclick.serp import geo_profile_for_region _EKB_ANCHOR = [(56.8400, 60.6050, "Центр")] def test_default_region_without_anchors_passes() -> None: """ЕКБ-прогоны (anchors=None, регион по умолчанию 66) не трогаем.""" _require_region_scope("avito_city_sweep", 66, None) def test_default_region_with_city_anchors_passes() -> None: """Свердловские oblast-строки (#179/#262) продолжают работать как прежде.""" _require_region_scope("cian_city_sweep", 66, [(57.910, 59.980, "Н.Тагил центр")]) @pytest.mark.parametrize("region_code", [50, 77]) def test_foreign_region_without_anchors_raises(region_code: int) -> None: """Москва/МО без своих anchors — тот самый случай, что молча давал ЕКБ.""" with pytest.raises(ValueError, match=f"region_code={region_code}"): _require_region_scope("avito_city_sweep", region_code, None) @pytest.mark.parametrize("region_code", [50, 77]) def test_foreign_region_with_explicit_anchors_passes(region_code: int) -> None: """Явные anchors — явный гео-скоуп: запрещаем не регион, а молчаливый ЕКБ-fallback.""" _require_region_scope("avito_city_sweep", region_code, _EKB_ANCHOR) def _required_kwargs(fn: Callable[..., Any], **overrides: Any) -> dict[str, Any]: """None во все обязательные keyword-параметры: гард обязан сработать раньше их.""" kwargs: dict[str, Any] = { name: None for name, prm in inspect.signature(fn).parameters.items() if prm.kind is inspect.Parameter.KEYWORD_ONLY and prm.default is inspect.Parameter.empty } kwargs.update(overrides) return kwargs @pytest.mark.parametrize( "sweep", [run_avito_city_sweep, run_cian_city_sweep, run_yandex_city_sweep], ) async def test_anchor_sweeps_reject_foreign_region(sweep: Callable[..., Any]) -> None: """Гард стоит до первого обращения к БД/сети — поэтому db=None здесь безопасен.""" with pytest.raises(ValueError, match="region_code=77"): await sweep(None, **_required_kwargs(sweep, run_id=1, region_code=77, anchors=None)) @pytest.mark.parametrize("region_code", [1, 99]) async def test_domclick_sweep_rejects_region_without_profile(region_code: int) -> None: """У домклика anchors нет вовсе: гео-скоуп задаёт профиль региона (DOMCLICK_GEO_PROFILES: address GUID, aids, страница прогрева, bbox). 66/77/50 теперь поддержаны, а код без профиля по-прежнему падает до первого запроса: тихий fallback на ЕКБ — ровно та порча, от которой стережёт этот файл.""" with pytest.raises(ValueError, match=f"region_code={region_code}"): await run_domclick_city_sweep( None, # type: ignore[arg-type] **_required_kwargs(run_domclick_city_sweep, run_id=1, region_code=region_code), ) @pytest.mark.parametrize("region_code", [66, 77, 50]) def test_domclick_supported_regions_have_profiles(region_code: int) -> None: """Москва и область больше НЕ режутся: у них свой GUID и свой bbox.""" profile = geo_profile_for_region(region_code) assert profile.region_code == region_code assert profile.address_guid