Initial commit: stateful OpenStack API laboratory simulator.
Ship Keystone auth, multi-service handlers (Yoga→Dalmatian), Compose/Helm packaging, API contract packs, and pytest/Pulumi coverage labs.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Golden HTTP input/output compatibility checks."""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.api.registry import HandlerRegistry
|
||||
from app.config import Settings
|
||||
from app.contracts.model import Method, Parameter, PathContract, Schema, Snapshot
|
||||
from app.main import create_app
|
||||
from app.security.auth import csrf_token, issue_ticket
|
||||
from tests.unit.test_health import FakeDatabase
|
||||
|
||||
|
||||
async def client_for(tmp_path: Path) -> AsyncClient:
|
||||
method = Method(
|
||||
verb="POST",
|
||||
name="update",
|
||||
parameters=(
|
||||
Parameter(name="node", definition=Schema(type="string")),
|
||||
Parameter(name="count", definition=Schema(type="integer", minimum=1)),
|
||||
Parameter(name="force", definition=Schema(type="boolean", optional=True)),
|
||||
Parameter(name="scsi[n]", definition=Schema(type="string", optional=True)),
|
||||
),
|
||||
returns=Schema(type="null"),
|
||||
checksum="1" * 64,
|
||||
)
|
||||
snapshot = Snapshot(
|
||||
source_version="test",
|
||||
retrieved_at=datetime(2026, 1, 1, tzinfo=UTC),
|
||||
raw_sha256="0" * 64,
|
||||
paths=(PathContract(path="/nodes/{node}/test", methods=(method,)),),
|
||||
path_count=1,
|
||||
method_count=1,
|
||||
)
|
||||
path = tmp_path / "snapshot.json"
|
||||
path.write_bytes(snapshot.canonical_bytes())
|
||||
handlers = HandlerRegistry()
|
||||
|
||||
async def handler(_request: Request, inputs: dict[str, Any]) -> None:
|
||||
assert inputs["values"]["count"] >= 1
|
||||
if "scsi0" in inputs["values"]:
|
||||
assert inputs["values"]["scsi0"] == "local:disk,size=8G"
|
||||
return None
|
||||
|
||||
handlers.register("/nodes/{node}/test", "POST", handler)
|
||||
app = create_app(
|
||||
Settings(contract_snapshot=path, compatibility_evidence=None),
|
||||
lambda _settings: FakeDatabase(True),
|
||||
handlers,
|
||||
worker_factories=(),
|
||||
)
|
||||
key = Settings().ticket_signing_key.get_secret_value().encode()
|
||||
ticket = issue_ticket("root@pam", key)
|
||||
return AsyncClient(
|
||||
transport=ASGITransport(app=app),
|
||||
base_url="http://test",
|
||||
cookies={"PVEAuthCookie": ticket},
|
||||
headers={"CSRFPreventionToken": csrf_token(ticket, key)},
|
||||
)
|
||||
|
||||
|
||||
async def test_json_input_and_null_envelope(tmp_path: Path) -> None:
|
||||
async with await client_for(tmp_path) as client:
|
||||
response = await client.post("/api2/json/nodes/pve/test", json={"count": 2, "force": True})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"data": None}
|
||||
|
||||
|
||||
async def test_form_input_and_validation_error_shape(tmp_path: Path) -> None:
|
||||
async with await client_for(tmp_path) as client:
|
||||
valid = await client.post(
|
||||
"/api2/json/nodes/pve/test",
|
||||
content="count=1&force=yes",
|
||||
headers={"content-type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
invalid = await client.post("/api2/json/nodes/pve/test", json={"count": 0, "unknown": "x"})
|
||||
|
||||
assert valid.status_code == 200
|
||||
assert invalid.status_code == 400
|
||||
assert invalid.json() == {
|
||||
"data": None,
|
||||
"message": "parameter verification failed",
|
||||
"errors": {
|
||||
"count": "value must be at least 1",
|
||||
"unknown": "property is not defined in schema",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def test_non_object_json_is_rejected_without_fastapi_body(tmp_path: Path) -> None:
|
||||
async with await client_for(tmp_path) as client:
|
||||
response = await client.post("/api2/json/nodes/pve/test", json=[1, 2])
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["errors"] == {"body": "expected an object"}
|
||||
assert "detail" not in response.json()
|
||||
|
||||
|
||||
async def test_indexed_contract_parameter_accepts_concrete_device(tmp_path: Path) -> None:
|
||||
async with await client_for(tmp_path) as client:
|
||||
response = await client.post(
|
||||
"/api2/json/nodes/pve/test", json={"count": 1, "scsi0": "local:disk,size=8G"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
Reference in New Issue
Block a user