mirror of
https://github.com/Such-Software/adaptive-multi-protocol-browser.git
synced 2026-10-17 14:31:46 +02:00
170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .interaction_policy import interaction_failures
|
|
from .routing import route_url
|
|
|
|
|
|
SUPPORTED_SCHEMAS = {"ampg.fixture-manifest.v1"}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FixtureCheck:
|
|
site_id: str
|
|
protocol: str
|
|
route_match: str
|
|
fixture_path: str
|
|
url: str
|
|
expected_transport: str
|
|
actual_transport: str
|
|
expected_profile: str
|
|
actual_profile: str
|
|
tier: str
|
|
identity: str
|
|
payments: str
|
|
realtime: bool
|
|
public_allowed: bool
|
|
status: str
|
|
message: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FixtureCheckResult:
|
|
manifest_path: Path
|
|
schema: str
|
|
site_id: str
|
|
checks: tuple[FixtureCheck, ...]
|
|
|
|
@property
|
|
def ok(self) -> bool:
|
|
return all(check.status == "ok" for check in self.checks)
|
|
|
|
|
|
def check_fixture_manifest(path: Path) -> FixtureCheckResult:
|
|
data = _load_manifest(path)
|
|
schema = _string(data, "schema")
|
|
if schema not in SUPPORTED_SCHEMAS:
|
|
raise ValueError(f"{path}: unsupported manifest schema {schema!r}")
|
|
|
|
site = data.get("site")
|
|
if not isinstance(site, dict):
|
|
raise ValueError(f"{path}: missing site table")
|
|
site_id = _string(site, "id")
|
|
|
|
fixtures = data.get("fixtures")
|
|
if not isinstance(fixtures, list):
|
|
raise ValueError(f"{path}: fixtures must be a list")
|
|
|
|
checks = tuple(_check_fixture(site_id, fixture, path) for fixture in fixtures)
|
|
return FixtureCheckResult(
|
|
manifest_path=path,
|
|
schema=schema,
|
|
site_id=site_id,
|
|
checks=checks,
|
|
)
|
|
|
|
|
|
def _check_fixture(site_id: str, fixture: Any, path: Path) -> FixtureCheck:
|
|
if not isinstance(fixture, dict):
|
|
raise ValueError(f"{path}: fixture entries must be tables")
|
|
protocol = _string(fixture, "protocol")
|
|
url = _string(fixture, "url")
|
|
checks = fixture.get("checks")
|
|
if not isinstance(checks, dict):
|
|
raise ValueError(f"{path}: {protocol}: missing checks table")
|
|
expected_transport = _string(checks, "transport")
|
|
expected_profile = _string(checks, "profile")
|
|
interaction = _interaction_policy(fixture)
|
|
route_metadata = _route_metadata(fixture)
|
|
|
|
route = route_url(url)
|
|
failures: list[str] = []
|
|
if route.transport != expected_transport:
|
|
failures.append(f"transport expected {expected_transport} got {route.transport}")
|
|
if route.profile != expected_profile:
|
|
failures.append(f"profile expected {expected_profile} got {route.profile}")
|
|
failures.extend(
|
|
interaction_failures(
|
|
expected_transport,
|
|
tier=interaction["tier"],
|
|
identity=interaction["identity"],
|
|
payments=interaction["payments"],
|
|
realtime=interaction["realtime"],
|
|
public_allowed=interaction["public_allowed"],
|
|
)
|
|
)
|
|
|
|
return FixtureCheck(
|
|
site_id=site_id,
|
|
protocol=protocol,
|
|
route_match=route_metadata["match"],
|
|
fixture_path=route_metadata["fixture_path"],
|
|
url=route.normalized,
|
|
expected_transport=expected_transport,
|
|
actual_transport=route.transport,
|
|
expected_profile=expected_profile,
|
|
actual_profile=route.profile,
|
|
tier=interaction["tier"],
|
|
identity=interaction["identity"],
|
|
payments=interaction["payments"],
|
|
realtime=interaction["realtime"],
|
|
public_allowed=interaction["public_allowed"],
|
|
status="fail" if failures else "ok",
|
|
message="; ".join(failures) if failures else "route matches",
|
|
)
|
|
|
|
|
|
def _load_manifest(path: Path) -> dict[str, Any]:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(data, dict):
|
|
raise ValueError(f"{path}: manifest must be a JSON object")
|
|
return data
|
|
|
|
|
|
def _string(data: dict[str, Any], key: str) -> str:
|
|
value = data.get(key)
|
|
if not isinstance(value, str) or not value:
|
|
raise ValueError(f"missing required string field {key!r}")
|
|
return value
|
|
|
|
|
|
def _interaction_policy(fixture: dict[str, Any]) -> dict[str, Any]:
|
|
raw = fixture.get("interaction", {})
|
|
if not isinstance(raw, dict):
|
|
raise ValueError(f"{fixture.get('protocol', 'fixture')}: interaction must be a table")
|
|
return {
|
|
"tier": _optional_string(raw, "tier", "static"),
|
|
"identity": _optional_string(raw, "identity", "none"),
|
|
"payments": _optional_string(raw, "payments", "none"),
|
|
"realtime": _optional_bool(raw, "realtime", False),
|
|
"public_allowed": _optional_bool(raw, "public_allowed", True),
|
|
}
|
|
|
|
|
|
def _route_metadata(fixture: dict[str, Any]) -> dict[str, str]:
|
|
raw = fixture.get("route", {})
|
|
if not isinstance(raw, dict):
|
|
raise ValueError(f"{fixture.get('protocol', 'fixture')}: route must be a table")
|
|
return {
|
|
"match": _optional_string(raw, "match", "/"),
|
|
"fixture_path": _optional_string(raw, "fixture_path", "/"),
|
|
}
|
|
|
|
|
|
def _optional_string(data: dict[str, Any], key: str, default: str) -> str:
|
|
value = data.get(key, default)
|
|
if not isinstance(value, str) or not value:
|
|
raise ValueError(f"interaction.{key} must be a non-empty string")
|
|
return value
|
|
|
|
|
|
def _optional_bool(data: dict[str, Any], key: str, default: bool) -> bool:
|
|
value = data.get(key, default)
|
|
if not isinstance(value, bool):
|
|
raise ValueError(f"interaction.{key} must be true or false")
|
|
return value
|