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)