from datetime import datetime from unittest.mock import patch from lib import firewall class TestParseForwardPorts: def test_single_entry(self): result = firewall._parse_forward_ports("port=443/proto=tcp") assert len(result) == 1 assert result[0]["port"] == 443 assert result[0]["proto"] == "tcp" def test_multiple_entries(self): result = firewall._parse_forward_ports( "port=443/proto=tcp port=80/proto=tcp/toaddr=10.0.0.1/toport=8080" ) assert len(result) == 2 assert result[0]["port"] == 443 assert result[1]["port"] == 80 assert result[1]["toaddr"] == "10.0.0.1" assert result[1]["toport"] == 8080 def test_empty_string(self): assert firewall._parse_forward_ports("") == [] class TestGetActiveZones: @patch("lib.firewall.run") def test_parses_active_zones(self, mock_run): mock_run.return_value = "public\n eth0\ninternal\n eth1\n eth2" result = firewall.get_active_zones() assert result == { "public": ["eth0"], "internal": ["eth1", "eth2"], } @patch("lib.firewall.run") def test_empty_output(self, mock_run): mock_run.return_value = "" result = firewall.get_active_zones() assert result == {} @patch("lib.firewall.run") def test_zone_with_no_interfaces(self, mock_run): mock_run.return_value = "dmz" result = firewall.get_active_zones() assert result == {"dmz": []} class TestGetZoneInfo: @patch("lib.firewall.run") def test_parses_zone_info(self, mock_run): mock_run.return_value = ( "target: default\n" "interfaces: eth0\n" "sources: \n" "services: ssh dhcp\n" "ports: 8080/tcp\n" "protocols: \n" "forward-ports: \n" "masquerade: yes\n" "ics: no\n" "rich-rules: \n" "icmp-blocks: \n" "module: \n" ) result = firewall.get_zone_info("public") assert result["name"] == "public" assert result["services"] == ["ssh", "dhcp"] assert result["ports"] == ["8080/tcp"] assert result["masquerade"] is True assert result["interfaces"] == ["eth0"] assert result["sources"] == [] assert result["rich-rules"] == [] class TestGetInterfaces: @patch("lib.firewall.run") def test_parses_interfaces(self, mock_run): mock_run.return_value = ( "1: lo: mtu 65536\n" "2: eth0: mtu 1500\n" "3: eth1: mtu 1500\n" ) result = firewall.get_interfaces() assert result == ["lo", "eth0", "eth1"] class TestGetRichRules: @patch("lib.firewall.run") def test_single_rule(self, mock_run): mock_run.return_value = ( 'rule family="ipv4" port protocol="tcp" port="443" accept;' ) result = firewall.get_rich_rules("public") assert len(result) == 1 @patch("lib.firewall.run") def test_empty_rules(self, mock_run): mock_run.return_value = "" result = firewall.get_rich_rules("public") assert result == [] @patch("lib.firewall.run") def test_multiline_rule(self, mock_run): mock_run.return_value = ( 'rule family="ipv4"\n source address="10.0.0.0/24"\n reject;' ) result = firewall.get_rich_rules("public") assert len(result) == 1 assert "10.0.0.0/24" in result[0] class TestNowIso: def test_returns_iso_string(self): result = firewall._now_iso() datetime.fromisoformat(result) assert "+" in result class TestAddForwardPort: @patch("lib.firewall.run") def test_forward_port_basic(self, mock_run): mock_run.return_value = "" firewall.add_forward_port("public", 443, "tcp", toaddr="10.0.0.5", toport=8080) calls = [c[0][0] for c in mock_run.call_args_list] assert any("--add-forward-port=" in str(c) for c in calls) @patch("lib.firewall.run") def test_forward_port_port_only(self, mock_run): mock_run.return_value = "" firewall.add_forward_port("public", 80, "tcp", toport=8080) class TestGetState: @patch("lib.firewall.get_available_zones") @patch("lib.firewall.get_zone_info") @patch("lib.firewall.get_active_zones") @patch("lib.firewall.get_interfaces") @patch("lib.firewall.get_services") @patch("lib.firewall.get_rich_rules") def test_returns_full_state( self, mock_rich, mock_services, mock_ifaces, mock_active, mock_zone_info, mock_available, ): mock_available.return_value = ["public", "internal"] mock_active.return_value = {"public": ["eth0"]} mock_ifaces.return_value = ["eth0", "eth1"] mock_services.return_value = ["ssh", "http"] mock_zone_info.return_value = {"name": "public", "services": []} mock_rich.return_value = [] result = firewall.get_state() assert "zones" in result assert "active_zones" in result assert "timestamp" in result class TestNormalizeTarget: def test_accept(self): assert firewall._normalize_target("ACCEPT") == "ACCEPT" def test_drop(self): assert firewall._normalize_target("DROP") == "DROP" def test_reject(self): assert firewall._normalize_target("REJECT") == "REJECT" def test_default(self): assert firewall._normalize_target("DEFAULT") == "default" assert firewall._normalize_target("default") == "default" assert firewall._normalize_target("UNKNOWN") == "default" class TestLiveTargetToConfig: def test_accept(self): assert firewall._live_target_to_config("ACCEPT") == "ACCEPT" def test_drop(self): assert firewall._live_target_to_config("DROP") == "DROP" def test_reject(self): assert firewall._live_target_to_config("REJECT") == "REJECT" def test_default(self): assert firewall._live_target_to_config("default") == "DEFAULT" assert firewall._live_target_to_config("") == "DEFAULT" class TestEnsureConfigFile: def test_creates_file_if_missing(self, tmp_path): cfg_dir = tmp_path / "config" / "firewall" cfg_file = cfg_dir / "config.json" with ( patch.object(firewall, "CONFIG_DIR", cfg_dir), patch.object(firewall, "CONFIG_FILE", cfg_file), ): firewall._ensure_config_file() assert cfg_file.exists() import json as _json content = _json.loads(cfg_file.read_text()) assert content == {"zones": {}} def test_skips_existing_file(self, tmp_path): cfg_dir = tmp_path / "config" / "firewall" cfg_file = cfg_dir / "config.json" cfg_dir.mkdir(parents=True) cfg_file.write_text('{"zones": {"public": {}}}') with ( patch.object(firewall, "CONFIG_DIR", cfg_dir), patch.object(firewall, "CONFIG_FILE", cfg_file), ): firewall._ensure_config_file() content = cfg_file.read_text() assert '{"zones": {"public": {}}}' in content class TestConfigGet: @patch("lib.firewall._ensure_config_file") def test_returns_config(self, mock_ensure, tmp_path): cfg_file = tmp_path / "config.json" cfg_file.write_text( '{"zones": {"public": {"interfaces": ["eth0"], "services": ["http"], "masquerade": true, "target": "DEFAULT"}}}' ) with patch.object(firewall, "CONFIG_FILE", cfg_file): result = firewall.get_config() assert result["zones"]["public"]["interfaces"] == ["eth0"] assert result["zones"]["public"]["services"] == ["http"] class TestConfigSet: def test_writes_config_atomic(self, tmp_path): cfg_file = tmp_path / "config.json" with ( patch.object(firewall, "CONFIG_FILE", cfg_file), patch.object(firewall, "CONFIG_DIR", tmp_path), ): firewall.save_config({"zones": {"test": {"interfaces": ["eth0"]}}}) import json as _json content = _json.loads(cfg_file.read_text()) assert content["zones"]["test"]["interfaces"] == ["eth0"] class TestConfigApply: @patch("lib.firewall.get_config") @patch("lib.firewall.save_backup") @patch("lib.firewall.get_available_zones") @patch("lib.firewall.create_zone") @patch("lib.firewall.set_zone_services") @patch("lib.firewall.set_zone_interfaces") @patch("lib.firewall.set_masquerade") @patch("lib.firewall._reload") def test_applies_existing_zone( self, mock_reload, mock_set_mq, mock_set_ifaces, mock_set_svcs, mock_create, mock_available, mock_backup, mock_cfg, ): mock_cfg.return_value = { "zones": { "public": { "target": "DEFAULT", "interfaces": ["eth0"], "services": ["http", "https"], "masquerade": True, }, }, } mock_available.return_value = ["public", "internal"] mock_backup.return_value = "/tmp/rules.json" result = firewall.config_apply() assert result["applied_zones"] == ["public"] assert result["backup"] == "/tmp/rules.json" mock_set_ifaces.assert_called_once_with("public", ["eth0"]) mock_set_svcs.assert_called_once_with("public", ["http", "https"]) mock_set_mq.assert_called_once_with("public", True) @patch("lib.firewall.get_config") @patch("lib.firewall.save_backup") @patch("lib.firewall.get_available_zones") @patch("lib.firewall.create_zone") @patch("lib.firewall.set_zone_services") @patch("lib.firewall.set_zone_interfaces") @patch("lib.firewall.set_masquerade") @patch("lib.firewall._reload") def test_creates_new_zone( self, mock_reload, mock_set_mq, mock_set_ifaces, mock_set_svcs, mock_create, mock_available, mock_backup, mock_cfg, ): mock_cfg.return_value = { "zones": { "custom": { "target": "ACCEPT", "interfaces": ["eth2"], "services": [], "masquerade": False, }, }, } mock_available.return_value = ["public", "internal"] mock_backup.return_value = "/tmp/rules.json" result = firewall.config_apply() assert result["applied_zones"] == ["custom"] mock_create.assert_called_once_with("custom", "ACCEPT") mock_set_ifaces.assert_called_once_with("custom", ["eth2"]) class TestConfigPending: @patch("lib.firewall.get_config") @patch("lib.firewall.get_state") def test_detects_interface_drift(self, mock_state, mock_cfg): mock_cfg.return_value = { "zones": { "public": { "interfaces": ["eth0"], "services": ["http"], "masquerade": False, }, }, } mock_state.return_value = { "zones": { "public": { "interfaces": ["eth1"], "services": ["http"], "masquerade": False, }, }, } result = firewall.config_pending() assert result["needs_apply"] is True assert any(c["type"] == "interfaces" for c in result["pending"]) @patch("lib.firewall.get_config") @patch("lib.firewall.get_state") def test_in_sync(self, mock_state, mock_cfg): mock_cfg.return_value = { "zones": { "public": { "interfaces": ["eth0"], "services": ["http"], "masquerade": False, }, }, } mock_state.return_value = { "zones": { "public": { "interfaces": ["eth0"], "services": ["http"], "masquerade": False, }, }, } result = firewall.config_pending() assert result["needs_apply"] is False @patch("lib.firewall.get_config") @patch("lib.firewall.get_state") def test_detects_services_drift(self, mock_state, mock_cfg): mock_cfg.return_value = { "zones": { "public": { "interfaces": ["eth0"], "services": ["http", "ssh"], "masquerade": False, }, }, } mock_state.return_value = { "zones": { "public": { "interfaces": ["eth0"], "services": ["http"], "masquerade": False, }, }, } result = firewall.config_pending() assert any(c["type"] == "services" for c in result["pending"]) @patch("lib.firewall.get_config") @patch("lib.firewall.get_state") def test_detects_unmanaged_zones(self, mock_state, mock_cfg): mock_cfg.return_value = {"zones": {}} mock_state.return_value = { "zones": { "public": { "interfaces": ["eth0"], "services": [], "masquerade": False, }, }, } result = firewall.config_pending() assert "public" in result["unmanaged_zones"] class TestConfigEmptyZones: @patch("lib.firewall.get_config") @patch("lib.firewall.save_backup") @patch("lib.firewall.get_available_zones") @patch("lib.firewall.create_zone") @patch("lib.firewall.set_zone_services") @patch("lib.firewall.set_zone_interfaces") @patch("lib.firewall.set_masquerade") @patch("lib.firewall._reload") def test_empty_config_no_ops( self, mock_reload, mock_set_mq, mock_set_ifaces, mock_set_svcs, mock_create, mock_available, mock_backup, mock_cfg, ): mock_cfg.return_value = {"zones": {}} mock_available.return_value = [] mock_backup.return_value = "/tmp/rules.json" result = firewall.config_apply() assert result["applied_zones"] == [] mock_create.assert_not_called() mock_set_ifaces.assert_not_called()