diff --git a/tests/discovery/__init__.py b/tests/discovery/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/discovery/mdns/__init__.py b/tests/discovery/mdns/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/discovery/mdns/test_broadcaster.py b/tests/discovery/mdns/test_broadcaster.py new file mode 100644 index 00000000..d4722ba7 --- /dev/null +++ b/tests/discovery/mdns/test_broadcaster.py @@ -0,0 +1,90 @@ +""" +Unit tests for mDNS broadcaster component. +""" +import socket +import pytest +from zeroconf import ServiceInfo, Zeroconf + +from libp2p.discovery.mdns.broadcaster import PeerBroadcaster +from libp2p.peer.id import ID + + +class TestPeerBroadcaster: + """Basic unit tests for PeerBroadcaster.""" + + def test_broadcaster_initialization(self): + """Test that broadcaster initializes correctly.""" + zeroconf = Zeroconf() + service_type = "_p2p._udp.local." + service_name = "test-peer._p2p._udp.local." + peer_id = "QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN" # String, not ID object + port = 8000 + + broadcaster = PeerBroadcaster( + zeroconf=zeroconf, + service_type=service_type, + service_name=service_name, + peer_id=peer_id, + port=port + ) + + assert broadcaster.zeroconf == zeroconf + assert broadcaster.service_type == service_type + assert broadcaster.service_name == service_name + assert broadcaster.peer_id == peer_id + assert broadcaster.port == port + + # Clean up + zeroconf.close() + + def test_broadcaster_service_creation(self): + """Test that broadcaster creates valid service info.""" + zeroconf = Zeroconf() + service_type = "_p2p._udp.local." + service_name = "test-peer2._p2p._udp.local." + peer_id_obj = ID.from_base58("QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN") + peer_id = str(peer_id_obj) # Convert to string + port = 8000 + + broadcaster = PeerBroadcaster( + zeroconf=zeroconf, + service_type=service_type, + service_name=service_name, + peer_id=peer_id, + port=port + ) + + # Verify service was created and registered + service_info = broadcaster.service_info + assert service_info is not None + assert service_info.type == service_type + assert service_info.name == service_name + assert service_info.port == port + assert b"id" in service_info.properties + assert service_info.properties[b"id"] == peer_id.encode() + + # Clean up + zeroconf.close() + + def test_broadcaster_start_stop(self): + """Test that broadcaster can start and stop correctly.""" + zeroconf = Zeroconf() + service_type = "_p2p._udp.local." + service_name = "test-start-stop._p2p._udp.local." + peer_id_obj = ID.from_base58("QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N") + peer_id = str(peer_id_obj) # Convert to string + port = 8001 + + broadcaster = PeerBroadcaster( + zeroconf=zeroconf, + service_type=service_type, + service_name=service_name, + peer_id=peer_id, + port=port + ) + + # Service should be registered + assert broadcaster.service_info is not None + + # Clean up + zeroconf.close() diff --git a/tests/discovery/mdns/test_integration.py b/tests/discovery/mdns/test_integration.py new file mode 100644 index 00000000..c7291aa7 --- /dev/null +++ b/tests/discovery/mdns/test_integration.py @@ -0,0 +1,128 @@ +""" +Basic integration tests for mDNS components. +""" +import socket +import pytest +from zeroconf import ServiceInfo, Zeroconf + +from libp2p.discovery.mdns.broadcaster import PeerBroadcaster +from libp2p.discovery.mdns.listener import PeerListener +from libp2p.peer.id import ID +from libp2p.peer.peerstore import PeerStore + + +class TestMDNSIntegration: + """Basic integration tests for mDNS components.""" + + def test_broadcaster_listener_basic_integration(self): + """Test basic broadcaster and listener integration with actual service discovery.""" + import time + + # Create two separate Zeroconf instances + zeroconf1 = Zeroconf() + zeroconf2 = Zeroconf() + + try: + # Set up broadcaster + broadcaster_peer_id_obj = ID.from_base58("QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN") + broadcaster_peer_id = str(broadcaster_peer_id_obj) # Convert to string + broadcaster = PeerBroadcaster( + zeroconf=zeroconf1, + service_type="_p2p._udp.local.", + service_name="broadcaster-peer._p2p._udp.local.", + peer_id=broadcaster_peer_id, + port=8000 + ) + + # Set up listener + peerstore = PeerStore() + listener = PeerListener( + peerstore=peerstore, + zeroconf=zeroconf2, + service_type="_p2p._udp.local.", + service_name="listener-peer._p2p._udp.local.", + ) + + # Verify initial state + assert broadcaster.service_info is not None + assert listener.discovered_services == {} + assert len(peerstore.peer_ids()) == 0 + + # Broadcaster registers its service + broadcaster.register() + + # Simulate discovery - listener discovers the broadcaster's service + listener.add_service( + zeroconf1, # Use broadcaster's zeroconf to find the service + "_p2p._udp.local.", + "broadcaster-peer._p2p._udp.local." + ) + + # Verify that the listener discovered the broadcaster + assert len(listener.discovered_services) > 0 + assert "broadcaster-peer._p2p._udp.local." in listener.discovered_services + + # Verify the discovered peer ID matches what was broadcast + discovered_peer_id = listener.discovered_services["broadcaster-peer._p2p._udp.local."] + assert str(discovered_peer_id) == broadcaster_peer_id + + # Verify the peer was added to the peerstore + assert len(peerstore.peer_ids()) > 0 + assert discovered_peer_id in peerstore.peer_ids() + + # Verify the addresses were correctly stored + addrs = peerstore.addrs(discovered_peer_id) + assert len(addrs) > 0 + # Should be TCP since we always use TCP protocol + assert "/tcp/8000" in str(addrs[0]) + + print(f"✅ Integration test successful!") + print(f" Broadcaster peer ID: {broadcaster_peer_id}") + print(f" Discovered peer ID: {discovered_peer_id}") + print(f" Discovered addresses: {[str(addr) for addr in addrs]}") + + # Clean up + broadcaster.unregister() + + finally: + zeroconf1.close() + zeroconf2.close() + + def test_service_info_extraction(self): + """Test service info extraction functionality.""" + peerstore = PeerStore() + zeroconf = Zeroconf() + + try: + listener = PeerListener( + peerstore=peerstore, + zeroconf=zeroconf, + service_type="_p2p._udp.local.", + service_name="test-listener._p2p._udp.local.", + ) + + # Create a test service info + test_peer_id = ID.from_base58("QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N") + hostname = socket.gethostname() + + service_info = ServiceInfo( + type_="_p2p._udp.local.", + name="test-service._p2p._udp.local.", + port=8001, + properties={b"id": str(test_peer_id).encode()}, + server=f"{hostname}.local.", + addresses=[socket.inet_aton("192.168.1.100")], + ) + + # Test extraction + peer_info = listener._extract_peer_info(service_info) + + assert peer_info is not None + assert peer_info.peer_id == test_peer_id + assert len(peer_info.addrs) == 1 + assert "/tcp/8001" in str(peer_info.addrs[0]) + + # Clean up + + finally: + zeroconf.close() diff --git a/tests/discovery/mdns/test_listener.py b/tests/discovery/mdns/test_listener.py new file mode 100644 index 00000000..aa4992c0 --- /dev/null +++ b/tests/discovery/mdns/test_listener.py @@ -0,0 +1,111 @@ +""" +Unit tests for mDNS listener component. +""" +import socket +import pytest +from zeroconf import ServiceInfo, Zeroconf + +from libp2p.discovery.mdns.listener import PeerListener +from libp2p.peer.id import ID +from libp2p.peer.peerstore import PeerStore +from libp2p.abc import Multiaddr + + +class TestPeerListener: + """Basic unit tests for PeerListener.""" + + def test_listener_initialization(self): + """Test that listener initializes correctly.""" + peerstore = PeerStore() + zeroconf = Zeroconf() + service_type = "_p2p._udp.local." + service_name = "local-peer._p2p._udp.local." + + listener = PeerListener( + peerstore=peerstore, + zeroconf=zeroconf, + service_type=service_type, + service_name=service_name, + ) + + assert listener.peerstore == peerstore + assert listener.zeroconf == zeroconf + assert listener.service_type == service_type + assert listener.service_name == service_name + assert listener.discovered_services == {} + + # Clean up + listener.stop() + zeroconf.close() + + def test_listener_extract_peer_info_success(self): + """Test successful PeerInfo extraction from ServiceInfo.""" + peerstore = PeerStore() + zeroconf = Zeroconf() + + listener = PeerListener( + peerstore=peerstore, + zeroconf=zeroconf, + service_type="_p2p._udp.local.", + service_name="local._p2p._udp.local.", + ) + + # Create sample service info + sample_peer_id = ID.from_base58("QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN") + hostname = socket.gethostname() + local_ip = "192.168.1.100" + + sample_service_info = ServiceInfo( + type_="_p2p._udp.local.", + name="test-peer._p2p._udp.local.", + port=8000, + properties={b"id": str(sample_peer_id).encode()}, + server=f"{hostname}.local.", + addresses=[socket.inet_aton(local_ip)], + ) + + peer_info = listener._extract_peer_info(sample_service_info) + + assert peer_info is not None + assert isinstance(peer_info.peer_id, ID) + assert len(peer_info.addrs) > 0 + assert all(isinstance(addr, Multiaddr) for addr in peer_info.addrs) + + # Check that protocol is TCP since we always use TCP + assert "/tcp/" in str(peer_info.addrs[0]) + + # Clean up + listener.stop() + zeroconf.close() + + def test_listener_extract_peer_info_invalid_id(self): + """Test PeerInfo extraction fails with invalid peer ID.""" + peerstore = PeerStore() + zeroconf = Zeroconf() + + listener = PeerListener( + peerstore=peerstore, + zeroconf=zeroconf, + service_type="_p2p._udp.local.", + service_name="local._p2p._udp.local.", + ) + + # Create service info with invalid peer ID + hostname = socket.gethostname() + local_ip = "192.168.1.100" + + service_info = ServiceInfo( + type_="_p2p._udp.local.", + name="invalid-peer._p2p._udp.local.", + port=8000, + properties={b"id": b"invalid_peer_id_format"}, + server=f"{hostname}.local.", + addresses=[socket.inet_aton(local_ip)], + ) + + peer_info = listener._extract_peer_info(service_info) + assert peer_info is None + + # Clean up + listener.stop() + zeroconf.close() diff --git a/tests/discovery/mdns/test_mdns.py b/tests/discovery/mdns/test_mdns.py new file mode 100644 index 00000000..502f8011 --- /dev/null +++ b/tests/discovery/mdns/test_mdns.py @@ -0,0 +1,152 @@ +""" +Integration test for mDNS discovery where one host finds another. +""" +import time +import socket +import pytest +from zeroconf import Zeroconf + +from libp2p.discovery.mdns.broadcaster import PeerBroadcaster +from libp2p.discovery.mdns.listener import PeerListener +from libp2p.peer.id import ID +from libp2p.peer.peerstore import PeerStore + + +class TestMDNSDiscovery: + """Integration test for mDNS peer discovery.""" + + def test_one_host_finds_another(self): + """Test that one host can find another host using mDNS.""" + # Create two separate Zeroconf instances to simulate different hosts + host1_zeroconf = Zeroconf() + host2_zeroconf = Zeroconf() + + try: + # Host 1: Set up as broadcaster (the host to be discovered) + host1_peer_id_obj = ID.from_base58("QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN") + host1_peer_id = str(host1_peer_id_obj) # Convert to string + host1_broadcaster = PeerBroadcaster( + zeroconf=host1_zeroconf, + service_type="_p2p._udp.local.", + service_name="host1._p2p._udp.local.", + peer_id=host1_peer_id, + port=8000 + ) + + # Host 2: Set up as listener (the host that discovers others) + host2_peerstore = PeerStore() + host2_listener = PeerListener( + peerstore=host2_peerstore, + zeroconf=host2_zeroconf, + service_type="_p2p._udp.local.", + service_name="host2._p2p._udp.local.", + ) + + # Host 1 registers its service for discovery + host1_broadcaster.register() + + + # Manually trigger discovery by calling add_service + # This simulates what happens when mDNS discovers a service + host2_listener.add_service( + host1_zeroconf, # Use host1's zeroconf so it can find the service + "_p2p._udp.local.", + "host1._p2p._udp.local." + ) + + # Verify that host2 discovered host1 + assert len(host2_listener.discovered_services) > 0 + assert "host1._p2p._udp.local." in host2_listener.discovered_services + + # Verify that host1's peer info was added to host2's peerstore + discovered_peer_id = host2_listener.discovered_services["host1._p2p._udp.local."] + assert str(discovered_peer_id) == host1_peer_id + + # Verify addresses were added to peerstore + try: + addrs = host2_peerstore.addrs(discovered_peer_id) + assert len(addrs) > 0 + # Should be TCP since we always use TCP protocol + assert "/tcp/8000" in str(addrs[0]) + except Exception: + # If no addresses found, the discovery didn't work properly + assert False, "Host1 addresses should be in Host2's peerstore" + + print(f"✅ Host2 successfully discovered Host1!") + print(f" Discovered peer ID: {discovered_peer_id}") + print(f" Discovered addresses: {[str(addr) for addr in addrs]}") + + # Clean up + host1_broadcaster.unregister() + host2_listener.stop() + + finally: + host1_zeroconf.close() + host2_zeroconf.close() + + def test_peer_discovery_with_multiple_addresses(self): + """Test discovery works with peers having multiple IP addresses.""" + host1_zeroconf = Zeroconf() + host2_zeroconf = Zeroconf() + + try: + # Create a peer with multiple addresses + host1_peer_id_obj = ID.from_base58("QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N") + host1_peer_id = str(host1_peer_id_obj) # Convert to string + + # Manually create service info with multiple addresses + from zeroconf import ServiceInfo + hostname = socket.gethostname() + + service_info = ServiceInfo( + type_="_p2p._udp.local.", + name="multi-addr-host._p2p._udp.local.", + port=8001, + properties={b"id": host1_peer_id.encode()}, + server=f"{hostname}.local.", + addresses=[ + socket.inet_aton("192.168.1.100"), + socket.inet_aton("10.0.0.50"), + ], + ) + + # Register the service + host1_zeroconf.register_service(service_info) + + # Set up listener + host2_peerstore = PeerStore() + host2_listener = PeerListener( + peerstore=host2_peerstore, + zeroconf=host2_zeroconf, + service_type="_p2p._udp.local.", + service_name="host2._p2p._udp.local.", + ) + + # Trigger discovery + host2_listener.add_service( + host1_zeroconf, + "_p2p._udp.local.", + "multi-addr-host._p2p._udp.local." + ) + + # Verify discovery + assert "multi-addr-host._p2p._udp.local." in host2_listener.discovered_services + discovered_peer_id = host2_listener.discovered_services["multi-addr-host._p2p._udp.local."] + + # Check multiple addresses were discovered + addrs = host2_peerstore.addrs(discovered_peer_id) + assert len(addrs) == 2 + + addr_strings = [str(addr) for addr in addrs] + assert "/ip4/192.168.1.100/tcp/8001" in addr_strings + assert "/ip4/10.0.0.50/tcp/8001" in addr_strings + + print(f"✅ Successfully discovered peer with multiple addresses!") + print(f" Addresses: {addr_strings}") + + # Clean up + host2_listener.stop() + + finally: + host1_zeroconf.close() + host2_zeroconf.close() diff --git a/tests/discovery/mdns/test_utils.py b/tests/discovery/mdns/test_utils.py new file mode 100644 index 00000000..b50fd44c --- /dev/null +++ b/tests/discovery/mdns/test_utils.py @@ -0,0 +1,39 @@ +""" +Basic unit tests for mDNS utils module. +""" +import string +import pytest + +from libp2p.discovery.mdns.utils import stringGen + + +class TestStringGen: + """Basic unit tests for stringGen function.""" + + def test_stringgen_default_length(self): + """Test stringGen with default length (63).""" + result = stringGen() + + assert isinstance(result, str) + assert len(result) == 63 + + # Check that all characters are from the expected charset + charset = string.ascii_lowercase + string.digits + for char in result: + assert char in charset + + def test_stringgen_custom_length(self): + """Test stringGen with custom lengths.""" + # Test various lengths + test_lengths = [1, 5, 10, 20, 50, 100] + + for length in test_lengths: + result = stringGen(length) + + assert isinstance(result, str) + assert len(result) == length + + # Check that all characters are from the expected charset + charset = string.ascii_lowercase + string.digits + for char in result: + assert char in charset