"""Tests for lib/state.py — state store and collect functions.""" from unittest.mock import patch from lib.state import State, state class TestState: def test_new_state_empty(self): s = State() assert s.get("firewall") is None assert s.is_populated() is False def test_set_and_get(self): s = State() s.set("firewall", {"zones": {"public": {}}}) assert s.get("firewall") == {"zones": {"public": {}}} def test_populate_all(self): s = State() with patch.object(s, "_data", {}): pass # Just verify populate doesn't crash on empty collectors # (our collect functions need subprocess, so test mocks only) pass def test_singleton_exists(self): assert state is not None assert isinstance(state, State) class TestCollectAll: @patch("lib.state.run") def test_collect_firewall_returns_dict(self, mock_run): from lib.state import _collect_firewall def run_side(args, **kwargs): if "--get-zones" in args: return "public\ninternal" if "--get-active-zones" in args: return "public\n eth0" if "--get-services" in args: return "ssh http" if "ip" in args[0]: if "link" in args: return "1: lo: mtu 65536\n2: eth0: mtu 1500 link/ether aa:bb\n" return "" if "--list-all" in args: return "target: default\ninterfaces: eth0\nsources: \nservices: \nports: \nprotocols: \nforward-ports: \nmasquerade: no\nics: no\nrich-rules: \nicmp-blocks: \nmodule: \n" return "" mock_run.side_effect = run_side result = _collect_firewall() assert isinstance(result, dict) assert "active_zones" in result assert "interfaces" in result assert "timestamp" in result @patch("lib.state.run") def test_collect_firewall_vlan_ips_populated(self, mock_run): """VLAN interfaces with @suffix in ip addr output get their IPs collected.""" from lib.state import _collect_firewall def run_side(args, **kwargs): if "--get-zones" in args: return "public\ninternal" if "--get-active-zones" in args: return "public\n eth0\ninternal eth0.100" if "--get-services" in args: return "ssh http" if "ip" in args[0]: if "link" in args: return ( "1: lo: mtu 65536\n" "2: eth0: mtu 1500 link/ether aa:bb\n" "3: eth0.100@eth0: mtu 1500 link/ether aa:bb\n" ) if "addr" in args: return ( "2: eth0 inet 192.168.1.1/24\n" "3: eth0.100@if100 inet 10.0.0.1/24\n" ) return "" if "--list-all" in args: return ( "target: default\ninterfaces: eth0\nsources: " "services: \nports: \nprotocols: \nforward-ports: " "masquerade: no\nics: no\nrich-rules: " "icmp-blocks: \nmodule: \n" ) return "" mock_run.side_effect = run_side result = _collect_firewall() vlan_iface = next( (i for i in result["interfaces"] if i["name"] == "eth0.100"), None ) assert vlan_iface is not None, "VLAN interface should be present" assert vlan_iface["ips"], "VLAN interface should have collected IPs" assert "10.0.0.1/24" in vlan_iface["ips"] @patch("lib.state.run_proc") def test_collect_dnsmasq_returns_dict(self, mock_proc): from unittest.mock import Mock from lib.state import _collect_dnsmasq mock_proc.return_value = Mock(stdout="active\n", returncode=0) result = _collect_dnsmasq() assert isinstance(result, dict) assert "status" in result assert "config" in result assert "leases" in result class TestCollectFailure: def test_state_clears_on_failure(self): """State collection failure sets the subsystem to None.""" s = State() s.set("firewall", {"zones": {"public": {}}}) s.set("firewall", None) # simulates failure assert s.get("firewall") is None assert s.is_populated() is False class TestStateVersions: def test_version_starts_at_zero(self): s = State() versions = s.get_versions() assert versions["firewall"] == 0 assert versions["dnsmasq"] == 0 def test_bump_increments_version(self): s = State() assert s.get_versions()["firewall"] == 0 s.bump("firewall") assert s.get_versions()["firewall"] == 1 def test_bump_unknown_subsystem_noop(self): s = State() versions = s.get_versions() s.bump("nonexistent") assert versions == s.get_versions() def test_get_updated_versions_first_call_empty(self): s = State() s.bump("firewall") updated = s.get_updated_versions() assert updated == {} assert s.get_updated_versions() == {} def test_get_updated_versions_detects_change(self): s = State() _ = s.get_updated_versions() # snapshot s.bump("firewall") updated = s.get_updated_versions() assert updated["firewall"] == 1 def test_broadcast_maintains_snapshot(self): s = State() s.bump("firewall") s.bump("dnsmasq") _ = s.get_updated_versions() # snapshot at fw=1, dm=1 s.bump("wireguard") updated = s.get_updated_versions() assert updated["wireguard"] == 1 assert s.get_updated_versions() == {} def test_multiple_bumps_aggregate(self): s = State() _ = s.get_updated_versions() s.bump("firewall") s.bump("firewall") s.bump("dnsmasq") updated = s.get_updated_versions() assert updated["firewall"] == 2 assert updated["dnsmasq"] == 1