Files
ITWorx_MCP_Hub-Public/tests/test_wp270_functional_operations.py
T

244 lines
8.1 KiB
Python

"""WP-270 operational UI inputs and bounded read-only invocation evidence."""
from __future__ import annotations
import json
import os
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any
import httpx
import pytest
from fastapi import HTTPException
from sqlalchemy import select
from scripts.write_backup_status import publish
from services.control_api.auth.models import AuthenticatedPrincipal, ManagementRole, PrincipalKind
from services.control_api.persistence import Base, create_engine_from_url, session_factory
from services.control_api.persistence.models import AuditEventRow
from services.runtime import control_api
from services.runtime.control_api import (
ToolInvokeRequest,
invoke_read_only_tool,
read_backup_status,
)
from services.runtime.state_sync import sync_runtime_state
ROOT = Path(__file__).resolve().parents[1]
def test_backup_status_is_healthy_only_after_fresh_restore_verified_projection(
tmp_path: Path,
) -> None:
artifact = tmp_path / "hub.sql"
artifact.write_bytes(b"bounded backup fixture")
status = tmp_path / "status.json"
document = publish(status, [artifact], restore_verified=True)
result = read_backup_status(
status, now=datetime.fromisoformat(str(document["completedAt"])) + timedelta(minutes=1)
)
assert result == {
"state": "healthy",
"lastSuccessfulAt": document["completedAt"],
"detail": "Latest backup and isolated restore verification succeeded.",
"restoreVerified": True,
"artifactCount": 1,
}
if os.name != "nt":
assert status.stat().st_mode & 0o777 == 0o600
assert json.loads(status.read_text(encoding="utf-8"))["artifacts"][0]["sha256"]
def test_backup_status_fails_closed_for_missing_stale_unverified_and_symlinked_input(
tmp_path: Path,
) -> None:
missing = read_backup_status(tmp_path / "missing.json")
assert missing["state"] == "unknown"
status = tmp_path / "status.json"
status.write_text(
json.dumps(
{
"schemaVersion": "1",
"completedAt": (datetime.now(UTC) - timedelta(days=3)).isoformat(),
"restoreVerified": True,
"artifactCount": 1,
}
),
encoding="utf-8",
)
assert read_backup_status(status)["state"] == "degraded"
value = json.loads(status.read_text(encoding="utf-8"))
value["completedAt"] = datetime.now(UTC).isoformat()
value["restoreVerified"] = False
status.write_text(json.dumps(value), encoding="utf-8")
assert read_backup_status(status)["state"] == "degraded"
link = tmp_path / "linked.json"
try:
link.symlink_to(status)
except OSError:
return
assert read_backup_status(link)["state"] == "unknown"
def test_backup_publisher_rejects_empty_and_symlinked_artifacts(tmp_path: Path) -> None:
empty = tmp_path / "empty.sql"
empty.touch()
try:
publish(tmp_path / "status.json", [empty], restore_verified=True)
except ValueError as exc:
assert "must not be empty" in str(exc)
else:
raise AssertionError("empty backup artifact was accepted")
target = tmp_path / "target.sql"
target.write_text("data", encoding="utf-8")
link = tmp_path / "link.sql"
try:
link.symlink_to(target)
except OSError:
return
try:
publish(tmp_path / "status.json", [link], restore_verified=True)
except ValueError as exc:
assert "regular file" in str(exc)
else:
raise AssertionError("symlinked backup artifact was accepted")
def test_management_console_invokes_only_the_cataloged_connector_and_audits(
tmp_path: Path, monkeypatch: Any
) -> None:
seen: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
seen.append(request)
data: dict[str, Any] = (
{"status": "not-in-the-registered-output-contract"}
if len(seen) == 2
else {
"tenant": "fleet-1",
"metrics": {
"available": 4,
"rented": 2,
"cleaning": 1,
"maintenance": 0,
"blocked": 0,
"openQualityIssues": 0,
"pendingOrFailedWorkflows": 0,
},
}
)
structured = {
"data": {**data, "unregisteredField": "must-not-cross-the-boundary"},
"meta": {
"project": "mobilityops",
"environment": "prod",
"source": "mobilityops",
"truncated": False,
"nextCursor": None,
"generatedAt": "2026-08-21T00:00:00+00:00",
},
"warnings": ["x" * 501] if len(seen) == 3 else [],
}
return httpx.Response(
200,
json={
"jsonrpc": "2.0",
"id": "ignored",
"result": {
"content": [{"type": "text", "text": "healthy"}],
"structuredContent": structured,
"isError": False,
},
},
)
monkeypatch.setattr(
control_api,
"evaluate_tool_readiness",
lambda *_args, **_kwargs: {"ready": True},
)
monkeypatch.setenv("MCP_CLIENT_BINDINGS_JSON", "[]")
engine = create_engine_from_url(f"sqlite:///{tmp_path / 'operations.db'}")
Base.metadata.create_all(engine)
sessions = session_factory(engine)
issuer = "https://auth.example.test/application/o/hub/"
with sessions() as session, session.begin():
sync_runtime_state(session, ROOT / "catalog", issuer)
principal = AuthenticatedPrincipal(
subject="operator@example.test",
issuer=issuer,
audience=("itworx-mcp-hub",),
kind=PrincipalKind.MANAGEMENT_USER,
expires_at=datetime.now(UTC) + timedelta(hours=1),
issued_at=datetime.now(UTC),
roles=frozenset({ManagementRole.OPERATOR}),
)
request = ToolInvokeRequest(
project="mobilityops",
environment="prod",
tool="mobilityops.operations.summary",
arguments={},
)
result = invoke_read_only_tool(
ROOT / "catalog",
object(), # type: ignore[arg-type]
object(), # type: ignore[arg-type]
sessions,
request,
principal,
b"a" * 32,
transport=httpx.MockTransport(handler),
)
assert result["ok"] is True
assert result["structuredContent"]["tenant"] == "fleet-1"
assert result["warnings"] == []
assert "content" not in result
assert "unregisteredField" not in result["structuredContent"]
assert len(seen) == 1
sent = seen[0]
assert str(sent.url) == "http://connector-mobilityops:8080/mcp"
assert sent.headers["x-itworx-project"] == "mobilityops"
assert sent.headers["x-itworx-environment"] == "prod"
assert sent.headers["x-itworx-identity-assertion"].startswith("v1:")
assert "operator@example.test" not in sent.content.decode()
with pytest.raises(HTTPException) as rejected:
invoke_read_only_tool(
ROOT / "catalog",
object(), # type: ignore[arg-type]
object(), # type: ignore[arg-type]
sessions,
request,
principal,
b"a" * 32,
transport=httpx.MockTransport(handler),
)
assert rejected.value.status_code == 502
assert len(seen) == 2
with pytest.raises(HTTPException) as malformed:
invoke_read_only_tool(
ROOT / "catalog",
object(), # type: ignore[arg-type]
object(), # type: ignore[arg-type]
sessions,
request,
principal,
b"a" * 32,
transport=httpx.MockTransport(handler),
)
assert malformed.value.status_code == 502
assert len(seen) == 3
with sessions() as session:
audit = session.scalars(select(AuditEventRow).order_by(AuditEventRow.occurred_at)).all()
assert [event.outcome for event in audit] == ["success", "error", "error"]
assert {event.tool_name for event in audit} == {"mobilityops.operations.summary"}
engine.dispose()