Files
core-api/tests/test_infrastructure.py
T
jpmschweitzerandClaude Opus 4.5 a22e168666 Improve test coverage to 65%
Add comprehensive test suites for:
- NPM client (27 tests)
- Ollama client (16 tests)
- AI client and controller (34 tests)
- Static controller (8 tests)
- Tools controller DNS lookup (9 tests)
- OIDC authentication (10 tests)
- Housekeeping endpoints (28 tests)
- Infrastructure endpoints (15 tests)
- Health endpoints (12 tests)
- Portainer client (12 tests)
- Home Assistant client (24 tests)

Total: 285 tests passing with 65% code coverage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 16:32:39 +01:00

405 lines
16 KiB
Python

"""Tests for infrastructure endpoints."""
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch, AsyncMock, MagicMock
from src.main import app
@pytest.fixture
def client():
"""Create a test client."""
return TestClient(app)
@pytest.fixture
def mock_portainer():
"""Create a mock Portainer client."""
mock = AsyncMock()
return mock
@pytest.fixture
def mock_npm():
"""Create a mock NPM client."""
mock = AsyncMock()
return mock
class TestInfrastructureHealth:
"""Test /infrastructure/health endpoint."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_health_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""Health endpoint should return 200."""
mock_portainer = AsyncMock()
mock_portainer.health_check.return_value = True
mock_portainer.get_stacks.return_value = []
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.health_check.return_value = True
mock_npm.get_proxy_hosts.return_value = []
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/health")
assert response.status_code == 200
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_health_returns_connection_status(self, mock_get_npm, mock_get_portainer, client):
"""Health endpoint should return connection status for both services."""
mock_portainer = AsyncMock()
mock_portainer.health_check.return_value = True
mock_portainer.get_stacks.return_value = [{"Id": 1}, {"Id": 2}]
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.health_check.return_value = True
mock_npm.get_proxy_hosts.return_value = [{"id": 1}]
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/health")
data = response.json()
assert "portainer_connected" in data
assert "npm_connected" in data
assert "total_stacks" in data
assert "total_proxy_hosts" in data
assert data["portainer_connected"] is True
assert data["npm_connected"] is True
assert data["total_stacks"] == 2
assert data["total_proxy_hosts"] == 1
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_health_handles_disconnected_services(self, mock_get_npm, mock_get_portainer, client):
"""Health should handle when services are disconnected."""
mock_portainer = AsyncMock()
mock_portainer.health_check.return_value = False
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.health_check.return_value = False
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/health")
data = response.json()
assert data["portainer_connected"] is False
assert data["npm_connected"] is False
assert data["total_stacks"] == 0
assert data["total_proxy_hosts"] == 0
class TestInfrastructureServices:
"""Test /infrastructure/services endpoints."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_list_services_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""List services should return 200."""
mock_portainer = AsyncMock()
mock_portainer.get_stacks.return_value = []
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.get_proxy_hosts.return_value = []
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/services")
assert response.status_code == 200
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_list_services_returns_stack_info(self, mock_get_npm, mock_get_portainer, client):
"""List services should return stack information."""
mock_portainer = AsyncMock()
mock_portainer.get_stacks.return_value = [
{"Id": 1, "Name": "stack1", "Status": 1, "EndpointId": 1},
{"Id": 2, "Name": "stack2", "Status": 2, "EndpointId": 1}
]
mock_portainer.get_containers.return_value = []
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.get_proxy_hosts.return_value = []
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/services")
data = response.json()
assert isinstance(data, list)
assert len(data) == 2
assert data[0]["name"] == "stack1"
assert data[1]["name"] == "stack2"
class TestInfrastructurePorts:
"""Test /infrastructure/ports endpoint."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_list_ports_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""List ports should return 200."""
mock_portainer = AsyncMock()
mock_portainer.get_endpoints.return_value = [{"Id": 1}]
mock_portainer.get_containers.return_value = []
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.get_proxy_hosts.return_value = []
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/ports")
assert response.status_code == 200
class TestInfrastructureDomains:
"""Test /infrastructure/domains endpoint."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_list_domains_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""List domains should return 200."""
mock_portainer = AsyncMock()
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.get_proxy_hosts.return_value = []
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/domains")
assert response.status_code == 200
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_list_domains_returns_domain_info(self, mock_get_npm, mock_get_portainer, client):
"""List domains should return domain information."""
mock_portainer = AsyncMock()
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.get_proxy_hosts.return_value = [
{
"id": 1,
"domain_names": ["example.com", "www.example.com"],
"forward_host": "app",
"forward_port": 8080,
"ssl_certificate_id": 1
}
]
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/domains")
data = response.json()
assert isinstance(data, list)
# Each domain name should be a separate entry
assert len(data) >= 1
class TestInfrastructureContainers:
"""Test /infrastructure/containers endpoint."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_list_containers_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""List containers should return 200."""
mock_portainer = AsyncMock()
mock_portainer.list_containers.return_value = []
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/containers")
assert response.status_code == 200
class TestInfrastructureWidgetData:
"""Test /infrastructure/widget-data endpoint."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_widget_data_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""Widget data should return 200."""
mock_portainer = AsyncMock()
mock_portainer.health_check.return_value = True
mock_portainer.get_stacks.return_value = []
mock_portainer.list_containers.return_value = []
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.health_check.return_value = True
mock_npm.get_proxy_hosts.return_value = []
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/widget-data")
assert response.status_code == 200
class TestInfrastructureServiceGroups:
"""Test /infrastructure/service-groups endpoint."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_service_groups_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""Service groups should return 200."""
mock_portainer = AsyncMock()
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/service-groups")
assert response.status_code == 200
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_service_groups_returns_group_data(self, mock_get_npm, mock_get_portainer, client):
"""Service groups should return group data."""
mock_portainer = AsyncMock()
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/service-groups")
data = response.json()
# Should return a dict or list of service groups
assert isinstance(data, (dict, list))
class TestInfrastructureResources:
"""Test /infrastructure/resources endpoints."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_system_resources_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""System resources should return 200."""
mock_portainer = AsyncMock()
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/resources/system")
assert response.status_code == 200
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_container_resources_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""Container resources should return 200."""
mock_portainer = AsyncMock()
mock_portainer.list_containers.return_value = []
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/resources/containers")
assert response.status_code == 200
class TestInfrastructureServiceStatus:
"""Test /infrastructure/services/{service}/status endpoint."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_service_status_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""Service status should return 200."""
mock_portainer = AsyncMock()
mock_portainer.get_stacks.return_value = [
{"Id": 1, "Name": "testservice", "Status": 1, "EndpointId": 1}
]
mock_portainer.get_containers.return_value = [
{"Names": ["/testservice_app_1"], "State": "running"}
]
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.get_proxy_hosts.return_value = []
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/services/testservice/status")
assert response.status_code == 200
class TestInfrastructureContainerActions:
"""Test container action endpoints."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_get_container_logs_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""Get container logs should return 200."""
mock_portainer = AsyncMock()
mock_portainer.list_containers.return_value = [
{"Names": ["/testcontainer"], "Id": "abc123"}
]
mock_portainer.get_endpoints.return_value = [{"Id": 1}]
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/containers/testcontainer/logs")
# Response depends on container existence
assert response.status_code in [200, 404]
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_get_single_container_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""Get single container should return 200."""
mock_portainer = AsyncMock()
mock_portainer.inspect_container.return_value = {
"Id": "abc123",
"Name": "/testcontainer",
"State": {"Status": "running"}
}
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/containers/testcontainer")
assert response.status_code == 200
class TestInfrastructureGetService:
"""Test /infrastructure/services/{name} endpoint."""
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_get_service_returns_200(self, mock_get_npm, mock_get_portainer, client):
"""Get service should return 200."""
mock_portainer = AsyncMock()
mock_portainer.get_stacks.return_value = [
{"Id": 1, "Name": "testservice", "Status": 1, "EndpointId": 1}
]
mock_portainer.get_containers.return_value = []
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_npm.get_proxy_hosts.return_value = []
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/services/testservice")
assert response.status_code == 200
@patch("src.controllers.infrastructure_controller.get_portainer_client")
@patch("src.controllers.infrastructure_controller.get_npm_client")
def test_get_service_returns_404_for_missing(self, mock_get_npm, mock_get_portainer, client):
"""Get service should return 404 for non-existent service."""
mock_portainer = AsyncMock()
mock_portainer.get_stacks.return_value = []
mock_get_portainer.return_value = mock_portainer
mock_npm = AsyncMock()
mock_get_npm.return_value = mock_npm
response = client.get("/infrastructure/services/nonexistent")
assert response.status_code == 404