Files
geointel/backend/tests/test_sprint106_map_bbox_extract.py
T
Codex 851d7220df
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled
Add map area selection extraction
2026-06-25 01:59:57 +02:00

179 lines
6.2 KiB
Python

from __future__ import annotations
import uuid
from pathlib import Path
from types import SimpleNamespace
from geoalchemy2.shape import from_shape
from shapely.geometry import Polygon
from app.core.errors import AppError
from app.models import Dataset, VectorFeature
from app.services.vector_feature_service import VectorFeatureService
ROOT = Path(__file__).resolve().parents[2]
class _FakeQuery:
def __init__(self, rows: list[VectorFeature]) -> None:
self.rows = rows
self.limit_value: int | None = None
def filter(self, *args, **kwargs): # noqa: ANN002, ANN003
return self
def order_by(self, *args, **kwargs): # noqa: ANN002, ANN003
return self
def limit(self, value: int):
self.limit_value = value
return self
def all(self) -> list[VectorFeature]:
if self.limit_value is None:
return self.rows
return self.rows[: self.limit_value]
class _FakeSession:
def __init__(self, rows: list[VectorFeature]) -> None:
self.rows = rows
def query(self, model): # noqa: ANN001
assert model is VectorFeature
return _FakeQuery(self.rows)
def _feature_row(dataset_id: uuid.UUID, *, source_feature_id: str, name: str) -> VectorFeature:
return VectorFeature(
id=uuid.uuid4(),
dataset_id=dataset_id,
feature_class="parcel",
source_feature_id=source_feature_id,
properties_json={"name": name},
geometry=from_shape(
Polygon(
[
(5.0, 51.0),
(5.001, 51.0),
(5.001, 51.001),
(5.0, 51.001),
(5.0, 51.0),
]
),
srid=4326,
),
)
def test_vector_feature_service_extracts_bbox_geojson_from_persisted_rows() -> None:
dataset_id = uuid.uuid4()
rows = [_feature_row(dataset_id, source_feature_id="src-1", name="Test parcel")]
result = VectorFeatureService.select_features_by_bbox(
_FakeSession(rows),
dataset_id=dataset_id,
bbox={"min_x": 4.9, "min_y": 50.9, "max_x": 5.2, "max_y": 51.2, "crs": "EPSG:4326"},
limit=25,
)
assert result["feature_count"] == 1
assert result["truncated"] is False
assert result["geojson"]["type"] == "FeatureCollection"
feature = result["geojson"]["features"][0]
assert feature["type"] == "Feature"
assert feature["properties"]["vector_feature_id"] == str(rows[0].id)
assert feature["properties"]["source_feature_id"] == "src-1"
assert feature["properties"]["feature_class"] == "parcel"
assert feature["properties"]["name"] == "Test parcel"
assert feature["geometry"]["type"] == "Polygon"
def test_vector_feature_service_rejects_invalid_bbox() -> None:
try:
VectorFeatureService.select_features_by_bbox(
_FakeSession([]),
dataset_id=uuid.uuid4(),
bbox={"min_x": 5.2, "min_y": 50.9, "max_x": 5.0, "max_y": 51.2, "crs": "EPSG:4326"},
)
except AppError as exc:
assert exc.code == "INVALID_SELECTION_BBOX"
else: # pragma: no cover
raise AssertionError("Expected INVALID_SELECTION_BBOX")
def test_vector_select_route_is_project_scoped_and_enveloped(monkeypatch) -> None:
from app.api.routes import datasets as dataset_routes
project_id = uuid.uuid4()
dataset_id = uuid.uuid4()
dataset = Dataset(id=dataset_id, project_id=project_id, dataset_type="vector", source="fixture", name="Vector")
expected_payload = {
"selection_bbox": {"min_x": 4.9, "min_y": 50.9, "max_x": 5.2, "max_y": 51.2, "crs": "EPSG:4326"},
"feature_count": 0,
"limit": 100,
"truncated": False,
"geojson": {"type": "FeatureCollection", "features": []},
}
monkeypatch.setattr(dataset_routes.DatasetService, "get_dataset", lambda db, selected_id: dataset)
monkeypatch.setattr(
dataset_routes.VectorFeatureService,
"select_features_by_bbox",
lambda db, dataset_id, bbox, limit=100: expected_payload,
)
response = dataset_routes.select_vector_features(
project_id=project_id,
dataset_id=dataset_id,
payload=dataset_routes.VectorSelectionRequest(
bbox=dataset_routes.VectorSelectionBBox(min_x=4.9, min_y=50.9, max_x=5.2, max_y=51.2),
limit=100,
),
db=SimpleNamespace(),
)
assert response == {"data": expected_payload}
def test_vector_select_route_rejects_non_vector_dataset(monkeypatch) -> None:
from app.api.routes import datasets as dataset_routes
project_id = uuid.uuid4()
dataset_id = uuid.uuid4()
dataset = Dataset(id=dataset_id, project_id=project_id, dataset_type="raster", source="fixture", name="Raster")
monkeypatch.setattr(dataset_routes.DatasetService, "get_dataset", lambda db, selected_id: dataset)
try:
dataset_routes.select_vector_features(
project_id=project_id,
dataset_id=dataset_id,
payload=dataset_routes.VectorSelectionRequest(
bbox=dataset_routes.VectorSelectionBBox(min_x=4.9, min_y=50.9, max_x=5.2, max_y=51.2),
limit=100,
),
db=SimpleNamespace(),
)
except AppError as exc:
assert exc.code == "DATASET_NOT_VECTOR"
else: # pragma: no cover
raise AssertionError("Expected DATASET_NOT_VECTOR")
def test_frontend_exposes_map_bbox_selection_contracts() -> None:
api_client = (ROOT / "frontend" / "src" / "services" / "api" / "datasets.ts").read_text(encoding="utf-8")
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
geomap = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(encoding="utf-8")
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
assert "selectVectorFeatures" in api_client
assert "Area selection" in map_workspace
assert "Start map bbox" in map_workspace
assert "Run area extract" in map_workspace
assert "Download area GeoJSON" in map_workspace
assert "bboxSelectionMode" in geomap
assert "selection-bbox" in geomap
assert "selection-result" in geomap
assert "useMapSelectionExtract" in app