import socket from urllib.parse import urlparse import pytest import mock from util import UpnpPunch as upnp @pytest.fixture def mock_socket(): mock_socket = mock.MagicMock() mock_socket.recv = mock.MagicMock(return_value=b'Hello') mock_socket.bind = mock.MagicMock() mock_socket.send_to = mock.MagicMock() return mock_socket @pytest.fixture def url_obj(): return urlparse('http://192.168.1.1/ctrlPoint.xml') @pytest.fixture(params=['WANPPPConnection', 'WANIPConnection']) def igd_profile(request): return """ urn:schemas-upnp-org:service:{}:1 urn:upnp-org:serviceId:wanpppc:pppoa /upnp/control/wanpppcpppoa /upnp/event/wanpppcpppoa /WANPPPConnection.xml """.format(request.param) @pytest.fixture def httplib_response(): class FakeResponse(object): def __init__(self, status=200, body='OK'): self.status = status self.body = body def read(self): return self.body return FakeResponse class TestUpnpPunch(object): def test_perform_m_search(self, mock_socket): local_ip = '127.0.0.1' with mock.patch('util.UpnpPunch.socket.socket', return_value=mock_socket): result = upnp.perform_m_search(local_ip) assert result == 'Hello' assert local_ip == mock_socket.bind.call_args_list[0][0][0][0] assert ('239.255.255.250', 1900) == mock_socket.sendto.call_args_list[0][0][1] def test_perform_m_search_socket_error(self, mock_socket): mock_socket.recv.side_effect = socket.error('Timeout error') with mock.patch('util.UpnpPunch.socket.socket', return_value=mock_socket): with pytest.raises(upnp.UpnpError): upnp.perform_m_search('127.0.0.1') def test_retrieve_location_from_ssdp(self, url_obj): ctrl_location = url_obj.geturl() parsed_location = urlparse(ctrl_location) rsp = ('auth: gibberish\r\nlocation: {0}\r\n' 'Content-Type: text/html\r\n\r\n').format(ctrl_location) result = upnp._retrieve_location_from_ssdp(rsp) assert result == parsed_location def test_retrieve_location_from_ssdp_no_header(self): rsp = 'auth: gibberish\r\nContent-Type: application/json\r\n\r\n' with pytest.raises(upnp.IGDError): upnp._retrieve_location_from_ssdp(rsp) def test_retrieve_igd_profile(self, url_obj): with mock.patch('urllib.request.urlopen') as mock_urlopen: upnp._retrieve_igd_profile(url_obj) mock_urlopen.assert_called_with(url_obj.geturl(), timeout=5) def test_retrieve_igd_profile_timeout(self, url_obj): with mock.patch('urllib.request.urlopen') as mock_urlopen: mock_urlopen.side_effect = socket.error('Timeout error') with pytest.raises(upnp.IGDError): upnp._retrieve_igd_profile(url_obj) def test_parse_igd_profile_service_type(self, igd_profile): control_path, upnp_schema = upnp._parse_igd_profile(igd_profile) assert control_path == '/upnp/control/wanpppcpppoa' assert upnp_schema in ('WANPPPConnection', 'WANIPConnection',) def test_parse_igd_profile_no_ctrlurl(self, igd_profile): igd_profile = igd_profile.replace('controlURL', 'nope') with pytest.raises(upnp.IGDError): control_path, upnp_schema = upnp._parse_igd_profile(igd_profile) def test_parse_igd_profile_no_schema(self, igd_profile): igd_profile = igd_profile.replace('Connection', 'nope') with pytest.raises(upnp.IGDError): control_path, upnp_schema = upnp._parse_igd_profile(igd_profile) def test_create_open_message_parsable(self): from xml.parsers.expat import ExpatError msg, _ = upnp._create_open_message('127.0.0.1', 8888) try: upnp.parseString(msg) except ExpatError as e: pytest.fail('Incorrect XML message: {}'.format(e)) def test_create_open_message_contains_right_stuff(self): settings = {'description': 'test desc', 'protocol': 'test proto', 'upnp_schema': 'test schema'} msg, fn_name = upnp._create_open_message('127.0.0.1', 8888, **settings) assert fn_name == 'AddPortMapping' assert '127.0.0.1' in msg assert '8888' in msg assert settings['description'] in msg assert settings['protocol'] in msg assert settings['upnp_schema'] in msg def test_parse_for_errors_bad_rsp(self, httplib_response): rsp = httplib_response(status=500) with pytest.raises(upnp.IGDError) as err: upnp._parse_for_errors(rsp) assert 'Unable to parse' in str(err.value) def test_parse_for_errors_error(self, httplib_response): soap_error = ('' '500' 'Bad request' '') rsp = httplib_response(status=500, body=soap_error) with pytest.raises(upnp.IGDError) as err: upnp._parse_for_errors(rsp) assert 'SOAP request error' in str(err.value) def test_parse_for_errors_good_rsp(self, httplib_response): rsp = httplib_response(status=200) assert rsp == upnp._parse_for_errors(rsp) def test_send_requests_success(self): with mock.patch( 'util.UpnpPunch._send_soap_request') as mock_send_request: mock_send_request.return_value = mock.MagicMock(status=200) upnp._send_requests(['msg'], None, None, None) assert mock_send_request.called def test_send_requests_failed(self): with mock.patch( 'util.UpnpPunch._send_soap_request') as mock_send_request: mock_send_request.return_value = mock.MagicMock(status=500) with pytest.raises(upnp.UpnpError): upnp._send_requests(['msg'], None, None, None) assert mock_send_request.called def test_collect_idg_data(self): pass @mock.patch('util.UpnpPunch._get_local_ips') @mock.patch('util.UpnpPunch._collect_idg_data') @mock.patch('util.UpnpPunch._send_requests') def test_ask_to_open_port_success(self, mock_send_requests, mock_collect_idg, mock_local_ips): mock_collect_idg.return_value = {'upnp_schema': 'schema-yo'} mock_local_ips.return_value = ['192.168.0.12'] result = upnp.ask_to_open_port(retries=5) soap_msg = mock_send_requests.call_args[0][0][0][0] assert result is True assert mock_collect_idg.called assert '192.168.0.12' in soap_msg assert '15441' in soap_msg assert 'schema-yo' in soap_msg @mock.patch('util.UpnpPunch._get_local_ips') @mock.patch('util.UpnpPunch._collect_idg_data') @mock.patch('util.UpnpPunch._send_requests') def test_ask_to_open_port_failure(self, mock_send_requests, mock_collect_idg, mock_local_ips): mock_local_ips.return_value = ['192.168.0.12'] mock_collect_idg.return_value = {'upnp_schema': 'schema-yo'} mock_send_requests.side_effect = upnp.UpnpError() with pytest.raises(upnp.UpnpError): upnp.ask_to_open_port() @mock.patch('util.UpnpPunch._collect_idg_data') @mock.patch('util.UpnpPunch._send_requests') def test_orchestrate_soap_request(self, mock_send_requests, mock_collect_idg): soap_mock = mock.MagicMock() args = ['127.0.0.1', 31337, soap_mock, 'upnp-test', {'upnp_schema': 'schema-yo'}] mock_collect_idg.return_value = args[-1] upnp._orchestrate_soap_request(*args[:-1]) assert mock_collect_idg.called soap_mock.assert_called_with( *args[:2] + ['upnp-test', 'UDP', 'schema-yo']) assert mock_send_requests.called @mock.patch('util.UpnpPunch._collect_idg_data') @mock.patch('util.UpnpPunch._send_requests') def test_orchestrate_soap_request_without_desc(self, mock_send_requests, mock_collect_idg): soap_mock = mock.MagicMock() args = ['127.0.0.1', 31337, soap_mock, {'upnp_schema': 'schema-yo'}] mock_collect_idg.return_value = args[-1] upnp._orchestrate_soap_request(*args[:-1]) assert mock_collect_idg.called soap_mock.assert_called_with(*args[:2] + [None, 'UDP', 'schema-yo']) assert mock_send_requests.called def test_create_close_message_parsable(self): from xml.parsers.expat import ExpatError msg, _ = upnp._create_close_message('127.0.0.1', 8888) try: upnp.parseString(msg) except ExpatError as e: pytest.fail('Incorrect XML message: {}'.format(e)) def test_create_close_message_contains_right_stuff(self): settings = {'protocol': 'test proto', 'upnp_schema': 'test schema'} msg, fn_name = upnp._create_close_message('127.0.0.1', 8888, ** settings) assert fn_name == 'DeletePortMapping' assert '8888' in msg assert settings['protocol'] in msg assert settings['upnp_schema'] in msg @mock.patch('util.UpnpPunch._get_local_ips') @mock.patch('util.UpnpPunch._orchestrate_soap_request') def test_communicate_with_igd_success(self, mock_orchestrate, mock_get_local_ips): mock_get_local_ips.return_value = ['192.168.0.12'] upnp._communicate_with_igd() assert mock_get_local_ips.called assert mock_orchestrate.called @mock.patch('util.UpnpPunch._get_local_ips') @mock.patch('util.UpnpPunch._orchestrate_soap_request') def test_communicate_with_igd_succeed_despite_single_failure( self, mock_orchestrate, mock_get_local_ips): mock_get_local_ips.return_value = ['192.168.0.12'] mock_orchestrate.side_effect = [upnp.UpnpError, None] upnp._communicate_with_igd(retries=2) assert mock_get_local_ips.called assert mock_orchestrate.called @mock.patch('util.UpnpPunch._get_local_ips') @mock.patch('util.UpnpPunch._orchestrate_soap_request') def test_communicate_with_igd_total_failure(self, mock_orchestrate, mock_get_local_ips): mock_get_local_ips.return_value = ['192.168.0.12'] mock_orchestrate.side_effect = [upnp.UpnpError, upnp.IGDError] with pytest.raises(upnp.UpnpError): upnp._communicate_with_igd(retries=2) assert mock_get_local_ips.called assert mock_orchestrate.called