368 lines
15 KiB
Python
368 lines
15 KiB
Python
"""Tests for MCP server functionality using official MCP testing patterns."""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, AsyncMock
|
|
from mcp.client.session import ClientSession
|
|
from mcp.client.stdio import stdio_client
|
|
from vultr_dns_mcp.server import VultrDNSServer, create_mcp_server
|
|
|
|
|
|
class TestMCPServerBasics:
|
|
"""Test basic MCP server functionality."""
|
|
|
|
def test_server_creation(self, mock_api_key):
|
|
"""Test that MCP server can be created successfully."""
|
|
server = create_mcp_server(mock_api_key)
|
|
assert server is not None
|
|
assert hasattr(server, '_tools')
|
|
assert hasattr(server, '_resources')
|
|
|
|
def test_server_creation_without_api_key(self):
|
|
"""Test that server creation fails without API key."""
|
|
with pytest.raises(ValueError, match="VULTR_API_KEY must be provided"):
|
|
create_mcp_server(None)
|
|
|
|
@patch.dict('os.environ', {'VULTR_API_KEY': 'env-test-key'})
|
|
def test_server_creation_from_env(self):
|
|
"""Test server creation using environment variable."""
|
|
server = create_mcp_server()
|
|
assert server is not None
|
|
|
|
|
|
@pytest.mark.mcp
|
|
class TestMCPTools:
|
|
"""Test MCP tools through in-memory client connection."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_dns_domains_tool(self, mcp_server, mock_vultr_client):
|
|
"""Test the list_dns_domains MCP tool."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
# For the official MCP package, we need to use ClientSession
|
|
async with ClientSession(server) as session:
|
|
result = await session.call_tool("list_dns_domains", {})
|
|
|
|
assert isinstance(result, list)
|
|
# The result should be a list containing the response
|
|
assert len(result) > 0
|
|
|
|
# Check if we got the mock data
|
|
domains_data = result[0].text if hasattr(result[0], 'text') else result
|
|
mock_vultr_client.list_domains.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_dns_domain_tool(self, mcp_server, mock_vultr_client):
|
|
"""Test the get_dns_domain MCP tool."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
result = await session.call_tool("get_dns_domain", {"domain": "example.com"})
|
|
|
|
assert result is not None
|
|
mock_vultr_client.get_domain.assert_called_once_with("example.com")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_dns_domain_tool(self, mcp_server, mock_vultr_client):
|
|
"""Test the create_dns_domain MCP tool."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
result = await session.call_tool("create_dns_domain", {
|
|
"domain": "newdomain.com",
|
|
"ip": "192.168.1.100"
|
|
})
|
|
|
|
assert result is not None
|
|
mock_vultr_client.create_domain.assert_called_once_with("newdomain.com", "192.168.1.100")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_dns_domain_tool(self, mcp_server, mock_vultr_client):
|
|
"""Test the delete_dns_domain MCP tool."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
result = await session.call_tool("delete_dns_domain", {"domain": "example.com"})
|
|
|
|
assert result is not None
|
|
mock_vultr_client.delete_domain.assert_called_once_with("example.com")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_dns_records_tool(self, mcp_server, mock_vultr_client):
|
|
"""Test the list_dns_records MCP tool."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
result = await session.call_tool("list_dns_records", {"domain": "example.com"})
|
|
|
|
assert result is not None
|
|
mock_vultr_client.list_records.assert_called_once_with("example.com")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_dns_record_tool(self, mcp_server, mock_vultr_client):
|
|
"""Test the create_dns_record MCP tool."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
result = await session.call_tool("create_dns_record", {
|
|
"domain": "example.com",
|
|
"record_type": "A",
|
|
"name": "www",
|
|
"data": "192.168.1.100",
|
|
"ttl": 300
|
|
})
|
|
|
|
assert result is not None
|
|
mock_vultr_client.create_record.assert_called_once_with(
|
|
"example.com", "A", "www", "192.168.1.100", 300, None
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_dns_record_tool(self, mcp_server):
|
|
"""Test the validate_dns_record MCP tool."""
|
|
async with ClientSession(mcp_server) as session:
|
|
# Test valid A record
|
|
result = await session.call_tool("validate_dns_record", {
|
|
"record_type": "A",
|
|
"name": "www",
|
|
"data": "192.168.1.100",
|
|
"ttl": 300
|
|
})
|
|
|
|
assert result is not None
|
|
# The validation should pass for a valid A record
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_dns_record_invalid(self, mcp_server):
|
|
"""Test the validate_dns_record tool with invalid data."""
|
|
async with ClientSession(mcp_server) as session:
|
|
# Test invalid A record (bad IP)
|
|
result = await session.call_tool("validate_dns_record", {
|
|
"record_type": "A",
|
|
"name": "www",
|
|
"data": "invalid-ip-address"
|
|
})
|
|
|
|
assert result is not None
|
|
# Should detect the invalid IP address
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_analyze_dns_records_tool(self, mcp_server, mock_vultr_client):
|
|
"""Test the analyze_dns_records MCP tool."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
result = await session.call_tool("analyze_dns_records", {"domain": "example.com"})
|
|
|
|
assert result is not None
|
|
mock_vultr_client.list_records.assert_called_once_with("example.com")
|
|
|
|
|
|
@pytest.mark.mcp
|
|
class TestMCPResources:
|
|
"""Test MCP resources through in-memory client connection."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_domains_resource(self, mcp_server, mock_vultr_client):
|
|
"""Test the vultr://domains resource."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
# Get available resources
|
|
resources = await session.list_resources()
|
|
|
|
# Check that domains resource is available
|
|
resource_uris = [r.uri for r in resources]
|
|
assert "vultr://domains" in resource_uris
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_capabilities_resource(self, mcp_server):
|
|
"""Test the vultr://capabilities resource."""
|
|
async with ClientSession(mcp_server) as session:
|
|
resources = await session.list_resources()
|
|
resource_uris = [r.uri for r in resources]
|
|
assert "vultr://capabilities" in resource_uris
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_domains_resource(self, mcp_server, mock_vultr_client):
|
|
"""Test reading the domains resource content."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
try:
|
|
result = await session.read_resource("vultr://domains")
|
|
assert result is not None
|
|
mock_vultr_client.list_domains.assert_called_once()
|
|
except Exception:
|
|
# Resource reading might not be available in all MCP versions
|
|
pass
|
|
|
|
|
|
@pytest.mark.mcp
|
|
class TestMCPToolErrors:
|
|
"""Test MCP tool error handling."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tool_with_api_error(self, mcp_server):
|
|
"""Test tool behavior when API returns an error."""
|
|
mock_client = AsyncMock()
|
|
mock_client.list_domains.side_effect = Exception("API Error")
|
|
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
result = await session.call_tool("list_dns_domains", {})
|
|
|
|
# Should handle the error gracefully
|
|
assert result is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_required_parameters(self, mcp_server):
|
|
"""Test tool behavior with missing required parameters."""
|
|
async with ClientSession(mcp_server) as session:
|
|
with pytest.raises(Exception):
|
|
# This should fail due to missing required 'domain' parameter
|
|
await session.call_tool("get_dns_domain", {})
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestMCPIntegration:
|
|
"""Integration tests for the complete MCP workflow."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_complete_domain_workflow(self, mcp_server, mock_vultr_client):
|
|
"""Test a complete domain management workflow."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
# 1. List domains
|
|
domains = await session.call_tool("list_dns_domains", {})
|
|
assert domains is not None
|
|
|
|
# 2. Get domain details
|
|
domain_info = await session.call_tool("get_dns_domain", {"domain": "example.com"})
|
|
assert domain_info is not None
|
|
|
|
# 3. List records
|
|
records = await session.call_tool("list_dns_records", {"domain": "example.com"})
|
|
assert records is not None
|
|
|
|
# 4. Analyze configuration
|
|
analysis = await session.call_tool("analyze_dns_records", {"domain": "example.com"})
|
|
assert analysis is not None
|
|
|
|
# Verify all expected API calls were made
|
|
mock_vultr_client.list_domains.assert_called()
|
|
mock_vultr_client.get_domain.assert_called_with("example.com")
|
|
mock_vultr_client.list_records.assert_called_with("example.com")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_record_management_workflow(self, mcp_server, mock_vultr_client):
|
|
"""Test record creation and management workflow."""
|
|
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
|
server = create_mcp_server("test-api-key")
|
|
|
|
async with ClientSession(server) as session:
|
|
# 1. Validate record before creation
|
|
validation = await session.call_tool("validate_dns_record", {
|
|
"record_type": "A",
|
|
"name": "www",
|
|
"data": "192.168.1.100"
|
|
})
|
|
assert validation is not None
|
|
|
|
# 2. Create the record
|
|
create_result = await session.call_tool("create_dns_record", {
|
|
"domain": "example.com",
|
|
"record_type": "A",
|
|
"name": "www",
|
|
"data": "192.168.1.100",
|
|
"ttl": 300
|
|
})
|
|
assert create_result is not None
|
|
|
|
# 3. Verify the record was created
|
|
mock_vultr_client.create_record.assert_called_with(
|
|
"example.com", "A", "www", "192.168.1.100", 300, None
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestValidationLogic:
|
|
"""Test DNS record validation logic in isolation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_record_validation(self, mcp_server):
|
|
"""Test A record validation logic."""
|
|
async with ClientSession(mcp_server) as session:
|
|
# Valid IPv4
|
|
result = await session.call_tool("validate_dns_record", {
|
|
"record_type": "A",
|
|
"name": "www",
|
|
"data": "192.168.1.1"
|
|
})
|
|
assert result is not None
|
|
|
|
# Invalid IPv4
|
|
result = await session.call_tool("validate_dns_record", {
|
|
"record_type": "A",
|
|
"name": "www",
|
|
"data": "999.999.999.999"
|
|
})
|
|
assert result is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cname_validation(self, mcp_server):
|
|
"""Test CNAME record validation logic."""
|
|
async with ClientSession(mcp_server) as session:
|
|
# Invalid: CNAME on root domain
|
|
result = await session.call_tool("validate_dns_record", {
|
|
"record_type": "CNAME",
|
|
"name": "@",
|
|
"data": "example.com"
|
|
})
|
|
assert result is not None
|
|
|
|
# Valid: CNAME on subdomain
|
|
result = await session.call_tool("validate_dns_record", {
|
|
"record_type": "CNAME",
|
|
"name": "www",
|
|
"data": "example.com"
|
|
})
|
|
assert result is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mx_validation(self, mcp_server):
|
|
"""Test MX record validation logic."""
|
|
async with ClientSession(mcp_server) as session:
|
|
# Invalid: Missing priority
|
|
result = await session.call_tool("validate_dns_record", {
|
|
"record_type": "MX",
|
|
"name": "@",
|
|
"data": "mail.example.com"
|
|
})
|
|
assert result is not None
|
|
|
|
# Valid: With priority
|
|
result = await session.call_tool("validate_dns_record", {
|
|
"record_type": "MX",
|
|
"name": "@",
|
|
"data": "mail.example.com",
|
|
"priority": 10
|
|
})
|
|
assert result is not None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__])
|