Files
proxmox-api-simulator/tests/unit/test_dynamic_routes.py
T
Sergey Antropoff 777926487b Add a stateful Proxmox API console and broad handler coverage beyond the
initial QEMU slice, backed by imported contracts for majors 6–9.
- Implement durable handlers for access/auth, cluster, LXC, storage, HA,
  firewall, Ceph, SDN, ACME, notifications, pools, mapping, and node ops
- Serve an interactive Web UI with catalog browsing, demo seed controls,
  and OpenAPI/help surfaces
- Bundle PVE 6.4-15, 7.4-16, and 8.4.5 contract revisions alongside 9.2.3
- Support in-memory runtime contract Apply (POST /ui/api/contract/apply)
  so /version and /api2 routes follow the selected major until restart
- Expand seed profiles (including demo-cluster), migrations 007–008, TLS
  gateway config, Compose/Makefile tooling, and compatibility evidence
- Tighten .gitignore for macOS, hidden directories (.*/), and local secrets
2026-07-16 01:08:01 +03:00

100 lines
3.3 KiB
Python

"""Contract-driven route registry tests."""
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import pytest
from fastapi import Request
from httpx import ASGITransport, AsyncClient
from pydantic import ValidationError
from app.api.registry import HandlerRegistry, RouteCollisionError
from app.config import Settings
from app.contracts.model import Method, PathContract, Schema, Snapshot
from app.main import create_app
from tests.unit.test_health import FakeDatabase
def contract_snapshot(*methods: Method) -> Snapshot:
paths = (PathContract(path="/version", methods=methods),)
return Snapshot(
source_version="test",
retrieved_at=datetime(2026, 1, 1, tzinfo=UTC),
raw_sha256="0" * 64,
paths=paths,
path_count=1,
method_count=len(methods),
)
def get_method() -> Method:
return Method(
verb="GET",
name="version",
returns=Schema(type="object", properties={"version": Schema(type="string")}),
checksum="1" * 64,
)
async def request_app(
tmp_path: Path, fallback: str, handlers: HandlerRegistry | None = None
) -> tuple[dict[str, Any], dict[str, Any]]:
snapshot_path = tmp_path / "snapshot.json"
snapshot_path.write_bytes(contract_snapshot(get_method()).canonical_bytes())
settings = Settings(
contract_snapshot=snapshot_path,
contract_fallback=fallback,
compatibility_evidence=None,
)
database = FakeDatabase(True)
app = create_app(
settings,
lambda _settings: database,
handlers if handlers is not None else HandlerRegistry(),
worker_factories=(),
)
async with app.router.lifespan_context(app):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
json_response = await client.get("/api2/json/version")
extjs_response = await client.get("/api2/extjs/version")
return json_response.json(), extjs_response.json()
async def test_registered_handler_serves_both_renderers(tmp_path: Path) -> None:
handlers = HandlerRegistry()
async def version(_request: Request, _inputs: dict[str, Any]) -> dict[str, str]:
return {"version": "9.2.3"}
handlers.register("/version", "GET", version)
json_body, extjs_body = await request_app(tmp_path, "error", handlers)
assert json_body == {"data": {"version": "9.2.3"}}
assert extjs_body == {"data": {"version": "9.2.3"}, "success": True}
async def test_explicit_fallback_modes(tmp_path: Path) -> None:
error_body, _ = await request_app(tmp_path, "error")
default_body, _ = await request_app(tmp_path, "schema-default")
assert error_body["errors"] == "handler pending for this contract method"
assert default_body["data"]["version"] in {None, "example"}
def test_duplicate_snapshot_routes_are_rejected() -> None:
with pytest.raises(ValidationError, match="duplicate"):
contract_snapshot(get_method(), get_method())
def test_duplicate_semantic_handlers_are_rejected() -> None:
handlers = HandlerRegistry()
async def handler(_request: Request, _inputs: dict[str, Any]) -> None:
return None
handlers.register("/version", "GET", handler)
with pytest.raises(RouteCollisionError, match="duplicate"):
handlers.register("/version", "GET", handler)