Esempio n. 1
0
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)