"""Тесты sync_functional_zones (#1058) — ГИСОГД-СО WFS → ekb_genplan_functional_zone. Сеть/БД не дёргаются: мокаем fetch_functional_zones + SessionLocal, проверяем маппинг properties → колонки, globalid-дедуп, upsert-идемпотентность. """ from __future__ import annotations from contextlib import contextmanager from typing import Any from app.workers.tasks import genplan_zones_sync # ── Фейковые данные ────────────────────────────────────────────────────────── def _make_feature( globalid: str = "aaaa-1111", full_name: str | None = "Жилые зоны", classid: str | None = "Ж", status: str | None = "Существующий", area_ha: float | None = 12.5, doc_ref: str | None = "REF-001", ) -> dict[str, Any]: return { "geometry": { "type": "Polygon", "coordinates": [[[60.5, 56.8], [60.6, 56.8], [60.5, 56.8]]], }, "properties": { "urban10_globalid": globalid, "full_name": full_name, "urban10_classid_v": "Жилые зоны (fallback)", "urban10_classid": classid, "urban10_status_v": status, "urban10_area": area_ha, "info_set_key_ref": doc_ref, }, } # ── Фейковая БД ────────────────────────────────────────────────────────────── class _FakeDB: def __init__(self) -> None: self.executed: list[tuple[Any, dict[str, Any] | None]] = [] self.committed = False self.closed = False @contextmanager def begin_nested(self) -> Any: yield def execute(self, sql: Any, params: dict[str, Any] | None = None) -> Any: self.executed.append((sql, params)) return None def commit(self) -> None: self.committed = True def close(self) -> None: self.closed = True # ── Тесты ──────────────────────────────────────────────────────────────────── def test_sync_maps_properties_to_columns(monkeypatch: Any) -> None: """Проверяем маппинг properties → параметры UPSERT.""" feat = _make_feature( globalid="uuid-abc", full_name="Жилые зоны", classid="Ж", status="Существующий", area_ha=10.5, doc_ref="DOC-777", ) db = _FakeDB() monkeypatch.setattr(genplan_zones_sync, "fetch_functional_zones", lambda: [feat]) monkeypatch.setattr(genplan_zones_sync, "SessionLocal", lambda: db) result = genplan_zones_sync.sync_functional_zones() assert result == {"zones": 1} assert db.committed is True assert db.closed is True assert len(db.executed) == 1 _, params = db.executed[0] assert params is not None assert params["globalid"] == "uuid-abc" assert params["zone_type"] == "Жилые зоны" assert params["zone_class_code"] == "Ж" assert params["status"] == "Существующий" assert params["area_ha"] == 10.5 assert params["doc_ref"] == "DOC-777" assert isinstance(params["geojson"], str) assert isinstance(params["raw_props"], str) def test_sync_fallback_to_classid_v_when_no_full_name(monkeypatch: Any) -> None: """full_name=None → zone_type берётся из urban10_classid_v.""" feat = _make_feature(full_name=None) db = _FakeDB() monkeypatch.setattr(genplan_zones_sync, "fetch_functional_zones", lambda: [feat]) monkeypatch.setattr(genplan_zones_sync, "SessionLocal", lambda: db) genplan_zones_sync.sync_functional_zones() _, params = db.executed[0] assert params is not None assert params["zone_type"] == "Жилые зоны (fallback)" def test_sync_skips_feature_without_globalid(monkeypatch: Any) -> None: """Фича без globalid → пропускается (defensive), не вызывает UPSERT.""" feat: dict[str, Any] = { "geometry": {"type": "Polygon", "coordinates": []}, "properties": {"full_name": "Жилые зоны"}, # нет urban10_globalid } db = _FakeDB() monkeypatch.setattr(genplan_zones_sync, "fetch_functional_zones", lambda: [feat]) monkeypatch.setattr(genplan_zones_sync, "SessionLocal", lambda: db) result = genplan_zones_sync.sync_functional_zones() assert result == {"zones": 0} assert db.executed == [] assert db.committed is True def test_sync_skips_feature_without_geometry(monkeypatch: Any) -> None: """Фича без geometry → пропускается.""" feat: dict[str, Any] = { "geometry": None, "properties": { "urban10_globalid": "uuid-xyz", "full_name": "Производственные зоны", }, } db = _FakeDB() monkeypatch.setattr(genplan_zones_sync, "fetch_functional_zones", lambda: [feat]) monkeypatch.setattr(genplan_zones_sync, "SessionLocal", lambda: db) result = genplan_zones_sync.sync_functional_zones() assert result == {"zones": 0} assert db.executed == [] def test_sync_globalid_dedup_two_identical_features(monkeypatch: Any) -> None: """Две фичи с одинаковым globalid — оба UPSERT (ON CONFLICT дедуп на уровне БД).""" feat = _make_feature(globalid="dup-uuid") db = _FakeDB() monkeypatch.setattr(genplan_zones_sync, "fetch_functional_zones", lambda: [feat, feat]) monkeypatch.setattr(genplan_zones_sync, "SessionLocal", lambda: db) result = genplan_zones_sync.sync_functional_zones() # Оба вызова UPSERT прошли (ON CONFLICT DO UPDATE разрулит на уровне БД). assert result == {"zones": 2} assert len(db.executed) == 2 def test_sync_savepoint_per_row_error_does_not_abort_batch(monkeypatch: Any) -> None: """SAVEPOINT per-row: ошибка в одной фиче не валит батч остальных.""" call_count = 0 class _FaultyDB(_FakeDB): @contextmanager def begin_nested(self) -> Any: nonlocal call_count call_count += 1 if call_count == 1: raise Exception("upsert error row 1") yield features = [_make_feature(globalid=f"uuid-{i}") for i in range(3)] db = _FaultyDB() monkeypatch.setattr(genplan_zones_sync, "fetch_functional_zones", lambda: features) monkeypatch.setattr(genplan_zones_sync, "SessionLocal", lambda: db) result = genplan_zones_sync.sync_functional_zones() # Строка 1 упала (savepoint), строки 2 и 3 записались. assert result == {"zones": 2} assert db.committed is True assert db.closed is True def test_sync_empty_wfs_response(monkeypatch: Any) -> None: """WFS вернул 0 фич → zones=0, commit без ошибок.""" db = _FakeDB() monkeypatch.setattr(genplan_zones_sync, "fetch_functional_zones", lambda: []) monkeypatch.setattr(genplan_zones_sync, "SessionLocal", lambda: db) result = genplan_zones_sync.sync_functional_zones() assert result == {"zones": 0} assert db.committed is True assert db.closed is True def test_upsert_sql_wraps_geom_with_st_makevalid() -> None: """_UPSERT_SQL содержит ST_MakeValid вокруг ST_SetSRID (#1156). Лёгкая проверка без PostGIS: строковое представление compiled SQL должно включать ST_MakeValid — гарантирует что backfill-логика присутствует в тексте запроса. """ sql_text = str(genplan_zones_sync._UPSERT_SQL) assert "ST_MakeValid" in sql_text assert "ST_SetSRID" in sql_text