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