Files
geointel/backend/tests/test_sprint204_bwk_natura2000.py
T
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +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, read_feature
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 = read_feature("datasets")
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