From 57db20acb14614473dabb10e7566aa87e94197c0 Mon Sep 17 00:00:00 2001 From: JoshuaVulcan <38018017+JoshuaVulcan@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:55:26 -0800 Subject: [PATCH] ERA-12660: achieve full sync/async parity for messages interface Sync ERClient: - Add post_message() for creating messages - Add get_message(id) for fetching a single message by ID - Fix get_messages() pagination bug (undefined variable 'p') - Accept **kwargs in get_messages() for query param pass-through Async AsyncERClient: - Add get_messages() as an async generator with automatic pagination - Add get_message(id) for fetching a single message by ID - Add delete_message(id) for deleting messages - Add _delete() convenience method to async client - Handle HTTP 204 No Content responses in async _call() Tests: - Add sync test suite (tests/sync_client/) with conftest and fixtures - Add comprehensive tests for all new sync methods (post, get, get_list, delete) - Add comprehensive tests for all new async methods (get_messages, get_message, delete_message) - Cover success, pagination, empty results, not-found, and forbidden cases Co-authored-by: Cursor --- erclient/client.py | 74 +++++++- tests/async_client/test_delete_message.py | 38 ++++ tests/async_client/test_get_message.py | 41 +++++ tests/async_client/test_get_messages.py | 157 ++++++++++++++++ tests/sync_client/__init__.py | 0 tests/sync_client/conftest.py | 207 ++++++++++++++++++++++ tests/sync_client/test_delete_message.py | 48 +++++ tests/sync_client/test_get_message.py | 30 ++++ tests/sync_client/test_get_messages.py | 78 ++++++++ tests/sync_client/test_post_message.py | 59 ++++++ 10 files changed, 724 insertions(+), 8 deletions(-) create mode 100644 tests/async_client/test_delete_message.py create mode 100644 tests/async_client/test_get_message.py create mode 100644 tests/async_client/test_get_messages.py create mode 100644 tests/sync_client/__init__.py create mode 100644 tests/sync_client/conftest.py create mode 100644 tests/sync_client/test_delete_message.py create mode 100644 tests/sync_client/test_get_message.py create mode 100644 tests/sync_client/test_get_messages.py create mode 100644 tests/sync_client/test_post_message.py diff --git a/erclient/client.py b/erclient/client.py index 02bbbc0..6a4b912 100644 --- a/erclient/client.py +++ b/erclient/client.py @@ -327,6 +327,17 @@ def delete_subject(self, subject_id): def delete_message(self, message_id): self._delete('messages/' + message_id + '/') + def post_message(self, message): + """ + Post a new message. + :param message: dict with message payload (message_type, text, message_time, device_location, etc.) + :return: created message data + """ + self.logger.debug('Posting message: %s', message) + result = self._post('messages', payload=message) + self.logger.debug('Result of message post is: %s', result) + return result + def delete_patrol(self, patrol_id): self._delete('activity/patrols/' + patrol_id + '/') @@ -576,23 +587,35 @@ def get_event_type(self, event_type_name): def get_event_categories(self, include_inactive=False): return self._get(f'activity/events/categories', params={"include_inactive": include_inactive}) - def get_messages(self): - - results = self._get(path='messages') + def get_messages(self, **kwargs): + """ + Get messages as a generator, with automatic pagination. + :param kwargs: optional query params (e.g. page_size) + :return: generator yielding individual message dicts + """ + params = dict((k, v) for k, v in kwargs.items()) + results = self._get(path='messages', params=params) while True: if results and results.get('results'): for r in results['results']: yield r - if results and results['next']: - url, params = split_link(results['next']) - # FixMe: p is not defined in this context - p['page'] = params['page'] - results = self._get(path='messages') + if results and results.get('next'): + url, query_params = split_link(results['next']) + params['page'] = query_params['page'] + results = self._get(path='messages', params=params) else: break + def get_message(self, message_id): + """ + Get a single message by ID. + :param message_id: UUID of the message + :return: message data dict + """ + return self._get(path=f'messages/{message_id}') + def get_event_types(self, include_inactive=False, include_schema=False): return self._get('activity/events/eventtypes', params={"include_inactive": include_inactive, "include_schema": include_schema}) @@ -1185,6 +1208,36 @@ async def post_message(self, message, params=None): self.logger.debug(f'Posting message: {message}') return await self._post('messages', payload=message, params=params) + async def get_messages(self, **kwargs): + """ + Returns an async generator to iterate over messages. + Optional kwargs passed as query params: + page_size: Change the page size. Default 100. + """ + params = {**kwargs} + if not params.get('page_size'): + params['page_size'] = 100 + async for message in self._get_data(endpoint='messages', params=params): + yield message + + async def get_message(self, message_id): + """ + Get a single message by ID. + :param message_id: UUID of the message + :return: message data dict + """ + self.logger.debug(f'Getting message: {message_id}') + return await self._get(f'messages/{message_id}') + + async def delete_message(self, message_id): + """ + Delete a message by ID. + :param message_id: UUID of the message + :return: response data + """ + self.logger.debug(f'Deleting message: {message_id}') + return await self._delete(f'messages/{message_id}/') + async def get_source_by_manufacturer_id(self, manufacturer_id): """ Get a source by manufacturer_id. @@ -1432,6 +1485,9 @@ async def _post(self, path, payload, params=None): async def _patch(self, path, payload, params=None): return await self._call(path, payload, "PATCH", params) + async def _delete(self, path, params=None): + return await self._call(path=path, payload=None, method="DELETE", params=params) + async def _call(self, path, payload, method, params=None): try: auth_headers = await self.auth_headers() @@ -1469,6 +1525,8 @@ async def _call(self, path, payload, method, params=None): except httpx.HTTPStatusError as e: self._handle_http_status_error(path, method, e) else: # Parse the response + if response.status_code == 204: + return None json_response = response.json() return json_response.get('data', json_response) diff --git a/tests/async_client/test_delete_message.py b/tests/async_client/test_delete_message.py new file mode 100644 index 0000000..bdc80bb --- /dev/null +++ b/tests/async_client/test_delete_message.py @@ -0,0 +1,38 @@ +import httpx +import pytest +import respx + +from erclient import ERClientNotFound + + +@pytest.mark.asyncio +async def test_delete_message_success(er_client): + message_id = "da783214-0d79-4d8c-ba6c-687688e3f6e7" + async with respx.mock( + base_url=er_client.service_root, assert_all_called=False + ) as respx_mock: + route = respx_mock.delete(f"messages/{message_id}/") + route.return_value = httpx.Response(httpx.codes.NO_CONTENT) + + result = await er_client.delete_message(message_id) + assert route.called + assert result is None # 204 No Content returns None + await er_client.close() + + +@pytest.mark.asyncio +async def test_delete_message_not_found(er_client, not_found_response): + message_id = "nonexistent-id" + async with respx.mock( + base_url=er_client.service_root, assert_all_called=False + ) as respx_mock: + route = respx_mock.delete(f"messages/{message_id}/") + route.return_value = httpx.Response( + httpx.codes.NOT_FOUND, + json=not_found_response, + ) + + with pytest.raises(ERClientNotFound): + await er_client.delete_message(message_id) + assert route.called + await er_client.close() diff --git a/tests/async_client/test_get_message.py b/tests/async_client/test_get_message.py new file mode 100644 index 0000000..c34589d --- /dev/null +++ b/tests/async_client/test_get_message.py @@ -0,0 +1,41 @@ +import httpx +import pytest +import respx + +from erclient import ERClientNotFound + + +@pytest.mark.asyncio +async def test_get_message_success(er_client, message_received_response): + message_id = "da783214-0d79-4d8c-ba6c-687688e3f6e7" + async with respx.mock( + base_url=er_client.service_root, assert_all_called=False + ) as respx_mock: + route = respx_mock.get(f"messages/{message_id}") + route.return_value = httpx.Response( + httpx.codes.OK, + json={"data": message_received_response}, + ) + + result = await er_client.get_message(message_id) + assert route.called + assert result == message_received_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_message_not_found(er_client, not_found_response): + message_id = "nonexistent-id" + async with respx.mock( + base_url=er_client.service_root, assert_all_called=False + ) as respx_mock: + route = respx_mock.get(f"messages/{message_id}") + route.return_value = httpx.Response( + httpx.codes.NOT_FOUND, + json=not_found_response, + ) + + with pytest.raises(ERClientNotFound): + await er_client.get_message(message_id) + assert route.called + await er_client.close() diff --git a/tests/async_client/test_get_messages.py b/tests/async_client/test_get_messages.py new file mode 100644 index 0000000..d8c981e --- /dev/null +++ b/tests/async_client/test_get_messages.py @@ -0,0 +1,157 @@ +import httpx +import pytest +import respx + + +@pytest.fixture +def get_messages_single_page_response(message_received_response): + return { + "count": 2, + "next": None, + "previous": None, + "results": [ + message_received_response, + { + "id": "ab123456-0d79-4d8c-ba6c-687688e3f6e7", + "sender": { + "content_type": "observations.subject", + "id": "d2bd0ac8-080d-4be9-a8c2-2250623e6782", + "name": "gundi2", + "subject_type": "unassigned", + "subject_subtype": "mm-inreach-test", + "common_name": None, + "additional": {}, + "created_at": "2025-06-05T07:05:12.817899-07:00", + "updated_at": "2025-06-05T07:05:12.817926-07:00", + "is_active": True, + "user": None, + "tracks_available": False, + "image_url": "/static/pin-black.svg", + }, + "receiver": None, + "device": "443724d6-043f-4014-bea6-4d80a38469c8", + "message_type": "inbox", + "text": "Second test message", + "status": "received", + "device_location": {"latitude": -51.687, "longitude": -72.71}, + "message_time": "2025-06-05T04:08:00.000000-07:00", + "read": False, + }, + ], + } + + +@pytest.fixture +def get_messages_page_one_response(message_received_response): + return { + "count": 3, + "next": "https://fake-site.erdomain.org/api/v1.0/messages?page=2&page_size=2&use_cursor=true", + "previous": None, + "results": [ + message_received_response, + { + "id": "ab123456-0d79-4d8c-ba6c-687688e3f6e7", + "sender": None, + "receiver": None, + "device": "443724d6-043f-4014-bea6-4d80a38469c8", + "message_type": "inbox", + "text": "Second message", + "status": "received", + "device_location": {"latitude": -51.687, "longitude": -72.71}, + "message_time": "2025-06-05T04:08:00.000000-07:00", + "read": False, + }, + ], + } + + +@pytest.fixture +def get_messages_page_two_response(): + return { + "count": 3, + "next": None, + "previous": "https://fake-site.erdomain.org/api/v1.0/messages?page_size=2&use_cursor=true", + "results": [ + { + "id": "cd789012-0d79-4d8c-ba6c-687688e3f6e7", + "sender": None, + "receiver": None, + "device": "443724d6-043f-4014-bea6-4d80a38469c8", + "message_type": "inbox", + "text": "Third message", + "status": "received", + "device_location": {"latitude": -51.687, "longitude": -72.71}, + "message_time": "2025-06-05T04:09:00.000000-07:00", + "read": True, + }, + ], + } + + +@pytest.mark.asyncio +async def test_get_messages_single_page(er_client, get_messages_single_page_response): + async with respx.mock( + base_url=er_client.service_root, assert_all_called=False + ) as respx_mock: + route = respx_mock.get("messages").return_value = httpx.Response( + httpx.codes.OK, + json={"data": get_messages_single_page_response}, + ) + + messages = [] + async for msg in er_client.get_messages(): + messages.append(msg) + + assert len(messages) == 2 + assert messages[0]["id"] == "da783214-0d79-4d8c-ba6c-687688e3f6e7" + assert messages[1]["id"] == "ab123456-0d79-4d8c-ba6c-687688e3f6e7" + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_messages_paginated( + er_client, get_messages_page_one_response, get_messages_page_two_response +): + async with respx.mock( + base_url=er_client.service_root, assert_all_called=False + ) as respx_mock: + # First page + respx_mock.get("messages").mock( + side_effect=[ + httpx.Response( + httpx.codes.OK, + json={"data": get_messages_page_one_response}, + ), + httpx.Response( + httpx.codes.OK, + json={"data": get_messages_page_two_response}, + ), + ] + ) + + messages = [] + async for msg in er_client.get_messages(page_size=2): + messages.append(msg) + + assert len(messages) == 3 + assert messages[0]["id"] == "da783214-0d79-4d8c-ba6c-687688e3f6e7" + assert messages[2]["id"] == "cd789012-0d79-4d8c-ba6c-687688e3f6e7" + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_messages_empty(er_client): + async with respx.mock( + base_url=er_client.service_root, assert_all_called=False + ) as respx_mock: + respx_mock.get("messages").return_value = httpx.Response( + httpx.codes.OK, + json={"data": {"count": 0, "next": None, "previous": None, "results": []}}, + ) + + messages = [] + async for msg in er_client.get_messages(): + messages.append(msg) + + assert len(messages) == 0 + await er_client.close() diff --git a/tests/sync_client/__init__.py b/tests/sync_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/sync_client/conftest.py b/tests/sync_client/conftest.py new file mode 100644 index 0000000..b08282c --- /dev/null +++ b/tests/sync_client/conftest.py @@ -0,0 +1,207 @@ +import pytest + +from erclient.client import ERClient + + +@pytest.fixture +def er_server_info(): + return { + "service_root": "https://fake-site.erdomain.org/api/v1.0", + "username": "test", + "password": "test", + "token": "1110c87681cd1d12ad07c2d0f57d15d6079ae5d8", + "token_url": "https://fake-auth.erdomain.org/oauth2/token", + "client_id": "das_web_client", + "provider_key": "testintegration", + } + + +@pytest.fixture +def er_client(er_server_info): + return ERClient(**er_server_info) + + +@pytest.fixture +def message(): + return { + "message_type": "inbox", + "text": "A test message!", + "message_time": "2025-06-05T11:07:37.401Z", + "device_location": { + "latitude": -51.687, + "longitude": -72.710, + }, + "additional": { + "status": { + "autonomous": 0, + "lowBattery": 1, + "intervalChange": 0, + "resetDetected": 0, + } + }, + } + + +@pytest.fixture +def message_created_response(): + return { + "data": { + "id": "da783214-0d79-4d8c-ba6c-687688e3f6e7", + "sender": { + "content_type": "observations.subject", + "id": "d2bd0ac8-080d-4be9-a8c2-2250623e6782", + "name": "gundi2", + "subject_type": "unassigned", + "subject_subtype": "mm-inreach-test", + "common_name": None, + "additional": {}, + "created_at": "2025-06-05T07:05:12.817899-07:00", + "updated_at": "2025-06-05T07:05:12.817926-07:00", + "is_active": True, + "user": None, + "tracks_available": False, + "image_url": "/static/pin-black.svg", + }, + "receiver": None, + "device": "443724d6-043f-4014-bea6-4d80a38469c8", + "message_type": "inbox", + "text": "A test message!", + "status": "received", + "device_location": {"latitude": -51.687, "longitude": -72.71}, + "message_time": "2025-06-05T04:07:37.401000-07:00", + "read": False, + }, + "status": {"code": 201, "message": "Created"}, + } + + +@pytest.fixture +def message_detail_response(): + return { + "data": { + "id": "da783214-0d79-4d8c-ba6c-687688e3f6e7", + "sender": { + "content_type": "observations.subject", + "id": "d2bd0ac8-080d-4be9-a8c2-2250623e6782", + "name": "gundi2", + "subject_type": "unassigned", + "subject_subtype": "mm-inreach-test", + "common_name": None, + "additional": {}, + "created_at": "2025-06-05T07:05:12.817899-07:00", + "updated_at": "2025-06-05T07:05:12.817926-07:00", + "is_active": True, + "user": None, + "tracks_available": False, + "image_url": "/static/pin-black.svg", + }, + "receiver": None, + "device": "443724d6-043f-4014-bea6-4d80a38469c8", + "message_type": "inbox", + "text": "A test message!", + "status": "received", + "device_location": {"latitude": -51.687, "longitude": -72.71}, + "message_time": "2025-06-05T04:07:37.401000-07:00", + "read": False, + }, + "status": {"code": 200, "message": "OK"}, + } + + +@pytest.fixture +def get_messages_single_page_response(): + return { + "data": { + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "id": "da783214-0d79-4d8c-ba6c-687688e3f6e7", + "sender": None, + "receiver": None, + "device": "443724d6-043f-4014-bea6-4d80a38469c8", + "message_type": "inbox", + "text": "First message", + "status": "received", + "device_location": {"latitude": -51.687, "longitude": -72.71}, + "message_time": "2025-06-05T04:07:37.401000-07:00", + "read": False, + }, + { + "id": "ab123456-0d79-4d8c-ba6c-687688e3f6e7", + "sender": None, + "receiver": None, + "device": "443724d6-043f-4014-bea6-4d80a38469c8", + "message_type": "inbox", + "text": "Second message", + "status": "received", + "device_location": {"latitude": -51.687, "longitude": -72.71}, + "message_time": "2025-06-05T04:08:00.000000-07:00", + "read": False, + }, + ], + } + } + + +@pytest.fixture +def get_messages_page_one_response(): + return { + "data": { + "count": 3, + "next": "https://fake-site.erdomain.org/api/v1.0/messages?page=2&page_size=2", + "previous": None, + "results": [ + { + "id": "da783214-0d79-4d8c-ba6c-687688e3f6e7", + "sender": None, + "receiver": None, + "device": "443724d6-043f-4014-bea6-4d80a38469c8", + "message_type": "inbox", + "text": "First message", + "status": "received", + "device_location": {"latitude": -51.687, "longitude": -72.71}, + "message_time": "2025-06-05T04:07:37.401000-07:00", + "read": False, + }, + { + "id": "ab123456-0d79-4d8c-ba6c-687688e3f6e7", + "sender": None, + "receiver": None, + "device": "443724d6-043f-4014-bea6-4d80a38469c8", + "message_type": "inbox", + "text": "Second message", + "status": "received", + "device_location": {"latitude": -51.687, "longitude": -72.71}, + "message_time": "2025-06-05T04:08:00.000000-07:00", + "read": False, + }, + ], + } + } + + +@pytest.fixture +def get_messages_page_two_response(): + return { + "data": { + "count": 3, + "next": None, + "previous": "https://fake-site.erdomain.org/api/v1.0/messages?page_size=2", + "results": [ + { + "id": "cd789012-0d79-4d8c-ba6c-687688e3f6e7", + "sender": None, + "receiver": None, + "device": "443724d6-043f-4014-bea6-4d80a38469c8", + "message_type": "inbox", + "text": "Third message", + "status": "received", + "device_location": {"latitude": -51.687, "longitude": -72.71}, + "message_time": "2025-06-05T04:09:00.000000-07:00", + "read": True, + }, + ], + } + } diff --git a/tests/sync_client/test_delete_message.py b/tests/sync_client/test_delete_message.py new file mode 100644 index 0000000..5ece466 --- /dev/null +++ b/tests/sync_client/test_delete_message.py @@ -0,0 +1,48 @@ +import json +from unittest.mock import patch, MagicMock + +import pytest + +from erclient import ERClientNotFound, ERClientPermissionDenied + + +def test_delete_message_success(er_client): + message_id = "da783214-0d79-4d8c-ba6c-687688e3f6e7" + mock_response = MagicMock() + mock_response.ok = True + mock_response.status_code = 204 + + with patch.object(er_client._http_session, "delete", return_value=mock_response) as mock_delete: + # Sync delete_message calls _delete which returns True but delete_message itself doesn't return + er_client.delete_message(message_id) + + # Verify the delete endpoint was called correctly + mock_delete.assert_called_once() + call_url = mock_delete.call_args[0][0] + assert "messages/" + message_id + "/" in call_url + + +def test_delete_message_not_found(er_client): + message_id = "nonexistent-id" + mock_response = MagicMock() + mock_response.ok = False + mock_response.status_code = 404 + mock_response.text = json.dumps({"status": {"detail": "not found"}}) + + with patch.object(er_client._http_session, "delete", return_value=mock_response): + with pytest.raises(ERClientNotFound): + er_client.delete_message(message_id) + + +def test_delete_message_forbidden(er_client): + message_id = "da783214-0d79-4d8c-ba6c-687688e3f6e7" + mock_response = MagicMock() + mock_response.ok = False + mock_response.status_code = 403 + mock_response.text = json.dumps( + {"status": {"detail": "You do not have permission to perform this action."}} + ) + + with patch.object(er_client._http_session, "delete", return_value=mock_response): + with pytest.raises(ERClientPermissionDenied): + er_client.delete_message(message_id) diff --git a/tests/sync_client/test_get_message.py b/tests/sync_client/test_get_message.py new file mode 100644 index 0000000..3750f78 --- /dev/null +++ b/tests/sync_client/test_get_message.py @@ -0,0 +1,30 @@ +import json +from unittest.mock import patch, MagicMock + +import pytest + +from erclient import ERClientNotFound + + +def test_get_message_success(er_client, message_detail_response): + message_id = "da783214-0d79-4d8c-ba6c-687688e3f6e7" + mock_response = MagicMock() + mock_response.ok = True + mock_response.text = json.dumps(message_detail_response) + + with patch.object(er_client._http_session, "get", return_value=mock_response): + result = er_client.get_message(message_id) + + assert result == message_detail_response["data"] + + +def test_get_message_not_found(er_client): + message_id = "nonexistent-id" + mock_response = MagicMock() + mock_response.ok = False + mock_response.status_code = 404 + mock_response.text = json.dumps({"status": {"detail": "not found"}}) + + with patch.object(er_client._http_session, "get", return_value=mock_response): + with pytest.raises(ERClientNotFound): + er_client.get_message(message_id) diff --git a/tests/sync_client/test_get_messages.py b/tests/sync_client/test_get_messages.py new file mode 100644 index 0000000..96b9670 --- /dev/null +++ b/tests/sync_client/test_get_messages.py @@ -0,0 +1,78 @@ +import json +from unittest.mock import patch, MagicMock, call + +import pytest + + +def test_get_messages_single_page(er_client, get_messages_single_page_response): + mock_response = MagicMock() + mock_response.ok = True + mock_response.text = json.dumps(get_messages_single_page_response) + + with patch.object(er_client._http_session, "get", return_value=mock_response): + messages = list(er_client.get_messages()) + + assert len(messages) == 2 + assert messages[0]["id"] == "da783214-0d79-4d8c-ba6c-687688e3f6e7" + assert messages[1]["id"] == "ab123456-0d79-4d8c-ba6c-687688e3f6e7" + + +def test_get_messages_paginated( + er_client, get_messages_page_one_response, get_messages_page_two_response +): + mock_response_page1 = MagicMock() + mock_response_page1.ok = True + mock_response_page1.text = json.dumps(get_messages_page_one_response) + + mock_response_page2 = MagicMock() + mock_response_page2.ok = True + mock_response_page2.text = json.dumps(get_messages_page_two_response) + + with patch.object( + er_client._http_session, + "get", + side_effect=[mock_response_page1, mock_response_page2], + ): + messages = list(er_client.get_messages()) + + assert len(messages) == 3 + assert messages[0]["id"] == "da783214-0d79-4d8c-ba6c-687688e3f6e7" + assert messages[1]["id"] == "ab123456-0d79-4d8c-ba6c-687688e3f6e7" + assert messages[2]["id"] == "cd789012-0d79-4d8c-ba6c-687688e3f6e7" + + +def test_get_messages_empty(er_client): + empty_response = { + "data": { + "count": 0, + "next": None, + "previous": None, + "results": [], + } + } + mock_response = MagicMock() + mock_response.ok = True + mock_response.text = json.dumps(empty_response) + + with patch.object(er_client._http_session, "get", return_value=mock_response): + messages = list(er_client.get_messages()) + + assert len(messages) == 0 + + +def test_get_messages_accepts_kwargs(er_client, get_messages_single_page_response): + """Verify that extra kwargs like page_size are passed as params.""" + mock_response = MagicMock() + mock_response.ok = True + mock_response.text = json.dumps(get_messages_single_page_response) + + with patch.object(er_client._http_session, "get", return_value=mock_response) as mock_get: + messages = list(er_client.get_messages(page_size=50)) + + # Verify that get was called with the page_size param + call_kwargs = mock_get.call_args + assert call_kwargs is not None + assert "params" in call_kwargs.kwargs or ( + len(call_kwargs.args) > 1 + or (call_kwargs.kwargs.get("params", {}).get("page_size") == 50) + ) diff --git a/tests/sync_client/test_post_message.py b/tests/sync_client/test_post_message.py new file mode 100644 index 0000000..fa08da6 --- /dev/null +++ b/tests/sync_client/test_post_message.py @@ -0,0 +1,59 @@ +import json +from unittest.mock import patch, MagicMock + +import pytest + +from erclient import ERClientNotFound, ERClientPermissionDenied + + +def test_post_message_success(er_client, message, message_created_response): + mock_response = MagicMock() + mock_response.ok = True + mock_response.json.return_value = message_created_response + mock_response.text = json.dumps(message_created_response) + + with patch.object(er_client._http_session, "post", return_value=mock_response): + result = er_client.post_message(message) + + assert result == message_created_response["data"] + + +def test_post_message_returns_full_response_when_no_data_key(er_client, message): + """When the response has no 'data' key, the full response is returned.""" + raw_response = { + "id": "da783214-0d79-4d8c-ba6c-687688e3f6e7", + "text": "A test message!", + } + mock_response = MagicMock() + mock_response.ok = True + mock_response.json.return_value = raw_response + mock_response.text = json.dumps(raw_response) + + with patch.object(er_client._http_session, "post", return_value=mock_response): + result = er_client.post_message(message) + + assert result == raw_response + + +def test_post_message_not_found(er_client, message): + mock_response = MagicMock() + mock_response.ok = False + mock_response.status_code = 404 + mock_response.text = json.dumps({"status": {"detail": "not found"}}) + + with patch.object(er_client._http_session, "post", return_value=mock_response): + with pytest.raises(ERClientNotFound): + er_client.post_message(message) + + +def test_post_message_forbidden(er_client, message): + mock_response = MagicMock() + mock_response.ok = False + mock_response.status_code = 403 + mock_response.text = json.dumps( + {"status": {"detail": "You do not have permission to perform this action."}} + ) + + with patch.object(er_client._http_session, "post", return_value=mock_response): + with pytest.raises(ERClientPermissionDenied): + er_client.post_message(message)