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"]