from __future__ import annotations from datetime import UTC, datetime import json from pathlib import Path import ssl import sys from types import SimpleNamespace from urllib.error import URLError from uuid import uuid4 from fastapi.testclient import TestClient from geoalchemy2.shape import from_shape import pytest from shapely.geometry import MultiPolygon, Polygon from app.core.config import Settings from app.core.errors import AppError from app.db.session import get_db from app.main import app from app.models import Area, Dataset, DatasetVersion, Project from app.schemas.bathymetry import BathymetryPartitionFinalizeRequest from app.services.bathymetry_profile_acquisition_service import BathymetryProfileAcquisitionService from app.services.mdk_bathymetry_probe_service import MdkBathymetryProbeService from app.services.vector_feature_service import VectorFeatureService ROOT = Path(__file__).resolve().parents[2] SCRIPTS = ROOT / "scripts" if str(SCRIPTS) not in sys.path: sys.path.insert(0, str(SCRIPTS)) import provision_flanders_geographic_scope as flanders_scope # noqa: E402 class BinaryResponse: def __init__(self, content: bytes, *, content_type: str = "application/xml"): self.content = content self.headers = {"Content-Type": content_type} def __enter__(self): return self def __exit__(self, *_args): return False def read(self, size=-1): return self.content if size < 0 else self.content[:size] class JsonResponse: def __init__(self, payload, *, url="https://geo.api.vlaanderen.be/VRBG/items"): self.payload = payload self.url = url def raise_for_status(self): return None def json(self): return self.payload class SourceSession: def __init__(self, payload): self.payload = payload def get(self, *_args, **_kwargs): return JsonResponse(self.payload) class VersionQuery: def __init__(self, versions): self.versions = versions def filter(self, *_args): return self def all(self): return self.versions class FinalizeSession: def __init__(self, rows, versions=None): self.rows = rows self.versions = versions or [] self.commit_count = 0 def get(self, model, row_id): return self.rows.get((model, row_id)) def query(self, model): assert model is DatasetVersion return VersionQuery(self.versions) def commit(self): self.commit_count += 1 def capabilities_xml() -> bytes: return b""" EL.GridCoverage GeoTIFF """ def test_mdk_probe_parses_capabilities_without_enabling_acquisition() -> None: seen = {} def opener(request, timeout): seen["url"] = request.full_url seen["timeout"] = timeout return BinaryResponse(capabilities_xml()) result = MdkBathymetryProbeService.probe( settings=Settings(_env_file=None), opener=opener, checked_at=datetime(2026, 7, 17, tzinfo=UTC), ) assert result["status"] == "reachable" assert result["tls_verified"] is True assert result["capabilities_reachable"] is True assert result["acquisition_supported"] is False assert result["coverage_identifiers"] == ["EL.GridCoverage"] assert result["advertised_formats"] == ["GeoTIFF"] assert result["response_sha256"] assert "request=GetCapabilities" in seen["url"] assert seen["timeout"] == 20 def test_mdk_probe_reports_tls_failure_and_never_uses_insecure_fallback() -> None: calls = 0 def opener(_request, timeout): nonlocal calls assert timeout == 20 calls += 1 raise URLError(ssl.SSLCertVerificationError("hostname mismatch")) result = MdkBathymetryProbeService.probe( settings=Settings(_env_file=None), opener=opener, ) assert calls == 1 assert result["status"] == "tls_error" assert result["tls_verified"] is False assert result["capabilities_reachable"] is False assert "insecure fallback is prohibited" in result["message"] def test_mdk_readiness_api_uses_canonical_envelope(monkeypatch) -> None: project_id = uuid4() db = SimpleNamespace(get=lambda model, row_id: Project(id=project_id, name="Mol") if model is Project else None) monkeypatch.setattr( MdkBathymetryProbeService, "probe", lambda: { "source_key": "mdk_bcp_bathymetry", "status": "tls_error", "configured_url": "https://example.invalid/wcs", "tls_verified": False, "capabilities_reachable": False, "acquisition_supported": False, "checked_at": "2026-07-18T00:00:00Z", "message": "TLS validation failed.", "limitation_message": "No insecure fallback is permitted.", }, ) app.dependency_overrides[get_db] = lambda: db try: response = TestClient(app).get( f"/api/v1/projects/{project_id}/datasets/bathymetry/sources/mdk_bcp_bathymetry/readiness" ) finally: app.dependency_overrides.clear() assert response.status_code == 200 assert set(response.json()) == {"data"} assert response.json()["data"]["status"] == "tls_error" assert response.json()["data"]["acquisition_supported"] is False def test_partition_finalization_requires_complete_area_accounting_and_updates_versions() -> None: project_id = uuid4() area_ids = [uuid4(), uuid4()] dataset_id = uuid4() project = Project(id=project_id, name="Flanders") areas = [ Area(id=area_ids[0], project_id=project_id, name="Gemeente Mol - officiële grens"), Area(id=area_ids[1], project_id=project_id, name="Gemeente Geel - officiële grens"), ] dataset = Dataset( id=dataset_id, project_id=project_id, area_id=area_ids[0], name="vha.geojson", dataset_type="vector", source="VHA", source_name=BathymetryProfileAcquisitionService.PROVIDER, source_metadata={ "profile_count": 3, "document_count": 2, "structured_depth_count": 1, "measurement_date_min": "1990-01-01", "measurement_date_max": "2020-01-01", }, provenance_metadata={}, status="ready", ) version = DatasetVersion(id=uuid4(), dataset_id=dataset_id, version=1) rows = { (Project, project_id): project, (Area, area_ids[0]): areas[0], (Area, area_ids[1]): areas[1], (Dataset, dataset_id): dataset, } db = FinalizeSession(rows, [version]) payload = BathymetryPartitionFinalizeRequest( partition_scope_key="flanders", expected_area_ids=area_ids, dataset_ids=[dataset_id], no_profile_area_ids=[area_ids[1]], manifest_sha256="a" * 64, observed_at=datetime(2026, 7, 17, tzinfo=UTC), ) result = BathymetryProfileAcquisitionService.finalize_partitions(db, project_id, payload) assert result["regional_partitions_complete"] is True assert result["partition_count"] == 2 assert result["data_partition_count"] == 1 assert result["no_profile_partition_count"] == 1 assert result["profile_count"] == 3 assert dataset.source_metadata["coverage_scope"] == "flanders" assert dataset.source_metadata["municipality"] == "Mol" assert dataset.source_metadata["partitioned_source_audit"] is True assert version.source_metadata == dataset.source_metadata assert version.provenance_metadata == dataset.provenance_metadata assert db.commit_count == 1 incomplete = payload.model_copy(update={"no_profile_area_ids": []}) with pytest.raises(AppError) as exc_info: BathymetryProfileAcquisitionService.finalize_partitions( FinalizeSession(rows, [version]), project_id, incomplete, ) assert exc_info.value.code == "BATHYMETRY_PARTITION_MANIFEST_INCOMPLETE" def test_partition_selection_uses_latest_complete_manifest_without_duplicates() -> None: project_id = uuid4() source_name = BathymetryProfileAcquisitionService.PROVIDER def partition(area_id, manifest, observed_at, data_partition_count): return Dataset( id=uuid4(), project_id=project_id, area_id=area_id, name="vha.geojson", dataset_type="vector", source="VHA", source_name=source_name, source_metadata={ "regional_partitions_complete": True, "partition_scope_key": "flanders", "partition_manifest_sha256": manifest, "partition_manifest_observed_at": observed_at, "data_partition_count": data_partition_count, }, status="ready", ) old = [partition(uuid4(), "a" * 64, "2026-07-16T00:00:00+00:00", 1)] new_area_ids = [uuid4(), uuid4()] new = [ partition(area_id, "b" * 64, "2026-07-17T00:00:00+00:00", 2) for area_id in new_area_ids ] incomplete = [partition(uuid4(), "c" * 64, "2026-07-18T00:00:00+00:00", 2)] selected = VectorFeatureService._latest_complete_partition_manifest( [*old, *new, *incomplete], source_name=source_name, partition_scope_key="flanders", ) assert {dataset.area_id for dataset in selected} == set(new_area_ids) assert all(dataset.source_metadata["partition_manifest_sha256"] == "b" * 64 for dataset in selected) def test_partition_selection_route_uses_exact_municipality_and_canonical_envelope(monkeypatch) -> None: project_id, area_id, dataset_id = uuid4(), uuid4(), uuid4() area_geometry = MultiPolygon( [ Polygon( [ (5.0, 51.0), (5.2, 51.0), (5.2, 51.2), (5.0, 51.2), (5.0, 51.0), ] ) ] ) area = Area( id=area_id, project_id=project_id, name="Gemeente Mol - officiele grens", geometry=from_shape(area_geometry, srid=4326), ) db = SimpleNamespace(get=lambda model, row_id: area if model is Area and row_id == area_id else None) captured = {} def select_partitions(_db, **kwargs): captured.update(kwargs) return { "selection_bbox": kwargs["bbox"], "selection_area_id": area_id, "feature_count": 1, "total_feature_count": 1, "limit": kwargs["limit"], "truncated": False, "geojson": {"type": "FeatureCollection", "features": []}, "partition_count": 1, "available_partition_count": 269, "partition_scope_key": "flanders", "source_name": BathymetryProfileAcquisitionService.PROVIDER, "dataset_ids": [dataset_id], } monkeypatch.setattr(VectorFeatureService, "select_partitioned_features_by_bbox", select_partitions) app.dependency_overrides[get_db] = lambda: db try: response = TestClient(app).post( f"/api/v1/projects/{project_id}/datasets/bathymetry/profiles/partitions/select", json={ "bbox": { "min_x": 5.0, "min_y": 51.0, "max_x": 5.2, "max_y": 51.2, "crs": "EPSG:4326", }, "area_id": str(area_id), "limit": 1000, }, ) finally: app.dependency_overrides.clear() assert response.status_code == 200 assert set(response.json()) == {"data"} assert response.json()["data"]["partition_count"] == 1 assert response.json()["data"]["available_partition_count"] == 269 assert captured["partition_area_id"] == area_id assert captured["source_name"] == BathymetryProfileAcquisitionService.PROVIDER assert captured["partition_scope_key"] == "flanders" def test_flanders_scope_discovery_uses_complete_unique_vrbg_inventory() -> None: features = [ { "type": "Feature", "id": f"Refgem.{index:05d}", "properties": {"NISCODE": f"{index:05d}", "NAAM": f"Gemeente {index:03d}"}, "geometry": { "type": "Polygon", "coordinates": [[[4.0, 50.5], [4.1, 50.5], [4.1, 50.6], [4.0, 50.5]]], }, } for index in range(1, 286) ] scope, selected, source_url = flanders_scope.discover_flanders_scope( SourceSession({"features": list(reversed(features))}), timeout=30, min_municipalities=270, max_municipalities=300, ) assert scope.key == "flanders" assert scope.project_name == "Flanders Regional Workbench" assert len(scope.members) == 285 assert len(set(scope.nis_codes)) == 285 assert [item["properties"]["NISCODE"] for item in selected] == sorted(scope.nis_codes) assert source_url.startswith("https://geo.api.vlaanderen.be/") def test_expansion_scripts_are_packaged_and_readiness_checked() -> None: dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8") readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8") for script in ( "provision_flanders_geographic_scope.py", "provision_flanders_bathymetry_profiles.py", "probe_mdk_bathymetry.py", ): assert f"COPY scripts/{script}" in dockerfile assert f"py_compile scripts/{script}" in readiness sources = { item["key"]: item for item in BathymetryProfileAcquisitionService.list_sources() } assert sources["mdk_bcp_bathymetry"]["integration_status"] == "probe_only" # Bounded acquisition is implemented but remains disabled by default. assert sources["mdk_bcp_bathymetry"]["acquisition_supported"] is True assert sources["mdk_bcp_bathymetry"]["configured"] is False assert "EL_wcs" in sources["mdk_bcp_bathymetry"]["service_url"] def test_frontend_exhaustively_pages_regional_area_inventory() -> None: area_api = (ROOT / "frontend" / "src" / "services" / "api" / "areas.ts").read_text( encoding="utf-8" ) assert "const AREA_PAGE_SIZE = 200" in area_api assert "while (offset < (total ?? 0))" in area_api assert "items.length !== total" in area_api assert "list: listProjectAreas" in area_api def test_frontend_bounds_large_area_and_dataset_catalogs() -> None: area_panel = ( ROOT / "frontend" / "src" / "components" / "project" / "AreaPanel.tsx" ).read_text(encoding="utf-8") dataset_panel = ( ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx" ).read_text(encoding="utf-8") assert "const AREA_CATALOG_PAGE_SIZE = 12" in area_panel assert "{catalogOpen ? (" in area_panel assert "visibleAreas.map" in area_panel assert "Zoek gemeente of regio" in area_panel assert "const DATASET_CATALOG_PAGE_SIZE = 10" in dataset_panel assert "visiblePrimaryDatasets.map" in dataset_panel assert "Zoek in beschikbare bronnen" in dataset_panel assert "{historyOpen ? None: dataset_display = ( ROOT / "frontend" / "src" / "lib" / "datasetDisplay.ts" ).read_text(encoding="utf-8") assert "coverageScope === 'flanders' && layer === 'regional_boundary'" in dataset_display assert "'Grens Vlaanderen'" in dataset_display assert "coverageScope === 'flanders' && layer === 'municipality_boundaries'" in dataset_display assert "'Gemeentegrenzen Vlaanderen'" in dataset_display def test_frontend_uses_partitioned_bathymetry_selection_for_regional_scope() -> None: app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") index = (ROOT / "frontend" / "index.html").read_text(encoding="utf-8") focus = ( ROOT / "frontend" / "src" / "config" / "primaryFocus.ts" ).read_text(encoding="utf-8") map_workspace = ( ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx" ).read_text(encoding="utf-8") theme_hook = ( ROOT / "frontend" / "src" / "hooks" / "useMapThemeSelectionInsights.ts" ).read_text(encoding="utf-8") dataset_api = ( ROOT / "frontend" / "src" / "services" / "api" / "datasets.ts" ).read_text(encoding="utf-8") assert "isPartitionedBathymetry" in map_workspace assert "regionalPartitionedThemeActive" in map_workspace assert "availabilityLabel" in map_workspace assert "activeSelectionResult?.geojson ?? null" in map_workspace assert "selectBathymetryProfilePartitions" in theme_hook assert "datasets/bathymetry/profiles/partitions/select" in dataset_api assert "regionalBathymetryContextActive" in app assert "regionalBathymetryProfileCount" in app assert "profielen · ${regionalBathymetryPartitions.length} gemeenten" in app assert "FLANDERS_WORKSPACE_LABEL = 'Vlaanderen (285 gemeenten)'" in focus assert "GeoIntel" in index