test(v2): harden protocol and clean-break contracts

This commit is contained in:
2026-07-27 18:52:47 +02:00
parent 791f4526f9
commit 92f2aaa27f
2 changed files with 109 additions and 10 deletions
+35 -3
View File
@@ -4,6 +4,8 @@ import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).parents[2]
@@ -26,12 +28,42 @@ def test_v1_reference_tag_exists() -> None:
assert result.stdout.strip() == "v1-reference-2026-07-27"
def test_forbidden_v1_scanner_accepts_runtime_tree() -> None:
result = subprocess.run(
[sys.executable, "tools/forbidden_v1_scan.py", "."],
def run_scanner(root: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(ROOT / "tools/forbidden_v1_scan.py"), str(root)],
cwd=ROOT,
capture_output=True,
check=False,
text=True,
)
def test_forbidden_v1_scanner_accepts_runtime_tree() -> None:
result = run_scanner(ROOT)
assert result.returncode == 0, result.stdout + result.stderr
@pytest.mark.parametrize(
("relative_path", "canary"),
[
("backend/src/backup_tool/database.py", 'DB_PATH = "backup_tool.db"'),
("backend/src/backup_tool/payload.py", "def read_legacy_payload(): ..."),
("backend/src/backup_tool/importer.py", "class LegacyBackupImporter: ..."),
("backend/src/backup_tool/converter.py", "def convert_v1_backup(): ..."),
("backend/src/backup_tool/layout.py", 'FORMAT = "%Y-%m-%d_%H%M%S"'),
("docker-compose.yml", "command: uvicorn app.main:app"),
],
)
def test_forbidden_v1_scanner_rejects_runtime_and_config_canaries(
tmp_path: Path,
relative_path: str,
canary: str,
) -> None:
path = tmp_path / relative_path
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(canary, encoding="utf-8")
result = run_scanner(tmp_path)
assert result.returncode == 1, relative_path
assert str(relative_path) in result.stderr
+74 -7
View File
@@ -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"]]