from unittest.mock import patch import pytest from lib import 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 = dnsmasq._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 = dnsmasq._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 = dnsmasq._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 TestSetDhcpRange: def test_add_new_range(self, temp_data_dir): dnsmasq.set_dhcp_range("eth1", "192.168.1.100", "192.168.1.200") cfg = dnsmasq.get_config() assert len(cfg["dhcp"]["ranges"]) == 1 assert cfg["dhcp"]["ranges"][0]["interface"] == "eth1" assert cfg["dhcp"]["ranges"][0]["start"] == "192.168.1.100" def test_replace_existing_range(self, temp_data_dir): dnsmasq.set_dhcp_range("eth1", "10.0.0.100", "10.0.0.200") dnsmasq.set_dhcp_range("eth1", "10.0.0.150", "10.0.0.250") cfg = dnsmasq.get_config() assert len(cfg["dhcp"]["ranges"]) == 1 assert cfg["dhcp"]["ranges"][0]["start"] == "10.0.0.150" class TestStaticLeases: def test_add_static_lease(self, temp_data_dir): dnsmasq.add_static_lease("AA:BB:CC:DD:EE:FF", "10.0.0.50", "printer") cfg = dnsmasq.get_config() assert len(cfg["dhcp"]["static_leases"]) == 1 assert cfg["dhcp"]["static_leases"][0]["mac"] == "AA:BB:CC:DD:EE:FF" assert cfg["dhcp"]["static_leases"][0]["hostname"] == "printer" def test_update_static_lease(self, temp_data_dir): dnsmasq.add_static_lease("AA:BB:CC", "10.0.0.50") dnsmasq.add_static_lease("AA:BB:CC", "10.0.0.51") cfg = dnsmasq.get_config() assert len(cfg["dhcp"]["static_leases"]) == 1 assert cfg["dhcp"]["static_leases"][0]["ip"] == "10.0.0.51" def test_remove_static_lease(self, temp_data_dir): dnsmasq.add_static_lease("AA:BB:CC", "10.0.0.50") dnsmasq.add_static_lease("11:22:33", "10.0.0.51") dnsmasq.remove_static_lease("aa:bb:cc") cfg = dnsmasq.get_config() assert len(cfg["dhcp"]["static_leases"]) == 1 assert cfg["dhcp"]["static_leases"][0]["mac"] == "11:22:33" class TestDnsRecords: def test_add_dns_record(self, temp_data_dir): dnsmasq.add_dns_record("host", "10.0.0.100") cfg = dnsmasq.get_config() assert len(cfg["dns"]["custom_records"]) == 1 def test_remove_dns_record(self, temp_data_dir): dnsmasq.add_dns_record("host", "10.0.0.100") dnsmasq.add_dns_record("other", "10.0.0.101") dnsmasq.remove_dns_record("host") cfg = dnsmasq.get_config() assert len(cfg["dns"]["custom_records"]) == 1 assert cfg["dns"]["custom_records"][0]["name"] == "other" class TestParseLeaseLine: def test_valid_line(self): line = "1700000000 AA:BB:CC:DD:EE:FF 10.0.0.50 printer eth1" result = dnsmasq._parse_lease_line(line) assert result is not None assert result["mac"] == "AA:BB:CC:DD:EE:FF" assert result["ip"] == "10.0.0.50" assert result["hostname"] == "printer" def test_empty_line(self): assert dnsmasq._parse_lease_line("") is None def test_comment_line(self): assert dnsmasq._parse_lease_line("# comment") is None def test_short_line(self): assert dnsmasq._parse_lease_line("incomplete") is None def test_minimal_fields(self): line = "1700000000 AA:BB:CC 10.0.0.50" result = dnsmasq._parse_lease_line(line) assert result is not None assert result["hostname"] == "" assert result["interface"] == "" 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