diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py
index 9ace4bf..1a09c8f 100644
--- a/radicale/tests/test_base.py
+++ b/radicale/tests/test_base.py
@@ -24,6 +24,7 @@ import os
import posixpath
import shutil
import tempfile
+import xml.etree.ElementTree as ET
import pytest
from radicale import Application, config
@@ -763,6 +764,177 @@ class BaseRequestsMixIn:
assert status == 207
assert "href>%s<" % event_path in answer
+ def _report_sync_token(self, calendar_path, sync_token=None):
+ sync_token_xml = (
+ "" % sync_token
+ if sync_token else "")
+ status, headers, answer = self.request(
+ "REPORT", calendar_path,
+ """
+
+
+
+
+ %s
+ """ % sync_token_xml)
+ if sync_token and status == 412:
+ return None, None
+ assert status == 207
+ xml = ET.fromstring(answer)
+ sync_token = xml.find("{DAV:}sync-token").text.strip()
+ assert sync_token
+ return sync_token, xml
+
+ def test_report_sync_collection_no_change(self):
+ """Test sync-collection report without modifying the collection"""
+ calendar_path = "/calendar.ics/"
+ self.request("MKCALENDAR", calendar_path)
+ event = get_file_content("event1.ics")
+ event_path = posixpath.join(calendar_path, "event.ics")
+ self.request("PUT", event_path, event)
+ sync_token, xml = self._report_sync_token(calendar_path)
+ assert xml.find("{DAV:}response") is not None
+ new_sync_token, xml = self._report_sync_token(calendar_path,
+ sync_token)
+ assert sync_token == new_sync_token
+ assert xml.find("{DAV:}response") is None
+
+ def test_report_sync_collection_add(self):
+ """Test sync-collection report with an added item"""
+ calendar_path = "/calendar.ics/"
+ self.request("MKCALENDAR", calendar_path)
+ sync_token, xml = self._report_sync_token(calendar_path)
+ event = get_file_content("event1.ics")
+ event_path = posixpath.join(calendar_path, "event.ics")
+ self.request("PUT", event_path, event)
+ sync_token, xml = self._report_sync_token(calendar_path, sync_token)
+ if not sync_token:
+ pytest.skip("storage backend does not support sync-token")
+ assert xml.find("{DAV:}response") is not None
+ assert xml.find("{DAV:}response/{DAV:}status") is None
+
+ def test_report_sync_collection_delete(self):
+ """Test sync-collection report with a deleted item"""
+ calendar_path = "/calendar.ics/"
+ self.request("MKCALENDAR", calendar_path)
+ event = get_file_content("event1.ics")
+ event_path = posixpath.join(calendar_path, "event.ics")
+ self.request("PUT", event_path, event)
+ sync_token, xml = self._report_sync_token(calendar_path)
+ self.request("DELETE", event_path)
+ sync_token, xml = self._report_sync_token(calendar_path, sync_token)
+ if not sync_token:
+ pytest.skip("storage backend does not support sync-token")
+ assert "404" in xml.find("{DAV:}response/{DAV:}status").text
+
+ def test_report_sync_collection_create_delete(self):
+ """Test sync-collection report with a created and deleted item"""
+ calendar_path = "/calendar.ics/"
+ self.request("MKCALENDAR", calendar_path)
+ sync_token, xml = self._report_sync_token(calendar_path)
+ event = get_file_content("event1.ics")
+ event_path = posixpath.join(calendar_path, "event.ics")
+ self.request("PUT", event_path, event)
+ self.request("DELETE", event_path)
+ sync_token, xml = self._report_sync_token(calendar_path, sync_token)
+ if not sync_token:
+ pytest.skip("storage backend does not support sync-token")
+ assert "404" in xml.find("{DAV:}response/{DAV:}status").text
+
+ def test_report_sync_collection_modify_undo(self):
+ """Test sync-collection report with a modified and changed back item"""
+ calendar_path = "/calendar.ics/"
+ self.request("MKCALENDAR", calendar_path)
+ event1 = get_file_content("event1.ics")
+ event2 = get_file_content("event2.ics")
+ event_path = posixpath.join(calendar_path, "event1.ics")
+ self.request("PUT", event_path, event1)
+ sync_token, xml = self._report_sync_token(calendar_path)
+ self.request("PUT", event_path, event2)
+ self.request("PUT", event_path, event1)
+ sync_token, xml = self._report_sync_token(calendar_path, sync_token)
+ if not sync_token:
+ pytest.skip("storage backend does not support sync-token")
+ assert xml.find("{DAV:}response") is not None
+ assert xml.find("{DAV:}response/{DAV:}status") is None
+
+ def test_report_sync_collection_move(self):
+ """Test sync-collection report a moved item"""
+ calendar_path = "/calendar.ics/"
+ self.request("MKCALENDAR", calendar_path)
+ event = get_file_content("event1.ics")
+ event1_path = posixpath.join(calendar_path, "event1.ics")
+ event2_path = posixpath.join(calendar_path, "event2.ics")
+ self.request("PUT", event1_path, event)
+ sync_token, xml = self._report_sync_token(calendar_path)
+ status, headers, answer = self.request(
+ "MOVE", event1_path, HTTP_DESTINATION=event2_path, HTTP_HOST="")
+ sync_token, xml = self._report_sync_token(calendar_path, sync_token)
+ if not sync_token:
+ pytest.skip("storage backend does not support sync-token")
+ for response in xml.findall("{DAV:}response"):
+ if response.find("{DAV:}status") is None:
+ assert response.find("{DAV:}href").text == event2_path
+ else:
+ assert "404" in response.find("{DAV:}status").text
+ assert response.find("{DAV:}href").text == event1_path
+
+ def test_report_sync_collection_move_undo(self):
+ """Test sync-collection report with a moved and moved back item"""
+ calendar_path = "/calendar.ics/"
+ self.request("MKCALENDAR", calendar_path)
+ event = get_file_content("event1.ics")
+ event1_path = posixpath.join(calendar_path, "event1.ics")
+ event2_path = posixpath.join(calendar_path, "event2.ics")
+ self.request("PUT", event1_path, event)
+ sync_token, xml = self._report_sync_token(calendar_path)
+ status, headers, answer = self.request(
+ "MOVE", event1_path, HTTP_DESTINATION=event2_path, HTTP_HOST="")
+ status, headers, answer = self.request(
+ "MOVE", event2_path, HTTP_DESTINATION=event1_path, HTTP_HOST="")
+ sync_token, xml = self._report_sync_token(calendar_path, sync_token)
+ if not sync_token:
+ pytest.skip("storage backend does not support sync-token")
+ created = deleted = 0
+ for response in xml.findall("{DAV:}response"):
+ if response.find("{DAV:}status") is None:
+ assert response.find("{DAV:}href").text == event1_path
+ created += 1
+ else:
+ assert "404" in response.find("{DAV:}status").text
+ assert response.find("{DAV:}href").text == event2_path
+ deleted += 1
+ assert created == 1 and deleted == 1
+
+ def test_report_sync_collection_invalid_sync_token(self):
+ """Test sync-collection report with an invalid sync token"""
+ calendar_path = "/calendar.ics/"
+ self.request("MKCALENDAR", calendar_path)
+ sync_token, xml = self._report_sync_token(
+ calendar_path, "http://radicale.org/ns/sync/INVALID")
+ assert not sync_token
+
+ def test_propfind_sync_token(self):
+ """Retrieve the sync-token with a propfind request"""
+ calendar_path = "/calendar.ics/"
+ self.request("MKCALENDAR", calendar_path)
+ sync_token, xml = self._report_sync_token(calendar_path)
+ event = get_file_content("event1.ics")
+ event_path = posixpath.join(calendar_path, "event.ics")
+ self.request("PUT", event_path, event)
+ new_sync_token, xml = self._report_sync_token(calendar_path,
+ sync_token)
+ assert sync_token != new_sync_token
+
+ def test_propfind_same_as_sync_collection_sync_token(self):
+ """Compare sync-token property with sync-collection sync-token"""
+ calendar_path = "/calendar.ics/"
+ self.request("MKCALENDAR", calendar_path)
+ sync_token, xml = self._report_sync_token(calendar_path)
+ new_sync_token, xml = self._report_sync_token(calendar_path,
+ sync_token)
+ assert sync_token == new_sync_token
+
def test_authorization(self):
authorization = "Basic " + base64.b64encode(b"user:").decode()
status, headers, answer = self.request(