[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) <arj@odoo.com>
This commit is contained in:
Gabriel de Paula Felix (gdpf)
2023-11-22 08:55:46 +00:00
parent 12b921f4c9
commit 94e9138a1b
2 changed files with 33 additions and 2 deletions
@@ -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
])
@@ -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