Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions etesync_dav/radicale/etesync_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ def etesync_for_user(self, user):

return etesync, True

def refresh_etebase_token(self, user):
etesync, _ = self.etesync_for_user(user)
if not isinstance(etesync, Etebase):
return False

etesync.etebase.fetch_token()
stored_session = etesync.etebase.save(None)
remote_url = self.creds.get_server_url(user)
self.creds.set_etebase(user, stored_session, remote_url)
self.creds.save()
etesync.stored_session = stored_session
return True


_etesync_cache = EteSyncCache(
creds_path=config.CREDS_FILE,
Expand All @@ -93,3 +106,8 @@ def etesync_for_user(user):
ret = _etesync_cache.etesync_for_user(user)

yield ret


def refresh_etebase_token(user):
with _get_etesync_lock:
return _etesync_cache.refresh_etebase_token(user)
21 changes: 15 additions & 6 deletions etesync_dav/radicale/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import etesync as api
import vobject
from etebase import etebase_python
from radicale import pathutils
from radicale.item import Item, get_etag
from radicale.storage import (
Expand All @@ -31,7 +32,7 @@
)

from ..local_cache import COL_TYPES, Etebase
from .etesync_cache import etesync_for_user
from .etesync_cache import etesync_for_user, refresh_etebase_token
from .href_mapper import HrefMapper
from .storage_etebase_collection import Collection as EtebaseCollection

Expand Down Expand Up @@ -74,14 +75,22 @@ def wait_for_sync(self, timeout=None):
raise e
return ret

def sync_once(self):
with etesync_for_user(self.user) as (etesync, _):
self.last_sync = time.time()
self._done_syncing.clear()

try:
etesync.sync()
except etebase_python.Error as e:
if str(e) != "Invalid token." or not refresh_etebase_token(self.user):
raise
etesync.sync()

def run(self):
while True:
try:
with etesync_for_user(self.user) as (etesync, _):
self.last_sync = time.time()
self._done_syncing.clear()

etesync.sync()
self.sync_once()
except Exception as e:
# Print errors but keep on syncing in the background
logger.exception(e)
Expand Down
69 changes: 69 additions & 0 deletions tests/test_token_refresh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from contextlib import contextmanager
from unittest import TestCase, mock

from etebase import etebase_python

from etesync_dav.local_cache import Etebase
from etesync_dav.radicale.etesync_cache import EteSyncCache
from etesync_dav.radicale.storage import SyncThread


class TokenRefreshTest(TestCase):
def test_refreshes_and_persists_etebase_session(self):
cache = EteSyncCache("unused-creds", "unused-db")
cache.creds = mock.Mock()
cache.creds.get_server_url.return_value = "https://example.com/"

etesync = Etebase.__new__(Etebase)
etesync.etebase = mock.Mock()
etesync.stored_session = "old-stored-session"
etesync.etebase.save.return_value = "new-stored-session"
cache.etesync_for_user = mock.Mock(return_value=(etesync, False))

self.assertTrue(cache.refresh_etebase_token("alice"))

etesync.etebase.fetch_token.assert_called_once_with()
etesync.etebase.save.assert_called_once_with(None)
cache.creds.set_etebase.assert_called_once_with("alice", "new-stored-session", "https://example.com/")
cache.creds.save.assert_called_once_with()
self.assertEqual(etesync.stored_session, "new-stored-session")

def test_does_not_refresh_legacy_session(self):
cache = EteSyncCache("unused-creds", "unused-db")
cache.etesync_for_user = mock.Mock(return_value=(object(), False))

self.assertFalse(cache.refresh_etebase_token("alice"))

@mock.patch("etesync_dav.radicale.storage.refresh_etebase_token", return_value=True)
@mock.patch("etesync_dav.radicale.storage.etesync_for_user")
def test_sync_retries_after_refreshing_invalid_token(self, etesync_for_user_mock, refresh_mock):
etesync = mock.Mock()
etesync.sync.side_effect = [etebase_python.Error("Invalid token."), None]

@contextmanager
def context():
yield etesync, False

etesync_for_user_mock.return_value = context()

SyncThread("alice").sync_once()

refresh_mock.assert_called_once_with("alice")
self.assertEqual(etesync.sync.call_count, 2)

@mock.patch("etesync_dav.radicale.storage.refresh_etebase_token")
@mock.patch("etesync_dav.radicale.storage.etesync_for_user")
def test_sync_does_not_refresh_other_etebase_errors(self, etesync_for_user_mock, refresh_mock):
etesync = mock.Mock()
etesync.sync.side_effect = etebase_python.Error("Invalid stoken.")

@contextmanager
def context():
yield etesync, False

etesync_for_user_mock.return_value = context()

with self.assertRaisesRegex(etebase_python.Error, "Invalid stoken"):
SyncThread("alice").sync_once()

refresh_mock.assert_not_called()