ws: migrate push stream to data streaming
- daemon: send full snapshot on connect; versions/tick now carry the full state of one subsystem (subsystem + data); no legacy updated/subsystems payloads; refresh_state and POST /status/refresh broadcast per-subsystem versions with data - client: modelSet() patches models in place; onMessage/topic refresh retired; 3s initial-load fallback via new POST /api/status/refresh - schema: lib/schema.py TypedDicts + hoover/schema.js defaults + docs/state-model.md as single source of truth for state shapes - system: poll at 1s, volatile metrics registered, dashboard uses a dedicated system model (status model removed) - firewall: refuse to strip both https and ssh from the default zone (409, force override via UI confirm); set_zone_services persists services to the declarative config; collector exposes default_zone - UI: pages migrate to flat state shapes; post-mutation modelFetch refreshes removed (WS delta covers it) - tests: ws snapshot/delta/broadcast, refresh-state, schema types, model-set/js ws handler and reconnect fallback
This commit is contained in:
+146
-2
@@ -1,11 +1,11 @@
|
||||
"""Tests for lib/firewall.py (pure logic) and daemon/handlers/firewall.py (privilege boundary)."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from daemon.handlers import firewall as daemonfirewall
|
||||
from daemon.server import NotFoundError
|
||||
from daemon.server import ConflictError, NotFoundError
|
||||
from lib import firewall
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -467,6 +467,150 @@ class TestDaemonConfigApply:
|
||||
assert result["applied_zones"] == ["public"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Management-lockout guard: default zone must keep https or ssh
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDaemonMgmtLockoutGuard:
|
||||
ZONES_OUT = "public\ninternal"
|
||||
|
||||
@patch("daemon.handlers.firewall._default_zone", return_value="public")
|
||||
def test_set_zone_services_blocks_default_zone(self, mock_dz):
|
||||
with (
|
||||
patch(
|
||||
"daemon.handlers.firewall.run", return_value=self.ZONES_OUT
|
||||
) as mock_run,
|
||||
pytest.raises(ConflictError) as exc,
|
||||
):
|
||||
daemonfirewall.set_zone_services(
|
||||
None, {"zone": "public", "services": ["http"]}
|
||||
)
|
||||
assert "https and ssh" in str(exc.value)
|
||||
# Guard fires before any mutation: only the zone-existence check ran.
|
||||
assert mock_run.call_args_list == [
|
||||
call(["firewall-cmd", "--get-zones"], sudo=True),
|
||||
]
|
||||
|
||||
@patch("daemon.handlers.firewall._default_zone", return_value="public")
|
||||
def test_set_zone_services_force_bypasses_guard(self, mock_dz):
|
||||
with (
|
||||
patch("daemon.handlers.firewall.run", return_value=self.ZONES_OUT),
|
||||
patch.object(
|
||||
daemonfirewall, "_parse_zone_output", return_value={"services": []}
|
||||
),
|
||||
patch.object(daemonfirewall, "_reload"),
|
||||
patch.object(daemonfirewall, "_get_config", return_value={"zones": {}}),
|
||||
patch.object(daemonfirewall, "_save_config") as mock_save,
|
||||
patch.object(daemonfirewall, "bus") as mock_bus,
|
||||
patch("daemon.handlers.firewall.refresh_state"),
|
||||
):
|
||||
mock_bus.emit.return_value = MagicMock(affected_subsystems=[])
|
||||
result = daemonfirewall.set_zone_services(
|
||||
None, {"zone": "public", "services": ["http"], "force": True}
|
||||
)
|
||||
assert result == {"zone": "public", "services": ["http"]}
|
||||
cfg = mock_save.call_args[0][0]
|
||||
assert cfg["zones"]["public"]["services"] == ["http"]
|
||||
|
||||
@patch("daemon.handlers.firewall._default_zone", return_value="internal")
|
||||
def test_set_zone_services_non_default_zone_allowed(self, mock_dz):
|
||||
with (
|
||||
patch("daemon.handlers.firewall.run", return_value=self.ZONES_OUT),
|
||||
patch.object(
|
||||
daemonfirewall, "_parse_zone_output", return_value={"services": []}
|
||||
),
|
||||
patch.object(daemonfirewall, "_reload"),
|
||||
patch.object(daemonfirewall, "_get_config", return_value={"zones": {}}),
|
||||
patch.object(daemonfirewall, "_save_config"),
|
||||
patch.object(daemonfirewall, "bus") as mock_bus,
|
||||
patch("daemon.handlers.firewall.refresh_state"),
|
||||
):
|
||||
mock_bus.emit.return_value = MagicMock(affected_subsystems=[])
|
||||
result = daemonfirewall.set_zone_services(
|
||||
None, {"zone": "public", "services": []}
|
||||
)
|
||||
assert result == {"zone": "public", "services": []}
|
||||
|
||||
def test_would_remove_mgmt_keeps_https(self):
|
||||
assert daemonfirewall._would_remove_mgmt("public", ["http", "https"]) is False
|
||||
assert daemonfirewall._would_remove_mgmt("public", ["ssh"]) is False
|
||||
|
||||
def test_would_remove_mgmt_fails_closed_on_error(self):
|
||||
with patch(
|
||||
"daemon.handlers.firewall._default_zone", side_effect=RuntimeError("boom")
|
||||
):
|
||||
assert daemonfirewall._would_remove_mgmt("public", ["http"]) is True
|
||||
|
||||
@patch("daemon.handlers.firewall._default_zone", return_value="default-zone")
|
||||
def test_would_remove_mgmt_other_zone(self, mock_dz):
|
||||
assert daemonfirewall._would_remove_mgmt("public", ["http"]) is False
|
||||
|
||||
@patch(
|
||||
"lib.firewall.get_config",
|
||||
return_value={
|
||||
"zones": {"public": {"services": ["http"], "interfaces": ["eth0"]}}
|
||||
},
|
||||
create=True,
|
||||
)
|
||||
@patch("daemon.handlers.firewall._default_zone", return_value="public")
|
||||
def test_config_apply_blocks_lockout_before_backup(self, mock_dz, mock_cfg):
|
||||
with (
|
||||
patch("daemon.handlers.firewall._save_backup") as mock_backup,
|
||||
pytest.raises(ConflictError) as exc,
|
||||
):
|
||||
daemonfirewall._config_apply()
|
||||
assert "https and ssh" in str(exc.value)
|
||||
mock_backup.assert_not_called()
|
||||
|
||||
@patch(
|
||||
"lib.firewall.get_config",
|
||||
return_value={
|
||||
"zones": {
|
||||
"public": {
|
||||
"target": "DEFAULT",
|
||||
"interfaces": ["eth0"],
|
||||
"services": ["http"],
|
||||
"masquerade": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
create=True,
|
||||
)
|
||||
@patch(
|
||||
"daemon.handlers.firewall.run",
|
||||
return_value="public\ninternal\ntarget: default\ninterfaces: \nsources: \nservices: \nports: \nprotocols: \nforward-ports: \nmasquerade: no\nics: no\nrich-rules: \nicmp-blocks: \nmodule: \n",
|
||||
)
|
||||
@patch("daemon.handlers.firewall._default_zone", return_value="public")
|
||||
def test_config_apply_force_bypasses_guard(self, mock_dz, mock_run, mock_cfg):
|
||||
with (
|
||||
patch(
|
||||
"daemon.handlers.firewall._save_backup", return_value="/tmp/rules.json"
|
||||
),
|
||||
patch(
|
||||
"daemon.handlers.firewall._get_state",
|
||||
return_value={"zones": {"public": {}}},
|
||||
),
|
||||
patch("daemon.handlers.firewall.refresh_state"),
|
||||
):
|
||||
result = daemonfirewall._config_apply(force=True)
|
||||
assert result["applied_zones"] == ["public"]
|
||||
|
||||
@patch(
|
||||
"daemon.handlers.firewall._config_apply",
|
||||
return_value={"applied_zones": ["public"], "backup": "/tmp/rules.json"},
|
||||
)
|
||||
@patch("daemon.handlers.firewall.bus")
|
||||
def test_config_apply_handler_force_propagation(self, mock_bus, mock_apply):
|
||||
mock_bus.emit.return_value = MagicMock(affected_subsystems=[])
|
||||
with patch("daemon.handlers.firewall.refresh_state"):
|
||||
daemonfirewall.config_apply(None, None)
|
||||
mock_apply.assert_called_once_with(force=False)
|
||||
mock_apply.reset_mock()
|
||||
daemonfirewall.config_apply(None, {"force": True})
|
||||
mock_apply.assert_called_once_with(force=True)
|
||||
|
||||
|
||||
class TestDaemonConfigPending:
|
||||
@patch("lib.state.state")
|
||||
def test_returns_pending(self, mock_st):
|
||||
|
||||
Reference in New Issue
Block a user