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:
198
tests/unit/test_config.py
Normal file
198
tests/unit/test_config.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Unit tests for config module."""
|
||||
|
||||
import os
|
||||
import re
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from haproxy_mcp.config import (
|
||||
DOMAIN_PATTERN,
|
||||
BACKEND_NAME_PATTERN,
|
||||
NON_ALNUM_PATTERN,
|
||||
StatField,
|
||||
StateField,
|
||||
POOL_COUNT,
|
||||
MAX_SLOTS,
|
||||
MAX_RESPONSE_SIZE,
|
||||
SOCKET_TIMEOUT,
|
||||
MAX_BULK_SERVERS,
|
||||
)
|
||||
|
||||
|
||||
class TestDomainPattern:
|
||||
"""Tests for DOMAIN_PATTERN regex."""
|
||||
|
||||
def test_simple_domain(self):
|
||||
"""Match simple domain."""
|
||||
assert DOMAIN_PATTERN.match("example.com") is not None
|
||||
|
||||
def test_subdomain(self):
|
||||
"""Match subdomain."""
|
||||
assert DOMAIN_PATTERN.match("api.example.com") is not None
|
||||
|
||||
def test_deep_subdomain(self):
|
||||
"""Match deep subdomain."""
|
||||
assert DOMAIN_PATTERN.match("a.b.c.d.example.com") is not None
|
||||
|
||||
def test_hyphenated_domain(self):
|
||||
"""Match domain with hyphens."""
|
||||
assert DOMAIN_PATTERN.match("my-api.example-site.com") is not None
|
||||
|
||||
def test_numeric_labels(self):
|
||||
"""Match domain with numeric labels."""
|
||||
assert DOMAIN_PATTERN.match("api123.example.com") is not None
|
||||
|
||||
def test_invalid_starts_with_hyphen(self):
|
||||
"""Reject domain starting with hyphen."""
|
||||
assert DOMAIN_PATTERN.match("-example.com") is None
|
||||
|
||||
def test_invalid_ends_with_hyphen(self):
|
||||
"""Reject label ending with hyphen."""
|
||||
assert DOMAIN_PATTERN.match("example-.com") is None
|
||||
|
||||
def test_invalid_underscore(self):
|
||||
"""Reject domain with underscore."""
|
||||
assert DOMAIN_PATTERN.match("my_api.example.com") is None
|
||||
|
||||
|
||||
class TestBackendNamePattern:
|
||||
"""Tests for BACKEND_NAME_PATTERN regex."""
|
||||
|
||||
def test_pool_name(self):
|
||||
"""Match pool backend names."""
|
||||
assert BACKEND_NAME_PATTERN.match("pool_1") is not None
|
||||
assert BACKEND_NAME_PATTERN.match("pool_100") is not None
|
||||
|
||||
def test_alphanumeric(self):
|
||||
"""Match alphanumeric names."""
|
||||
assert BACKEND_NAME_PATTERN.match("backend123") is not None
|
||||
|
||||
def test_underscore(self):
|
||||
"""Match names with underscores."""
|
||||
assert BACKEND_NAME_PATTERN.match("my_backend") is not None
|
||||
|
||||
def test_hyphen(self):
|
||||
"""Match names with hyphens."""
|
||||
assert BACKEND_NAME_PATTERN.match("my-backend") is not None
|
||||
|
||||
def test_mixed(self):
|
||||
"""Match mixed character names."""
|
||||
assert BACKEND_NAME_PATTERN.match("api_example-com_backend") is not None
|
||||
|
||||
def test_invalid_dot(self):
|
||||
"""Reject names with dots."""
|
||||
assert BACKEND_NAME_PATTERN.match("my.backend") is None
|
||||
|
||||
def test_invalid_special_chars(self):
|
||||
"""Reject names with special characters."""
|
||||
assert BACKEND_NAME_PATTERN.match("my@backend") is None
|
||||
assert BACKEND_NAME_PATTERN.match("my/backend") is None
|
||||
|
||||
|
||||
class TestNonAlnumPattern:
|
||||
"""Tests for NON_ALNUM_PATTERN regex."""
|
||||
|
||||
def test_replace_dots(self):
|
||||
"""Replace dots."""
|
||||
result = NON_ALNUM_PATTERN.sub("_", "example.com")
|
||||
assert result == "example_com"
|
||||
|
||||
def test_replace_hyphens(self):
|
||||
"""Replace hyphens."""
|
||||
result = NON_ALNUM_PATTERN.sub("_", "my-api")
|
||||
assert result == "my_api"
|
||||
|
||||
def test_preserve_alphanumeric(self):
|
||||
"""Preserve alphanumeric characters."""
|
||||
result = NON_ALNUM_PATTERN.sub("_", "abc123")
|
||||
assert result == "abc123"
|
||||
|
||||
def test_complex_replacement(self):
|
||||
"""Complex domain replacement."""
|
||||
result = NON_ALNUM_PATTERN.sub("_", "api.my-site.example.com")
|
||||
assert result == "api_my_site_example_com"
|
||||
|
||||
|
||||
class TestStatField:
|
||||
"""Tests for StatField constants."""
|
||||
|
||||
def test_field_indices(self):
|
||||
"""Verify stat field indices."""
|
||||
assert StatField.PXNAME == 0
|
||||
assert StatField.SVNAME == 1
|
||||
assert StatField.SCUR == 4
|
||||
assert StatField.SMAX == 6
|
||||
assert StatField.STATUS == 17
|
||||
assert StatField.WEIGHT == 18
|
||||
assert StatField.CHECK_STATUS == 36
|
||||
|
||||
|
||||
class TestStateField:
|
||||
"""Tests for StateField constants."""
|
||||
|
||||
def test_field_indices(self):
|
||||
"""Verify state field indices."""
|
||||
assert StateField.BE_ID == 0
|
||||
assert StateField.BE_NAME == 1
|
||||
assert StateField.SRV_ID == 2
|
||||
assert StateField.SRV_NAME == 3
|
||||
assert StateField.SRV_ADDR == 4
|
||||
assert StateField.SRV_OP_STATE == 5
|
||||
assert StateField.SRV_ADMIN_STATE == 6
|
||||
assert StateField.SRV_PORT == 18
|
||||
|
||||
|
||||
class TestConfigConstants:
|
||||
"""Tests for configuration constants."""
|
||||
|
||||
def test_pool_count(self):
|
||||
"""Pool count has expected value."""
|
||||
assert POOL_COUNT == 100
|
||||
|
||||
def test_max_slots(self):
|
||||
"""Max slots has expected value."""
|
||||
assert MAX_SLOTS == 10
|
||||
|
||||
def test_max_response_size(self):
|
||||
"""Max response size is reasonable."""
|
||||
assert MAX_RESPONSE_SIZE == 10 * 1024 * 1024 # 10 MB
|
||||
|
||||
def test_socket_timeout(self):
|
||||
"""Socket timeout is reasonable."""
|
||||
assert SOCKET_TIMEOUT == 5
|
||||
|
||||
def test_max_bulk_servers(self):
|
||||
"""Max bulk servers is reasonable."""
|
||||
assert MAX_BULK_SERVERS == 10
|
||||
|
||||
|
||||
class TestEnvironmentVariables:
|
||||
"""Tests for environment variable configuration."""
|
||||
|
||||
def test_default_mcp_host(self):
|
||||
"""Default MCP host is 0.0.0.0."""
|
||||
# Import fresh to get defaults
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
# Re-import to test defaults
|
||||
from importlib import reload
|
||||
import haproxy_mcp.config as config
|
||||
reload(config)
|
||||
# Note: Due to Python's module caching, this test verifies the
|
||||
# default values are what we expect from the source code
|
||||
assert config.MCP_HOST == "0.0.0.0"
|
||||
|
||||
def test_default_mcp_port(self):
|
||||
"""Default MCP port is 8000."""
|
||||
from haproxy_mcp.config import MCP_PORT
|
||||
assert MCP_PORT == 8000
|
||||
|
||||
def test_default_haproxy_host(self):
|
||||
"""Default HAProxy host is localhost."""
|
||||
from haproxy_mcp.config import HAPROXY_HOST
|
||||
assert HAPROXY_HOST == "localhost"
|
||||
|
||||
def test_default_haproxy_port(self):
|
||||
"""Default HAProxy port is 9999."""
|
||||
from haproxy_mcp.config import HAPROXY_PORT
|
||||
assert HAPROXY_PORT == 9999
|
||||
Reference in New Issue
Block a user