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
184 lines
6.0 KiB
Python
184 lines
6.0 KiB
Python
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
|
|
from app.utils.geometry import normalize_to_multipolygon # 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_area_geometry_normalization_drops_source_z_dimension() -> None:
|
|
geometry = normalize_to_multipolygon(
|
|
{
|
|
"type": "Polygon",
|
|
"coordinates": [
|
|
[
|
|
[2.5, 49.5, 0],
|
|
[6.4, 49.5, 0],
|
|
[6.4, 51.5, 0],
|
|
[2.5, 51.5, 0],
|
|
[2.5, 49.5, 0],
|
|
]
|
|
],
|
|
}
|
|
)
|
|
|
|
assert geometry.has_z is False
|
|
assert geometry.geom_type == "MultiPolygon"
|
|
|
|
|
|
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 "frame.to_json(drop_id=False, default=str)" in source
|
|
assert "from app.models" not in source
|
|
assert "INSERT INTO vector_features" not in source
|