
- 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>
347 lines
12 KiB
Python
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) |