@@ -433,3 +433,94 @@ def call_to_tuple(c):
433433 expected_calls_set = {call_to_tuple (c ) for c in expected_calls }
434434
435435 assert actual_calls_set == expected_calls_set
436+
437+
438+ def test_create_sockets_multicast_addresses_v4 () -> None :
439+ """Extra IPv4 addresses join the listen socket multicast group but get no respond socket."""
440+ listen_mock = Mock (spec = socket .socket )
441+ respond_mock = Mock (spec = socket .socket )
442+
443+ def _new_socket (bind_addr , ** kwargs ):
444+ return listen_mock if bind_addr == ("" ,) else respond_mock
445+
446+ with (
447+ patch ("zeroconf._utils.net.new_socket" , side_effect = _new_socket ),
448+ patch ("zeroconf._utils.net.add_multicast_member" , return_value = True ) as mock_add ,
449+ patch ("zeroconf._utils.net.set_respond_socket_multicast_options" ),
450+ patch ("zeroconf._utils.net.socket.socket.setsockopt" ),
451+ ):
452+ listen_socket , respond_sockets = r .create_sockets (
453+ interfaces = ["127.0.0.1" ],
454+ multicast_addresses = ["192.168.1.5" , "10.0.0.5" ],
455+ ip_version = r .IPVersion .V4Only ,
456+ )
457+
458+ assert listen_socket is listen_mock
459+ assert respond_sockets == [respond_mock ]
460+ joined = [c .args [1 ] for c in mock_add .call_args_list if c .args [0 ] is listen_mock ]
461+ assert "127.0.0.1" in joined
462+ assert "192.168.1.5" in joined
463+ assert "10.0.0.5" in joined
464+
465+
466+ def test_create_sockets_multicast_addresses_v6 () -> None :
467+ """Extra IPv6 addresses join the listen socket multicast group."""
468+ listen_mock = Mock (spec = socket .socket )
469+ respond_mock = Mock (spec = socket .socket )
470+
471+ def _new_socket (bind_addr , ** kwargs ):
472+ return listen_mock if bind_addr == ("" ,) else respond_mock
473+
474+ with (
475+ patch ("zeroconf._utils.net.new_socket" , side_effect = _new_socket ),
476+ patch ("zeroconf._utils.net.add_multicast_member" , return_value = True ) as mock_add ,
477+ patch ("zeroconf._utils.net.set_respond_socket_multicast_options" ),
478+ patch (
479+ "zeroconf._utils.net.ifaddr.get_adapters" ,
480+ return_value = _generate_mock_adapters (),
481+ ),
482+ patch ("zeroconf._utils.net.socket.socket.setsockopt" ),
483+ ):
484+ r .create_sockets (
485+ interfaces = [1 ],
486+ multicast_addresses = ["2001:db8::" ],
487+ ip_version = r .IPVersion .V6Only ,
488+ )
489+
490+ joined = [c .args [1 ] for c in mock_add .call_args_list if c .args [0 ] is listen_mock ]
491+ # Both the interface index 1 and the extra multicast address resolve to the
492+ # same adapter tuple — what matters is the listen socket joined that group.
493+ assert (("2001:db8::" , 1 , 1 ), 1 ) in joined
494+
495+
496+ def test_create_sockets_multicast_addresses_unicast_rejected () -> None :
497+ """multicast_addresses is incompatible with unicast=True (there is no listen socket)."""
498+ with pytest .raises (ValueError ):
499+ r .create_sockets (
500+ interfaces = ["127.0.0.1" ],
501+ multicast_addresses = ["192.168.1.5" ],
502+ unicast = True ,
503+ )
504+
505+
506+ def test_create_sockets_multicast_addresses_default_path () -> None :
507+ """multicast_addresses also works on the InterfaceChoice.Default fast path."""
508+ listen_mock = Mock (spec = socket .socket )
509+
510+ with (
511+ patch ("zeroconf._utils.net.new_socket" , return_value = listen_mock ),
512+ patch ("zeroconf._utils.net.add_multicast_member" , return_value = True ) as mock_add ,
513+ patch ("zeroconf._utils.net.set_respond_socket_multicast_options" ),
514+ patch ("zeroconf._utils.net.socket.socket.setsockopt" ),
515+ ):
516+ listen_socket , respond_sockets = r .create_sockets (
517+ interfaces = r .InterfaceChoice .Default ,
518+ multicast_addresses = ["192.168.1.5" ],
519+ ip_version = r .IPVersion .V4Only ,
520+ )
521+
522+ assert listen_socket is listen_mock
523+ assert respond_sockets == [listen_mock ]
524+ joined = [c .args [1 ] for c in mock_add .call_args_list if c .args [0 ] is listen_mock ]
525+ assert "0.0.0.0" in joined
526+ assert "192.168.1.5" in joined
0 commit comments