Files
geointel/backend/tests/test_data_contract_validation.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
18 KiB
Python

from __future__ import annotations
from datetime import datetime, timedelta, timezone
from hashlib import sha256
import json
from pathlib import Path
import pytest
from shapely.geometry import box
from app.core.errors import AppError
from app.services.data_contract_validation import (
AttributeRule,
BoundingBox,
ContractKind,
DataAssetValidationInput,
DataContract,
DataContractRegistry,
DataContractValidator,
FreshnessRules,
GeometryRecord,
GeometryRules,
LineageEvidence,
LineageRules,
RasterRules,
RequirementLevel,
Resolution,
ResolutionRules,
TransformationEvidence,
ValidationStatus,
build_default_data_contract_registry,
build_label_validation_input,
build_model_validation_input,
build_raster_ingest_input,
build_vector_ingest_input,
validate_registered_asset,
)
from app.services.data_quarantine_service import AssetUse, DataQuarantineService
FIXTURE_ROOT = Path(__file__).resolve().parents[2] / "tests" / "fixtures" / "data-contracts"
NOW = datetime(2026, 8, 1, 12, 0, tzinfo=timezone.utc)
CHECKSUM_A = "a" * 64
def _fixture_json(name: str) -> tuple[bytes, object]:
raw = (FIXTURE_ROOT / name).read_bytes()
return raw, json.loads(raw)
def _checksum(raw: bytes) -> str:
return sha256(raw).hexdigest()
def _lineage_with_transform() -> LineageEvidence:
return LineageEvidence(
transformations=(
TransformationEvidence(
name="epsg31370-to-epsg4326",
version="1.0.0",
checksum_sha256=CHECKSUM_A,
),
),
)
def _vector_input_from_fixture(name: str, *, source_crs: str = "EPSG:31370", storage_crs: str = "EPSG:4326") -> DataAssetValidationInput:
raw, payload = _fixture_json(name)
assert isinstance(payload, dict)
return build_vector_ingest_input(
asset_id=f"fixture:{name}",
source_crs=source_crs,
storage_crs=storage_crs,
feature_collection=payload,
checksum_sha256=_checksum(raw),
computed_checksum_sha256=_checksum(raw),
content=raw,
source_registry_id="source:digitaal-vlaanderen:grb",
source_snapshot_id="snapshot:grb:2026-07-31",
imported_at=NOW,
metadata={"license": "Open Data Licence", "provider": "Digitaal Vlaanderen"},
observed_at=NOW - timedelta(days=1),
source_version="2026.07.31",
lineage=_lineage_with_transform() if source_crs != storage_crs else LineageEvidence(),
)
def _issue_codes(report) -> set[str]:
return {issue.code for issue in report.issues}
def test_default_vector_contract_accepts_transformed_geojson_with_complete_provenance() -> None:
report = validate_registered_asset(_vector_input_from_fixture("vector-building-valid.geojson"), now=NOW)
assert report.validation_status == ValidationStatus.PASSED
assert report.quarantine_status == "not_quarantined"
assert report.provenance_status == "complete"
assert report.lineage_status == "complete"
persisted = report.persistence_fields()
assert persisted["data_contract_key"] == "geointel.vector.geojson"
assert persisted["data_contract_version"] == "1.0.0"
assert persisted["validation_report_json"]["report_sha256"] == report.report_sha256
def test_default_vector_contract_quarantines_lambert_coordinates_mislabelled_as_epsg4326() -> None:
report = validate_registered_asset(
_vector_input_from_fixture(
"vector-lambert-mislabelled-as-4326.geojson",
source_crs="EPSG:4326",
storage_crs="EPSG:4326",
),
now=NOW,
)
assert report.validation_status == ValidationStatus.FAILED
assert report.quarantine_status == "quarantined"
assert "CRS_COORDINATE_DOMAIN_VIOLATION" in _issue_codes(report)
def test_vector_contract_checks_geometry_attributes_bounds_and_topology_fail_closed() -> None:
contract = DataContract(
key="test.vector.buildings",
version="1.0.0",
kind=ContractKind.VECTOR,
accepted_source_crs=frozenset({"EPSG:4326"}),
canonical_storage_crs="EPSG:4326",
spatial_domain=BoundingBox(2.0, 49.0, 7.0, 52.0),
require_bounds=True,
geometry_rules=GeometryRules(
allowed_geometry_types=frozenset({"Polygon"}),
attribute_rules=(AttributeRule("native_id", accepted_types=("integer",)),),
forbid_shared_area=True,
),
)
raw = b"overlapping-vector"
asset = DataAssetValidationInput(
asset_id="vector:bad",
data_contract_key=contract.key,
data_contract_version=contract.version,
kind=ContractKind.VECTOR,
source_crs="EPSG:4326",
storage_crs="EPSG:4326",
bounds=BoundingBox(4.0, 51.0, 4.1, 51.1),
checksum_sha256=_checksum(raw),
computed_checksum_sha256=_checksum(raw),
content=raw,
geometry_records=(
GeometryRecord(box(4.0, 51.0, 4.05, 51.05), {"native_id": "wrong-type"}),
GeometryRecord(box(4.025, 51.025, 4.075, 51.075), {}),
),
source_registry_id="source:test",
source_snapshot_id="snapshot:test",
imported_at=NOW,
)
report = DataContractValidator.validate(contract, asset, now=NOW)
assert report.validation_status == ValidationStatus.FAILED
assert {"ATTRIBUTE_TYPE_INVALID", "ATTRIBUTE_REQUIRED", "TOPOLOGY_SHARED_AREA"} <= _issue_codes(report)
assert "BOUNDS_GEOMETRY_MISMATCH" in _issue_codes(report)
def test_default_vector_contract_validates_replayable_large_partition_stream_without_materialising_geometry_list() -> None:
"""Regional imports may be large but remain fully schema/domain checked.
The default contract has no source-specific shared-area rule, so the
validator must make its bounds/schema passes over a replayable stream
without accumulating every Shapely geometry in memory. A stricter
source-specific contract can still opt into a bounded topology batch.
"""
class ReplayableRecords:
def __init__(self, count: int) -> None:
self.count = count
self.iterations = 0
def __iter__(self):
self.iterations += 1
for index in range(self.count):
yield GeometryRecord(
box(4.69, 51.09, 4.70, 51.10),
{"partition_feature": index},
)
raw = b"partitioned-vector-stream"
records = ReplayableRecords(12_000)
asset = DataAssetValidationInput(
asset_id="vector:partitioned-stream",
data_contract_key="geointel.vector.geojson",
data_contract_version="1.0.0",
kind=ContractKind.VECTOR,
source_crs="EPSG:4326",
storage_crs="EPSG:4326",
bounds=BoundingBox(4.69, 51.09, 4.70, 51.10),
checksum_sha256=_checksum(raw),
computed_checksum_sha256=_checksum(raw),
content=raw,
metadata={"license": "Open Data"},
geometry_records=records,
source_registry_id="source:grb",
source_snapshot_id="snapshot:grb:partitioned",
imported_at=NOW,
observed_at=NOW,
source_version="2026-08-01",
)
report = validate_registered_asset(asset, now=NOW)
assert report.validation_status == ValidationStatus.PASSED
assert records.iterations >= 2
def test_raster_contract_accepts_explicit_units_and_quarantines_stale_bad_profile() -> None:
raw = b"raster-stage"
valid = build_raster_ingest_input(
asset_id="raster:valid",
source_crs="EPSG:31370",
storage_crs="EPSG:31370",
raster_profile={"width": 512, "height": 512, "band_count": 3, "dtype": ["uint8"]},
bounds=BoundingBox(193_277.5, 205_708.3, 193_777.5, 206_208.3),
resolution=Resolution(0.9765625, 0.9765625, "m"),
checksum_sha256=_checksum(raw),
computed_checksum_sha256=_checksum(raw),
content=raw,
source_registry_id="source:orthophoto",
source_snapshot_id="snapshot:orthophoto:2026.01",
imported_at=NOW,
metadata={"license": "Open Data"},
observed_at=None,
temporal_unknown_reason="latest mosaic has no per-pixel observation date",
source_version=None,
source_version_unknown_reason="provider did not publish an edition",
)
assert validate_registered_asset(valid, now=NOW).validation_status == ValidationStatus.PASSED
strict = DataContract(
key="test.raster.strict",
version="1.0.0",
kind=ContractKind.RASTER,
accepted_source_crs=frozenset({"EPSG:31370"}),
require_bounds=True,
raster_rules=RasterRules(allowed_band_counts=frozenset({3}), allowed_dtypes=frozenset({"uint8"})),
resolution_rules=ResolutionRules(allowed_units=frozenset({"m"}), min_x=0.2, max_x=1.0, min_y=0.2, max_y=1.0),
freshness_rules=FreshnessRules(observed_at=RequirementLevel.REQUIRED, max_age=timedelta(days=30)),
lineage_rules=LineageRules(require_transformation_when_crs_changes=False),
)
invalid = DataAssetValidationInput(
asset_id="raster:bad",
data_contract_key=strict.key,
data_contract_version=strict.version,
kind=ContractKind.RASTER,
source_crs="EPSG:31370",
storage_crs="EPSG:31370",
bounds=BoundingBox(100.0, 100.0, 200.0, 200.0),
checksum_sha256=_checksum(raw),
computed_checksum_sha256=_checksum(raw),
content=raw,
raster_profile={"width": 0, "height": 10, "band_count": 2, "dtype": ["float32"]},
resolution=Resolution(2.0, 0.1, "degree"),
source_registry_id="source:raster",
source_snapshot_id="snapshot:raster",
imported_at=NOW,
observed_at=NOW - timedelta(days=31),
)
report = DataContractValidator.validate(strict, invalid, now=NOW)
assert report.validation_status == ValidationStatus.FAILED
assert {
"RASTER_PROFILE_VALUE_INVALID",
"RASTER_BAND_COUNT_NOT_ALLOWED",
"RASTER_DTYPE_NOT_ALLOWED",
"RESOLUTION_UNIT_NOT_ALLOWED",
"RESOLUTION_OUT_OF_RANGE",
"FRESHNESS_EXCEEDED",
} <= _issue_codes(report)
def test_default_label_and_model_contracts_validate_good_and_bad_fixtures() -> None:
valid_raw, valid_labels = _fixture_json("labels-valid.json")
invalid_raw, invalid_labels = _fixture_json("labels-invalid.json")
assert isinstance(valid_labels, list)
assert isinstance(invalid_labels, list)
lineage = LineageEvidence(upstream_asset_ids=("image:1",), upstream_checksums_sha256=(CHECKSUM_A,))
valid_label = build_label_validation_input(
asset_id="label:valid",
label_records=valid_labels,
checksum_sha256=_checksum(valid_raw),
computed_checksum_sha256=_checksum(valid_raw),
content=valid_raw,
source_registry_id="source:labels",
source_snapshot_id="snapshot:labels:1",
imported_at=NOW,
metadata={
"image_checksum_sha256": CHECKSUM_A,
"class_ontology_version": "buildings-v1",
"source_corpus_manifest_sha256": CHECKSUM_A,
},
temporal_unknown_reason="labels inherit image observation handling",
source_version_unknown_reason="label release is represented by its snapshot",
lineage=lineage,
)
valid_report = validate_registered_asset(valid_label, now=NOW)
assert valid_report.validation_status == ValidationStatus.PASSED
invalid_label = build_label_validation_input(
asset_id="label:invalid",
label_records=invalid_labels,
checksum_sha256=_checksum(invalid_raw),
computed_checksum_sha256=_checksum(invalid_raw),
content=invalid_raw,
source_registry_id="source:labels",
source_snapshot_id="snapshot:labels:1",
imported_at=NOW,
metadata={
"image_checksum_sha256": "not-a-sha256",
"class_ontology_version": "buildings-v1",
"source_corpus_manifest_sha256": CHECKSUM_A,
},
temporal_unknown_reason="labels inherit image observation handling",
source_version_unknown_reason="label release is represented by its snapshot",
lineage=lineage,
)
invalid_report = validate_registered_asset(invalid_label, now=NOW)
assert invalid_report.validation_status == ValidationStatus.FAILED
assert {
"LABEL_CLASS_ID_NOT_ALLOWED",
"LABEL_NORMALIZED_COORDINATE_INVALID",
"METADATA_CHECKSUM_INVALID",
} <= _issue_codes(invalid_report)
pure_background_raw = b""
pure_background_metadata = {
"image_checksum_sha256": CHECKSUM_A,
"class_ontology_version": "buildings-v1",
"source_corpus_manifest_sha256": CHECKSUM_A,
"label_mode": "pure_background",
"sample_slug": "forest-background-aoi",
"split": "train",
"raster_dataset_id": "dataset:raster:1",
"reference_dataset_id": "dataset:reference:1",
"review_decision": "accepted",
"reviewer_id": "reviewer@example.test",
"reviewed_at": "2026-08-01T11:00:00+00:00",
"review_artifact_sha256": CHECKSUM_A,
}
pure_background = build_label_validation_input(
asset_id="label:pure-background",
label_records=(),
label_mode="pure_background",
checksum_sha256=_checksum(pure_background_raw),
computed_checksum_sha256=_checksum(pure_background_raw),
content=pure_background_raw,
source_registry_id="source:labels",
source_snapshot_id="snapshot:labels:1",
imported_at=NOW,
metadata=pure_background_metadata,
temporal_unknown_reason="labels inherit image observation handling",
source_version_unknown_reason="label release is represented by its snapshot",
lineage=LineageEvidence(
upstream_asset_ids=("dataset:raster:1", "dataset:reference:1"),
upstream_checksums_sha256=(CHECKSUM_A, CHECKSUM_A),
),
)
assert validate_registered_asset(pure_background, now=NOW).validation_status == ValidationStatus.PASSED
unmarked_empty = build_label_validation_input(
asset_id="label:unmarked-empty",
label_records=(),
checksum_sha256=_checksum(pure_background_raw),
computed_checksum_sha256=_checksum(pure_background_raw),
content=pure_background_raw,
source_registry_id="source:labels",
source_snapshot_id="snapshot:labels:1",
imported_at=NOW,
metadata={
"image_checksum_sha256": CHECKSUM_A,
"class_ontology_version": "buildings-v1",
"source_corpus_manifest_sha256": CHECKSUM_A,
},
temporal_unknown_reason="labels inherit image observation handling",
source_version_unknown_reason="label release is represented by its snapshot",
lineage=LineageEvidence(
upstream_asset_ids=("dataset:raster:1", "dataset:reference:1"),
upstream_checksums_sha256=(CHECKSUM_A, CHECKSUM_A),
),
)
assert "PURE_BACKGROUND_MODE_REQUIRED" in _issue_codes(validate_registered_asset(unmarked_empty, now=NOW))
model_raw = b"model-asset"
model = build_model_validation_input(
asset_id="model:valid",
model_metadata={"model_format": "pytorch", "framework": "torch", "class_mapping": {"0": "building"}},
checksum_sha256=_checksum(model_raw),
computed_checksum_sha256=_checksum(model_raw),
content=model_raw,
source_registry_id="source:model-registry",
source_snapshot_id="snapshot:model:1",
imported_at=NOW,
source_version="candidate-1",
metadata={"training_manifest_sha256": CHECKSUM_A, "runtime_manifest_sha256": CHECKSUM_A},
lineage=lineage,
)
assert validate_registered_asset(model, now=NOW).validation_status == ValidationStatus.PASSED
def test_unknown_contract_and_quarantine_gate_are_deterministic_and_fail_closed() -> None:
unknown = DataAssetValidationInput(
asset_id="asset:unknown",
data_contract_key="does.not.exist",
data_contract_version="9.9.9",
kind=ContractKind.VECTOR,
)
report = DataContractRegistry().validate(unknown, now=NOW)
assert report.validation_status == ValidationStatus.FAILED
assert report.quarantine_status == "quarantined"
assert _issue_codes(report) == {"DATA_CONTRACT_UNKNOWN"}
first = DataQuarantineService.decide(report)
second = DataQuarantineService.decide(report)
assert first.idempotency_key == second.idempotency_key
assert first.reason_codes == ("DATA_CONTRACT_UNKNOWN",)
with pytest.raises(AppError, match="cannot enter this pipeline") as exc_info:
DataQuarantineService.require_eligible(first, use=AssetUse.PRODUCTION_INFERENCE)
assert exc_info.value.code == "DATASET_QUARANTINED"
assert exc_info.value.details["use"] == "production_inference"
clean_report = validate_registered_asset(_vector_input_from_fixture("vector-building-valid.geojson"), now=NOW)
release_request = DataQuarantineService.decide(clean_report, previous=first)
assert release_request.quarantine_status == "quarantined"
assert release_request.requires_explicit_release is True
assert release_request.reason_codes == ("QUARANTINE_RELEASE_REQUIRES_EXPLICIT_PERSISTENCE",)
def test_registry_requires_exact_contract_version_and_fingerprints_schema() -> None:
registry = build_default_data_contract_registry()
version_mismatch = _vector_input_from_fixture("vector-building-valid.geojson")
mismatched = DataAssetValidationInput(
**{**version_mismatch.__dict__, "data_contract_version": "2.0.0"},
)
report = registry.validate(mismatched, now=NOW)
assert report.validation_status == ValidationStatus.FAILED
assert "DATA_CONTRACT_UNKNOWN" in _issue_codes(report)
contract = registry.resolve("geointel.vector.geojson", "1.0.0")
assert contract is not None
direct_report = DataContractValidator.validate(contract, mismatched, now=NOW)
assert direct_report.validation_status == ValidationStatus.FAILED
assert "DATA_CONTRACT_IDENTITY_MISMATCH" in _issue_codes(direct_report)