test_mcp_2025_direct.py•15.1 kB
"""Direct function tests for MCP 2025 features to improve diff coverage.
This module tests features by calling functions directly rather than through
the MCP client, allowing us to test code paths that require direct context
passing (like progress notifications with ctx parameter).
"""
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
from datetime import datetime
from prometheus_mcp_server.server import (
execute_query,
execute_range_query,
list_metrics,
get_metric_metadata,
get_targets,
health_check,
config
)
@pytest.fixture
def mock_make_request():
"""Mock the make_prometheus_request function."""
with patch("prometheus_mcp_server.server.make_prometheus_request") as mock:
yield mock
class TestDirectFunctionCalls:
"""Test functions called directly to cover context-dependent code paths."""
@pytest.mark.asyncio
async def test_execute_query_direct_call(self, mock_make_request):
"""Test execute_query by calling it directly."""
mock_make_request.return_value = {
"resultType": "vector",
"result": [{"metric": {"__name__": "up"}, "value": [1617898448.214, "1"]}]
}
# Access the underlying function from FunctionTool
result = await execute_query.fn(query="up", time="2023-01-01T00:00:00Z")
assert "resultType" in result
assert "result" in result
assert "links" in result
assert result["links"][0]["rel"] == "prometheus-ui"
assert "up" in result["links"][0]["href"]
@pytest.mark.asyncio
async def test_execute_range_query_with_context(self, mock_make_request):
"""Test execute_range_query with context for progress reporting."""
mock_make_request.return_value = {
"resultType": "matrix",
"result": [{"metric": {"__name__": "up"}, "values": [[1617898400, "1"]]}]
}
# Create mock context
mock_ctx = AsyncMock()
mock_ctx.report_progress = AsyncMock()
result = await execute_range_query.fn(
query="up",
start="2023-01-01T00:00:00Z",
end="2023-01-01T01:00:00Z",
step="15s",
ctx=mock_ctx
)
# Verify progress was reported
assert mock_ctx.report_progress.call_count >= 3
calls = mock_ctx.report_progress.call_args_list
# Check initial progress
assert calls[0].kwargs["progress"] == 0
assert calls[0].kwargs["total"] == 100
assert "Initiating" in calls[0].kwargs["message"]
# Check completion progress
assert calls[-1].kwargs["progress"] == 100
assert calls[-1].kwargs["total"] == 100
assert "completed" in calls[-1].kwargs["message"]
# Verify result includes links
assert "links" in result
assert result["links"][0]["rel"] == "prometheus-ui"
@pytest.mark.asyncio
async def test_execute_range_query_without_context(self, mock_make_request):
"""Test execute_range_query without context (backward compatibility)."""
mock_make_request.return_value = {
"resultType": "matrix",
"result": []
}
# Call without context - should not error
result = await execute_range_query.fn(
query="up",
start="2023-01-01T00:00:00Z",
end="2023-01-01T01:00:00Z",
step="15s",
ctx=None
)
assert "resultType" in result
assert "links" in result
@pytest.mark.asyncio
async def test_list_metrics_with_context(self, mock_make_request):
"""Test list_metrics with context for progress reporting."""
mock_make_request.return_value = ["metric1", "metric2", "metric3"]
# Create mock context
mock_ctx = AsyncMock()
mock_ctx.report_progress = AsyncMock()
result = await list_metrics.fn(ctx=mock_ctx)
# Verify progress was reported
assert mock_ctx.report_progress.call_count >= 2
calls = mock_ctx.report_progress.call_args_list
# Check initial progress
assert calls[0].kwargs["progress"] == 0
assert calls[0].kwargs["total"] == 100
assert "Fetching" in calls[0].kwargs["message"]
# Check completion progress with count
assert calls[-1].kwargs["progress"] == 100
assert calls[-1].kwargs["total"] == 100
assert "3" in calls[-1].kwargs["message"]
# Verify result
assert len(result) == 3
assert "metric1" in result
@pytest.mark.asyncio
async def test_list_metrics_without_context(self, mock_make_request):
"""Test list_metrics without context (backward compatibility)."""
mock_make_request.return_value = ["metric1", "metric2"]
result = await list_metrics.fn(ctx=None)
assert len(result) == 2
assert "metric1" in result
@pytest.mark.asyncio
async def test_get_metric_metadata_direct_call(self, mock_make_request):
"""Test get_metric_metadata by calling it directly."""
# Test when data is in "metadata" key
mock_make_request.return_value = {
"metadata": [
{"metric": "up", "type": "gauge", "help": "Up status", "unit": ""}
]
}
result = await get_metric_metadata.fn(metric="up")
assert len(result) == 1
assert result[0]["metric"] == "up"
assert result[0]["type"] == "gauge"
@pytest.mark.asyncio
async def test_get_metric_metadata_data_key(self, mock_make_request):
"""Test get_metric_metadata when data is in 'data' key instead of 'metadata'."""
# Test when data is in "data" key (fallback path)
mock_make_request.return_value = {
"data": [
{"metric": "http_requests", "type": "counter", "help": "HTTP requests", "unit": ""}
]
}
result = await get_metric_metadata.fn(metric="http_requests")
assert len(result) == 1
assert result[0]["metric"] == "http_requests"
assert result[0]["type"] == "counter"
@pytest.mark.asyncio
async def test_get_targets_direct_call(self, mock_make_request):
"""Test get_targets by calling it directly."""
mock_make_request.return_value = {
"activeTargets": [
{
"discoveredLabels": {"__address__": "localhost:9090"},
"labels": {"job": "prometheus"},
"health": "up"
}
],
"droppedTargets": [
{
"discoveredLabels": {"__address__": "localhost:9091"}
}
]
}
result = await get_targets.fn()
assert "activeTargets" in result
assert "droppedTargets" in result
assert len(result["activeTargets"]) == 1
assert result["activeTargets"][0]["health"] == "up"
assert len(result["droppedTargets"]) == 1
class TestHealthCheckFunction:
"""Test health_check function directly to improve coverage."""
@pytest.mark.asyncio
async def test_health_check_healthy_with_prometheus(self, mock_make_request):
"""Test health_check when Prometheus is accessible."""
mock_make_request.return_value = {
"resultType": "vector",
"result": []
}
with patch("prometheus_mcp_server.server.config") as mock_config:
mock_config.url = "http://prometheus:9090"
mock_config.username = "admin"
mock_config.password = "secret"
mock_config.org_id = None
mock_config.mcp_server_config = MagicMock()
mock_config.mcp_server_config.mcp_server_transport = "stdio"
result = await health_check.fn()
assert result["status"] == "healthy"
assert result["service"] == "prometheus-mcp-server"
assert result["version"] == "1.4.1"
assert "timestamp" in result
assert result["prometheus_connectivity"] == "healthy"
assert result["prometheus_url"] == "http://prometheus:9090"
assert result["configuration"]["prometheus_url_configured"] is True
assert result["configuration"]["authentication_configured"] is True
@pytest.mark.asyncio
async def test_health_check_degraded_prometheus_error(self, mock_make_request):
"""Test health_check when Prometheus is not accessible."""
mock_make_request.side_effect = Exception("Connection refused")
with patch("prometheus_mcp_server.server.config") as mock_config:
mock_config.url = "http://prometheus:9090"
mock_config.username = None
mock_config.password = None
mock_config.token = None
mock_config.org_id = None
mock_config.mcp_server_config = MagicMock()
mock_config.mcp_server_config.mcp_server_transport = "http"
result = await health_check.fn()
assert result["status"] == "degraded"
assert result["prometheus_connectivity"] == "unhealthy"
assert "prometheus_error" in result
assert "Connection refused" in result["prometheus_error"]
@pytest.mark.asyncio
async def test_health_check_unhealthy_no_url(self):
"""Test health_check when PROMETHEUS_URL is not configured."""
with patch("prometheus_mcp_server.server.config") as mock_config:
mock_config.url = ""
mock_config.username = None
mock_config.password = None
mock_config.token = None
mock_config.org_id = None
mock_config.mcp_server_config = MagicMock()
mock_config.mcp_server_config.mcp_server_transport = "stdio"
result = await health_check.fn()
assert result["status"] == "unhealthy"
assert "error" in result
assert "PROMETHEUS_URL not configured" in result["error"]
assert result["configuration"]["prometheus_url_configured"] is False
@pytest.mark.asyncio
async def test_health_check_with_token_auth(self, mock_make_request):
"""Test health_check with token authentication."""
mock_make_request.return_value = {
"resultType": "vector",
"result": []
}
with patch("prometheus_mcp_server.server.config") as mock_config:
mock_config.url = "http://prometheus:9090"
mock_config.username = None
mock_config.password = None
mock_config.token = "bearer-token-123"
mock_config.org_id = "org-1"
mock_config.mcp_server_config = MagicMock()
mock_config.mcp_server_config.mcp_server_transport = "sse"
result = await health_check.fn()
assert result["status"] == "healthy"
assert result["configuration"]["authentication_configured"] is True
assert result["configuration"]["org_id_configured"] is True
assert result["transport"] == "sse"
@pytest.mark.asyncio
async def test_health_check_exception_handling(self):
"""Test health_check handles unexpected exceptions."""
with patch("prometheus_mcp_server.server.config") as mock_config:
# Make accessing config.url raise an exception
type(mock_config).url = property(lambda self: (_ for _ in ()).throw(RuntimeError("Unexpected error")))
result = await health_check.fn()
assert result["status"] == "unhealthy"
assert "error" in result
assert "Unexpected error" in result["error"]
@pytest.mark.asyncio
async def test_health_check_with_org_id(self, mock_make_request):
"""Test health_check includes org_id configuration."""
mock_make_request.return_value = {
"resultType": "vector",
"result": []
}
with patch("prometheus_mcp_server.server.config") as mock_config:
mock_config.url = "http://prometheus:9090"
mock_config.username = None
mock_config.password = None
mock_config.token = None
mock_config.org_id = "tenant-123"
mock_config.mcp_server_config = MagicMock()
mock_config.mcp_server_config.mcp_server_transport = "stdio"
result = await health_check.fn()
assert result["configuration"]["org_id_configured"] is True
@pytest.mark.asyncio
async def test_health_check_no_mcp_server_config(self, mock_make_request):
"""Test health_check when mcp_server_config is None."""
mock_make_request.return_value = {
"resultType": "vector",
"result": []
}
with patch("prometheus_mcp_server.server.config") as mock_config:
mock_config.url = "http://prometheus:9090"
mock_config.username = None
mock_config.password = None
mock_config.token = None
mock_config.org_id = None
mock_config.mcp_server_config = None
result = await health_check.fn()
assert result["status"] == "healthy"
assert result["transport"] == "stdio"
class TestProgressNotificationsPaths:
"""Test progress notification code paths for complete coverage."""
@pytest.mark.asyncio
async def test_range_query_progress_all_stages(self, mock_make_request):
"""Test all three progress stages in execute_range_query."""
mock_make_request.return_value = {
"resultType": "matrix",
"result": []
}
mock_ctx = AsyncMock()
mock_ctx.report_progress = AsyncMock()
await execute_range_query.fn(
query="up",
start="2023-01-01T00:00:00Z",
end="2023-01-01T01:00:00Z",
step="15s",
ctx=mock_ctx
)
# Verify all three stages
calls = [call.kwargs for call in mock_ctx.report_progress.call_args_list]
# Stage 1: Initiation (0%)
assert any(c["progress"] == 0 and "Initiating" in c["message"] for c in calls)
# Stage 2: Processing (50%)
assert any(c["progress"] == 50 and "Processing" in c["message"] for c in calls)
# Stage 3: Completion (100%)
assert any(c["progress"] == 100 and "completed" in c["message"] for c in calls)
@pytest.mark.asyncio
async def test_list_metrics_progress_both_stages(self, mock_make_request):
"""Test both progress stages in list_metrics."""
mock_make_request.return_value = ["m1", "m2", "m3", "m4", "m5"]
mock_ctx = AsyncMock()
mock_ctx.report_progress = AsyncMock()
await list_metrics.fn(ctx=mock_ctx)
calls = [call.kwargs for call in mock_ctx.report_progress.call_args_list]
# Stage 1: Fetching (0%)
assert any(c["progress"] == 0 and "Fetching" in c["message"] for c in calls)
# Stage 2: Completion (100%) with count
assert any(c["progress"] == 100 and "5" in c["message"] for c in calls)