from __future__ import annotations import time import pytest from fastapi.testclient import TestClient from cloud_api.app import create_app from cloud.control_config import CloudConfigurationError, CloudControlConfig def test_create_app_returns_independent_cloud_application() -> None: app = create_app() assert app.title == "Device Cloud API" assert callable(create_app) paths = set(app.openapi()["paths"]) assert "/v1/tasks" in paths assert "/internal/v1/hosts/{host_id}/heartbeat" in paths def test_cloud_application_owns_database_lifecycle() -> None: events: list[str] = [] class FakeRepository: def health_check(self) -> None: events.append("healthy") def list_queued_tasks(self) -> list[object]: events.append("scheduled") return [] def reap_expired_leases(self, *, now, max_attempts: int) -> list[str]: events.append("reaped") return [] class FakeDatabase: repository = FakeRepository() def close(self) -> None: events.append("closed") fake_database = FakeDatabase() app = create_app( config=CloudControlConfig(database_url="sqlite:///:memory:"), database_factory=lambda _config: fake_database, # type: ignore[arg-type,return-value] ) with TestClient(app): assert app.state.database is fake_database assert ( app.state.cloud_services.pool.store is app.state.cloud_services.repository ) assert "healthy" in events assert events[-1] == "closed" def test_repository_proxy_is_available_only_during_lifespan() -> None: class FakeRepository: def health_check(self) -> None: return None def list_queued_tasks(self) -> list[object]: return [] def reap_expired_leases(self, *, now, max_attempts: int) -> list[str]: return [] class FakeDatabase: repository = FakeRepository() def close(self) -> None: return None app = create_app( config=CloudControlConfig(database_url="sqlite:///:memory:"), database_factory=lambda _config: FakeDatabase(), # type: ignore[arg-type,return-value] ) with TestClient(app): app.state.cloud_services.repository.health_check() with pytest.raises(RuntimeError, match="outside app lifespan"): app.state.cloud_services.repository.health_check() def test_lifespan_runs_scheduler_and_reaper_until_shutdown() -> None: events: list[str] = [] class FakeRepository: def health_check(self) -> None: events.append("healthy") def list_queued_tasks(self) -> list[object]: events.append("scheduled") return [] def reap_expired_leases(self, *, now, max_attempts: int) -> list[str]: events.append(f"reaped:{max_attempts}") return [] class FakeDatabase: repository = FakeRepository() def close(self) -> None: events.append("closed") app = create_app( config=CloudControlConfig( database_url="sqlite:///:memory:", scheduler_interval_seconds=0.01, lease_reaper_interval_seconds=0.01, max_task_attempts=4, ), database_factory=lambda _config: FakeDatabase(), # type: ignore[arg-type,return-value] ) with TestClient(app): time.sleep(0.04) assert events.count("scheduled") >= 2 assert events.count("reaped:4") >= 2 assert all(not task.done() for task in app.state.worker_tasks) assert all(task.done() for task in app.state.worker_tasks) assert events[-1] == "closed" def test_production_app_rejects_missing_credentials() -> None: with pytest.raises(CloudConfigurationError, match="credential"): create_app( config=CloudControlConfig( environment="production", database_url="postgresql://db/cloud", ) )