44These tests verify the behavior of the fake sync token implementation
55used when servers don't support sync-collection REPORT.
66"""
7+ from unittest .mock import MagicMock
8+ from unittest .mock import Mock
9+ from unittest .mock import patch
710
811import pytest
9- from unittest . mock import Mock , MagicMock , patch
10- from caldav .collection import Calendar , SynchronizableCalendarObjectCollection
11- from caldav .lib . url import URL
12+
13+ from caldav .collection import Calendar
14+ from caldav .collection import SynchronizableCalendarObjectCollection
1215from caldav .elements import dav
16+ from caldav .lib .url import URL
1317
1418
1519class TestSyncTokenFallback :
@@ -22,8 +26,7 @@ def setup_method(self):
2226 self .mock_client .features .is_supported = Mock (return_value = {})
2327
2428 self .calendar = Calendar (
25- client = self .mock_client ,
26- url = URL ("http://example.com/calendar/" )
29+ client = self .mock_client , url = URL ("http://example.com/calendar/" )
2730 )
2831
2932 def create_mock_object (self , url_str : str , etag : str = None , data : str = None ):
@@ -110,10 +113,11 @@ def test_generate_fake_sync_token_cannot_detect_changes_without_etags(self) -> N
110113
111114 # BUG: Tokens will be the same because we're using URLs as fallback
112115 # and URLs don't change when content changes
113- assert token_before == token_after , \
114- "This test documents a KNOWN BUG: without ETags, modifications aren't detected"
116+ assert (
117+ token_before == token_after
118+ ), "This test documents a KNOWN BUG: without ETags, modifications aren't detected"
115119
116- @patch .object (Calendar , ' search' )
120+ @patch .object (Calendar , " search" )
117121 def test_fallback_returns_empty_when_nothing_changed (self , mock_search ) -> None :
118122 """Test that fallback returns empty list when sync token matches."""
119123 # Setup: search returns same objects with ETags
@@ -125,7 +129,9 @@ def test_fallback_returns_empty_when_nothing_changed(self, mock_search) -> None:
125129 self .mock_client .features .is_supported .return_value = {"support" : "unsupported" }
126130
127131 # First call: get initial state
128- result1 = self .calendar .objects_by_sync_token (sync_token = None , load_objects = False )
132+ result1 = self .calendar .objects_by_sync_token (
133+ sync_token = None , load_objects = False
134+ )
129135 initial_token = result1 .sync_token
130136
131137 # Second call: with same token, should return empty
@@ -136,7 +142,7 @@ def test_fallback_returns_empty_when_nothing_changed(self, mock_search) -> None:
136142 assert len (list (result2 )) == 0 , "Should return empty when nothing changed"
137143 assert result2 .sync_token == initial_token
138144
139- @patch .object (Calendar , ' search' )
145+ @patch .object (Calendar , " search" )
140146 def test_fallback_returns_all_when_etag_changed (self , mock_search ) -> None :
141147 """Test that fallback returns all objects when ETags change."""
142148 # First call: return objects with initial ETags
@@ -146,11 +152,15 @@ def test_fallback_returns_all_when_etag_changed(self, mock_search) -> None:
146152
147153 self .mock_client .features .is_supported .return_value = {"support" : "unsupported" }
148154
149- result1 = self .calendar .objects_by_sync_token (sync_token = None , load_objects = False )
155+ result1 = self .calendar .objects_by_sync_token (
156+ sync_token = None , load_objects = False
157+ )
150158 initial_token = result1 .sync_token
151159
152160 # Simulate modification: search now returns objects with changed ETags
153- obj1_modified = self .create_mock_object ("http://example.com/1.ics" , etag = "etag-1-new" )
161+ obj1_modified = self .create_mock_object (
162+ "http://example.com/1.ics" , etag = "etag-1-new"
163+ )
154164 obj2_same = self .create_mock_object ("http://example.com/2.ics" , etag = "etag-2" )
155165 mock_search .return_value = [obj1_modified , obj2_same ]
156166
@@ -159,13 +169,19 @@ def test_fallback_returns_all_when_etag_changed(self, mock_search) -> None:
159169 sync_token = initial_token , load_objects = False
160170 )
161171
162- assert len (list (result2 )) == 2 , "Should return all objects when changes detected"
172+ assert (
173+ len (list (result2 )) == 2
174+ ), "Should return all objects when changes detected"
163175 assert result2 .sync_token != initial_token
164176
165- @pytest .mark .xfail (reason = "Mock objects don't preserve props updates properly - integration test needed" )
166- @patch .object (Calendar , '_query_properties' )
167- @patch .object (Calendar , 'search' )
168- def test_fallback_fetches_etags_when_missing (self , mock_search , mock_query_props ) -> None :
177+ @pytest .mark .xfail (
178+ reason = "Mock objects don't preserve props updates properly - integration test needed"
179+ )
180+ @patch .object (Calendar , "_query_properties" )
181+ @patch .object (Calendar , "search" )
182+ def test_fallback_fetches_etags_when_missing (
183+ self , mock_search , mock_query_props
184+ ) -> None :
169185 """
170186 Test that fallback fetches ETags when search() doesn't return them.
171187
@@ -176,25 +192,33 @@ def test_fallback_fetches_etags_when_missing(self, mock_search, mock_query_props
176192 props updates properly. The actual functionality works in integration tests.
177193 """
178194 # First call: return objects without ETags
179- obj1 = self .create_mock_object ("http://example.com/calendar/1.ics" , data = "DATA1" )
180- obj2 = self .create_mock_object ("http://example.com/calendar/2.ics" , data = "DATA2" )
195+ obj1 = self .create_mock_object (
196+ "http://example.com/calendar/1.ics" , data = "DATA1"
197+ )
198+ obj2 = self .create_mock_object (
199+ "http://example.com/calendar/2.ics" , data = "DATA2"
200+ )
181201 mock_search .return_value = [obj1 , obj2 ]
182202
183203 # Mock PROPFIND response with ETags
184204 mock_response = Mock ()
185205 mock_response .expand_simple_props .return_value = {
186206 "http://example.com/calendar/1.ics" : {dav .GetEtag .tag : "etag-1" },
187- "http://example.com/calendar/2.ics" : {dav .GetEtag .tag : "etag-2" }
207+ "http://example.com/calendar/2.ics" : {dav .GetEtag .tag : "etag-2" },
188208 }
189209 mock_query_props .return_value = mock_response
190210
191211 self .mock_client .features .is_supported .return_value = {"support" : "unsupported" }
192212
193- result1 = self .calendar .objects_by_sync_token (sync_token = None , load_objects = False )
213+ result1 = self .calendar .objects_by_sync_token (
214+ sync_token = None , load_objects = False
215+ )
194216 initial_token = result1 .sync_token
195217
196218 # Verify PROPFIND was called to fetch ETags
197- assert mock_query_props .call_count >= 1 , "PROPFIND should be called to fetch ETags"
219+ assert (
220+ mock_query_props .call_count >= 1
221+ ), "PROPFIND should be called to fetch ETags"
198222
199223 # Check that ETags were actually added to the first batch of objects
200224 # (This verifies the ETag fetching mechanism worked)
@@ -207,14 +231,16 @@ def test_fallback_fetches_etags_when_missing(self, mock_search, mock_query_props
207231 obj1_modified = self .create_mock_object (
208232 "http://example.com/calendar/1.ics" , data = "MODIFIED_DATA1"
209233 )
210- obj2_same = self .create_mock_object ("http://example.com/calendar/2.ics" , data = "DATA2" )
234+ obj2_same = self .create_mock_object (
235+ "http://example.com/calendar/2.ics" , data = "DATA2"
236+ )
211237 mock_search .return_value = [obj1_modified , obj2_same ]
212238
213239 # Mock PROPFIND to return different ETag for modified object
214240 mock_response2 = Mock ()
215241 mock_response2 .expand_simple_props .return_value = {
216242 "http://example.com/calendar/1.ics" : {dav .GetEtag .tag : "etag-1-new" },
217- "http://example.com/calendar/2.ics" : {dav .GetEtag .tag : "etag-2" }
243+ "http://example.com/calendar/2.ics" : {dav .GetEtag .tag : "etag-2" },
218244 }
219245 mock_query_props .return_value = mock_response2
220246
@@ -225,15 +251,19 @@ def test_fallback_fetches_etags_when_missing(self, mock_search, mock_query_props
225251
226252 # Debug: check if ETags were added to second batch
227253 if obj1_modified .props :
228- print (f"DEBUG: obj1_modified props after second call: { obj1_modified .props } " )
254+ print (
255+ f"DEBUG: obj1_modified props after second call: { obj1_modified .props } "
256+ )
229257 if obj2_same .props :
230258 print (f"DEBUG: obj2_same props after second call: { obj2_same .props } " )
231259
232260 # Should return all objects because change was detected
233- assert len (list (result2 )) == 2 , \
234- "Should detect modification via ETags and return all objects"
235- assert result2 .sync_token != initial_token , \
236- "Token should change when ETag changes"
261+ assert (
262+ len (list (result2 )) == 2
263+ ), "Should detect modification via ETags and return all objects"
264+ assert (
265+ result2 .sync_token != initial_token
266+ ), "Token should change when ETag changes"
237267
238268
239269if __name__ == "__main__" :
0 commit comments