test(v2): harden protocol and clean-break contracts
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -19,20 +20,24 @@ def load(path: Path) -> Any:
|
||||
raise AssertionError(f"invalid contract JSON: {path}") from error
|
||||
|
||||
|
||||
def validator(name: str) -> jsonschema.Draft202012Validator:
|
||||
schema = load(CONTRACT / f"{name}.schema.json")
|
||||
jsonschema.Draft202012Validator.check_schema(schema)
|
||||
return jsonschema.Draft202012Validator(
|
||||
schema,
|
||||
format_checker=jsonschema.FormatChecker(),
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
validator(name).validate(load(FIXTURES / f"valid-{name}.json"))
|
||||
|
||||
|
||||
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)
|
||||
validator(name).validate(load(FIXTURES / f"invalid-{name}.json"))
|
||||
|
||||
|
||||
def test_valid_golden_files_are_canonical_json() -> None:
|
||||
@@ -52,6 +57,68 @@ def test_protocol_contracts_are_versioned_and_closed() -> None:
|
||||
assert not manifest_schema["additionalProperties"]
|
||||
|
||||
|
||||
def test_uuidv7_and_rfc3339_formats_are_enforced() -> None:
|
||||
repository = load(FIXTURES / "valid-repository.json")
|
||||
manifest = load(FIXTURES / "valid-manifest.json")
|
||||
|
||||
repository["repository_id"] = "123e4567-e89b-42d3-a456-426614174000"
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
validator("repository").validate(repository)
|
||||
|
||||
manifest["created_at"] = "2026-07-27 12:00:00"
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
validator("manifest").validate(manifest)
|
||||
|
||||
|
||||
def test_manifest_signature_and_source_consistency_are_required() -> None:
|
||||
manifest = load(FIXTURES / "valid-manifest.json")
|
||||
assert manifest["manifest_signature"]["algorithm"] == "ed25519"
|
||||
|
||||
missing_signature = deepcopy(manifest)
|
||||
del missing_signature["manifest_signature"]
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
validator("manifest").validate(missing_signature)
|
||||
|
||||
incomplete_consistency = deepcopy(manifest)
|
||||
incomplete_consistency["source_consistency"] = {"adapter": "local"}
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
validator("manifest").validate(incomplete_consistency)
|
||||
|
||||
|
||||
def test_entry_types_bind_blobs_and_safe_link_targets() -> None:
|
||||
manifest = load(FIXTURES / "valid-manifest.json")
|
||||
entries = manifest["entries"]
|
||||
|
||||
file_without_blob = deepcopy(manifest)
|
||||
file_without_blob["entries"][1]["blob_digest"] = None
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
validator("manifest").validate(file_without_blob)
|
||||
|
||||
directory_with_blob = deepcopy(manifest)
|
||||
directory_with_blob["entries"][0]["blob_digest"] = "c" * 64
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
validator("manifest").validate(directory_with_blob)
|
||||
|
||||
for unsafe_target in ("/etc/passwd", "../escape", "dir\\escape"):
|
||||
unsafe_symlink = deepcopy(manifest)
|
||||
unsafe_symlink["entries"] = [
|
||||
{
|
||||
"blob_digest": None,
|
||||
"link_target": unsafe_target,
|
||||
"metadata_support": [],
|
||||
"mode": None,
|
||||
"mtime_ns": None,
|
||||
"path": "link",
|
||||
"size": 0,
|
||||
"type": "symlink",
|
||||
}
|
||||
]
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
validator("manifest").validate(unsafe_symlink)
|
||||
|
||||
assert entries[1]["type"] == "file"
|
||||
|
||||
|
||||
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"]]
|
||||
|
||||
Reference in New Issue
Block a user