All checks were successful
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / changes (push) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m59s
CI / openapi-codegen-check (pull_request) Successful in 1m58s
CI / backend-tests (push) Successful in 8m48s
CI / backend-tests (pull_request) Successful in 8m48s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 4m58s
Deploy / build-worker (push) Successful in 5m46s
Deploy / deploy (push) Successful in 1m30s
Оживляет мёртвый foundation list_objects_in_building (#168 Q3-deferred). Fix реального блокера: NSPDOptions не парсил objdocId (camelCase из NSPD search) → objdoc_id всегда None, метод нельзя было вызвать. AliasChoices(objdoc_id, objdocId) + registers. premises_lookup.get_building_premises(cad_num) — резолв objdoc → list_objects_in_building, graceful (WAF/сеть/not-found → None). parking_ratio = машино-места/помещения (None при 0 помещений, 0.0 при реальном отсутствии паркинга). Verified live 66:41:0106036:183. MVP on-demand (без bulk/схемы/prod-записей). parking_ratio готов, но НЕ wired в analyze: конкуренты приходят из ДОМ.РФ без cad_num → нужен domrf↔cad_buildings geom-match (отдельная задача). Сервис примет cad_num как только matching появится. 11 тестов, ruff clean. Refs #96
150 lines
5.6 KiB
Python
150 lines
5.6 KiB
Python
"""Тесты premises_lookup (#96 MVP on-demand per-building помещения/паркинг).
|
||
|
||
Сетевые вызовы НЕ делаем — мокаем sync NSPDClient.search_by_cad (резолв objdoc)
|
||
и async NSPDBulkClient.list_objects_in_building (tab-group counts). Проверяем
|
||
happy path + graceful-фоллбэки (здание не найдено / нет objdoc / WAF).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
from app.schemas.nspd_bulk import ObjectsListing
|
||
from app.services.scrapers.nspd_client import NspdLiteWafError
|
||
|
||
MODULE = "app.services.site_finder.premises_lookup"
|
||
|
||
|
||
def _run_returns(listing: ObjectsListing | None) -> Any:
|
||
"""side_effect для мока asyncio.run: закрывает переданную корутину и отдаёт listing.
|
||
|
||
Без close() реальная корутина _fetch_listing(...) остаётся never-awaited
|
||
(RuntimeWarning), т.к. мок asyncio.run её не исполняет. Закрываем явно.
|
||
"""
|
||
|
||
def _side_effect(coro: Any) -> ObjectsListing | None:
|
||
coro.close()
|
||
return listing
|
||
|
||
return _side_effect
|
||
|
||
|
||
def _search_result(options: dict[str, Any] | None, feature_id: str | None = "999") -> MagicMock:
|
||
"""Замокать NSPDSearchResult.first → NSPDFeature-подобный объект."""
|
||
result = MagicMock()
|
||
if options is None and feature_id is None:
|
||
result.first = None
|
||
return result
|
||
feature = MagicMock()
|
||
feature.properties = {"options": options or {}}
|
||
feature.feature_id = feature_id
|
||
result.first = feature
|
||
return result
|
||
|
||
|
||
def test_get_building_premises_happy_path() -> None:
|
||
"""search → objdocId → list_objects → ObjectsListing с parking_ratio."""
|
||
from app.services.site_finder.premises_lookup import get_building_premises
|
||
|
||
listing = ObjectsListing(objdoc_id=40995027, flats_count=200, parking_count=100)
|
||
|
||
with (
|
||
patch(f"{MODULE}.NSPDClient") as mock_client_cls,
|
||
patch(f"{MODULE}.asyncio.run", side_effect=_run_returns(listing)) as mock_run,
|
||
):
|
||
mock_client_cls.return_value.search_by_cad.return_value = _search_result(
|
||
{"objdocId": 40995027}
|
||
)
|
||
out = get_building_premises("66:41:0106036:183")
|
||
|
||
assert out is listing
|
||
assert out.parking_ratio == 0.5
|
||
mock_run.assert_called_once()
|
||
|
||
|
||
def test_get_building_premises_snake_case_objdoc() -> None:
|
||
"""objdoc_id в snake_case тоже резолвится (bulk-shape)."""
|
||
from app.services.site_finder.premises_lookup import get_building_premises
|
||
|
||
listing = ObjectsListing(objdoc_id=777, flats_count=10, parking_count=2)
|
||
|
||
with (
|
||
patch(f"{MODULE}.NSPDClient") as mock_client_cls,
|
||
patch(f"{MODULE}.asyncio.run", side_effect=_run_returns(listing)),
|
||
):
|
||
mock_client_cls.return_value.search_by_cad.return_value = _search_result(
|
||
{"objdoc_id": 777}
|
||
)
|
||
out = get_building_premises("66:41:0106036:183")
|
||
|
||
assert out is listing
|
||
|
||
|
||
def test_get_building_premises_fallback_to_feature_id() -> None:
|
||
"""Нет objdocId в options → fallback на feature.id (verified live: id=objdoc)."""
|
||
from app.services.site_finder.premises_lookup import get_building_premises
|
||
|
||
listing = ObjectsListing(objdoc_id=12345, flats_count=5, parking_count=0)
|
||
|
||
with (
|
||
patch(f"{MODULE}.NSPDClient") as mock_client_cls,
|
||
patch(f"{MODULE}.asyncio.run", side_effect=_run_returns(listing)) as mock_run,
|
||
):
|
||
mock_client_cls.return_value.search_by_cad.return_value = _search_result(
|
||
{}, feature_id="12345"
|
||
)
|
||
out = get_building_premises("66:41:0106036:183")
|
||
|
||
assert out is listing
|
||
# objdoc_id=12345 должен был уйти в _fetch_listing
|
||
mock_run.assert_called_once()
|
||
|
||
|
||
def test_get_building_premises_not_found_returns_none() -> None:
|
||
"""Здание не найдено в НСПД (result.first=None) → None, async не дёргается."""
|
||
from app.services.site_finder.premises_lookup import get_building_premises
|
||
|
||
with (
|
||
patch(f"{MODULE}.NSPDClient") as mock_client_cls,
|
||
patch(f"{MODULE}.asyncio.run") as mock_run,
|
||
):
|
||
mock_client_cls.return_value.search_by_cad.return_value = _search_result(
|
||
None, feature_id=None
|
||
)
|
||
out = get_building_premises("66:41:0106036:183")
|
||
|
||
assert out is None
|
||
mock_run.assert_not_called()
|
||
|
||
|
||
def test_get_building_premises_no_objdoc_returns_none() -> None:
|
||
"""Нет ни objdocId, ни feature.id → None (нельзя дёрнуть tab-group)."""
|
||
from app.services.site_finder.premises_lookup import get_building_premises
|
||
|
||
with (
|
||
patch(f"{MODULE}.NSPDClient") as mock_client_cls,
|
||
patch(f"{MODULE}.asyncio.run") as mock_run,
|
||
):
|
||
mock_client_cls.return_value.search_by_cad.return_value = _search_result(
|
||
{}, feature_id=None
|
||
)
|
||
out = get_building_premises("66:41:0106036:183")
|
||
|
||
assert out is None
|
||
mock_run.assert_not_called()
|
||
|
||
|
||
def test_get_building_premises_waf_returns_none() -> None:
|
||
"""WAF/сеть на search_by_cad → None (graceful, analyze не падает)."""
|
||
from app.services.site_finder.premises_lookup import get_building_premises
|
||
|
||
with (
|
||
patch(f"{MODULE}.NSPDClient") as mock_client_cls,
|
||
patch(f"{MODULE}.asyncio.run") as mock_run,
|
||
):
|
||
mock_client_cls.return_value.search_by_cad.side_effect = NspdLiteWafError("HTTP 403 WAF")
|
||
out = get_building_premises("66:41:0106036:183")
|
||
|
||
assert out is None
|
||
mock_run.assert_not_called()
|