Files
geointel/backend/tests/test_sprint204_bwk_natura2000.py
T
JensandClaude Opus 5 39c12822bc extract the map workspace's domain layer out of the component
MapWorkspace.tsx opened with ~590 lines of theme catalogue, dataset matching
and label formatting above a 3.200-line component. None of it is React, all of
it is independently testable, and both render paths read from it, so it belongs
beside the pure helpers that already live in mapWorkspaceUtils.

The contract tests that read MapWorkspace.tsx would have gone red for a move
that changes no behaviour at all — 24 of them. That is the brittleness the
frontend_contract helper exists to remove, so it gains read_map_workspace():
the workspace is one feature spread over several modules, and a contract
belongs to the feature rather than to whichever file currently holds it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 18:49:06 +02:00

273 lines
9.6 KiB
Python

from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
from uuid import uuid4
import pytest
from shapely.geometry import box, shape
from shapely.ops import transform as transform_geometry
from app.models import Dataset
from app.schemas.operations import VectorSelectionSummary
from app.services.vector_feature_service import VectorFeatureService
from tests.frontend_contract import read_map_workspace
ROOT = Path(__file__).resolve().parents[2]
BBOX = {"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3, "crs": "EPSG:4326"}
def load_operator():
script_path = ROOT / "scripts" / "provision_mol_bwk_natura2000.py"
spec = importlib.util.spec_from_file_location("bwk_natura2000_operator", script_path)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class FakeResponse:
ok = True
status_code = 200
text = ""
def __init__(self, payload: dict, url: str):
self.payload = payload
self.url = url
self.content = json.dumps(payload).encode("utf-8")
def raise_for_status(self):
return None
def json(self):
return self.payload
class FakeSession:
def __init__(self, responses: list[FakeResponse]):
self.responses = iter(responses)
self.calls: list[tuple[str, dict | None]] = []
def get(self, url, *, params=None, timeout): # noqa: ANN001, ARG002
self.calls.append((url, params))
return next(self.responses)
class ScalarQuery:
def __init__(self, value: float):
self.value = value
def filter(self, *args): # noqa: ANN002, ARG002
return self
def scalar(self):
return self.value
class SequenceScalarSession:
def __init__(self, values: list[float]):
self.values = iter(values)
def query(self, *args): # noqa: ANN002, ARG002
return ScalarQuery(next(self.values))
def test_official_bwk_contract_and_class_labels_are_fixed() -> None:
module = load_operator()
assert module.WFS_URL == "https://geo.api.vlaanderen.be/BWK/wfs"
assert module.TYPE_NAME == "BWK:Bwkhab"
assert module.SOURCE_VERSION == "2025"
assert module.TEMPORAL_SERIES_KEY == "inbo-bwk-natura2000:mol"
assert module.ATTRIBUTION == "Bron: INBO"
assert module.EVALUATION_LABELS == {
"z": "Biologisch zeer waardevol",
"w": "Biologisch waardevol",
"m": "Biologisch minder waardevol",
"wz": "Complex van waardevolle en zeer waardevolle elementen",
"mwz": "Complex van minder waardevolle, waardevolle en zeer waardevolle elementen",
"mz": "Complex van minder waardevolle en zeer waardevolle elementen",
"mw": "Complex van minder waardevolle en waardevolle elementen",
}
def test_wfs_pagination_follows_server_next_links() -> None:
module = load_operator()
page_one = {
"type": "FeatureCollection",
"features": [{"id": "one"}],
"numberReturned": 1,
"links": [{"rel": "next", "href": "https://geo.api.vlaanderen.be/BWK/wfs?STARTINDEX=1"}],
}
page_two = {"type": "FeatureCollection", "features": [], "numberReturned": 0, "links": []}
session = FakeSession(
[
FakeResponse(page_one, "https://geo.api.vlaanderen.be/BWK/wfs?first"),
FakeResponse(page_two, "https://geo.api.vlaanderen.be/BWK/wfs?STARTINDEX=1"),
]
)
pages = list(module.iter_wfs_pages(session, (5.0, 51.1, 5.2, 51.3), page_limit=1000, timeout=30))
assert len(pages) == 2
assert session.calls[0][1]["sortBy"] == "UIDN"
assert session.calls[0][1]["srsName"] == "EPSG:4326"
assert session.calls[1] == ("https://geo.api.vlaanderen.be/BWK/wfs?STARTINDEX=1", None)
def test_wfs_page_limit_without_next_link_uses_controlled_start_index_fallback() -> None:
module = load_operator()
session = FakeSession(
[
FakeResponse(
{"type": "FeatureCollection", "features": [{"id": "one"}], "numberReturned": 1},
"https://geo.api.vlaanderen.be/BWK/wfs",
),
FakeResponse(
{"type": "FeatureCollection", "features": [], "numberReturned": 0},
"https://geo.api.vlaanderen.be/BWK/wfs?startIndex=1",
),
]
)
pages = list(module.iter_wfs_pages(session, (5.0, 51.1, 5.2, 51.3), page_limit=1, timeout=30))
assert len(pages) == 2
assert session.calls[1][1]["startIndex"] == "1"
def test_feature_is_clipped_in_lambert72_and_keeps_bwk_habitat_provenance() -> None:
module = load_operator()
boundary_wgs84 = box(5.10, 51.20, 5.11, 51.21)
boundary_lambert72 = transform_geometry(module.TO_LAMBERT72.transform, boundary_wgs84)
feature = {
"type": "Feature",
"id": "Bwkhab.42",
"geometry": mapping_box(5.095, 51.195, 5.105, 51.205),
"properties": {
"UIDN": 42,
"EVAL": "wz",
"BWKLABEL": "qb + qs",
"EENH1": "qb",
"EENH2": "qs",
"HERK": "225",
"HAB1": "9190",
"PHAB1": 60,
"HAB2": "rbbppm",
"PHAB2": 30,
"HAB3": "gh",
"PHAB3": 10,
"HABLEGENDE": "phab",
"HERKHAB": "225",
"HERKPHAB": "a",
},
}
normalized, was_clipped = module.normalize_feature(feature, boundary_lambert72)
assert normalized is not None
assert was_clipped is True
normalized_geometry = shape(normalized["geometry"])
assert normalized_geometry.difference(boundary_wgs84.buffer(1e-7)).area < 1e-12
properties = normalized["properties"]
assert properties["bwk_evaluation_code"] == "wz"
assert properties["bwk_evaluation_label"].startswith("Complex van waardevolle")
assert properties["natura2000_codes"] == "9190"
assert properties["regional_biotope_codes"] == "rbbppm"
assert properties["natura2000_area_ha"] == pytest.approx(properties["clipped_area_ha"] * 0.6)
assert properties["regional_biotope_area_ha"] == pytest.approx(properties["clipped_area_ha"] * 0.3)
assert properties["habitat_share_origin_code"] == "a"
def mapping_box(min_x: float, min_y: float, max_x: float, max_y: float) -> dict:
return {
"type": "Polygon",
"coordinates": [[
[min_x, min_y],
[max_x, min_y],
[max_x, max_y],
[min_x, max_y],
[min_x, min_y],
]],
}
def test_uncertain_habitat_status_is_not_presented_as_confirmed_habitat() -> None:
module = load_operator()
entries, natura_share, regional_share, uncertain_share = module.habitat_breakdown(
{"HAB1": "gh", "PHAB1": 100, "HABLEGENDE": "ohab"}
)
assert entries == [{"code": "gh", "share_percent": 100.0}]
assert natura_share == 0
assert regional_share == 0
assert uncertain_share == 100
def test_nature_value_summary_returns_separate_official_classes_and_habitat_metrics() -> None:
module = load_operator()
dataset = Dataset(
id=uuid4(),
project_id=uuid4(),
name="bwk_natura2000_2025_mol.geojson",
dataset_type="vector",
dataset_role="reference",
source_name="inbo_bwk_natura2000",
reference_layer_name="nature_value",
source_metadata={
"theme": "nature_value",
"semantic_metrics": False,
"selection_aggregation": {
"metric_key": "bwk_mapped_area",
"method": "intersection_area",
"label": "BWK-gekarteerde oppervlakte",
"unit": "ha",
"geometry_dimension": 2,
},
"selection_metrics": module.selection_metrics(),
},
)
result = VectorFeatureService.summarize_features_by_bbox(
SequenceScalarSession([100_000.0, 10_000.0, 20_000.0, 30_000.0, 40_000.0, 5.5, 2.5, 1.5]),
dataset=dataset,
bbox=BBOX,
total_feature_count=125,
full_dataset_area=True,
)
assert result["primary_metric_key"] == "bwk_mapped_area"
assert result["metric_value"] == 10.0
metrics = {item["metric_key"]: item for item in result["metrics"]}
assert metrics["bwk_very_valuable_area"]["metric_value"] == 1.0
assert metrics["bwk_valuable_area"]["metric_value"] == 2.0
assert metrics["bwk_less_valuable_area"]["metric_value"] == 3.0
assert metrics["bwk_mixed_value_area"]["metric_value"] == 4.0
assert metrics["natura2000_area"]["metric_value"] == 5.5
assert metrics["natura2000_area"]["is_estimate"] is True
assert metrics["regional_biotope_area"]["metric_value"] == 2.5
assert metrics["uncertain_habitat_area"]["metric_value"] == 1.5
assert metrics["feature_count"]["metric_value"] == 125
VectorSelectionSummary(**result)
def test_operator_is_packaged_readiness_checked_and_wired_to_map() -> 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")
service = (ROOT / "backend" / "app" / "services" / "vector_feature_service.py").read_text(encoding="utf-8")
map_workspace = read_map_workspace()
source_catalog = (ROOT / "frontend" / "src" / "components" / "datasets" / "SourceCatalogPanel.tsx").read_text(encoding="utf-8")
assert "COPY scripts/provision_mol_bwk_natura2000.py" in dockerfile
assert "py_compile scripts/provision_mol_bwk_natura2000.py" in readiness
assert '"provision_mol_bwk_natura2000.py"' in service
assert "id: 'nature_value'" in map_workspace
assert "Natuurwaarde" in map_workspace
assert "source.key === 'bwk'" in source_catalog