refactor: daemon collectors, thin webui proxies, pure config reads
- move state collectors from lib/state.py to daemon/collectors/ (7 modules, registration side-effect; daemon/server.py imports the package before the first populate()) - webui/api: new daemon_route() decorator factory in common.py collapses the try/except daemon-proxy boilerplate in all 8 blueprints (rules/params/body/transform keep responses identical) - firewall: interface-coverage invariant — config is the source of truth for zone interfaces (absent key = empty, no hands-off zones); pure validate_coverage() enforced at save (400) and apply (409, force: true overrides), top-level `unmanaged` exemption - lib: get_config() reads are now pure (no dir creation or writes); new lib/bootstrap.py creates runtime dirs and persists the one-shot nginx legacy migration at daemon start, after system_import (lib.nginx.migrate_config_file) - lib/common: compute_pending() apply-bookkeeping helper - daemon: emit_and_refresh() handler helper; refresh_state(bump=) so /status/refresh no longer bumps versions (poll/mutation only) - acme: move --log last so acme.sh never treats a real arg as the log-file argument - docs: AGENTS.md, config.md, state-model.md, api.md updated; HARDEN.md dropped (plan implemented); apply-confirm force wording Tests: 917 passed; ruff check + format clean.
This commit is contained in:
+31
-19
@@ -3,7 +3,9 @@
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import lib
|
||||
import daemon.collectors.acme
|
||||
import daemon.collectors.dnsmasq
|
||||
import daemon.collectors.firewall
|
||||
from lib.state import State, state
|
||||
|
||||
|
||||
@@ -51,9 +53,9 @@ class TestState:
|
||||
|
||||
|
||||
class TestCollectAll:
|
||||
@patch("lib.state.run")
|
||||
@patch("daemon.collectors.firewall.run")
|
||||
def test_collect_firewall_returns_dict(self, mock_run):
|
||||
from lib.state import _collect_firewall
|
||||
from daemon.collectors.firewall import _collect_firewall
|
||||
|
||||
def run_side(args, **kwargs):
|
||||
if "--get-active-zones" in args:
|
||||
@@ -88,10 +90,10 @@ class TestCollectAll:
|
||||
assert "interfaces" in result
|
||||
assert "timestamp" in result
|
||||
|
||||
@patch("lib.state.run")
|
||||
@patch("daemon.collectors.firewall.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
|
||||
from daemon.collectors.firewall import _collect_firewall
|
||||
|
||||
def run_side(args, **kwargs):
|
||||
if "--get-active-zones" in args:
|
||||
@@ -144,10 +146,10 @@ class TestCollectAll:
|
||||
assert vlan_iface["ips"], "VLAN interface should have collected IPs"
|
||||
assert "10.0.0.1/24" in vlan_iface["ips"]
|
||||
|
||||
@patch("lib.state.get_service_descriptions")
|
||||
@patch("lib.state.run")
|
||||
@patch("daemon.collectors.firewall.get_service_descriptions")
|
||||
@patch("daemon.collectors.firewall.run")
|
||||
def test_collect_firewall_includes_service_descriptions(self, mock_run, mock_desc):
|
||||
from lib.state import _collect_firewall
|
||||
from daemon.collectors.firewall import _collect_firewall
|
||||
|
||||
def run_side(args, **kwargs):
|
||||
if "--get-active-zones" in args:
|
||||
@@ -168,11 +170,11 @@ class TestCollectAll:
|
||||
mock_desc.assert_called_once_with()
|
||||
assert result["service_descriptions"] == descs
|
||||
|
||||
@patch("lib.state.run_proc")
|
||||
@patch("daemon.collectors.dnsmasq.run_proc")
|
||||
def test_collect_dnsmasq_returns_dict(self, mock_proc):
|
||||
from unittest.mock import Mock
|
||||
|
||||
from lib.state import _collect_dnsmasq
|
||||
from daemon.collectors.dnsmasq import _collect_dnsmasq
|
||||
|
||||
mock_proc.return_value = Mock(stdout="active\n", returncode=0)
|
||||
result = _collect_dnsmasq()
|
||||
@@ -181,12 +183,12 @@ class TestCollectAll:
|
||||
assert "config" in result
|
||||
assert "leases" in result
|
||||
|
||||
@patch("lib.state.run_proc")
|
||||
@patch("daemon.collectors.dnsmasq.run_proc")
|
||||
def test_collect_dnsmasq_pending_diff(self, mock_proc, tmp_path, monkeypatch):
|
||||
from unittest.mock import Mock
|
||||
|
||||
from daemon.collectors.dnsmasq import _collect_dnsmasq
|
||||
from lib.common import _APPLY_HASH_KEY, _LAST_APPLIED_CONFIG_KEY
|
||||
from lib.state import _collect_dnsmasq
|
||||
|
||||
(tmp_path / "config" / "dnsmasq").mkdir(parents=True)
|
||||
applied = {
|
||||
@@ -226,7 +228,9 @@ class TestCollectAll:
|
||||
_APPLY_HASH_KEY: "stale-hash",
|
||||
}
|
||||
(tmp_path / "config" / "dnsmasq" / "config.json").write_text(json.dumps(cfg))
|
||||
monkeypatch.setattr("lib.state.PROJECT_DIR", tmp_path)
|
||||
monkeypatch.setattr(
|
||||
"lib.dnsmasq.CONFIG_PATH", tmp_path / "config" / "dnsmasq" / "config.json"
|
||||
)
|
||||
# service check -> active; lease file read -> no lines
|
||||
mock_proc.return_value = Mock(stdout="active\n", returncode=0)
|
||||
|
||||
@@ -257,15 +261,19 @@ class TestAcmeCollectNonFatal:
|
||||
"""A broken acme.sh must not clear the acme subsystem (dashboard guard)."""
|
||||
|
||||
def test_list_failure_yields_empty_certs_and_error(self):
|
||||
from lib.state import _collect_acme
|
||||
from daemon.collectors.acme import _collect_acme
|
||||
|
||||
with (
|
||||
patch.object(lib.state, "_get_acme_email", return_value="a@b.c"),
|
||||
patch.object(
|
||||
daemon.collectors.acme, "_get_acme_email", return_value="a@b.c"
|
||||
),
|
||||
patch(
|
||||
"lib.acme.list_certs",
|
||||
side_effect=RuntimeError("acme.sh failed with exit code 2"),
|
||||
),
|
||||
patch.object(lib.state, "_parse_account_conf", return_value=_ACCOUNT),
|
||||
patch.object(
|
||||
daemon.collectors.acme, "_parse_account_conf", return_value=_ACCOUNT
|
||||
),
|
||||
):
|
||||
result = _collect_acme()
|
||||
|
||||
@@ -275,12 +283,16 @@ class TestAcmeCollectNonFatal:
|
||||
assert "exit code 2" in result["status"]["error"]
|
||||
|
||||
def test_success_reports_no_error(self):
|
||||
from lib.state import _collect_acme
|
||||
from daemon.collectors.acme import _collect_acme
|
||||
|
||||
with (
|
||||
patch.object(lib.state, "_get_acme_email", return_value="a@b.c"),
|
||||
patch.object(
|
||||
daemon.collectors.acme, "_get_acme_email", return_value="a@b.c"
|
||||
),
|
||||
patch("lib.acme.list_certs", return_value=[]),
|
||||
patch.object(lib.state, "_parse_account_conf", return_value=_ACCOUNT),
|
||||
patch.object(
|
||||
daemon.collectors.acme, "_parse_account_conf", return_value=_ACCOUNT
|
||||
),
|
||||
):
|
||||
result = _collect_acme()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user