from __future__ import annotations import argparse import importlib.util import sys import tempfile import unittest from pathlib import Path from types import SimpleNamespace from unittest.mock import patch from uvm.cli import build_parser, main from uvm.config import Settings from uvm.errors import ConfigurationError, UvmError from uvm.server import create_api, run_server def fake_application(*, api_token: str | None = None): settings = SimpleNamespace( app_name="uvm", default_vcpu=1, default_ram_mib=512, default_ssh_user="root", api_token=api_token, api_tls_cert=None, api_tls_key=None, ) return SimpleNamespace(settings=settings) class ServerCliTests(unittest.TestCase): def test_parser_accepts_the_requested_serve_invocation(self) -> None: parser = build_parser(fake_application()) args = parser.parse_args(["--serve", "--port", "8123", "--host", "127.0.0.1"]) self.assertTrue(args.serve) self.assertEqual(args.port, 8123) self.assertEqual(args.host, "127.0.0.1") self.assertIsNone(args.command) def test_parser_rejects_an_invalid_server_port(self) -> None: parser = build_parser(fake_application()) with self.assertRaises(SystemExit): parser.parse_args(["--serve", "--port", "70000"]) def test_create_parser_defaults_guest_credentials_to_root(self) -> None: args = build_parser(fake_application()).parse_args(["create"]) self.assertEqual(args.username, "root") self.assertEqual(args.password, "root") def test_main_delegates_serve_mode_to_the_server_launcher(self) -> None: application = fake_application() with ( patch("uvm.cli.build_application", return_value=application), patch("uvm.server.run_server") as run_server_mock, ): result = main(["--serve", "--host", "127.0.0.1", "--port", "8123"]) self.assertEqual(result, 0) run_server_mock.assert_called_once_with(application, host="127.0.0.1", port=8123) def test_server_requires_an_api_token_even_on_loopback(self) -> None: with self.assertRaises(UvmError): run_server(fake_application(api_token=None), host="127.0.0.1", port=8000) def test_non_loopback_server_requires_tls(self) -> None: with self.assertRaises(UvmError): run_server(fake_application(api_token="test-token"), host="0.0.0.0", port=8000) def test_factory_requires_an_explicit_host(self) -> None: with self.assertRaises(UvmError): create_api(fake_application(api_token="test-token")) def test_invalid_tls_material_is_rejected_before_uvicorn_starts(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: certificate = Path(temporary_directory) / "cert.pem" key = Path(temporary_directory) / "key.pem" certificate.touch() key.touch() application = fake_application(api_token="test-token") application.settings.api_tls_cert = certificate application.settings.api_tls_key = key with self.assertRaises(UvmError): run_server(application, host="127.0.0.1", port=8000) def test_non_loopback_server_uses_configured_tls(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: certificate = Path(temporary_directory) / "cert.pem" key = Path(temporary_directory) / "key.pem" certificate.touch() key.touch() application = fake_application(api_token="test-token") application.settings.api_tls_cert = certificate application.settings.api_tls_key = key fake_uvicorn = SimpleNamespace(run=lambda *_args, **_kwargs: None) with ( patch("uvm.server._validate_server_settings"), patch("uvm.server.create_api", return_value=object()), patch.dict(sys.modules, {"uvicorn": fake_uvicorn}), patch.object(fake_uvicorn, "run") as run_mock, ): run_server(application, host="0.0.0.0", port=8443) run_mock.assert_called_once() self.assertEqual(run_mock.call_args.kwargs["ssl_certfile"], str(certificate)) self.assertEqual(run_mock.call_args.kwargs["ssl_keyfile"], str(key)) def test_api_token_rejects_non_ascii_or_whitespace(self) -> None: for token in ("s\u00e9cret", "contains space", ""): with self.subTest(token=token): with self.assertRaises(ConfigurationError): Settings(api_token=token) FASTAPI_AVAILABLE = ( importlib.util.find_spec("fastapi") is not None and importlib.util.find_spec("httpx") is not None ) if FASTAPI_AVAILABLE: from fastapi.testclient import TestClient from uvm.domain import VmRecord from uvm.lifecycle import ListedVm from uvm.server import create_api class FakeLifecycle: def __init__(self) -> None: self.last_spec = None self.vm = VmRecord( id="vm-test", cpu=1, ram_mib=512, guest_ip="10.42.0.2", gateway="10.42.0.1", tap="uvm-test", mac="02:fc:00:00:00:01", socket="/tmp/firecracker.sock", config="/tmp/config.json", log="/tmp/firecracker.log", status="running", ) def list_vms(self) -> list[ListedVm]: return [ListedVm(vm=self.vm, observed_status=self.vm.status)] def create(self, spec): self.last_spec = spec self.vm.username = spec.username self.vm.password = spec.password self.vm.status = "running" return self.vm def stop(self, _vm_id: str): self.vm.status = "stopped" return self.vm def destroy(self, _vm_id: str): return self.vm @unittest.skipUnless(FASTAPI_AVAILABLE, "FastAPI is not installed") class ApiRouterTests(unittest.TestCase): def setUp(self) -> None: application = fake_application(api_token="test-token") self.lifecycle = FakeLifecycle() application.lifecycle = self.lifecycle application.installer = SimpleNamespace( install=lambda **_kwargs: ( Path("/tmp/firecracker"), SimpleNamespace(kernel=Path("/tmp/vmlinux"), rootfs=Path("/tmp/rootfs")), ) ) application.state_store = SimpleNamespace(initialize=lambda: None) self.client = TestClient(create_api(application, host="127.0.0.1")) def test_health_is_available_without_credentials(self) -> None: response = self.client.get("/health") self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"status": "ok"}) def test_vm_routes_require_and_accept_the_api_token(self) -> None: unauthorized = self.client.get("/vms") authorized = self.client.get("/vms", headers={"X-UVM-Token": "test-token"}) self.assertEqual(unauthorized.status_code, 401) self.assertEqual(authorized.status_code, 200) self.assertEqual(authorized.json()[0]["id"], "vm-test") self.assertNotIn("password", authorized.json()[0]) def test_vm_lifecycle_routes_and_extra_field_validation(self) -> None: headers = {"X-UVM-Token": "test-token"} created = self.client.post( "/vms", headers=headers, json={ "cpu": 1, "ram": "512", "username": "root", "password": "api-secret", }, ) detail = self.client.get("/vms/vm-test", headers=headers) stopped = self.client.post("/vms/vm-test/stop", headers=headers) destroyed = self.client.delete("/vms/vm-test", headers=headers) invalid = self.client.post("/vms", headers=headers, json={"forse": True}) self.assertEqual(created.status_code, 201) self.assertEqual(created.json()["username"], "root") self.assertNotIn("password", created.json()) self.assertEqual(self.lifecycle.last_spec.password, "api-secret") self.assertEqual(detail.status_code, 200) self.assertEqual(stopped.json()["status"], "stopped") self.assertEqual(destroyed.json()["status"], "terminated") self.assertEqual(invalid.status_code, 422) def test_vm_create_defaults_guest_credentials_to_root(self) -> None: response = self.client.post( "/vms", headers={"X-UVM-Token": "test-token"}, json={"cpu": 1, "ram": "512"}, ) self.assertEqual(response.status_code, 201) self.assertEqual(self.lifecycle.last_spec.username, "root") self.assertEqual(self.lifecycle.last_spec.password, "root") def test_install_route_uses_the_existing_installer(self) -> None: response = self.client.post( "/install", headers={"X-UVM-Token": "test-token"}, json={"force": True}, ) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["firecracker"], "/tmp/firecracker")