"""Тесты GET /parcels/by-bbox (SF-B1). Проверяют: 1. Валидный bbox → 200 + корректная структура ответа. 2. Некорректный bbox (min_lat >= max_lat) → 400. 3. user_id передан → status заполняется из mock DB; без user_id → status=null. """ 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 def _make_db_row( cad_num: str = "66:41:0204016:10", centroid_lat: float = 56.83, centroid_lon: float = 60.64, area_m2: float = 1200.0, land_category: str | None = "land_residential", user_status: str | None = None, ) -> dict[str, Any]: return { "cad_num": cad_num, "centroid_lat": centroid_lat, "centroid_lon": centroid_lon, "area_m2": area_m2, "land_category": land_category, "user_status": user_status, } def _build_mock_db(rows: list[dict[str, Any]]) -> MagicMock: """Сконструировать mock Session, возвращающий rows при execute().mappings().all().""" mock_db = MagicMock() mappings_mock = MagicMock() mappings_mock.all.return_value = rows execute_result = MagicMock() execute_result.mappings.return_value = mappings_mock mock_db.execute.return_value = execute_result return mock_db @pytest.fixture() def client() -> TestClient: return TestClient(app) # ── Test 1: валидный bbox возвращает корректную структуру ────────────────── def test_by_bbox_valid_returns_structure(client: TestClient) -> None: rows = [_make_db_row()] mock_db = _build_mock_db(rows) with patch("app.api.v1.parcels.get_db", return_value=iter([mock_db])): resp = client.get( "/api/v1/parcels/by-bbox", params={ "min_lat": 56.80, "min_lon": 60.60, "max_lat": 56.90, "max_lon": 60.70, "limit": 200, }, ) assert resp.status_code == 200 body = resp.json() assert "parcels" in body assert "count" in body assert "limit" in body assert "bbox_area_km2" in body assert body["count"] == 1 assert body["limit"] == 200 parcel = body["parcels"][0] assert parcel["cad_num"] == "66:41:0204016:10" assert parcel["centroid_lat"] == pytest.approx(56.83, abs=0.01) assert parcel["centroid_lon"] == pytest.approx(60.64, abs=0.01) assert parcel["area_m2"] == pytest.approx(1200.0) assert parcel["land_category"] == "land_residential" assert parcel["status"] is None # user_id не передан assert parcel["last_analysis_date"] is None # ── Test 2: некорректный bbox → 400 ─────────────────────────────────────── def test_by_bbox_invalid_bbox_returns_400(client: TestClient) -> None: resp = client.get( "/api/v1/parcels/by-bbox", params={ "min_lat": 56.90, # min >= max → ошибка "min_lon": 60.60, "max_lat": 56.80, "max_lon": 60.70, }, ) assert resp.status_code == 400 assert "bbox" in resp.json()["detail"].lower() # ── Test 3: user_id → status из БД; без user_id → status null ───────────── def test_by_bbox_status_overlay_with_user_id(client: TestClient) -> None: rows = [_make_db_row(user_status="in_work")] mock_db = _build_mock_db(rows) with patch("app.api.v1.parcels.get_db", return_value=iter([mock_db])): resp = client.get( "/api/v1/parcels/by-bbox", params={ "min_lat": 56.80, "min_lon": 60.60, "max_lat": 56.90, "max_lon": 60.70, "user_id": "user-abc-123", }, ) assert resp.status_code == 200 parcel = resp.json()["parcels"][0] assert parcel["status"] == "in_work" def test_by_bbox_no_user_id_status_is_null(client: TestClient) -> None: rows = [_make_db_row(user_status="favorite")] # DB вернёт статус mock_db = _build_mock_db(rows) with patch("app.api.v1.parcels.get_db", return_value=iter([mock_db])): resp = client.get( "/api/v1/parcels/by-bbox", params={ "min_lat": 56.80, "min_lon": 60.60, "max_lat": 56.90, "max_lon": 60.70, # user_id не передан }, ) assert resp.status_code == 200 parcel = resp.json()["parcels"][0] # Без user_id статус принудительно null (endpoint не раскрывает чужие статусы) assert parcel["status"] is None