All checks were successful
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 7m21s
Сбор ДомКлика по Москве пошёл (PR #3496), но переливать его в listings было нечем: в SOURCE_VIEWS стояли только Циан, Авито и Яндекс. Вью msk_raw.domclick_latest в проде уже существует, не хватало двух записей в словарях и распознавания города. ## Город читается первым компонентом адреса У Циана города в адресе нет вовсе и Москву выдаёт префикс округа; у Яндекса регион стоит вторым компонентом после страны. У ДомКлика адрес начинается прямо с города: «Москва, улица Нижние Мнёвники, 16 к2». Замер по 5 024 карточкам первого прохода: первый компонент имеет ровно одно значение — «Москва», областных нет ни одной. Это ожидаемо, сырьё собрано запросом с GUID-ом Москвы и отфильтровано по bbox ещё на стороне сборщика. Поэтому фильтр здесь — вторая линия, а не единственная. Сменится GUID в сборщике или приедет в ту же таблицу второй город — импорт не потащит его в Москву молча, а отложит в skipped_not_moscow. Новая Москва регион не ломает: «Москва, пос. Птичное, …», «Москва, x. Ильичевка, …» — посёлок стоит ВТОРЫМ компонентом, первым всегда город, что и соответствует коду 77. Сравнение идёт по компоненту целиком, а не по префиксу строки: иначе «Московская область» и «Московский» прошли бы как Москва. Оба случая закреплены тестом. ## Тесты Распознавание Москвы, включая две формы Новой Москвы; отбраковка области, Екатеринбурга, похожего на префикс «Московский», пустой строки и None; проводка источника в оба словаря. 45 passed, ruff чистый. Claude-Session: https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs
485 lines
23 KiB
Python
485 lines
23 KiB
Python
"""Тесты импорта московского сырья `msk_raw` в listings.
|
||
|
||
Изоляция — как у соседей (tests/tasks/test_backfill_house_coords_from_listings.py):
|
||
DATABASE_URL проставляется до импорта модуля, в сеть и в прод-БД тест не ходит.
|
||
Сессия — заглушка, отдающая заранее заготовленные payload'ы; писатель
|
||
(`save_listings`) подменяется monkeypatch'ем, так что проверяется ровно наш отбор
|
||
и пересчёт метки, а не поведение кита.
|
||
|
||
Главные риски задачи, которые тут и караулятся:
|
||
* в регион 77 уедет Московская область (адрес без округа);
|
||
* артефакт парсера («Екатеринбург (Cian)») растворится в «не Москва»;
|
||
* метка кита 'novostroyki' будет скопирована как есть и выкинет 29 000 карточек
|
||
из подбора аналогов (гвард estimator'а берёт только NULL/'vtorichka').
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from typing import Any
|
||
|
||
import pytest
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||
|
||
from app.tasks import msk_raw_import as mod
|
||
|
||
|
||
def _payload(**over: Any) -> dict[str, Any]:
|
||
"""Минимальный сериализованный ScrapedLot, как он лежит в msk_raw.payload."""
|
||
base: dict[str, Any] = {
|
||
"source": "cian",
|
||
"source_url": "https://www.cian.ru/sale/flat/330200428/",
|
||
"source_id": "330200428",
|
||
"address": "ЦАО, р-н Арбат, улица Арбат, 10",
|
||
"price_rub": 25_000_000,
|
||
"rooms": 2,
|
||
"area_m2": 55.0,
|
||
"listing_segment": "novostroyki", # метка кита — её и пересчитываем
|
||
"raw_payload": {"is_from_developer": False},
|
||
}
|
||
base.update(over)
|
||
return base
|
||
|
||
|
||
# ── отбор по адресу ──────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.parametrize("okrug", mod.MOSCOW_OKRUGS)
|
||
def test_every_okrug_prefix_is_moscow(okrug: str) -> None:
|
||
assert mod.is_moscow_address(f"{okrug}, р-н Хамовники, улица Льва Толстого, 16")
|
||
|
||
|
||
def test_oblast_address_without_okrug_is_not_moscow() -> None:
|
||
"""Адрес без округа — Московская область (регион 50), его в импорт не берём."""
|
||
assert not mod.is_moscow_address("Химки, микрорайон Новогорск, Ивановская улица, 5")
|
||
assert not mod.is_moscow_address("городской округ Мытищи, Олимпийский проспект, 29")
|
||
|
||
|
||
def test_okrug_must_be_a_whole_token() -> None:
|
||
"""«ЗАО» в начале улицы — не округ; иначе область просочилась бы в регион 77."""
|
||
assert not mod.is_moscow_address("ЗАОзёрная улица, 25")
|
||
|
||
|
||
def test_ekaterinburg_artifact_is_its_own_reason() -> None:
|
||
assert mod.is_artifact_address("Екатеринбург (Cian)")
|
||
assert not mod.is_moscow_address("Екатеринбург (Cian)")
|
||
|
||
|
||
# ── пересчёт listing_segment ─────────────────────────────────────────────────
|
||
|
||
|
||
def test_segment_recomputed_to_vtorichka_when_developer_false() -> None:
|
||
lot = mod.build_lot(_payload())
|
||
assert lot.listing_segment == "vtorichka"
|
||
|
||
|
||
def test_segment_recomputed_to_vtorichka_when_flag_absent() -> None:
|
||
"""Отсутствие флага — не признак новостройки (у 31 464 карточек его нет вовсе)."""
|
||
lot = mod.build_lot(_payload(raw_payload={}))
|
||
assert lot.listing_segment == "vtorichka"
|
||
assert mod.build_lot(_payload(raw_payload=None)).listing_segment == "vtorichka"
|
||
|
||
|
||
def test_segment_is_novostroyki_only_when_developer_sells() -> None:
|
||
lot = mod.build_lot(_payload(raw_payload={"is_from_developer": True}))
|
||
assert lot.listing_segment == "novostroyki"
|
||
|
||
|
||
def test_unknown_payload_keys_are_dropped() -> None:
|
||
"""В payload может приехать ключ, которого в модели нет — это не должно ронять импорт."""
|
||
lot = mod.build_lot(_payload(some_future_field="x"))
|
||
assert lot.source_id == "330200428"
|
||
|
||
|
||
# ── прогон целиком ───────────────────────────────────────────────────────────
|
||
|
||
|
||
class _FakeResult:
|
||
def __init__(self, rows: list[dict[str, Any]]) -> None:
|
||
self._rows = rows
|
||
|
||
def mappings(self) -> _FakeResult:
|
||
return self
|
||
|
||
def all(self) -> list[dict[str, Any]]:
|
||
return self._rows
|
||
|
||
def scalar(self) -> Any:
|
||
"""Проба существования кэш-таблицы: None = таблицы ещё нет."""
|
||
return self._rows[0] if self._rows else None
|
||
|
||
|
||
class _FakeSession:
|
||
"""Отдаёт строки постранично по тому же keyset-условию, что и настоящая вью."""
|
||
|
||
def __init__(self, rows: list[dict[str, Any]]) -> None:
|
||
self.rows = rows
|
||
self.commits = 0
|
||
|
||
def execute(self, _sql: Any, params: dict[str, Any] | None = None) -> _FakeResult:
|
||
if not params or "after" not in params:
|
||
return _FakeResult([]) # DDL кэша пред-геокода — строк не отдаёт
|
||
after, limit = params["after"], params["limit"]
|
||
return _FakeResult([r for r in self.rows if r["id"] > after][:limit])
|
||
|
||
def commit(self) -> None:
|
||
self.commits += 1
|
||
|
||
|
||
def _rows() -> list[dict[str, Any]]:
|
||
return [
|
||
{"id": 1, "payload": _payload(source_id="1")},
|
||
{"id": 2, "payload": _payload(source_id="2", address="Химки, Ивановская улица, 5")},
|
||
{"id": 3, "payload": _payload(source_id="3", address="Екатеринбург (Cian)")},
|
||
{"id": 4, "payload": _payload(source_id="4", address="СВАО, улица Милашенкова, 7")},
|
||
{"id": 5, "payload": _payload(source_id="5", price_rub=0)}, # невалидный: цена > 0
|
||
]
|
||
|
||
|
||
@pytest.fixture()
|
||
def captured(monkeypatch: pytest.MonkeyPatch) -> list[dict[str, Any]]:
|
||
calls: list[dict[str, Any]] = []
|
||
|
||
def _fake_save(db: Any, lots: list[Any], **kwargs: Any) -> tuple[int, int]:
|
||
calls.append({"lots": lots, **kwargs})
|
||
return len(lots), 0
|
||
|
||
monkeypatch.setattr(mod, "save_listings", _fake_save)
|
||
return calls
|
||
|
||
|
||
def test_only_moscow_rows_reach_the_writer(captured: list[dict[str, Any]]) -> None:
|
||
db = _FakeSession(_rows())
|
||
counters = mod.import_msk_raw(db, batch_size=10)
|
||
|
||
assert [lot.source_id for lot in captured[0]["lots"]] == ["1", "4"]
|
||
assert counters.read == 5
|
||
assert counters.selected == 2
|
||
assert counters.skipped_not_moscow == 1
|
||
assert counters.skipped_artifact == 1
|
||
assert counters.skipped_invalid == 1
|
||
assert counters.check(), "счётчики обязаны сходиться с прочитанным"
|
||
|
||
|
||
def test_writer_gets_region_77_and_city(captured: list[dict[str, Any]]) -> None:
|
||
mod.import_msk_raw(_FakeSession(_rows()), batch_size=10)
|
||
assert captured[0]["region_code"] == 77
|
||
assert captured[0]["city"] == "Москва"
|
||
assert all(lot.listing_segment == "vtorichka" for lot in captured[0]["lots"])
|
||
|
||
|
||
def test_commit_per_batch(captured: list[dict[str, Any]]) -> None:
|
||
"""Обрыв на середине не должен терять уже перелитые батчи."""
|
||
db = _FakeSession(_rows())
|
||
mod.import_msk_raw(db, batch_size=1)
|
||
assert db.commits == 2 # ровно столько батчей содержали московские строки
|
||
|
||
|
||
def test_dry_run_writes_nothing(captured: list[dict[str, Any]]) -> None:
|
||
db = _FakeSession(_rows())
|
||
counters = mod.import_msk_raw(db, batch_size=10, dry_run=True)
|
||
assert captured == []
|
||
assert db.commits == 0
|
||
assert counters.selected == 2 and counters.written == 0
|
||
|
||
|
||
def test_limit_caps_rows_read(captured: list[dict[str, Any]]) -> None:
|
||
counters = mod.import_msk_raw(_FakeSession(_rows()), batch_size=2, limit=3)
|
||
assert counters.read == 3
|
||
|
||
|
||
def test_avito_refuses_to_write_without_explicit_opt_in(captured: list[dict[str, Any]]) -> None:
|
||
"""У Авито в адресе нет города, а координат нет ни у одной карточки — молча
|
||
залить область в регион 77 нельзя."""
|
||
rows = [{"id": 1, "payload": _payload(source="avito", address="Варшавское ш.,62к1")}]
|
||
with pytest.raises(SystemExit):
|
||
mod.import_msk_raw(_FakeSession(rows), source="avito", batch_size=10)
|
||
|
||
counters = mod.import_msk_raw(
|
||
_FakeSession(rows), source="avito", batch_size=10, allow_unfiltered=True
|
||
)
|
||
assert counters.selected == 1 # префиксный фильтр к Авито не применяется
|
||
|
||
|
||
# ── пред-геокод Авито ────────────────────────────────────────────────────────
|
||
#
|
||
# Риски этой ветки, которые тут и караулятся:
|
||
# * регион возьмут из слага, а не из ответа — Новая Москва (Троицк/Щербинка/
|
||
# Коммунарка) уедет в область, хотя это регион 77;
|
||
# * область (регион 50) просочится в listings;
|
||
# * слаг выпадет из ключа кэша — адреса-тёзки из разных муниципалитетов
|
||
# («Центральная ул.,5») схлопнутся в один дом;
|
||
# * повторный прогон заново потратит квоту по уже разрешённым адресам.
|
||
|
||
|
||
def _avito(**over: Any) -> dict[str, Any]:
|
||
base = _payload(
|
||
source="avito",
|
||
source_url="https://www.avito.ru/moskva/kvartiry/2-k._kvartira_56_m_56_et._46317006",
|
||
address="Варшавское ш.,62к1",
|
||
)
|
||
base.update(over)
|
||
return base
|
||
|
||
|
||
class _Sugg:
|
||
"""Минимальный DadataSuggestion — нужны только поля, которые читает отбор."""
|
||
|
||
def __init__(self, kind: str, lat: float | None, lon: float | None, kladr: str | None) -> None:
|
||
self.kind, self.lat, self.lon, self.kladr_id = kind, lat, lon, kladr
|
||
self.unrestricted_value = "г Москва, ш Варшавское, д 62 к 1"
|
||
|
||
|
||
def test_slug_comes_from_source_url() -> None:
|
||
assert mod.avito_city_slug(_avito()) == "moskva"
|
||
himki = _avito(source_url="https://www.avito.ru/himki/kvartiry/1")
|
||
assert mod.avito_city_slug(himki) == "himki"
|
||
assert mod.avito_city_slug(_avito(source_url="")) is None
|
||
assert mod.avito_city_slug({}) is None
|
||
|
||
|
||
def test_cache_key_separates_same_address_in_different_towns() -> None:
|
||
"""1110 адресов из 21 570 живут под несколькими слагами — это разные дома."""
|
||
assert mod.geo_cache_key("Центральная ул.,5", "moskva") != mod.geo_cache_key(
|
||
"Центральная ул.,5", "himki"
|
||
)
|
||
# нормализация адреса общая с геокодером — регистр и пробелы ключ не множат
|
||
assert mod.geo_cache_key(" Варшавское Ш.,62к1 ", "moskva") == mod.geo_cache_key(
|
||
"варшавское ш.,62к1", "moskva"
|
||
)
|
||
|
||
|
||
def test_moscow_slug_narrows_constraint_others_keep_both_regions() -> None:
|
||
"""Не-московский слаг может оказаться Новой Москвой — оба региона в констрейнте."""
|
||
assert mod._dadata_regions_for("moskva") == [mod.DADATA_MOSCOW_REGION]
|
||
assert mod._dadata_regions_for("moskovskaya_oblast_troitsk") == [
|
||
mod.DADATA_MOSCOW_REGION,
|
||
mod.DADATA_OBLAST_REGION,
|
||
]
|
||
assert mod._dadata_regions_for(None) == [
|
||
mod.DADATA_MOSCOW_REGION,
|
||
mod.DADATA_OBLAST_REGION,
|
||
]
|
||
|
||
|
||
def test_region_is_read_from_the_answer_not_from_the_slug() -> None:
|
||
"""КЛАДР ответа 77… → регион 77, даже если слаг был подмосковным."""
|
||
point = mod._point_from_suggestions([_Sugg("house", 55.6, 37.6, "77000000000000000")])
|
||
assert point is not None and point.region_code == 77
|
||
oblast = mod._point_from_suggestions([_Sugg("house", 55.9, 37.4, "50000000000000000")])
|
||
assert oblast is not None and oblast.region_code == 50
|
||
|
||
|
||
def test_only_house_level_answers_become_points() -> None:
|
||
"""Улица/город дают точку в середине улицы — для radius-подбора это хуже, чем ничего."""
|
||
assert mod._point_from_suggestions([_Sugg("street", 55.6, 37.6, "77000000000000000")]) is None
|
||
assert mod._point_from_suggestions([_Sugg("house", None, None, "77000000000000000")]) is None
|
||
# чужой регион (констрейнт пробит) — тоже не берём
|
||
assert mod._point_from_suggestions([_Sugg("house", 56.8, 60.6, "66000000000000000")]) is None
|
||
|
||
|
||
def test_budget_stops_asking_instead_of_failing() -> None:
|
||
budget = mod.GeocodeBudget(remaining=2)
|
||
assert budget.take(5) == 2 and budget.exhausted and budget.spent == 2
|
||
assert budget.take(1) == 0 # квота кончилась — просто больше не спрашиваем
|
||
|
||
|
||
def _geo_rows() -> list[dict[str, Any]]:
|
||
return [
|
||
{"id": 1, "payload": _avito(source_id="1")}, # Москва
|
||
{
|
||
"id": 2,
|
||
"payload": _avito(
|
||
source_id="2",
|
||
source_url="https://www.avito.ru/himki/kvartiry/2",
|
||
address="Ивановская ул.,5",
|
||
),
|
||
}, # область
|
||
{
|
||
"id": 3,
|
||
"payload": _avito(source_id="3", address="Мкр-н имени В.Н. Махалина, 33"),
|
||
}, # не разрешился
|
||
]
|
||
|
||
|
||
@pytest.fixture()
|
||
def geocoded(monkeypatch: pytest.MonkeyPatch) -> list[list[dict[str, Any]]]:
|
||
"""Подменяет резолвер целиком — в сеть и в кэш-таблицу тест не ходит."""
|
||
seen: list[list[dict[str, Any]]] = []
|
||
|
||
def _fake_resolve(db: Any, rows: list[Any], budget: Any, **kwargs: Any) -> dict[str, Any]:
|
||
seen.append(list(rows))
|
||
return {
|
||
mod.geo_cache_key("Варшавское ш.,62к1", "moskva"): mod.GeoPoint(
|
||
55.6412, 37.6089, 77, "г Москва, ш Варшавское, д 62 к 1"
|
||
),
|
||
mod.geo_cache_key("Ивановская ул.,5", "himki"): mod.GeoPoint(
|
||
55.8970, 37.4297, 50, "Московская обл, г Химки, ул Ивановская, д 5"
|
||
),
|
||
}
|
||
|
||
monkeypatch.setattr(mod, "_resolve_points", _fake_resolve)
|
||
return seen
|
||
|
||
|
||
def test_geocode_lets_avito_through_without_allow_unfiltered(
|
||
captured: list[dict[str, Any]], geocoded: list[list[dict[str, Any]]]
|
||
) -> None:
|
||
"""Пред-геокод — штатный путь: аварийный флаг больше не нужен."""
|
||
counters = mod.import_msk_raw(
|
||
_FakeSession(_geo_rows()), source="avito", batch_size=10, geocode=True
|
||
)
|
||
assert [lot.source_id for lot in captured[0]["lots"]] == ["1"]
|
||
assert counters.skipped_oblast == 1
|
||
assert counters.skipped_ungeocoded == 1
|
||
assert counters.check(), "счётчики обязаны сходиться с прочитанным"
|
||
|
||
|
||
def test_coordinates_reach_the_writer(
|
||
captured: list[dict[str, Any]], geocoded: list[list[dict[str, Any]]]
|
||
) -> None:
|
||
"""Ради geom всё и затевалось — карточка не должна лечь без координат."""
|
||
mod.import_msk_raw(_FakeSession(_geo_rows()), source="avito", batch_size=10, geocode=True)
|
||
lot = captured[0]["lots"][0]
|
||
assert (lot.lat, lot.lon) == (55.6412, 37.6089)
|
||
assert lot.geo_precision == "house"
|
||
assert captured[0]["region_code"] == 77
|
||
|
||
|
||
def test_oblast_is_never_written_as_region_77(
|
||
captured: list[dict[str, Any]], geocoded: list[list[dict[str, Any]]]
|
||
) -> None:
|
||
mod.import_msk_raw(_FakeSession(_geo_rows()), source="avito", batch_size=10, geocode=True)
|
||
written = [lot.source_id for call in captured for lot in call["lots"]]
|
||
assert "2" not in written, "химкинская карточка не должна уехать в регион 77"
|
||
|
||
|
||
def test_avito_without_geocode_demands_opt_in(captured: list[dict[str, Any]]) -> None:
|
||
rows = [{"id": 1, "payload": _avito()}]
|
||
with pytest.raises(SystemExit):
|
||
mod.import_msk_raw(_FakeSession(rows), source="avito", batch_size=10)
|
||
|
||
|
||
def test_geocode_is_refused_for_cian(captured: list[dict[str, Any]]) -> None:
|
||
"""У Циана город виден по префиксу округа даром — квоту на него не тратим."""
|
||
with pytest.raises(SystemExit):
|
||
mod.import_msk_raw(_FakeSession(_rows()), source="cian", batch_size=10, geocode=True)
|
||
|
||
|
||
def test_dry_run_survives_missing_geocode_cache(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
"""Первый `--dry-run` идёт по проду, где кэш-таблицы ещё нет.
|
||
|
||
Таблицу создаёт только боевой прогон, поэтому SELECT по ней в сухом ронял
|
||
ровно ту репетицию, ради которой сухой прогон и существует (замер на проде
|
||
11.09: UndefinedTable msk_raw.avito_geocode). Резолвер обязан молча считать
|
||
кэш пустым и идти к DaData за остатком.
|
||
"""
|
||
|
||
class _NoCacheSession:
|
||
def __init__(self) -> None:
|
||
self.cache_reads = 0
|
||
|
||
def execute(self, sql: Any, params: dict[str, Any] | None = None) -> _FakeResult:
|
||
if "to_regclass" in str(sql):
|
||
return _FakeResult([]) # таблицы нет
|
||
self.cache_reads += 1
|
||
raise AssertionError("к отсутствующей кэш-таблице ходить нельзя")
|
||
|
||
point = mod.GeoPoint(55.6412, 37.6089, 77, "г Москва, ш Варшавское, д 62 к 1")
|
||
|
||
async def _fake_geocode(misses: list[Any]) -> dict[str, Any]:
|
||
return {misses[0][0]: point}
|
||
|
||
monkeypatch.setattr(mod, "_geocode_many", _fake_geocode)
|
||
|
||
db = _NoCacheSession()
|
||
points = mod._resolve_points(
|
||
db, [{"id": 1, "payload": _avito()}], mod.GeocodeBudget(10), dry_run=True
|
||
)
|
||
|
||
assert db.cache_reads == 0
|
||
assert list(points.values()) == [point]
|
||
|
||
|
||
# ── Яндекс: город из второго компонента адреса ───────────────────────────────
|
||
|
||
|
||
def _yandex(**over: Any) -> dict[str, Any]:
|
||
base = _payload(
|
||
source="yandex",
|
||
source_url="https://realty.yandex.ru/offer/1234567890123456789/",
|
||
source_id="1234567890123456789",
|
||
address="Россия, Москва, Коробейников переулок, 1",
|
||
lat=55.737312,
|
||
lon=37.601006,
|
||
)
|
||
base.update(over)
|
||
return base
|
||
|
||
|
||
def test_yandex_moscow_is_recognised_without_geocode() -> None:
|
||
assert mod.is_moscow_yandex_address("Россия, Москва, Коробейников переулок, 1")
|
||
assert mod.is_moscow_yandex_address("Россия, Москва, Зеленоград, корпус 1130")
|
||
|
||
|
||
def test_yandex_oblast_is_not_moscow() -> None:
|
||
"""Ровно те два значения, что встречаются в корпусе; область писать нельзя."""
|
||
oblast = "Россия, Московская область, Химки, Ивановская улица, 5"
|
||
assert not mod.is_moscow_yandex_address(oblast)
|
||
assert not mod.is_moscow_yandex_address("Москва, Коробейников переулок, 1") # без страны
|
||
assert not mod.is_moscow_yandex_address("")
|
||
assert not mod.is_moscow_yandex_address(None)
|
||
|
||
|
||
def test_yandex_oblast_rows_do_not_reach_the_writer(captured: list[dict[str, Any]]) -> None:
|
||
rows = [
|
||
{"id": 1, "payload": _yandex(source_id="1")},
|
||
{
|
||
"id": 2,
|
||
"payload": _yandex(
|
||
source_id="2", address="Россия, Московская область, Химки, Ивановская улица, 5"
|
||
),
|
||
},
|
||
]
|
||
counters = mod.import_msk_raw(_FakeSession(rows), source="yandex", batch_size=10)
|
||
assert [lot.source_id for lot in captured[0]["lots"]] == ["1"]
|
||
assert counters.skipped_not_moscow == 1
|
||
|
||
|
||
def test_yandex_keeps_its_own_coordinates(captured: list[dict[str, Any]]) -> None:
|
||
"""Координаты у Яндекса свои — геокод ему не нужен, geom появляется сразу."""
|
||
rows = [{"id": 1, "payload": _yandex()}]
|
||
mod.import_msk_raw(_FakeSession(rows), source="yandex", batch_size=10)
|
||
lot = captured[0]["lots"][0]
|
||
assert (lot.lat, lot.lon) == (55.737312, 37.601006)
|
||
|
||
|
||
def test_geocode_is_refused_for_yandex(captured: list[dict[str, Any]]) -> None:
|
||
"""Квота нужна только Авито: у Яндекса город и точка есть даром."""
|
||
with pytest.raises(SystemExit):
|
||
mod.import_msk_raw(
|
||
_FakeSession([{"id": 1, "payload": _yandex()}]), source="yandex", geocode=True
|
||
)
|
||
|
||
|
||
def test_domclick_moscow_is_recognised_without_geocode() -> None:
|
||
"""Регион ДомКлика — первый компонент адреса; Новая Москва тоже Москва."""
|
||
assert mod.is_moscow_domclick_address("Москва, улица Нижние Мнёвники, 16 к2")
|
||
assert mod.is_moscow_domclick_address("Москва, пос. Птичное, Лесная улица, 82")
|
||
assert mod.is_moscow_domclick_address("Москва, x. Ильичевка, хутор Ильичёвка, 1")
|
||
|
||
|
||
def test_domclick_non_moscow_is_rejected() -> None:
|
||
"""Вторая линия обороны: GUID в сборщике сменится — импорт не смолчит."""
|
||
assert not mod.is_moscow_domclick_address("Московская область, Химки, 9 Мая, 21к3")
|
||
assert not mod.is_moscow_domclick_address("Екатеринбург, улица Ясная, 2")
|
||
# «Москва» должна быть отдельным компонентом, а не префиксом другого слова.
|
||
assert not mod.is_moscow_domclick_address("Московский, 1-я улица, 5")
|
||
assert not mod.is_moscow_domclick_address("")
|
||
assert not mod.is_moscow_domclick_address(None)
|
||
|
||
|
||
def test_domclick_view_is_wired_into_source_views() -> None:
|
||
assert mod.SOURCE_VIEWS["domclick"] == "msk_raw.domclick_latest"
|
||
assert mod.CITY_FILTERS["domclick"] is mod.is_moscow_domclick_address
|