From 6c432473958fd2e7612e3c8f83e30465b3564f65 Mon Sep 17 00:00:00 2001 From: Panda <735591750@QQ.COM> Date: Wed, 30 Sep 2026 18:41:52 +0800 Subject: [PATCH] [core] Ignore empty CLI address cache entries (#19924) --- esphome/address_cache.py | 11 ++++++-- tests/unit_tests/test_address_cache.py | 36 ++++++++++++++++++++++++++ tests/unit_tests/test_helpers.py | 20 ++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/esphome/address_cache.py b/esphome/address_cache.py index 4fb3689818..8d7107b782 100644 --- a/esphome/address_cache.py +++ b/esphome/address_cache.py @@ -148,6 +148,13 @@ class AddressCache: continue hostname, ips = arg.split("=", 1) # Normalize hostname for consistent lookups - normalized = normalize_hostname(hostname) - cache[normalized] = [ip.strip() for ip in ips.split(",")] + normalized = normalize_hostname(hostname.strip()) + addresses = [ip for value in ips.split(",") if (ip := value.strip())] + if not normalized or not addresses: + _LOGGER.warning( + "Invalid cache entry: %s (hostname and at least one address are required)", + arg, + ) + continue + cache[normalized] = addresses return cache diff --git a/tests/unit_tests/test_address_cache.py b/tests/unit_tests/test_address_cache.py index 1ca28c4f02..71fb632d6d 100644 --- a/tests/unit_tests/test_address_cache.py +++ b/tests/unit_tests/test_address_cache.py @@ -257,6 +257,42 @@ def test_from_cli_args_invalid_format(caplog: LogCaptureFixture) -> None: assert "Invalid cache format: also_invalid" in caplog.text +@pytest.mark.parametrize( + "args,expected", + [ + (["device.local="], {}), + (["device.local= , , "], {}), + (["=192.168.1.10"], {}), + (["...=192.168.1.10"], {}), + ( + [" device.local. = , 192.168.1.10, , fe80::1, "], + {"device.local": ["192.168.1.10", "fe80::1"]}, + ), + ], +) +def test_from_cli_args_discards_empty_addresses_and_hostnames( + args: list[str], expected: dict[str, list[str]], caplog: LogCaptureFixture +) -> None: + """Only usable cache entries may prevent normal hostname resolution.""" + cache = AddressCache.from_cli_args(args, args) + + assert cache.mdns_cache == expected + assert cache.dns_cache == expected + assert cache.has_cache() == bool(expected) + if not expected: + assert cache.get_addresses("device.local") is None + assert "Invalid cache entry" in caplog.text + + +def test_invalid_cache_entry_does_not_replace_valid_entry() -> None: + """A later empty entry must not remove an earlier usable address.""" + args = ["device.local=192.168.1.10", "device.local=", "server.com=10.0.0.1"] + cache = AddressCache.from_cli_args(args, args) + + assert cache.get_mdns_addresses("DEVICE.LOCAL.") == ["192.168.1.10"] + assert cache.get_dns_addresses("server.com") == ["10.0.0.1"] + + def test_from_cli_args_ipv6() -> None: """Test handling of IPv6 addresses.""" mdns_args: list[str] = ["device.local=fe80::1,2001:db8::1"] diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index ff82fa3c80..5bdecf2fd3 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1129,6 +1129,26 @@ def test_resolve_ip_address_cache_miss() -> None: assert result[0][4][0] == "192.168.1.100" +@pytest.mark.parametrize("hostname", ["test.local", "example.com"]) +def test_resolve_ip_address_empty_cache_entry_falls_back(hostname: str) -> None: + """An empty CLI cache entry must use normal DNS or mDNS resolution.""" + cache = AddressCache.from_cli_args([f"{hostname}="], [f"{hostname}="]) + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + MockResolver.return_value.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address(hostname, 6053, address_cache=cache) + + MockResolver.assert_called_once_with([hostname], 6053) + assert result[0][4][0] == "192.168.1.100" + + def test_resolve_ip_address_mixed_cached_uncached() -> None: """Test resolution with mix of cached and uncached hosts.""" cache = AddressCache(mdns_cache={"cached.local": ["192.168.1.50"]})