mcp-name-cheap/tests/test_server.py
Claude Code f5e63c888d Initial commit: MCP Name Cheap server implementation
- Production-ready MCP server for Name Cheap API integration
- Domain management (registration, renewal, availability checking)
- DNS management (records, nameserver configuration)
- SSL certificate management and monitoring
- Account information and balance checking
- Smart identifier resolution for improved UX
- Comprehensive error handling with specific exception types
- 80%+ test coverage with unit, integration, and MCP tests
- CLI and MCP server interfaces
- FastMCP 2.10.5+ implementation with full MCP spec compliance

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 03:43:11 -06:00

347 lines
12 KiB
Python

"""Test Name Cheap API server."""
import pytest
from unittest.mock import Mock, patch
import httpx
import xmltodict
from mcp_namecheap.server import (
NameCheapAPIServer,
NameCheapConfig,
NameCheapAPIError,
NameCheapAuthError,
NameCheapNotFoundError,
NameCheapRateLimitError,
NameCheapValidationError
)
class TestNameCheapAPIServer:
"""Test Name Cheap API server functionality."""
@pytest.mark.unit
def test_config_creation(self):
"""Test configuration creation."""
config = NameCheapConfig(
api_key="test_key",
username="test_user",
client_ip="127.0.0.1",
sandbox=True
)
assert config.api_key == "test_key"
assert config.username == "test_user"
assert config.client_ip == "127.0.0.1"
assert config.sandbox is True
@pytest.mark.unit
def test_server_initialization(self, mock_config):
"""Test server initialization."""
with patch('mcp_namecheap.server.httpx.Client') as mock_client:
server = NameCheapAPIServer(mock_config)
assert server.config == mock_config
assert "sandbox" in server.base_url
mock_client.assert_called_once()
@pytest.mark.unit
def test_load_config_from_env_missing_vars(self):
"""Test loading config from environment with missing variables."""
with patch.dict('os.environ', {}, clear=True):
with pytest.raises(NameCheapAuthError):
NameCheapAPIServer()
@pytest.mark.unit
def test_load_config_from_env_success(self):
"""Test loading config from environment successfully."""
env_vars = {
'NAMECHEAP_API_KEY': 'test_key',
'NAMECHEAP_USERNAME': 'test_user',
'NAMECHEAP_CLIENT_IP': '127.0.0.1',
'NAMECHEAP_SANDBOX': 'true'
}
with patch.dict('os.environ', env_vars):
with patch('mcp_namecheap.server.httpx.Client'):
server = NameCheapAPIServer()
assert server.config.api_key == 'test_key'
assert server.config.sandbox is True
@pytest.mark.unit
def test_make_request_success(self, mock_server):
"""Test successful API request."""
# Mock response
mock_response = Mock()
mock_response.text = '''
<ApiResponse Status="OK">
<CommandResponse>
<TestResult>Success</TestResult>
</CommandResponse>
</ApiResponse>
'''
mock_response.raise_for_status.return_value = None
mock_server.client.get.return_value = mock_response
result = mock_server._make_request("test.command")
assert result["TestResult"] == "Success"
mock_server.client.get.assert_called_once()
@pytest.mark.unit
def test_make_request_api_error(self, mock_server):
"""Test API request with API error response."""
# Mock error response
mock_response = Mock()
mock_response.text = '''
<ApiResponse Status="ERROR">
<Errors>
<Error Number="1234">Test error message</Error>
</Errors>
</ApiResponse>
'''
mock_response.raise_for_status.return_value = None
mock_server.client.get.return_value = mock_response
with pytest.raises(NameCheapAPIError):
mock_server._make_request("test.command")
@pytest.mark.unit
def test_make_request_http_error(self, mock_server):
"""Test API request with HTTP error."""
mock_response = Mock()
mock_response.status_code = 401
mock_response.text = "Unauthorized"
mock_server.client.get.side_effect = httpx.HTTPStatusError(
"Unauthorized", request=Mock(), response=mock_response
)
with pytest.raises(NameCheapAuthError):
mock_server._make_request("test.command")
@pytest.mark.unit
def test_make_request_rate_limit(self, mock_server):
"""Test API request with rate limit error."""
mock_response = Mock()
mock_response.status_code = 429
mock_response.text = "Too Many Requests"
mock_server.client.get.side_effect = httpx.HTTPStatusError(
"Too Many Requests", request=Mock(), response=mock_response
)
with pytest.raises(NameCheapRateLimitError):
mock_server._make_request("test.command")
@pytest.mark.unit
def test_make_request_timeout(self, mock_server):
"""Test API request with timeout."""
mock_server.client.get.side_effect = httpx.TimeoutException("Timeout")
with pytest.raises(NameCheapAPIError) as exc_info:
mock_server._make_request("test.command")
assert "timeout" in str(exc_info.value).lower()
@pytest.mark.unit
def test_handle_api_errors_not_found(self, mock_server):
"""Test API error handling for not found errors."""
api_response = {
"Errors": {
"Error": {
"@Number": "2030166",
"#text": "Domain not found"
}
}
}
with pytest.raises(NameCheapNotFoundError):
mock_server._handle_api_errors(api_response)
@pytest.mark.unit
def test_handle_api_errors_validation(self, mock_server):
"""Test API error handling for validation errors."""
api_response = {
"Errors": {
"Error": {
"@Number": "1234",
"#text": "Invalid domain format"
}
}
}
with pytest.raises(NameCheapValidationError):
mock_server._handle_api_errors(api_response)
@pytest.mark.unit
def test_handle_api_errors_multiple(self, mock_server):
"""Test API error handling with multiple errors."""
api_response = {
"Errors": {
"Error": [
{"@Number": "1", "#text": "First error"},
{"@Number": "2", "#text": "Second error"}
]
}
}
with pytest.raises(NameCheapAPIError) as exc_info:
mock_server._handle_api_errors(api_response)
assert "First error" in str(exc_info.value)
assert "Second error" in str(exc_info.value)
@pytest.mark.unit
def test_split_domain_valid(self, mock_server):
"""Test domain splitting with valid domains."""
sld, tld = mock_server._split_domain("example.com")
assert sld == "example"
assert tld == "com"
sld, tld = mock_server._split_domain("test.co.uk")
assert sld == "test"
assert tld == "co.uk"
@pytest.mark.unit
def test_split_domain_invalid(self, mock_server):
"""Test domain splitting with invalid domains."""
with pytest.raises(NameCheapValidationError):
mock_server._split_domain("invalid")
@pytest.mark.unit
def test_list_domains(self, mock_server):
"""Test list domains method."""
mock_response = Mock()
mock_response.text = '''
<ApiResponse Status="OK">
<CommandResponse>
<DomainGetListResult>
<Domain ID="12345" Name="example.com" User="test" />
</DomainGetListResult>
</CommandResponse>
</ApiResponse>
'''
mock_response.raise_for_status.return_value = None
mock_server.client.get.return_value = mock_response
domains = mock_server.list_domains()
assert len(domains) == 1
assert domains[0]["@Name"] == "example.com"
@pytest.mark.unit
def test_check_domain_availability(self, mock_server):
"""Test check domain availability method."""
mock_response = Mock()
mock_response.text = '''
<ApiResponse Status="OK">
<CommandResponse>
<DomainCheckResult Domain="test.com" Available="true" />
</CommandResponse>
</ApiResponse>
'''
mock_response.raise_for_status.return_value = None
mock_server.client.get.return_value = mock_response
results = mock_server.check_domain_availability(["test.com"])
assert len(results) == 1
assert results[0]["@Available"] == "true"
@pytest.mark.unit
def test_get_dns_records(self, mock_server):
"""Test get DNS records method."""
mock_response = Mock()
mock_response.text = '''
<ApiResponse Status="OK">
<CommandResponse>
<DomainDNSGetHostsResult>
<host HostId="1" Name="@" Type="A" Address="192.168.1.1" TTL="1800" />
</DomainDNSGetHostsResult>
</CommandResponse>
</ApiResponse>
'''
mock_response.raise_for_status.return_value = None
mock_server.client.get.return_value = mock_response
records = mock_server.get_dns_records("example.com")
assert len(records) == 1
assert records[0]["@Type"] == "A"
@pytest.mark.unit
def test_context_manager(self, mock_config):
"""Test server as context manager."""
with patch('mcp_namecheap.server.httpx.Client') as mock_client_class:
mock_client = Mock()
mock_client_class.return_value = mock_client
with NameCheapAPIServer(mock_config) as server:
assert server.config == mock_config
mock_client.close.assert_called_once()
class TestErrorClassification:
"""Test error classification logic."""
@pytest.mark.unit
def test_auth_error_classification(self, mock_server):
"""Test authentication error classification."""
api_response = {
"Errors": {
"Error": {
"@Number": "1001",
"#text": "Authentication failed"
}
}
}
with pytest.raises(NameCheapAuthError):
mock_server._handle_api_errors(api_response)
@pytest.mark.unit
def test_not_found_error_classification(self, mock_server):
"""Test not found error classification."""
api_response = {
"Errors": {
"Error": {
"@Number": "2030166",
"#text": "Domain not found"
}
}
}
with pytest.raises(NameCheapNotFoundError):
mock_server._handle_api_errors(api_response)
@pytest.mark.unit
def test_validation_error_classification(self, mock_server):
"""Test validation error classification."""
api_response = {
"Errors": {
"Error": {
"@Number": "1234",
"#text": "Invalid parameter"
}
}
}
with pytest.raises(NameCheapValidationError):
mock_server._handle_api_errors(api_response)
@pytest.mark.unit
def test_generic_error_classification(self, mock_server):
"""Test generic error classification."""
api_response = {
"Errors": {
"Error": {
"@Number": "9999",
"#text": "Unknown error"
}
}
}
with pytest.raises(NameCheapAPIError):
mock_server._handle_api_errors(api_response)