Files
geointel/backend/tests/test_sprint233_operational_completion.py
T

488 lines
17 KiB
Python

from __future__ import annotations
import json
from datetime import UTC, datetime
from hashlib import sha256
from pathlib import Path
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from pydantic import ValidationError
from app.main import app
from app.models import Dataset, Export, SourceRegistry, SourceSnapshot
from app.schemas.export import ExportCreateResponse, MapResultExportRequest
from app.schemas.project import ProjectRead
from app.services.export_service import ExportService
from app.services.project_service import ProjectService
from app.services.storage_service import StorageService
from app.services.source_registry_service import SourceRegistryService
from app.services.temporal_analysis_service import TemporalAnalysisService
from app.services.thematic_raster_analysis_service import ThematicRasterAnalysisService
class FakeSession:
def __init__(self, rows=None):
self.rows = rows or {}
self.added = []
def get(self, model, row_id):
return self.rows.get((model, row_id))
def add(self, row):
self.added.append(row)
def commit(self):
return None
def refresh(self, row):
return row
def bbox_payload() -> dict:
return {
"min_x": 5.10,
"min_y": 51.17,
"max_x": 5.11,
"max_y": 51.18,
"crs": "EPSG:4326",
}
def governed_dataset(
*,
project_id,
dataset_id,
name: str,
dataset_type: str,
source_key: str,
dataset_role: str = "source",
) -> Dataset:
"""Build an in-memory stand-in for a passed governed dataset.
Map-result export is an operational consumption boundary. These tests
must therefore model the same source registry/snapshot, checksum and
passed-contract evidence supplied by a real adapter rather than relying
on an old transient Dataset fixture.
"""
source = SourceRegistry(
id=uuid4(),
**SourceRegistryService.definition_for(source_key).as_model_values(),
)
checksum = sha256(f"{dataset_id}:{source_key}:{dataset_type}".encode("utf-8")).hexdigest()
snapshot = SourceSnapshot(
id=uuid4(),
source_registry_id=source.id,
snapshot_key=f"test:{source_key}:{checksum}",
checksum_sha256=checksum,
fetched_at=datetime.now(UTC),
crs="EPSG:31370",
units=source.default_units,
spatial_resolution_json={"x": 1.0, "y": 1.0, "unit": "m"},
temporal_coverage_json={"status": "test-fixture"},
geographic_coverage_json={"zone": "Flanders"},
observed_schema_json={"dataset_type": dataset_type},
freshness_status="current",
ingest_status="ingested",
known_limitations_json=["In-memory governed fixture used only by this export test."],
snapshot_metadata_json={"fixture_mode": True},
)
return Dataset(
id=dataset_id,
project_id=project_id,
name=name,
dataset_type=dataset_type,
source="governed test fixture",
dataset_role=dataset_role,
source_name=source.source_key,
source_registry_id=source.id,
source_snapshot_id=snapshot.id,
source_registry=source,
source_snapshot=snapshot,
data_contract_key=("geointel.raster.geotiff" if dataset_type == "raster" else "geointel.vector.geojson"),
data_contract_version="1.0.0",
validation_status="passed",
provenance_status="complete",
lineage_status="not_applicable",
quarantine_status="not_quarantined",
checksum_sha256=checksum,
metadata_json={"fixture_mode": True},
source_metadata={"fixture_mode": True, "source_registry_key": source.source_key},
provenance_metadata={"fixture_mode": True, "source_snapshot_id": str(snapshot.id)},
status="ready",
)
def test_map_result_export_request_requires_a_complete_target() -> None:
with pytest.raises(ValidationError):
MapResultExportRequest(project_id=uuid4(), mode="current", bbox=bbox_payload())
with pytest.raises(ValidationError):
MapResultExportRequest(project_id=uuid4(), mode="evolution", bbox=bbox_payload())
with pytest.raises(ValidationError):
MapResultExportRequest(
project_id=uuid4(),
mode="current",
dataset_id=uuid4(),
bbox=bbox_payload(),
partitioned=True,
)
def test_current_vector_map_result_uses_authoritative_selection_export(monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
area_id = uuid4()
dataset = governed_dataset(
project_id=project_id,
dataset_id=dataset_id,
name="buildings.geojson",
dataset_type="vector",
source_key="grb",
)
db = FakeSession({(Dataset, dataset_id): dataset})
expected = ExportCreateResponse(
export_id=uuid4(),
path="storage/exports/buildings-selection.geojson",
status="ready",
export_type="vector_selection_geojson",
)
captured: dict = {}
def fake_vector_export(*_args, **kwargs):
captured.update(kwargs)
return expected
monkeypatch.setattr(ExportService, "export_vector_selection_geojson", fake_vector_export)
response = ExportService.export_map_result(
db,
MapResultExportRequest(
project_id=project_id,
mode="current",
dataset_id=dataset_id,
area_id=area_id,
bbox=bbox_payload(),
theme_id="buildings",
),
)
assert response is expected
assert captured["area_id"] == area_id
assert captured["limit"] == 1000
def test_partitioned_vector_map_result_uses_governed_partition_export(monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
dataset = governed_dataset(
project_id=project_id,
dataset_id=dataset_id,
name="vha-municipality.geojson",
dataset_type="vector",
source_key="vmm_vha_bathymetry_profiles",
)
db = FakeSession({(Dataset, dataset_id): dataset})
expected = ExportCreateResponse(
export_id=uuid4(),
path="storage/exports/bathymetry-profile-selection.geojson",
status="ready",
export_type="partitioned_vector_selection_geojson",
)
captured: dict = {}
def fake_partition_export(*args, **kwargs):
captured["dataset"] = args[1]
captured.update(kwargs)
return expected
monkeypatch.setattr(
ExportService,
"export_partitioned_vector_selection_geojson",
fake_partition_export,
)
response = ExportService.export_map_result(
db,
MapResultExportRequest(
project_id=project_id,
mode="current",
dataset_id=dataset_id,
bbox=bbox_payload(),
partitioned=True,
partition_scope_key="flanders",
theme_id="bathymetry",
),
)
assert response is expected
assert captured["dataset"] is dataset
assert captured["partition_scope_key"] == "flanders"
assert captured["limit"] == 1000
def test_raster_map_result_is_recomputed_and_persisted(tmp_path, monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
dataset = governed_dataset(
project_id=project_id,
dataset_id=dataset_id,
name="space-occupation.tif",
dataset_type="raster",
source_key="department_omgeving_thematic_raster",
)
db = FakeSession({(Dataset, dataset_id): dataset})
export_path = tmp_path / "space-occupation-analysis.json"
captured: dict = {}
def fake_analyze(_db, captured_project_id, captured_dataset_id, payload):
captured.update(
project_id=captured_project_id,
dataset_id=captured_dataset_id,
area_id=payload.area_id,
)
return {
"selection_bbox": bbox_payload(),
"summary": {"metric_label": "Ruimtebeslag", "metric_value": 12.5, "metric_unit": "ha"},
}
monkeypatch.setattr(ThematicRasterAnalysisService, "analyze", fake_analyze)
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path))
response = ExportService.export_map_result(
db,
MapResultExportRequest(
project_id=project_id,
mode="current",
dataset_id=dataset_id,
bbox=bbox_payload(),
theme_id="space_occupation",
),
)
persisted = [row for row in db.added if isinstance(row, Export)]
assert response.export_type == "map_analysis_json"
assert len(persisted) == 1
assert persisted[0].metadata_json["server_recomputed"] is True
assert persisted[0].metadata_json["theme_id"] == "space_occupation"
assert captured == {"project_id": project_id, "dataset_id": dataset_id, "area_id": None}
assert json.loads(export_path.read_text(encoding="utf-8"))["result"]["summary"]["metric_value"] == 12.5
def test_evolution_map_result_is_recomputed_and_persisted(tmp_path, monkeypatch) -> None:
project_id = uuid4()
earlier_id = uuid4()
later_id = uuid4()
earlier_dataset = governed_dataset(
project_id=project_id,
dataset_id=earlier_id,
name="forest-earlier.geojson",
dataset_type="vector",
source_key="inbo_bwk_natura2000",
)
later_dataset = governed_dataset(
project_id=project_id,
dataset_id=later_id,
name="forest-later.geojson",
dataset_type="vector",
source_key="inbo_bwk_natura2000",
)
db = FakeSession(
{
(Dataset, earlier_id): earlier_dataset,
(Dataset, later_id): later_dataset,
}
)
export_path = tmp_path / "forest-evolution.json"
captured: dict = {}
class Comparison:
def model_dump(self, *, mode):
assert mode == "json"
return {"temporal_series_key": "forest", "metric": {"absolute_change": -2.0}}
def fake_compare(_db, *, project_id, payload):
captured.update(project_id=project_id, payload=payload)
return Comparison()
monkeypatch.setattr(TemporalAnalysisService, "compare", fake_compare)
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path))
response = ExportService.export_map_result(
db,
MapResultExportRequest(
project_id=project_id,
mode="evolution",
earlier_dataset_id=earlier_id,
later_dataset_id=later_id,
bbox=bbox_payload(),
theme_id="forest",
),
)
assert response.export_type == "map_evolution_json"
assert captured["project_id"] == project_id
assert captured["payload"].earlier_dataset_id == earlier_id
assert json.loads(export_path.read_text(encoding="utf-8"))["metric"]["absolute_change"] == -2.0
def test_map_result_export_endpoint_uses_canonical_envelope(monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
export_id = uuid4()
monkeypatch.setattr(
ExportService,
"export_map_result",
lambda *_args: ExportCreateResponse(
export_id=export_id,
path="storage/exports/map-analysis.json",
status="ready",
export_type="map_analysis_json",
),
)
response = TestClient(app).post(
"/api/v1/exports/map-result",
json={
"project_id": str(project_id),
"mode": "current",
"dataset_id": str(dataset_id),
"bbox": bbox_payload(),
"theme_id": "space_occupation",
},
)
assert response.status_code == 200
assert response.json() == {
"data": {
"export_id": str(export_id),
"path": "storage/exports/map-analysis.json",
"status": "ready",
"export_type": "map_analysis_json",
"metadata_json": None,
}
}
def test_frontend_persists_map_result_before_opening_downloads() -> None:
root = Path(__file__).resolve().parents[2]
workspace = (root / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
hook = (root / "frontend/src/hooks/useExportWorkflow.ts").read_text(encoding="utf-8")
api = (root / "frontend/src/services/api/exports.ts").read_text(encoding="utf-8")
assert "persistActiveResultAndOpenDownloads" in workspace
assert "onPersistMapResult(payload)" in workspace
assert "Bewaar in downloads" in workspace
assert "persistMapResult" in hook
assert "exportsApi.exportMapResult(payload)" in hook
assert "/api/v1/exports/map-result" in api
def test_project_list_supports_exact_canonical_workspace_lookup(monkeypatch) -> None:
project_id = uuid4()
captured: dict = {}
def fake_list(_db, *, limit, offset, name, project_status):
captured.update(limit=limit, offset=offset, name=name, project_status=project_status)
return [
ProjectRead(
id=project_id,
name="Kempen Regional Workbench",
region="Kempen",
status="active",
)
], 1
monkeypatch.setattr(ProjectService, "list_projects", fake_list)
response = TestClient(app).get(
"/api/v1/projects",
params={"name": "Kempen Regional Workbench", "limit": 1},
)
assert response.status_code == 200
assert response.json()["data"]["items"][0]["id"] == str(project_id)
assert captured == {
"limit": 1,
"offset": 0,
"name": "Kempen Regional Workbench",
"project_status": "active",
}
def test_frontend_fetches_canonical_workspace_outside_default_project_page() -> None:
root = Path(__file__).resolve().parents[2]
workflow = (root / "frontend/src/hooks/useProjectWorkspace.ts").read_text(encoding="utf-8")
api = (root / "frontend/src/services/api/projects.ts").read_text(encoding="utf-8")
assert "projectsApi.list({ name: REGIONAL_WORKSPACE_PROJECT_NAME, limit: 1 })" in workflow
assert "[...canonicalResponse.items, ...response.items]" in workflow
assert "new URLSearchParams()" in api
def test_theme_failures_name_the_source_and_reason() -> None:
root = Path(__file__).resolve().parents[2]
hook = (root / "frontend/src/hooks/useMapThemeSelectionInsights.ts").read_text(encoding="utf-8")
assert "queries[index]?.dataset?.name" in hook
assert "queries[index]?.acquisition?.displayName" in hook
assert "reason: formatError(item.reason" in hook
assert "failure.dataset}: ${failure.reason}" in hook
def test_workspace_navigation_resets_the_actual_scroll_container() -> None:
root = Path(__file__).resolve().parents[2]
app = (root / "frontend/src/App.tsx").read_text(encoding="utf-8")
assert "const workbenchMainRef = useRef<HTMLElement | null>(null)" in app
assert "workbenchMainRef.current?.scrollTo({ top: 0, left: 0 })" in app
assert "<main" in app
assert "ref={workbenchMainRef}" in app
def test_quality_scores_have_plain_language_interpretation() -> None:
root = Path(__file__).resolve().parents[2]
quality = (root / "frontend/src/components/quality/QualityResultsPanel.tsx").read_text(encoding="utf-8")
detection = (root / "frontend/src/components/detection/DetectionLab.tsx").read_text(encoding="utf-8")
assert "Laatste score (0-1)" in quality
assert "Bruikbaar na controle" in quality
assert "Verkennend, controle vereist" in quality
assert "detectionQualityInterpretation" in detection
assert "Verkennend resultaat; beoordeel fouten" in detection
def test_map_and_detection_workspaces_avoid_page_length_driven_layouts() -> None:
root = Path(__file__).resolve().parents[2]
styles = (root / "frontend/src/styles/app.css").read_text(encoding="utf-8")
premium = (root / "frontend/src/styles/premium.css").read_text(encoding="utf-8")
assert "height: clamp(34rem, calc(100dvh - 10rem), 58rem);" in styles
assert ".geo-theme-list" in styles and "overflow-y: auto;" in styles
assert "@media (max-width: 1500px)" in styles
assert ".workspace-grid-ai {\n grid-template-columns: minmax(0, 1fr);" in premium
assert "max-height: none;" in premium
def test_detection_lab_only_receives_operational_imagery_rasters() -> None:
root = Path(__file__).resolve().parents[2]
app_source = (root / "frontend/src/App.tsx").read_text(encoding="utf-8")
capability_source = (root / "frontend/src/lib/datasetCapabilities.ts").read_text(encoding="utf-8")
assert "department_omgeving_thematic_raster" in capability_source
assert "digitaal_vlaanderen_dhmv" in capability_source
assert "vmm_flood_hazard" in capability_source
assert "dataset.dataset_type !== 'raster' || dataset.status !== 'ready'" in capability_source
assert "const detectionRasterDatasets = useMemo(" in app_source
assert "rasterDatasets: detectionRasterDatasets" in app_source
assert "rasterDatasets={detectionRasterDatasets}" in app_source
assert "!isDetectionImageryDataset(selectedDataset)" in app_source
def test_download_workspace_surfaces_map_results_in_plain_dutch() -> None:
root = Path(__file__).resolve().parents[2]
exports = (root / "frontend/src/components/exports/ExportCenter.tsx").read_text(encoding="utf-8")
assert "Gebiedsanalyse (JSON)" in exports
assert "Historische vergelijking (JSON)" in exports
assert "Kaartselectie (GeoJSON)" in exports
assert "Klaar om te delen" in exports
assert "Downloads vernieuwen" in exports
assert "JSON bekijken" in exports