Remove Node.js cache files containing npm vulnerability data for vitest and vite packages
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

This commit is contained in:
2025-12-15 20:36:06 -05:00
parent 27eed55c9b
commit 68a9b0f390
123 changed files with 13216 additions and 157 deletions
+6
View File
@@ -33,6 +33,12 @@ from app.core.dependencies import get_db, verify_api_key, get_current_user
from app import create_app
@pytest.fixture
def api_key_header() -> dict:
"""Header containing the test API key."""
return {"X-API-Key": os.environ["API_KEY"]}
# ============================================================================
# DATABASE FIXTURES
# ============================================================================
+393
View File
@@ -0,0 +1,393 @@
"""Tests for Docker service functionality."""
import pytest
from datetime import datetime, timezone
from contextlib import asynccontextmanager
from unittest.mock import AsyncMock, MagicMock, patch
from app.models.docker_container import DockerContainer
from app.models.docker_image import DockerImage
from app.models.docker_volume import DockerVolume
from app.models.docker_alert import DockerAlert
from app.crud.docker_container import DockerContainerRepository
from app.crud.docker_image import DockerImageRepository
from app.crud.docker_volume import DockerVolumeRepository
from app.crud.docker_alert import DockerAlertRepository
class TestDockerContainerRepository:
"""Tests for DockerContainerRepository."""
@pytest.mark.asyncio
async def test_upsert_creates_new_container(self, db_session):
"""Test that upsert creates a new container when it doesn't exist."""
repo = DockerContainerRepository(db_session)
container = await repo.upsert(
host_id="test-host-1",
container_id="abc123def456",
name="nginx",
image="nginx:latest",
state="running",
status="Up 2 hours"
)
assert container.name == "nginx"
assert container.state == "running"
assert container.container_id == "abc123def456"
@pytest.mark.asyncio
async def test_upsert_updates_existing_container(self, db_session):
"""Test that upsert updates an existing container."""
repo = DockerContainerRepository(db_session)
# Create first
await repo.upsert(
host_id="test-host-1",
container_id="abc123def456",
name="nginx",
state="running"
)
await db_session.commit()
# Update
container = await repo.upsert(
host_id="test-host-1",
container_id="abc123def456",
name="nginx",
state="exited"
)
assert container.state == "exited"
@pytest.mark.asyncio
async def test_list_by_host(self, db_session):
"""Test listing containers by host."""
repo = DockerContainerRepository(db_session)
await repo.upsert(host_id="host-1", container_id="c1", name="container1", state="running")
await repo.upsert(host_id="host-1", container_id="c2", name="container2", state="exited")
await repo.upsert(host_id="host-2", container_id="c3", name="container3", state="running")
await db_session.commit()
host1_containers = await repo.list_by_host("host-1")
assert len(host1_containers) == 2
@pytest.mark.asyncio
async def test_count_by_host(self, db_session):
"""Test counting containers by host."""
repo = DockerContainerRepository(db_session)
await repo.upsert(host_id="host-1", container_id="c1", name="container1", state="running")
await repo.upsert(host_id="host-1", container_id="c2", name="container2", state="running")
await repo.upsert(host_id="host-1", container_id="c3", name="container3", state="exited")
await db_session.commit()
counts = await repo.count_by_host("host-1")
assert counts["total"] == 3
assert counts["running"] == 2
class TestDockerAlertRepository:
"""Tests for DockerAlertRepository."""
@pytest.mark.asyncio
async def test_create_alert(self, db_session):
"""Test creating a new alert."""
repo = DockerAlertRepository(db_session)
alert = await repo.create(
host_id="test-host-1",
container_name="nginx",
severity="error",
message="Container is down"
)
await db_session.commit()
assert alert.id is not None
assert alert.state == "open"
assert alert.severity == "error"
@pytest.mark.asyncio
async def test_get_open_alert(self, db_session):
"""Test getting an open alert for a container."""
repo = DockerAlertRepository(db_session)
await repo.create(
host_id="test-host-1",
container_name="nginx",
severity="error",
message="Container is down"
)
await db_session.commit()
alert = await repo.get_open_alert("test-host-1", "nginx")
assert alert is not None
assert alert.container_name == "nginx"
@pytest.mark.asyncio
async def test_acknowledge_alert(self, db_session):
"""Test acknowledging an alert."""
repo = DockerAlertRepository(db_session)
alert = await repo.create(
host_id="test-host-1",
container_name="nginx",
severity="error",
message="Container is down"
)
await db_session.commit()
acknowledged = await repo.acknowledge(alert.id, "admin")
assert acknowledged is not None
assert acknowledged.state == "acknowledged"
assert acknowledged.acknowledged_by == "admin"
@pytest.mark.asyncio
async def test_close_alert(self, db_session):
"""Test closing an alert."""
repo = DockerAlertRepository(db_session)
alert = await repo.create(
host_id="test-host-1",
container_name="nginx",
severity="error",
message="Container is down"
)
await db_session.commit()
closed = await repo.close(alert.id)
assert closed is not None
assert closed.state == "closed"
assert closed.closed_at is not None
@pytest.mark.asyncio
async def test_close_for_container(self, db_session):
"""Test closing all alerts for a container."""
repo = DockerAlertRepository(db_session)
await repo.create(host_id="host-1", container_name="nginx", severity="error", message="Down")
await repo.create(host_id="host-1", container_name="nginx", severity="warning", message="Health check")
await db_session.commit()
count = await repo.close_for_container("host-1", "nginx")
assert count == 2
class TestDockerAlertDetection:
"""Tests for Docker alert detection logic."""
@pytest.mark.asyncio
async def test_detect_container_down(self):
"""Test detection of container that should be running but is stopped."""
container = MagicMock()
container.state = "exited"
container.health = None
container.labels = {"homelab.monitor": "true", "homelab.desired": "running"}
container.name = "nginx"
container.host_id = "host-1"
# Container is exited but should be running -> should trigger alert
expected_state = container.labels.get("homelab.desired", "running")
should_alert = expected_state == "running" and container.state != "running"
assert should_alert is True
@pytest.mark.asyncio
async def test_detect_unhealthy_container(self):
"""Test detection of unhealthy container."""
container = MagicMock()
container.state = "running"
container.health = "unhealthy"
container.labels = {"homelab.monitor": "true"}
container.name = "nginx"
# Container is unhealthy -> should trigger warning
should_alert = container.health == "unhealthy"
assert should_alert is True
@pytest.mark.asyncio
async def test_no_alert_for_healthy_running_container(self):
"""Test that healthy running containers don't trigger alerts."""
container = MagicMock()
container.state = "running"
container.health = "healthy"
container.labels = {"homelab.monitor": "true", "homelab.desired": "running"}
expected_state = container.labels.get("homelab.desired", "running")
should_close_alert = container.state == "running" and container.health in ("healthy", None)
assert should_close_alert is True
class TestDockerServiceSSH:
"""Tests for Docker service SSH functionality."""
@pytest.mark.asyncio
async def test_parse_docker_json_lines(self):
"""Test parsing Docker JSON output."""
from app.services.docker_service import DockerService
service = DockerService()
output = '{"ID":"abc123","Names":"nginx","State":"running"}\n{"ID":"def456","Names":"redis","State":"exited"}'
result = await service._parse_docker_json_lines(output)
assert len(result) == 2
assert result[0]["ID"] == "abc123"
assert result[1]["State"] == "exited"
def test_parse_size(self):
"""Test parsing Docker size strings."""
from app.services.docker_service import DockerService
service = DockerService()
assert service._parse_size("100MB") == 100 * 1024 * 1024
assert service._parse_size("1.5GB") == int(1.5 * 1024 * 1024 * 1024)
assert service._parse_size("500KB") == 500 * 1024
assert service._parse_size("") == 0
assert service._parse_size("invalid") == 0
class TestDockerServiceHostFiltering:
"""Tests for Docker host listing and filtering."""
@pytest.mark.asyncio
async def test_get_docker_hosts_filters_role_docker(self):
"""Only hosts in inventory group role_docker must be visible in Docker section."""
from app.services.docker_service import docker_service
host1 = MagicMock()
host1.id = "host-1"
host1.name = "docker-1"
host1.ip_address = "10.0.0.1"
host1.deleted_at = None
host1.docker_enabled = True
host1.docker_version = None
host1.docker_status = None
host1.docker_last_collect_at = None
host2 = MagicMock()
host2.id = "host-2"
host2.name = "docker-2"
host2.ip_address = "10.0.0.2"
host2.deleted_at = None
host2.docker_enabled = False
host2.docker_version = None
host2.docker_status = None
host2.docker_last_collect_at = None
host3 = MagicMock()
host3.id = "host-3"
host3.name = "not-docker"
host3.ip_address = "10.0.0.3"
host3.deleted_at = None
host3.docker_enabled = True
host3.docker_version = None
host3.docker_status = None
host3.docker_last_collect_at = None
inv_h1 = MagicMock()
inv_h1.name = "docker-1"
inv_h1.groups = ["role_docker"]
inv_h2 = MagicMock()
inv_h2.name = "docker-2"
inv_h2.groups = ["role_docker"]
inv_h3 = MagicMock()
inv_h3.name = "not-docker"
inv_h3.groups = ["role_web"]
@asynccontextmanager
async def _dummy_session_ctx():
yield MagicMock()
host_repo_instance = MagicMock()
host_repo_instance.list = AsyncMock(return_value=[host1, host2, host3])
container_repo_instance = MagicMock()
container_repo_instance.count_by_host = AsyncMock(return_value={"total": 0, "running": 0})
image_repo_instance = MagicMock()
image_repo_instance.count_by_host = AsyncMock(return_value={"total": 0, "total_size": 0})
volume_repo_instance = MagicMock()
volume_repo_instance.count_by_host = AsyncMock(return_value=0)
alert_repo_instance = MagicMock()
alert_repo_instance.count_open_by_host = AsyncMock(return_value=0)
with patch("app.services.docker_service.async_session_maker", new=_dummy_session_ctx):
with patch("app.services.docker_service.ansible_service.get_hosts_from_inventory", return_value=[inv_h1, inv_h2, inv_h3]):
with patch("app.services.docker_service.HostRepository", return_value=host_repo_instance):
with patch("app.services.docker_service.DockerContainerRepository", return_value=container_repo_instance):
with patch("app.services.docker_service.DockerImageRepository", return_value=image_repo_instance):
with patch("app.services.docker_service.DockerVolumeRepository", return_value=volume_repo_instance):
with patch("app.services.docker_service.DockerAlertRepository", return_value=alert_repo_instance):
hosts = await docker_service.get_docker_hosts()
assert [h["host_name"] for h in hosts] == ["docker-1", "docker-2"]
by_id = {h["host_id"]: h for h in hosts}
assert by_id["host-1"]["docker_enabled"] is True
assert by_id["host-2"]["docker_enabled"] is False
class TestDockerActions:
"""Tests for Docker container actions."""
@pytest.mark.asyncio
async def test_start_container_success(self):
"""Test successful container start."""
from app.services.docker_actions import DockerActionsService
service = DockerActionsService()
with patch.object(service, '_get_host_info', new_callable=AsyncMock) as mock_host:
mock_host.return_value = {"id": "host-1", "name": "test-host", "ip": "192.168.1.10"}
with patch.object(service, '_ssh_connect', new_callable=AsyncMock) as mock_connect:
mock_conn = AsyncMock()
mock_connect.return_value = mock_conn
with patch.object(service, '_ssh_exec', new_callable=AsyncMock) as mock_exec:
mock_exec.return_value = {
"success": True,
"stdout": "abc123",
"stderr": "",
"exit_code": 0
}
result = await service.start_container("host-1", "abc123")
assert result["success"] is True
assert result["action"] == "start"
@pytest.mark.asyncio
async def test_stop_container_success(self):
"""Test successful container stop."""
from app.services.docker_actions import DockerActionsService
service = DockerActionsService()
with patch.object(service, '_get_host_info', new_callable=AsyncMock) as mock_host:
mock_host.return_value = {"id": "host-1", "name": "test-host", "ip": "192.168.1.10"}
with patch.object(service, '_ssh_connect', new_callable=AsyncMock) as mock_connect:
mock_conn = AsyncMock()
mock_connect.return_value = mock_conn
with patch.object(service, '_ssh_exec', new_callable=AsyncMock) as mock_exec:
mock_exec.return_value = {
"success": True,
"stdout": "abc123",
"stderr": "",
"exit_code": 0
}
result = await service.stop_container("host-1", "abc123")
assert result["success"] is True
assert result["action"] == "stop"
+315
View File
@@ -0,0 +1,315 @@
"""Tests for Docker API routes."""
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from httpx import AsyncClient
class TestDockerHostsRoutes:
"""Tests for Docker hosts endpoints."""
@pytest.mark.asyncio
async def test_list_docker_hosts(self, client: AsyncClient, api_key_header):
"""Test listing Docker hosts."""
with patch('app.services.docker_service.docker_service.get_docker_hosts', new_callable=AsyncMock) as mock:
mock.return_value = [
{
"host_id": "host-1",
"host_name": "server1",
"host_ip": "192.168.1.10",
"docker_enabled": True,
"docker_version": "24.0.7",
"docker_status": "online",
"docker_last_collect_at": None,
"containers_total": 5,
"containers_running": 3,
"images_total": 10,
"volumes_total": 2,
"open_alerts": 0
},
{
"host_id": "host-2",
"host_name": "server2",
"host_ip": "192.168.1.11",
"docker_enabled": False,
"docker_version": None,
"docker_status": None,
"docker_last_collect_at": None,
"containers_total": 0,
"containers_running": 0,
"images_total": 0,
"volumes_total": 0,
"open_alerts": 0
}
]
response = await client.get("/api/docker/hosts", headers=api_key_header)
assert response.status_code == 200
data = response.json()
assert "hosts" in data
assert len(data["hosts"]) == 2
assert data["hosts"][0]["host_name"] == "server1"
hosts_by_id = {h["host_id"]: h for h in data["hosts"]}
assert hosts_by_id["host-1"]["docker_enabled"] is True
assert hosts_by_id["host-2"]["docker_enabled"] is False
@pytest.mark.asyncio
async def test_enable_docker_monitoring(self, client: AsyncClient, api_key_header):
"""Test enabling Docker monitoring on a host."""
with patch('app.services.docker_service.docker_service.enable_docker_monitoring', new_callable=AsyncMock) as mock:
mock.return_value = True
response = await client.post(
"/api/docker/hosts/host-1/enable",
headers=api_key_header,
json={"enabled": True}
)
assert response.status_code == 200
data = response.json()
assert "message" in data
@pytest.mark.asyncio
async def test_collect_docker_now(self, client: AsyncClient, api_key_header):
"""Test forcing Docker collection on a host."""
with patch('app.services.docker_service.docker_service.collect_docker_host', new_callable=AsyncMock) as mock:
mock.return_value = {
"success": True,
"host_id": "host-1",
"host_name": "server1",
"docker_version": "24.0.7",
"containers_count": 5,
"images_count": 10,
"volumes_count": 2,
"duration_ms": 1500,
"error": None
}
response = await client.post(
"/api/docker/hosts/host-1/collect",
headers=api_key_header
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["containers_count"] == 5
class TestDockerContainersRoutes:
"""Tests for Docker containers endpoints."""
@pytest.mark.asyncio
async def test_list_containers(self, client: AsyncClient, api_key_header, db_session):
"""Test listing containers for a host."""
# This test would require setting up container data in the database
# For now, test that the endpoint returns correctly structured data
response = await client.get(
"/api/docker/hosts/host-1/containers",
headers=api_key_header
)
assert response.status_code == 200
data = response.json()
assert "containers" in data
assert "total" in data
assert "running" in data
@pytest.mark.asyncio
async def test_start_container(self, client: AsyncClient, api_key_header):
"""Test starting a container."""
with patch('app.services.docker_actions.docker_actions.start_container', new_callable=AsyncMock) as mock:
mock.return_value = {
"success": True,
"message": "Container started successfully",
"container_id": "abc123",
"action": "start",
"output": "abc123",
"error": None
}
response = await client.post(
"/api/docker/containers/host-1/abc123/start",
headers=api_key_header
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["action"] == "start"
@pytest.mark.asyncio
async def test_stop_container(self, client: AsyncClient, api_key_header):
"""Test stopping a container."""
with patch('app.services.docker_actions.docker_actions.stop_container', new_callable=AsyncMock) as mock:
mock.return_value = {
"success": True,
"message": "Container stopped successfully",
"container_id": "abc123",
"action": "stop",
"output": "abc123",
"error": None
}
response = await client.post(
"/api/docker/containers/host-1/abc123/stop",
headers=api_key_header
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["action"] == "stop"
@pytest.mark.asyncio
async def test_restart_container(self, client: AsyncClient, api_key_header):
"""Test restarting a container."""
with patch('app.services.docker_actions.docker_actions.restart_container', new_callable=AsyncMock) as mock:
mock.return_value = {
"success": True,
"message": "Container restarted successfully",
"container_id": "abc123",
"action": "restart",
"output": "abc123",
"error": None
}
response = await client.post(
"/api/docker/containers/host-1/abc123/restart",
headers=api_key_header
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["action"] == "restart"
@pytest.mark.asyncio
async def test_get_container_logs(self, client: AsyncClient, api_key_header):
"""Test getting container logs."""
with patch('app.services.docker_actions.docker_actions.get_container_logs', new_callable=AsyncMock) as mock:
mock.return_value = {
"container_id": "abc123",
"container_name": "nginx",
"logs": "2024-01-15 10:00:00 Starting nginx...\n2024-01-15 10:00:01 Ready",
"lines": 2
}
response = await client.get(
"/api/docker/containers/host-1/abc123/logs?tail=100",
headers=api_key_header
)
assert response.status_code == 200
data = response.json()
assert "logs" in data
assert data["lines"] == 2
class TestDockerAlertsRoutes:
"""Tests for Docker alerts endpoints."""
@pytest.mark.asyncio
async def test_list_alerts(self, client: AsyncClient, api_key_header):
"""Test listing Docker alerts."""
with patch('app.services.docker_alerts.docker_alerts_service.get_alerts', new_callable=AsyncMock) as mock:
mock.return_value = {
"alerts": [
{
"id": 1,
"host_id": "host-1",
"host_name": "server1",
"container_name": "nginx",
"severity": "error",
"state": "open",
"message": "Container is down",
"opened_at": "2024-01-15T10:00:00Z"
}
],
"total": 1,
"open_count": 1,
"acknowledged_count": 0
}
response = await client.get(
"/api/docker/alerts",
headers=api_key_header
)
assert response.status_code == 200
data = response.json()
assert "alerts" in data
assert data["total"] == 1
assert data["open_count"] == 1
@pytest.mark.asyncio
async def test_acknowledge_alert(self, client: AsyncClient, api_key_header):
"""Test acknowledging an alert."""
with patch('app.services.docker_alerts.docker_alerts_service.acknowledge_alert', new_callable=AsyncMock) as mock:
mock.return_value = {
"id": 1,
"state": "acknowledged",
"acknowledged_by": "admin",
"acknowledged_at": "2024-01-15T10:30:00Z"
}
response = await client.post(
"/api/docker/alerts/1/acknowledge",
headers=api_key_header,
json={"note": "Investigating"}
)
assert response.status_code == 200
data = response.json()
assert "alert" in data
@pytest.mark.asyncio
async def test_close_alert(self, client: AsyncClient, api_key_header):
"""Test closing an alert."""
with patch('app.services.docker_alerts.docker_alerts_service.close_alert', new_callable=AsyncMock) as mock:
mock.return_value = {
"id": 1,
"state": "closed",
"closed_at": "2024-01-15T11:00:00Z"
}
response = await client.post(
"/api/docker/alerts/1/close",
headers=api_key_header
)
assert response.status_code == 200
data = response.json()
assert "alert" in data
class TestDockerStatsRoute:
"""Tests for Docker stats endpoint."""
@pytest.mark.asyncio
async def test_get_docker_stats(self, client: AsyncClient, api_key_header):
"""Test getting global Docker statistics."""
with patch('app.services.docker_alerts.docker_alerts_service.get_stats', new_callable=AsyncMock) as mock:
mock.return_value = {
"total_hosts": 5,
"enabled_hosts": 3,
"online_hosts": 2,
"total_containers": 25,
"running_containers": 20,
"total_images": 50,
"total_volumes": 10,
"open_alerts": 2,
"last_collection": "2024-01-15T10:00:00Z"
}
response = await client.get(
"/api/docker/stats",
headers=api_key_header
)
assert response.status_code == 200
data = response.json()
assert data["total_hosts"] == 5
assert data["running_containers"] == 20
assert data["open_alerts"] == 2
+9
View File
@@ -3,6 +3,7 @@ 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
@@ -54,6 +55,11 @@ class TestGetAllHostsMetrics:
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:
@@ -157,6 +163,9 @@ class TestGetHostMetrics:
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: