Files
geointel/backend/tests/test_area_crs_semantics.py
T
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

160 lines
5.2 KiB
Python

from __future__ import annotations
from uuid import uuid4
from geoalchemy2.shape import from_shape, to_shape
from pyproj import Transformer
import pytest
from shapely.geometry import Polygon, mapping
from shapely.ops import transform
from app.core.errors import AppError
from app.models import Area, Project
from app.schemas.area import AreaCreate, AreaUpdate
from app.services.area_service import AreaService
from app.utils.geometry import area_m2, normalize_area_to_epsg4326
class FakeSession:
def __init__(self, objects=None) -> None:
self.objects = objects or {}
self.added = []
self.commits = 0
self.refreshes = []
def get(self, model, item_id):
return self.objects.get((model, item_id))
def add(self, item) -> None:
self.added.append(item)
def commit(self) -> None:
self.commits += 1
def refresh(self, item) -> None:
self.refreshes.append(item)
def _wgs84_polygon(offset: float = 0.0) -> Polygon:
return Polygon(
[
(5.00 + offset, 51.00),
(5.01 + offset, 51.00),
(5.01 + offset, 51.01),
(5.00 + offset, 51.01),
(5.00 + offset, 51.00),
]
)
def _to_lambert(geometry: Polygon) -> Polygon:
transformer = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
return transform(transformer.transform, geometry)
def test_create_area_transforms_declared_lambert_geometry_before_storage() -> None:
project_id = uuid4()
db = FakeSession({(Project, project_id): Project(id=project_id, name="Belgium")})
source = _wgs84_polygon()
area = AreaService.create_area(
db,
project_id,
AreaCreate(name="Lambert AOI", geometry=mapping(_to_lambert(source)), crs="EPSG:31370"),
)
stored = to_shape(area.geometry)
assert stored.bounds == pytest.approx(source.bounds, abs=1e-7)
assert area.original_crs == "EPSG:31370"
assert area.area_m2 == pytest.approx(area_m2(normalize_area_to_epsg4326(mapping(source), "EPSG:4326")[0]))
assert area.area_m2 and area.area_m2 > 0
assert to_shape(area.bbox).bounds == pytest.approx(source.bounds, abs=1e-7)
def test_patch_area_replaces_geometry_and_recomputes_all_spatial_fields() -> None:
area_id = uuid4()
project_id = uuid4()
original = _wgs84_polygon()
normalized, _ = normalize_area_to_epsg4326(mapping(original), "EPSG:4326")
area = Area(
id=area_id,
project_id=project_id,
name="Original",
geometry=from_shape(normalized, srid=4326),
bbox=from_shape(normalized.envelope, srid=4326),
original_crs="EPSG:4326",
area_m2=area_m2(normalized),
)
db = FakeSession({(Area, area_id): area})
replacement = _wgs84_polygon(offset=0.05)
updated = AreaService.update_area(
db,
area_id,
AreaUpdate(
name="Replacement",
geometry=mapping(_to_lambert(replacement)),
crs="EPSG:31370",
),
)
assert updated.name == "Replacement"
assert updated.original_crs == "EPSG:31370"
assert to_shape(updated.geometry).bounds == pytest.approx(replacement.bounds, abs=1e-7)
assert to_shape(updated.bbox).bounds == pytest.approx(replacement.bounds, abs=1e-7)
assert updated.area_m2 and updated.area_m2 > 0
assert db.commits == 1
@pytest.mark.parametrize(
("geometry", "crs", "message_fragment"),
[
(mapping(_wgs84_polygon()), "EPSG:not-real", "unknown or invalid"),
(mapping(_wgs84_polygon()), "EPSG:4979", "exactly two spatial axes"),
(
{
"type": "Polygon",
"coordinates": [[[5.0, 51.0], [float("nan"), 51.0], [5.1, 51.1], [5.0, 51.0]]],
},
"EPSG:4326",
"finite",
),
(mapping(Polygon([(10.0, 51.0), (10.1, 51.0), (10.1, 51.1), (10.0, 51.0)])), "EPSG:4326", "workbench domain"),
(
{
"type": "Polygon",
"coordinates": [[[5.0, 51.0], [5.1, 51.1], [5.1, 51.0], [5.0, 51.1], [5.0, 51.0]]],
},
"EPSG:4326",
"invalid",
),
({"type": "Point", "coordinates": [5.0, 51.0]}, "EPSG:4326", "Polygon or MultiPolygon"),
],
)
def test_create_area_rejects_invalid_crs_nonfinite_and_out_of_domain_geometry(
geometry: dict,
crs: str,
message_fragment: str,
) -> None:
project_id = uuid4()
db = FakeSession({(Project, project_id): Project(id=project_id, name="Belgium")})
with pytest.raises(AppError) as exc_info:
AreaService.create_area(db, project_id, AreaCreate(name="Invalid", geometry=geometry, crs=crs))
assert exc_info.value.code == "INVALID_GEOMETRY"
assert message_fragment in exc_info.value.message
assert db.commits == 0
def test_patch_area_rejects_crs_without_replacement_geometry() -> None:
area_id = uuid4()
area = Area(id=area_id, project_id=uuid4(), name="AOI", original_crs="EPSG:4326")
db = FakeSession({(Area, area_id): area})
with pytest.raises(AppError) as exc_info:
AreaService.update_area(db, area_id, AreaUpdate(crs="EPSG:31370"))
assert exc_info.value.code == "INVALID_AREA_CRS_UPDATE"
assert db.commits == 0