178 lines
6.1 KiB
Python
178 lines
6.1 KiB
Python
"""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: <LOOPBACK> mtu 65536\n2: eth0: <UP> 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: <LOOPBACK> mtu 65536\n"
|
|
"2: eth0: <UP> mtu 1500 link/ether aa:bb\n"
|
|
"3: eth0.100@eth0: <UP> 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
|