class TestHTTPClientConnect: """Tests the http client connection's 'connect' functionality.""" def setup(self): """Initialise the class.""" self.address = get_host() self.port = get_unused_tcp_port() self.agent_identity = Identity("name", address="some string") self.agent_address = self.agent_identity.address configuration = ConnectionConfig( host=self.address, port=self.port, connection_id=HTTPClientConnection.connection_id, ) self.http_client_connection = HTTPClientConnection( configuration=configuration, identity=self.agent_identity) self.http_client_connection.loop = asyncio.get_event_loop() self.connection_address = str(HTTPClientConnection.connection_id) self.http_dialogs = HttpDialogues(self.connection_address) @pytest.mark.asyncio async def test_initialization(self): """Test the initialisation of the class.""" assert self.http_client_connection.address == self.agent_identity.address @pytest.mark.asyncio async def test_connection(self): """Test the connect functionality of the http client connection.""" await self.http_client_connection.connect() assert self.http_client_connection.is_connected is True @pytest.mark.asyncio async def test_disconnect(self): """Test the disconnect functionality of the http client connection.""" await self.http_client_connection.connect() assert self.http_client_connection.is_connected is True await self.http_client_connection.disconnect() assert self.http_client_connection.is_connected is False @pytest.mark.asyncio async def test_http_send_error(self): """Test request fails and send back result with code 600.""" await self.http_client_connection.connect() request_http_message = HttpMessage( dialogue_reference=self.http_dialogs. new_self_initiated_dialogue_reference(), performative=HttpMessage.Performative.REQUEST, method="get", url="bad url", headers="", version="", bodyy=b"", ) request_http_message.counterparty = self.connection_address sending_dialogue = self.http_dialogs.update(request_http_message) assert sending_dialogue is not None request_envelope = Envelope( to=self.connection_address, sender=self.agent_address, protocol_id=UNKNOWN_PROTOCOL_PUBLIC_ID, message=request_http_message, ) connection_response_mock = Mock() connection_response_mock.status_code = 200 await self.http_client_connection.send(envelope=request_envelope) # TODO: Consider returning the response from the server in order to be able to assert that the message send! envelope = await asyncio.wait_for( self.http_client_connection.receive(), timeout=10) assert envelope assert envelope.message.status_code == 600 await self.http_client_connection.disconnect() @pytest.mark.asyncio async def test_http_client_send_not_connected_error(self): """Test connection.send error if not conencted.""" with pytest.raises(ConnectionError): await self.http_client_connection.send(Mock()) @pytest.mark.asyncio async def test_http_channel_send_not_connected_error(self): """Test channel.send error if not conencted.""" with pytest.raises(ValueError): self.http_client_connection.channel.send(Mock()) @pytest.mark.asyncio async def test_send_envelope_excluded_protocol_fail(self): """Test send error if protocol not supported.""" request_http_message = HttpMessage( dialogue_reference=self.http_dialogs. new_self_initiated_dialogue_reference(), performative=HttpMessage.Performative.REQUEST, method="get", url="bad url", headers="", version="", bodyy=b"", ) request_http_message.counterparty = self.connection_address sending_dialogue = self.http_dialogs.update(request_http_message) assert sending_dialogue is not None request_envelope = Envelope( to=self.connection_address, sender=self.agent_address, protocol_id=UNKNOWN_PROTOCOL_PUBLIC_ID, message=request_http_message, ) await self.http_client_connection.connect() with patch.object( self.http_client_connection.channel, "excluded_protocols", new=[UNKNOWN_PROTOCOL_PUBLIC_ID], ): with pytest.raises(ValueError): await self.http_client_connection.send(request_envelope) @pytest.mark.asyncio async def test_send_empty_envelope_skip(self): """Test skip on empty envelope request sent.""" await self.http_client_connection.connect() with patch.object(self.http_client_connection.channel, "_http_request_task") as mock: await self.http_client_connection.send(None) mock.assert_not_called() @pytest.mark.asyncio async def test_channel_get_message_not_connected(self): """Test errro on message get if not connected.""" with pytest.raises(ValueError): await self.http_client_connection.channel.get_message() @pytest.mark.asyncio async def test_channel_cancel_tasks_on_disconnect(self): """Test requests tasks cancelled on disconnect.""" await self.http_client_connection.connect() request_http_message = HttpMessage( dialogue_reference=self.http_dialogs. new_self_initiated_dialogue_reference(), performative=HttpMessage.Performative.REQUEST, method="get", url="https://not-a-google.com", headers="", version="", bodyy=b"", ) request_http_message.counterparty = self.connection_address sending_dialogue = self.http_dialogs.update(request_http_message) assert sending_dialogue is not None request_envelope = Envelope( to=self.connection_address, sender=self.agent_address, protocol_id=UNKNOWN_PROTOCOL_PUBLIC_ID, message=request_http_message, ) connection_response_mock = Mock() connection_response_mock.status_code = 200 response_mock = Mock() response_mock.status = 200 response_mock.headers = {"headers": "some header"} response_mock.reason = "OK" response_mock._body = b"Some content" response_mock.read.return_value = asyncio.Future() with patch.object( aiohttp.ClientSession, "request", return_value=_MockRequest(response_mock), ): await self.http_client_connection.send(envelope=request_envelope) assert self.http_client_connection.channel._tasks task = list(self.http_client_connection.channel._tasks)[0] assert not task.done() await self.http_client_connection.disconnect() assert not self.http_client_connection.channel._tasks assert task.done() with pytest.raises(CancelledError): await task @pytest.mark.asyncio async def test_http_send_ok(self): """Test request is ok cause mocked.""" await self.http_client_connection.connect() request_http_message = HttpMessage( dialogue_reference=self.http_dialogs. new_self_initiated_dialogue_reference(), performative=HttpMessage.Performative.REQUEST, method="get", url="https://not-a-google.com", headers="", version="", bodyy=b"", ) request_http_message.counterparty = self.connection_address sending_dialogue = self.http_dialogs.update(request_http_message) assert sending_dialogue is not None request_envelope = Envelope( to=self.connection_address, sender=self.agent_address, protocol_id=request_http_message.protocol_id, message=request_http_message, ) connection_response_mock = Mock() connection_response_mock.status_code = 200 response_mock = Mock() response_mock.status = 200 response_mock.headers = {"headers": "some header"} response_mock.reason = "OK" response_mock._body = b"Some content" response_mock.read.return_value = asyncio.Future() response_mock.read.return_value.set_result("") with patch.object( aiohttp.ClientSession, "request", return_value=_MockRequest(response_mock), ): await self.http_client_connection.send(envelope=request_envelope) # TODO: Consider returning the response from the server in order to be able to assert that the message send! envelope = await asyncio.wait_for( self.http_client_connection.receive(), timeout=10) assert envelope is not None and envelope.message is not None response = copy.copy(envelope.message) response.is_incoming = True response.counterparty = envelope.message.sender response_dialogue = self.http_dialogs.update(response) assert response.status_code == response_mock.status, response.bodyy.decode( "utf-8") assert sending_dialogue == response_dialogue await self.http_client_connection.disconnect() @pytest.mark.asyncio async def test_http_dialogue_construct_fail(self): """Test dialogue not properly constructed.""" await self.http_client_connection.connect() http_message = HttpMessage( dialogue_reference=self.http_dialogs. new_self_initiated_dialogue_reference(), performative=HttpMessage.Performative.RESPONSE, status_code=500, headers="", status_text="", bodyy=b"", version="", ) http_message.counterparty = self.connection_address http_dialogue = self.http_dialogs.update(http_message) http_message.sender = self.agent_address assert http_dialogue is None envelope = Envelope( to=http_message.counterparty, sender=http_message.sender, protocol_id=http_message.protocol_id, message=http_message, ) with patch.object(self.http_client_connection.channel.logger, "warning") as mock_logger: await self.http_client_connection.channel._http_request_task( envelope) mock_logger.assert_any_call( AnyStringWith("Could not create dialogue for message="))
class TestClientServer: """Client-Server end-to-end test.""" def setup_server(self): """Set up server connection.""" self.server_agent_address = "server_agent_address" self.server_agent_identity = Identity( "agent running server", address=self.server_agent_address ) self.host = get_host() self.port = get_unused_tcp_port() self.connection_id = HTTPServerConnection.connection_id self.protocol_id = PublicId.from_str("fetchai/http:0.4.0") self.configuration = ConnectionConfig( host=self.host, port=self.port, api_spec_path=None, # do not filter on API spec connection_id=HTTPServerConnection.connection_id, restricted_to_protocols=set([self.protocol_id]), ) self.server = HTTPServerConnection( configuration=self.configuration, identity=self.server_agent_identity, ) self.loop = asyncio.get_event_loop() self.loop.run_until_complete(self.server.connect()) # skill side dialogues self._server_dialogues = HttpDialogues(self.server_agent_address) def setup_client(self): """Set up client connection.""" self.client_agent_address = "client_agent_address" self.client_agent_identity = Identity( "agent running client", address=self.client_agent_address ) configuration = ConnectionConfig( host="localost", port="8888", # TODO: remove host/port for client? connection_id=HTTPClientConnection.connection_id, ) self.client = HTTPClientConnection( configuration=configuration, identity=self.client_agent_identity ) self.client.loop = asyncio.get_event_loop() self.loop.run_until_complete(self.client.connect()) # skill side dialogues self._client_dialogues = HttpDialogues(self.client_agent_address) def setup(self): """Set up test case.""" self.setup_server() self.setup_client() def _make_request( self, path: str, method: str = "get", headers: str = "", bodyy: bytes = b"" ) -> Envelope: """Make request envelope.""" request_http_message = HttpMessage( dialogue_reference=self._client_dialogues.new_self_initiated_dialogue_reference(), target=0, message_id=1, performative=HttpMessage.Performative.REQUEST, method=method, url=f"http://{self.host}:{self.port}{path}", headers="", version="", bodyy=b"", ) request_http_message.counterparty = str(HTTPClientConnection.connection_id) assert self._client_dialogues.update(request_http_message) is not None request_envelope = Envelope( to=request_http_message.counterparty, sender=request_http_message.sender, protocol_id=request_http_message.protocol_id, message=request_http_message, ) return request_envelope def _make_response( self, request_envelope: Envelope, status_code: int = 200, status_text: str = "" ) -> Envelope: """Make response envelope.""" incoming_message = cast(HttpMessage, request_envelope.message) incoming_message.is_incoming = True incoming_message.counterparty = str(HTTPServerConnection.connection_id) dialogue = self._server_dialogues.update(incoming_message) assert dialogue is not None message = HttpMessage( performative=HttpMessage.Performative.RESPONSE, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=incoming_message.message_id, message_id=incoming_message.message_id + 1, version=incoming_message.version, headers=incoming_message.headers, status_code=status_code, status_text=status_text, bodyy=incoming_message.bodyy, ) message.counterparty = incoming_message.counterparty assert dialogue.update(message) is not None response_envelope = Envelope( to=message.counterparty, sender=message.sender, protocol_id=message.protocol_id, context=request_envelope.context, message=message, ) return response_envelope @pytest.mark.asyncio async def test_post_with_payload(self): """Test client and server with post request.""" initial_request = self._make_request("/test", "POST", bodyy=b"1234567890") await self.client.send(initial_request) request = await asyncio.wait_for(self.server.receive(), timeout=5) # this is "inside" the server agent initial_response = self._make_response(request) await self.server.send(initial_response) response = await asyncio.wait_for(self.client.receive(), timeout=5) assert ( cast(HttpMessage, initial_request.message).bodyy == cast(HttpMessage, response.message).bodyy ) assert ( initial_request.message.dialogue_reference[0] == response.message.dialogue_reference[0] ) def teardown(self): """Tear down testcase.""" self.loop.run_until_complete(self.client.disconnect()) self.loop.run_until_complete(self.server.disconnect())
class HttpPingPongHandler(Handler): """Dummy handler to handle messages.""" SUPPORTED_PROTOCOL = HttpMessage.protocol_id def setup(self) -> None: """Noop setup.""" # pylint: disable=attribute-defined-outside-init, unused-argument self.count: int = 0 self.rtt_total_time: float = 0.0 self.rtt_count: int = 0 self.latency_total_time: float = 0.0 self.latency_count: int = 0 def role(m: Message, addr: Address) -> Dialogue.Role: return HttpDialogue.Role.CLIENT self.dialogues = HttpDialogues(self.context.agent_address, role_from_first_message=role) def teardown(self) -> None: """Noop teardown.""" def handle(self, message: Message) -> None: """Handle incoming message.""" self.count += 1 message = cast(HttpMessage, message) dialogue = self.dialogues.update(message) if not dialogue: raise Exception("something goes wrong") rtt_ts, latency_ts = struct.unpack("dd", message.body) # type: ignore if message.performative == HttpMessage.Performative.REQUEST: self.latency_total_time += time.time() - latency_ts self.latency_count += 1 self.make_response(cast(HttpDialogue, dialogue), message) elif message.performative == HttpMessage.Performative.RESPONSE: self.rtt_total_time += time.time() - rtt_ts self.rtt_count += 1 # got response, make another requet to the same agent self.make_request(message.sender) def make_response(self, dialogue: HttpDialogue, message: HttpMessage) -> None: """Construct and send a response for message received.""" response_message = dialogue.reply( target_message=message, performative=HttpMessage.Performative.RESPONSE, version=message.version, headers="", status_code=200, status_text="Success", body=message.body, ) self.context.outbox.put_message(response_message) def make_request(self, recipient_addr: str) -> None: """Make initial http request.""" message, _ = self.dialogues.create( recipient_addr, performative=HttpMessage.Performative.REQUEST, method="get", url="some url", headers="", version="", body=struct.pack("dd", time.time(), time.time()), ) self.context.outbox.put_message(message)