Files
nest/tests/test_server.py
T
2026-09-04 20:56:18 +00:00

242 lines
9.4 KiB
Python

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