from unittest.mock import MagicMock, patch import pytest from src.utilities.zeroconf_server import ZeroconfServer class TestConfigure: """Configuring service parameters.""" def test_configure_sets_attributes(self, zeroconf_server_instance): with patch('socket.gethostbyname', return_value='192.168.1.1'): ZeroconfServer.configure('_zordon._tcp.local.', 'test-server', 8080) assert zeroconf_server_instance.service_type == '_zordon._tcp.local.' assert zeroconf_server_instance.server_name == 'test-server' assert zeroconf_server_instance.server_port == 8080 def test_configure_calls_sync_class(self, zeroconf_server_instance): with patch('socket.gethostbyname', return_value='192.168.1.1'): ZeroconfServer.configure('_zordon._tcp.local.', 'test-server', 8080) assert ZeroconfServer.service_type == '_zordon._tcp.local.' assert ZeroconfServer.server_port == 8080 def test_configure_stops_on_gaierror(self, zeroconf_server_instance): import socket with patch('socket.gethostbyname', side_effect=socket.gaierror): with patch.object(zeroconf_server_instance, '_stop') as mock_stop: ZeroconfServer.configure('_zordon._tcp.local.', 'test-server', 8080) mock_stop.assert_called_once() class TestSyncClass: """_sync_class propagates instance attrs to class level.""" def test_sync_class_propagates_all_attrs(self, zeroconf_server_instance): zeroconf_server_instance.service_type = '_test._tcp.' zeroconf_server_instance.server_name = 'foo' zeroconf_server_instance.server_port = 9999 zeroconf_server_instance.server_ip = '10.0.0.1' zeroconf_server_instance.properties = {'key': 'val'} ZeroconfServer._sync_class() assert ZeroconfServer.service_type == '_test._tcp.' assert ZeroconfServer.server_name == 'foo' assert ZeroconfServer.server_port == 9999 assert ZeroconfServer.server_ip == '10.0.0.1' assert ZeroconfServer.properties == {'key': 'val'} class TestStartStop: """Service lifecycle.""" def test_start_raises_without_configure(self, zeroconf_server_instance): with pytest.raises(RuntimeError, match='configure'): ZeroconfServer.start() @patch('src.utilities.zeroconf_server.ServiceBrowser') def test_start_listen_only_skips_register(self, mock_browser, zeroconf_server_instance): with patch('socket.gethostbyname', return_value='192.168.1.1'): ZeroconfServer.configure('_zordon._tcp.local.', 'test', 8080) with patch.object(zeroconf_server_instance, '_register_service') as mock_register: ZeroconfServer.start(listen_only=True) mock_register.assert_not_called() @patch('src.utilities.zeroconf_server.ServiceBrowser') def test_start_registers_service(self, mock_browser, zeroconf_server_instance): with patch('socket.gethostbyname', return_value='192.168.1.1'): ZeroconfServer.configure('_zordon._tcp.local.', 'test', 8080) with patch.object(zeroconf_server_instance, '_register_service') as mock_register: ZeroconfServer.start(listen_only=False) mock_register.assert_called_once() def test_stop_unregisters_and_closes(self, zeroconf_server_instance): zeroconf_server_instance.service_info = MagicMock() with patch.object(zeroconf_server_instance, '_unregister_service') as mock_unreg: with patch.object(zeroconf_server_instance.zeroconf, 'close') as mock_close: ZeroconfServer.stop() mock_unreg.assert_called_once() mock_close.assert_called_once() class TestRegisterService: """Service registration with Zeroconf.""" @patch('src.utilities.zeroconf_server.ServiceInfo') @patch('socket.gethostbyname') def test_registers_service_info(self, mock_gethostbyname, mock_service_info, zeroconf_server_instance): mock_gethostbyname.return_value = '192.168.1.1' zeroconf_server_instance.service_type = '_zordon._tcp.local.' zeroconf_server_instance.server_name = 'test' zeroconf_server_instance.server_port = 8080 zeroconf_server_instance.properties = {} # Replace real Zeroconf with a mock so we don't actually register zeroconf_server_instance.zeroconf = MagicMock() mock_info = MagicMock() mock_service_info.return_value = mock_info with patch('socket.inet_aton', return_value=b'\xc0\xa8\x01\x01'): zeroconf_server_instance._register_service() zeroconf_server_instance.zeroconf.register_service.assert_called_once_with(mock_info) assert zeroconf_server_instance.service_info == mock_info class TestFoundHostnames: """Discovery cache.""" def test_found_hostnames_empty_initially(self, zeroconf_server_instance): result = zeroconf_server_instance._found_hostnames() assert result == [] @patch('socket.gethostname', return_value='my-machine') def test_hostnames_sorted_with_local_first(self, mock_hostname, zeroconf_server_instance): zeroconf_server_instance.client_cache = { 'other-machine': MagicMock(), 'my-machine': MagicMock(), } result = zeroconf_server_instance._found_hostnames() # sort_key returns False (0) for local → sorted first assert result[0] == 'my-machine' def test_get_hostname_properties_returns_decoded(self, zeroconf_server_instance): info = MagicMock() info.properties = {b'key': b'value', b'num': b'42'} zeroconf_server_instance.client_cache['server-1'] = info result = zeroconf_server_instance._get_hostname_properties('server-1') assert result == {'key': 'value', 'num': '42'} def test_get_hostname_properties_returns_empty_for_unknown(self, zeroconf_server_instance): result = zeroconf_server_instance._get_hostname_properties('unknown') assert result == {} class TestForwarders: """Classmethod forwarders delegate to instance.""" def test_found_hostnames_forwarder(self, zeroconf_server_instance): zeroconf_server_instance.client_cache['svr'] = MagicMock() result = ZeroconfServer.found_hostnames() assert 'svr' in result def test_get_hostname_properties_forwarder(self, zeroconf_server_instance): info = MagicMock() info.properties = {} zeroconf_server_instance.client_cache['svr'] = info result = ZeroconfServer.get_hostname_properties('svr') assert result == {}