"""Тесты для _save_territorial_zones (bulk_harvest.py) — mock-based. Проверяет: - Успешный UPSERT 3 features → 3 строки вставлены - Повторный вызов → ON CONFLICT обновляет, не дублирует - Feature без geometry → строка вставлена с geom=NULL, без краша - Feature без zone_id → синтетический fallback zone_id используется """ from __future__ import annotations from typing import Any from unittest.mock import MagicMock from app.services.cadastre.bulk_harvest import _save_territorial_zones def _make_feature( feature_id: Any = "zone_1", zone_code: str = "Ж-1", zone_name: str = "Жилая смешанная", permitted_use: str = "ИЖС", has_geometry: bool = True, ) -> dict: """Создать raw feature dict в формате get_features_in_bbox_grid.""" geom = ( { "type": "Polygon", "coordinates": [ [ [6090000.0, 7590000.0], [6090100.0, 7590000.0], [6090100.0, 7590100.0], [6090000.0, 7590100.0], [6090000.0, 7590000.0], ] ], } if has_geometry else None ) return { "id": feature_id, "geometry": geom, "properties": { "zone_code": zone_code, "zone_name": zone_name, "permitted_use": permitted_use, }, } def _make_db_mock() -> MagicMock: """Mock SQLAlchemy Session с begin_nested() savepoint support.""" db = MagicMock() # begin_nested() используется как context manager savepoint_ctx = MagicMock() savepoint_ctx.__enter__ = MagicMock(return_value=savepoint_ctx) savepoint_ctx.__exit__ = MagicMock(return_value=False) db.begin_nested.return_value = savepoint_ctx return db class TestSaveTerritorialZones: """Тесты для _save_territorial_zones.""" def test_three_features_inserted(self) -> None: """3 features → returned count == 3, execute вызван 3 раза.""" db = _make_db_mock() features = [ _make_feature("z1", "Ж-1"), _make_feature("z2", "ОД-1"), _make_feature("z3", "П-1"), ] result = _save_territorial_zones(db, "66:41:0204016", features) assert result == 3 assert db.execute.call_count == 3 db.commit.assert_called_once() def test_empty_features_list(self) -> None: """Пустой список → 0 inserted, commit всё равно вызван.""" db = _make_db_mock() result = _save_territorial_zones(db, "66:41:0204016", []) assert result == 0 db.execute.assert_not_called() db.commit.assert_called_once() def test_feature_without_geometry_no_crash(self) -> None: """Feature без geometry → geom=NULL, строка вставлена без краша.""" db = _make_db_mock() features = [_make_feature("zone_no_geom", has_geometry=False)] result = _save_territorial_zones(db, "66:41:0204016", features) assert result == 1 # Проверяем что geom параметр передан как None call_kwargs: dict = db.execute.call_args[0][1] assert call_kwargs["geom"] is None def test_feature_without_zone_id_uses_fallback(self) -> None: """Feature без id → md5-based fallback zone_id (stable между runs).""" db = _make_db_mock() features = [ { "id": None, "geometry": None, "properties": {"zone_code": "Ж-2"}, } ] result = _save_territorial_zones(db, "66:41:0204016", features) assert result == 1 call_kwargs = db.execute.call_args[0][1] zone_id: str = call_kwargs["zone_id"] # fallback zone_id содержит quarter_cad и стабильный hash (12 hex chars) assert zone_id.startswith("66:41:0204016_") suffix = zone_id.split("_", 3)[-1] assert len(suffix) == 12 assert all(c in "0123456789abcdef" for c in suffix) # Второй вызов с теми же данными → тот же zone_id (идемпотентность) db2 = _make_db_mock() _save_territorial_zones(db2, "66:41:0204016", features) call_kwargs2 = db2.execute.call_args[0][1] assert call_kwargs2["zone_id"] == zone_id def test_zone_id_from_props_id(self) -> None: """Если feature.id=None, но props['id'] есть — используется props['id'].""" db = _make_db_mock() features = [ { "id": None, "geometry": None, "properties": {"id": "props_id_42", "zone_code": "Ж-3"}, } ] result = _save_territorial_zones(db, "66:41:0204016", features) assert result == 1 call_kwargs = db.execute.call_args[0][1] assert call_kwargs["zone_id"] == "props_id_42" def test_execute_error_logged_not_raised(self) -> None: """Exception в execute → строка не вставлена, warning залогирован, не re-raise.""" db = _make_db_mock() db.execute.side_effect = RuntimeError("DB error") features = [_make_feature("z_err")] # Не должен бросить исключение result = _save_territorial_zones(db, "66:41:0204016", features) assert result == 0 db.commit.assert_called_once() def test_savepoint_used_per_row(self) -> None: """begin_nested() вызывается для каждой строки (SAVEPOINT паттерн).""" db = _make_db_mock() features = [_make_feature(f"z{i}") for i in range(3)] _save_territorial_zones(db, "66:41:0204016", features) assert db.begin_nested.call_count == 3 def test_quarter_cad_param_passed(self) -> None: """quarter_cad правильно передаётся в SQL параметры.""" db = _make_db_mock() features = [_make_feature("zone_check")] _save_territorial_zones(db, "66:41:9999999", features) call_kwargs = db.execute.call_args[0][1] assert call_kwargs["quarter_cad"] == "66:41:9999999" def test_raw_props_serialized(self) -> None: """raw_props — JSON строка из properties dict.""" import json db = _make_db_mock() features = [ { "id": "z_props", "geometry": None, "properties": {"zone_code": "ОД-2", "extra": "value"}, } ] _save_territorial_zones(db, "66:41:0204016", features) call_kwargs = db.execute.call_args[0][1] raw = json.loads(call_kwargs["raw_props"]) assert raw["zone_code"] == "ОД-2" assert raw["extra"] == "value"