From 486981fff6827f373d4bb18cf431d6e003698dee Mon Sep 17 00:00:00 2001 From: Elliot <3186037+elliot-100@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:46:10 +0100 Subject: [PATCH 1/2] refactor: extract common code in `Spond.get_event()` and `Spond.get_group()` to non-public `Spond._get_entity()' --- spond/spond.py | 67 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/spond/spond.py b/spond/spond.py index 8b96b59..ad8c4b0 100644 --- a/spond/spond.py +++ b/spond/spond.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, ClassVar, Optional from .base import _SpondBase @@ -14,6 +14,9 @@ class Spond(_SpondBase): DT_FORMAT = "%Y-%m-%dT00:00:00.000Z" + _EVENT: ClassVar = "event" + _GROUP: ClassVar = "group" + def __init__(self, username: str, password: str) -> None: super().__init__(username, password, "https://api.spond.com/core/v1/") self.chat_url = None @@ -44,7 +47,6 @@ async def get_groups(self) -> list[dict]: self.groups = await r.json() return self.groups - @_SpondBase.require_authentication async def get_group(self, uid: str) -> dict: """ Get a group by unique ID. @@ -62,15 +64,9 @@ async def get_group(self, uid: str) -> dict: Raises ------ KeyError if no group is matched. - """ - if not self.groups: - await self.get_groups() - for group in self.groups: - if group["id"] == uid: - return group - errmsg = f"No group with id='{uid}'." - raise KeyError(errmsg) + """ + return await self._get_entity(self._GROUP, uid) @_SpondBase.require_authentication async def get_person(self, user: str) -> dict: @@ -286,7 +282,6 @@ async def get_events( self.events = await r.json() return self.events - @_SpondBase.require_authentication async def get_event(self, uid: str) -> dict: """ Get an event by unique ID. @@ -306,13 +301,7 @@ async def get_event(self, uid: str) -> dict: KeyError if no event is matched. """ - if not self.events: - await self.get_events() - for event in self.events: - if event["id"] == uid: - return event - errmsg = f"No event with id='{uid}'." - raise KeyError(errmsg) + return await self._get_entity(self._EVENT, uid) @_SpondBase.require_authentication async def update_event(self, uid: str, updates: dict): @@ -434,3 +423,45 @@ async def change_response(self, uid: str, user: str, payload: dict) -> dict: url, headers=self.auth_headers, json=payload ) as r: return await r.json() + + @_SpondBase.require_authentication + async def _get_entity(self, entity_type: str, uid: str) -> dict: + """ + Get an event or group by unique ID. + + Subject to authenticated user's access. + + Parameters + ---------- + entity_type : str + self._EVENT or self._GROUP. + uid : str + UID of the entity. + + Returns + ------- + Details of the entity. + + Raises + ------ + KeyError if no entity is matched. + NotImplementedError if no/unsupported entity type is specified. + + """ + if entity_type == self._EVENT: + if not self.events: + await self.get_events() + entities = self.events + elif entity_type == self._GROUP: + if not self.groups: + await self.get_groups() + entities = self.groups + else: + err_msg = f"Entity type '{entity_type}' is not supported." + raise NotImplementedError(err_msg) + + for entity in entities: + if entity["id"] == uid: + return entity + errmsg = f"No {entity_type} with id='{uid}'." + raise KeyError(errmsg) From 2e72a259e9c40b5483fb80b9d233713ce09b7445 Mon Sep 17 00:00:00 2001 From: Elliot <3186037+elliot-100@users.noreply.github.com> Date: Tue, 25 Jun 2024 16:52:16 +0100 Subject: [PATCH 2/2] tests: refactor into Event, Group, Export classes for clarity --- tests/test_spond.py | 327 ++++++++++++++++++++++---------------------- 1 file changed, 163 insertions(+), 164 deletions(-) diff --git a/tests/test_spond.py b/tests/test_spond.py index 80feb8a..3a38a6f 100644 --- a/tests/test_spond.py +++ b/tests/test_spond.py @@ -23,36 +23,6 @@ async def wrapper(*args, **kwargs): _SpondBase.require_authentication = mock_require_authentication(Spond.get_event) -@pytest.fixture -def mock_events(): - """Mock a minimal list of events.""" - return [ - { - "id": "ID1", - "name": "Event One", - }, - { - "id": "ID2", - "name": "Event Two", - }, - ] - - -@pytest.fixture -def mock_groups(): - """Mock a minimal list of groups.""" - return [ - { - "id": "ID1", - "name": "Group One", - }, - { - "id": "ID2", - "name": "Group Two", - }, - ] - - @pytest.fixture def mock_token(): return MOCK_TOKEN @@ -63,137 +33,166 @@ def mock_payload(): return MOCK_PAYLOAD -@pytest.mark.asyncio -async def test_get_event__happy_path(mock_events, mock_token): - """Test that a valid `id` returns the matching event.""" - - s = Spond(MOCK_USERNAME, MOCK_PASSWORD) - s.events = mock_events - s.token = mock_token - g = await s.get_event("ID1") - - assert g == { - "id": "ID1", - "name": "Event One", - } - - -@pytest.mark.asyncio -async def test_get_event__no_match_raises_exception(mock_events, mock_token): - """Test that a non-matched `id` raises KeyError.""" - - s = Spond(MOCK_USERNAME, MOCK_PASSWORD) - s.events = mock_events - s.token = mock_token - - with pytest.raises(KeyError): - await s.get_event("ID3") - - -@pytest.mark.asyncio -async def test_get_event__blank_id_match_raises_exception(mock_events, mock_token): - """Test that a blank `id` raises KeyError.""" - - s = Spond(MOCK_USERNAME, MOCK_PASSWORD) - s.events = mock_events - s.token = mock_token - - with pytest.raises(KeyError): - await s.get_event("") - - -@pytest.mark.asyncio -async def test_get_group__happy_path(mock_groups, mock_token): - """Test that a valid `id` returns the matching group.""" - - s = Spond(MOCK_USERNAME, MOCK_PASSWORD) - s.groups = mock_groups - s.token = mock_token - g = await s.get_group("ID2") - - assert g == { - "id": "ID2", - "name": "Group Two", - } - - -@pytest.mark.asyncio -async def test_get_group__no_match_raises_exception(mock_groups, mock_token): - """Test that a non-matched `id` raises KeyError.""" - - s = Spond(MOCK_USERNAME, MOCK_PASSWORD) - s.groups = mock_groups - s.token = mock_token - - with pytest.raises(KeyError): - await s.get_group("ID3") - - -@pytest.mark.asyncio -async def test_get_group__blank_id_raises_exception(mock_groups, mock_token): - """Test that a blank `id` raises KeyError.""" - - s = Spond(MOCK_USERNAME, MOCK_PASSWORD) - s.groups = mock_groups - s.token = mock_token - - with pytest.raises(KeyError): - await s.get_group("") - - -@pytest.mark.asyncio -@patch("aiohttp.ClientSession.get") -async def test_get_export(mock_get, mock_token): - s = Spond(MOCK_USERNAME, MOCK_PASSWORD) - s.token = mock_token - - mock_binary = b"\x68\x65\x6c\x6c\x6f\x77\x6f\x72\x6c\x64" # helloworld - mock_get.return_value.__aenter__.return_value.status = 200 - mock_get.return_value.__aenter__.return_value.read = AsyncMock( - return_value=mock_binary - ) - - data = await s.get_event_attendance_xlsx(uid="ID1") - - mock_url = "https://api.spond.com/core/v1/sponds/ID1/export" - mock_get.assert_called_once_with( - mock_url, - headers={ - "content-type": "application/json", - "Authorization": f"Bearer {mock_token}", - }, - ) - assert data == mock_binary - - -@pytest.mark.asyncio -@patch("aiohttp.ClientSession.put") -async def test_change_response(mock_put, mock_payload, mock_token): - s = Spond(MOCK_USERNAME, MOCK_PASSWORD) - s.token = mock_token - - mock_response_data = { - "acceptedIds": ["PID1", "PID2"], - "declinedIds": ["PID3"], - "unansweredIds": [], - "waitinglistIds": [], - "unconfirmedIds": [], - "declineMessages": {"PID3": "sick cannot make it"}, - } - mock_put.return_value.__aenter__.return_value.status = 200 - mock_put.return_value.__aenter__.return_value.json = AsyncMock( - return_value=mock_response_data - ) - - response = await s.change_response(uid="ID1", user="PID3", payload=mock_payload) - - mock_url = "https://api.spond.com/core/v1/sponds/ID1/responses/PID3" - mock_put.assert_called_once_with( - mock_url, - headers={ - "content-type": "application/json", - "Authorization": f"Bearer {mock_token}", - }, - json=mock_payload, - ) - assert response == mock_response_data +class TestEventMethods: + + @pytest.fixture + def mock_events(self): + """Mock a minimal list of events.""" + return [ + { + "id": "ID1", + "name": "Event One", + }, + { + "id": "ID2", + "name": "Event Two", + }, + ] + + @pytest.mark.asyncio + async def test_get_event__happy_path(self, mock_events, mock_token): + """Test that a valid `id` returns the matching event.""" + + s = Spond(MOCK_USERNAME, MOCK_PASSWORD) + s.events = mock_events + s.token = mock_token + g = await s.get_event("ID1") + + assert g == { + "id": "ID1", + "name": "Event One", + } + + @pytest.mark.asyncio + async def test_get_event__no_match_raises_exception(self, mock_events, mock_token): + """Test that a non-matched `id` raises KeyError.""" + + s = Spond(MOCK_USERNAME, MOCK_PASSWORD) + s.events = mock_events + s.token = mock_token + + with pytest.raises(KeyError): + await s.get_event("ID3") + + @pytest.mark.asyncio + async def test_get_event__blank_id_match_raises_exception( + self, mock_events, mock_token + ): + """Test that a blank `id` raises KeyError.""" + + s = Spond(MOCK_USERNAME, MOCK_PASSWORD) + s.events = mock_events + s.token = mock_token + + with pytest.raises(KeyError): + await s.get_event("") + + @pytest.mark.asyncio + @patch("aiohttp.ClientSession.put") + async def test_change_response(self, mock_put, mock_payload, mock_token): + s = Spond(MOCK_USERNAME, MOCK_PASSWORD) + s.token = mock_token + + mock_response_data = { + "acceptedIds": ["PID1", "PID2"], + "declinedIds": ["PID3"], + "unansweredIds": [], + "waitinglistIds": [], + "unconfirmedIds": [], + "declineMessages": {"PID3": "sick cannot make it"}, + } + mock_put.return_value.__aenter__.return_value.status = 200 + mock_put.return_value.__aenter__.return_value.json = AsyncMock( + return_value=mock_response_data + ) + + response = await s.change_response(uid="ID1", user="PID3", payload=mock_payload) + + mock_url = "https://api.spond.com/core/v1/sponds/ID1/responses/PID3" + mock_put.assert_called_once_with( + mock_url, + headers={ + "content-type": "application/json", + "Authorization": f"Bearer {mock_token}", + }, + json=mock_payload, + ) + assert response == mock_response_data + + +class TestGroupMethods: + @pytest.fixture + def mock_groups(self): + """Mock a minimal list of groups.""" + return [ + { + "id": "ID1", + "name": "Group One", + }, + { + "id": "ID2", + "name": "Group Two", + }, + ] + + @pytest.mark.asyncio + async def test_get_group__happy_path(self, mock_groups, mock_token): + """Test that a valid `id` returns the matching group.""" + + s = Spond(MOCK_USERNAME, MOCK_PASSWORD) + s.groups = mock_groups + s.token = mock_token + g = await s.get_group("ID2") + + assert g == { + "id": "ID2", + "name": "Group Two", + } + + @pytest.mark.asyncio + async def test_get_group__no_match_raises_exception(self, mock_groups, mock_token): + """Test that a non-matched `id` raises KeyError.""" + + s = Spond(MOCK_USERNAME, MOCK_PASSWORD) + s.groups = mock_groups + s.token = mock_token + + with pytest.raises(KeyError): + await s.get_group("ID3") + + @pytest.mark.asyncio + async def test_get_group__blank_id_raises_exception(self, mock_groups, mock_token): + """Test that a blank `id` raises KeyError.""" + + s = Spond(MOCK_USERNAME, MOCK_PASSWORD) + s.groups = mock_groups + s.token = mock_token + + with pytest.raises(KeyError): + await s.get_group("") + + +class TestExportMethod: + @pytest.mark.asyncio + @patch("aiohttp.ClientSession.get") + async def test_get_export(self, mock_get, mock_token): + s = Spond(MOCK_USERNAME, MOCK_PASSWORD) + s.token = mock_token + + mock_binary = b"\x68\x65\x6c\x6c\x6f\x77\x6f\x72\x6c\x64" # helloworld + mock_get.return_value.__aenter__.return_value.status = 200 + mock_get.return_value.__aenter__.return_value.read = AsyncMock( + return_value=mock_binary + ) + + data = await s.get_event_attendance_xlsx(uid="ID1") + + mock_url = "https://api.spond.com/core/v1/sponds/ID1/export" + mock_get.assert_called_once_with( + mock_url, + headers={ + "content-type": "application/json", + "Authorization": f"Bearer {mock_token}", + }, + ) + assert data == mock_binary