Files
Unraid-AppOps-Gateway-Public/backend/tests/test_unraid_vm_actions.py
T

155 lines
5.4 KiB
Python

from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from sqlmodel import SQLModel, create_engine
@pytest.fixture
def api_client(tmp_path, monkeypatch):
db_path = tmp_path / "unraid_vm_actions.db"
monkeypatch.setenv("APPOPS_DB_PATH", str(db_path))
from backend.app.db import session
from backend.app.db.init_db import init_db
from backend.app.main import app
engine = create_engine(f"sqlite:///{Path(db_path)}", connect_args={"check_same_thread": False})
session.engine = engine
SQLModel.metadata.drop_all(engine)
SQLModel.metadata.create_all(engine)
init_db()
with TestClient(app) as client:
yield client
def test_vm_capabilities_expose_safe_execution_boundary(api_client):
response = api_client.get("/api/unraid/vms/capabilities")
assert response.status_code == 200
payload = response.json()
actions = {item["id"]: item for item in payload["actions"]}
assert actions["start"]["policyDecision"] == "allowed"
assert actions["start"]["executionSupported"] is True
assert actions["pause"]["policyDecision"] == "approval_required"
assert actions["pause"]["executionSupported"] is True
assert actions["stop"]["policyDecision"] == "blocked"
assert actions["stop"]["executionSupported"] is False
assert actions["force_stop"]["policyDecision"] == "blocked"
def test_manual_vm_start_runs_job_postcheck_and_audit(api_client, monkeypatch):
from backend.app.services import unraid_vm_service
inventory_calls = {"count": 0}
def fake_inventory(_self):
inventory_calls["count"] += 1
state = "SHUTOFF" if inventory_calls["count"] == 1 else "RUNNING"
return {"status": "ok", "vms": [{"id": "server:vm-1", "name": "Test VM", "state": state}]}
monkeypatch.setattr(unraid_vm_service.UnraidConnector, "get_vm_summary", fake_inventory)
monkeypatch.setattr(
unraid_vm_service.UnraidConnector,
"mutate_vm",
lambda _self, vm_id, action: {
"success": vm_id == "server:vm-1" and action == "start",
"status": "ok",
"message": "vm_start_accepted",
},
)
response = api_client.post(
"/api/unraid/vms/server%3Avm-1/actions/start",
json={"source": "manual", "confirm": True},
)
assert response.status_code == 200
payload = response.json()
assert payload["success"] is True
assert payload["decision"] == "allowed"
assert payload["before_state"]["state"] == "SHUTOFF"
assert payload["after_state"] == {"state": "RUNNING", "verified": True}
assert payload["jobId"].startswith("job-")
assert payload["auditId"].startswith("audit-")
jobs = api_client.get("/api/jobs").json()
assert any(job["id"] == payload["jobId"] and job["status"] == "succeeded" for job in jobs)
audits = api_client.get("/api/audit").json()
audit = next(item for item in audits if item["id"] == payload["auditId"])
assert audit["action_id"] == "unraid.vm.start"
assert audit["target_type"] == "virtual_machine"
def test_vm_start_requires_explicit_confirmation(api_client):
response = api_client.post(
"/api/unraid/vms/vm-1/actions/start",
json={"source": "manual", "confirm": False},
)
assert response.status_code == 200
assert response.json()["status"] == "confirm_required"
def test_vm_pause_creates_executable_approval(api_client, monkeypatch):
from backend.app.services import unraid_vm_service
inventory_calls = {"count": 0}
def fake_inventory(_self):
inventory_calls["count"] += 1
state = "RUNNING" if inventory_calls["count"] == 1 else "PAUSED"
return {"status": "ok", "vms": [{"id": "vm-2", "name": "Workload", "state": state}]}
monkeypatch.setattr(unraid_vm_service.UnraidConnector, "get_vm_summary", fake_inventory)
monkeypatch.setattr(
unraid_vm_service.UnraidConnector,
"mutate_vm",
lambda _self, vm_id, action: {
"success": vm_id == "vm-2" and action == "pause",
"status": "ok",
"message": "vm_pause_accepted",
},
)
requested = api_client.post(
"/api/unraid/vms/vm-2/actions/pause",
json={"source": "manual", "confirm": True},
).json()
assert requested["decision"] == "approval_required"
assert requested["approvalId"].startswith("appr-")
approval_id = requested["approvalId"]
approved = api_client.patch(f"/api/approvals/{approval_id}", json={"status": "approved"})
assert approved.status_code == 200
executed = api_client.post(f"/api/approvals/{approval_id}/execute")
assert executed.status_code == 200
payload = executed.json()
assert payload["success"] is True
assert payload["approvalStatus"] == "executed"
assert payload["after_state"] == {"state": "PAUSED", "verified": True}
def test_vm_stop_is_blocked_without_calling_connector(api_client, monkeypatch):
from backend.app.services import unraid_vm_service
monkeypatch.setattr(
unraid_vm_service.UnraidConnector,
"mutate_vm",
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("blocked action reached connector")),
)
response = api_client.post(
"/api/unraid/vms/vm-3/actions/stop",
json={"source": "manual", "confirm": True},
)
assert response.status_code == 200
payload = response.json()
assert payload["decision"] == "blocked"
assert payload["result"] == "blocked"
assert "force-stop" in payload["message"]