homelab_automation/tests/backend/test_routes_metrics.py
Bruno Charest 6c51fb5c75
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Add metrics collection scheduler with automatic job management, improve terminal session cleanup on 404/403 errors, and refactor metrics collection endpoint to use shared background job function with WebSocket broadcast support
2025-12-21 20:51:18 -05:00

324 lines
12 KiB
Python

"""
Tests pour les routes metrics.
"""
import pytest
import re
from unittest.mock import patch, AsyncMock, MagicMock
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
pytestmark = pytest.mark.unit
class TestGetMetrics:
"""Tests pour GET /api/metrics."""
@pytest.mark.asyncio
async def test_get_metrics(self, client: AsyncClient):
"""Récupération des métriques globales."""
response = await client.get("/api/metrics")
assert response.status_code == 200
data = response.json()
assert "online_hosts" in data or "total_tasks" in data
class TestGetAllHostsMetrics:
"""Tests pour GET /api/metrics/all-hosts."""
@pytest.mark.asyncio
async def test_get_all_hosts_metrics_empty(self, client: AsyncClient, db_session: AsyncSession):
"""Métriques vides."""
response = await client.get("/api/metrics/all-hosts")
assert response.status_code == 200
data = response.json()
assert isinstance(data, dict)
@pytest.mark.asyncio
async def test_get_all_hosts_metrics_with_data(self, client: AsyncClient, db_session: AsyncSession, host_factory):
"""Métriques avec données."""
from app.crud.host_metrics import HostMetricsRepository
host = await host_factory.create(db_session, name="metrics-test-host")
repo = HostMetricsRepository(db_session)
await repo.create(
host_id=host.id,
metric_type="system_info",
cpu_usage_percent=50.0,
memory_usage_percent=60.0
)
await db_session.commit()
response = await client.get("/api/metrics/all-hosts")
assert response.status_code == 200
data = response.json()
assert host.id in data
assert data[host.id]["last_collected"] is not None
assert data[host.id]["last_collected"] == data[host.id]["collected_at"]
assert re.search(r"[+-]\d{2}:\d{2}$", data[host.id]["last_collected"]) is not None
class TestGetCollectionSchedule:
"""Tests pour GET /api/metrics/collection-schedule."""
@pytest.mark.asyncio
async def test_get_schedule_default(self, client: AsyncClient):
"""Intervalle par défaut."""
response = await client.get("/api/metrics/collection-schedule")
assert response.status_code == 200
data = response.json()
assert "interval" in data
@pytest.mark.asyncio
async def test_get_schedule_with_setting(self, client: AsyncClient, db_session: AsyncSession):
"""Intervalle configuré."""
from app.crud.app_setting import AppSettingRepository
repo = AppSettingRepository(db_session)
await repo.set("metrics_collection_interval", "15min")
await db_session.commit()
response = await client.get("/api/metrics/collection-schedule")
assert response.status_code == 200
data = response.json()
assert data["interval"] == "15min"
class TestSetCollectionSchedule:
"""Tests pour POST /api/metrics/collection-schedule."""
@pytest.mark.asyncio
async def test_set_schedule_valid(self, client: AsyncClient):
"""Définition d'un intervalle valide."""
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "30min"}
)
assert response.status_code == 200
data = response.json()
assert data["interval"] == "30min"
@pytest.mark.asyncio
async def test_set_schedule_triggers_reschedule(self, client: AsyncClient):
"""Le changement d'intervalle doit rescheduler le job de collecte."""
with patch(
"app.routes.metrics.apply_interval_using_global_scheduler",
autospec=True,
) as mock_apply:
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "5min"}
)
assert response.status_code == 200
mock_apply.assert_called_once_with("5min")
@pytest.mark.asyncio
async def test_set_schedule_off(self, client: AsyncClient):
"""Désactivation de la collecte."""
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "off"}
)
assert response.status_code == 200
data = response.json()
assert data["interval"] == "off"
@pytest.mark.asyncio
async def test_set_schedule_invalid(self, client: AsyncClient):
"""Intervalle invalide."""
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "invalid"}
)
assert response.status_code == 400
class TestGetHostMetrics:
"""Tests pour GET /api/metrics/{host_id}."""
@pytest.mark.asyncio
async def test_get_host_metrics_no_data(self, client: AsyncClient):
"""Hôte sans métriques."""
response = await client.get("/api/metrics/nonexistent-host")
assert response.status_code == 200
data = response.json()
assert data["collection_status"] == "no_data"
@pytest.mark.asyncio
async def test_get_host_metrics_with_data(self, client: AsyncClient, db_session: AsyncSession, host_factory):
"""Hôte avec métriques."""
from app.crud.host_metrics import HostMetricsRepository
host = await host_factory.create(db_session, name="host-with-metrics")
repo = HostMetricsRepository(db_session)
await repo.create(
host_id=host.id,
metric_type="system_info",
cpu_usage_percent=45.5,
memory_usage_percent=70.0,
disk_root_usage_percent=55.0
)
await db_session.commit()
response = await client.get(f"/api/metrics/{host.id}")
assert response.status_code == 200
data = response.json()
assert data["host_id"] == host.id
assert data["cpu_usage_percent"] == 45.5
assert data["last_collected"] is not None
assert data["last_collected"] == data["collected_at"]
assert re.search(r"[+-]\d{2}:\d{2}$", data["last_collected"]) is not None
class TestMetricsWithErrors:
"""Tests pour les cas d'erreur des métriques."""
@pytest.mark.asyncio
async def test_get_all_hosts_metrics_exception(self, client: AsyncClient):
"""Gestion des exceptions pour all-hosts."""
with patch("app.routes.metrics.HostMetricsRepository") as mock_repo:
mock_repo.return_value.get_all_latest = AsyncMock(side_effect=Exception("DB error"))
response = await client.get("/api/metrics/all-hosts")
# Should return empty dict on error
assert response.status_code == 200
assert response.json() == {}
@pytest.mark.asyncio
async def test_get_collection_schedule_exception(self, client: AsyncClient):
"""Gestion des exceptions pour collection-schedule."""
with patch("app.routes.metrics.AppSettingRepository") as mock_repo:
mock_repo.return_value.get = AsyncMock(side_effect=Exception("DB error"))
response = await client.get("/api/metrics/collection-schedule")
# Should return default "off" on error
assert response.status_code == 200
assert response.json()["interval"] == "off"
@pytest.mark.asyncio
async def test_set_collection_schedule_exception(self, client: AsyncClient):
"""Gestion des exceptions pour set collection-schedule."""
with patch("app.routes.metrics.AppSettingRepository") as mock_repo:
mock_repo.return_value.set = AsyncMock(side_effect=Exception("DB error"))
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "5min"}
)
assert response.status_code == 500
@pytest.mark.asyncio
async def test_get_host_metrics_exception(self, client: AsyncClient):
"""Gestion des exceptions pour host metrics."""
with patch("app.routes.metrics.HostMetricsRepository") as mock_repo:
mock_repo.return_value.get_latest_for_host = AsyncMock(side_effect=Exception("DB error"))
response = await client.get("/api/metrics/test-host")
assert response.status_code == 200
data = response.json()
assert data["collection_status"] == "error"
assert "error_message" in data
@pytest.mark.asyncio
async def test_get_host_metrics_with_error_message(self, client: AsyncClient, db_session: AsyncSession, host_factory):
"""Métriques avec message d'erreur."""
from app.crud.host_metrics import HostMetricsRepository
host = await host_factory.create(db_session, name="host-with-error")
repo = HostMetricsRepository(db_session)
await repo.create(
host_id=host.id,
metric_type="system_info",
error_message="Connection failed"
)
await db_session.commit()
response = await client.get(f"/api/metrics/{host.id}")
assert response.status_code == 200
data = response.json()
assert data["collection_status"] == "failed"
assert data["error_message"] == "Connection failed"
class TestSetCollectionScheduleAllIntervals:
"""Tests pour tous les intervalles valides."""
@pytest.mark.asyncio
async def test_set_schedule_5min(self, client: AsyncClient):
"""Intervalle 5min."""
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "5min"}
)
assert response.status_code == 200
assert response.json()["interval"] == "5min"
@pytest.mark.asyncio
async def test_set_schedule_15min(self, client: AsyncClient):
"""Intervalle 15min."""
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "15min"}
)
assert response.status_code == 200
assert response.json()["interval"] == "15min"
@pytest.mark.asyncio
async def test_set_schedule_1h(self, client: AsyncClient):
"""Intervalle 1h."""
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "1h"}
)
assert response.status_code == 200
assert response.json()["interval"] == "1h"
@pytest.mark.asyncio
async def test_set_schedule_6h(self, client: AsyncClient):
"""Intervalle 6h."""
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "6h"}
)
assert response.status_code == 200
assert response.json()["interval"] == "6h"
@pytest.mark.asyncio
async def test_set_schedule_12h(self, client: AsyncClient):
"""Intervalle 12h."""
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "12h"}
)
assert response.status_code == 200
assert response.json()["interval"] == "12h"
@pytest.mark.asyncio
async def test_set_schedule_24h(self, client: AsyncClient):
"""Intervalle 24h."""
response = await client.post(
"/api/metrics/collection-schedule",
json={"interval": "24h"}
)
assert response.status_code == 200
assert response.json()["interval"] == "24h"