- Replaced `easymapping` configuration with `containers` for better maintainability and clarity. - Introduced `DockerComposeFixture` class in `utils.py` to manage Docker Compose lifecycle and smart build strategy. - Enhanced YAML-to-environment variable conversion in `ContainerEnv` for dynamic configuration support. - Updated HAProxy configurations and test fixtures to reflect the new format. - Improved test coverage for YAML parsing, environment variable handling, and HAProxy config generation.
259 lines
No EOL
9 KiB
Python
259 lines
No EOL
9 KiB
Python
"""
|
|
Pytest test suite for EasyHAProxy Static Configuration Mode
|
|
|
|
These tests verify static YAML configuration mode (EASYHAPROXY_DISCOVER=static).
|
|
Tests are organized by configuration file and can be run individually or as a suite.
|
|
|
|
Requirements:
|
|
- pytest
|
|
- requests
|
|
- PyJWT
|
|
- cryptography
|
|
- docker-compose
|
|
|
|
Usage:
|
|
# Run all static tests
|
|
pytest test_static.py -v
|
|
|
|
# Run specific test class
|
|
pytest test_static.py::TestStaticBasic -v
|
|
|
|
# Run specific test
|
|
pytest test_static.py::TestStaticBasic::test_https_host1 -v
|
|
"""
|
|
|
|
import subprocess
|
|
import shutil
|
|
from pathlib import Path
|
|
import pytest
|
|
import requests
|
|
from typing import Generator
|
|
from utils import extract_backend_block, DockerComposeFixture
|
|
|
|
# Base directory for static configuration
|
|
BASE_DIR = Path(__file__).parent.absolute()
|
|
STATIC_DIR = BASE_DIR / "static"
|
|
CONF_DIR = STATIC_DIR / "conf"
|
|
|
|
class StaticDockerComposeFixture(DockerComposeFixture):
|
|
"""Helper class to manage static docker-compose lifecycle with config file switching"""
|
|
|
|
def __init__(self, config_file: str, startup_wait: int = 3, build: bool = None):
|
|
# Initialize parent with static docker-compose.yml path
|
|
super().__init__(str(STATIC_DIR / "docker-compose.yml"), startup_wait, build)
|
|
|
|
self.config_file = config_file
|
|
self.config_source = CONF_DIR / config_file
|
|
self.config_target = CONF_DIR / "config.yml"
|
|
|
|
def up(self):
|
|
"""Start docker-compose services with specified config"""
|
|
print() # Newline for better test output formatting
|
|
print(f" → Using static config: {self.config_file}")
|
|
|
|
# Copy the config file to config.yml
|
|
shutil.copy(self.config_source, self.config_target)
|
|
print(f" ✓ Config copied to config.yml")
|
|
|
|
# Call parent's up() method to start services
|
|
super().up()
|
|
|
|
|
|
# =============================================================================
|
|
# Fixtures
|
|
# =============================================================================
|
|
|
|
@pytest.fixture
|
|
def static_basic() -> Generator[None, None, None]:
|
|
"""Fixture for config-basic.yml"""
|
|
fixture = StaticDockerComposeFixture("config-basic.yml")
|
|
fixture.up()
|
|
yield
|
|
fixture.down()
|
|
|
|
|
|
@pytest.fixture
|
|
def static_deny_pages() -> Generator[None, None, None]:
|
|
"""Fixture for config-deny-pages.yml"""
|
|
fixture = StaticDockerComposeFixture("config-deny-pages.yml")
|
|
fixture.up()
|
|
yield
|
|
fixture.down()
|
|
|
|
|
|
@pytest.fixture
|
|
def static_jwt_validator() -> Generator[None, None, None]:
|
|
"""Fixture for config-jwt-validator.yml"""
|
|
fixture = StaticDockerComposeFixture("config-jwt-validator.yml")
|
|
fixture.up()
|
|
yield
|
|
fixture.down()
|
|
|
|
|
|
# =============================================================================
|
|
# Test: config-basic.yml - Basic HTTP→HTTPS Redirect
|
|
# =============================================================================
|
|
|
|
@pytest.mark.static
|
|
class TestStaticBasic:
|
|
"""Tests for static config-basic.yml"""
|
|
|
|
def test_haproxy_config(self, static_basic):
|
|
"""Test HAProxy configuration has SSL and redirect configurations"""
|
|
result = subprocess.run(
|
|
["docker", "exec", "static-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
config = result.stdout
|
|
|
|
# Test HTTPS backend for host1
|
|
https_host1_block = extract_backend_block(config, "srv_host1_local_443")
|
|
assert https_host1_block, "Backend srv_host1_local_443 not found"
|
|
assert "mode http" in https_host1_block
|
|
|
|
# Verify SSL frontend exists
|
|
assert "frontend https_in_443" in config or "bind *:443" in config
|
|
|
|
# Verify HTTP to HTTPS redirect (new format uses http-request redirect scheme)
|
|
assert "http-request redirect scheme https code 301" in config
|
|
|
|
def test_https_host1(self, static_basic):
|
|
"""Test HTTPS access to host1.local"""
|
|
response = requests.get(
|
|
"https://127.0.0.1/",
|
|
headers={"Host": "host1.local"},
|
|
verify=False
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_http_redirect_host1(self, static_basic):
|
|
"""Test HTTP to HTTPS redirect for host1.local"""
|
|
response = requests.get(
|
|
"http://127.0.0.1/",
|
|
headers={"Host": "host1.local"},
|
|
allow_redirects=False
|
|
)
|
|
assert response.status_code == 301
|
|
assert "https://host1.local" in response.headers.get("location", "")
|
|
|
|
def test_haproxy_stats(self, static_basic):
|
|
"""Test HAProxy stats interface"""
|
|
from conftest import verify_haproxy_stats
|
|
verify_haproxy_stats()
|
|
|
|
|
|
# =============================================================================
|
|
# Test: config-deny-pages.yml - Deny Pages Plugin
|
|
# =============================================================================
|
|
|
|
@pytest.mark.static
|
|
class TestStaticDenyPages:
|
|
"""Tests for static config-deny-pages.yml"""
|
|
|
|
def test_haproxy_config(self, static_deny_pages):
|
|
"""Test HAProxy configuration has deny pages rules"""
|
|
result = subprocess.run(
|
|
["docker", "exec", "static-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
config = result.stdout
|
|
|
|
# Extract backend for host1.local (should have global deny_pages config)
|
|
backend_block = extract_backend_block(config, "srv_host1_local_80")
|
|
assert backend_block, "Backend srv_host1_local_80 not found"
|
|
|
|
# Verify deny pages plugin is configured
|
|
assert "# Deny Pages - Block specific paths" in backend_block
|
|
assert "acl denied_path path_beg" in backend_block
|
|
assert "/admin" in backend_block
|
|
assert "/.env" in backend_block
|
|
assert "/config" in backend_block
|
|
assert "http-request deny" in backend_block
|
|
|
|
def test_normal_access(self, static_deny_pages):
|
|
"""Test normal access to allowed paths"""
|
|
response = requests.get(
|
|
"http://127.0.0.1/",
|
|
headers={"Host": "host1.local"}
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_blocked_paths(self, static_deny_pages):
|
|
"""Test access to blocked paths"""
|
|
blocked_paths = ["/admin", "/.env", "/config"]
|
|
for path in blocked_paths:
|
|
response = requests.get(
|
|
f"http://127.0.0.1{path}",
|
|
headers={"Host": "host1.local"}
|
|
)
|
|
assert response.status_code == 404, f"Path {path} should be blocked with 404"
|
|
|
|
def test_haproxy_stats(self, static_deny_pages):
|
|
"""Test HAProxy stats interface"""
|
|
from conftest import verify_haproxy_stats
|
|
verify_haproxy_stats()
|
|
|
|
|
|
# =============================================================================
|
|
# Test: config-jwt-validator.yml - JWT Validator Plugin
|
|
# =============================================================================
|
|
|
|
@pytest.mark.static
|
|
class TestStaticJWTValidator:
|
|
"""Tests for static config-jwt-validator.yml"""
|
|
|
|
def test_haproxy_config(self, static_jwt_validator):
|
|
"""Test HAProxy configuration has JWT validation rules"""
|
|
result = subprocess.run(
|
|
["docker", "exec", "static-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
config = result.stdout
|
|
|
|
# Extract backend for API (static mode uses different naming)
|
|
# Find any backend that contains JWT validation
|
|
assert "# JWT Validator - Validate JWT tokens" in config, \
|
|
"JWT Validator plugin comment not found"
|
|
assert "jwt_verify" in config, \
|
|
"JWT signature verification not found"
|
|
assert "Missing Authorization HTTP header" in config, \
|
|
"JWT authorization check not found"
|
|
|
|
def test_without_token(self, static_jwt_validator):
|
|
"""Test API access without JWT token (should fail)"""
|
|
response = requests.get(
|
|
"http://127.0.0.1/",
|
|
headers={"Host": "api.local"}
|
|
)
|
|
assert response.status_code == 403
|
|
assert "Missing Authorization HTTP header" in response.text
|
|
|
|
def test_with_valid_token(self, static_jwt_validator, jwt_token):
|
|
"""Test API access with valid JWT token (should succeed)"""
|
|
response = requests.get(
|
|
"http://127.0.0.1/",
|
|
headers={
|
|
"Host": "api.local",
|
|
"Authorization": f"Bearer {jwt_token}"
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_haproxy_stats(self, static_jwt_validator):
|
|
"""Test HAProxy stats interface"""
|
|
from conftest import verify_haproxy_stats
|
|
verify_haproxy_stats()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("This is a pytest test suite. Run with: pytest test_static.py -v")
|
|
print("\nAvailable test classes:")
|
|
print(" - TestStaticBasic: Basic static configuration tests")
|
|
print(" - TestStaticDenyPages: Deny pages plugin tests")
|
|
print(" - TestStaticJWTValidator: JWT validator plugin tests") |