def test_receive_valid_json_valid_message_from_old_peer(self): """ A good message is received then the node handles the message as expected. The cached protocol object for the peer node is expired since a new protocol object is used in this instance. """ nc = NetstringConnector(self.event_loop) old_protocol = mock.MagicMock() network_id = sha512(PUBLIC_KEY.encode('ascii')).hexdigest() nc._connections[network_id] = old_protocol ok = { 'uuid': str(uuid.uuid4()), 'recipient': PUBLIC_KEY, 'sender': PUBLIC_KEY, 'reply_port': 1908, 'version': self.version, } seal = get_seal(ok, PRIVATE_KEY) ok['seal'] = seal ok['message'] = 'ok' raw = json.dumps(ok) sender = '192.168.0.1' handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, nc, 1908) handler.message_received = mock.MagicMock() protocol = mock.MagicMock() nc.receive(raw, sender, handler, protocol) self.assertIn(network_id, nc._connections) self.assertEqual(nc._connections[network_id], protocol) msg = from_dict(ok) handler.message_received.assert_called_once_with(msg, 'netstring', sender, msg.reply_port)
def test_receive(self): """ The good case. Should return whatever handler.message_received returns. """ connector = HttpConnector(self.event_loop) ok = { 'uuid': str(uuid.uuid4()), 'recipient': PUBLIC_KEY, 'sender': PUBLIC_KEY, 'reply_port': 1908, 'version': self.version, } seal = get_seal(ok, PRIVATE_KEY) ok['seal'] = seal ok['message'] = 'ok' raw = json.dumps(ok).encode('utf-8') sender = '192.168.0.1' handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) handler.message_received = mock.MagicMock() connector.receive(raw, sender, handler) msg = from_dict(ok) handler.message_received.assert_called_once_with(msg, 'http', sender, msg.reply_port)
def setUp(self): """ Common vars. """ loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) self.event_loop = asyncio.get_event_loop() self.version = get_version() self.sender = mock.MagicMock() self.reply_port = 1908 self.node = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, self.sender, self.reply_port) self.target = TARGET self.seal = 'afakesealthatwillnotverify' node_list = [] remote_node_list = [] for i in range(100, 120): uri = 'netstring://192.168.0.%d:9999/' % i contact = PeerNode(ORDERED_HASHES[i], self.version, uri, 0) node_list.append(contact) remote_node_list.append((ORDERED_HASHES[i], self.version, uri)) self.nodes = tuple(sort_peer_nodes(node_list, self.target)) self.remote_nodes = tuple(remote_node_list) def side_effect(*args, **kwargs): return (str(uuid.uuid4()), asyncio.Future()) self.node.send_find = mock.MagicMock(side_effect=side_effect) self.contacts = [] node_list = [] for i in range(20): uri = 'netstring://192.168.0.%d:%d/' % (i, self.reply_port) contact = PeerNode(ORDERED_HASHES[i], self.version, uri, 0) self.node.routing_table.add_contact(contact) self.contacts.append((ORDERED_HASHES[i], self.version, uri))
def test_get_existing_lookup_failed(self): """ Getting an existing key that has resulted in an error returns a 'finished' status and an 'error' flag. """ connector = HttpConnector(self.event_loop) self.assertEqual({}, connector.lookups) handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) faux_lookup = asyncio.Future() ex = Exception('Bang!') faux_lookup.set_exception(ex) test_key = hashlib.sha512().hexdigest() connector.lookups[test_key] = { 'last_access': 123.45, 'lookup': faux_lookup } result = connector.get(test_key, handler) self.assertIn(test_key, connector.lookups) self.assertTrue(connector.lookups[test_key]['last_access'] > 123.45) self.assertEqual(connector.lookups[test_key]['lookup'], faux_lookup) self.assertEqual(result['key'], test_key) self.assertEqual(result['status'], faux_lookup._state.lower()) self.assertEqual(result['error'], True) self.assertEqual(3, len(result))
def test_handle_GET_internal_server_error(self): """ A GET request that causes an exception simply returns a 500 (Internal Server Error). """ test_key = hashlib.sha512().hexdigest() mockMessage = mock.MagicMock() mockMessage.method = 'GET' mockMessage.version = '1.1' mockMessage.path = ''.join(['/', test_key]) connector = HttpConnector(self.event_loop) def faux_get(*args, **kwargs): raise Exception('Bang!') connector.get = mock.MagicMock(side_effect=faux_get) node = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) hrh = HttpRequestHandler(connector, node, debug=True) peer = '192.168.0.1' hrh.transport = mock.MagicMock() hrh.transport.get_extra_info = mock.MagicMock(side_effect=peer) hrh.writer = mock.MagicMock() with mock.patch.object(aiohttp, 'Response', return_value=mock.MagicMock()) as response: self.event_loop.run_until_complete(hrh.handle_request(mockMessage, None)) response.assert_called_once_with(hrh.writer, 500, http_version=mockMessage.version)
def test_handle_GET_no_cache(self): """ Ensure that if the cache-control header in the request is set to no-cache then the lookup is foreced (i.e. don't use the local cache). """ test_key = hashlib.sha512().hexdigest() mockMessage = mock.MagicMock() mockMessage.method = 'GET' mockMessage.version = '1.1' mockMessage.path = ''.join(['/', test_key]) mockMessage.headers = {'Cache-Control': 'no-cache', } connector = HttpConnector(self.event_loop) def faux_get(*args, **kwargs): return { 'key': test_key, 'status': 'pending', } connector.get = mock.MagicMock(side_effect=faux_get) node = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) hrh = HttpRequestHandler(connector, node, debug=True) peer = '192.168.0.1' hrh.transport = mock.MagicMock() hrh.transport.get_extra_info = mock.MagicMock(side_effect=peer) hrh.writer = mock.MagicMock() with mock.patch.object(aiohttp, 'Response', return_value=mock.MagicMock()) as response: self.event_loop.run_until_complete(hrh.handle_request(mockMessage, None)) response.assert_called_once_with(hrh.writer, 200, http_version=mockMessage.version) # The connector's get method was called with the forced flag set # to True. connector.get.assert_called_once_with(test_key, node, True)
def test_send_to_new_contact_failed_to_connect(self): """ Sending a message to a new but unreachable contact results in the resulting deferred to be resolved with the expected exception. """ nc = NetstringConnector(self.event_loop) contact = PeerNode(PUBLIC_KEY, self.version, 'netstring://192.168.0.1:1908') msg = OK('uuid', 'recipient', 'sender', 9999, 'version', 'seal') protocol = mock.MagicMock() def side_effect(*args, **kwargs): raise ValueError() protocol.send_string = mock.MagicMock(side_effect=side_effect) sender = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, nc, 1908) @asyncio.coroutine def faux_connect(protocol=protocol): return ('foo', protocol) with mock.patch.object(self.event_loop, 'create_connection', return_value=faux_connect()): result = nc.send(contact, msg, sender) with self.assertRaises(ValueError) as ex: self.event_loop.run_until_complete(result) self.assertEqual(1, protocol.send_string.call_count) self.assertTrue(result.done()) self.assertEqual(ex.exception, result.exception()) self.assertNotIn(contact.network_id, nc._connections)
def test_handle_request_causes_exception(self): """ A request that raises an exception causes a 500 response. """ mockMessage = mock.MagicMock() mockMessage.method = 'POST' mockMessage.version = '1.1' mockPayload = mock.MagicMock() @asyncio.coroutine def faux_read(*args, **kwargs): return 'raw_data' mockPayload.read = mock.MagicMock(side_effect=faux_read) connector = HttpConnector(self.event_loop) def faux_receive(*args, **kwargs): raise ValueError('Boom! Something went wrong.') connector.receive = mock.MagicMock(side_effect=faux_receive) node = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) hrh = HttpRequestHandler(connector, node, debug=True) peer = '192.168.0.1' hrh.transport = mock.MagicMock() hrh.transport.get_extra_info = mock.MagicMock(side_effect=peer) hrh.writer = mock.MagicMock() with mock.patch.object(aiohttp, 'Response', return_value=mock.MagicMock()) as response: self.event_loop.run_until_complete(hrh.handle_request(mockMessage, mockPayload)) response.assert_called_once_with(hrh.writer, 500, http_version=mockMessage.version)
def test_get_existing_lookup(self): """ Getting an existing key that has completed returns a 'finished' status and associated value. """ connector = HttpConnector(self.event_loop) self.assertEqual({}, connector.lookups) handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) faux_lookup = asyncio.Future() faux_lookup.set_result('foo') test_key = hashlib.sha512().hexdigest() connector.lookups[test_key] = { 'last_access': 123.45, 'lookup': faux_lookup } result = connector.get(test_key, handler) self.assertIn(test_key, connector.lookups) # Check the last_access has been updated self.assertTrue(connector.lookups[test_key]['last_access'] > 123.45) self.assertEqual(connector.lookups[test_key]['lookup'], faux_lookup) self.assertEqual(result['key'], test_key) self.assertEqual(result['status'], faux_lookup._state.lower()) self.assertEqual(result['value'], 'foo') self.assertEqual(3, len(result))
def test_handle_GET_request(self): """ A valid GET request casues a 200 reponse. """ test_key = hashlib.sha512().hexdigest() mockMessage = mock.MagicMock() mockMessage.method = 'GET' mockMessage.version = '1.1' mockMessage.path = ''.join(['/', test_key]) connector = HttpConnector(self.event_loop) def faux_get(*args, **kwargs): return { 'key': test_key, 'status': 'pending', } connector.get = mock.MagicMock(side_effect=faux_get) node = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) hrh = HttpRequestHandler(connector, node, debug=True) peer = '192.168.0.1' hrh.transport = mock.MagicMock() hrh.transport.get_extra_info = mock.MagicMock(side_effect=peer) hrh.writer = mock.MagicMock() with mock.patch.object(aiohttp, 'Response', return_value=mock.MagicMock()) as response: self.event_loop.run_until_complete(hrh.handle_request(mockMessage, None)) response.assert_called_once_with(hrh.writer, 200, http_version=mockMessage.version)
def test_receive_valid_json_invalid_message(self): """ If a message is received that consists of valid json but a malformed message then log the incident for later analysis. """ patcher = mock.patch('drogulus.net.netstring.log.error') nc = NetstringConnector(self.event_loop) ping = { 'uuid': str(uuid.uuid4()), 'recipient': PUBLIC_KEY, 'sender': BAD_PUBLIC_KEY, 'reply_port': 1908, 'version': self.version, } seal = get_seal(ping, PRIVATE_KEY) ping['seal'] = seal ping['message'] = 'ping' raw = json.dumps(ping) sender = '192.168.0.1' handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, nc, 1908) protocol = mock.MagicMock() mock_log = patcher.start() nc.receive(raw, sender, handler, protocol) self.assertEqual(3, mock_log.call_count) patcher.stop()
def test_send_to_new_contact_successful_connection(self): """ Send a message to a new contact causes a new connection to be made whose associated protocol object is cached for later use. """ nc = NetstringConnector(self.event_loop) contact = PeerNode(PUBLIC_KEY, self.version, 'netstring://192.168.0.1:1908') msg = OK('uuid', 'recipient', 'sender', 9999, 'version', 'seal') protocol = mock.MagicMock() protocol.send_string = mock.MagicMock() sender = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, nc, 1908) @asyncio.coroutine def faux_connect(protocol=protocol): return ('foo', protocol) with mock.patch.object(self.event_loop, 'create_connection', return_value=faux_connect()): result = nc.send(contact, msg, sender) self.event_loop.run_until_complete(result) self.assertEqual(1, protocol.send_string.call_count) self.assertTrue(result.done()) self.assertEqual(True, result.result()) self.assertIn(contact.network_id, nc._connections) self.assertEqual(nc._connections[contact.network_id], protocol) expected = to_dict(msg) actual = json.loads(protocol.send_string.call_args[0][0]) self.assertEqual(expected, actual)
def test_init_(self): """ The connector and node instances should be set properly. """ connector = HttpConnector(self.event_loop) node = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) hrh = HttpRequestHandler(connector, node) self.assertEqual(connector, hrh.connector) self.assertEqual(node, hrh.node)
def test_init_with_extra_kwargs(self): """ An further arguments passed in (above and beyond the connector and node instances) are correctly handled. """ connector = HttpConnector(self.event_loop) node = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) hrh = HttpRequestHandler(connector, node, debug=True) self.assertEqual(connector, hrh.connector) self.assertEqual(node, hrh.node) self.assertEqual(True, hrh.debug)
def test_get_forced_refresh_no_existing_cached_value(self): """ Ensures that even if there's no cached value a new lookup is executed if the 'forced' flag is True. """ connector = HttpConnector(self.event_loop) self.assertEqual({}, connector.lookups) handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) faux_lookup = asyncio.Future() handler.retrieve = mock.MagicMock(return_value=faux_lookup) test_key = hashlib.sha512().hexdigest() result = connector.get(test_key, handler, forced=True) handler.retrieve.assert_called_once_with(test_key) self.assertIn(test_key, connector.lookups) self.assertIsInstance(connector.lookups[test_key]['last_access'], float) self.assertEqual(connector.lookups[test_key]['lookup'], faux_lookup) self.assertEqual(result['key'], test_key) self.assertEqual(result['status'], faux_lookup._state.lower()) self.assertEqual(2, len(result))
def test_get_new_lookup(self): """ Getting an unknown key fires a new lookup that is initially produces a 'pending' status. """ connector = HttpConnector(self.event_loop) self.assertEqual({}, connector.lookups) handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) faux_lookup = asyncio.Future() handler.retrieve = mock.MagicMock(return_value=faux_lookup) test_key = hashlib.sha512().hexdigest() result = connector.get(test_key, handler) handler.retrieve.assert_called_once_with(test_key) self.assertIn(test_key, connector.lookups) self.assertIsInstance(connector.lookups[test_key]['last_access'], float) self.assertEqual(connector.lookups[test_key]['lookup'], faux_lookup) self.assertEqual(result['key'], test_key) self.assertEqual(result['status'], faux_lookup._state.lower()) self.assertEqual(2, len(result))
def test_receive_not_json(self): """ Appropriately handle a message that doesn't contain JSON. """ patcher = mock.patch('drogulus.net.http.log.error') connector = HttpConnector(self.event_loop) raw = "junk from the network".encode('utf-8') sender = '192.168.0.1' handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) mock_log = patcher.start() self.assertRaises(ValueError, connector.receive, raw, sender, handler) self.assertEqual(4, mock_log.call_count) patcher.stop()
def test_get_forced_refresh_existing_value(self): """ Ensures that an existing result is ignored and a new lookup is executed if the 'forced' flag is True. """ connector = HttpConnector(self.event_loop) self.assertEqual({}, connector.lookups) handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) cached_lookup = asyncio.Future() cached_lookup.set_result('foo') test_key = hashlib.sha512().hexdigest() connector.lookups[test_key] = cached_lookup new_lookup = asyncio.Future() handler.retrieve = mock.MagicMock(return_value=new_lookup) result = connector.get(test_key, handler, forced=True) self.assertIn(test_key, connector.lookups) self.assertIsInstance(connector.lookups[test_key]['last_access'], float) self.assertEqual(connector.lookups[test_key]['lookup'], new_lookup) self.assertEqual(result['key'], test_key) self.assertEqual(result['status'], new_lookup._state.lower()) self.assertEqual(2, len(result))
def test_receive_invalid_json(self): """ If a message is received that contains bad json then log the incident for later analysis. """ patcher = mock.patch('drogulus.net.netstring.log.error') nc = NetstringConnector(self.event_loop) sender = '192.168.0.1' handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, nc, 1908) protocol = mock.MagicMock() raw = 'invalid JSON' mock_log = patcher.start() nc.receive(raw, sender, handler, protocol) self.assertEqual(3, mock_log.call_count) patcher.stop()
def test_send_with_cached_protocol(self): """ Send the message to the referenced contact using a cached protocol object. """ nc = NetstringConnector(self.event_loop) nc._send_message_with_protocol = mock.MagicMock() contact = PeerNode(PUBLIC_KEY, self.version, 'netstring://192.168.0.1:1908') msg = OK('uuid', 'recipient', 'sender', 9999, 'version', 'seal') protocol = mock.MagicMock() sender = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, nc, 1908) nc._connections[contact.network_id] = protocol result = nc.send(contact, msg, sender) self.assertIsInstance(result, asyncio.Future) self.assertTrue(result.done()) self.assertEqual(result.result(), True) nc._send_message_with_protocol.assert_called_once_with(msg, protocol)
def test_handle_request_not_POST_or_GET(self): """ A request that is not a POST causes a 405 response. """ mockMessage = mock.MagicMock() mockMessage.method = 'PUT' mockMessage.version = '1.1' mockPayload = mock.MagicMock() connector = HttpConnector(self.event_loop) node = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) hrh = HttpRequestHandler(connector, node, debug=True) hrh.writer = mock.MagicMock() with mock.patch.object(aiohttp, 'Response', return_value=mock.MagicMock()) as response: self.event_loop.run_until_complete(hrh.handle_request(mockMessage, mockPayload)) response.assert_called_once_with(hrh.writer, 405, http_version=mockMessage.version)
def test_handle_GET_bad_request(self): """ A GET request without a valid sha512 hexdigest as its path causes a 400 (Bad Request) response. """ test_key = 'not_a_valid_sha512_hexdigest' mockMessage = mock.MagicMock() mockMessage.method = 'GET' mockMessage.version = '1.1' mockMessage.path = ''.join(['/', test_key]) connector = HttpConnector(self.event_loop) node = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) hrh = HttpRequestHandler(connector, node, debug=True) hrh.writer = mock.MagicMock() with mock.patch.object(aiohttp, 'Response', return_value=mock.MagicMock()) as response: self.event_loop.run_until_complete(hrh.handle_request(mockMessage, None)) response.assert_called_once_with(hrh.writer, 400, http_version=mockMessage.version)
def test_receive_bad_message(self): """ Appropriately handle a message that is valid JSON but not a valid message type understood as part of the drogulus protocol. """ patcher = mock.patch('drogulus.net.http.log.error') connector = HttpConnector(self.event_loop) ok = { 'uuid': str(uuid.uuid4()), 'recipient': PUBLIC_KEY, 'sender': PUBLIC_KEY, 'reply_port': 1908, 'version': self.version, } raw = json.dumps(ok).encode('utf-8') sender = '192.168.0.1' handler = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) mock_log = patcher.start() self.assertRaises(KeyError, connector.receive, raw, sender, handler) self.assertEqual(4, mock_log.call_count) patcher.stop()
def test_send_with_failing_cached_protocol(self): """ Attempting to send a message to the referenced contact using a cached protocol object that cannot send (e.g. perhaps the transport was dropped?) causes a retry as if the contact were new. """ nc = NetstringConnector(self.event_loop) contact = PeerNode(PUBLIC_KEY, self.version, 'netstring://192.168.0.1:1908') msg = OK('uuid', 'recipient', 'sender', 9999, 'version', 'seal') protocol = mock.MagicMock() def side_effect(*args, **kwargs): raise ValueError() protocol.send_string = mock.MagicMock(side_effect=side_effect) nc._connections[contact.network_id] = protocol new_protocol = mock.MagicMock() new_protocol.send_string = mock.MagicMock() sender = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, nc, 1908) @asyncio.coroutine def faux_connect(protocol=new_protocol): return ('foo', protocol) with mock.patch.object(self.event_loop, 'create_connection', return_value=faux_connect()): result = nc.send(contact, msg, sender) self.event_loop.run_until_complete(result) self.assertEqual(1, new_protocol.send_string.call_count) self.assertTrue(result.done()) self.assertEqual(True, result.result()) self.assertIn(contact.network_id, nc._connections) self.assertEqual(nc._connections[contact.network_id], new_protocol) expected = to_dict(msg) actual = json.loads(protocol.send_string.call_args[0][0]) self.assertEqual(expected, actual)
def test_handle_POST_request(self): """ A valid POST request causes a 200 response. * WARNING * Too much mocking going on here (in the vain attempt to achieve 100% test coverage). """ mockMessage = mock.MagicMock() mockMessage.method = 'POST' mockMessage.version = '1.1' mockPayload = mock.MagicMock() @asyncio.coroutine def faux_read(*args, **kwargs): return 'raw_data' mockPayload.read = mock.MagicMock(side_effect=faux_read) connector = HttpConnector(self.event_loop) def faux_receive(*args, **kwargs): return OK('uuid', 'recipient', 'sender', 9999, 'version', 'seal') connector.receive = mock.MagicMock(side_effect=faux_receive) node = Node(PUBLIC_KEY, PRIVATE_KEY, self.event_loop, connector, 1908) hrh = HttpRequestHandler(connector, node, debug=True) peer = '192.168.0.1' hrh.transport = mock.MagicMock() hrh.transport.get_extra_info = mock.MagicMock(side_effect=peer) hrh.writer = mock.MagicMock() with mock.patch.object(aiohttp, 'Response', return_value=mock.MagicMock()) as response: self.event_loop.run_until_complete(hrh.handle_request(mockMessage, mockPayload)) response.assert_called_once_with(hrh.writer, 200, http_version=mockMessage.version)