Files
mteehan 8c13ad55ce Add update-vendor.sh symlink support, unify install.sh vendor flow
- update-vendor.sh now creates webui/vendor symlinks (htm.js)
- install.sh calls update-vendor.sh after package install
- Add vendor/.empty and webui/vendor/.empty as directory placeholders in git
2026-07-01 00:55:03 +00:00

189 lines
6.5 KiB
Python

from unittest.mock import patch
import pytest
from lib import common, dnsmasq
@pytest.fixture
def temp_data_dir(tmp_path):
original_config_dir = dnsmasq.CONFIG_DIR
original = dnsmasq.DATA_DIR
original_config = dnsmasq.CONFIG_PATH
original_fragments = dnsmasq.FRAGMENTS_DIR
dnsmasq.CONFIG_DIR = tmp_path / "dnsmasq"
dnsmasq.DATA_DIR = tmp_path / "dnsmasq"
dnsmasq.CONFIG_PATH = dnsmasq.CONFIG_DIR / "config.json"
dnsmasq.FRAGMENTS_DIR = dnsmasq.DATA_DIR / "fragments"
dnsmasq.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
dnsmasq.DATA_DIR.mkdir(parents=True, exist_ok=True)
dnsmasq.FRAGMENTS_DIR.mkdir(parents=True, exist_ok=True)
yield tmp_path
dnsmasq.CONFIG_DIR = original_config_dir
dnsmasq.DATA_DIR = original
dnsmasq.CONFIG_PATH = original_config
dnsmasq.FRAGMENTS_DIR = original_fragments
class TestDeepMerge:
def test_merge_flat_dicts(self):
base = {"a": 1, "b": 2}
override = {"b": 3, "c": 4}
result = common.deep_merge(base, override)
assert result == {"a": 1, "b": 3, "c": 4}
def test_merge_nested_dicts(self):
base = {"a": {"x": 1, "y": 2}}
override = {"a": {"y": 3, "z": 4}}
result = common.deep_merge(base, override)
assert result == {"a": {"x": 1, "y": 3, "z": 4}}
def test_merge_non_dict_override(self):
base = {"a": {"x": 1}}
override = {"a": "flat"}
result = common.deep_merge(base, override)
assert result == {"a": "flat"}
class TestGetConfig:
@patch("lib.dnsmasq.load_json")
def test_returns_default_when_no_config(self, mock_load, temp_data_dir):
mock_load.return_value = {}
result = dnsmasq.get_config()
assert "dhcp" in result
assert "dns" in result
assert result["dns"]["upstreams"] == ["8.8.8.8", "1.1.1.1"]
@patch("lib.dnsmasq.load_json")
def test_merges_with_existing_config(self, mock_load, temp_data_dir):
mock_load.return_value = {"dns": {"upstreams": ["9.9.9.9"]}}
result = dnsmasq.get_config()
assert result["dns"]["upstreams"] == ["9.9.9.9"]
class TestSaveConfig:
def test_saves_and_reloads(self, temp_data_dir):
cfg = {"dns": {"upstreams": ["1.2.3.4"], "domain": "test.lan"}}
dnsmasq.save_config(cfg)
loaded = dnsmasq.get_config()
assert loaded["dns"]["upstreams"] == ["1.2.3.4"]
assert loaded["dns"]["domain"] == "test.lan"
class TestUpstreamsAndDomain:
def test_set_upstreams(self, temp_data_dir):
dnsmasq.set_upstreams(["1.1.1.1", "9.9.9.9"])
cfg = dnsmasq.get_config()
assert cfg["dns"]["upstreams"] == ["1.1.1.1", "9.9.9.9"]
def test_set_domain(self, temp_data_dir):
dnsmasq.set_domain("internal.lan")
cfg = dnsmasq.get_config()
assert cfg["dns"]["domain"] == "internal.lan"
def test_clear_domain(self, temp_data_dir):
dnsmasq.set_domain("internal.lan")
dnsmasq.set_domain(None)
cfg = dnsmasq.get_config()
assert cfg["dns"]["domain"] is None
# ── Daemon handler tests (NotFoundError on missing resources) ──
class TestDaemonRemoveStaticLease:
@patch("daemon.handlers.dnsmasq._get_config")
@patch("daemon.handlers.dnsmasq._save_config")
def test_raises_not_found_when_missing(self, mock_save, mock_get):
from daemon.handlers import dnsmasq as daemon_dnsmasq
mock_get.return_value = {"dhcp": {"static_leases": []}, "dns": {}}
with pytest.raises(Exception, match="not found"):
daemon_dnsmasq.remove_static_lease(None, {"mac": "FF:FF:FF"})
@patch("daemon.handlers.dnsmasq._get_config")
@patch("daemon.handlers.dnsmasq._save_config")
def test_succeeds_when_exists(self, mock_save, mock_get):
from daemon.handlers import dnsmasq as daemon_dnsmasq
mock_get.return_value = {
"dhcp": {"static_leases": [{"mac": "aa:bb:cc", "ip": "10.0.0.1"}]},
"dns": {},
}
result = daemon_dnsmasq.remove_static_lease(None, {"mac": "AA:BB:CC"})
assert result["mac"] == "AA:BB:CC"
class TestDaemonRemoveDnsRecord:
@patch("daemon.handlers.dnsmasq._get_config")
@patch("daemon.handlers.dnsmasq._save_config")
def test_raises_not_found_when_missing(self, mock_save, mock_get):
from daemon.handlers import dnsmasq as daemon_dnsmasq
mock_get.return_value = {
"dhcp": {},
"dns": {"custom_records": []},
}
with pytest.raises(Exception, match="not found"):
daemon_dnsmasq.remove_dns_record(None, {"name": "nonexistent"})
@patch("daemon.handlers.dnsmasq._get_config")
@patch("daemon.handlers.dnsmasq._save_config")
def test_succeeds_when_exists(self, mock_save, mock_get):
from daemon.handlers import dnsmasq as daemon_dnsmasq
mock_get.return_value = {
"dhcp": {},
"dns": {"custom_records": [{"name": "host", "address": "10.0.0.1"}]},
}
result = daemon_dnsmasq.remove_dns_record(None, {"name": "host"})
assert result["name"] == "host"
class TestDaemonRemoveDhcpRange:
@patch("daemon.handlers.dnsmasq._get_config")
@patch("daemon.handlers.dnsmasq._save_config")
def test_raises_not_found_when_missing(self, mock_save, mock_get):
from daemon.handlers import dnsmasq as daemon_dnsmasq
mock_get.return_value = {
"dhcp": {"ranges": []},
"dns": {},
}
with pytest.raises(Exception, match="not found"):
daemon_dnsmasq.remove_dhcp_range(
None,
{
"interface": "eth0",
"start": "10.0.0.100",
"end": "10.0.0.200",
},
)
@patch("daemon.handlers.dnsmasq._get_config")
@patch("daemon.handlers.dnsmasq._save_config")
def test_succeeds_when_exists(self, mock_save, mock_get):
from daemon.handlers import dnsmasq as daemon_dnsmasq
mock_get.return_value = {
"dhcp": {
"ranges": [
{
"interface": "eth0",
"start": "10.0.0.100",
"end": "10.0.0.200",
}
]
},
"dns": {},
}
result = daemon_dnsmasq.remove_dhcp_range(
None,
{
"interface": "eth0",
"start": "10.0.0.100",
"end": "10.0.0.200",
},
)
assert result["interface"] == "eth0"