gendesign/tradein-mvp/backend/tests/test_msk_region_scope_guard.py
lekss361 6da7de06a4
All checks were successful
Deploy Trade-In / changes (push) Successful in 15s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m50s
Deploy Trade-In / build-backend (push) Successful in 1m49s
Deploy Trade-In / deploy (push) Successful in 4m41s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 1m44s
Черновик регулярного сбора по Москве и области: якоря, ID площадок, гео-профили ДомКлика (#3533)
ДомКлик: DomClickGeoProfile вместо зашитых _EKB_ADDRESS_GUID/_EKB_AREA_ID; GUID Москвы
и области проверены живьём, aids вне ЕКБ не нужен, гард по bbox профиля. Страница
прогрева 77/50 — апексный domclick.ru: субдомены msk./moskovskaya-oblast. отдают 301.

Циан/Авито/Яндекс: CityLocation.cian_host, три новых скоупа (moskva, moskovskaya_oblast,
moskva_i_mo), avito_slug_is_region, city=NULL у мультигородских скоупов. Неизвестный слаг
теперь падает с ValueError вместо молчаливого отката на Екатеринбург.

Якоря: Москва — сетка 25 точек под radius_m=8000; область — 22 города-спутника
(10 добраны из Nominatim) плюс 22 кластера лот-массы. Замер на проде: города радиусом
10 км дают 73.8% лот-массы области, вместе с кластерами — 95.2%.

Ценовые коридоры: планировщик plan_price_corridors со статистикой усечения плюс
BisectionStats в живом движке. Провайдеры их пока не передают — отдельный заход.

Расписаний scrape_schedules для 77/50 в этом PR нет: они пойдут после первого ручного
прогона, подтверждающего живость профилей.
2026-09-15 17:51:30 +00:00

94 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Гард гео-скоупа свипов: `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