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"