Files
geointel/backend/tests/test_rc10_data_operations.py
T
Codex 22fb8d51a9
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s
Classify national maritime source editions
2026-07-18 07:37:28 +02:00

359 lines
13 KiB
Python

from __future__ import annotations
import hashlib
import importlib.util
import json
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4
import pytest
ROOT = Path(__file__).resolve().parents[2]
SCRIPTS = ROOT / "scripts"
def load_script(name: str):
path = SCRIPTS / name
spec = importlib.util.spec_from_file_location(f"rc10_{path.stem}", path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def write_backup(root: Path, *, created_at: datetime, inventory_mode: str = "sha256") -> None:
root.mkdir(parents=True)
manifest = {
"schema_version": 1,
"release_id": "rc10-test",
"created_at": created_at.isoformat(),
"read_only_source": True,
"database_password_secure": True,
"inventory_mode": inventory_mode,
"storage_inventory_requested": True,
"git_commit": "0123456789abcdef",
}
files = {
"manifest.json": json.dumps(manifest),
"database.dump": "database",
"database.list": "list",
"database-metadata.tsv": "alembic_head\t202607160001",
"table-counts.tsv": "datasets\t1",
"storage-manifest.tsv": "relative_path\tsize_bytes\tmtime_ns\tsha256",
}
for name, content in files.items():
(root / name).write_text(content, encoding="utf-8")
checksums = []
for name in sorted(files):
digest = hashlib.sha256((root / name).read_bytes()).hexdigest()
checksums.append(f"{digest} {name}")
(root / "CHECKSUMS.sha256").write_text("\n".join(checksums) + "\n", encoding="utf-8")
def test_backup_guard_requires_recent_complete_sha256_storage_backup(tmp_path: Path) -> None:
guard = load_script("release_backup_guard.py")
now = datetime(2026, 7, 18, 12, tzinfo=timezone.utc)
backup = tmp_path / "backup"
write_backup(backup, created_at=now - timedelta(hours=2))
verified = guard.verify_current_backup(backup, now=now)
assert verified.release_id == "rc10-test"
assert verified.age_hours == pytest.approx(2)
def test_backup_guard_rejects_stale_or_tampered_backup(tmp_path: Path) -> None:
guard = load_script("release_backup_guard.py")
now = datetime(2026, 7, 18, 12, tzinfo=timezone.utc)
stale = tmp_path / "stale"
write_backup(stale, created_at=now - timedelta(hours=30))
with pytest.raises(RuntimeError, match="maximum allowed age"):
guard.verify_current_backup(stale, now=now)
current = tmp_path / "tampered"
write_backup(current, created_at=now)
(current / "database.dump").write_text("tampered", encoding="utf-8")
with pytest.raises(RuntimeError, match="checksum mismatch"):
guard.verify_current_backup(current, now=now)
def test_storage_lifecycle_is_fail_closed_and_protects_release_evidence() -> None:
audit = load_script("audit_data_operations.py")
assert audit.classify_relative_path("release-evidence/rc11/manifest.json") == (
"release-evidence",
True,
False,
)
assert audit.classify_relative_path("operator-evidence/source/raw.json")[1:] == (True, False)
assert audit.classify_relative_path("uploads/project/data.geojson")[1:] == (True, False)
assert audit.classify_relative_path("exports/project/old.json")[1:] == (False, True)
assert audit.classify_relative_path("unknown/value.bin")[1:] == (True, False)
def test_storage_audit_only_selects_old_unreferenced_allowlisted_files(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
audit = load_script("audit_data_operations.py")
storage = tmp_path / "storage"
old_orphan = storage / "exports" / "project" / "old.json"
referenced = storage / "exports" / "project" / "kept.json"
protected = storage / "release-evidence" / "rc" / "manifest.json"
unknown = storage / "misc" / "unknown.bin"
for path in (old_orphan, referenced, protected, unknown):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(path.name, encoding="utf-8")
old_timestamp = (datetime.now(timezone.utc) - timedelta(days=30)).timestamp()
for path in (old_orphan, referenced, protected, unknown):
path.touch()
Path(path).chmod(0o644)
import os
os.utime(path, (old_timestamp, old_timestamp))
monkeypatch.setattr(
audit,
"collect_database_state",
lambda _db, _root: {
"references": {referenced.resolve()},
"counts": {},
"source_families": {"national": [], "regional": [], "maritime": []},
},
)
monkeypatch.setattr(
audit,
"disk_pressure",
lambda _root: {
"status": "ok",
"total_bytes": 100,
"used_bytes": 50,
"free_bytes": 50,
"free_percent": 50.0,
"acquisition_allowed": True,
},
)
report, candidates = audit.build_report(storage, SimpleNamespace(), minimum_age_days=7)
assert [candidate.relative_path for candidate in candidates] == ["exports/project/old.json"]
assert report["cleanup"]["candidate_count"] == 1
assert "release-evidence" in report["cleanup"]["protected_prefixes"]
assert report["integrity"]["missing_referenced_path_count"] == 0
assert report["integrity"]["missing_manifest_artifact_count"] == 0
def test_referenced_tile_manifest_protects_its_tiles(tmp_path: Path) -> None:
audit = load_script("audit_data_operations.py")
storage = tmp_path / "storage"
manifest = storage / "tiles" / "dataset" / "set" / "manifest.json"
tile = manifest.parent / "tile_0000.tif"
tile.parent.mkdir(parents=True)
tile.write_bytes(b"tile")
manifest.write_text(json.dumps({"tiles": [{"path": "tile_0000.tif"}]}), encoding="utf-8")
expanded = audit.expand_manifest_references({manifest.resolve()}, storage)
assert manifest.resolve() in expanded
assert tile.resolve() in expanded
def test_ordinary_json_export_is_not_treated_as_an_artifact_manifest(tmp_path: Path) -> None:
audit = load_script("audit_data_operations.py")
storage = tmp_path / "storage"
export = storage / "exports" / "project" / "report.json"
export.parent.mkdir(parents=True)
export.write_text(json.dumps({"dataset_id": "not-a-file"}), encoding="utf-8")
expanded = audit.expand_manifest_references({export.resolve()}, storage)
assert expanded == {export.resolve()}
def test_manifest_ids_dates_and_labels_are_not_treated_as_paths(tmp_path: Path) -> None:
audit = load_script("audit_data_operations.py")
storage = tmp_path / "storage"
manifest = storage / "operator-data" / "scope" / "scope.manifest.json"
actual = manifest.parent / "scope.geojson"
manifest.parent.mkdir(parents=True)
actual.write_text("{}", encoding="utf-8")
manifest.write_text(
json.dumps(
{
"municipality_ids": ["13025", "11001"],
"generated_at": "2026-07-18T00:00:00Z",
"label": "Belgium",
"output_path": "scope.geojson",
"output_checksum_sha256": "a" * 64,
"output_crs": "EPSG:4326",
}
),
encoding="utf-8",
)
expanded = audit.expand_manifest_references({manifest.resolve()}, storage)
assert expanded == {manifest.resolve(), actual.resolve()}
def test_disk_pressure_uses_absolute_headroom_for_large_arrays(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
audit = load_script("audit_data_operations.py")
monkeypatch.setattr(
audit.shutil,
"disk_usage",
lambda _path: SimpleNamespace(
total=56 * 1024**4,
used=(56 * 1024**4) - (700 * 1024**3),
free=700 * 1024**3,
),
)
pressure = audit.disk_pressure(tmp_path)
assert pressure["free_percent"] < 2
assert pressure["status"] == "ok"
assert pressure["acquisition_allowed"] is True
def test_source_family_report_covers_national_regional_and_maritime(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
audit = load_script("audit_data_operations.py")
national_id = uuid4()
regional_id = uuid4()
now = datetime.now(timezone.utc)
rows = {
audit.Project: [
SimpleNamespace(id=national_id, name=audit.NATIONAL_PROJECT_NAME, status="active"),
SimpleNamespace(id=regional_id, name="Wallonia operator", status="active"),
],
audit.Dataset: [
SimpleNamespace(
project_id=national_id,
source_name="ngi_adminvector",
source="ngi",
name="Belgium boundary",
source_metadata={"coverage_zones": ["belgium"]},
source_version="2026",
imported_at=now,
status="ready",
storage_path=None,
metadata_json=None,
provenance_metadata=None,
),
SimpleNamespace(
project_id=national_id,
source_name="rbins_marine_reporting_units",
source="rbins",
name="Belgian North Sea",
source_metadata={"coverage_zones": ["belgian_north_sea"]},
source_version="2024",
imported_at=now,
status="ready",
storage_path=None,
metadata_json=None,
provenance_metadata=None,
),
SimpleNamespace(
project_id=regional_id,
source_name="wallonia_manual",
source="manual",
name="Wallonia source",
source_metadata={},
source_version="1",
imported_at=now,
status="ready",
storage_path=None,
metadata_json=None,
provenance_metadata=None,
),
],
audit.DatasetVersion: [],
audit.Export: [],
audit.Detection: [],
audit.Segmentation: [],
audit.Job: [],
audit.AnalysisRun: [],
}
class Query:
def __init__(self, values):
self.values = values
def all(self):
return self.values
class Session:
def query(self, model, *_fields):
if model in rows:
return Query(rows[model])
owner = getattr(model, "class_", None)
if owner in rows:
return Query(rows[owner])
raise AssertionError(f"Unexpected query entity: {model!r}")
monkeypatch.setattr(audit, "query_count", lambda _db, model, *_conditions: len(rows[model]))
monkeypatch.setattr(audit, "query_distinct_nonnull", lambda _db, _column: [])
state = audit.collect_database_state(Session(), tmp_path)
assert {item["source_name"] for item in state["source_families"]["national"]} == {
"ngi_adminvector",
"rbins_marine_reporting_units",
}
assert {item["source_name"] for item in state["source_families"]["maritime"]} == {
"rbins_marine_reporting_units"
}
assert {item["source_name"] for item in state["source_families"]["regional"]} == {
"wallonia_manual"
}
def test_national_and_maritime_sources_have_explicit_freshness_policies() -> None:
from app.services.source_freshness_service import SOURCE_POLICIES
for source_name in (
"ngi_adminvector",
"rbins_marine_reporting_units",
"rbins_msp_2026",
):
assert SOURCE_POLICIES[source_name].refresh_policy == "edition"
def test_cleanup_commands_require_backup_confirmation_and_read_only_mount() -> None:
generic = (SCRIPTS / "cleanup_storage_artifacts.py").read_text(encoding="utf-8")
demo = (ROOT / "backend/scripts/cleanup_demo_artifacts.py").read_text(encoding="utf-8")
compose = (ROOT / "docker-compose.unraid.yml").read_text(encoding="utf-8")
dockerman = (ROOT / "deploy/unraid/run-dockerman-container.sh").read_text(encoding="utf-8")
dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8")
readiness = (SCRIPTS / "run_readiness_check.sh").read_text(encoding="utf-8")
live_audit = (SCRIPTS / "run_rc10_data_operations_audit.sh").read_text(encoding="utf-8")
assert "DELETE_STORAGE_ARTIFACTS" in generic
assert "verify_current_backup" in generic
assert "DELETE_DEMO_EXPORTS" in demo
assert "verify_current_backup" in demo
assert "/app/backups:ro" in compose
assert '/app/backups:ro"' in dockerman
for name in (
"release_backup_guard.py",
"audit_data_operations.py",
"cleanup_storage_artifacts.py",
):
assert f"COPY scripts/{name}" in dockerfile
assert f"py_compile scripts/{name}" in readiness
assert "bash -n scripts/run_rc10_data_operations_audit.sh" in readiness
assert "--apply" not in live_audit
assert "table-counts-before.tsv" in live_audit
assert "table-counts-after.tsv" in live_audit
assert "deleted_count" in live_audit
assert "missing_manifest_artifact_count" in live_audit