test(v2): define protocol and clean-break contracts
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import jsonschema
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
CONTRACT = ROOT / "contracts" / "repository" / "v1"
|
||||
FIXTURES = CONTRACT / "fixtures"
|
||||
|
||||
|
||||
def load(path: Path) -> Any:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise AssertionError(f"invalid contract JSON: {path}") from error
|
||||
|
||||
|
||||
def test_repository_and_manifest_golden_files_validate() -> None:
|
||||
for name in ("repository", "manifest"):
|
||||
schema = load(CONTRACT / f"{name}.schema.json")
|
||||
fixture = load(FIXTURES / f"valid-{name}.json")
|
||||
jsonschema.Draft202012Validator.check_schema(schema)
|
||||
jsonschema.validate(fixture, schema)
|
||||
|
||||
|
||||
def test_invalid_golden_files_are_rejected() -> None:
|
||||
for name in ("repository", "manifest"):
|
||||
schema = load(CONTRACT / f"{name}.schema.json")
|
||||
fixture = load(FIXTURES / f"invalid-{name}.json")
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
jsonschema.validate(fixture, schema)
|
||||
|
||||
|
||||
def test_valid_golden_files_are_canonical_json() -> None:
|
||||
for path in sorted(FIXTURES.glob("valid-*.json")):
|
||||
data = load(path)
|
||||
canonical = json.dumps(data, sort_keys=True, separators=(",", ":")) + "\n"
|
||||
assert path.read_text(encoding="utf-8") == canonical
|
||||
|
||||
|
||||
def test_protocol_contracts_are_versioned_and_closed() -> None:
|
||||
repository_schema = load(CONTRACT / "repository.schema.json")
|
||||
manifest_schema = load(CONTRACT / "manifest.schema.json")
|
||||
|
||||
assert repository_schema["properties"]["format_version"]["const"] == 1
|
||||
assert manifest_schema["properties"]["format_version"]["const"] == 1
|
||||
assert not repository_schema["additionalProperties"]
|
||||
assert not manifest_schema["additionalProperties"]
|
||||
|
||||
|
||||
def test_normalized_path_vectors_are_relative_posix_paths() -> None:
|
||||
vectors = load(CONTRACT / "normalized-paths.json")
|
||||
valid = [case for case in vectors if case["valid"]]
|
||||
invalid = [case for case in vectors if not case["valid"]]
|
||||
|
||||
assert {case["raw"] for case in invalid} >= {"/absolute", "../escape", "a\\b"}
|
||||
for case in valid:
|
||||
normalized = case["normalized"]
|
||||
assert normalized and not normalized.startswith("/")
|
||||
assert "\\" not in normalized
|
||||
assert ".." not in normalized.split("/")
|
||||
|
||||
|
||||
def test_state_errors_capabilities_and_fault_points_are_frozen() -> None:
|
||||
transitions = load(CONTRACT / "execution-transitions.json")
|
||||
assert transitions["queued"] == ["cancelled", "preparing"]
|
||||
assert transitions["verifying"] == ["committed", "failed"]
|
||||
assert transitions["committed"] == []
|
||||
|
||||
errors = load(CONTRACT / "error-codes.json")
|
||||
assert len(errors) == len(set(errors))
|
||||
assert all(code == code.lower() and " " not in code for code in errors)
|
||||
|
||||
capabilities = load(CONTRACT / "capabilities-v2.0.json")
|
||||
assert capabilities["sources"] == ["local", "ssh"]
|
||||
assert not capabilities["features"]["tar_download"]
|
||||
assert not capabilities["features"]["postgresql"]
|
||||
assert not capabilities["features"]["mysql"]
|
||||
|
||||
fault_points = load(CONTRACT / "fault-points.json")
|
||||
assert {
|
||||
"blob.before_write",
|
||||
"blob.after_fsync",
|
||||
"manifest.before_publish",
|
||||
"metadata.before_commit",
|
||||
"restore.before_replace",
|
||||
}.issubset(fault_points)
|
||||
Reference in New Issue
Block a user