"""Integration tests for the math server."""
from unittest.mock import patch
from chuk_mcp_math_server import (
ConfigurableMCPMathServer,
MathServerConfig,
create_math_server,
)
class TestMathServer:
"""Test the math server functionality."""
def test_server_initialization(self, math_config):
"""Test that server initializes correctly."""
server = ConfigurableMCPMathServer(math_config)
assert server is not None
assert server.config == math_config
assert server.mcp_server is not None
def test_server_with_all_functions(self, math_config):
"""Test server with all functions loaded."""
server = ConfigurableMCPMathServer(math_config)
stats = server.get_function_stats()
assert stats["total_available"] == 642
assert stats["total_filtered"] == 642
assert not stats["filtering_active"]
def test_server_with_filtered_functions(self, filtered_config):
"""Test server with function filtering."""
server = ConfigurableMCPMathServer(filtered_config)
stats = server.get_function_stats()
assert stats["total_available"] == 642
assert stats["total_filtered"] == 4
assert stats["filtering_active"]
def test_server_with_domain_filtering(self, domain_filtered_config):
"""Test server with domain filtering."""
server = ConfigurableMCPMathServer(domain_filtered_config)
stats = server.get_function_stats()
assert stats["total_filtered"] > 0
assert stats["total_filtered"] < 642
assert stats["filtering_active"]
assert "arithmetic" in stats["domains_filtered"]
assert "trigonometry" not in stats["domains_filtered"]
def test_function_stats(self, math_config):
"""Test getting function statistics."""
server = ConfigurableMCPMathServer(math_config)
stats = server.get_function_stats()
assert "total_available" in stats
assert "total_filtered" in stats
assert "filtering_active" in stats
assert "domains_filtered" in stats
assert isinstance(stats["domains_filtered"], dict)
def test_server_logging_configuration(self):
"""Test that logging is configured correctly."""
config = MathServerConfig(transport="stdio", log_level="WARNING")
server = ConfigurableMCPMathServer(config)
# Should initialize without errors
assert server is not None
def test_multiple_server_instances(self, math_config, filtered_config):
"""Test that multiple server instances can coexist."""
server1 = ConfigurableMCPMathServer(math_config)
server2 = ConfigurableMCPMathServer(filtered_config)
stats1 = server1.get_function_stats()
stats2 = server2.get_function_stats()
assert stats1["total_filtered"] == 642
assert stats2["total_filtered"] == 4
class TestServerResources:
"""Test server resource registration."""
def test_resources_registered(self, math_config):
"""Test that resources are registered."""
server = ConfigurableMCPMathServer(math_config)
# Server should have mcp_server initialized
assert server.mcp_server is not None
# Resources should be registered (we can't easily check the internal
# registry without accessing private attributes, so we just verify
# the server initialized successfully)
assert server is not None
@patch("chuk_mcp_math_server.math_server.logger")
def test_dynamic_tool_registration(self, mock_logger, math_config):
"""Test dynamic tool registration."""
server = ConfigurableMCPMathServer(math_config)
# Should have registered tools
stats = server.get_function_stats()
assert stats["total_filtered"] > 0
# Logger should have been called about registration
mock_logger.info.assert_any_call(
f"Registered {stats['total_filtered']} mathematical tools"
)
@patch("chuk_mcp_math_server.math_server.logger")
def test_tool_registration_error_handling(self, mock_logger):
"""Test tool registration error handling."""
# Create a server with invalid function spec
config = MathServerConfig(function_allowlist=["add"])
# Mock _register_dynamic_tool to raise an exception
with patch.object(
ConfigurableMCPMathServer,
"_register_dynamic_tool",
side_effect=RuntimeError("Test error"),
):
server = ConfigurableMCPMathServer(config)
# Should still initialize successfully
assert server is not None
# Error should have been logged
mock_logger.error.assert_called()
@patch("chuk_mcp_math_server.math_server.logger")
def test_missing_function_ref(self, mock_logger):
"""Test handling of function spec without function_ref."""
from unittest.mock import MagicMock
config = MathServerConfig()
server = ConfigurableMCPMathServer(config)
# Create a mock function spec with no function_ref
mock_spec = MagicMock()
mock_spec.function_ref = None
mock_spec.function_name = "test_func"
# Call _register_dynamic_tool with the mock spec
server._register_dynamic_tool(mock_spec)
# Should have logged a warning
mock_logger.warning.assert_called_once()
assert "No function reference" in str(mock_logger.warning.call_args)
def test_server_run_stdio(self, math_config):
"""Test server run with stdio transport."""
server = ConfigurableMCPMathServer(math_config)
# Mock the underlying run method
with patch.object(server.mcp_server, "run") as mock_run:
server.run()
mock_run.assert_called_once_with(stdio=True, log_level="warning")
def test_server_run_http(self):
"""Test server run with HTTP transport."""
config = MathServerConfig(transport="http", port=9000, host="127.0.0.1")
server = ConfigurableMCPMathServer(config)
# Mock the underlying run method
with patch.object(server.mcp_server, "run") as mock_run:
server.run()
mock_run.assert_called_once_with(
host="127.0.0.1", port=9000, log_level="info"
)
async def test_available_functions_resource(self):
"""Test the available_functions resource."""
import json
config = MathServerConfig(function_allowlist=["add", "subtract"])
server = ConfigurableMCPMathServer(config)
# Get registered resources
resources = server.mcp_server.get_resources()
# Find the available_functions resource
available_funcs_resource = None
for resource in resources:
if "available-functions" in resource.uri:
available_funcs_resource = resource
break
assert available_funcs_resource is not None
# Call the resource function
result = await available_funcs_resource.handler()
result_data = json.loads(result)
# Verify structure
assert "total_functions" in result_data
assert "functions_by_domain" in result_data
assert "filtering_applied" in result_data
assert result_data["total_functions"] == 2
async def test_function_stats_resource(self):
"""Test the function_stats resource."""
import json
config = MathServerConfig(domain_allowlist=["arithmetic"])
server = ConfigurableMCPMathServer(config)
# Get registered resources
resources = server.mcp_server.get_resources()
# Find the function-stats resource
stats_resource = None
for resource in resources:
if "function-stats" in resource.uri:
stats_resource = resource
break
assert stats_resource is not None
# Call the resource function
result = await stats_resource.handler()
result_data = json.loads(result)
# Verify stats structure
assert "total_available" in result_data
assert "total_filtered" in result_data
assert "filtering_active" in result_data
assert result_data["filtering_active"] is True
async def test_server_config_resource(self):
"""Test the server_config resource."""
import json
config = MathServerConfig(transport="http", port=9001)
server = ConfigurableMCPMathServer(config)
# Get registered resources
resources = server.mcp_server.get_resources()
# Find the server-config resource
config_resource = None
for resource in resources:
if "server-config" in resource.uri:
config_resource = resource
break
assert config_resource is not None
# Call the resource function
result = await config_resource.handler()
result_data = json.loads(result)
assert result_data["transport"] == "http"
assert result_data["port"] == 9001
class TestServerFactory:
"""Test server factory function."""
def test_create_server_with_kwargs(self):
"""Test creating server with keyword arguments."""
server = create_math_server(transport="stdio", log_level="WARNING")
assert server is not None
assert server.config.transport == "stdio"
assert server.config.log_level == "WARNING"
def test_create_server_with_filtering(self):
"""Test creating server with filtering options."""
server = create_math_server(function_allowlist=["add", "subtract"])
stats = server.get_function_stats()
assert stats["total_filtered"] == 2
assert stats["filtering_active"]
class TestServerEdgeCases:
"""Test edge cases and error handling."""
def test_empty_filter_list(self):
"""Test server with empty filter lists."""
config = MathServerConfig(function_allowlist=[])
server = ConfigurableMCPMathServer(config)
stats = server.get_function_stats()
# Empty allowlist is treated as "no filtering", so all functions pass
# To actually filter to zero, you'd need a allowlist with non-existent functions
assert stats["total_filtered"] >= 0
def test_invalid_domain_filter(self):
"""Test server with invalid domain filter."""
config = MathServerConfig(domain_allowlist=["nonexistent_domain"])
server = ConfigurableMCPMathServer(config)
stats = server.get_function_stats()
# Invalid domain means no functions pass
assert stats["total_filtered"] == 0
def test_conflicting_filters(self):
"""Test server with conflicting allowlist and denylist."""
config = MathServerConfig(
function_allowlist=["add", "subtract"],
function_denylist=["add"], # Conflicts with allowlist
)
server = ConfigurableMCPMathServer(config)
stats = server.get_function_stats()
# Whitelist takes precedence, but denylist filters from allowlist
assert stats["total_filtered"] <= 2
def test_create_server_with_config_file(self, tmp_path):
"""Test create_math_server with config file."""
import yaml
config_file = tmp_path / "server_config.yaml"
config_file.write_text(
yaml.dump({"transport": "stdio", "function_allowlist": ["add", "subtract"]})
)
server = create_math_server(config_file=str(config_file), port=9999)
# Config from file should be loaded
assert server.config.function_allowlist == ["add", "subtract"]
# Kwarg override should be applied
assert server.config.port == 9999
def test_server_logging_levels(self):
"""Test server with different logging levels."""
config = MathServerConfig(log_level="DEBUG")
server = ConfigurableMCPMathServer(config)
assert server is not None
# Test with ERROR level
config2 = MathServerConfig(log_level="ERROR")
server2 = ConfigurableMCPMathServer(config2)
assert server2 is not None