refactor: migrate data storage from JSON/map files to SQLite

Replace servers.json, certificates.json, and map file parsing with
SQLite (WAL mode) as single source of truth. HAProxy map files are
now generated from SQLite via sync_map_files().

Key changes:
- Add db.py with schema, connection management, and JSON migration
- Add DB_FILE config constant
- Delegate file_ops.py functions to db.py
- Refactor domains.py to use file_ops instead of direct list manipulation
- Fix subprocess.TimeoutExpired not caught (doesn't inherit TimeoutError)
- Add DB health check in health.py
- Init DB on startup in server.py and __main__.py
- Update all 359 tests to use SQLite-backed functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-08 11:07:29 +09:00
parent 05bff61b85
commit cf554f3f89
19 changed files with 1525 additions and 564 deletions

View File

@@ -6,6 +6,8 @@ from unittest.mock import patch, MagicMock
import pytest
from haproxy_mcp.file_ops import add_cert_to_config
class TestGetPemPaths:
"""Tests for get_pem_paths function."""
@@ -127,8 +129,7 @@ class TestRestoreCertificates:
def test_restore_certificates_success(self, patch_config_paths, tmp_path, mock_socket_class, mock_select):
"""Restore certificates successfully."""
# Save config
with open(patch_config_paths["certs_file"], "w") as f:
json.dump({"domains": ["example.com"]}, f)
add_cert_to_config("example.com")
# Create PEM
certs_dir = tmp_path / "certs"
@@ -283,11 +284,17 @@ class TestHaproxyCertInfo:
pem_file = tmp_path / "example.com.pem"
pem_file.write_text("cert content")
mock_subprocess.return_value = MagicMock(
returncode=0,
stdout="subject=CN = example.com\nissuer=CN = Google Trust Services\nnotBefore=Jan 1 00:00:00 2024 GMT\nnotAfter=Apr 1 00:00:00 2024 GMT",
stderr=""
)
def subprocess_side_effect(*args, **kwargs):
cmd = args[0] if args else kwargs.get("args", [])
if isinstance(cmd, list) and "stat" in cmd:
return MagicMock(returncode=0, stdout="1704067200", stderr="")
return MagicMock(
returncode=0,
stdout="subject=CN = example.com\nissuer=CN = Google Trust Services\nnotBefore=Jan 1 00:00:00 2024 GMT\nnotAfter=Apr 1 00:00:00 2024 GMT",
stderr=""
)
mock_subprocess.side_effect = subprocess_side_effect
mock_sock = mock_socket_class(responses={
"show ssl cert": "/etc/haproxy/certs/example.com.pem",
@@ -337,25 +344,33 @@ class TestHaproxyIssueCert:
assert "Error" in result
assert "Invalid domain" in result
def test_issue_cert_no_cf_token(self, tmp_path):
def test_issue_cert_no_cf_token(self, tmp_path, mock_subprocess):
"""Fail when CF_Token is not set."""
acme_sh = str(tmp_path / "acme.sh")
mock_subprocess.return_value = MagicMock(
returncode=1,
stdout="",
stderr="CF_Token is not set. Please export CF_Token environment variable.",
)
with patch.dict(os.environ, {}, clear=True):
with patch("haproxy_mcp.tools.certificates.ACME_HOME", str(tmp_path)):
with patch("os.path.exists", return_value=False):
from haproxy_mcp.tools.certificates import register_certificate_tools
mcp = MagicMock()
registered_tools = {}
with patch("haproxy_mcp.tools.certificates.ACME_SH", acme_sh):
with patch("os.path.exists", return_value=False):
from haproxy_mcp.tools.certificates import register_certificate_tools
mcp = MagicMock()
registered_tools = {}
def capture_tool():
def decorator(func):
registered_tools[func.__name__] = func
return func
return decorator
def capture_tool():
def decorator(func):
registered_tools[func.__name__] = func
return func
return decorator
mcp.tool = capture_tool
register_certificate_tools(mcp)
mcp.tool = capture_tool
register_certificate_tools(mcp)
result = registered_tools["haproxy_issue_cert"](domain="example.com", wildcard=True)
result = registered_tools["haproxy_issue_cert"](domain="example.com", wildcard=True)
assert "CF_Token" in result
@@ -845,8 +860,8 @@ class TestHaproxyRenewAllCertsMultiple:
def test_renew_all_certs_multiple_renewals(self, mock_subprocess, mock_socket_class, mock_select, patch_config_paths, tmp_path):
"""Renew multiple certificates successfully."""
# Write config with multiple domains
with open(patch_config_paths["certs_file"], "w") as f:
json.dump({"domains": ["example.com", "example.org"]}, f)
add_cert_to_config("example.com")
add_cert_to_config("example.org")
# Create PEM files
certs_dir = tmp_path / "certs"
@@ -1038,30 +1053,32 @@ class TestHaproxyDeleteCertPartialFailure:
"show ssl cert": "", # Not loaded
})
# Mock os.remove to fail
def mock_remove(path):
if "example.com.pem" in str(path):
raise PermissionError("Permission denied")
raise FileNotFoundError()
# Mock subprocess to succeed for acme.sh remove but fail for rm (PEM removal)
def subprocess_side_effect(*args, **kwargs):
cmd = args[0] if args else kwargs.get("args", [])
if isinstance(cmd, list) and cmd[0] == "rm":
return MagicMock(returncode=1, stdout="", stderr="Permission denied")
return MagicMock(returncode=0, stdout="", stderr="")
mock_subprocess.side_effect = subprocess_side_effect
with patch("haproxy_mcp.tools.certificates.ACME_HOME", str(tmp_path / "acme")):
with patch("haproxy_mcp.tools.certificates.CERTS_DIR", str(certs_dir)):
with patch("socket.socket", return_value=mock_sock):
with patch("os.remove", side_effect=mock_remove):
from haproxy_mcp.tools.certificates import register_certificate_tools
mcp = MagicMock()
registered_tools = {}
from haproxy_mcp.tools.certificates import register_certificate_tools
mcp = MagicMock()
registered_tools = {}
def capture_tool():
def decorator(func):
registered_tools[func.__name__] = func
return func
return decorator
def capture_tool():
def decorator(func):
registered_tools[func.__name__] = func
return func
return decorator
mcp.tool = capture_tool
register_certificate_tools(mcp)
mcp.tool = capture_tool
register_certificate_tools(mcp)
result = registered_tools["haproxy_delete_cert"](domain="example.com")
result = registered_tools["haproxy_delete_cert"](domain="example.com")
# Should report partial success (acme.sh deleted) and error (PEM failed)
assert "Deleted" in result
@@ -1118,8 +1135,8 @@ class TestRestoreCertificatesFailure:
def test_restore_certificates_partial_failure(self, patch_config_paths, tmp_path, mock_socket_class, mock_select):
"""Handle partial failure when restoring certificates."""
# Save config with multiple domains
with open(patch_config_paths["certs_file"], "w") as f:
json.dump({"domains": ["example.com", "missing.com"]}, f)
add_cert_to_config("example.com")
add_cert_to_config("missing.com")
# Create only one PEM file
certs_dir = tmp_path / "certs"

View File

@@ -5,6 +5,8 @@ from unittest.mock import patch, MagicMock
import pytest
from haproxy_mcp.file_ops import add_domain_to_map, add_server_to_config
class TestRestoreServersFromConfig:
"""Tests for restore_servers_from_config function."""
@@ -19,12 +21,12 @@ class TestRestoreServersFromConfig:
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")
# Add domains and servers to database
add_domain_to_map("example.com", "pool_1")
add_server_to_config("example.com", 1, "10.0.0.1", 80)
add_server_to_config("example.com", 2, "10.0.0.2", 80)
add_domain_to_map("api.example.com", "pool_2")
add_server_to_config("api.example.com", 1, "10.0.0.10", 8080)
mock_sock = mock_socket_class(responses={
"set server": "",
@@ -40,9 +42,8 @@ class TestRestoreServersFromConfig:
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)
# Add server for unknown.com but no map entry (simulates missing domain)
add_server_to_config("unknown.com", 1, "10.0.0.1", 80)
mock_sock = mock_socket_class(responses={"set server": ""})
@@ -55,11 +56,9 @@ class TestRestoreServersFromConfig:
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")
# Add domain to map and server with empty IP (will be skipped during restore)
add_domain_to_map("example.com", "pool_1")
add_server_to_config("example.com", 1, "", 80)
mock_sock = mock_socket_class(responses={"set server": ""})
@@ -321,11 +320,12 @@ class TestHaproxyRestoreState:
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")
# Add domains and servers to database
add_domain_to_map("example.com", "pool_1")
add_server_to_config("example.com", 1, "10.0.0.1", 80)
add_server_to_config("example.com", 2, "10.0.0.2", 80)
add_domain_to_map("api.example.com", "pool_2")
add_server_to_config("api.example.com", 1, "10.0.0.10", 8080)
mock_sock = mock_socket_class(responses={"set server": ""})
@@ -373,17 +373,10 @@ class TestRestoreServersFromConfigBatchFailure:
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")
# Add domain and servers to database
add_domain_to_map("example.com", "pool_1")
add_server_to_config("example.com", 1, "10.0.0.1", 80)
add_server_to_config("example.com", 2, "10.0.0.2", 80)
# Track call count to simulate batch failure then individual success
call_count = [0]
@@ -457,51 +450,51 @@ class TestRestoreServersFromConfigBatchFailure:
def test_restore_servers_invalid_slot(self, mock_socket_class, mock_select, patch_config_paths):
"""Skip servers with invalid slot number."""
# Add domain to map
add_domain_to_map("example.com", "pool_1")
# Mock load_servers_config to return config with invalid slot
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")
with patch("haproxy_mcp.tools.configuration.load_servers_config", return_value=config):
mock_sock = mock_socket_class(responses={"set server": ""})
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
with patch("socket.socket", return_value=mock_sock):
from haproxy_mcp.tools.configuration import restore_servers_from_config
result = restore_servers_from_config()
result = restore_servers_from_config()
# Should only restore the valid server
assert result == 1
# 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
# Add domain to map
add_domain_to_map("example.com", "pool_1")
# Mock load_servers_config to return config with invalid port
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")
with patch("haproxy_mcp.tools.configuration.load_servers_config", return_value=config):
mock_sock = mock_socket_class(responses={"set server": ""})
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
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()
result = restore_servers_from_config()
# Should only restore the valid server
assert result == 1
# Should only restore the valid server
assert result == 1
class TestStartupRestoreFailures:
@@ -658,11 +651,12 @@ class TestHaproxyRestoreStateFailures:
"""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")
# Add domains and servers to database
add_domain_to_map("example.com", "pool_1")
add_server_to_config("example.com", 1, "10.0.0.1", 80)
add_server_to_config("example.com", 2, "10.0.0.2", 80)
add_domain_to_map("api.example.com", "pool_2")
add_server_to_config("api.example.com", 1, "10.0.0.10", 8080)
with patch("haproxy_mcp.tools.configuration.restore_servers_from_config", side_effect=HaproxyError("Connection refused")):
from haproxy_mcp.tools.configuration import register_config_tools

View File

@@ -6,6 +6,7 @@ from unittest.mock import patch, MagicMock
import pytest
from haproxy_mcp.exceptions import HaproxyError
from haproxy_mcp.file_ops import add_domain_to_map
class TestHaproxyListDomains:
@@ -38,9 +39,8 @@ class TestHaproxyListDomains:
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")
# Add domain to DB
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([
@@ -70,10 +70,8 @@ class TestHaproxyListDomains:
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")
add_domain_to_map("example.com", "pool_1")
add_domain_to_map(".example.com", "pool_1", is_wildcard=True)
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([]),
@@ -100,10 +98,8 @@ class TestHaproxyListDomains:
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")
add_domain_to_map("example.com", "pool_1")
add_domain_to_map(".example.com", "pool_1", is_wildcard=True)
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([]),
@@ -230,8 +226,7 @@ class TestHaproxyAddDomain:
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")
add_domain_to_map("example.com", "pool_1")
from haproxy_mcp.tools.domains import register_domain_tools
mcp = MagicMock()
@@ -362,8 +357,7 @@ class TestHaproxyRemoveDomain:
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")
add_domain_to_map("example.com", "legacy_backend")
from haproxy_mcp.tools.domains import register_domain_tools
mcp = MagicMock()
@@ -385,10 +379,8 @@ class TestHaproxyRemoveDomain:
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")
add_domain_to_map("example.com", "pool_1")
add_domain_to_map(".example.com", "pool_1", is_wildcard=True)
mock_sock = mock_socket_class(responses={
"del map": "",

View File

@@ -6,6 +6,7 @@ from unittest.mock import patch, MagicMock
import pytest
from haproxy_mcp.exceptions import HaproxyError
from haproxy_mcp.file_ops import add_domain_to_map
class TestHaproxyHealth:
@@ -80,7 +81,7 @@ class TestHaproxyHealth:
# 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("haproxy_mcp.tools.health.DB_FILE", str(tmp_path / "nonexistent.db")):
with patch("socket.socket", return_value=mock_sock):
from haproxy_mcp.tools.health import register_health_tools
mcp = MagicMock()
@@ -160,8 +161,7 @@ class TestHaproxyDomainHealth:
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")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([
@@ -197,8 +197,7 @@ class TestHaproxyDomainHealth:
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")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([
@@ -234,8 +233,7 @@ class TestHaproxyDomainHealth:
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")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([
@@ -269,8 +267,7 @@ class TestHaproxyDomainHealth:
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")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([

View File

@@ -6,6 +6,7 @@ from unittest.mock import patch, MagicMock
import pytest
from haproxy_mcp.exceptions import HaproxyError
from haproxy_mcp.file_ops import add_domain_to_map, load_servers_config
class TestHaproxyListServers:
@@ -33,8 +34,7 @@ class TestHaproxyListServers:
def test_list_servers_empty_backend(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""List servers for domain with no servers."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([
@@ -63,8 +63,7 @@ class TestHaproxyListServers:
def test_list_servers_with_active_servers(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""List servers with active servers."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([
@@ -224,8 +223,7 @@ class TestHaproxyAddServer:
def test_add_server_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""Successfully add server."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"set server": "",
@@ -258,8 +256,7 @@ class TestHaproxyAddServer:
def test_add_server_auto_slot(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""Auto-select slot when slot=0."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([
@@ -413,8 +410,7 @@ class TestHaproxyAddServers:
def test_add_servers_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""Successfully add multiple servers."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"set server": "",
@@ -495,8 +491,7 @@ class TestHaproxyRemoveServer:
def test_remove_server_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""Successfully remove server."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"set server": "",
@@ -689,8 +684,7 @@ class TestHaproxyAddServersRollback:
def test_add_servers_partial_failure_rollback(self, mock_socket_class, mock_select, patch_config_paths):
"""Rollback only failed slots when HAProxy error occurs."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
# Mock configure_server_slot to fail on second slot
call_count = [0]
@@ -735,18 +729,16 @@ class TestHaproxyAddServersRollback:
assert "slot 2" in result # Failed
# Verify servers.json only has successfully added server
with open(patch_config_paths["servers_file"], "r") as f:
config = json.load(f)
config = load_servers_config()
assert "example.com" in config
assert "1" in config["example.com"] # Successfully added stays
assert "2" not in config["example.com"] # Failed one was rolled back
assert "2" not in config.get("example.com", {}) # Failed one was rolled back
def test_add_servers_unexpected_error_rollback_only_successful(
self, mock_socket_class, mock_select, patch_config_paths
):
"""Rollback only successfully added servers on unexpected error."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
# Track which servers were configured
configured_slots = []
@@ -793,8 +785,7 @@ class TestHaproxyAddServersRollback:
assert "Unexpected system error" in result
# Verify servers.json is empty (all rolled back)
with open(patch_config_paths["servers_file"], "r") as f:
config = json.load(f)
config = load_servers_config()
assert config == {} or "example.com" not in config or config.get("example.com") == {}
def test_add_servers_rollback_failure_logged(
@@ -802,8 +793,7 @@ class TestHaproxyAddServersRollback:
):
"""Log rollback failures during error recovery."""
import logging
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
def mock_configure_server_slot(backend, server_prefix, slot, ip, http_port):
if slot == 2:
@@ -858,8 +848,7 @@ class TestHaproxyAddServerAutoSlot:
def test_add_server_auto_slot_all_used(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""Auto-select slot fails when all slots are in use."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
# Build response with all 10 slots used
servers = []
@@ -902,8 +891,7 @@ class TestHaproxyAddServerAutoSlot:
def test_add_server_negative_slot_auto_select(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""Negative slot number triggers auto-selection."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([
@@ -970,8 +958,7 @@ class TestHaproxyWaitDrain:
def test_wait_drain_success(self, patch_config_paths):
"""Successfully wait for connections to drain."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
# Mock haproxy_cmd to return 0 connections
with patch("haproxy_mcp.tools.servers.haproxy_cmd") as mock_cmd:
@@ -1000,8 +987,7 @@ class TestHaproxyWaitDrain:
def test_wait_drain_timeout(self, patch_config_paths):
"""Timeout when connections don't drain."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
time_values = [0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0] # Simulate time passing
time_iter = iter(time_values)
@@ -1087,9 +1073,6 @@ class TestHaproxyWaitDrain:
def test_wait_drain_domain_not_found(self, mock_socket_class, mock_select, patch_config_paths):
"""Error when domain not found in map."""
# Empty map file - domain not configured
with open(patch_config_paths["map_file"], "w") as f:
f.write("")
from haproxy_mcp.tools.servers import register_server_tools
mcp = MagicMock()
registered_tools = {}
@@ -1202,8 +1185,7 @@ class TestHaproxySetDomainState:
def test_set_domain_state_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""Set all servers of a domain to a state."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([
@@ -1283,8 +1265,7 @@ class TestHaproxySetDomainState:
def test_set_domain_state_no_active_servers(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""No active servers found for domain."""
with open(patch_config_paths["map_file"], "w") as f:
f.write("example.com pool_1\n")
add_domain_to_map("example.com", "pool_1")
# All servers have 0.0.0.0 address (not configured)
mock_sock = mock_socket_class(responses={
@@ -1318,9 +1299,6 @@ class TestHaproxySetDomainState:
def test_set_domain_state_domain_not_found(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
"""Handle domain not found in map - shows no active servers."""
# Empty map file
with open(patch_config_paths["map_file"], "w") as f:
f.write("")
# Mock should show no servers for unknown domain's backend
mock_sock = mock_socket_class(responses={
"show servers state": response_builder.servers_state([]),