From 94e9138a1b33edd771c1a533ce37cdbc8216a455 Mon Sep 17 00:00:00 2001 From: "Gabriel de Paula Felix (gdpf)" Date: Wed, 15 Nov 2023 12:51:10 -0300 Subject: [PATCH] [FIX] microsoft_calendar: outdated sync token error fetching events Before this commit, when Odoo faced an internal error and lost the current synchronization token with Microsoft, after fetching events from Graph API an `410 Gone` was being thrown carrying the `SyncStateNotFound` code. Since we didn't handle this error code, we were experiencing a traceback after the request: ``` 2023-11-15 09:16:15,046 4 ERROR report-one-16-0-staging-1-10315840 odoo.addons.microsoft_account.models.microsoft_service: Bad microsoft request : b'{"error":{"code":"SyncStateNotFound","message":"The sync state generation is not found; generation=25;[highest=28][28][26][27]."}}' ! Traceback (most recent call last): File "/home/odoo/src/odoo/addons/microsoft_account/models/microsoft_service.py", line 154, in _do_request res.raise_for_status() File "/usr/lib/python3/dist-packages/requests/models.py", line 943, in raise_for_status raise HTTPError(http_error_msg, response=self) requests.exceptions.HTTPError: 410 Client Error: Gone for url: https://graph.microsoft.com/v1.0/me/calendarView/delta?%24deltatoken=MOCK_TOKEN_HERE 2023-11-15 09:16:15,051 4 ERROR report-one-16-0-staging-1-10315840 odoo.http: Exception during request handling. Traceback (most recent call last): File "/home/odoo/src/odoo/odoo/http.py", line 2003, in __call__ response = request._serve_db() File "/home/odoo/src/odoo/odoo/http.py", line 1589, in _serve_db return service_model.retrying(self._serve_ir_http, self.env) File "/home/odoo/src/odoo/odoo/service/model.py", line 133, in retrying result = func() File "/home/odoo/src/odoo/odoo/http.py", line 1616, in _serve_ir_http response = self.dispatcher.dispatch(rule.endpoint, args) File "/home/odoo/src/odoo/odoo/http.py", line 1820, in dispatch result = self.request.registry['ir.http']._dispatch(endpoint) File "/home/odoo/src/odoo/addons/website/models/ir_http.py", line 237, in _dispatch response = super()._dispatch(endpoint) File "/home/odoo/src/odoo/odoo/addons/base/models/ir_http.py", line 154, in _dispatch result = endpoint(**request.params) File "/home/odoo/src/odoo/odoo/http.py", line 697, in route_wrapper result = endpoint(self, *args, **params_ok) File "/home/odoo/src/odoo/addons/microsoft_calendar/controllers/main.py", line 55, in sync_data need_refresh = request.env.user.sudo().with_context(sync_context)._sync_microsoft_calendar() File "/home/odoo/src/odoo/addons/microsoft_calendar/models/res_users.py", line 99, in _sync_microsoft_calendar events, next_sync_token = calendar_service.get_events(self.microsoft_calendar_sync_token, token=token) File "/home/odoo/src/odoo/addons/microsoft_calendar/utils/microsoft_calendar.py", line 20, in wrapped return func(self, *args, **kwargs) File "/home/odoo/src/odoo/addons/microsoft_calendar/utils/microsoft_calendar.py", line 152, in get_events events, next_sync_token = self._get_events_delta(sync_token=sync_token, token=token, timeout=timeout) File "/home/odoo/src/odoo/addons/microsoft_calendar/utils/microsoft_calendar.py", line 20, in wrapped return func(self, *args, **kwargs) File "/home/odoo/src/odoo/addons/microsoft_calendar/utils/microsoft_calendar.py", line 111, in _get_events_delta raise e File "/home/odoo/src/odoo/addons/microsoft_calendar/utils/microsoft_calendar.py", line 105, in _get_events_delta events, next_sync_token = self._get_events_from_paginated_url( File "/home/odoo/src/odoo/addons/microsoft_calendar/utils/microsoft_calendar.py", line 20, in wrapped return func(self, *args, **kwargs) File "/home/odoo/src/odoo/addons/microsoft_calendar/utils/microsoft_calendar.py", line 76, in _get_events_from_paginated_url _, data, _ = self.microsoft_service._do_request( File "/home/odoo/src/odoo/addons/microsoft_account/models/microsoft_service.py", line 173, in _do_request raise error File "/home/odoo/src/odoo/addons/microsoft_account/models/microsoft_service.py", line 154, in _do_request res.raise_for_status() File "/usr/lib/python3/dist-packages/requests/models.py", line 943, in raise_for_status raise HTTPError(http_error_msg, response=self) requests.exceptions.HTTPError: 410 Client Error: Gone for url: https://graph.microsoft.com/v1.0/me/calendarView/delta?%24deltatoken=MOCK_TOKEN_HERE ``` After this commit, everytime we receive the `SyncStateNotFound` code from Microsoft, we will trigger the full synchronization with Graph API. This way, the outdated token will be replaced with a brand new token. A function which checks if the full sync is required was added in order to be mocked by the new unit test, since it is necessary mocking its return value for testing if a given `HTTPError` with the 'SyncStateNotFound' error code will call the all events fetching. closes odoo/odoo#142982 Task-id: 3597211 X-original-commit: 54ae2f2b469e3bd5294386b882e57d7519dc044a Signed-off-by: Arnaud Joset (arj) --- .../tests/test_microsoft_service.py | 26 ++++++++++++++++++- .../utils/microsoft_calendar.py | 9 ++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/addons/microsoft_calendar/tests/test_microsoft_service.py b/addons/microsoft_calendar/tests/test_microsoft_service.py index 471b954f8db..2bcfe022f5c 100644 --- a/addons/microsoft_calendar/tests/test_microsoft_service.py +++ b/addons/microsoft_calendar/tests/test_microsoft_service.py @@ -62,8 +62,9 @@ class TestMicrosoftService(TransactionCase): with self.assertRaises(Exception): self.service._get_events_delta(token=self.fake_token, timeout=DEFAULT_TIMEOUT) + @patch.object(MicrosoftCalendarService, "_check_full_sync_required") @patch.object(MicrosoftService, "_do_request") - def test_get_events_delta_token_error(self, mock_do_request): + def test_get_events_delta_token_error(self, mock_do_request, mock_check_full_sync_required): """ When the provided sync token is invalid, an exception should be raised and then a full sync should be done. @@ -72,6 +73,7 @@ class TestMicrosoftService(TransactionCase): requests.HTTPError(response=MagicMock(status_code=410, content="fullSyncRequired")), self._do_request_result({"value": []}), ] + mock_check_full_sync_required.return_value = (True) events, next_token = self.service._get_events_delta( token=self.fake_token, sync_token=self.fake_sync_token, timeout=DEFAULT_TIMEOUT @@ -437,3 +439,25 @@ class TestMicrosoftService(TransactionCase): json.dumps(values), self.header, method="POST", timeout=DEFAULT_TIMEOUT ) + + @patch.object(MicrosoftCalendarService, "_check_full_sync_required") + @patch.object(MicrosoftService, "_do_request") + def test_get_events_delta_with_outdated_sync_token(self, mock_do_request, mock_check_full_sync_required): + """ When an outdated sync token is provided, we must fetch all events again for updating the old token. """ + # Throw a 'HTTPError' when the token is outdated, thus triggering the fetching of all events. + # Simulate a scenario which the full sync is required, such as when getting the 'SyncStateNotFound' error code. + mock_do_request.side_effect = [ + requests.HTTPError(response=MagicMock(status_code=410, error={'code': "SyncStateNotFound"})), + self._do_request_result({"value": []}), + ] + mock_check_full_sync_required.return_value = (True) + + # Call the regular 'delta' get events with an outdated token for triggering the all events fetching. + self.env.user.microsoft_calendar_sync_token = self.fake_sync_token + self.service._get_events_delta(token=self.fake_token, sync_token=self.fake_sync_token, timeout=DEFAULT_TIMEOUT) + + # Two calls must have been made: one call with the outdated sync token and another one with no sync token. + mock_do_request.assert_has_calls([ + self.call_with_sync_token, + self.call_without_sync_token + ]) diff --git a/addons/microsoft_calendar/utils/microsoft_calendar.py b/addons/microsoft_calendar/utils/microsoft_calendar.py index 8882b0592a8..62cb416b09a 100644 --- a/addons/microsoft_calendar/utils/microsoft_calendar.py +++ b/addons/microsoft_calendar/utils/microsoft_calendar.py @@ -92,6 +92,12 @@ class MicrosoftCalendarService(): return events, next_sync_token + def _check_full_sync_required(self, response): + """ Checks if full sync is required according to the error code received. """ + response_json = response.json() + response_code = response_json.get('error', {}).get('code', '') + return any(error_code in response_code for error_code in ['fullSyncRequired', 'SyncStateNotFound']) + @requires_auth_token def _get_events_delta(self, sync_token=None, token=None, timeout=TIMEOUT): """ @@ -105,7 +111,8 @@ class MicrosoftCalendarService(): events, next_sync_token = self._get_events_from_paginated_url( url, params=params, token=token, timeout=timeout) except requests.HTTPError as e: - if e.response.status_code == 410 and 'fullSyncRequired' in str(e.response.content) and sync_token: + full_sync_needed = self._check_full_sync_required(e.response) + if e.response.status_code == 410 and full_sync_needed and sync_token: # retry with a full sync return self._get_events_delta(token=token, timeout=timeout) raise e