From 91f8ff5d085231d7e09ecb63f23596b3526ba659 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Thu, 27 Nov 2025 18:17:58 -0500 Subject: [PATCH] Add tests for plugin integration and improve plugin configuration handling - Created fixtures for `services-with-cloudflare`, `services-with-deny-pages`, and `services-with-multiple-plugins`. - Added comprehensive test cases for `CloudflarePlugin`, `DenyPagesPlugin`, and `CleanupPlugin`. - Ensured proper configuration propagation to plugins using labels in `easymapping/__init__.py`. - Validated multi-plugin integration and consistent output in HAProxy configuration generation. --- src/easymapping/__init__.py | 18 + src/tests/fixtures/services-with-cloudflare | 8 + src/tests/fixtures/services-with-deny-pages | 10 + .../fixtures/services-with-multiple-plugins | 11 + src/tests/test_plugins.py | 477 ++++++++++++++++++ 5 files changed, 524 insertions(+) create mode 100644 src/tests/fixtures/services-with-cloudflare create mode 100644 src/tests/fixtures/services-with-deny-pages create mode 100644 src/tests/fixtures/services-with-multiple-plugins create mode 100644 src/tests/test_plugins.py diff --git a/src/easymapping/__init__.py b/src/easymapping/__init__.py index 2c1d232..24b5277 100644 --- a/src/easymapping/__init__.py +++ b/src/easymapping/__init__.py @@ -218,6 +218,24 @@ class HaproxyConfigGenerator: ).split(",") enabled_plugins = [p.strip() for p in enabled_plugins if p.strip()] + # Extract plugin configurations from labels + # Format: easyhaproxy.http.plugin.PLUGIN_NAME.CONFIG_KEY + plugin_configs = {} + for plugin_name in enabled_plugins: + plugin_configs[plugin_name] = {} + # Look for all labels matching easyhaproxy.{definition}.plugin.{plugin_name}.* + plugin_label_prefix = self.label.create([definition, "plugin", plugin_name]) + for label_key in d.keys(): + if label_key.startswith(plugin_label_prefix + "."): + # Extract config key (everything after plugin_label_prefix + ".") + config_key = label_key[len(plugin_label_prefix) + 1:] + plugin_configs[plugin_name][config_key] = d[label_key] + + # Configure plugins with label-specific configs before execution + for plugin_name, config in plugin_configs.items(): + if plugin_name in self.plugin_manager.plugins: + self.plugin_manager.plugins[plugin_name].configure(config) + domain_results = self.plugin_manager.execute_domain_plugins( domain_context, enabled_list=enabled_plugins diff --git a/src/tests/fixtures/services-with-cloudflare b/src/tests/fixtures/services-with-cloudflare new file mode 100644 index 0000000..902c62d --- /dev/null +++ b/src/tests/fixtures/services-with-cloudflare @@ -0,0 +1,8 @@ +{ + "192.168.1.10": { + "easyhaproxy.http.host": "example.com", + "easyhaproxy.http.port": "80", + "easyhaproxy.http.localport": "8080", + "easyhaproxy.http.plugins": "cloudflare" + } +} diff --git a/src/tests/fixtures/services-with-deny-pages b/src/tests/fixtures/services-with-deny-pages new file mode 100644 index 0000000..b592182 --- /dev/null +++ b/src/tests/fixtures/services-with-deny-pages @@ -0,0 +1,10 @@ +{ + "192.168.1.20": { + "easyhaproxy.http.host": "secure.example.com", + "easyhaproxy.http.port": "80", + "easyhaproxy.http.localport": "8080", + "easyhaproxy.http.plugins": "deny_pages", + "easyhaproxy.http.plugin.deny_pages.paths": "/admin,/wp-admin", + "easyhaproxy.http.plugin.deny_pages.status_code": "404" + } +} diff --git a/src/tests/fixtures/services-with-multiple-plugins b/src/tests/fixtures/services-with-multiple-plugins new file mode 100644 index 0000000..1b13766 --- /dev/null +++ b/src/tests/fixtures/services-with-multiple-plugins @@ -0,0 +1,11 @@ +{ + "192.168.1.30": { + "easyhaproxy.http.host": "multi.example.com", + "easyhaproxy.http.port": "80", + "easyhaproxy.http.localport": "8080", + "easyhaproxy.http.plugins": "cloudflare,deny_pages", + "easyhaproxy.http.plugin.cloudflare.ip_list_path": "/etc/haproxy/cloudflare_ips.lst", + "easyhaproxy.http.plugin.deny_pages.paths": "/admin,/private", + "easyhaproxy.http.plugin.deny_pages.status_code": "403" + } +} diff --git a/src/tests/test_plugins.py b/src/tests/test_plugins.py new file mode 100644 index 0000000..8defa6e --- /dev/null +++ b/src/tests/test_plugins.py @@ -0,0 +1,477 @@ +""" +Tests for EasyHAProxy Plugin System + +Tests all builtin plugins: +- CloudflarePlugin (domain) +- CleanupPlugin (global) +- DenyPagesPlugin (domain) +""" + +import os +import sys +import json +import tempfile +import time + +# Add src to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from plugins import PluginManager, PluginContext +from plugins.builtin.cloudflare import CloudflarePlugin +from plugins.builtin.cleanup import CleanupPlugin +from plugins.builtin.deny_pages import DenyPagesPlugin +import easymapping + + +def load_fixture(file): + """Load a test fixture""" + fixture_path = os.path.join(os.path.dirname(__file__), "fixtures", file) + with open(fixture_path, 'r') as content_file: + line_list = json.loads("".join(content_file.readlines())) + return line_list + + +class TestCloudflarePlugin: + """Test cases for CloudflarePlugin (DOMAIN plugin)""" + + def test_cloudflare_plugin_initialization(self): + """Test plugin initializes with correct defaults""" + plugin = CloudflarePlugin() + assert plugin.name == "cloudflare" + assert plugin.enabled is True + assert plugin.ip_list_path == "/etc/haproxy/cloudflare_ips.lst" + + def test_cloudflare_plugin_configuration(self): + """Test plugin configuration""" + plugin = CloudflarePlugin() + + # Test custom IP list path + plugin.configure({"ip_list_path": "/custom/path/cf_ips.txt"}) + assert plugin.ip_list_path == "/custom/path/cf_ips.txt" + + # Test disabling + plugin.configure({"enabled": "false"}) + assert plugin.enabled is False + + # Test enabling with various values + plugin.configure({"enabled": "true"}) + assert plugin.enabled is True + + plugin.configure({"enabled": "1"}) + assert plugin.enabled is True + + plugin.configure({"enabled": "yes"}) + assert plugin.enabled is True + + def test_cloudflare_plugin_generates_config(self): + """Test plugin generates correct HAProxy config""" + plugin = CloudflarePlugin() + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="example.com", + port="80", + host_config={} + ) + + result = plugin.process(context) + + assert result.haproxy_config is not None + assert "Cloudflare" in result.haproxy_config + assert "acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst" in result.haproxy_config + assert "http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)]" in result.haproxy_config + assert result.metadata["domain"] == "example.com" + assert result.metadata["ip_list_path"] == "/etc/haproxy/cloudflare_ips.lst" + + def test_cloudflare_plugin_disabled(self): + """Test plugin returns empty config when disabled""" + plugin = CloudflarePlugin() + plugin.configure({"enabled": "false"}) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="example.com" + ) + + result = plugin.process(context) + assert result.haproxy_config == "" + assert result.metadata == {} + + def test_cloudflare_plugin_in_haproxy_config(self): + """Test Cloudflare plugin integration in full HAProxy config generation""" + # Use fixture with cloudflare plugin enabled via labels + line_list = load_fixture("services-with-cloudflare") + + result = { + "customerrors": False, + "certbot": {"email": "test@example.com"}, + "stats": {"port": 0} + } + + cfg = easymapping.HaproxyConfigGenerator(result) + haproxy_config = cfg.generate(line_list) + + # Verify Cloudflare config is in the output + assert "Cloudflare - Restore original visitor IP" in haproxy_config + assert "acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst" in haproxy_config + assert "http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)]" in haproxy_config + + +class TestCleanupPlugin: + """Test cases for CleanupPlugin (GLOBAL plugin)""" + + def test_cleanup_plugin_initialization(self): + """Test plugin initializes with correct defaults""" + plugin = CleanupPlugin() + assert plugin.name == "cleanup" + assert plugin.enabled is True + assert plugin.max_idle_time == 300 + assert plugin.cleanup_temp_files is True + + def test_cleanup_plugin_configuration(self): + """Test plugin configuration""" + plugin = CleanupPlugin() + + # Test max_idle_time + plugin.configure({"max_idle_time": "600"}) + assert plugin.max_idle_time == 600 + + # Test cleanup_temp_files + plugin.configure({"cleanup_temp_files": "false"}) + assert plugin.cleanup_temp_files is False + + # Test enabled + plugin.configure({"enabled": "false"}) + assert plugin.enabled is False + + def test_cleanup_plugin_processes_files(self): + """Test plugin cleans up old temp files""" + plugin = CleanupPlugin() + plugin.configure({"max_idle_time": "1"}) # 1 second + + # Create a temp file + with tempfile.NamedTemporaryFile(prefix="easyhaproxy_", delete=False) as tmp: + temp_file = tmp.name + + # Wait for file to age + time.sleep(2) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={} + ) + + # Run cleanup + result = plugin.process(context) + + # Verify file was removed + assert not os.path.exists(temp_file) + assert result.haproxy_config == "" + assert result.metadata["actions_performed"] >= 0 + + def test_cleanup_plugin_disabled(self): + """Test plugin does nothing when disabled""" + plugin = CleanupPlugin() + plugin.configure({"enabled": "false"}) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={} + ) + + result = plugin.process(context) + assert result.haproxy_config == "" + assert result.metadata == {} + + def test_cleanup_plugin_in_haproxy_config(self): + """Test Cleanup plugin integration (should not affect config output)""" + line_list = load_fixture("services") + + # Enable cleanup plugin + result = { + "customerrors": False, + "certbot": {"email": "test@example.com"}, + "stats": {"port": 0}, + "plugins": { + "enabled": ["cleanup"], + "config": { + "cleanup": { + "max_idle_time": "300" + } + } + } + } + + cfg = easymapping.HaproxyConfigGenerator(result) + haproxy_config = cfg.generate(line_list) + + # Cleanup plugin should not add any HAProxy config + assert "cleanup" not in haproxy_config.lower() + # But the config should still be valid + assert "backend certbot_backend" in haproxy_config + + +class TestDenyPagesPlugin: + """Test cases for DenyPagesPlugin (DOMAIN plugin)""" + + def test_deny_pages_plugin_initialization(self): + """Test plugin initializes with correct defaults""" + plugin = DenyPagesPlugin() + assert plugin.name == "deny_pages" + assert plugin.enabled is True + assert plugin.paths == [] + assert plugin.status_code == 403 + + def test_deny_pages_plugin_configuration(self): + """Test plugin configuration""" + plugin = DenyPagesPlugin() + + # Test paths + plugin.configure({"paths": "/admin,/private,/internal"}) + assert plugin.paths == ["/admin", "/private", "/internal"] + + # Test status code + plugin.configure({"status_code": "404"}) + assert plugin.status_code == 404 + + # Test enabled + plugin.configure({"enabled": "false"}) + assert plugin.enabled is False + + def test_deny_pages_plugin_generates_config(self): + """Test plugin generates correct HAProxy config""" + plugin = DenyPagesPlugin() + plugin.configure({ + "paths": "/admin,/private", + "status_code": "403" + }) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="example.com", + port="80", + host_config={} + ) + + result = plugin.process(context) + + assert result.haproxy_config is not None + assert "Deny Pages" in result.haproxy_config + assert "acl denied_path path_beg /admin /private" in result.haproxy_config + assert "http-request deny deny_status 403 if denied_path" in result.haproxy_config + assert result.metadata["domain"] == "example.com" + assert result.metadata["blocked_paths"] == ["/admin", "/private"] + assert result.metadata["status_code"] == 403 + + def test_deny_pages_plugin_disabled(self): + """Test plugin returns empty config when disabled""" + plugin = DenyPagesPlugin() + plugin.configure({"enabled": "false", "paths": "/admin"}) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="example.com" + ) + + result = plugin.process(context) + assert result.haproxy_config == "" + + def test_deny_pages_plugin_no_paths(self): + """Test plugin returns empty config when no paths configured""" + plugin = DenyPagesPlugin() + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="example.com" + ) + + result = plugin.process(context) + assert result.haproxy_config == "" + + def test_deny_pages_plugin_in_haproxy_config(self): + """Test Deny Pages plugin integration in full HAProxy config generation""" + # Use fixture with deny_pages plugin enabled via labels + line_list = load_fixture("services-with-deny-pages") + + result = { + "customerrors": False, + "certbot": {"email": "test@example.com"}, + "stats": {"port": 0} + } + + cfg = easymapping.HaproxyConfigGenerator(result) + haproxy_config = cfg.generate(line_list) + + # Verify Deny Pages config is in the output + assert "Deny Pages - Block specific paths" in haproxy_config + assert "acl denied_path path_beg /admin /wp-admin" in haproxy_config + assert "http-request deny deny_status 404 if denied_path" in haproxy_config + + +class TestPluginManager: + """Test cases for PluginManager""" + + def test_plugin_manager_loads_builtin_plugins(self): + """Test that plugin manager loads all builtin plugins""" + manager = PluginManager() + manager.load_plugins() + + # Verify all builtin plugins are loaded + assert "cloudflare" in manager.plugins + assert "cleanup" in manager.plugins + assert "deny_pages" in manager.plugins + + # Verify plugin types + assert len(manager.global_plugins) == 1 # cleanup + assert len(manager.domain_plugins) == 2 # cloudflare, deny_pages + + # Verify plugin instances + assert manager.plugins["cloudflare"].name == "cloudflare" + assert manager.plugins["cleanup"].name == "cleanup" + assert manager.plugins["deny_pages"].name == "deny_pages" + + def test_plugin_manager_executes_global_plugins(self): + """Test plugin manager executes global plugins correctly""" + manager = PluginManager() + manager.load_plugins() + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={} + ) + + # Execute only cleanup plugin + results = manager.execute_global_plugins(context, enabled_list=["cleanup"]) + + assert len(results) == 1 + assert results[0].haproxy_config == "" # Cleanup doesn't generate config + + def test_plugin_manager_executes_domain_plugins(self): + """Test plugin manager executes domain plugins correctly""" + manager = PluginManager() + manager.load_plugins() + + # Configure deny_pages + manager.configure_plugins({ + "deny_pages": { + "paths": "/admin,/private", + "status_code": "403" + } + }) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="example.com", + port="80", + host_config={} + ) + + # Execute both domain plugins + results = manager.execute_domain_plugins(context, enabled_list=["cloudflare", "deny_pages"]) + + assert len(results) == 2 + + # Verify configs were generated + configs = [r.haproxy_config for r in results if r.haproxy_config] + assert len(configs) == 2 + + # Verify both plugin outputs + all_config = "\n".join(configs) + assert "Cloudflare" in all_config + assert "Deny Pages" in all_config + + def test_plugin_manager_empty_enabled_list(self): + """Test that empty enabled list means no plugins execute""" + manager = PluginManager() + manager.load_plugins() + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="example.com" + ) + + # Execute with empty list - should execute nothing + results = manager.execute_domain_plugins(context, enabled_list=[]) + assert len(results) == 0 + + results = manager.execute_global_plugins(context, enabled_list=[]) + assert len(results) == 0 + + +class TestMultiplePluginsCombined: + """Test cases for multiple plugins working together""" + + def test_multiple_plugins_in_haproxy_config(self): + """Test multiple plugins working together in HAProxy config""" + # Use fixture with both plugins enabled via labels + line_list = load_fixture("services-with-multiple-plugins") + + # Enable cleanup plugin globally + result = { + "customerrors": False, + "certbot": {"email": "test@example.com"}, + "stats": {"port": 0}, + "plugins": { + "enabled": ["cleanup"], + "config": { + "cleanup": { + "max_idle_time": "600" + } + } + } + } + + cfg = easymapping.HaproxyConfigGenerator(result) + haproxy_config = cfg.generate(line_list) + + # Verify both domain plugins are in the output + assert "Cloudflare - Restore original visitor IP" in haproxy_config + assert "Deny Pages - Block specific paths" in haproxy_config + assert "acl from_cloudflare" in haproxy_config + assert "acl denied_path path_beg /admin /private" in haproxy_config + + # Cleanup doesn't add to config + assert "cleanup" not in haproxy_config.lower() + + def test_plugins_order_in_output(self): + """Test that plugins maintain consistent order in output""" + # Use fixture with both plugins enabled via labels + line_list = load_fixture("services-with-multiple-plugins") + + result = { + "customerrors": False, + "certbot": {"email": "test@example.com"}, + "stats": {"port": 0} + } + + cfg = easymapping.HaproxyConfigGenerator(result) + haproxy_config = cfg.generate(line_list) + + # Find positions of plugin configs + cloudflare_pos = haproxy_config.find("Cloudflare") + deny_pages_pos = haproxy_config.find("Deny Pages") + + # Both should be present + assert cloudflare_pos != -1 + assert deny_pages_pos != -1 + + # They should appear in backend sections (not in global/defaults) + assert cloudflare_pos > haproxy_config.find("backend srv_") + assert deny_pages_pos > haproxy_config.find("backend srv_")