diff --git a/backend/tests/test_scheduler.py b/backend/tests/test_scheduler.py index 12eb8a5..5c14e73 100644 --- a/backend/tests/test_scheduler.py +++ b/backend/tests/test_scheduler.py @@ -10,6 +10,8 @@ from app.core.scheduler import ( _run_service_checks, _run_status_checks, reschedule_proxmox_sync, + reschedule_service_checks, + reschedule_status_checks, set_proxmox_sync_enabled, set_service_checks_enabled, start_scheduler, @@ -299,3 +301,102 @@ async def test_run_proxmox_sync_skips_when_no_token(): mock_settings.proxmox_token_id = "" mock_settings.proxmox_token_secret = "" await _run_proxmox_sync() # no exception, no fetch + + +def _proxmox_settings(mock_settings): + mock_settings.proxmox_sync_enabled = True + mock_settings.proxmox_host = "pve" + mock_settings.proxmox_port = 8006 + mock_settings.proxmox_token_id = "root@pam!tok" + mock_settings.proxmox_token_secret = "secret" + mock_settings.proxmox_verify_tls = False + + +@pytest.mark.asyncio +async def test_run_proxmox_sync_persists_inventory(): + db = MagicMock() + session_ctx = MagicMock() + session_ctx.__aenter__ = AsyncMock(return_value=db) + session_ctx.__aexit__ = AsyncMock(return_value=False) + result = MagicMock(device_count=3, pending_created=2, pending_updated=1) + with ( + patch("app.core.scheduler.settings") as mock_settings, + patch("app.core.scheduler.AsyncSessionLocal", MagicMock(return_value=session_ctx)), + patch( + "app.services.proxmox_service.fetch_proxmox_inventory", + new_callable=AsyncMock, + return_value=(["node-raw"], ["edge-raw"]), + ) as mock_fetch, + patch( + "app.api.routes.proxmox._persist_pending_import", + new_callable=AsyncMock, + return_value=result, + ) as mock_persist, + ): + _proxmox_settings(mock_settings) + await _run_proxmox_sync() + mock_fetch.assert_awaited_once() + mock_persist.assert_awaited_once_with(db, ["node-raw"], ["edge-raw"]) + + +@pytest.mark.asyncio +async def test_run_proxmox_sync_logs_and_swallows_errors(): + with ( + patch("app.core.scheduler.settings") as mock_settings, + patch( + "app.services.proxmox_service.fetch_proxmox_inventory", + new_callable=AsyncMock, + side_effect=RuntimeError("proxmox api down"), + ), + ): + _proxmox_settings(mock_settings) + await _run_proxmox_sync() # exception is logged, never propagated + + +# --- reschedule_* validation and not-running guards --- + +def test_reschedule_status_checks_rejects_short_interval(): + with pytest.raises(ValueError): + reschedule_status_checks(5) + + +def test_reschedule_status_checks_noop_when_not_running(): + mock_sched = MagicMock() + mock_sched.running = False + with patch("app.core.scheduler.scheduler", mock_sched): + reschedule_status_checks(60) + mock_sched.reschedule_job.assert_not_called() + + +def test_reschedule_status_checks_updates_running_job(): + mock_sched = MagicMock() + mock_sched.running = True + with patch("app.core.scheduler.scheduler", mock_sched): + reschedule_status_checks(60) + mock_sched.reschedule_job.assert_called_once_with( + "status_checks", trigger="interval", seconds=60, + ) + + +def test_reschedule_service_checks_rejects_short_interval(): + with pytest.raises(ValueError): + reschedule_service_checks(10) + + +def test_reschedule_service_checks_updates_when_job_exists(): + mock_sched = MagicMock() + mock_sched.running = True + mock_sched.get_job.return_value = MagicMock() + with patch("app.core.scheduler.scheduler", mock_sched): + reschedule_service_checks(60) + mock_sched.reschedule_job.assert_called_once_with( + "service_checks", trigger="interval", seconds=60, + ) + + +def test_reschedule_proxmox_sync_noop_when_not_running(): + mock_sched = MagicMock() + mock_sched.running = False + with patch("app.core.scheduler.scheduler", mock_sched): + reschedule_proxmox_sync(600) + mock_sched.reschedule_job.assert_not_called() diff --git a/backend/tests/test_status_checker.py b/backend/tests/test_status_checker.py index 6555585..2551a60 100644 --- a/backend/tests/test_status_checker.py +++ b/backend/tests/test_status_checker.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from app.services.status_checker import ( + _http_get, _ping, _tcp_connect, check_node, @@ -11,6 +12,18 @@ from app.services.status_checker import ( check_services, ) + +def _mock_httpx_client(status_code): + """Build a stand-in for httpx.AsyncClient whose GET returns status_code.""" + resp = MagicMock() + resp.status_code = status_code + client = MagicMock() + client.get = AsyncMock(return_value=resp) + ctx = MagicMock() + ctx.__aenter__ = AsyncMock(return_value=client) + ctx.__aexit__ = AsyncMock(return_value=False) + return MagicMock(return_value=ctx) + # --- check_node dispatcher --- @pytest.mark.asyncio @@ -462,3 +475,35 @@ async def test_check_services_returns_status_per_service(): @pytest.mark.asyncio async def test_check_services_empty_list(): assert await check_services("10.0.0.1", []) == [] + + +# --- _http_get status-code interpretation (real primitive, mocked transport) --- + +@pytest.mark.asyncio +async def test_http_get_true_on_2xx(): + with patch("app.services.status_checker.httpx.AsyncClient", _mock_httpx_client(200)): + assert await _http_get("http://host") is True + + +@pytest.mark.asyncio +async def test_http_get_true_on_4xx(): + # A 4xx means the server is up and answering — still "online". + with patch("app.services.status_checker.httpx.AsyncClient", _mock_httpx_client(404)): + assert await _http_get("http://host") is True + + +@pytest.mark.asyncio +async def test_http_get_false_on_5xx(): + with patch("app.services.status_checker.httpx.AsyncClient", _mock_httpx_client(503)): + assert await _http_get("http://host") is False + + +@pytest.mark.asyncio +async def test_check_service_http_exception_returns_offline(): + svc = {"port": 80, "protocol": "tcp", "service_name": "http"} + with patch( + "app.services.status_checker._http_get", + new_callable=AsyncMock, + side_effect=RuntimeError("connection refused"), + ): + assert await check_service(svc, "10.0.0.1") == "offline"