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>
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
"""Tests for AI controller."""
|
||||
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_ai_client():
|
||||
"""Create a mock AI client."""
|
||||
mock = AsyncMock()
|
||||
return mock
|
||||
|
||||
|
||||
class TestAIHealth:
|
||||
"""Test /ai/health endpoint."""
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_health_returns_200(self, mock_get_client, client):
|
||||
"""AI health should return 200."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.health_check.return_value = True
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/health")
|
||||
assert response.status_code == 200
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_health_returns_healthy_status(self, mock_get_client, client):
|
||||
"""AI health should return healthy status when service is up."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.health_check.return_value = True
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/health")
|
||||
data = response.json()
|
||||
|
||||
assert data["service"] == "core-ai"
|
||||
assert data["status"] == "healthy"
|
||||
assert data["accessible"] is True
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_health_returns_unhealthy_status(self, mock_get_client, client):
|
||||
"""AI health should return unhealthy status when service is down."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.health_check.return_value = False
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/health")
|
||||
data = response.json()
|
||||
|
||||
assert data["status"] == "unhealthy"
|
||||
assert data["accessible"] is False
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_health_handles_exception(self, mock_get_client, client):
|
||||
"""AI health should handle exceptions gracefully."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.health_check.side_effect = Exception("Connection refused")
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/health")
|
||||
data = response.json()
|
||||
|
||||
assert data["status"] == "error"
|
||||
assert data["accessible"] is False
|
||||
assert "error" in data
|
||||
|
||||
|
||||
class TestAIMetrics:
|
||||
"""Test /ai/metrics endpoint."""
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_metrics_returns_200(self, mock_get_client, client):
|
||||
"""AI metrics should return 200."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_metrics.return_value = {
|
||||
"uptime_seconds": 3600,
|
||||
"agent": {"total_requests": 100}
|
||||
}
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics")
|
||||
assert response.status_code == 200
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_metrics_returns_data(self, mock_get_client, client):
|
||||
"""AI metrics should return metrics data."""
|
||||
metrics_data = {
|
||||
"uptime_seconds": 3600,
|
||||
"agent": {"total_requests": 100},
|
||||
"tools": {"total_calls": 250}
|
||||
}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_metrics.return_value = metrics_data
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics")
|
||||
data = response.json()
|
||||
|
||||
assert data["uptime_seconds"] == 3600
|
||||
assert data["agent"]["total_requests"] == 100
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_metrics_returns_503_on_error(self, mock_get_client, client):
|
||||
"""AI metrics should return 503 when service unavailable."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_metrics.side_effect = Exception("Service unavailable")
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics")
|
||||
assert response.status_code == 503
|
||||
|
||||
|
||||
class TestAIErrors:
|
||||
"""Test /ai/metrics/errors endpoint."""
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_errors_returns_200(self, mock_get_client, client):
|
||||
"""AI errors should return 200."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_recent_errors.return_value = []
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics/errors")
|
||||
assert response.status_code == 200
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_errors_returns_error_list(self, mock_get_client, client):
|
||||
"""AI errors should return list of errors."""
|
||||
errors = [
|
||||
{"timestamp": "2025-12-03T19:45:12Z", "error": "Timeout"},
|
||||
{"timestamp": "2025-12-03T19:46:00Z", "error": "Connection refused"}
|
||||
]
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_recent_errors.return_value = errors
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics/errors")
|
||||
data = response.json()
|
||||
|
||||
assert "errors" in data
|
||||
assert "total" in data
|
||||
assert data["total"] == 2
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_errors_accepts_limit_parameter(self, mock_get_client, client):
|
||||
"""AI errors should accept limit parameter."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_recent_errors.return_value = []
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics/errors?limit=5")
|
||||
assert response.status_code == 200
|
||||
mock_client.get_recent_errors.assert_called_with(limit=5)
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_errors_returns_503_on_error(self, mock_get_client, client):
|
||||
"""AI errors should return 503 when service unavailable."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_recent_errors.side_effect = Exception("Service unavailable")
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics/errors")
|
||||
assert response.status_code == 503
|
||||
|
||||
|
||||
class TestAIToolFailures:
|
||||
"""Test /ai/metrics/tool-failures endpoint."""
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_tool_failures_returns_200(self, mock_get_client, client):
|
||||
"""Tool failures should return 200."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_tool_failures.return_value = []
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics/tool-failures")
|
||||
assert response.status_code == 200
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_tool_failures_returns_failure_list(self, mock_get_client, client):
|
||||
"""Tool failures should return list of failures."""
|
||||
failures = [
|
||||
{"tool_name": "list_containers", "error": "Connection refused"}
|
||||
]
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_tool_failures.return_value = failures
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics/tool-failures")
|
||||
data = response.json()
|
||||
|
||||
assert "failures" in data
|
||||
assert "total" in data
|
||||
assert data["total"] == 1
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_tool_failures_accepts_limit_parameter(self, mock_get_client, client):
|
||||
"""Tool failures should accept limit parameter."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_tool_failures.return_value = []
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics/tool-failures?limit=10")
|
||||
assert response.status_code == 200
|
||||
mock_client.get_tool_failures.assert_called_with(limit=10)
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_tool_failures_returns_503_on_error(self, mock_get_client, client):
|
||||
"""Tool failures should return 503 when service unavailable."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_tool_failures.side_effect = Exception("Service unavailable")
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.get("/ai/metrics/tool-failures")
|
||||
assert response.status_code == 503
|
||||
|
||||
|
||||
class TestAIMetricsReset:
|
||||
"""Test /ai/metrics/reset endpoint."""
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_reset_returns_200(self, mock_get_client, client):
|
||||
"""Reset metrics should return 200."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.reset_metrics.return_value = True
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.post("/ai/metrics/reset")
|
||||
assert response.status_code == 200
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_reset_returns_success_message(self, mock_get_client, client):
|
||||
"""Reset metrics should return success message."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.reset_metrics.return_value = True
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.post("/ai/metrics/reset")
|
||||
data = response.json()
|
||||
|
||||
assert data["success"] is True
|
||||
assert "message" in data
|
||||
|
||||
@patch("src.controllers.ai_controller.get_ai_client")
|
||||
def test_reset_returns_503_on_error(self, mock_get_client, client):
|
||||
"""Reset metrics should return 503 when service unavailable."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.reset_metrics.side_effect = Exception("Service unavailable")
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
response = client.post("/ai/metrics/reset")
|
||||
assert response.status_code == 503
|
||||
Reference in New Issue
Block a user