gendesign/backend/tests/services/site_finder/test_premises_lookup.py
Light1YT 2c22c3f7ea
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
feat(site-finder): per-building помещения/машино-места + parking_ratio (on-demand MVP, #96)
Оживляет мёртвый 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
2026-06-14 18:48:16 +05:00

150 lines
5.6 KiB
Python
Raw 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.

"""Тесты 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()