"""Unit tests for configuration management tools.""" import json from unittest.mock import patch, MagicMock import pytest class TestRestoreServersFromConfig: """Tests for restore_servers_from_config function.""" def test_restore_empty_config(self, patch_config_paths): """No servers to restore when config is empty.""" from haproxy_mcp.tools.configuration import restore_servers_from_config result = restore_servers_from_config() assert result == 0 def test_restore_servers_success(self, mock_socket_class, mock_select, patch_config_paths, sample_servers_config): """Restore servers successfully.""" # Write config and map with open(patch_config_paths["servers_file"], "w") as f: json.dump(sample_servers_config, f) with open(patch_config_paths["map_file"], "w") as f: f.write("example.com pool_1\n") f.write("api.example.com pool_2\n") mock_sock = mock_socket_class(responses={ "set server": "", }) with patch("socket.socket", return_value=mock_sock): from haproxy_mcp.tools.configuration import restore_servers_from_config result = restore_servers_from_config() # example.com has 2 servers, api.example.com has 1 assert result == 3 def test_restore_servers_skip_missing_domain(self, mock_socket_class, mock_select, patch_config_paths): """Skip domains not in map file.""" config = {"unknown.com": {"1": {"ip": "10.0.0.1", "http_port": 80}}} with open(patch_config_paths["servers_file"], "w") as f: json.dump(config, f) mock_sock = mock_socket_class(responses={"set server": ""}) with patch("socket.socket", return_value=mock_sock): from haproxy_mcp.tools.configuration import restore_servers_from_config result = restore_servers_from_config() assert result == 0 def test_restore_servers_skip_empty_ip(self, mock_socket_class, mock_select, patch_config_paths): """Skip servers with empty IP.""" config = {"example.com": {"1": {"ip": "", "http_port": 80}}} with open(patch_config_paths["servers_file"], "w") as f: json.dump(config, f) with open(patch_config_paths["map_file"], "w") as f: f.write("example.com pool_1\n") mock_sock = mock_socket_class(responses={"set server": ""}) with patch("socket.socket", return_value=mock_sock): from haproxy_mcp.tools.configuration import restore_servers_from_config result = restore_servers_from_config() assert result == 0 class TestStartupRestore: """Tests for startup_restore function.""" def test_startup_restore_haproxy_not_ready(self, mock_select): """Skip restore if HAProxy is not ready.""" call_count = 0 def raise_error(*args, **kwargs): nonlocal call_count call_count += 1 raise ConnectionRefusedError() with patch("socket.socket", side_effect=raise_error): with patch("haproxy_mcp.tools.configuration.STARTUP_RETRY_COUNT", 2): from haproxy_mcp.tools.configuration import startup_restore startup_restore() # Should have tried multiple times assert call_count >= 2 def test_startup_restore_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder): """Successfully restore servers and certificates on startup.""" mock_sock = mock_socket_class(responses={ "show info": response_builder.info(), "set server": "", "show ssl cert": "", }) with patch("socket.socket", return_value=mock_sock): with patch("haproxy_mcp.tools.configuration.restore_servers_from_config", return_value=0): with patch("haproxy_mcp.tools.certificates.restore_certificates", return_value=0): from haproxy_mcp.tools.configuration import startup_restore startup_restore() # No assertions needed - just verify no exceptions class TestHaproxyReload: """Tests for haproxy_reload tool function.""" def test_reload_success(self, mock_socket_class, mock_select, mock_subprocess, response_builder): """Reload HAProxy successfully.""" mock_subprocess.return_value = MagicMock(returncode=0, stdout="", stderr="") mock_sock = mock_socket_class(responses={ "show info": response_builder.info(), "set server": "", }) with patch("socket.socket", return_value=mock_sock): with patch("haproxy_mcp.tools.configuration.restore_servers_from_config", return_value=5): from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_reload"]() assert "reloaded successfully" in result assert "5 servers restored" in result def test_reload_validation_failure(self, mock_subprocess): """Reload fails on config validation error.""" mock_subprocess.return_value = MagicMock( returncode=1, stdout="", stderr="Configuration error" ) from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_reload"]() assert "validation failed" in result.lower() or "Configuration error" in result class TestHaproxyCheckConfig: """Tests for haproxy_check_config tool function.""" def test_check_config_valid(self, mock_subprocess): """Configuration is valid.""" mock_subprocess.return_value = MagicMock(returncode=0, stdout="", stderr="") from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_check_config"]() assert "valid" in result.lower() def test_check_config_invalid(self, mock_subprocess): """Configuration has errors.""" mock_subprocess.return_value = MagicMock( returncode=1, stdout="", stderr="[ALERT] Syntax error" ) from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_check_config"]() assert "error" in result.lower() assert "Syntax error" in result def test_check_config_timeout(self, mock_subprocess): """Configuration check times out.""" import subprocess mock_subprocess.side_effect = subprocess.TimeoutExpired("podman", 30) from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_check_config"]() assert "timed out" in result.lower() def test_check_config_podman_not_found(self, mock_subprocess): """Podman not found.""" mock_subprocess.side_effect = FileNotFoundError() from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_check_config"]() assert "podman" in result.lower() assert "not found" in result.lower() class TestHaproxySaveState: """Tests for haproxy_save_state tool function.""" def test_save_state_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder): """Save state successfully.""" mock_sock = mock_socket_class(responses={ "show servers state": response_builder.servers_state([ {"be_name": "pool_1", "srv_name": "pool_1_1", "srv_addr": "10.0.0.1", "srv_port": 80}, ]), }) with patch("haproxy_mcp.tools.configuration.STATE_FILE", patch_config_paths["state_file"]): with patch("socket.socket", return_value=mock_sock): from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_save_state"]() assert "saved" in result.lower() def test_save_state_haproxy_error(self, mock_select): """Handle HAProxy connection error.""" def raise_error(*args, **kwargs): raise ConnectionRefusedError() with patch("socket.socket", side_effect=raise_error): from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_save_state"]() assert "Error" in result class TestHaproxyRestoreState: """Tests for haproxy_restore_state tool function.""" def test_restore_state_success(self, mock_socket_class, mock_select, patch_config_paths, sample_servers_config): """Restore state successfully.""" with open(patch_config_paths["servers_file"], "w") as f: json.dump(sample_servers_config, f) with open(patch_config_paths["map_file"], "w") as f: f.write("example.com pool_1\n") f.write("api.example.com pool_2\n") mock_sock = mock_socket_class(responses={"set server": ""}) with patch("socket.socket", return_value=mock_sock): from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_restore_state"]() assert "restored" in result.lower() assert "3 servers" in result def test_restore_state_no_servers(self, patch_config_paths): """No servers to restore.""" from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_restore_state"]() assert "No servers to restore" in result class TestRestoreServersFromConfigBatchFailure: """Tests for restore_servers_from_config batch failure and fallback.""" def test_restore_servers_batch_failure_fallback(self, mock_socket_class, mock_select, patch_config_paths): """Fall back to individual commands when batch fails.""" # Create config with servers config = { "example.com": { "1": {"ip": "10.0.0.1", "http_port": 80}, "2": {"ip": "10.0.0.2", "http_port": 80}, } } with open(patch_config_paths["servers_file"], "w") as f: json.dump(config, f) with open(patch_config_paths["map_file"], "w") as f: f.write("example.com pool_1\n") # Track call count to simulate batch failure then individual success call_count = [0] class BatchFailMockSocket: def __init__(self): self.sent_commands = [] self._response_buffer = b"" self._closed = False def connect(self, address): pass def settimeout(self, timeout): pass def setblocking(self, blocking): pass def sendall(self, data): command = data.decode().strip() self.sent_commands.append(command) call_count[0] += 1 # First batch call fails (contains multiple commands) if call_count[0] == 1 and "\n" in data.decode(): self._response_buffer = b"error: batch command failed" else: self._response_buffer = b"" def shutdown(self, how): pass def recv(self, bufsize): if self._response_buffer: data = self._response_buffer[:bufsize] self._response_buffer = self._response_buffer[bufsize:] return data return b"" def close(self): self._closed = True def fileno(self): return 999 def __enter__(self): return self def __exit__(self, *args): self.close() mock_sock = BatchFailMockSocket() with patch("socket.socket", return_value=mock_sock): from haproxy_mcp.tools.configuration import restore_servers_from_config from haproxy_mcp.exceptions import HaproxyError # Mock batch to raise error with patch("haproxy_mcp.tools.configuration.haproxy_cmd_batch") as mock_batch: # First call (batch) fails, subsequent calls succeed mock_batch.side_effect = [ HaproxyError("Batch failed"), # Initial batch fails None, # Individual server 1 succeeds None, # Individual server 2 succeeds ] result = restore_servers_from_config() # Should have restored servers via individual commands assert result == 2 def test_restore_servers_invalid_slot(self, mock_socket_class, mock_select, patch_config_paths): """Skip servers with invalid slot number.""" config = { "example.com": { "invalid": {"ip": "10.0.0.1", "http_port": 80}, # Invalid slot "1": {"ip": "10.0.0.2", "http_port": 80}, # Valid slot } } with open(patch_config_paths["servers_file"], "w") as f: json.dump(config, f) with open(patch_config_paths["map_file"], "w") as f: f.write("example.com pool_1\n") mock_sock = mock_socket_class(responses={"set server": ""}) with patch("socket.socket", return_value=mock_sock): from haproxy_mcp.tools.configuration import restore_servers_from_config result = restore_servers_from_config() # Should only restore the valid server assert result == 1 def test_restore_servers_invalid_port(self, mock_socket_class, mock_select, patch_config_paths, caplog): """Skip servers with invalid port.""" import logging config = { "example.com": { "1": {"ip": "10.0.0.1", "http_port": "invalid"}, # Invalid port "2": {"ip": "10.0.0.2", "http_port": 80}, # Valid port } } with open(patch_config_paths["servers_file"], "w") as f: json.dump(config, f) with open(patch_config_paths["map_file"], "w") as f: f.write("example.com pool_1\n") mock_sock = mock_socket_class(responses={"set server": ""}) with patch("socket.socket", return_value=mock_sock): with caplog.at_level(logging.WARNING, logger="haproxy_mcp"): from haproxy_mcp.tools.configuration import restore_servers_from_config result = restore_servers_from_config() # Should only restore the valid server assert result == 1 class TestStartupRestoreFailures: """Tests for startup_restore failure scenarios.""" def test_startup_restore_haproxy_timeout(self, mock_select): """Skip restore if HAProxy doesn't become ready in time.""" from haproxy_mcp.exceptions import HaproxyError # Mock haproxy_cmd to always fail with patch("haproxy_mcp.tools.configuration.haproxy_cmd", side_effect=HaproxyError("Connection refused")): with patch("haproxy_mcp.tools.configuration.STARTUP_RETRY_COUNT", 2): with patch("time.sleep", return_value=None): from haproxy_mcp.tools.configuration import startup_restore # Should not raise, just log warning startup_restore() def test_startup_restore_server_restore_failure(self, mock_socket_class, mock_select, patch_config_paths, response_builder, caplog): """Handle server restore failure during startup.""" import logging mock_sock = mock_socket_class(responses={ "show info": response_builder.info(), }) with patch("socket.socket", return_value=mock_sock): with patch("haproxy_mcp.tools.configuration.restore_servers_from_config", side_effect=OSError("Disk error")): with patch("haproxy_mcp.tools.certificates.restore_certificates", return_value=0): with caplog.at_level(logging.WARNING, logger="haproxy_mcp"): from haproxy_mcp.tools.configuration import startup_restore startup_restore() # Should have logged the failure assert any("Failed to restore servers" in record.message for record in caplog.records) def test_startup_restore_certificate_failure(self, mock_socket_class, mock_select, patch_config_paths, response_builder, caplog): """Handle certificate restore failure during startup.""" import logging mock_sock = mock_socket_class(responses={ "show info": response_builder.info(), }) with patch("socket.socket", return_value=mock_sock): with patch("haproxy_mcp.tools.configuration.restore_servers_from_config", return_value=0): with patch("haproxy_mcp.tools.certificates.restore_certificates", side_effect=IOError("Certificate error")): with caplog.at_level(logging.WARNING, logger="haproxy_mcp"): from haproxy_mcp.tools.configuration import startup_restore startup_restore() # Should have logged the failure assert any("Failed to restore certificates" in record.message for record in caplog.records) class TestHaproxyReloadFailures: """Tests for haproxy_reload failure scenarios.""" def test_reload_haproxy_not_responding_after_reload(self, mock_subprocess, response_builder): """Handle HAProxy not responding after reload.""" from haproxy_mcp.exceptions import HaproxyError mock_subprocess.return_value = MagicMock(returncode=0, stdout="", stderr="") # Mock haproxy_cmd to fail after reload with patch("haproxy_mcp.haproxy_client.reload_haproxy", return_value=(True, "Reloaded")): with patch("haproxy_mcp.tools.configuration.haproxy_cmd", side_effect=HaproxyError("Not responding")): with patch("haproxy_mcp.tools.configuration.STARTUP_RETRY_COUNT", 2): with patch("time.sleep", return_value=None): from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_reload"]() assert "not responding" in result.lower() def test_reload_server_restore_failure(self, mock_subprocess, mock_socket_class, mock_select, response_builder): """Handle server restore failure after reload.""" mock_subprocess.return_value = MagicMock(returncode=0, stdout="", stderr="") mock_sock = mock_socket_class(responses={ "show info": response_builder.info(), }) with patch("socket.socket", return_value=mock_sock): with patch("haproxy_mcp.haproxy_client.reload_haproxy", return_value=(True, "Reloaded")): with patch("haproxy_mcp.tools.configuration.restore_servers_from_config", side_effect=OSError("Restore failed")): with patch("time.sleep", return_value=None): from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_reload"]() assert "reloaded" in result.lower() assert "failed" in result.lower() class TestHaproxySaveStateFailures: """Tests for haproxy_save_state failure scenarios.""" def test_save_state_io_error(self, mock_socket_class, mock_select, patch_config_paths, response_builder): """Handle IO error when saving state.""" mock_sock = mock_socket_class(responses={ "show servers state": response_builder.servers_state([ {"be_name": "pool_1", "srv_name": "pool_1_1", "srv_addr": "10.0.0.1", "srv_port": 80}, ]), }) with patch("haproxy_mcp.tools.configuration.STATE_FILE", patch_config_paths["state_file"]): with patch("socket.socket", return_value=mock_sock): with patch("haproxy_mcp.tools.configuration.atomic_write_file", side_effect=IOError("Disk full")): from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_save_state"]() assert "Error" in result class TestHaproxyRestoreStateFailures: """Tests for haproxy_restore_state failure scenarios.""" def test_restore_state_haproxy_error(self, mock_socket_class, mock_select, patch_config_paths, sample_servers_config): """Handle HAProxy error when restoring state.""" from haproxy_mcp.exceptions import HaproxyError with open(patch_config_paths["servers_file"], "w") as f: json.dump(sample_servers_config, f) with open(patch_config_paths["map_file"], "w") as f: f.write("example.com pool_1\n") f.write("api.example.com pool_2\n") with patch("haproxy_mcp.tools.configuration.restore_servers_from_config", side_effect=HaproxyError("Connection refused")): from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_restore_state"]() assert "Error" in result def test_restore_state_os_error(self, patch_config_paths): """Handle OS error when restoring state.""" with patch("haproxy_mcp.tools.configuration.restore_servers_from_config", side_effect=OSError("File not found")): from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_restore_state"]() assert "Error" in result def test_restore_state_value_error(self, patch_config_paths): """Handle ValueError when restoring state.""" with patch("haproxy_mcp.tools.configuration.restore_servers_from_config", side_effect=ValueError("Invalid config")): from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_restore_state"]() assert "Error" in result class TestHaproxyCheckConfigOSError: """Tests for haproxy_check_config OS error handling.""" def test_check_config_os_error(self, mock_subprocess): """Handle OS error during config check.""" mock_subprocess.side_effect = OSError("Permission denied") from haproxy_mcp.tools.configuration import register_config_tools mcp = MagicMock() registered_tools = {} def capture_tool(): def decorator(func): registered_tools[func.__name__] = func return func return decorator mcp.tool = capture_tool register_config_tools(mcp) result = registered_tools["haproxy_check_config"]() assert "Error" in result assert "OS error" in result