From adebd333558f276c90a1a4f2b12989cbaa370b18 Mon Sep 17 00:00:00 2001 From: cadeagostinelli <144862955+cadeagostinelli@users.noreply.github.com> Date: Thu, 7 Mar 2024 09:38:27 -0600 Subject: [PATCH] Pull Request for Fully Tested Async Client and Utils (#4) * Fully Tested Async Client and Utils --------- Co-authored-by: cadea --- conftest.py | 7 + fractal/matrix/async_client.py | 2 - pyproject.toml | 4 +- ...ctal_async_client_get_latest_sync_token.py | 0 .../test_fractal_async_client_invite.py | 12 +- .../test_fractal_async_client_send_message.py | 0 .../{ => fractal-async-client}/test_matrix.py | 227 +++++++++++++++--- tests/test_fractal_async_client.py | 82 ------- tests/test_utils.py | 68 ++++++ 9 files changed, 285 insertions(+), 117 deletions(-) rename tests/{ => fractal-async-client}/test_fractal_async_client_get_latest_sync_token.py (100%) rename tests/{ => fractal-async-client}/test_fractal_async_client_invite.py (92%) rename tests/{ => fractal-async-client}/test_fractal_async_client_send_message.py (100%) rename tests/{ => fractal-async-client}/test_matrix.py (60%) delete mode 100644 tests/test_fractal_async_client.py create mode 100644 tests/test_utils.py diff --git a/conftest.py b/conftest.py index fac2806..8ee8cb4 100644 --- a/conftest.py +++ b/conftest.py @@ -1,13 +1,20 @@ import pytest +from aioresponses import aioresponses from fractal.matrix.async_client import FractalAsyncClient +@pytest.fixture def test_fractal_async_client(): test_object = FractalAsyncClient() print("work") return test_object +@pytest.fixture +def mock_aiohttp_client(): + with aioresponses() as m: + yield m + @pytest.fixture() def mock_async_context_manager(): class AsyncContextManager: diff --git a/fractal/matrix/async_client.py b/fractal/matrix/async_client.py index 08b9f13..1b859a6 100644 --- a/fractal/matrix/async_client.py +++ b/fractal/matrix/async_client.py @@ -157,7 +157,6 @@ async def get_room_invites(self) -> Dict[str, InviteInfo]: # save previous next batch since sync will replace it. prev_next_batch = self.next_batch res = await self.sync(since=None, timeout=0, sync_filter=invite_filter()) - print(f"RES IS ============== {res}") if isinstance(res, SyncError): raise Exception(res.message) # restore previous next batch @@ -255,7 +254,6 @@ async def register_with_token( # register will replace the access token that's on the client with the one returned # from a successful registration. We want to keep the original access token. self.access_token = access_token - if disable_ratelimiting: await self.disable_ratelimiting(matrix_id) diff --git a/pyproject.toml b/pyproject.toml index f03dad7..95a5ec4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,9 +22,9 @@ pytest-mock = { version = "^3.11.1", optional = true } aioresponses = {version = "^0.7.6", optional = true} ipython = { version = "^8.17.2", optional = true } - [tool.poetry.extras] -dev = ["pytest-django", "pytest", "pytest-cov", "pytest-mock", "pytest-asyncio", "ipython"] +dev = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "ipython", "pytest-benchmark", "aioresponses"] + [build-system] requires = ["poetry-core"] diff --git a/tests/test_fractal_async_client_get_latest_sync_token.py b/tests/fractal-async-client/test_fractal_async_client_get_latest_sync_token.py similarity index 100% rename from tests/test_fractal_async_client_get_latest_sync_token.py rename to tests/fractal-async-client/test_fractal_async_client_get_latest_sync_token.py diff --git a/tests/test_fractal_async_client_invite.py b/tests/fractal-async-client/test_fractal_async_client_invite.py similarity index 92% rename from tests/test_fractal_async_client_invite.py rename to tests/fractal-async-client/test_fractal_async_client_invite.py index 57b5c7d..4f8daf7 100644 --- a/tests/test_fractal_async_client_invite.py +++ b/tests/fractal-async-client/test_fractal_async_client_invite.py @@ -6,6 +6,7 @@ from nio import ( RoomGetStateEventError, RoomGetStateEventResponse, + RoomInviteError, RoomInviteResponse, RoomPutStateError, RoomPutStateResponse, @@ -56,6 +57,16 @@ async def test_invite_send_invite(): ) +async def test_invite_roominviteerror(): + sample_user_id = "@sample_user:sample_domain" + sample_room_id = "sample_id" + client = FractalAsyncClient() + client.room_invite = AsyncMock(return_value=RoomInviteError("Room Invite Failed")) + with pytest.raises(Exception) as e: + await client.invite(user_id=sample_user_id, room_id=sample_room_id, admin=True) + assert "Room Invite Failed" in str(e.value) + + async def test_invite_raise_exception_for_userID(): sample_user_id = "sample_user:sample_domain" sample_room_id = "sample_id" @@ -99,7 +110,6 @@ async def test_invite_room_get_state_event_error_when_has_message(): assert "Error message" in str(e.value) -# @pytest.mark.skip("Having trouble reaching the else condition and testing the exception") async def test_invite_room_get_state_event_error_when_no_message(): sample_user_id = "@sample_user:sample_domain" sample_room_id = "sample_id" diff --git a/tests/test_fractal_async_client_send_message.py b/tests/fractal-async-client/test_fractal_async_client_send_message.py similarity index 100% rename from tests/test_fractal_async_client_send_message.py rename to tests/fractal-async-client/test_fractal_async_client_send_message.py diff --git a/tests/test_matrix.py b/tests/fractal-async-client/test_matrix.py similarity index 60% rename from tests/test_matrix.py rename to tests/fractal-async-client/test_matrix.py index ae6d584..4cd9b96 100644 --- a/tests/test_matrix.py +++ b/tests/fractal-async-client/test_matrix.py @@ -1,5 +1,6 @@ import asyncio import os +from builtins import super from copy import deepcopy from unittest.mock import AsyncMock, MagicMock, patch @@ -7,23 +8,33 @@ import pytest from aioresponses import aioresponses from fractal.matrix import MatrixClient, get_homeserver_for_matrix_id -from fractal.matrix.async_client import FractalAsyncClient +from fractal.matrix.async_client import FractalAsyncClient, parse_matrix_id from fractal.matrix.exceptions import ( UnknownDiscoveryInfoException, WellKnownNotFoundException, ) from nio import ( AsyncClient, + DeviceList, + DeviceOneTimeKeyCount, DiscoveryInfoError, DiscoveryInfoResponse, + InviteInfo, JoinError, + JoinResponse, + PresenceEvent, RegisterResponse, + RoomInfo, + Rooms, SyncError, + SyncResponse, + Timeline, TransferMonitor, UploadError, UploadResponse, ) from nio.http import TransportResponse +from nio.responses import RegisterErrorResponse async def test_decorator_async_context_manager_raises(): @@ -206,17 +217,43 @@ async def test_get_room_invites_sync_error(): assert "Error with request" in str(e.value) -@pytest.mark.skip("Either my logic is wrong or the get_room_invites is bugged") -async def test_get_room_invites_save_prev_next_batch(): +async def test_get_room_invites_return_inviteinfo(): client = FractalAsyncClient() - client.next_batch = "sample_batch_value" - mock_sync_response = {"rooms": {"invite": {"room_id_1": {}, "room_id_2": {}}}} - # we create a mock of sync and make it return our dictionary - with patch.object(client, "sync", new=AsyncMock(return_value=mock_sync_response)): - invites_dict = await client.get_room_invites() - expected_invites_dict = mock_sync_response["rooms"]["invite"] - assert client.next_batch == "sample_batch_value" - assert invites_dict == expected_invites_dict + sample_next_batch = "sample_batch_value" + rooms = Rooms( + invite={"invite_room_id": InviteInfo(invite_state=[])}, + join={ + "join_room_id": RoomInfo( + timeline=Timeline(events=[], limited=True, prev_batch=""), + state=[], + ephemeral=[], + account_data=[], + ) + }, + leave={ + "leave_room_id": RoomInfo( + timeline=Timeline(events=[], limited=True, prev_batch=""), + state=[], + ephemeral=[], + account_data=[], + ) + }, + ) + devicelist = DeviceList(changed=[], left=[]) + devicecount = DeviceOneTimeKeyCount(curve25519=None, signed_curve25519=None) + client.sync = AsyncMock( + return_value=SyncResponse( + next_batch=sample_next_batch, + rooms=rooms, + device_key_count=devicecount, + device_list=devicelist, + to_device_events=[], + presence_events=[], + ) + ) + result = await client.get_room_invites() + expected_invite_info = {"invite_room_id": InviteInfo(invite_state=[])} + assert result == expected_invite_info async def test_join_room_logger(): @@ -228,6 +265,16 @@ async def test_join_room_logger(): mock_logger.info.assert_called_once_with(f"Joining room: {room_id}") +async def test_join_room_join_response(): + client = FractalAsyncClient() + room_id = "sample_room_id" + join_response = JoinResponse(room_id=room_id) + client.join = AsyncMock(return_value=join_response) + await client.join_room(room_id=room_id) + client.join.assert_called_once_with(room_id) + assert await client.join_room(room_id=room_id) is None + + async def test_join_room_join_error(): client = FractalAsyncClient() client.join = AsyncMock(return_value=JoinError("Failed to join room")) @@ -237,39 +284,159 @@ async def test_join_room_join_error(): assert "Failed to join room" in str(e.value) -@pytest.mark.skip("Don't know how to use aiohttp") -async def test_disable_ratelimiting_url_creation(): +async def test_disable_ratelimiting_post_mock_correct(mock_aiohttp_client): client = FractalAsyncClient() matrix_id = "sample_matrix_id" + request_url = f"{client.homeserver}/_synapse/admin/v1/users/{matrix_id}/override_ratelimit" + mock_aiohttp_client.post(request_url, status=200) await client.disable_ratelimiting(matrix_id=matrix_id) + mock_aiohttp_client.assert_called_with( + request_url, + method="POST", + headers={"Authorization": f"Bearer {client.access_token}"}, + json={}, + ) -@pytest.mark.skip( - "Type error, either lack of knowledge of aiohttp or bug in disable_ratelimiting" -) -async def test_disable_ratelimiting_logger(): +async def test_disable_ratelimiting_override_error(mock_aiohttp_client): + client = FractalAsyncClient() + matrix_id = "sample_matrix_id" + request_url = f"{client.homeserver}/_synapse/admin/v1/users/{matrix_id}/override_ratelimit" + status = 500 + mock_aiohttp_client.post(request_url, status=status) + with pytest.raises(Exception) as e: + await client.disable_ratelimiting(matrix_id=matrix_id) + assert f"Failed to override rate limit. Error Response status {status}: " in str(e.value) + + +async def test_disable_ratelimiting_logger(mock_aiohttp_client): + client = FractalAsyncClient() + matrix_id = "sample_matrix_id" + request_url = f"{client.homeserver}/_synapse/admin/v1/users/{matrix_id}/override_ratelimit" + mock_aiohttp_client.post(request_url, status=200) + with patch("fractal.matrix.async_client.logger", new=MagicMock()) as mock_logger: + await client.disable_ratelimiting(matrix_id=matrix_id) + mock_logger.info.assert_called_with("Rate limit override successful.") + mock_aiohttp_client.assert_called_with( + request_url, + method="POST", + headers={"Authorization": f"Bearer {client.access_token}"}, + json={}, + ) + + +async def test_generate_registration_token_post_mock(mock_aiohttp_client): + client = FractalAsyncClient() + request_url = f"{client.homeserver}/_synapse/admin/v1/registration_tokens/new" + token_value = "sample_token" + expected_payload = {"token": token_value} + mock_aiohttp_client.post(request_url, status=200, payload=expected_payload) + token = await client.generate_registration_token() + assert isinstance(token, str) + mock_aiohttp_client.assert_called_once_with( + request_url, + method="POST", + headers={"Authorization": f"Bearer {client.access_token}"}, + json={}, + ) + + +async def test_generate_registration_token_override_error(mock_aiohttp_client): + client = FractalAsyncClient() + request_url = f"{client.homeserver}/_synapse/admin/v1/registration_tokens/new" + status = 500 + mock_aiohttp_client.post(request_url, status=status) + with patch("fractal.matrix.async_client.logger", new=MagicMock()) as mock_logger: + with pytest.raises(Exception): + await client.generate_registration_token() + mock_logger.error.assert_called_with( + f"Failed to override rate limit. Error Response status {status}: " + ) + + +async def test_register_with_token_username_created_and_parent_register_with_token_called(): client = FractalAsyncClient() matrix_id = "sample_matrix_id" - url = f"https://_synapse/admin/v1/users/{matrix_id}/override_ratelimit" - mock_post = AsyncMock(return_value=MagicMock(ok=True, text=AsyncMock(return_value="OK"))) + password = "sample_password" + registration_token = "sample_registration_token" with patch( - "fractal.matrix.async_client.aiohttp.ClientSession.post", new=mock_post - ) as mock_post_call: - with patch("fractal.matrix.async_client.logger", new=MagicMock()) as mock_logger: - await client.disable_ratelimiting(matrix_id=matrix_id) - mock_logger.info.assert_called_once_with("Rate limit override successful.") - mock_post_call.assert_called_once_with( - url, json={}, headers={"Authorization": f"Bearer {client.access_token}"} + "fractal.matrix.async_client.parse_matrix_id", + new=MagicMock(return_value=["sample_username"]), + ) as mock_parse: + with patch( + "fractal.matrix.async_client.AsyncClient.register_with_token", new=AsyncMock() + ) as mock_register_with_token: + client.disable_ratelimiting = AsyncMock() + await client.register_with_token( + matrix_id=matrix_id, + password=password, + registration_token=registration_token, + ) + mock_register_with_token.assert_called_once_with( + "sample_username", password, registration_token, device_name="" ) -@pytest.mark.skip("come back to") -async def test_register_with_token(): +async def test_register_with_token_registererrorresponse(): client = FractalAsyncClient() matrix_id = "sample_matrix_id" password = "sample_password" registration_token = "sample_registration_token" - client.register_with_token = AsyncMock() + with patch("fractal.matrix.async_client.parse_matrix_id", new=MagicMock()) as mock_parse: + with patch( + "fractal.matrix.async_client.AsyncClient.register_with_token", + new=AsyncMock(return_value=RegisterErrorResponse("Error with response")), + ) as mock_register_with_token: + with pytest.raises(Exception) as e: + await client.register_with_token( + matrix_id=matrix_id, + password=password, + registration_token=registration_token, + disable_ratelimiting=True, + ) + assert "Error with response" in str(e) + + +async def test_register_with_token_disable_ratelimiting_for_user(): + client = FractalAsyncClient() + matrix_id = "sample_matrix_id" + password = "sample_password" + registration_token = "sample_registration_token" + with patch("fractal.matrix.async_client.parse_matrix_id", new=MagicMock()) as mock_parse: + with patch( + "fractal.matrix.async_client.AsyncClient.register_with_token", new=AsyncMock() + ) as mock_register_with_token: + client.disable_ratelimiting = AsyncMock() + await client.register_with_token( + matrix_id=matrix_id, + password=password, + registration_token=registration_token, + ) + client.disable_ratelimiting.assert_called_once_with(matrix_id) + + +async def test_register_with_token_successful_registration_access_token(): + client = FractalAsyncClient() + matrix_id = "sample_matrix_id" + password = "sample_password" + registration_token = "sample_registration_token" + expected_access_token = "expected_token" + with patch("fractal.matrix.async_client.parse_matrix_id", new=MagicMock()) as mock_parse: + with patch( + "fractal.matrix.async_client.AsyncClient.register_with_token", new=AsyncMock() + ) as mock_register_with_token: + mock_register_with_token.return_value = RegisterResponse( + user_id="sample_user", + device_id="sample_device", + access_token=expected_access_token, + ) + client.disable_ratelimiting = AsyncMock() + access_token = await client.register_with_token( + matrix_id=matrix_id, + password=password, + registration_token=registration_token, + ) + assert access_token == expected_access_token async def test_upload_file_success_no_monitor(mock_async_context_manager): @@ -326,4 +493,4 @@ async def test_upload_file_monitor_success(mock_async_context_manager): mock_logger.info.assert_called_once_with(f"Uploading file: {file_path}") assert "http://Someurl" in content_uri client.upload.assert_called() - assert client.upload.call_args.kwargs["monitor"].total_size == 10 \ No newline at end of file + assert client.upload.call_args.kwargs["monitor"].total_size == 10 diff --git a/tests/test_fractal_async_client.py b/tests/test_fractal_async_client.py deleted file mode 100644 index 62e5492..0000000 --- a/tests/test_fractal_async_client.py +++ /dev/null @@ -1,82 +0,0 @@ -from unittest.mock import AsyncMock, MagicMock, patch - -from fractal.matrix.async_client import FractalAsyncClient, RoomSendResponse -from nio import RoomSendError - - -async def test_send_message_contains_bytes(): - test_fractal_client = FractalAsyncClient() - with patch( - "fractal.matrix.async_client.FractalAsyncClient.room_send", new=AsyncMock() - ) as mock_room_send: - mock_room_send.return_value = MagicMock(spec=RoomSendResponse) - room = "test_room" - test_bytes = b"test_message" - await test_fractal_client.send_message(room=room, message=test_bytes) - expected_argument = {"msgtype": "taskiq.task", "body": "test_message"} - mock_room_send.assert_called_with(room, "taskiq.task", expected_argument) - - -async def test_send_message_contains_string(): - test_fractal_client = FractalAsyncClient() - with patch( - "fractal.matrix.async_client.FractalAsyncClient.room_send", new=AsyncMock() - ) as mock_room_send: - mock_room_send.return_value = MagicMock(spec=RoomSendResponse) - room = "test_room" - test_string = "test_string" - await test_fractal_client.send_message(room=room, message=test_string) - expected_argument = {"msgtype": "taskiq.task", "body": "test_string"} - mock_room_send.assert_called_with(room, "taskiq.task", expected_argument) - - -async def test_send_message_contains_list(): - test_fractal_client = FractalAsyncClient() - with patch( - "fractal.matrix.async_client.FractalAsyncClient.room_send", new=AsyncMock() - ) as mock_room_send: - mock_room_send.return_value = MagicMock(spec=RoomSendResponse) - room = "test_room" - test_list = ["testlist"] - await test_fractal_client.send_message(room=room, message=test_list) - expected_argument = {"msgtype": "taskiq.task", "body": test_list} - mock_room_send.assert_called_with(room, "taskiq.task", expected_argument) - - -async def test_send_message_contains_dictionary(): - test_fractal_client = FractalAsyncClient() - with patch( - "fractal.matrix.async_client.FractalAsyncClient.room_send", new=AsyncMock() - ) as mock_room_send: - mock_room_send.return_value = MagicMock(spec=RoomSendResponse) - room = "test_room" - test_dic = {"val": "1"} - await test_fractal_client.send_message(room=room, message=test_dic) - expected_argument = {"msgtype": "taskiq.task", "body": test_dic} - mock_room_send.assert_called_with(room, "taskiq.task", expected_argument) - - -async def test_send_message_returns_error(): - test_fractal_client = FractalAsyncClient() - with patch( - "fractal.matrix.async_client.FractalAsyncClient.room_send", new=AsyncMock() - ) as mock_room_send: - mock_room_send.return_value = RoomSendError(message="Test Error Message") - with patch("fractal.matrix.async_client.logger") as mock_logger: - room = "test_room" - test_dic = {"val": "1"} - await test_fractal_client.send_message(room=room, message=test_dic) - mock_logger.error.assert_called_once() - - -async def test_send_message_raises_exception(): - test_fractal_client = FractalAsyncClient() - with patch( - "fractal.matrix.async_client.FractalAsyncClient.room_send", new=AsyncMock() - ) as mock_room_send: - mock_room_send.side_effect = Exception() - with patch("fractal.matrix.async_client.logger") as mock_logger: - room = "test_room" - test_dic = {"val": "1"} - await test_fractal_client.send_message(room=room, message=test_dic) - mock_logger.error.assert_called_once() diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..56d94b8 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,68 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from fractal.matrix import utils +from fractal.matrix.exceptions import InvalidMatrixIdException + + +async def test_prompt_matrix_password_if_homeserver_url(capsys): + matrix_id = "test_matrix_id" + homeserver_url = "homeserver_url" + sample_password = "sample_password" + with patch("fractal.matrix.utils.getpass", new=MagicMock(return_value=sample_password)): + utils.prompt_matrix_password(matrix_id=matrix_id, homeserver_url=homeserver_url) + text = capsys.readouterr() + assert ( + f"Login with Matrix ID ({matrix_id}) to sign in to {homeserver_url}" == text.out.strip() + ) + + +async def test_prompt_matrix_password_if_no_homeserver_url(capsys): + matrix_id = "test_matrix_id" + sample_password = "sample_password" + with patch("fractal.matrix.utils.getpass", new=MagicMock(return_value=sample_password)): + utils.prompt_matrix_password(matrix_id=matrix_id) + text = capsys.readouterr() + assert f"Login with Matrix ID ({matrix_id}) to continue" == text.out.strip() + + +async def test_prompt_matrix_password_no_interrupt(): + matrix_id = "test_matrix_id" + sample_password = "sample_password" + with patch("fractal.matrix.utils.getpass", new=MagicMock(return_value=sample_password)): + password = utils.prompt_matrix_password(matrix_id) + assert password == sample_password + + +async def test_prompt_matrix_password_keyboard_interrupt(): + matrix_id = "test_matrix_id" + with pytest.raises(SystemExit) as e: + with patch("fractal.matrix.utils.getpass", new=MagicMock(side_effect=KeyboardInterrupt)): + utils.prompt_matrix_password(matrix_id) + assert e.type == SystemExit + + +async def test_parse_matrix_id_group_returns(): + matrix_id = "test_matrix_id" + sample_localpart = "test_localpart" + sample_homeserver = "test_homeserver" + compiled_pattern_mock = MagicMock() + match_mock = MagicMock() + compiled_pattern_mock.match.return_value = match_mock + with patch("re.compile", return_value=compiled_pattern_mock): + with patch.object( + match_mock, "group", return_value=(sample_localpart, sample_homeserver) + ): + localpart, homeserver = utils.parse_matrix_id(matrix_id) + assert localpart[0] == sample_localpart + assert homeserver[1] == sample_homeserver + + +async def test_parse_matrix_id_invalidmatrixidexception(): + matrix_id = "test_matrix_id" + compiled_pattern_mock = MagicMock() + compiled_pattern_mock.match.return_value = None + with patch("re.compile", return_value=compiled_pattern_mock): + with pytest.raises(InvalidMatrixIdException) as e: + utils.parse_matrix_id(matrix_id) + assert f"{matrix_id} is not a valid Matrix ID." in str(e.value)