89 lines
3.6 KiB
Python
89 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
CONTRACTS = HERE.parents[1]
|
|
EVENT_SUPPORT = HERE.parent / "events-v1"
|
|
sys.path.insert(0, str(EVENT_SUPPORT))
|
|
|
|
from contract_support import load_json, validate # noqa: E402
|
|
|
|
|
|
class EvidenceV1ContractTests(unittest.TestCase):
|
|
schema_path = CONTRACTS / "evidence" / "v1" / "evidence-reference.schema.json"
|
|
schema = load_json(schema_path)
|
|
|
|
def errors_for(self, payload: object) -> list[str]:
|
|
return validate(payload, self.schema, self.schema_path)
|
|
|
|
def test_pending_success_and_failed_examples_are_valid(self) -> None:
|
|
for name in ("pending.json", "success.json", "failed.json"):
|
|
with self.subTest(name=name):
|
|
payload = load_json(CONTRACTS / "evidence" / "v1" / "examples" / name)
|
|
self.assertEqual([], self.errors_for(payload))
|
|
|
|
def test_state_specific_metadata_is_enforced(self) -> None:
|
|
success = load_json(CONTRACTS / "evidence" / "v1" / "examples" / "success.json")
|
|
for required in ("content_type", "integrity"):
|
|
with self.subTest(success_requires=required):
|
|
candidate = copy.deepcopy(success)
|
|
del candidate[required]
|
|
self.assertTrue(self.errors_for(candidate))
|
|
|
|
legacy_available = copy.deepcopy(success)
|
|
legacy_available["status"] = "available"
|
|
self.assertTrue(self.errors_for(legacy_available))
|
|
|
|
failed = load_json(CONTRACTS / "evidence" / "v1" / "examples" / "failed.json")
|
|
del failed["failure"]
|
|
self.assertTrue(self.errors_for(failed))
|
|
|
|
pending = load_json(CONTRACTS / "evidence" / "v1" / "examples" / "pending.json")
|
|
pending["content_type"] = "image/jpeg"
|
|
self.assertTrue(self.errors_for(pending))
|
|
|
|
def test_sensitive_access_material_and_unknown_version_are_rejected(self) -> None:
|
|
fixture = load_json(HERE / "fixtures" / "sensitive-field.json")
|
|
self.assertTrue(any("local_path" in error for error in self.errors_for(fixture)))
|
|
base = load_json(CONTRACTS / "evidence" / "v1" / "examples" / "pending.json")
|
|
for forbidden, value in {
|
|
"camera_password": "forbidden",
|
|
"user_token": "forbidden",
|
|
"signed_url": "https://forbidden.invalid/object?signature=forbidden",
|
|
"face_feature": "forbidden",
|
|
"alert_state": "acked"
|
|
}.items():
|
|
with self.subTest(forbidden=forbidden):
|
|
candidate = copy.deepcopy(base)
|
|
candidate[forbidden] = value
|
|
self.assertTrue(self.errors_for(candidate))
|
|
unknown = copy.deepcopy(base)
|
|
unknown["schema_version"] = "yovision.evidence-reference/v2"
|
|
self.assertTrue(self.errors_for(unknown))
|
|
|
|
def test_openapi_refs_and_degradation_responses(self) -> None:
|
|
path = CONTRACTS / "evidence" / "v1" / "openapi.json"
|
|
spec = load_json(path)
|
|
operation = spec["paths"]["/v1/evidence/{evidence_id}"]["get"]
|
|
self.assertEqual({"200", "404", "410"}, set(operation["responses"]))
|
|
refs: list[str] = []
|
|
|
|
def collect(value: object) -> None:
|
|
if isinstance(value, dict):
|
|
refs.extend(item for key, item in value.items() if key == "$ref")
|
|
for item in value.values(): collect(item)
|
|
elif isinstance(value, list):
|
|
for item in value: collect(item)
|
|
|
|
collect(spec)
|
|
for ref in refs:
|
|
self.assertTrue((path.parent / ref).resolve().is_file(), ref)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|