92 lines
3.3 KiB
Python
92 lines
3.3 KiB
Python
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)
|