Skip to content

Commit fdd00e6

Browse files
committed
Add discovery tests
1 parent 5983eea commit fdd00e6

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed

tests/test_discovery.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,131 @@ async def test_async_discover_devices_generic_oserror(
398398
assert "cannot_connect" in str(excinfo.value)
399399
close_mock = cast(MagicMock, mock_transport.close)
400400
close_mock.assert_not_called()
401+
402+
403+
@pytest.mark.asyncio
404+
async def test_parse_airos_packet_short_for_next_tlv() -> None:
405+
"""Test parsing stops gracefully after the MAC TLV when no more data exists."""
406+
protocol = AirOSDiscoveryProtocol(AsyncMock())
407+
# Header + valid MAC TLV, but then abruptly ends
408+
data_with_fragment = (
409+
b"\x01\x06\x00\x00\x00\x00" + b"\x06" + bytes.fromhex("0123456789CD")
410+
)
411+
host_ip = "192.168.1.100"
412+
413+
with patch("airos.discovery._LOGGER.debug") as mock_log_debug:
414+
parsed_data = protocol.parse_airos_packet(data_with_fragment, host_ip)
415+
416+
assert parsed_data is not None
417+
assert parsed_data["mac_address"] == "01:23:45:67:89:CD"
418+
# The debug log for the successfully parsed MAC address should be called exactly once.
419+
mock_log_debug.assert_called_once_with(
420+
"Parsed MAC from type 0x06: 01:23:45:67:89:CD"
421+
)
422+
423+
424+
@pytest.mark.asyncio
425+
async def test_parse_airos_packet_truncated_two_byte_tlv() -> None:
426+
"""Test parsing with a truncated 2-byte length field TLV."""
427+
protocol = AirOSDiscoveryProtocol(AsyncMock())
428+
# Header + valid MAC TLV, then a valid type (0x0a) but a truncated length field
429+
data_with_fragment = (
430+
b"\x01\x06\x00\x00\x00\x00"
431+
+ b"\x06"
432+
+ bytes.fromhex("0123456789CD")
433+
+ b"\x0a\x00" # TLV type 0x0a, followed by only 1 byte for length (should be 2)
434+
)
435+
host_ip = "192.168.1.100"
436+
437+
with patch("airos.discovery._LOGGER.warning") as mock_log_warning:
438+
with pytest.raises(AirOSEndpointError):
439+
protocol.parse_airos_packet(data_with_fragment, host_ip)
440+
441+
mock_log_warning.assert_called_once()
442+
assert "no 2-byte length field" in mock_log_warning.call_args[0][0]
443+
444+
445+
@pytest.mark.asyncio
446+
async def test_parse_airos_packet_malformed_tlv_length() -> None:
447+
"""Test parsing with a malformed TLV length field."""
448+
protocol = AirOSDiscoveryProtocol(AsyncMock())
449+
# Header + valid MAC TLV, then a valid type (0x02) but a truncated length field
450+
data_with_fragment = (
451+
b"\x01\x06\x00\x00\x00\x00"
452+
+ b"\x06"
453+
+ bytes.fromhex("0123456789CD")
454+
+ b"\x02\x00" # TLV type 0x02, followed by only 1 byte for length (should be 2)
455+
)
456+
host_ip = "192.168.1.100"
457+
458+
with patch("airos.discovery._LOGGER.warning") as mock_log_warning:
459+
with pytest.raises(AirOSEndpointError):
460+
protocol.parse_airos_packet(data_with_fragment, host_ip)
461+
462+
mock_log_warning.assert_called_once()
463+
assert "no 2-byte length field" in mock_log_warning.call_args[0][0]
464+
465+
466+
@pytest.mark.parametrize(
467+
"packet_fragment, unhandled_type",
468+
[
469+
(b"\x0e\x00\x02\x01\x02", "0xe"), # Unhandled 2-byte length TLV
470+
(b"\x10\x00\x02\x01\x02", "0x10"), # Unhandled 2-byte length TLV
471+
],
472+
)
473+
@pytest.mark.asyncio
474+
async def test_parse_airos_packet_unhandled_tlv_continues_parsing(
475+
packet_fragment: bytes, unhandled_type: str
476+
) -> None:
477+
"""Test that the parser logs an unhandled TLV type but continues parsing the packet."""
478+
protocol = AirOSDiscoveryProtocol(AsyncMock())
479+
480+
# Construct a packet with a valid MAC TLV followed by the unhandled TLV
481+
valid_mac_tlv = b"\x06" + bytes.fromhex("0123456789CD")
482+
base_packet = b"\x01\x06\x00\x00\x00\x00"
483+
484+
# This new packet structure ensures two TLVs are present
485+
malformed_packet = base_packet + valid_mac_tlv + packet_fragment
486+
host_ip = "192.168.1.100"
487+
488+
with patch("airos.discovery._LOGGER.debug") as mock_log_debug:
489+
parsed_data = protocol.parse_airos_packet(malformed_packet, host_ip)
490+
491+
assert parsed_data is not None
492+
assert parsed_data["mac_address"] == "01:23:45:67:89:CD"
493+
494+
# Now, two debug logs are expected: one for the MAC and one for the unhandled TLV.
495+
assert mock_log_debug.call_count == 2
496+
497+
# Check the first log call for the MAC address
498+
assert (
499+
mock_log_debug.call_args_list[0][0][0]
500+
== "Parsed MAC from type 0x06: 01:23:45:67:89:CD"
501+
)
502+
503+
# Check the second log call for the unhandled TLV
504+
log_message = mock_log_debug.call_args_list[1][0][0]
505+
assert f"Unhandled TLV type: {unhandled_type}" in log_message
506+
assert "with length" in log_message
507+
508+
509+
@pytest.mark.asyncio
510+
async def test_async_discover_devices_generic_exception(
511+
mock_datagram_endpoint: tuple[asyncio.DatagramTransport, AirOSDiscoveryProtocol],
512+
) -> None:
513+
"""Test discovery handles a generic exception during the main execution."""
514+
mock_transport, _ = mock_datagram_endpoint
515+
516+
with (
517+
patch(
518+
"asyncio.sleep", new=AsyncMock(side_effect=Exception("Unexpected error"))
519+
),
520+
patch("airos.discovery._LOGGER.exception") as mock_log_exception,
521+
pytest.raises(AirOSListenerError) as excinfo,
522+
):
523+
await airos_discover_devices(timeout=1)
524+
525+
assert "cannot_connect" in str(excinfo.value)
526+
mock_log_exception.assert_called_once()
527+
close_mock = cast(MagicMock, mock_transport.close)
528+
close_mock.assert_called_once()

0 commit comments

Comments
 (0)