Files
yovision/contracts/tests/source-config-v1/test_source_config_v1.py
T

262 lines
11 KiB
Python

from __future__ import annotations
import copy
import hashlib
import json
import re
import unittest
from datetime import datetime
from pathlib import Path
from typing import Any
import rfc8785
from jsonschema import Draft202012Validator, FormatChecker
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
CONTRACT_ROOT = REPOSITORY_ROOT / "contracts" / "source-config" / "v1"
SCHEMA_PATH = CONTRACT_ROOT / "source-config.schema.json"
VALID_ROOT = CONTRACT_ROOT / "examples" / "valid"
INVALID_ROOT = CONTRACT_ROOT / "examples" / "invalid"
FORBIDDEN_KEY = re.compile(r"(?:credential|password|secret|token|username|cookie|jwt)", re.IGNORECASE)
FORBIDDEN_MEDIA_CHARACTER = re.compile(r"[?@#\\]")
def load_json(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
value = json.load(handle)
if not isinstance(value, dict):
raise AssertionError(f"{path} must contain a JSON object")
return value
SCHEMA = load_json(SCHEMA_PATH)
VALIDATOR = Draft202012Validator(SCHEMA, format_checker=FormatChecker())
def integrity_value(payload: dict[str, Any]) -> str:
content = copy.deepcopy(payload)
content.pop("integrity", None)
return hashlib.sha256(rfc8785.dumps(content)).hexdigest()
def set_integrity(payload: dict[str, Any]) -> None:
payload["integrity"] = {"algorithm": "sha256", "value": integrity_value(payload)}
def reject_secrets(value: Any, path: str = "config") -> None:
if isinstance(value, dict):
for key, child in value.items():
if FORBIDDEN_KEY.search(str(key)):
raise ValueError(f"secret field is forbidden at {path}.{key}")
reject_secrets(child, f"{path}.{key}")
elif isinstance(value, list):
for index, child in enumerate(value):
reject_secrets(child, f"{path}[{index}]")
def polygon_area(points: list[dict[str, float]]) -> float:
return abs(
sum(
point["x"] * points[(index + 1) % len(points)]["y"]
- points[(index + 1) % len(points)]["x"] * point["y"]
for index, point in enumerate(points)
)
/ 2
)
def validate_payload(payload: dict[str, Any]) -> None:
if payload.get("schema_version") != "yovision.source-config/v1":
raise ValueError("unknown schema major version")
reject_secrets(payload)
media_ref = str(payload.get("media", {}).get("ref", ""))
if (
FORBIDDEN_MEDIA_CHARACTER.search(media_ref)
or "://" in media_ref
or re.match(r"^[A-Za-z]:", media_ref)
):
raise ValueError("secret, query, authority, or internal path in media reference")
errors = sorted(VALIDATOR.iter_errors(payload), key=lambda error: list(error.absolute_path))
if errors:
first = errors[0]
location = ".".join(str(part) for part in first.absolute_path) or "config"
raise ValueError(f"schema validation failed at {location}: {first.message}")
profile = payload["profile"]
binding = payload["rule_set"]["profile_binding"]
if (binding["profile_id"], binding["width"], binding["height"]) != (
profile["id"],
profile["width"],
profile["height"],
):
raise ValueError("profile binding does not match the media profile")
published_at = datetime.fromisoformat(payload["published_at"].replace("Z", "+00:00"))
effective_at = datetime.fromisoformat(payload["effective_at"].replace("Z", "+00:00"))
if effective_at < published_at:
raise ValueError("effective_at precedes published_at")
rule_set = payload["rule_set"]
rules = [*rule_set["areas"], *rule_set["directional_lines"]]
identifiers = [rule["id"] for rule in rules]
if len(identifiers) != len(set(identifiers)):
raise ValueError("rule ids must be unique across the rule set")
if rule_set["state"] == "recalibration_required" and any(rule["enabled"] for rule in rules):
raise ValueError("recalibration-required rules must not remain enabled")
for area in rule_set["areas"]:
if polygon_area(area["points"]) <= 1e-12:
raise ValueError(f"area {area['id']} is a degenerate polygon")
for line in rule_set["directional_lines"]:
if line["start"] == line["end"]:
raise ValueError(f"directional line {line['id']} has identical endpoints")
if payload["integrity"]["value"] != integrity_value(payload):
raise ValueError("integrity digest mismatch")
def validate_transition(previous: dict[str, Any], current: dict[str, Any]) -> None:
validate_payload(previous)
validate_payload(current)
if previous["config_id"] != current["config_id"]:
raise ValueError("config_id cannot change within one revision stream")
if current["revision"] <= previous["revision"]:
raise ValueError("revision must increase strictly")
previous_profile = previous["profile"]
current_profile = current["profile"]
profile_changed = any(
previous_profile[field] != current_profile[field]
for field in ("id", "width", "height", "encoding")
)
previous_rule_versions = sorted(
(rule["id"], rule["version"])
for rule in [*previous["rule_set"]["areas"], *previous["rule_set"]["directional_lines"]]
)
current_rule_versions = sorted(
(rule["id"], rule["version"])
for rule in [*current["rule_set"]["areas"], *current["rule_set"]["directional_lines"]]
)
if (
profile_changed
and previous_rule_versions == current_rule_versions
and current["rule_set"]["state"] != "recalibration_required"
):
raise ValueError("profile changed without rule recalibration state or new rule versions")
class SourceConfigV1ContractTests(unittest.TestCase):
def test_schema_is_valid_draft_2020_12(self) -> None:
Draft202012Validator.check_schema(SCHEMA)
def test_all_valid_examples_pass_schema_semantics_and_integrity(self) -> None:
examples = sorted(VALID_ROOT.glob("*.json"))
self.assertGreaterEqual(len(examples), 2)
for path in examples:
with self.subTest(path=path.name):
validate_payload(load_json(path))
def test_invalid_examples_fail_for_the_declared_reason(self) -> None:
expected = load_json(INVALID_ROOT / "expected-errors.json")
self.assertGreaterEqual(len(expected), 6)
for filename, reason in expected.items():
with self.subTest(path=filename):
with self.assertRaisesRegex(ValueError, str(reason)):
validate_payload(load_json(INVALID_ROOT / filename))
def test_tampering_is_detected_after_other_validation(self) -> None:
payload = load_json(VALID_ROOT / "active.json")
payload["revision"] += 1
with self.assertRaisesRegex(ValueError, "integrity digest mismatch"):
validate_payload(payload)
def test_namespaced_optional_extensions_are_compatible_but_not_secret_bearing(self) -> None:
payload = load_json(VALID_ROOT / "active.json")
payload["extensions"] = {"example.analytics": {"samplingHint": "balanced"}}
set_integrity(payload)
validate_payload(payload)
payload["extensions"] = {"example.analytics": {"accessToken": "forbidden"}}
set_integrity(payload)
with self.assertRaisesRegex(ValueError, "secret field"):
validate_payload(payload)
def test_profile_revision_and_recalibration_semantics_are_safe(self) -> None:
payload = load_json(VALID_ROOT / "active.json")
payload["rule_set"]["profile_binding"]["width"] = 1280
set_integrity(payload)
with self.assertRaisesRegex(ValueError, "profile binding"):
validate_payload(payload)
payload = load_json(VALID_ROOT / "active.json")
payload["rule_set"]["state"] = "recalibration_required"
set_integrity(payload)
with self.assertRaisesRegex(ValueError, "must not remain enabled"):
validate_payload(payload)
def test_revision_stream_rejects_stale_and_unrecalibrated_profile_change(self) -> None:
previous = load_json(VALID_ROOT / "active.json")
current = copy.deepcopy(previous)
current["revision"] = previous["revision"]
set_integrity(current)
with self.assertRaisesRegex(ValueError, "revision must increase"):
validate_transition(previous, current)
current["revision"] += 1
current["profile"].update({"id": "main-stream-v2", "width": 1280, "height": 720})
current["rule_set"]["profile_binding"].update(
{"profile_id": "main-stream-v2", "width": 1280, "height": 720}
)
set_integrity(current)
with self.assertRaisesRegex(ValueError, "without rule recalibration"):
validate_transition(previous, current)
validate_transition(previous, load_json(VALID_ROOT / "recalibration-required.json"))
def test_rule_geometry_and_global_ids_are_semantically_validated(self) -> None:
payload = load_json(VALID_ROOT / "active.json")
payload["rule_set"]["areas"][0]["points"] = [
{"x": 0, "y": 0},
{"x": 0.5, "y": 0.5},
{"x": 1, "y": 1},
]
set_integrity(payload)
with self.assertRaisesRegex(ValueError, "degenerate polygon"):
validate_payload(payload)
payload = load_json(VALID_ROOT / "active.json")
payload["rule_set"]["directional_lines"][0]["id"] = payload["rule_set"]["areas"][0]["id"]
set_integrity(payload)
with self.assertRaisesRegex(ValueError, "ids must be unique"):
validate_payload(payload)
def test_effective_time_cannot_precede_publication(self) -> None:
payload = load_json(VALID_ROOT / "active.json")
payload["effective_at"] = "2026-08-30T23:59:59Z"
set_integrity(payload)
with self.assertRaisesRegex(ValueError, "precedes"):
validate_payload(payload)
def test_shared_payload_does_not_claim_either_product_internal_model(self) -> None:
for path in sorted(VALID_ROOT.glob("*.json")):
serialized = json.dumps(load_json(path), ensure_ascii=False).lower()
self.assertNotIn("brain.internal.input", serialized)
self.assertNotIn("streamuri", serialized)
self.assertNotIn("profiletoken", serialized)
self.assertNotIn("database", serialized)
self.assertNotRegex(serialized, r"[a-z]:\\")
def test_mapper_documents_both_product_test_responsibilities(self) -> None:
mapper = (CONTRACT_ROOT / "mapper-fields.md").read_text(encoding="utf-8")
self.assertIn("Sense 生产者契约测试", mapper)
self.assertIn("Brain 消费者契约测试", mapper)
self.assertIn("brain.internal.input/v1", mapper)
self.assertIn("admissionProfile.StreamURI", mapper)
if __name__ == "__main__":
unittest.main()