refactor: Improve code quality, error handling, and test coverage
- Add file_lock context manager to eliminate duplicate locking patterns - Add ValidationError, ConfigurationError, CertificateError exceptions - Improve rollback logic in haproxy_add_servers (track successful ops only) - Decompose haproxy_add_domain into smaller helper functions - Consolidate certificate constants (CERTS_DIR, ACME_HOME) to config.py - Enhance docstrings for internal functions and magic numbers - Add pytest framework with 48 new tests (269 -> 317 total) - Increase test coverage from 76% to 86% - servers.py: 58% -> 82% - certificates.py: 67% -> 86% - configuration.py: 69% -> 94% Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1
tests/unit/tools/__init__.py
Normal file
1
tests/unit/tools/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Unit tests for HAProxy MCP tools."""
|
||||
1198
tests/unit/tools/test_certificates.py
Normal file
1198
tests/unit/tools/test_certificates.py
Normal file
File diff suppressed because it is too large
Load Diff
749
tests/unit/tools/test_configuration.py
Normal file
749
tests/unit/tools/test_configuration.py
Normal file
@@ -0,0 +1,749 @@
|
||||
"""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=Exception("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=Exception("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
|
||||
476
tests/unit/tools/test_domains.py
Normal file
476
tests/unit/tools/test_domains.py
Normal file
@@ -0,0 +1,476 @@
|
||||
"""Unit tests for domain management tools."""
|
||||
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from haproxy_mcp.exceptions import HaproxyError
|
||||
|
||||
|
||||
class TestHaproxyListDomains:
|
||||
"""Tests for haproxy_list_domains tool function."""
|
||||
|
||||
def test_list_empty_domains(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""List domains when none configured."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show servers state": response_builder.servers_state([]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
# Import here to get patched config
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_list_domains"](include_wildcards=False)
|
||||
|
||||
assert result == "No domains configured"
|
||||
|
||||
def test_list_domains_with_servers(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""List domains with configured servers."""
|
||||
# Write map file
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
|
||||
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("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_list_domains"](include_wildcards=False)
|
||||
|
||||
assert "example.com" in result
|
||||
assert "pool_1" in result
|
||||
assert "10.0.0.1" in result
|
||||
|
||||
def test_list_domains_exclude_wildcards(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""List domains excluding wildcards by default."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
with open(patch_config_paths["wildcards_file"], "w") as f:
|
||||
f.write(".example.com pool_1\n")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show servers state": response_builder.servers_state([]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_list_domains"](include_wildcards=False)
|
||||
|
||||
assert "example.com" in result
|
||||
assert ".example.com" not in result
|
||||
|
||||
def test_list_domains_include_wildcards(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""List domains including wildcards when requested."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
with open(patch_config_paths["wildcards_file"], "w") as f:
|
||||
f.write(".example.com pool_1\n")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show servers state": response_builder.servers_state([]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_list_domains"](include_wildcards=True)
|
||||
|
||||
assert "example.com" in result
|
||||
assert ".example.com" in result
|
||||
|
||||
|
||||
class TestHaproxyAddDomain:
|
||||
"""Tests for haproxy_add_domain tool function."""
|
||||
|
||||
def test_add_domain_invalid_format(self, patch_config_paths):
|
||||
"""Reject invalid domain format."""
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_add_domain"](
|
||||
domain="-invalid.com",
|
||||
ip="",
|
||||
http_port=80
|
||||
)
|
||||
|
||||
assert "Error" in result
|
||||
assert "Invalid domain" in result
|
||||
|
||||
def test_add_domain_invalid_ip(self, patch_config_paths):
|
||||
"""Reject invalid IP address."""
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_add_domain"](
|
||||
domain="example.com",
|
||||
ip="not-an-ip",
|
||||
http_port=80
|
||||
)
|
||||
|
||||
assert "Error" in result
|
||||
assert "Invalid IP" in result
|
||||
|
||||
def test_add_domain_invalid_port(self, patch_config_paths):
|
||||
"""Reject invalid port."""
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_add_domain"](
|
||||
domain="example.com",
|
||||
ip="10.0.0.1",
|
||||
http_port=70000
|
||||
)
|
||||
|
||||
assert "Error" in result
|
||||
assert "Port" in result
|
||||
|
||||
def test_add_domain_starts_with_dot(self, patch_config_paths):
|
||||
"""Reject domain starting with dot (wildcard)."""
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_add_domain"](
|
||||
domain=".example.com",
|
||||
ip="",
|
||||
http_port=80
|
||||
)
|
||||
|
||||
assert "Error" in result
|
||||
assert "cannot start with '.'" in result
|
||||
|
||||
def test_add_domain_already_exists(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Reject adding domain that already exists."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_add_domain"](
|
||||
domain="example.com",
|
||||
ip="",
|
||||
http_port=80
|
||||
)
|
||||
|
||||
assert "Error" in result
|
||||
assert "already exists" in result
|
||||
|
||||
def test_add_domain_success_without_ip(self, mock_socket_class, mock_select, patch_config_paths, response_builder, mock_subprocess):
|
||||
"""Successfully add domain without IP."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"add map": "",
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_add_domain"](
|
||||
domain="newdomain.com",
|
||||
ip="",
|
||||
http_port=80
|
||||
)
|
||||
|
||||
assert "newdomain.com" in result
|
||||
assert "pool_1" in result
|
||||
assert "no servers configured" in result
|
||||
|
||||
def test_add_domain_success_with_ip(self, mock_socket_class, mock_select, patch_config_paths, response_builder, mock_subprocess):
|
||||
"""Successfully add domain with IP."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"add map": "",
|
||||
"set server": "",
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_add_domain"](
|
||||
domain="newdomain.com",
|
||||
ip="10.0.0.1",
|
||||
http_port=8080
|
||||
)
|
||||
|
||||
assert "newdomain.com" in result
|
||||
assert "pool_1" in result
|
||||
assert "10.0.0.1:8080" in result
|
||||
|
||||
|
||||
class TestHaproxyRemoveDomain:
|
||||
"""Tests for haproxy_remove_domain tool function."""
|
||||
|
||||
def test_remove_domain_invalid_format(self, patch_config_paths):
|
||||
"""Reject invalid domain format."""
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_remove_domain"](domain="-invalid.com")
|
||||
|
||||
assert "Error" in result
|
||||
assert "Invalid domain" in result
|
||||
|
||||
def test_remove_domain_not_found(self, patch_config_paths):
|
||||
"""Reject removing domain that doesn't exist."""
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_remove_domain"](domain="nonexistent.com")
|
||||
|
||||
assert "Error" in result
|
||||
assert "not found" in result
|
||||
|
||||
def test_remove_legacy_domain_rejected(self, patch_config_paths):
|
||||
"""Reject removing legacy (non-pool) domain."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com legacy_backend\n")
|
||||
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_remove_domain"](domain="example.com")
|
||||
|
||||
assert "Error" in result
|
||||
assert "legacy" in result.lower()
|
||||
|
||||
def test_remove_domain_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Successfully remove domain."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
with open(patch_config_paths["wildcards_file"], "w") as f:
|
||||
f.write(".example.com pool_1\n")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"del map": "",
|
||||
"set server": "",
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.domains import register_domain_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_domain_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_remove_domain"](domain="example.com")
|
||||
|
||||
assert "example.com" in result
|
||||
assert "removed" in result.lower()
|
||||
|
||||
|
||||
class TestCheckCertificateCoverage:
|
||||
"""Tests for check_certificate_coverage function."""
|
||||
|
||||
def test_no_cert_directory(self, tmp_path):
|
||||
"""No certificate coverage when directory doesn't exist."""
|
||||
from haproxy_mcp.tools.domains import check_certificate_coverage
|
||||
|
||||
with patch("haproxy_mcp.tools.domains.CERTS_DIR", str(tmp_path / "nonexistent")):
|
||||
covered, info = check_certificate_coverage("example.com")
|
||||
|
||||
assert covered is False
|
||||
assert "not found" in info.lower()
|
||||
|
||||
def test_exact_cert_match(self, tmp_path):
|
||||
"""Exact certificate match."""
|
||||
from haproxy_mcp.tools.domains import check_certificate_coverage
|
||||
|
||||
certs_dir = tmp_path / "certs"
|
||||
certs_dir.mkdir()
|
||||
(certs_dir / "example.com.pem").write_text("cert content")
|
||||
|
||||
with patch("haproxy_mcp.tools.domains.CERTS_DIR", str(certs_dir)):
|
||||
covered, info = check_certificate_coverage("example.com")
|
||||
|
||||
assert covered is True
|
||||
assert info == "example.com"
|
||||
|
||||
def test_wildcard_cert_coverage(self, tmp_path, mock_subprocess):
|
||||
"""Wildcard certificate covers subdomain."""
|
||||
from haproxy_mcp.tools.domains import check_certificate_coverage
|
||||
|
||||
certs_dir = tmp_path / "certs"
|
||||
certs_dir.mkdir()
|
||||
(certs_dir / "example.com.pem").write_text("cert content")
|
||||
|
||||
# Mock openssl output showing wildcard SAN
|
||||
mock_subprocess.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout="X509v3 Subject Alternative Name:\n DNS:example.com, DNS:*.example.com"
|
||||
)
|
||||
|
||||
with patch("haproxy_mcp.tools.domains.CERTS_DIR", str(certs_dir)):
|
||||
covered, info = check_certificate_coverage("api.example.com")
|
||||
|
||||
assert covered is True
|
||||
assert "wildcard" in info
|
||||
|
||||
def test_no_matching_cert(self, tmp_path):
|
||||
"""No matching certificate."""
|
||||
from haproxy_mcp.tools.domains import check_certificate_coverage
|
||||
|
||||
certs_dir = tmp_path / "certs"
|
||||
certs_dir.mkdir()
|
||||
|
||||
with patch("haproxy_mcp.tools.domains.CERTS_DIR", str(certs_dir)):
|
||||
covered, info = check_certificate_coverage("example.com")
|
||||
|
||||
assert covered is False
|
||||
assert "No matching" in info
|
||||
433
tests/unit/tools/test_health.py
Normal file
433
tests/unit/tools/test_health.py
Normal file
@@ -0,0 +1,433 @@
|
||||
"""Unit tests for health check tools."""
|
||||
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from haproxy_mcp.exceptions import HaproxyError
|
||||
|
||||
|
||||
class TestHaproxyHealth:
|
||||
"""Tests for haproxy_health tool function."""
|
||||
|
||||
def test_health_all_ok(self, mock_socket_class, mock_select, patch_config_paths, response_builder, mock_subprocess):
|
||||
"""Health check returns healthy when all components are OK."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show info": response_builder.info(version="3.3.2", uptime=3600),
|
||||
})
|
||||
|
||||
mock_subprocess.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout="running"
|
||||
)
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result_str = registered_tools["haproxy_health"]()
|
||||
result = json.loads(result_str)
|
||||
|
||||
assert result["status"] == "healthy"
|
||||
assert result["components"]["mcp"]["status"] == "ok"
|
||||
assert result["components"]["haproxy"]["status"] == "ok"
|
||||
assert result["components"]["haproxy"]["version"] == "3.3.2"
|
||||
|
||||
def test_health_haproxy_error(self, mock_socket_class, mock_select, patch_config_paths, mock_subprocess):
|
||||
"""Health check returns degraded when HAProxy is unreachable."""
|
||||
|
||||
def raise_error(*args, **kwargs):
|
||||
raise ConnectionRefusedError()
|
||||
|
||||
with patch("socket.socket", side_effect=raise_error):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result_str = registered_tools["haproxy_health"]()
|
||||
result = json.loads(result_str)
|
||||
|
||||
assert result["status"] == "degraded"
|
||||
assert result["components"]["haproxy"]["status"] == "error"
|
||||
|
||||
def test_health_missing_config_files(self, mock_socket_class, mock_select, tmp_path, response_builder, mock_subprocess):
|
||||
"""Health check returns degraded when config files are missing."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show info": response_builder.info(),
|
||||
})
|
||||
|
||||
mock_subprocess.return_value = MagicMock(returncode=0, stdout="running")
|
||||
|
||||
# Use paths that don't exist
|
||||
with patch("haproxy_mcp.tools.health.MAP_FILE", str(tmp_path / "nonexistent.map")):
|
||||
with patch("haproxy_mcp.tools.health.SERVERS_FILE", str(tmp_path / "nonexistent.json")):
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result_str = registered_tools["haproxy_health"]()
|
||||
result = json.loads(result_str)
|
||||
|
||||
assert result["status"] == "degraded"
|
||||
assert result["components"]["config_files"]["status"] == "warning"
|
||||
|
||||
def test_health_container_not_running(self, mock_socket_class, mock_select, patch_config_paths, response_builder, mock_subprocess):
|
||||
"""Health check returns unhealthy when container is not running."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show info": response_builder.info(),
|
||||
})
|
||||
|
||||
mock_subprocess.return_value = MagicMock(
|
||||
returncode=1,
|
||||
stdout="",
|
||||
stderr="No such container"
|
||||
)
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result_str = registered_tools["haproxy_health"]()
|
||||
result = json.loads(result_str)
|
||||
|
||||
assert result["status"] == "unhealthy"
|
||||
assert result["components"]["container"]["status"] == "error"
|
||||
|
||||
|
||||
class TestHaproxyDomainHealth:
|
||||
"""Tests for haproxy_domain_health tool function."""
|
||||
|
||||
def test_domain_health_invalid_domain(self, patch_config_paths):
|
||||
"""Reject invalid domain format."""
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result_str = registered_tools["haproxy_domain_health"](domain="-invalid")
|
||||
result = json.loads(result_str)
|
||||
|
||||
assert "error" in result
|
||||
assert "Invalid domain" in result["error"]
|
||||
|
||||
def test_domain_health_healthy(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Domain health returns healthy when all servers are UP."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
|
||||
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},
|
||||
{"be_name": "pool_1", "srv_name": "pool_1_2", "srv_addr": "10.0.0.2", "srv_port": 80},
|
||||
]),
|
||||
"show stat": response_builder.stat_csv([
|
||||
{"pxname": "pool_1", "svname": "pool_1_1", "status": "UP", "check_status": "L4OK"},
|
||||
{"pxname": "pool_1", "svname": "pool_1_2", "status": "UP", "check_status": "L4OK"},
|
||||
]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result_str = registered_tools["haproxy_domain_health"](domain="example.com")
|
||||
result = json.loads(result_str)
|
||||
|
||||
assert result["status"] == "healthy"
|
||||
assert result["healthy_count"] == 2
|
||||
assert result["total_count"] == 2
|
||||
|
||||
def test_domain_health_degraded(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Domain health returns degraded when some servers are DOWN."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
|
||||
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},
|
||||
{"be_name": "pool_1", "srv_name": "pool_1_2", "srv_addr": "10.0.0.2", "srv_port": 80},
|
||||
]),
|
||||
"show stat": response_builder.stat_csv([
|
||||
{"pxname": "pool_1", "svname": "pool_1_1", "status": "UP"},
|
||||
{"pxname": "pool_1", "svname": "pool_1_2", "status": "DOWN"},
|
||||
]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result_str = registered_tools["haproxy_domain_health"](domain="example.com")
|
||||
result = json.loads(result_str)
|
||||
|
||||
assert result["status"] == "degraded"
|
||||
assert result["healthy_count"] == 1
|
||||
assert result["total_count"] == 2
|
||||
|
||||
def test_domain_health_down(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Domain health returns down when all servers are DOWN."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
|
||||
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},
|
||||
]),
|
||||
"show stat": response_builder.stat_csv([
|
||||
{"pxname": "pool_1", "svname": "pool_1_1", "status": "DOWN"},
|
||||
]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result_str = registered_tools["haproxy_domain_health"](domain="example.com")
|
||||
result = json.loads(result_str)
|
||||
|
||||
assert result["status"] == "down"
|
||||
assert result["healthy_count"] == 0
|
||||
assert result["total_count"] == 1
|
||||
|
||||
def test_domain_health_no_servers(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Domain health returns no_servers when no servers configured."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show servers state": response_builder.servers_state([
|
||||
{"be_name": "pool_1", "srv_name": "pool_1_1", "srv_addr": "0.0.0.0", "srv_port": 0},
|
||||
]),
|
||||
"show stat": response_builder.stat_csv([]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result_str = registered_tools["haproxy_domain_health"](domain="example.com")
|
||||
result = json.loads(result_str)
|
||||
|
||||
assert result["status"] == "no_servers"
|
||||
assert result["total_count"] == 0
|
||||
|
||||
|
||||
class TestHaproxyGetServerHealth:
|
||||
"""Tests for haproxy_get_server_health tool function."""
|
||||
|
||||
def test_get_server_health_invalid_backend(self, patch_config_paths):
|
||||
"""Reject invalid backend name."""
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_get_server_health"](backend="invalid@name")
|
||||
|
||||
assert "Error" in result
|
||||
assert "Invalid backend" in result
|
||||
|
||||
def test_get_server_health_all_backends(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Get health for all backends."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show stat": response_builder.stat_csv([
|
||||
{"pxname": "pool_1", "svname": "pool_1_1", "status": "UP", "weight": 1, "check_status": "L4OK"},
|
||||
{"pxname": "pool_2", "svname": "pool_2_1", "status": "DOWN", "weight": 1, "check_status": "L4TOUT"},
|
||||
]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_get_server_health"](backend="")
|
||||
|
||||
assert "pool_1" in result
|
||||
assert "pool_2" in result
|
||||
assert "UP" in result
|
||||
assert "DOWN" in result
|
||||
|
||||
def test_get_server_health_filter_backend(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Get health for specific backend."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show stat": response_builder.stat_csv([
|
||||
{"pxname": "pool_1", "svname": "pool_1_1", "status": "UP"},
|
||||
{"pxname": "pool_1", "svname": "pool_1_2", "status": "UP"},
|
||||
{"pxname": "pool_2", "svname": "pool_2_1", "status": "DOWN"},
|
||||
]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_get_server_health"](backend="pool_1")
|
||||
|
||||
assert "pool_1" in result
|
||||
assert "pool_2" not in result
|
||||
|
||||
def test_get_server_health_no_servers(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""No servers returns appropriate message."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show stat": response_builder.stat_csv([
|
||||
{"pxname": "pool_1", "svname": "FRONTEND", "status": "OPEN"},
|
||||
{"pxname": "pool_1", "svname": "BACKEND", "status": "UP"},
|
||||
]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_get_server_health"](backend="")
|
||||
|
||||
assert "No servers found" in result
|
||||
|
||||
def test_get_server_health_haproxy_error(self, mock_socket_class, mock_select, patch_config_paths):
|
||||
"""HAProxy error returns error message."""
|
||||
def raise_error(*args, **kwargs):
|
||||
raise ConnectionRefusedError()
|
||||
|
||||
with patch("socket.socket", side_effect=raise_error):
|
||||
from haproxy_mcp.tools.health import register_health_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_health_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_get_server_health"](backend="")
|
||||
|
||||
assert "Error" in result
|
||||
325
tests/unit/tools/test_monitoring.py
Normal file
325
tests/unit/tools/test_monitoring.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""Unit tests for monitoring tools."""
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestHaproxyStats:
|
||||
"""Tests for haproxy_stats tool function."""
|
||||
|
||||
def test_stats_success(self, mock_socket_class, mock_select, response_builder):
|
||||
"""Get HAProxy stats successfully."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show info": response_builder.info(version="3.3.2", uptime=3600),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_stats"]()
|
||||
|
||||
assert "Version" in result
|
||||
assert "3.3.2" in result
|
||||
|
||||
def test_stats_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.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_stats"]()
|
||||
|
||||
assert "Error" in result
|
||||
|
||||
|
||||
class TestHaproxyBackends:
|
||||
"""Tests for haproxy_backends tool function."""
|
||||
|
||||
def test_backends_success(self, mock_socket_class, mock_select):
|
||||
"""List backends successfully."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show backend": "pool_1\npool_2\npool_3",
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_backends"]()
|
||||
|
||||
assert "Backends" in result
|
||||
assert "pool_1" in result
|
||||
assert "pool_2" in result
|
||||
assert "pool_3" in result
|
||||
|
||||
def test_backends_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.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_backends"]()
|
||||
|
||||
assert "Error" in result
|
||||
|
||||
|
||||
class TestHaproxyListFrontends:
|
||||
"""Tests for haproxy_list_frontends tool function."""
|
||||
|
||||
def test_list_frontends_success(self, mock_socket_class, mock_select, response_builder):
|
||||
"""List frontends successfully."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show stat": response_builder.stat_csv([
|
||||
{"pxname": "http_front", "svname": "FRONTEND", "status": "OPEN", "scur": 10},
|
||||
{"pxname": "https_front", "svname": "FRONTEND", "status": "OPEN", "scur": 50},
|
||||
{"pxname": "pool_1", "svname": "pool_1_1", "status": "UP", "scur": 5},
|
||||
]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_list_frontends"]()
|
||||
|
||||
assert "Frontends" in result
|
||||
assert "http_front" in result
|
||||
assert "https_front" in result
|
||||
# pool_1 is not a FRONTEND
|
||||
assert "pool_1_1" not in result
|
||||
|
||||
def test_list_frontends_no_frontends(self, mock_socket_class, mock_select, response_builder):
|
||||
"""No frontends found."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show stat": response_builder.stat_csv([
|
||||
{"pxname": "pool_1", "svname": "pool_1_1", "status": "UP"},
|
||||
]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_list_frontends"]()
|
||||
|
||||
assert "No frontends found" in result
|
||||
|
||||
def test_list_frontends_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.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_list_frontends"]()
|
||||
|
||||
assert "Error" in result
|
||||
|
||||
|
||||
class TestHaproxyGetConnections:
|
||||
"""Tests for haproxy_get_connections tool function."""
|
||||
|
||||
def test_get_connections_all_backends(self, mock_socket_class, mock_select, response_builder):
|
||||
"""Get connections for all backends."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show stat": response_builder.stat_csv([
|
||||
{"pxname": "pool_1", "svname": "FRONTEND", "status": "OPEN", "scur": 10, "smax": 100},
|
||||
{"pxname": "pool_1", "svname": "pool_1_1", "status": "UP", "scur": 5},
|
||||
{"pxname": "pool_1", "svname": "BACKEND", "status": "UP", "scur": 10, "smax": 100},
|
||||
]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_get_connections"](backend="")
|
||||
|
||||
assert "pool_1" in result
|
||||
assert "FRONTEND" in result or "BACKEND" in result
|
||||
assert "connections" in result
|
||||
|
||||
def test_get_connections_filter_backend(self, mock_socket_class, mock_select, response_builder):
|
||||
"""Filter connections by backend."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show stat": response_builder.stat_csv([
|
||||
{"pxname": "pool_1", "svname": "FRONTEND", "status": "OPEN", "scur": 10, "smax": 100},
|
||||
{"pxname": "pool_2", "svname": "FRONTEND", "status": "OPEN", "scur": 20, "smax": 200},
|
||||
]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_get_connections"](backend="pool_1")
|
||||
|
||||
assert "pool_1" in result
|
||||
assert "pool_2" not in result
|
||||
|
||||
def test_get_connections_invalid_backend(self):
|
||||
"""Reject invalid backend name."""
|
||||
from haproxy_mcp.tools.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_get_connections"](backend="invalid@name")
|
||||
|
||||
assert "Error" in result
|
||||
assert "Invalid backend" in result
|
||||
|
||||
def test_get_connections_no_data(self, mock_socket_class, mock_select, response_builder):
|
||||
"""No connection data found."""
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show stat": response_builder.stat_csv([]),
|
||||
})
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_get_connections"](backend="")
|
||||
|
||||
assert "No connection data" in result
|
||||
|
||||
def test_get_connections_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.monitoring import register_monitoring_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_monitoring_tools(mcp)
|
||||
|
||||
result = registered_tools["haproxy_get_connections"](backend="")
|
||||
|
||||
assert "Error" in result
|
||||
1350
tests/unit/tools/test_servers.py
Normal file
1350
tests/unit/tools/test_servers.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user