Repository: allenporter/gcal_sync (language: python)

Issue:
Add sync token error handling and testing 
Currently the invalid sync token error is never thrown, and so we need to check statuses, etc and verify sync is reset via tests.


Discussion / hints:

Fix patch:
diff --git a/gcal_sync/auth.py b/gcal_sync/auth.py
index 430f88d..0263ca2 100644
--- a/gcal_sync/auth.py
+++ b/gcal_sync/auth.py
@@ -9,11 +9,12 @@
 import aiohttp
 from aiohttp.client_exceptions import ClientError, ClientResponseError
 
-from .exceptions import ApiException, AuthException
+from .exceptions import ApiException, AuthException, InvalidSyncTokenException
 
 _LOGGER = logging.getLogger(__name__)
 
 HTTP_UNAUTHORIZED = 401
+HTTP_GONE = 410
 AUTHORIZATION_HEADER = "Authorization"
 ERROR = "error"
 STATUS = "status"
@@ -103,6 +104,10 @@ async def _raise_for_status(resp: aiohttp.ClientResponse) -> aiohttp.ClientRespo
         except ClientResponseError as err:
             if err.status == HTTP_UNAUTHORIZED:
                 raise AuthException(f"Unable to authenticate with API: {err}") from err
+            if err.status == HTTP_GONE:
+                raise InvalidSyncTokenException(
+                    "Sync token invalidated by server"
+                ) from err
             detail.append(err.message)
             raise ApiException(": ".join(detail)) from err
         except ClientError as err:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 4c5d40f..b39cd82 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -160,6 +160,17 @@ def _put_result(data: dict[str, Any]) -> None:
     return _put_result
 
 
+@pytest.fixture(name="request_reset")
+def mock_request_reset(app: aiohttp.web.Application) -> Callable[[], None]:
+    """Reset the request/response fixtures."""
+
+    def _reset() -> None:
+        app["request"].clear()
+        app["response"].clear()
+
+    return _reset
+
+
 @pytest.fixture(name="url_request")
 def mock_url_request(app: aiohttp.web.Application) -> Callable[[], list[str]]:
     """Fixture to return the requested url."""
diff --git a/tests/test_event_sync.py b/tests/test_event_sync.py
index d866d06..f8a5670 100644
--- a/tests/test_event_sync.py
+++ b/tests/test_event_sync.py
@@ -354,3 +354,110 @@ async def test_sync_datetime_pages(
         "&fields=kind,nextPageToken,nextSyncToken,items(id,summary,description,location,start"
         ",end,transparency,timeZone)&syncToken=sync-token-1",
     ]
+
+
+@freeze_time("2022-04-05 07:31:02", tz_offset=-7)
+async def test_invalidated_sync_token(
+    event_sync_manager_cb: Callable[[], Awaitable[EventSyncManager]],
+    json_response: ApiResult,
+    response: ResponseResult,
+    url_request: Callable[[], str],
+    request_reset: Callable[[], str],
+) -> None:
+    """Test lookup events API."""
+
+    json_response(
+        {
+            "items": [
+                {
+                    "id": "some-event-id-1",
+                    "summary": "Event 1",
+                    "description": "Event description 1",
+                    "start": {
+                        "date": "2022-04-13",
+                    },
+                    "end": {
+                        "date": "2022-04-14",
+                    },
+                },
+                {
+                    "id": "some-event-id-2",
+                    "summary": "Event 2",
+                    "description": "Event description 2",
+                    "start": {
+                        "date": "2022-04-15",
+                    },
+                    "end": {
+                        "date": "2022-04-20",
+                    },
+                },
+            ],
+            "nextSyncToken": "sync-token-1",
+        }
+    )
+
+    sync = await event_sync_manager_cb()
+    await sync.run()
+    assert url_request() == [
+        "/calendars/some-calendar-id/events?maxResult=100&singleEvents=true&orderBy=startTime"
+        "&fields=kind,nextPageToken,nextSyncToken,items(id,summary,description,location,start"
+        ",end,transparency,timeZone)&timeMin=2022-03-08T00:31:02"
+    ]
+
+    result = await sync.async_lookup_events(LookupEventsRequest())
+    assert result.events == [
+        Event(
+            id="some-event-id-1",
+            summary="Event 1",
+            description="Event description 1",
+            start=DateOrDatetime(date=datetime.date(2022, 4, 13)),
+            end=DateOrDatetime(date=datetime.date(2022, 4, 14)),
+        ),
+        Event(
+            id="some-event-id-2",
+            summary="Event 2",
+            description="Event description 2",
+            start=DateOrDatetime(date=datetime.date(2022, 4, 15)),
+            end=DateOrDatetime(date=datetime.date(2022, 4, 20)),
+        ),
+    ]
+
+    request_reset()
+    response(aiohttp.web.Response(status=410))  # Token invalid
+    json_response(
+        {
+            "items": [
+                {
+                    "id": "some-event-id-3",
+                    "summary": "Event 3",
+                    "description": "Event description 3",
+                    "start": {
+                        "date": "2022-04-12",
+                    },
+                    "end": {
+                        "date": "2022-04-13",
+                    },
+                },
+            ],
+            "nextSyncToken": "sync-token-2",
+        }
+    )
+    await sync.run()
+    assert url_request() == [
+        "/calendars/some-calendar-id/events?maxResult=100&singleEvents=true&orderBy=startTime"
+        "&fields=kind,nextPageToken,nextSyncToken,items(id,summary,description,location,start"
+        ",end,transparency,timeZone)&syncToken=sync-token-1",
+        "/calendars/some-calendar-id/events?maxResult=100&singleEvents=true&orderBy=startTime"
+        "&fields=kind,nextPageToken,nextSyncToken,items(id,summary,description,location,start"
+        ",end,transparency,timeZone)&timeMin=2022-03-08T00:31:02",
+    ]
+    result = await sync.async_lookup_events(LookupEventsRequest())
+    assert result.events == [
+        Event(
+            id="some-event-id-3",
+            summary="Event 3",
+            description="Event description 3",
+            start=DateOrDatetime(date=datetime.date(2022, 4, 12)),
+            end=DateOrDatetime(date=datetime.date(2022, 4, 13)),
+        ),
+    ]