Files
geointel/backend/tests/test_sprint231_orthophoto_release_management.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

450 lines
16 KiB
Python

from __future__ import annotations
import argparse
from datetime import datetime, timezone
import importlib.util
import json
from pathlib import Path
from types import SimpleNamespace
import sys
import numpy as np
import pytest
from rasterio.io import MemoryFile
from rasterio.transform import from_origin
ROOT = Path(__file__).resolve().parents[2]
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
def load_script(name: str):
path = SCRIPTS / name
module_name = f"test_{path.stem}_sprint231"
spec = importlib.util.spec_from_file_location(module_name, path)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
MANAGER = load_script("manage_orthophoto_release.py")
def arguments(tmp_path: Path, **overrides) -> argparse.Namespace:
values = {
"action": "plan",
"project_id": "00000000-0000-0000-0000-000000000001",
"scope": "kempen-transport-region",
"api_url": "http://127.0.0.1:8000/api/v1",
"bbox": [5.110, 51.180, 5.113, 51.182],
"area_id": None,
"confirm_edition": None,
"confirm_plan_sha256": None,
"confirm_review_sha256": None,
"approve": False,
"reviewer": None,
"review_note": "",
"establish_official_baseline": False,
"confirm_local_version": None,
"plan_path": None,
"review_path": None,
"evidence_root": tmp_path / "operator-evidence" / "orthophoto-release",
"refresh_catalog": False,
"api_timeout": 180,
"wms_timeout": 60,
"import_timeout": 600,
"max_response_mb": 32,
}
values.update(overrides)
return argparse.Namespace(**values)
def report(*, status: str = "update_available", local: str | None = "2024.03") -> dict:
return {
"schema_version": 1,
"status": "passed",
"generated_at": "2026-07-17T10:00:00Z",
"project_id": "00000000-0000-0000-0000-000000000001",
"scope": "kempen-transport-region",
"product": {
"key": "most_recent",
"display_name": "Orthofoto meest recent",
"temporal_granularity": "snapshot",
"native_resolution_m": 0.15,
"supports_detection": True,
"color_mode": "rgb",
"catalog_url": MANAGER.preflight.CATALOG_URL,
},
"release": {
"status": status,
"remote_edition": "2025.04",
"remote_year": 2025,
"local_source_version": local,
"comparison_status": "different" if status == "update_available" else "not_comparable",
"metadata_identifier": MANAGER.preflight.METADATA_IDENTIFIER,
"metadata_url": "https://metadata.vlaanderen.be/srv/dut/catalog.search#/metadata/f5304d6d",
"remote_title": "Orthofoto meest recent, 2025.04",
"remote_modified_at": "2026-04-27T00:00:00Z",
"remote_published_at": "2025-12-11T00:00:00Z",
"catalog_checked_at": "2026-07-17T10:00:00Z",
"capabilities_url": MANAGER.WMS_BASE_URL + "?SERVICE=WMS&REQUEST=GetCapabilities",
"capabilities_sha256": "a" * 64,
},
"capabilities": {
"service_version": "1.3.0",
"capabilities_sha256": "a" * 64,
"layers": ["Ortho", "Vliegdagcontour"],
"vliegdagcontour_queryable": True,
"feature_info_format": "application/geo+json",
"extent_epsg31370": [0.0, 0.0, 300000.0, 300000.0],
"metadata_identifier": MANAGER.preflight.METADATA_IDENTIFIER,
},
"coverage_domain": {
"coverage_id": "Ortho",
"crs": "EPSG:31370",
"extent_epsg31370": [0.0, 0.0, 300000.0, 300000.0],
"native_resolution_m": 0.15,
"band_count": 3,
"native_format": "image/tiff",
"coverage_description_sha256": "b" * 64,
"selected_area_fully_inside_domain": True,
"pixel_data_requested": False,
},
"selection": {
"bbox_epsg4326": [5.110, 51.180, 5.113, 51.182],
"bbox_epsg31370": [200000.0, 210000.0, 200200.0, 210160.0],
"width_m": 200.0,
"height_m": 160.0,
},
"flight_day_coverage": {
"status": "passed",
"mode": "official_queryable_flight_day_grid",
"sample_count": 4,
"grid_columns": 2,
"grid_rows": 2,
"covered_sample_count": 4,
"sample_coverage_ratio": 1.0,
"flight_dates": ["5/4/2025"],
"flight_years": [2025],
"feature_ids": ["123"],
"sample_evidence_sha256": "c" * 64,
"claim_boundary": "Bounded point evidence, not a polygon-union proof.",
},
"flight_year_matches_release": True,
"staging_permitted": status in {"not_loaded", "update_available"},
"next_action": "governed_pixel_stage",
"pixel_requests_performed": 0,
"datasets_mutated": 0,
"automatic_staging": False,
"automatic_import": False,
}
def raw_rgb_tiff(width: int = 200, height: int = 160) -> bytes:
pixels = np.zeros((3, height, width), dtype=np.uint8)
pixels[0] = 80
pixels[1] = np.arange(width, dtype=np.uint8)[None, :]
pixels[2] = np.arange(height, dtype=np.uint8)[:, None]
with MemoryFile() as memory:
with memory.open(
driver="GTiff",
width=width,
height=height,
count=3,
dtype="uint8",
crs="EPSG:31370",
transform=from_origin(200000.0, 210160.0, 1.0, 1.0),
) as dataset:
dataset.write(pixels)
return memory.read()
class Response:
def __init__(self, body: bytes, url: str, content_type: str = "image/tiff") -> None:
self.body = body
self.url = url
self.headers = {"Content-Type": content_type, "Content-Length": str(len(body))}
def __enter__(self):
return self
def __exit__(self, *_args):
return None
def geturl(self) -> str:
return self.url
def read(self, amount: int | None = None) -> bytes:
return self.body if amount is None else self.body[:amount]
def staged_plan(tmp_path: Path) -> tuple[argparse.Namespace, Path, dict, dict]:
args = arguments(
tmp_path,
action="stage",
confirm_edition="2025.04",
)
release_report = report()
request = MANAGER.map_request(release_report)
staged = MANAGER.stage_artifacts(
args,
release_report,
request,
opener=lambda _request, timeout: Response(raw_rgb_tiff(), request["url"]),
)
plan = MANAGER.build_staged_plan(
args,
release_report,
MANAGER.authorize_stage(args, release_report),
staged,
)
path = MANAGER.default_plan_path(args, "2025.04")
MANAGER.write_json(path, plan)
return args, path, plan, staged
def test_orthophoto_catalog_prefers_latest_official_edition_over_rolling_marker() -> None:
from app.services.source_catalog_probe_service import _latest_local_version
now = datetime(2026, 7, 17, tzinfo=timezone.utc)
datasets = [
SimpleNamespace(
source_name=MANAGER.SOURCE_NAME,
source="",
source_version="2025.04",
imported_at=now,
observed_at=now,
id="official-2025",
),
SimpleNamespace(
source_name=MANAGER.SOURCE_NAME,
source="",
source_version="most_recent_at_2026-07-18",
imported_at=datetime(2026, 7, 18, tzinfo=timezone.utc),
observed_at=now,
id="rolling",
),
SimpleNamespace(
source_name=MANAGER.SOURCE_NAME,
source="",
source_version="2026.02",
imported_at=datetime(2026, 7, 16, tzinfo=timezone.utc),
observed_at=now,
id="official-2026",
),
]
assert _latest_local_version(MANAGER.SOURCE_NAME, datasets) == "2026.02"
def test_stage_requires_exact_official_or_explicit_legacy_baseline_confirmation(tmp_path: Path) -> None:
args = arguments(tmp_path)
normal = report()
assert MANAGER.authorize_stage(args, normal)["mode"] == "normal_release"
legacy = report(status="blocked_local_version", local="most_recent_at_2026-07-15")
with pytest.raises(RuntimeError, match="establish-official-baseline"):
MANAGER.authorize_stage(args, legacy)
args.establish_official_baseline = True
args.confirm_local_version = "most_recent_at_2026-07-15"
assert MANAGER.authorize_stage(args, legacy)["mode"] == "explicit_legacy_baseline_transition"
args.confirm_local_version = "different"
with pytest.raises(RuntimeError, match="exact"):
MANAGER.authorize_stage(args, legacy)
def test_stage_fetches_one_allowlisted_map_and_writes_reviewable_rgb_evidence(tmp_path: Path) -> None:
args = arguments(tmp_path)
release_report = report()
request = MANAGER.map_request(release_report)
calls = []
def opener(http_request, timeout):
calls.append((http_request.full_url, timeout))
return Response(raw_rgb_tiff(), request["url"])
staged = MANAGER.stage_artifacts(args, release_report, request, opener=opener)
manifest = MANAGER.validate_staged_artifacts(args, Path(staged["manifest_path"]))
assert calls == [(request["url"], 60)]
assert manifest["pixel_request_count"] == 1
assert manifest["datasets_mutated"] == 0
assert manifest["normalized_geotiff"]["crs"] == "EPSG:31370"
assert manifest["normalized_geotiff"]["band_count"] == 3
assert Path(manifest["review_preview"]["path"]).read_bytes().startswith(b"\x89PNG")
with pytest.raises(RuntimeError, match="will not be overwritten"):
MANAGER.stage_artifacts(args, release_report, request, opener=opener)
assert len(calls) == 1
def test_getmap_redirect_and_response_limits_fail_closed(tmp_path: Path) -> None:
request = MANAGER.map_request(report())
with pytest.raises(RuntimeError, match="allowlist"):
MANAGER.fetch_map(
request,
timeout=5,
max_bytes=10_000_000,
opener=lambda *_args, **_kwargs: Response(raw_rgb_tiff(), "https://example.com/image.tif"),
)
body = raw_rgb_tiff()
with pytest.raises(RuntimeError, match="release limit"):
MANAGER.fetch_map(
request,
timeout=5,
max_bytes=len(body) - 1,
opener=lambda *_args, **_kwargs: Response(body, request["url"]),
)
def test_plan_and_artifact_tampering_are_rejected(tmp_path: Path) -> None:
args, path, plan, staged = staged_plan(tmp_path)
args.confirm_plan_sha256 = plan["plan_sha256"]
_, loaded, _ = MANAGER.load_staged_plan(args, "2025.04")
assert loaded == plan
Path(staged["normalized_geotiff"]["path"]).write_bytes(b"tampered")
with pytest.raises(RuntimeError, match="no longer match"):
MANAGER.load_staged_plan(args, "2025.04")
outside = tmp_path / "outside.json"
with pytest.raises(RuntimeError, match="outside the governed"):
MANAGER.governed_path(args, outside)
def test_review_requires_named_approval_exact_plan_and_unchanged_preflight(tmp_path: Path) -> None:
args, path, plan, staged = staged_plan(tmp_path)
manifest = MANAGER.validate_staged_artifacts(args, Path(staged["manifest_path"]))
with pytest.raises(RuntimeError, match="named --reviewer"):
MANAGER.build_review_evidence(args, path, plan, manifest)
args.approve = True
args.reviewer = "Jens"
review = MANAGER.build_review_evidence(args, path, plan, manifest)
assert review["review_preview_sha256"] == manifest["review_preview"]["sha256"]
assert review["review_sha256"] == MANAGER.canonical_sha256(review, "review_sha256")
changed = report()
changed["capabilities"]["capabilities_sha256"] = "d" * 64
with pytest.raises(RuntimeError, match="evidence changed"):
MANAGER.require_preflight_unchanged(plan, changed, require_local=True)
def test_approved_upload_uses_canonical_dataset_route_and_complete_provenance(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
args, path, plan, staged = staged_plan(tmp_path)
manifest = MANAGER.validate_staged_artifacts(args, Path(staged["manifest_path"]))
args.approve = True
args.reviewer = "Jens"
review = MANAGER.build_review_evidence(args, path, plan, manifest)
captured = {}
class UploadResponse:
def __enter__(self):
return self
def __exit__(self, *_args):
return None
def read(self):
return json.dumps(
{
"data": {
"id": "dataset-1",
"status": "ready",
"source_name": MANAGER.SOURCE_NAME,
"source_version": "2025.04",
"checksum_sha256": manifest["normalized_geotiff"]["sha256"],
}
}
).encode("utf-8")
def fake_urlopen(request, timeout):
captured["url"] = request.full_url
captured["body"] = request.data
captured["timeout"] = timeout
return UploadResponse()
monkeypatch.setattr(MANAGER, "urlopen", fake_urlopen)
dataset = MANAGER.upload_approved_dataset(args, plan, manifest, review)
assert dataset["id"] == "dataset-1"
assert captured["url"].endswith(f"/projects/{args.project_id}/datasets/upload")
assert b'name="source_version"\r\n\r\n2025.04' in captured["body"]
assert b'name="temporal_series_key"' in captured["body"]
assert plan["plan_sha256"].encode("ascii") in captured["body"]
assert review["review_sha256"].encode("ascii") in captured["body"]
plan_path_json = json.dumps(str(MANAGER.default_plan_path(args, "2025.04")))[1:-1]
review_path_json = json.dumps(str(MANAGER.default_review_path(args, "2025.04")))[1:-1]
assert plan_path_json.encode("utf-8") in captured["body"]
assert review_path_json.encode("utf-8") in captured["body"]
def test_apply_target_must_be_loopback_api() -> None:
assert MANAGER.internal_api_url("http://127.0.0.1:8000/api/v1").endswith("/api/v1")
with pytest.raises(RuntimeError, match="local /api/v1"):
MANAGER.internal_api_url("http://192.168.123.45:1202/api/v1")
with pytest.raises(RuntimeError, match="local /api/v1"):
MANAGER.internal_api_url("http://127.0.0.1:8000/not-api")
def test_official_flight_dates_are_persisted_without_inventing_a_catalog_date() -> None:
dates = MANAGER.parse_flight_dates(["5/4/2025", "2025-04-06", "05-04-2025"])
assert [value.date().isoformat() for value in dates] == ["2025-04-05", "2025-04-06"]
with pytest.raises(RuntimeError, match="not safely parseable"):
MANAGER.parse_flight_dates(["spring 2025"])
def test_applied_evidence_is_immutable_on_idempotent_retry(tmp_path: Path) -> None:
args, plan_path, plan, staged = staged_plan(tmp_path)
manifest = MANAGER.validate_staged_artifacts(args, Path(staged["manifest_path"]))
args.approve = True
args.reviewer = "Jens"
review = MANAGER.build_review_evidence(args, plan_path, plan, manifest)
review_path = MANAGER.default_review_path(args, "2025.04")
dataset = {
"id": "dataset-1",
"checksum_sha256": manifest["normalized_geotiff"]["sha256"],
}
final_report = report(status="current", local="2025.04")
final_report["release"]["comparison_status"] = "same"
path, first = MANAGER.applied_evidence(
args,
plan_path,
plan,
review_path,
review,
dataset,
final_report,
reused=False,
)
original_bytes = path.read_bytes()
_, second = MANAGER.applied_evidence(
args,
plan_path,
plan,
review_path,
review,
dataset,
final_report,
reused=True,
)
assert second == first
assert path.read_bytes() == original_bytes
assert second["dataset_status"] == "imported"
def test_release_manager_is_packaged_and_compiled_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")
assert "COPY scripts/manage_orthophoto_release.py /app/scripts/manage_orthophoto_release.py" in dockerfile
assert "-m py_compile scripts/manage_orthophoto_release.py" in readiness