215 lines
8.0 KiB
Python
215 lines
8.0 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
from fastapi.testclient import TestClient
|
|
from shapely.geometry import box
|
|
|
|
from app.core.errors import AppError
|
|
from app.main import app
|
|
from app.models import Area, Dataset, Project
|
|
from app.schemas.coverage import CoverageBBox
|
|
from app.services.coverage_registry_service import CoverageRegistryService, THEMES, ZONES
|
|
|
|
|
|
class FakeQuery:
|
|
def __init__(self, rows):
|
|
self.rows = rows
|
|
|
|
def filter(self, *_args, **_kwargs):
|
|
return self
|
|
|
|
def all(self):
|
|
return list(self.rows)
|
|
|
|
|
|
class FakeSession:
|
|
def __init__(self, *, project, areas, datasets):
|
|
self.project = project
|
|
self.areas = areas
|
|
self.datasets = datasets
|
|
|
|
def get(self, model, object_id):
|
|
if model is Project and str(self.project.id) == str(object_id):
|
|
return self.project
|
|
return None
|
|
|
|
def query(self, model):
|
|
if model is Area:
|
|
return FakeQuery(self.areas)
|
|
if model is Dataset:
|
|
return FakeQuery(self.datasets)
|
|
raise AssertionError(f"Unexpected query model: {model}")
|
|
|
|
|
|
def scope_area(name: str, geometry):
|
|
return SimpleNamespace(name=name, geometry=geometry)
|
|
|
|
|
|
def test_coverage_catalog_uses_normalized_contracts_and_does_not_change_provider_registry() -> None:
|
|
catalog = CoverageRegistryService.catalog()
|
|
|
|
assert set(catalog.themes) == set(THEMES)
|
|
assert set(catalog.zones) == set(ZONES)
|
|
assert catalog.statuses == ["unsupported", "not_configured", "partial", "operational"]
|
|
assert {source.source_name for source in catalog.sources} >= {
|
|
"ngi_adminvector",
|
|
"statbel",
|
|
"digitaal_vlaanderen",
|
|
"spw_geoportail",
|
|
"urbis",
|
|
"rbins_marine_reporting_units",
|
|
"rbins_msp_2026",
|
|
"mdk_bathymetry",
|
|
}
|
|
assert next(source for source in catalog.sources if source.source_name == "ngi_adminvector").license_note == "CC BY 4.0"
|
|
assert next(source for source in catalog.sources if source.source_name == "mdk_bathymetry").integration_status == "not_configured"
|
|
|
|
response = TestClient(app).get("/api/v1/external/coverage/catalog")
|
|
assert response.status_code == 200
|
|
assert response.json()["data"]["themes"] == list(THEMES)
|
|
|
|
|
|
def test_coverage_resolver_only_reports_operational_for_materialized_ready_dataset() -> None:
|
|
project_id = uuid4()
|
|
project = SimpleNamespace(id=project_id)
|
|
areas = [
|
|
scope_area("Belgium land", box(2.5, 49.5, 6.4, 51.5)),
|
|
scope_area("Flanders", box(2.5, 50.7, 5.9, 51.5)),
|
|
]
|
|
bbox = CoverageBBox(minx=4.9, miny=50.9, maxx=5.0, maxy=51.0)
|
|
|
|
without_materialized = CoverageRegistryService.resolve(
|
|
FakeSession(project=project, areas=areas, datasets=[]),
|
|
project_id,
|
|
bbox,
|
|
["admin"],
|
|
)
|
|
assert without_materialized.intersected_zones == ["flanders"]
|
|
assert without_materialized.items[0].status == "partial"
|
|
assert without_materialized.items[0].materialized_dataset_ids == []
|
|
|
|
dataset_id = uuid4()
|
|
materialized = SimpleNamespace(
|
|
id=dataset_id,
|
|
status="ready",
|
|
source_name="ngi_adminvector",
|
|
reference_layer_name="belgium_regions",
|
|
source_metadata={"coverage_zones": ["belgium", "flanders", "wallonia", "brussels"]},
|
|
)
|
|
with_materialized = CoverageRegistryService.resolve(
|
|
FakeSession(project=project, areas=areas, datasets=[materialized]),
|
|
project_id,
|
|
bbox,
|
|
["admin"],
|
|
)
|
|
admin_item = next(item for item in with_materialized.items if item.zone == "flanders")
|
|
assert admin_item.status == "operational"
|
|
assert admin_item.materialized_dataset_ids == [dataset_id]
|
|
|
|
|
|
def test_mixed_land_and_north_sea_selection_remains_split() -> None:
|
|
project_id = uuid4()
|
|
project = SimpleNamespace(id=project_id)
|
|
areas = [
|
|
scope_area("Belgium land", box(2.5, 49.5, 6.4, 51.5)),
|
|
scope_area("Flanders", box(2.5, 50.7, 5.9, 51.5)),
|
|
scope_area("Belgian part of the North Sea", box(2.2, 51.1, 3.4, 51.9)),
|
|
scope_area("Belgian territorial sea (0-12 nautical miles)", box(2.7, 51.1, 3.4, 51.5)),
|
|
scope_area("Belgian exclusive economic zone beyond territorial sea", box(2.2, 51.5, 3.1, 51.9)),
|
|
scope_area("Belgian continental shelf beyond territorial sea", box(2.2, 51.5, 3.1, 51.9)),
|
|
]
|
|
|
|
result = CoverageRegistryService.resolve(
|
|
FakeSession(project=project, areas=areas, datasets=[]),
|
|
project_id,
|
|
CoverageBBox(minx=2.65, miny=51.05, maxx=2.85, maxy=51.2),
|
|
["admin", "bathymetry"],
|
|
)
|
|
|
|
assert result.intersected_zones == ["flanders", "territorial_sea"]
|
|
assert len(result.items) == 4
|
|
assert any("crosses coverage zones" in warning for warning in result.warnings)
|
|
bathymetry = next(item for item in result.items if item.zone == "territorial_sea" and item.theme == "bathymetry")
|
|
assert bathymetry.status == "not_configured"
|
|
assert bathymetry.materialized_dataset_ids == []
|
|
|
|
|
|
def test_flemish_materialization_is_theme_specific() -> None:
|
|
project_id = uuid4()
|
|
project = SimpleNamespace(id=project_id)
|
|
orthophoto_id = uuid4()
|
|
orthophoto = SimpleNamespace(
|
|
id=orthophoto_id,
|
|
status="ready",
|
|
source_name="digitaal_vlaanderen_orthophoto",
|
|
reference_layer_name="orthophoto",
|
|
source_metadata={"coverage_zones": ["flanders"]},
|
|
)
|
|
result = CoverageRegistryService.resolve(
|
|
FakeSession(
|
|
project=project,
|
|
areas=[scope_area("Flanders", box(2.5, 50.7, 5.9, 51.5))],
|
|
datasets=[orthophoto],
|
|
),
|
|
project_id,
|
|
CoverageBBox(minx=4.9, miny=50.9, maxx=5.0, maxy=51.0),
|
|
["orthophoto", "roads"],
|
|
)
|
|
|
|
assert next(item for item in result.items if item.theme == "orthophoto").status == "operational"
|
|
assert next(item for item in result.items if item.theme == "roads").status == "partial"
|
|
|
|
|
|
def test_outside_scope_and_unknown_theme_are_explicit() -> None:
|
|
project_id = uuid4()
|
|
project = SimpleNamespace(id=project_id)
|
|
db = FakeSession(
|
|
project=project,
|
|
areas=[scope_area("Belgium land", box(2.5, 49.5, 6.4, 51.5))],
|
|
datasets=[],
|
|
)
|
|
result = CoverageRegistryService.resolve(
|
|
db,
|
|
project_id,
|
|
CoverageBBox(minx=7.0, miny=52.0, maxx=7.1, maxy=52.1),
|
|
["admin"],
|
|
)
|
|
assert result.intersected_zones == []
|
|
assert result.outside_supported_scope is True
|
|
assert result.items == []
|
|
|
|
try:
|
|
CoverageRegistryService.resolve(
|
|
db,
|
|
project_id,
|
|
CoverageBBox(minx=4.0, miny=50.0, maxx=4.1, maxy=50.1),
|
|
["invented_metric"],
|
|
)
|
|
except AppError as exc:
|
|
assert exc.code == "COVERAGE_THEME_UNSUPPORTED"
|
|
assert exc.status_code == 422
|
|
assert exc.details["unsupported_themes"] == ["invented_metric"]
|
|
else:
|
|
raise AssertionError("Unknown coverage theme was accepted")
|
|
|
|
|
|
def test_frontend_prefers_materialized_national_workspace_and_resolves_drawn_bbox() -> None:
|
|
root = Path(__file__).parents[2]
|
|
focus = (root / "frontend" / "src" / "config" / "primaryFocus.ts").read_text(encoding="utf-8")
|
|
workspace_hook = (root / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(encoding="utf-8")
|
|
coverage_hook = (root / "frontend" / "src" / "hooks" / "useCoverageResolver.ts").read_text(encoding="utf-8")
|
|
map_workspace = (root / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
|
|
|
assert "Belgium and North Sea Workbench" in focus
|
|
assert "nationalProject" in workspace_hook
|
|
assert "data.areas.length > 0" in workspace_hook
|
|
assert "dataset.status === 'ready'" in workspace_hook
|
|
assert "externalApi.resolveCoverage" in coverage_hook
|
|
assert "coverage.outside_supported_scope" in map_workspace
|
|
assert "coverageStatusLabel" in map_workspace
|
|
assert "coverageSelectionAvailable" in map_workspace
|
|
assert "activeThemeAvailable && !regionalPartitionedThemeActive" in map_workspace
|