from __future__ import annotations import json from pathlib import Path from types import SimpleNamespace from uuid import uuid4 from fastapi.testclient import TestClient from geoalchemy2.shape import from_shape, to_shape from shapely.geometry import box from app.core.errors import AppError from app.main import app from app.models import Area, Dataset, Export, SourceRegistry, SourceSnapshot from app.schemas.export import ExportCreateResponse from app.services.export_service import ExportService from app.services.storage_service import StorageService from app.services.vector_feature_service import VectorFeatureService ROOT = Path(__file__).resolve().parents[2] class FakeSession: def __init__(self, rows): self.rows = rows self.added = [] def get(self, model, row_id): row = self.rows.get((model, row_id)) if row is not None: return row for item in self.added: if isinstance(item, model) and item.id == row_id: return item return None def add(self, row): self.added.append(row) def commit(self): return None def refresh(self, row): return row def _govern_fixture_dataset(dataset: Dataset) -> Dataset: source_id = uuid4() snapshot_id = uuid4() checksum = "a" * 64 source = SourceRegistry( id=source_id, source_key="grb", display_name="GRB map export test source", classification="authoritative", authority_name="Digitaal Vlaanderen", authority_scope_json={"zone": "Flanders"}, usage_policy_json={"ground_truth_allowed": True}, ) snapshot = SourceSnapshot( id=snapshot_id, source_registry_id=source_id, snapshot_key=f"map-export-grb-{dataset.id}", checksum_sha256=checksum, ingest_status="ingested", freshness_status="current", ) dataset.source = "grb" dataset.source_name = "grb" dataset.checksum_sha256 = checksum dataset.source_registry_id = source_id dataset.source_snapshot_id = snapshot_id dataset.data_contract_key = "geointel.vector.geojson" dataset.data_contract_version = "1.0.0" dataset.validation_status = "passed" dataset.provenance_status = "complete" dataset.lineage_status = "not_applicable" dataset.quarantine_status = "not_quarantined" dataset.status = "ready" dataset.source_registry = source dataset.source_snapshot = snapshot return dataset def test_vector_selection_geojson_export_persists_handoff_artifact(tmp_path, monkeypatch) -> None: project_id = uuid4() dataset_id = uuid4() export_path = tmp_path / "exports" / "selection.geojson" dataset = _govern_fixture_dataset(Dataset( id=dataset_id, project_id=project_id, name="candidate.geojson", dataset_type="vector", source="fixture", status="ready", )) db = FakeSession({(Dataset, dataset_id): dataset}) selection_bbox = {"min_x": 4.9, "min_y": 50.9, "max_x": 5.2, "max_y": 51.2, "crs": "EPSG:4326"} selection_payload = { "selection_bbox": selection_bbox, "feature_count": 1, "limit": 250, "truncated": False, "geojson": { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": {"type": "Point", "coordinates": [5.0, 51.0]}, "properties": {"vector_feature_id": "vf-1"}, } ], }, } monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path)) monkeypatch.setattr(VectorFeatureService, "select_features_by_bbox", lambda *_args, **_kwargs: selection_payload) response = ExportService.export_vector_selection_geojson(db, dataset_id, selection_bbox, limit=250, name="selected-buildings") exports = [item for item in db.added if isinstance(item, Export)] assert len(exports) == 1 assert response.export_id == exports[0].id assert response.export_type == "vector_selection_geojson" assert response.metadata_json["source"] == "vector_selection" assert response.metadata_json["dataset_id"] == str(dataset_id) assert response.metadata_json["selection_bbox"] == selection_bbox assert response.metadata_json["feature_count"] == 1 assert response.metadata_json["source_table"] == "vector_features" assert json.loads(export_path.read_text(encoding="utf-8"))["features"][0]["properties"]["vector_feature_id"] == "vf-1" def test_vector_selection_geojson_export_endpoint_returns_canonical_envelope(monkeypatch) -> None: export_id = uuid4() dataset_id = uuid4() area_id = uuid4() expected_bbox = {"min_x": 4.9, "min_y": 50.9, "max_x": 5.2, "max_y": 51.2, "crs": "EPSG:4326"} captured: dict = {} def fake_export(*_args, **kwargs): captured.update(kwargs) return ExportCreateResponse( export_id=export_id, path="storage/exports/demo-selection.geojson", status="ready", export_type="vector_selection_geojson", metadata_json={"source": "vector_selection", "selection_bbox": expected_bbox}, ) monkeypatch.setattr(ExportService, "export_vector_selection_geojson", fake_export) response = TestClient(app).post( "/api/v1/exports/geojson", json={ "dataset_id": str(dataset_id), "area_id": str(area_id), "export_kind": "vector_selection", "bbox": expected_bbox, "limit": 250, }, ) assert response.status_code == 200 payload = response.json() assert set(payload) == {"data"} assert payload["data"]["export_id"] == str(export_id) assert payload["data"]["export_type"] == "vector_selection_geojson" assert payload["data"]["metadata_json"]["selection_bbox"] == expected_bbox assert captured["area_id"] == area_id def test_vector_selection_export_uses_exact_area_scope_when_requested(tmp_path, monkeypatch) -> None: project_id = uuid4() dataset_id = uuid4() area_id = uuid4() dataset = _govern_fixture_dataset(Dataset( id=dataset_id, project_id=project_id, name="regional-buildings.geojson", dataset_type="vector", source="fixture", status="ready", )) area_shape = box(5.0, 51.1, 5.2, 51.3) area_geometry = from_shape(area_shape, srid=4326) area = SimpleNamespace(id=area_id, project_id=project_id, name="Gemeente Mol", geometry=area_geometry) db = FakeSession({(Dataset, dataset_id): dataset, (Area, area_id): area}) selection_bbox = {"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3, "crs": "EPSG:4326"} captured: dict = {} selection_payload = { "selection_bbox": selection_bbox, "selection_area_id": str(area_id), "feature_count": 0, "limit": 250, "truncated": False, "geojson": {"type": "FeatureCollection", "features": []}, } monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(tmp_path / "selection.geojson")) monkeypatch.setattr(VectorFeatureService, "can_use_full_area_fast_path", lambda *_args: True) def fake_select(*_args, **kwargs): captured.update(kwargs) return selection_payload monkeypatch.setattr(VectorFeatureService, "select_features_by_bbox", fake_select) response = ExportService.export_vector_selection_geojson( db, dataset_id, selection_bbox, area_id=area_id, ) assert to_shape(captured["selection_geometry"]).equals(area_shape) assert captured["selection_area_id"] == area_id assert captured["full_dataset_area"] is True assert response.metadata_json["selection_area_id"] == str(area_id) def test_area_constrained_bbox_uses_intersection_and_disables_full_area_fast_path(tmp_path, monkeypatch) -> None: project_id = uuid4() dataset_id = uuid4() area_id = uuid4() dataset = _govern_fixture_dataset(Dataset( id=dataset_id, project_id=project_id, area_id=area_id, name="mol-buildings.geojson", dataset_type="vector", source="fixture", source_metadata={"geometry_clipped_to_area": True}, status="ready", )) area_shape = box(5.0, 51.0, 5.2, 51.2) area = SimpleNamespace( id=area_id, project_id=project_id, name="Gemeente Mol - officiƫle grens", geometry=from_shape(area_shape, srid=4326), ) db = FakeSession({(Dataset, dataset_id): dataset, (Area, area_id): area}) crossing_bbox = {"min_x": 4.9, "min_y": 51.1, "max_x": 5.1, "max_y": 51.3, "crs": "EPSG:4326"} captured: dict = {} selection_payload = { "selection_bbox": crossing_bbox, "selection_area_id": str(area_id), "feature_count": 0, "limit": 250, "truncated": False, "geojson": {"type": "FeatureCollection", "features": []}, } monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(tmp_path / "selection.geojson")) def fake_select(*_args, **kwargs): captured.update(kwargs) return selection_payload monkeypatch.setattr(VectorFeatureService, "select_features_by_bbox", fake_select) ExportService.export_vector_selection_geojson( db, dataset_id, crossing_bbox, area_id=area_id, ) assert to_shape(captured["selection_geometry"]).equals(box(5.0, 51.1, 5.1, 51.2)) assert captured["selection_area_id"] == area_id assert captured["full_dataset_area"] is False def test_area_constrained_bbox_rejects_selection_outside_work_area() -> None: area_geometry = from_shape(box(5.0, 51.0, 5.2, 51.2), srid=4326) outside_bbox = {"min_x": 4.0, "min_y": 50.0, "max_x": 4.1, "max_y": 50.1, "crs": "EPSG:4326"} try: VectorFeatureService.constrain_bbox_to_area(outside_bbox, area_geometry) except AppError as error: assert error.code == "VECTOR_SELECTION_OUTSIDE_AREA" assert error.status_code == 422 else: raise AssertionError("Expected an outside-area selection to be rejected") def test_frontend_exposes_map_selection_export_action() -> None: types = (ROOT / "frontend" / "src" / "types.ts").read_text(encoding="utf-8") exports_api = (ROOT / "frontend" / "src" / "services" / "api" / "exports.ts").read_text(encoding="utf-8") export_hook = (ROOT / "frontend" / "src" / "hooks" / "useExportWorkflow.ts").read_text(encoding="utf-8") map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8") app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") assert "'vector_selection'" in types assert "bbox?: VectorSelectionBBox" in exports_api assert "area_id?: string" in exports_api assert "exportMapSelectionGeoJson" in export_hook assert "vector_selection" in export_hook assert "Gebiedsdownload bewaren" in map_workspace assert "onExportMapSelection" in map_workspace assert "selectionExportError" in map_workspace assert "onExportMapSelection={exportMapSelectionGeoJson}" in app