Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local>
201 lines
7.4 KiB
Python
201 lines
7.4 KiB
Python
"""Тесты 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
|