Add unit and integration tests for Certbot and HAProxy configuration
- Added integration tests to validate HAProxy configuration generation for Certbot's HTTP-01 challenges. - Verified handling of ACLs, SSL redirects, multi-domain setups, and backend behaviors. - Introduced unit tests for the Certbot class to ensure proper ACME server handling, certificate status checks, and configuration validation. - Improved test coverage for edge cases, including EAB credentials, manual hooks, and custom ports. - Enhanced certbot-related environment variable parsing and error scenarios.
This commit is contained in:
parent
ece012a884
commit
2e45c3f2e5
2 changed files with 1179 additions and 0 deletions
414
tests/test_certbot_haproxy_config.py
Normal file
414
tests/test_certbot_haproxy_config.py
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
"""
|
||||
Integration tests for Certbot/ACME HAProxy configuration generation
|
||||
|
||||
Tests that verify the HAProxy configuration is correctly generated for HTTP-01 challenges:
|
||||
- ACLs for /.well-known/acme-challenge/ paths
|
||||
- certbot_backend routing to 127.0.0.1:2080
|
||||
- ACME challenges bypass SSL redirect
|
||||
- Multiple domains with certbot enabled
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
# Add src to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from easymapping import HaproxyConfigGenerator
|
||||
from functions import ContainerEnv
|
||||
|
||||
|
||||
class TestCertbotHAProxyConfig:
|
||||
"""Test HAProxy configuration generation for ACME/certbot"""
|
||||
|
||||
def test_certbot_backend_always_created(self):
|
||||
"""Test that certbot_backend is always present in HAProxy config"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
# Empty config should still have certbot_backend
|
||||
haproxy_config = cfg.generate({})
|
||||
|
||||
assert 'backend certbot_backend' in haproxy_config
|
||||
assert 'server certbot 127.0.0.1:2080' in haproxy_config
|
||||
|
||||
def test_certbot_acl_for_single_domain(self):
|
||||
"""Test ACME challenge ACL for single domain with certbot enabled"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.http.host': 'example.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '3000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
}
|
||||
}
|
||||
|
||||
haproxy_config = cfg.generate(parsed_object)
|
||||
|
||||
# Verify ACME challenge ACL
|
||||
assert 'acl is_certbot_example_com_80 path_beg /.well-known/acme-challenge/' in haproxy_config
|
||||
|
||||
# Verify routing to certbot_backend
|
||||
assert 'use_backend certbot_backend if is_certbot_example_com_80' in haproxy_config
|
||||
|
||||
# Verify certbot_backend exists
|
||||
assert 'backend certbot_backend' in haproxy_config
|
||||
assert 'server certbot 127.0.0.1:2080' in haproxy_config
|
||||
|
||||
def test_certbot_acl_for_multiple_domains(self):
|
||||
"""Test ACME challenge ACLs for multiple domains with certbot enabled"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.http.host': 'example.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '3000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
},
|
||||
'container2': {
|
||||
'easyhaproxy.http.host': 'test.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '4000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
},
|
||||
'container3': {
|
||||
'easyhaproxy.http.host': 'nocert.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '5000',
|
||||
# certbot not enabled
|
||||
}
|
||||
}
|
||||
|
||||
haproxy_config = cfg.generate(parsed_object)
|
||||
|
||||
# Verify ACLs for domains with certbot=true
|
||||
assert 'acl is_certbot_example_com_80 path_beg /.well-known/acme-challenge/' in haproxy_config
|
||||
assert 'acl is_certbot_test_com_80 path_beg /.well-known/acme-challenge/' in haproxy_config
|
||||
|
||||
# Verify NO ACL for domain without certbot
|
||||
assert 'acl is_certbot_nocert_com_80' not in haproxy_config
|
||||
|
||||
# Verify routing for each certbot-enabled domain
|
||||
assert 'use_backend certbot_backend if is_certbot_example_com_80' in haproxy_config
|
||||
assert 'use_backend certbot_backend if is_certbot_test_com_80' in haproxy_config
|
||||
|
||||
# Verify certbot_backend definition exists (only once)
|
||||
# Count lines starting with "backend certbot_backend" (not use_backend lines)
|
||||
backend_lines = [line for line in haproxy_config.split('\n') if line.startswith('backend certbot_backend')]
|
||||
assert len(backend_lines) == 1
|
||||
assert haproxy_config.count('server certbot 127.0.0.1:2080') == 1
|
||||
|
||||
def test_certbot_bypasses_ssl_redirect(self):
|
||||
"""Test that ACME challenges bypass SSL redirect"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.http.host': 'example.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '3000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
'easyhaproxy.http.redirect_ssl': 'true', # Force HTTPS
|
||||
}
|
||||
}
|
||||
|
||||
haproxy_config = cfg.generate(parsed_object)
|
||||
|
||||
# Find the redirect rule
|
||||
lines = haproxy_config.split('\n')
|
||||
redirect_line = None
|
||||
for line in lines:
|
||||
if 'http-request redirect scheme https' in line and 'example_com' in line:
|
||||
redirect_line = line
|
||||
break
|
||||
|
||||
assert redirect_line is not None, "SSL redirect rule not found"
|
||||
|
||||
# Verify ACME challenge is excluded from redirect
|
||||
# Should contain: if !is_certbot_example_com_80 is_rule_...
|
||||
assert '!is_certbot_example_com_80' in redirect_line
|
||||
|
||||
def test_certbot_with_ssl_clone(self):
|
||||
"""Test certbot with clone_to_ssl (auto-create port 443)"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.http.host': 'example.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '3000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
'easyhaproxy.http.clone_to_ssl': 'true',
|
||||
}
|
||||
}
|
||||
|
||||
haproxy_config = cfg.generate(parsed_object)
|
||||
|
||||
# Should have HTTP frontend (port 80) with certbot ACL
|
||||
assert 'frontend http_in_80' in haproxy_config
|
||||
assert 'acl is_certbot_example_com_80 path_beg /.well-known/acme-challenge/' in haproxy_config
|
||||
|
||||
# Should have HTTPS frontend (port 443) without certbot ACL
|
||||
# (ACME challenges only happen on HTTP port 80)
|
||||
assert 'frontend http_in_443' in haproxy_config or 'frontend https_in_443' in haproxy_config
|
||||
|
||||
# Port 443 should NOT have certbot ACL
|
||||
lines = haproxy_config.split('\n')
|
||||
in_443_frontend = False
|
||||
for line in lines:
|
||||
if 'frontend http_in_443' in line or 'frontend https_in_443' in line:
|
||||
in_443_frontend = True
|
||||
if in_443_frontend and 'frontend' in line and '443' not in line:
|
||||
break # Moved to next frontend
|
||||
if in_443_frontend and 'is_certbot' in line:
|
||||
assert False, "ACME challenge ACL should not be in port 443 frontend"
|
||||
|
||||
def test_certbot_without_email_no_acl(self):
|
||||
"""Test that no ACME ACLs are created when email is not configured"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': '', # No email
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.http.host': 'example.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '3000',
|
||||
'easyhaproxy.http.certbot': 'true', # Set but won't work without email
|
||||
}
|
||||
}
|
||||
|
||||
haproxy_config = cfg.generate(parsed_object)
|
||||
|
||||
# Should NOT create ACME ACL without email
|
||||
assert 'acl is_certbot_example_com_80' not in haproxy_config
|
||||
assert 'use_backend certbot_backend' not in haproxy_config
|
||||
|
||||
# certbot_backend should still exist (always created)
|
||||
assert 'backend certbot_backend' in haproxy_config
|
||||
|
||||
def test_certbot_acl_naming_special_chars(self):
|
||||
"""Test ACME ACL naming with domains containing special characters"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.http.host': 'sub-domain.example.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '3000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
}
|
||||
}
|
||||
|
||||
haproxy_config = cfg.generate(parsed_object)
|
||||
|
||||
# Domain with dots should have them replaced with underscores in ACL name
|
||||
assert 'acl is_certbot_sub-domain_example_com_80' in haproxy_config
|
||||
assert 'use_backend certbot_backend if is_certbot_sub-domain_example_com_80' in haproxy_config
|
||||
|
||||
def test_certbot_get_certbot_hosts(self):
|
||||
"""Test that get_certbot_hosts returns list of domains with certbot enabled"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.http.host': 'example.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '3000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
},
|
||||
'container2': {
|
||||
'easyhaproxy.http.host': 'test.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '4000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
},
|
||||
'container3': {
|
||||
'easyhaproxy.http.host': 'nocert.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '5000',
|
||||
}
|
||||
}
|
||||
|
||||
cfg.generate(parsed_object)
|
||||
|
||||
# Should return list of hosts with certbot=true
|
||||
certbot_hosts = cfg.certbot_hosts
|
||||
assert 'example.com' in certbot_hosts
|
||||
assert 'test.com' in certbot_hosts
|
||||
assert 'nocert.com' not in certbot_hosts
|
||||
|
||||
def test_certbot_port_must_be_80(self):
|
||||
"""Test that certbot only works on port 80 (HTTP-01 requirement)"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
# Try certbot on port 8080 (not standard HTTP port)
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.http.host': 'example.com',
|
||||
'easyhaproxy.http.port': '8080', # Non-standard port
|
||||
'easyhaproxy.http.localport': '3000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
}
|
||||
}
|
||||
|
||||
haproxy_config = cfg.generate(parsed_object)
|
||||
|
||||
# ACME ACL should still be created (up to user to ensure proper routing)
|
||||
# Note: The actual ACME validation will fail if port 80 isn't accessible
|
||||
assert 'acl is_certbot_example_com_8080' in haproxy_config
|
||||
|
||||
|
||||
class TestCertbotHAProxyConfigEdgeCases:
|
||||
"""Test edge cases and error conditions"""
|
||||
|
||||
def test_multiple_containers_same_domain_with_certbot(self):
|
||||
"""Test multiple containers serving the same domain with certbot"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.http.host': 'example.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '3000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
},
|
||||
'container2': {
|
||||
'easyhaproxy.http.host': 'example.com', # Same domain
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '4000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
}
|
||||
}
|
||||
|
||||
haproxy_config = cfg.generate(parsed_object)
|
||||
|
||||
# Should only create ACL once (not duplicate)
|
||||
assert haproxy_config.count('acl is_certbot_example_com_80') == 1
|
||||
|
||||
# Should route to certbot_backend
|
||||
assert 'use_backend certbot_backend if is_certbot_example_com_80' in haproxy_config
|
||||
|
||||
def test_certbot_with_custom_ports(self):
|
||||
"""Test certbot behavior with custom frontend ports"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.custom.host': 'example.com',
|
||||
'easyhaproxy.custom.port': '8080',
|
||||
'easyhaproxy.custom.localport': '3000',
|
||||
'easyhaproxy.custom.certbot': 'true',
|
||||
}
|
||||
}
|
||||
|
||||
haproxy_config = cfg.generate(parsed_object)
|
||||
|
||||
# Should create frontend on port 8080 with certbot ACL
|
||||
assert 'frontend http_in_8080' in haproxy_config
|
||||
assert 'acl is_certbot_example_com_8080' in haproxy_config
|
||||
|
||||
def test_certbot_backend_format(self):
|
||||
"""Test exact format of certbot_backend"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
haproxy_config = cfg.generate({})
|
||||
|
||||
# Verify exact backend format
|
||||
assert 'backend certbot_backend' in haproxy_config
|
||||
assert 'mode http' in haproxy_config
|
||||
assert 'server certbot 127.0.0.1:2080' in haproxy_config
|
||||
|
||||
# Should NOT have any load balancing, health checks, etc.
|
||||
# (it's a simple pass-through to localhost)
|
||||
lines = haproxy_config.split('\n')
|
||||
in_certbot_backend = False
|
||||
certbot_backend_lines = []
|
||||
for line in lines:
|
||||
if 'backend certbot_backend' in line:
|
||||
in_certbot_backend = True
|
||||
elif in_certbot_backend and line.strip() and not line.startswith(' '):
|
||||
break # End of backend section
|
||||
elif in_certbot_backend:
|
||||
certbot_backend_lines.append(line.strip())
|
||||
|
||||
# Should only have mode and server lines
|
||||
assert 'mode http' in certbot_backend_lines
|
||||
assert 'server certbot 127.0.0.1:2080' in certbot_backend_lines
|
||||
assert len([l for l in certbot_backend_lines if l]) == 2 # Only 2 non-empty lines
|
||||
|
||||
def test_certbot_acl_order_before_use_backend(self):
|
||||
"""Test that ACL definitions come before use_backend rules"""
|
||||
with patch.dict(os.environ, {
|
||||
'EASYHAPROXY_CERTBOT_EMAIL': 'test@example.com',
|
||||
}, clear=False):
|
||||
mapping = ContainerEnv.read()
|
||||
cfg = HaproxyConfigGenerator(mapping)
|
||||
|
||||
parsed_object = {
|
||||
'container1': {
|
||||
'easyhaproxy.http.host': 'example.com',
|
||||
'easyhaproxy.http.port': '80',
|
||||
'easyhaproxy.http.localport': '3000',
|
||||
'easyhaproxy.http.certbot': 'true',
|
||||
}
|
||||
}
|
||||
|
||||
haproxy_config = cfg.generate(parsed_object)
|
||||
|
||||
# Find positions
|
||||
acl_pos = haproxy_config.find('acl is_certbot_example_com_80')
|
||||
use_backend_pos = haproxy_config.find('use_backend certbot_backend if is_certbot_example_com_80')
|
||||
|
||||
assert acl_pos > 0, "ACL not found"
|
||||
assert use_backend_pos > 0, "use_backend not found"
|
||||
assert acl_pos < use_backend_pos, "ACL must be defined before use_backend"
|
||||
Loading…
Add table
Add a link
Reference in a new issue