"""Determinism and validation checks for normalized contracts.""" import json from datetime import UTC, datetime from pathlib import Path import pytest from hypothesis import given from hypothesis import strategies as st from pydantic import ValidationError from app.contracts.model import Snapshot, canonical_json from app.contracts.normalize import normalize_snapshot from app.contracts.source import ApiViewerParser pytestmark = pytest.mark.pve_stub FIXTURE = Path(__file__).parents[1] / "fixtures" / "api-viewer" / "pve-9.2.3-version.json" RETRIEVED_AT = datetime(2026, 7, 12, 20, 8, 59, tzinfo=UTC) def make_snapshot() -> Snapshot: raw = FIXTURE.read_bytes() parsed = ApiViewerParser().parse(raw) snapshot, _ = normalize_snapshot( parsed, raw=raw, source_version="9.2.3", retrieved_at=RETRIEVED_AT ) return snapshot def test_normalization_is_deterministic_and_round_trips() -> None: first = make_snapshot() second = make_snapshot() assert first.canonical_bytes() == second.canonical_bytes() assert first.checksum() == second.checksum() assert Snapshot.model_validate_json(first.canonical_bytes()) == first assert first.paths[0].methods[0].checksum == second.paths[0].methods[0].checksum def test_snapshot_validates_declared_counts() -> None: data = make_snapshot().model_dump(mode="json") data["method_count"] = 99 with pytest.raises(ValidationError, match="method_count"): Snapshot.model_validate(data) def test_unknown_schema_fields_are_retained() -> None: raw = json.dumps( [ { "path": "/future", "info": { "GET": { "name": "future", "returns": {"type": "string", "futureKeyword": {"x": 1}}, } }, } ] ).encode() snapshot, _ = normalize_snapshot( ApiViewerParser().parse(raw), raw=raw, source_version="test", retrieved_at=RETRIEVED_AT, ) assert snapshot.paths[0].methods[0].returns.extra["futureKeyword"] == {"x": 1} def test_nullable_source_collections_normalize_as_empty() -> None: raw = ( b'[{"path":"/nullable","info":{"GET":{"parameters":{"properties":null},' b'"returns":{"type":"string","enum":null}}}}]' ) snapshot, _ = normalize_snapshot( ApiViewerParser().parse(raw), raw=raw, source_version="test", retrieved_at=RETRIEVED_AT, ) method = snapshot.paths[0].methods[0] assert method.parameters == () assert method.returns.enum == () @given(st.dictionaries(st.text(min_size=1), st.integers(), max_size=10)) def test_canonical_json_is_independent_of_mapping_order(values: dict[str, int]) -> None: reversed_values = dict(reversed(tuple(values.items()))) assert canonical_json(values) == canonical_json(reversed_values)