Files
webmanager-mvp/webui/backend/tests/golden/test_api_clients_golden.py
T
2026-03-26 19:41:58 +01:00

140 lines
5.2 KiB
Python

from __future__ import annotations
import asyncio
import sys
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path
import httpx
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
from backend.app.dependencies import get_remote_client_service
from backend.app.db.remote_client_repository import RemoteClientRepository
from backend.app.main import app
from backend.app.services.remote_client_service import RemoteClientService
class _Clock:
def __init__(self, current: datetime):
self.current = current
def now(self) -> datetime:
return self.current
def advance(self, *, seconds: int) -> None:
self.current += timedelta(seconds=seconds)
class RemoteClientsApiGoldenTest(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory()
self.clock = _Clock(datetime(2026, 3, 26, 12, 0, 0, tzinfo=timezone.utc))
repository = RemoteClientRepository(str(Path(self.temp_dir.name) / "remote-clients.db"))
service = RemoteClientService(
repository=repository,
registration_token="secret-token",
offline_timeout_seconds=60,
now=self.clock.now,
)
async def _override_remote_client_service() -> RemoteClientService:
return service
app.dependency_overrides[get_remote_client_service] = _override_remote_client_service
def tearDown(self) -> None:
app.dependency_overrides.clear()
self.temp_dir.cleanup()
def _request(self, method: str, url: str, payload: dict | None = None, token: str | None = None) -> httpx.Response:
async def _run() -> httpx.Response:
transport = httpx.ASGITransport(app=app)
headers = {}
if token is not None:
headers["Authorization"] = f"Bearer {token}"
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
if method == "GET":
return await client.get(url, headers=headers)
return await client.post(url, json=payload, headers=headers)
return asyncio.run(_run())
@staticmethod
def _register_payload() -> dict:
return {
"client_id": "client-123",
"display_name": "Jan MacBook",
"platform": "macos",
"agent_version": "1.1.0",
"endpoint": "http://192.168.1.25:8765",
"shares": [{"key": "downloads", "label": "Downloads"}],
}
def test_list_is_empty_by_default(self) -> None:
response = self._request("GET", "/api/clients")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"items": []})
def test_register_then_list_then_heartbeat_and_status_timeout(self) -> None:
register_response = self._request(
"POST",
"/api/clients/register",
self._register_payload(),
token="secret-token",
)
self.assertEqual(register_response.status_code, 200)
register_body = register_response.json()
self.assertEqual(register_body["client_id"], "client-123")
self.assertEqual(register_body["display_name"], "Jan MacBook")
self.assertEqual(register_body["status"], "online")
self.assertEqual(register_body["last_seen"], "2026-03-26T12:00:00Z")
self.assertIsNone(register_body["last_error"])
self.assertIsNone(register_body["reachable_at"])
list_response = self._request("GET", "/api/clients")
self.assertEqual(list_response.status_code, 200)
self.assertEqual(len(list_response.json()["items"]), 1)
self.assertEqual(list_response.json()["items"][0]["status"], "online")
self.clock.advance(seconds=30)
heartbeat_response = self._request(
"POST",
"/api/clients/heartbeat",
{"client_id": "client-123", "agent_version": "1.1.1"},
token="secret-token",
)
self.assertEqual(heartbeat_response.status_code, 200)
heartbeat_body = heartbeat_response.json()
self.assertEqual(heartbeat_body["agent_version"], "1.1.1")
self.assertEqual(heartbeat_body["last_seen"], "2026-03-26T12:00:30Z")
self.assertEqual(heartbeat_body["status"], "online")
self.clock.advance(seconds=61)
timed_out_list = self._request("GET", "/api/clients")
self.assertEqual(timed_out_list.status_code, 200)
timed_out_item = timed_out_list.json()["items"][0]
self.assertEqual(timed_out_item["status"], "offline")
self.assertEqual(timed_out_item["last_seen"], "2026-03-26T12:00:30Z")
self.assertIsNone(timed_out_item["last_error"])
self.assertIsNone(timed_out_item["reachable_at"])
def test_register_rejects_invalid_token(self) -> None:
response = self._request(
"POST",
"/api/clients/register",
self._register_payload(),
token="wrong-token",
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.json()["error"]["code"], "forbidden")
if __name__ == "__main__":
unittest.main()