gendesign/backend/tests/workers/test_genplan_zones_sync.py
lekss361 3921180d07
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
fix(sf): ST_MakeValid genplan geom + backfill 881 invalid polygons (#1157)
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 17:59:04 +00:00

213 lines
8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Тесты 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