diff --git a/erclient/client.py b/erclient/client.py index 49c69d2..8076cec 100644 --- a/erclient/client.py +++ b/erclient/client.py @@ -390,6 +390,61 @@ def post_eventsource(self, eventprovider_id, eventsource): self.logger.debug('Result of eventsource post is: %s', result) return result + def get_eventproviders(self): + """ + Get a list of event providers. + :return: list of event provider dicts + """ + return self._get('activity/eventproviders') + + def get_eventprovider(self, eventprovider_id): + """ + Get a single event provider by ID. + :param eventprovider_id: UUID of the event provider + :return: event provider data dict + """ + return self._get(f'activity/eventprovider/{eventprovider_id}') + + def patch_eventprovider(self, eventprovider_id, payload): + """ + Update an event provider with partial data. + :param eventprovider_id: UUID of the event provider + :param payload: dict of fields to update + :return: updated event provider data dict + """ + self.logger.debug('Patching eventprovider %s: %s', eventprovider_id, payload) + result = self._patch(f'activity/eventprovider/{eventprovider_id}', payload=payload) + self.logger.debug('Result of eventprovider patch is: %s', result) + return result + + def get_eventsources(self, eventprovider_id): + """ + Get the list of event sources for a given event provider. + :param eventprovider_id: UUID of the event provider + :return: list of event source dicts + """ + return self._get(f'activity/eventprovider/{eventprovider_id}/eventsources') + + def get_eventsource(self, eventsource_id): + """ + Get a single event source by ID. + :param eventsource_id: UUID of the event source + :return: event source data dict + """ + return self._get(f'activity/eventsource/{eventsource_id}') + + def patch_eventsource(self, eventsource_id, payload): + """ + Update an event source with partial data. + :param eventsource_id: UUID of the event source + :param payload: dict of fields to update + :return: updated event source data dict + """ + self.logger.debug('Patching eventsource %s: %s', eventsource_id, payload) + result = self._patch(f'activity/eventsource/{eventsource_id}', payload=payload) + self.logger.debug('Result of eventsource patch is: %s', result) + return result + def post_event_photo(self, event_id, image): raise ValueError('post_event_photo is no longer valid.') @@ -1332,6 +1387,85 @@ 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 post_eventprovider(self, eventprovider): + """ + Create a new event provider. + :param eventprovider: dict with event provider payload + :return: created event provider data + """ + self.logger.debug(f'Posting eventprovider: {eventprovider}') + result = await self._post('activity/eventproviders/', payload=eventprovider) + self.logger.debug(f'Result of eventprovider post is: {result}') + return result + + async def post_eventsource(self, eventprovider_id, eventsource): + """ + Create a new event source under an event provider. + :param eventprovider_id: UUID of the parent event provider + :param eventsource: dict with event source payload + :return: created event source data + """ + self.logger.debug(f'Posting eventsource: {eventsource}') + result = await self._post( + f'activity/eventprovider/{eventprovider_id}/eventsources', payload=eventsource) + self.logger.debug(f'Result of eventsource post is: {result}') + return result + + async def get_eventproviders(self): + """ + Get a list of event providers. + :return: list of event provider dicts + """ + return await self._get('activity/eventproviders') + + async def get_eventprovider(self, eventprovider_id): + """ + Get a single event provider by ID. + :param eventprovider_id: UUID of the event provider + :return: event provider data dict + """ + return await self._get(f'activity/eventprovider/{eventprovider_id}') + + async def patch_eventprovider(self, eventprovider_id, payload): + """ + Update an event provider with partial data. + :param eventprovider_id: UUID of the event provider + :param payload: dict of fields to update + :return: updated event provider data dict + """ + self.logger.debug(f'Patching eventprovider {eventprovider_id}: {payload}') + result = await self._patch(f'activity/eventprovider/{eventprovider_id}', payload=payload) + self.logger.debug(f'Result of eventprovider patch is: {result}') + return result + + async def get_eventsources(self, eventprovider_id): + """ + Get the list of event sources for a given event provider. + :param eventprovider_id: UUID of the event provider + :return: list of event source dicts + """ + return await self._get(f'activity/eventprovider/{eventprovider_id}/eventsources') + + async def get_eventsource(self, eventsource_id): + """ + Get a single event source by ID. + :param eventsource_id: UUID of the event source + :return: event source data dict + """ + return await self._get(f'activity/eventsource/{eventsource_id}') + + async def patch_eventsource(self, eventsource_id, payload): + """ + Update an event source with partial data. + :param eventsource_id: UUID of the event source + :param payload: dict of fields to update + :return: updated event source data dict + """ + self.logger.debug(f'Patching eventsource {eventsource_id}: {payload}') + result = await self._patch(f'activity/eventsource/{eventsource_id}', payload=payload) + self.logger.debug(f'Result of eventsource patch is: {result}') + return result + async def get_source_by_manufacturer_id(self, manufacturer_id): """ Get a source by manufacturer_id. diff --git a/tests/async_client/test_eventproviders.py b/tests/async_client/test_eventproviders.py new file mode 100644 index 0000000..31893b1 --- /dev/null +++ b/tests/async_client/test_eventproviders.py @@ -0,0 +1,300 @@ +import httpx +import pytest +import respx + +from erclient import ERClientNotFound, ERClientPermissionDenied + + +EVENTPROVIDER_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" +EVENTSOURCE_ID = "f0e1d2c3-b4a5-6789-0fed-cba987654321" + + +@pytest.fixture +def eventprovider_payload(): + return { + "display": "My Test Provider", + "owner": {"id": "user-id-123"}, + } + + +@pytest.fixture +def eventprovider_response(): + return { + "data": { + "id": EVENTPROVIDER_ID, + "display": "My Test Provider", + "is_active": True, + "owner": {"id": "user-id-123"}, + }, + "status": {"code": 201, "message": "Created"}, + } + + +@pytest.fixture +def eventprovider_detail_response(): + return { + "data": { + "id": EVENTPROVIDER_ID, + "display": "My Test Provider", + "is_active": True, + "owner": {"id": "user-id-123"}, + }, + } + + +@pytest.fixture +def eventproviders_list_response(): + return { + "data": [ + { + "id": EVENTPROVIDER_ID, + "display": "My Test Provider", + "is_active": True, + }, + { + "id": "00000000-0000-0000-0000-000000000002", + "display": "Another Provider", + "is_active": False, + }, + ], + } + + +@pytest.fixture +def eventprovider_patched_response(): + return { + "data": { + "id": EVENTPROVIDER_ID, + "display": "Updated Provider Name", + "is_active": True, + }, + } + + +@pytest.fixture +def eventsource_payload(): + return { + "display": "Test Event Source", + } + + +@pytest.fixture +def eventsource_response(): + return { + "data": { + "id": EVENTSOURCE_ID, + "display": "Test Event Source", + "eventprovider": EVENTPROVIDER_ID, + }, + "status": {"code": 201, "message": "Created"}, + } + + +@pytest.fixture +def eventsources_list_response(): + return { + "data": [ + { + "id": EVENTSOURCE_ID, + "display": "Test Event Source", + "eventprovider": EVENTPROVIDER_ID, + }, + { + "id": "11111111-1111-1111-1111-111111111111", + "display": "Second Source", + "eventprovider": EVENTPROVIDER_ID, + }, + ], + } + + +@pytest.fixture +def eventsource_detail_response(): + return { + "data": { + "id": EVENTSOURCE_ID, + "display": "Test Event Source", + "eventprovider": EVENTPROVIDER_ID, + }, + } + + +@pytest.fixture +def eventsource_patched_response(): + return { + "data": { + "id": EVENTSOURCE_ID, + "display": "Renamed Source", + "eventprovider": EVENTPROVIDER_ID, + }, + } + + +# -- Event Provider Tests -- + + +@pytest.mark.asyncio +async def test_post_eventprovider_success(er_client, eventprovider_payload, eventprovider_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.post("activity/eventproviders/") + route.return_value = httpx.Response(httpx.codes.CREATED, json=eventprovider_response) + result = await er_client.post_eventprovider(eventprovider_payload) + assert route.called + assert result == eventprovider_response["data"] + await er_client.close() + + +@pytest.mark.asyncio +async def test_post_eventprovider_forbidden(er_client, eventprovider_payload, forbidden_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.post("activity/eventproviders/") + route.return_value = httpx.Response(httpx.codes.FORBIDDEN, json=forbidden_response) + with pytest.raises(ERClientPermissionDenied): + await er_client.post_eventprovider(eventprovider_payload) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_eventproviders_success(er_client, eventproviders_list_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("activity/eventproviders") + route.return_value = httpx.Response(httpx.codes.OK, json=eventproviders_list_response) + result = await er_client.get_eventproviders() + assert route.called + assert result == eventproviders_list_response["data"] + assert len(result) == 2 + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_eventprovider_success(er_client, eventprovider_detail_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get(f"activity/eventprovider/{EVENTPROVIDER_ID}") + route.return_value = httpx.Response(httpx.codes.OK, json=eventprovider_detail_response) + result = await er_client.get_eventprovider(EVENTPROVIDER_ID) + assert route.called + assert result == eventprovider_detail_response["data"] + assert result["id"] == EVENTPROVIDER_ID + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_eventprovider_not_found(er_client, not_found_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get(f"activity/eventprovider/{EVENTPROVIDER_ID}") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json=not_found_response) + with pytest.raises(ERClientNotFound): + await er_client.get_eventprovider(EVENTPROVIDER_ID) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_eventprovider_success(er_client, eventprovider_patched_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.patch(f"activity/eventprovider/{EVENTPROVIDER_ID}") + route.return_value = httpx.Response(httpx.codes.OK, json=eventprovider_patched_response) + result = await er_client.patch_eventprovider( + EVENTPROVIDER_ID, {"display": "Updated Provider Name"} + ) + assert route.called + assert result == eventprovider_patched_response["data"] + assert result["display"] == "Updated Provider Name" + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_eventprovider_not_found(er_client, not_found_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.patch(f"activity/eventprovider/{EVENTPROVIDER_ID}") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json=not_found_response) + with pytest.raises(ERClientNotFound): + await er_client.patch_eventprovider(EVENTPROVIDER_ID, {"display": "X"}) + assert route.called + await er_client.close() + + +# -- Event Source Tests -- + + +@pytest.mark.asyncio +async def test_post_eventsource_success(er_client, eventsource_payload, eventsource_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.post(f"activity/eventprovider/{EVENTPROVIDER_ID}/eventsources") + route.return_value = httpx.Response(httpx.codes.CREATED, json=eventsource_response) + result = await er_client.post_eventsource(EVENTPROVIDER_ID, eventsource_payload) + assert route.called + assert result == eventsource_response["data"] + await er_client.close() + + +@pytest.mark.asyncio +async def test_post_eventsource_provider_not_found(er_client, eventsource_payload, not_found_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.post(f"activity/eventprovider/{EVENTPROVIDER_ID}/eventsources") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json=not_found_response) + with pytest.raises(ERClientNotFound): + await er_client.post_eventsource(EVENTPROVIDER_ID, eventsource_payload) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_eventsources_success(er_client, eventsources_list_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get(f"activity/eventprovider/{EVENTPROVIDER_ID}/eventsources") + route.return_value = httpx.Response(httpx.codes.OK, json=eventsources_list_response) + result = await er_client.get_eventsources(EVENTPROVIDER_ID) + assert route.called + assert result == eventsources_list_response["data"] + assert len(result) == 2 + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_eventsource_success(er_client, eventsource_detail_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get(f"activity/eventsource/{EVENTSOURCE_ID}") + route.return_value = httpx.Response(httpx.codes.OK, json=eventsource_detail_response) + result = await er_client.get_eventsource(EVENTSOURCE_ID) + assert route.called + assert result == eventsource_detail_response["data"] + assert result["id"] == EVENTSOURCE_ID + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_eventsource_not_found(er_client, not_found_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get(f"activity/eventsource/{EVENTSOURCE_ID}") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json=not_found_response) + with pytest.raises(ERClientNotFound): + await er_client.get_eventsource(EVENTSOURCE_ID) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_eventsource_success(er_client, eventsource_patched_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.patch(f"activity/eventsource/{EVENTSOURCE_ID}") + route.return_value = httpx.Response(httpx.codes.OK, json=eventsource_patched_response) + result = await er_client.patch_eventsource( + EVENTSOURCE_ID, {"display": "Renamed Source"} + ) + assert route.called + assert result == eventsource_patched_response["data"] + assert result["display"] == "Renamed Source" + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_eventsource_not_found(er_client, not_found_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.patch(f"activity/eventsource/{EVENTSOURCE_ID}") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json=not_found_response) + with pytest.raises(ERClientNotFound): + await er_client.patch_eventsource(EVENTSOURCE_ID, {"display": "X"}) + assert route.called + await er_client.close() diff --git a/tests/sync_client/test_eventproviders.py b/tests/sync_client/test_eventproviders.py new file mode 100644 index 0000000..1ce3c4a --- /dev/null +++ b/tests/sync_client/test_eventproviders.py @@ -0,0 +1,306 @@ +import json +from unittest.mock import MagicMock + +import pytest + +from erclient.client import ERClient +from erclient import ERClientNotFound, ERClientPermissionDenied + + +EVENTPROVIDER_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" +EVENTSOURCE_ID = "f0e1d2c3-b4a5-6789-0fed-cba987654321" + + +@pytest.fixture +def eventprovider_payload(): + return { + "display": "My Test Provider", + "owner": {"id": "user-id-123"}, + } + + +@pytest.fixture +def eventprovider_created_response(): + return { + "data": { + "id": EVENTPROVIDER_ID, + "display": "My Test Provider", + "is_active": True, + "owner": {"id": "user-id-123"}, + }, + "status": {"code": 201, "message": "Created"}, + } + + +@pytest.fixture +def eventproviders_list_response(): + return { + "data": [ + { + "id": EVENTPROVIDER_ID, + "display": "My Test Provider", + "is_active": True, + }, + { + "id": "00000000-0000-0000-0000-000000000002", + "display": "Another Provider", + "is_active": False, + }, + ], + } + + +@pytest.fixture +def eventprovider_detail_response(): + return { + "data": { + "id": EVENTPROVIDER_ID, + "display": "My Test Provider", + "is_active": True, + }, + } + + +@pytest.fixture +def eventprovider_patched_response(): + return { + "data": { + "id": EVENTPROVIDER_ID, + "display": "Updated Provider Name", + "is_active": True, + }, + } + + +@pytest.fixture +def eventsource_payload(): + return { + "display": "Test Event Source", + } + + +@pytest.fixture +def eventsource_created_response(): + return { + "data": { + "id": EVENTSOURCE_ID, + "display": "Test Event Source", + "eventprovider": EVENTPROVIDER_ID, + }, + "status": {"code": 201, "message": "Created"}, + } + + +@pytest.fixture +def eventsources_list_response(): + return { + "data": [ + { + "id": EVENTSOURCE_ID, + "display": "Test Event Source", + "eventprovider": EVENTPROVIDER_ID, + }, + { + "id": "11111111-1111-1111-1111-111111111111", + "display": "Second Source", + "eventprovider": EVENTPROVIDER_ID, + }, + ], + } + + +@pytest.fixture +def eventsource_detail_response(): + return { + "data": { + "id": EVENTSOURCE_ID, + "display": "Test Event Source", + "eventprovider": EVENTPROVIDER_ID, + }, + } + + +@pytest.fixture +def eventsource_patched_response(): + return { + "data": { + "id": EVENTSOURCE_ID, + "display": "Renamed Source", + "eventprovider": EVENTPROVIDER_ID, + }, + } + + +def _mock_response(status_code=200, json_data=None): + """Helper to create a mock response object.""" + response = MagicMock() + response.ok = 200 <= status_code < 400 + response.status_code = status_code + response.text = json.dumps(json_data) if json_data else "" + response.json.return_value = json_data + response.url = "https://fake-site.erdomain.org/api/v1.0/test" + return response + + +# -- Sync Event Provider POST Tests -- + + +def test_post_eventprovider_success(er_client, eventprovider_payload, eventprovider_created_response): + er_client._http_session.post = MagicMock( + return_value=_mock_response(201, eventprovider_created_response) + ) + result = er_client.post_eventprovider(eventprovider_payload) + er_client._http_session.post.assert_called_once() + assert result == eventprovider_created_response["data"] + + +def test_post_eventprovider_forbidden(er_client, eventprovider_payload): + er_client._http_session.post = MagicMock( + return_value=_mock_response(403, { + "status": {"detail": "You do not have permission to perform this action."} + }) + ) + with pytest.raises(ERClientPermissionDenied): + er_client.post_eventprovider(eventprovider_payload) + + +# -- Sync Event Provider GET List Tests -- + + +def test_get_eventproviders_success(er_client, eventproviders_list_response): + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, eventproviders_list_response) + ) + result = er_client.get_eventproviders() + er_client._http_session.get.assert_called_once() + assert result == eventproviders_list_response["data"] + assert len(result) == 2 + + +# -- Sync Event Provider GET Detail Tests -- + + +def test_get_eventprovider_success(er_client, eventprovider_detail_response): + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, eventprovider_detail_response) + ) + result = er_client.get_eventprovider(EVENTPROVIDER_ID) + er_client._http_session.get.assert_called_once() + assert result == eventprovider_detail_response["data"] + + +def test_get_eventprovider_not_found(er_client): + er_client._http_session.get = MagicMock( + return_value=_mock_response(404, { + "status": {"code": 404, "detail": "Not found"} + }) + ) + with pytest.raises(ERClientNotFound): + er_client.get_eventprovider(EVENTPROVIDER_ID) + + +# -- Sync Event Provider PATCH Tests -- + + +def test_patch_eventprovider_success(er_client, eventprovider_patched_response): + er_client._http_session.patch = MagicMock( + return_value=_mock_response(200, eventprovider_patched_response) + ) + result = er_client.patch_eventprovider( + EVENTPROVIDER_ID, {"display": "Updated Provider Name"} + ) + er_client._http_session.patch.assert_called_once() + assert result == eventprovider_patched_response["data"] + assert result["display"] == "Updated Provider Name" + + +def test_patch_eventprovider_not_found(er_client): + er_client._http_session.patch = MagicMock( + return_value=_mock_response(404, { + "status": {"code": 404, "detail": "Not found"} + }) + ) + with pytest.raises(ERClientNotFound): + er_client.patch_eventprovider(EVENTPROVIDER_ID, {"display": "X"}) + + +# -- Sync Event Source POST Tests -- + + +def test_post_eventsource_success(er_client, eventsource_payload, eventsource_created_response): + er_client._http_session.post = MagicMock( + return_value=_mock_response(201, eventsource_created_response) + ) + result = er_client.post_eventsource(EVENTPROVIDER_ID, eventsource_payload) + er_client._http_session.post.assert_called_once() + assert result == eventsource_created_response["data"] + + +def test_post_eventsource_not_found(er_client, eventsource_payload): + er_client._http_session.post = MagicMock( + return_value=_mock_response(404, { + "status": {"code": 404, "detail": "Provider not found"} + }) + ) + with pytest.raises(ERClientNotFound): + er_client.post_eventsource(EVENTPROVIDER_ID, eventsource_payload) + + +# -- Sync Event Source GET List Tests -- + + +def test_get_eventsources_success(er_client, eventsources_list_response): + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, eventsources_list_response) + ) + result = er_client.get_eventsources(EVENTPROVIDER_ID) + er_client._http_session.get.assert_called_once() + assert result == eventsources_list_response["data"] + assert len(result) == 2 + + +# -- Sync Event Source GET Detail Tests -- + + +def test_get_eventsource_success(er_client, eventsource_detail_response): + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, eventsource_detail_response) + ) + result = er_client.get_eventsource(EVENTSOURCE_ID) + er_client._http_session.get.assert_called_once() + assert result == eventsource_detail_response["data"] + + +def test_get_eventsource_not_found(er_client): + er_client._http_session.get = MagicMock( + return_value=_mock_response(404, { + "status": {"code": 404, "detail": "Not found"} + }) + ) + with pytest.raises(ERClientNotFound): + er_client.get_eventsource(EVENTSOURCE_ID) + + +# -- Sync Event Source PATCH Tests -- + + +def test_patch_eventsource_success(er_client, eventsource_patched_response): + er_client._http_session.patch = MagicMock( + return_value=_mock_response(200, eventsource_patched_response) + ) + result = er_client.patch_eventsource( + EVENTSOURCE_ID, {"display": "Renamed Source"} + ) + er_client._http_session.patch.assert_called_once() + assert result == eventsource_patched_response["data"] + assert result["display"] == "Renamed Source" + + +def test_patch_eventsource_not_found(er_client): + er_client._http_session.patch = MagicMock( + return_value=_mock_response(404, { + "status": {"code": 404, "detail": "Not found"} + }) + ) + with pytest.raises(ERClientNotFound): + er_client.patch_eventsource(EVENTSOURCE_ID, {"display": "X"})