|
159 | 159 | END:VTODO |
160 | 160 | END:VCALENDAR""" |
161 | 161 |
|
| 162 | +## Mock response with 2 pending and 2 completed todos, for testing include_completed behavior |
| 163 | +## https://github.com/python-caldav/caldav/issues/650 |
| 164 | +mixed_todos_response = """<d:multistatus xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav"> |
| 165 | + <d:response> |
| 166 | + <d:href>/calendar/pending1.ics</d:href> |
| 167 | + <d:propstat> |
| 168 | + <d:prop> |
| 169 | + <cal:calendar-data>BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VTODO\r\nUID:pending1\r\nSUMMARY:Pending 1\r\nSTATUS:NEEDS-ACTION\r\nDTSTAMP:20250101T000000Z\r\nEND:VTODO\r\nEND:VCALENDAR\r\n</cal:calendar-data> |
| 170 | + </d:prop> |
| 171 | + <d:status>HTTP/1.1 200 OK</d:status> |
| 172 | + </d:propstat> |
| 173 | + </d:response> |
| 174 | + <d:response> |
| 175 | + <d:href>/calendar/pending2.ics</d:href> |
| 176 | + <d:propstat> |
| 177 | + <d:prop> |
| 178 | + <cal:calendar-data>BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VTODO\r\nUID:pending2\r\nSUMMARY:Pending 2\r\nDTSTAMP:20250101T000000Z\r\nEND:VTODO\r\nEND:VCALENDAR\r\n</cal:calendar-data> |
| 179 | + </d:prop> |
| 180 | + <d:status>HTTP/1.1 200 OK</d:status> |
| 181 | + </d:propstat> |
| 182 | + </d:response> |
| 183 | + <d:response> |
| 184 | + <d:href>/calendar/completed1.ics</d:href> |
| 185 | + <d:propstat> |
| 186 | + <d:prop> |
| 187 | + <cal:calendar-data>BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VTODO\r\nUID:completed1\r\nSUMMARY:Completed 1\r\nSTATUS:COMPLETED\r\nDTSTAMP:20250101T000000Z\r\nCOMPLETED:20250101T120000Z\r\nEND:VTODO\r\nEND:VCALENDAR\r\n</cal:calendar-data> |
| 188 | + </d:prop> |
| 189 | + <d:status>HTTP/1.1 200 OK</d:status> |
| 190 | + </d:propstat> |
| 191 | + </d:response> |
| 192 | + <d:response> |
| 193 | + <d:href>/calendar/completed2.ics</d:href> |
| 194 | + <d:propstat> |
| 195 | + <d:prop> |
| 196 | + <cal:calendar-data>BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VTODO\r\nUID:completed2\r\nSUMMARY:Completed 2\r\nSTATUS:COMPLETED\r\nDTSTAMP:20250101T000000Z\r\nCOMPLETED:20250101T120000Z\r\nEND:VTODO\r\nEND:VCALENDAR\r\n</cal:calendar-data> |
| 197 | + </d:prop> |
| 198 | + <d:status>HTTP/1.1 200 OK</d:status> |
| 199 | + </d:propstat> |
| 200 | + </d:response> |
| 201 | +</d:multistatus>""" |
| 202 | + |
162 | 203 | ## from https://github.com/python-caldav/caldav/issues/495 |
163 | 204 | recurring_task_response = """<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> |
164 | 205 | <d:response> |
@@ -342,11 +383,54 @@ def testSearchForRecurringTask(self): |
342 | 383 | expand=True, |
343 | 384 | start=datetime(2025, 1, 1), |
344 | 385 | end=datetime(2025, 6, 5), |
345 | | - ## TODO - TEMP workaround for compatibility issues! post_filter should not be needed! |
346 | | - post_filter=True, |
347 | 386 | ) |
348 | 387 | assert len(mytasks) == 9 |
349 | 388 |
|
| 389 | + def testSearcherReuseConsistency_Issue650(self): |
| 390 | + """ |
| 391 | + Regression test for https://github.com/python-caldav/caldav/issues/650 |
| 392 | +
|
| 393 | + A CalDAVSearcher with todo=True and default include_completed (None) should |
| 394 | + return consistent results across multiple search() calls. Previously, the |
| 395 | + icalendar_searcher library would mutate include_completed from None to False |
| 396 | + during the first search(), changing which code path subsequent calls took. |
| 397 | + """ |
| 398 | + client = MockedDAVClient(mixed_todos_response) |
| 399 | + calendar = Calendar(client, url="/calendar/issue650/") |
| 400 | + |
| 401 | + ## Test 1: searcher with include_completed=None (default) should give consistent results |
| 402 | + searcher = calendar.searcher(todo=True) |
| 403 | + assert searcher.include_completed is None, "include_completed should start as None" |
| 404 | + |
| 405 | + first_result = searcher.search() |
| 406 | + assert len(first_result) == 2, f"Expected 2 pending todos, got {len(first_result)}" |
| 407 | + |
| 408 | + ## After calling search(), include_completed must not have been mutated |
| 409 | + assert searcher.include_completed is None, ( |
| 410 | + "include_completed was mutated from None during search() - " |
| 411 | + "this breaks reuse of the searcher object (issue #650)" |
| 412 | + ) |
| 413 | + |
| 414 | + second_result = searcher.search() |
| 415 | + assert len(second_result) == 2, ( |
| 416 | + f"Second search() call returned {len(second_result)} results, " |
| 417 | + f"expected 2 - inconsistent behavior after searcher reuse (issue #650)" |
| 418 | + ) |
| 419 | + |
| 420 | + ## Test 2: explicit include_completed=False should also give correct results |
| 421 | + searcher_false = calendar.searcher(todo=True, include_completed=False) |
| 422 | + result_false = searcher_false.search() |
| 423 | + assert len(result_false) == 2, ( |
| 424 | + f"include_completed=False returned {len(result_false)} results, expected 2" |
| 425 | + ) |
| 426 | + |
| 427 | + ## Test 3: include_completed=True should return all todos |
| 428 | + searcher_true = calendar.searcher(todo=True, include_completed=True) |
| 429 | + result_true = searcher_true.search() |
| 430 | + assert len(result_true) == 4, ( |
| 431 | + f"include_completed=True returned {len(result_true)} results, expected 4" |
| 432 | + ) |
| 433 | + |
350 | 434 | def testLoadByMultiGet404(self): |
351 | 435 | xml = """ |
352 | 436 | <D:multistatus xmlns:D="DAV:"> |
|
0 commit comments