Add Belgium and North Sea coverage foundation
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from shapely.geometry import box, mapping, shape
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
SCRIPTS = ROOT / "scripts"
|
||||
if str(SCRIPTS) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
import provision_belgium_north_sea_scope as operator # noqa: E402
|
||||
|
||||
|
||||
def marine_feature(identifier: str, geometry):
|
||||
return {
|
||||
"type": "Feature",
|
||||
"id": identifier,
|
||||
"geometry": mapping(geometry),
|
||||
"properties": {"MarineReportingUnitId": identifier},
|
||||
}
|
||||
|
||||
|
||||
def test_marine_legal_scopes_are_derived_from_official_reporting_units() -> None:
|
||||
reporting_units = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
marine_feature("ANS-BE-MS-1", box(0, 0, 10, 10)),
|
||||
marine_feature("ANS-BE-AA-CW", box(0, 0, 2, 2)),
|
||||
marine_feature("ANS-BE-AA-TEW", box(0, 2, 2, 4)),
|
||||
marine_feature("ANS-BE-AA-OFFSHORE", box(2, 0, 10, 10)),
|
||||
],
|
||||
}
|
||||
|
||||
payload = operator.derive_marine_scope_payload(reporting_units)
|
||||
by_zone = {
|
||||
feature["properties"]["coverage_zone"]: feature
|
||||
for feature in payload["features"]
|
||||
}
|
||||
|
||||
assert set(by_zone) == {
|
||||
"belgian_north_sea",
|
||||
"territorial_sea",
|
||||
"exclusive_economic_zone",
|
||||
"continental_shelf",
|
||||
}
|
||||
assert shape(by_zone["territorial_sea"]["geometry"]).area == pytest.approx(8.0)
|
||||
assert shape(by_zone["exclusive_economic_zone"]["geometry"]).equals(
|
||||
shape(by_zone["continental_shelf"]["geometry"])
|
||||
)
|
||||
assert (
|
||||
by_zone["exclusive_economic_zone"]["properties"]["legal_domain"]
|
||||
!= by_zone["continental_shelf"]["properties"]["legal_domain"]
|
||||
)
|
||||
assert by_zone["territorial_sea"]["properties"]["derived_from_reporting_unit_ids"] == [
|
||||
"ANS-BE-AA-CW",
|
||||
"ANS-BE-AA-TEW",
|
||||
]
|
||||
|
||||
|
||||
def test_marine_scope_derivation_fails_when_a_required_unit_is_missing() -> None:
|
||||
with pytest.raises(RuntimeError, match="ANS-BE-AA-CW"):
|
||||
operator.derive_marine_scope_payload(
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
marine_feature("ANS-BE-MS-1", box(0, 0, 10, 10)),
|
||||
marine_feature("ANS-BE-AA-TEW", box(0, 2, 2, 4)),
|
||||
marine_feature("ANS-BE-AA-OFFSHORE", box(2, 0, 10, 10)),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_archive_extraction_accepts_one_safe_geopackage_and_rejects_traversal(tmp_path: Path) -> None:
|
||||
archive = tmp_path / "adminvector.zip"
|
||||
with zipfile.ZipFile(archive, "w") as handle:
|
||||
handle.writestr("release/adminvector.gpkg", b"sqlite-bytes")
|
||||
|
||||
result = operator.extract_single_geopackage(archive, tmp_path / "output")
|
||||
assert result.read_bytes() == b"sqlite-bytes"
|
||||
|
||||
unsafe = tmp_path / "unsafe.zip"
|
||||
with zipfile.ZipFile(unsafe, "w") as handle:
|
||||
handle.writestr("../adminvector.gpkg", b"unsafe")
|
||||
with pytest.raises(RuntimeError, match="unsafe"):
|
||||
operator.extract_single_geopackage(unsafe, tmp_path / "unsafe-output")
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, payload):
|
||||
self.payload = payload
|
||||
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, pages):
|
||||
self.pages = list(pages)
|
||||
self.calls = []
|
||||
|
||||
def get(self, url, params, timeout):
|
||||
self.calls.append({"url": url, "params": params, "timeout": timeout})
|
||||
return FakeResponse(self.pages.pop(0))
|
||||
|
||||
|
||||
def test_wfs_fetch_is_allowlisted_paginated_and_complete() -> None:
|
||||
pages = [
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"numberMatched": 2,
|
||||
"features": [{"type": "Feature", "id": "unit.1", "geometry": None, "properties": {}}],
|
||||
},
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"numberMatched": 2,
|
||||
"features": [{"type": "Feature", "id": "unit.2", "geometry": None, "properties": {}}],
|
||||
},
|
||||
]
|
||||
session = FakeSession(pages)
|
||||
payload = operator.fetch_wfs_layer(
|
||||
session,
|
||||
service_url=operator.RBINS_MRU_WFS_URL,
|
||||
layer_name=operator.RBINS_MRU_LAYER,
|
||||
timeout=30,
|
||||
page_size=1,
|
||||
)
|
||||
|
||||
assert [feature["id"] for feature in payload["features"]] == ["unit.1", "unit.2"]
|
||||
assert [call["params"]["startIndex"] for call in session.calls] == [0, 1]
|
||||
assert all(call["params"]["srsName"] == "EPSG:4326" for call in session.calls)
|
||||
|
||||
with pytest.raises(RuntimeError, match="allowlist"):
|
||||
operator.fetch_wfs_layer(
|
||||
FakeSession([]),
|
||||
service_url=operator.RBINS_MSP_WFS_URL,
|
||||
layer_name="untrusted:layer",
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
def test_operator_is_packaged_and_guarded_by_readiness() -> 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")
|
||||
source = (ROOT / "scripts" / "provision_belgium_north_sea_scope.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "COPY scripts/provision_belgium_north_sea_scope.py /app/scripts/" in dockerfile
|
||||
assert "py_compile scripts/provision_belgium_north_sea_scope.py" in readiness
|
||||
assert "/datasets/upload" in source
|
||||
assert "from app.models" not in source
|
||||
assert "INSERT INTO vector_features" not in source
|
||||
Reference in New Issue
Block a user