"""Тесты для GET /{cad_num}/connection-points (issue #115). Использует FastAPI TestClient с mock DB — без реального Postgres. """ from __future__ import annotations from typing import Any from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient from app.main import app # ── Helpers ─────────────────────────────────────────────────────────────────── _VALID_CAD = "66:41:0204016:10" _QUARTER = "66:41:0204016" def _mock_get_db(db: Any): """FastAPI dependency override factory.""" def _get_db_override(): yield db return _get_db_override def _make_structure(distance_m: float = 120.5) -> dict[str, Any]: return { "name": "ТП-101", "type": "Трансформаторная подстанция", "cad_num": None, "distance_to_boundary_m": distance_m, "geometry_geojson": {"type": "Point", "coordinates": [60.6, 56.8]}, "readable_address": None, "raw_props": {"name": "ТП-101"}, "source": "nspd_36328", } def _make_zouit_overlap() -> dict[str, Any]: return { "reg_numb_border": "RN-001", "type_zone": "Охранная зона ЛЭП", "subcategory": 5, "intersects_parcel": True, "geometry_geojson": {"type": "Polygon", "coordinates": [[]]}, "raw_props": {"type_zone": "Охранная зона ЛЭП"}, "source": "nspd_37578", } def _make_summary( nearest: float | None = 120.5, in_zone: bool = False, zones_count: int = 0, total: int = 1, ) -> dict[str, Any]: return { "nearest_structure_distance_m": nearest, "in_protection_zone": in_zone, "protection_zones_intersecting": zones_count, "total_structures_in_radius": total, } def _make_full_response( structures: list[dict[str, Any]] | None = None, overlaps: list[dict[str, Any]] | None = None, summary: dict[str, Any] | None = None, dump_available: bool = True, dump_fetched_at: str | None = "2026-05-01T12:00:00+00:00", ) -> dict[str, Any]: if structures is None: structures = [_make_structure()] if overlaps is None: overlaps = [] if summary is None: summary = _make_summary(total=len(structures)) return { "engineering_structures": structures, "zouit_engineering_overlaps": overlaps, "summary": summary, "dump_available": dump_available, "dump_fetched_at": dump_fetched_at, } # ── Tests ───────────────────────────────────────────────────────────────────── def test_connection_points_no_dump_returns_empty() -> None: """Квартал без dump → dump_available=false, пустые массивы, 200 OK.""" from app.core.db import get_db db = MagicMock() app.dependency_overrides[get_db] = _mock_get_db(db) empty_response = { "engineering_structures": [], "zouit_engineering_overlaps": [], "summary": { "nearest_structure_distance_m": None, "in_protection_zone": False, "protection_zones_intersecting": 0, "total_structures_in_radius": 0, }, "dump_available": False, "dump_fetched_at": None, } try: with patch( "app.api.v1.parcels.get_connection_points", return_value=empty_response, ): client = TestClient(app) response = client.get(f"/api/v1/parcels/{_VALID_CAD}/connection-points") assert response.status_code == 200, response.text body = response.json() assert body["dump_available"] is False assert body["engineering_structures"] == [] assert body["zouit_engineering_overlaps"] == [] assert body["summary"]["total_structures_in_radius"] == 0 assert body["summary"]["nearest_structure_distance_m"] is None finally: app.dependency_overrides.clear() def test_connection_points_parcel_not_found_404() -> None: """cad_num не найден в БД (ValueError из сервиса) → 404.""" from app.core.db import get_db db = MagicMock() app.dependency_overrides[get_db] = _mock_get_db(db) try: with patch( "app.api.v1.parcels.get_connection_points", side_effect=ValueError("Участок '66:41:9999999:1' не найден в БД"), ): client = TestClient(app) response = client.get("/api/v1/parcels/66:41:9999999:1/connection-points") assert response.status_code == 404 assert "не найден" in response.json()["detail"] finally: app.dependency_overrides.clear() def test_connection_points_filters_by_radius() -> None: """Фичи за radius_m не попадают в ответ (сервис фильтрует). Мокаем сервис: при radius_m=100 возвращаем только близкую структуру, при radius_m=500 — обе. Проверяем что endpoint передаёт radius_m в сервис. """ from app.core.db import get_db db = MagicMock() app.dependency_overrides[get_db] = _mock_get_db(db) close_only = _make_full_response(structures=[_make_structure(distance_m=80.0)]) far_included = _make_full_response( structures=[_make_structure(distance_m=80.0), _make_structure(distance_m=450.0)], summary=_make_summary(nearest=80.0, total=2), ) try: with patch("app.api.v1.parcels.get_connection_points") as mock_svc: mock_svc.return_value = close_only client = TestClient(app) r100 = client.get(f"/api/v1/parcels/{_VALID_CAD}/connection-points?radius_m=100") assert r100.status_code == 200 assert r100.json()["summary"]["total_structures_in_radius"] == 1 # Проверяем что radius_m=100 передан в сервис call_kwargs = mock_svc.call_args assert call_kwargs[0][2] == 100 or call_kwargs[1].get("radius_m") == 100 mock_svc.return_value = far_included r500 = client.get(f"/api/v1/parcels/{_VALID_CAD}/connection-points?radius_m=500") assert r500.status_code == 200 assert r500.json()["summary"]["total_structures_in_radius"] == 2 finally: app.dependency_overrides.clear() def test_connection_points_zouit_intersects_flag() -> None: """zouit_engineering_overlaps содержат intersects_parcel=true и правильные поля.""" from app.core.db import get_db db = MagicMock() app.dependency_overrides[get_db] = _mock_get_db(db) overlap = _make_zouit_overlap() full = _make_full_response( overlaps=[overlap], summary=_make_summary(in_zone=True, zones_count=1), ) try: with patch( "app.api.v1.parcels.get_connection_points", return_value=full, ): client = TestClient(app) response = client.get(f"/api/v1/parcels/{_VALID_CAD}/connection-points") assert response.status_code == 200 body = response.json() assert body["summary"]["in_protection_zone"] is True assert body["summary"]["protection_zones_intersecting"] == 1 overlaps = body["zouit_engineering_overlaps"] assert len(overlaps) == 1 assert overlaps[0]["intersects_parcel"] is True assert overlaps[0]["reg_numb_border"] == "RN-001" assert overlaps[0]["source"] == "nspd_37578" finally: app.dependency_overrides.clear() def test_summary_nearest_distance() -> None: """summary.nearest_structure_distance_m = расстояние до ближайшей структуры.""" from app.core.db import get_db db = MagicMock() app.dependency_overrides[get_db] = _mock_get_db(db) s1 = _make_structure(distance_m=42.7) s2 = _make_structure(distance_m=180.0) full = _make_full_response( structures=[s1, s2], summary=_make_summary(nearest=42.7, total=2), ) try: with patch( "app.api.v1.parcels.get_connection_points", return_value=full, ): client = TestClient(app) response = client.get(f"/api/v1/parcels/{_VALID_CAD}/connection-points") assert response.status_code == 200 body = response.json() assert body["summary"]["nearest_structure_distance_m"] == pytest.approx(42.7) assert body["summary"]["total_structures_in_radius"] == 2 assert body["engineering_structures"][0]["distance_to_boundary_m"] == pytest.approx(42.7) finally: app.dependency_overrides.clear() def test_connection_points_radius_out_of_range_422() -> None: """radius_m=10 (< min 50) → 422 Unprocessable Entity.""" client = TestClient(app) response = client.get(f"/api/v1/parcels/{_VALID_CAD}/connection-points?radius_m=10") assert response.status_code == 422 def test_connection_points_radius_too_large_422() -> None: """radius_m=5000 (> max 2000) → 422 Unprocessable Entity.""" client = TestClient(app) response = client.get(f"/api/v1/parcels/{_VALID_CAD}/connection-points?radius_m=5000") assert response.status_code == 422