@@ -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