test_expand.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. # This file is part of Radicale - CalDAV and CardDAV server
  2. # Copyright © 2012-2017 Guillaume Ayoub
  3. # Copyright © 2017-2019 Unrud <unrud@outlook.com>
  4. # Copyright © 2024 Pieter Hijma <pieterhijma@users.noreply.github.com>
  5. # Copyright © 2025 David Greaves <david@dgreaves.com>
  6. # Copyright © 2025 Peter Bieringer <pb@bieringer.de>
  7. #
  8. # This library is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This library is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  20. """
  21. Radicale tests with expand requests.
  22. """
  23. import os
  24. from typing import ClassVar, List, Optional
  25. from xml.etree import ElementTree
  26. from radicale.log import logger
  27. from radicale.tests import BaseTest
  28. from radicale.tests.helpers import get_file_content
  29. ONLY_DATES = True
  30. CONTAINS_TIMES = False
  31. class TestExpandRequests(BaseTest):
  32. """Tests with expand requests."""
  33. # Allow skipping sync-token tests, when not fully supported by the backend
  34. full_sync_token_support: ClassVar[bool] = True
  35. def setup_method(self) -> None:
  36. BaseTest.setup_method(self)
  37. rights_file_path = os.path.join(self.colpath, "rights")
  38. with open(rights_file_path, "w") as f:
  39. f.write("""\
  40. [permit delete collection]
  41. user: .*
  42. collection: test-permit-delete
  43. permissions: RrWwD
  44. [forbid delete collection]
  45. user: .*
  46. collection: test-forbid-delete
  47. permissions: RrWwd
  48. [permit overwrite collection]
  49. user: .*
  50. collection: test-permit-overwrite
  51. permissions: RrWwO
  52. [forbid overwrite collection]
  53. user: .*
  54. collection: test-forbid-overwrite
  55. permissions: RrWwo
  56. [allow all]
  57. user: .*
  58. collection: .*
  59. permissions: RrWw""")
  60. self.configure({"rights": {"file": rights_file_path,
  61. "type": "from_file"}})
  62. def _req_without_expand(self,
  63. expected_uid: str,
  64. start: str,
  65. end: str,
  66. ) -> str:
  67. self.put("/calendar.ics/", get_file_content(f"{expected_uid}.ics"))
  68. return \
  69. f"""<?xml version="1.0" encoding="utf-8" ?>
  70. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  71. <D:prop>
  72. <C:calendar-data>
  73. </C:calendar-data>
  74. </D:prop>
  75. <C:filter>
  76. <C:comp-filter name="VCALENDAR">
  77. <C:comp-filter name="VEVENT">
  78. <C:time-range start="{start}" end="{end}"/>
  79. </C:comp-filter>
  80. </C:comp-filter>
  81. </C:filter>
  82. </C:calendar-query>
  83. """
  84. def _req_with_expand(self,
  85. expected_uid: str,
  86. start: str,
  87. end: str,
  88. ) -> str:
  89. self.put("/calendar.ics/", get_file_content(f"{expected_uid}.ics"))
  90. return \
  91. f"""<?xml version="1.0" encoding="utf-8" ?>
  92. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  93. <D:prop>
  94. <C:calendar-data>
  95. <C:expand start="{start}" end="{end}"/>
  96. </C:calendar-data>
  97. </D:prop>
  98. <C:filter>
  99. <C:comp-filter name="VCALENDAR">
  100. <C:comp-filter name="VEVENT">
  101. <C:time-range start="{start}" end="{end}"/>
  102. </C:comp-filter>
  103. </C:comp-filter>
  104. </C:filter>
  105. </C:calendar-query>
  106. """
  107. def _test_expand(self,
  108. expected_uid: str,
  109. start: str,
  110. end: str,
  111. expected_recurrence_ids: List[str],
  112. expected_start_times: List[str],
  113. expected_end_times: List[str],
  114. only_dates: bool,
  115. nr_uids: int) -> None:
  116. _, responses = self.report("/calendar.ics/",
  117. self._req_without_expand(expected_uid, start, end))
  118. assert len(responses) == 1
  119. response_without_expand = responses[f'/calendar.ics/{expected_uid}.ics']
  120. assert isinstance(response_without_expand, dict)
  121. status, element = response_without_expand["C:calendar-data"]
  122. assert status == 200 and element.text
  123. assert "RRULE" in element.text
  124. if not only_dates:
  125. assert "BEGIN:VTIMEZONE" in element.text
  126. if nr_uids == 1:
  127. assert "RECURRENCE-ID" not in element.text
  128. uids: List[str] = []
  129. for line in element.text.split("\n"):
  130. if line.startswith("UID:"):
  131. uid = line[len("UID:"):]
  132. assert uid == expected_uid
  133. uids.append(uid)
  134. assert len(uids) == nr_uids
  135. _, responses = self.report("/calendar.ics/",
  136. self._req_with_expand(expected_uid, start, end))
  137. assert len(responses) == 1
  138. response_with_expand = responses[f'/calendar.ics/{expected_uid}.ics']
  139. assert isinstance(response_with_expand, dict)
  140. status, element = response_with_expand["C:calendar-data"]
  141. logger.debug("lbt: element is %s",
  142. ElementTree.tostring(element, encoding='unicode'))
  143. assert status == 200 and element.text
  144. assert "RRULE" not in element.text
  145. assert "BEGIN:VTIMEZONE" not in element.text
  146. uids = []
  147. recurrence_ids = []
  148. for line in element.text.split("\n"):
  149. if line.startswith("UID:"):
  150. assert line == f"UID:{expected_uid}"
  151. uids.append(line)
  152. if line.startswith("RECURRENCE-ID:"):
  153. assert line in expected_recurrence_ids
  154. recurrence_ids.append(line)
  155. if line.startswith("DTSTART:"):
  156. assert line in expected_start_times
  157. if line.startswith("DTEND:"):
  158. assert line in expected_end_times
  159. assert len(uids) == len(expected_recurrence_ids)
  160. assert len(set(recurrence_ids)) == len(expected_recurrence_ids)
  161. def _test_expand_max(self,
  162. expected_uid: str,
  163. start: str,
  164. end: str,
  165. check: Optional[int] = None) -> None:
  166. _, responses = self.report("/calendar.ics/",
  167. self._req_without_expand(expected_uid, start, end))
  168. assert len(responses) == 1
  169. response_without_expand = responses[f'/calendar.ics/{expected_uid}.ics']
  170. assert isinstance(response_without_expand, dict)
  171. status, element = response_without_expand["C:calendar-data"]
  172. assert status == 200 and element.text
  173. assert "RRULE" in element.text
  174. status, _, _ = self.request(
  175. "REPORT", "/calendar.ics/",
  176. self._req_with_expand(expected_uid, start, end),
  177. check=check)
  178. assert status == 400
  179. def test_report_with_expand_property(self) -> None:
  180. """Test report with expand property"""
  181. self._test_expand(
  182. "event_daily_rrule",
  183. "20060103T000000Z",
  184. "20060105T000000Z",
  185. ["RECURRENCE-ID:20060103T170000Z", "RECURRENCE-ID:20060104T170000Z"],
  186. ["DTSTART:20060103T170000Z", "DTSTART:20060104T170000Z"],
  187. [],
  188. CONTAINS_TIMES,
  189. 1
  190. )
  191. def test_report_with_expand_property_start_inside(self) -> None:
  192. """Test report with expand property start inside"""
  193. self._test_expand(
  194. "event_daily_rrule",
  195. "20060103T171500Z",
  196. "20060105T000000Z",
  197. ["RECURRENCE-ID:20060103T170000Z", "RECURRENCE-ID:20060104T170000Z"],
  198. ["DTSTART:20060103T170000Z", "DTSTART:20060104T170000Z"],
  199. [],
  200. CONTAINS_TIMES,
  201. 1
  202. )
  203. def test_report_with_expand_property_just_inside(self) -> None:
  204. """Test report with expand property start and end inside event"""
  205. self._test_expand(
  206. "event_daily_rrule",
  207. "20060103T171500Z",
  208. "20060103T171501Z",
  209. ["RECURRENCE-ID:20060103T170000Z"],
  210. ["DTSTART:20060103T170000Z"],
  211. [],
  212. CONTAINS_TIMES,
  213. 1
  214. )
  215. def test_report_with_expand_property_issue1812(self) -> None:
  216. """Test report with expand property for issue 1812"""
  217. self._test_expand(
  218. "event_issue1812",
  219. "20250127T183000Z",
  220. "20250127T183001Z",
  221. ["RECURRENCE-ID:20250127T180000Z"],
  222. ["DTSTART:20250127T180000Z"],
  223. ["DTEND:20250127T233000Z"],
  224. CONTAINS_TIMES,
  225. 11
  226. )
  227. def test_report_with_expand_property_issue1812_DS(self) -> None:
  228. """Test report with expand property for issue 1812 - DS active"""
  229. self._test_expand(
  230. "event_issue1812",
  231. "20250627T183000Z",
  232. "20250627T183001Z",
  233. ["RECURRENCE-ID:20250627T170000Z"],
  234. ["DTSTART:20250627T170000Z"],
  235. ["DTEND:20250627T223000Z"],
  236. CONTAINS_TIMES,
  237. 11
  238. )
  239. def test_report_with_expand_property_all_day_event(self) -> None:
  240. """Test report with expand property for all day events"""
  241. self._test_expand(
  242. "event_full_day_rrule",
  243. "20060103T000000Z",
  244. "20060105T000000Z",
  245. ["RECURRENCE-ID:20060103", "RECURRENCE-ID:20060104", "RECURRENCE-ID:20060105"],
  246. ["DTSTART:20060103", "DTSTART:20060104", "DTSTART:20060105"],
  247. ["DTEND:20060104", "DTEND:20060105", "DTEND:20060106"],
  248. ONLY_DATES,
  249. 1
  250. )
  251. def test_report_with_expand_property_overridden(self) -> None:
  252. """Test report with expand property with overridden events"""
  253. self._test_expand(
  254. "event_daily_rrule_overridden",
  255. "20060103T000000Z",
  256. "20060105T000000Z",
  257. ["RECURRENCE-ID:20060103T170000Z", "RECURRENCE-ID:20060104T170000Z"],
  258. ["DTSTART:20060103T170000Z", "DTSTART:20060104T190000Z"],
  259. [],
  260. CONTAINS_TIMES,
  261. 2
  262. )
  263. def test_report_with_expand_property_timezone(self):
  264. self._test_expand(
  265. "event_weekly_rrule",
  266. "20060320T000000Z",
  267. "20060414T000000Z",
  268. [
  269. "RECURRENCE-ID:20060321T200000Z",
  270. "RECURRENCE-ID:20060328T200000Z",
  271. "RECURRENCE-ID:20060404T190000Z",
  272. "RECURRENCE-ID:20060411T190000Z",
  273. ],
  274. [
  275. "DTSTART:20060321T200000Z",
  276. "DTSTART:20060328T200000Z",
  277. "DTSTART:20060404T190000Z",
  278. "DTSTART:20060411T190000Z",
  279. ],
  280. [],
  281. CONTAINS_TIMES,
  282. 1
  283. )
  284. def test_report_with_expand_property_max_occur(self) -> None:
  285. """Test report with expand property too many vevents"""
  286. self.configure({"reporting": {"max_freebusy_occurrence": 100}})
  287. self._test_expand_max(
  288. "event_daily_rrule_forever",
  289. "20060103T000000Z",
  290. "20060501T000000Z",
  291. check=400
  292. )
  293. def test_report_with_max_occur(self) -> None:
  294. """Test report with too many vevents"""
  295. self.configure({"reporting": {"max_freebusy_occurrence": 10}})
  296. uid = "event_multiple_too_many"
  297. start = "20130901T000000Z"
  298. end = "20130902T000000Z"
  299. check = 400
  300. status, responses = self.report("/calendar.ics/",
  301. self._req_without_expand(uid, start, end),
  302. check=check)
  303. assert len(responses) == 0
  304. assert status == check
  305. def test_report_vcalendar_all_components(self) -> None:
  306. """Test calendar-query with comp-filter VCALENDAR, returns all components."""
  307. self.mkcalendar("/test/")
  308. self.put("/test/calendar.ics", get_file_content("event_daily_rrule.ics"))
  309. self.put("/test/todo.ics", get_file_content("todo1.ics"))
  310. request = """
  311. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  312. <D:prop>
  313. <C:calendar-data/>
  314. </D:prop>
  315. <C:filter>
  316. <C:comp-filter name="VCALENDAR"/>
  317. </C:filter>
  318. </C:calendar-query>
  319. """
  320. status, responses = self.report("/test", request)
  321. assert status == 207
  322. assert len(responses) == 2
  323. assert "/test/calendar.ics" in responses
  324. assert "/test/todo.ics" in responses
  325. def test_report_vevent_only(self) -> None:
  326. """Test calendar-query with comp-filter VEVENT, returns only VEVENT."""
  327. self.mkcalendar("/test/")
  328. self.put("/test/calendar.ics", get_file_content("event_daily_rrule.ics"))
  329. self.put("/test/todo.ics", get_file_content("todo1.ics"))
  330. start = "20060101T000000Z"
  331. end = "20060104T000000Z"
  332. request = f"""
  333. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  334. <D:prop>
  335. <C:calendar-data>
  336. <C:expand start="{start}" end="{end}"/>
  337. </C:calendar-data>
  338. </D:prop>
  339. <C:filter>
  340. <C:comp-filter name="VCALENDAR">
  341. <C:comp-filter name="VEVENT">
  342. <C:time-range start="{start}" end="{end}"/>
  343. </C:comp-filter>
  344. </C:comp-filter>
  345. </C:filter>
  346. </C:calendar-query>
  347. """
  348. status, responses = self.report("/test", request)
  349. assert status == 207
  350. assert len(responses) == 1
  351. assert "/test/calendar.ics" in responses
  352. vevent_response = responses["/test/calendar.ics"]
  353. assert type(vevent_response) is dict
  354. status, element = vevent_response["C:calendar-data"]
  355. assert status == 200 and element.text
  356. assert "BEGIN:VEVENT" in element.text
  357. assert "UID:" in element.text
  358. assert "BEGIN:VTODO" not in element.text
  359. def test_report_time_range_no_vevent(self) -> None:
  360. """Test calendar-query with time-range filter, no matching VEVENT."""
  361. self.mkcalendar("/test/")
  362. self.put("/test/calendar.ics/", get_file_content("event_daily_rrule.ics"))
  363. request = """
  364. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  365. <D:prop>
  366. <C:calendar-data>
  367. <C:expand start="20000101T000000Z" end="20000105T000000Z"/>
  368. </C:calendar-data>
  369. </D:prop>
  370. <C:filter>
  371. <C:comp-filter name="VCALENDAR">
  372. <C:comp-filter name="VEVENT">
  373. <C:time-range start="20000101T000000Z" end="20000105T000000Z"/>
  374. </C:comp-filter>
  375. </C:comp-filter>
  376. </C:filter>
  377. </C:calendar-query>
  378. """
  379. status, responses = self.report("/test", request)
  380. assert status == 207
  381. assert len(responses) == 0
  382. def test_report_time_range_one_vevent(self) -> None:
  383. """Test calendar-query with time-range filter, matches one VEVENT."""
  384. self.mkcalendar("/test/")
  385. self.put("/test/calendar1.ics/", get_file_content("event_daily_rrule.ics"))
  386. self.put("/test/calendar2.ics/", get_file_content("event1.ics"))
  387. start = "20060101T000000Z"
  388. end = "20060104T000000Z"
  389. request = f"""
  390. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  391. <D:prop>
  392. <C:calendar-data>
  393. <C:expand start="{start}" end="{end}"/>
  394. </C:calendar-data>
  395. </D:prop>
  396. <C:filter>
  397. <C:comp-filter name="VCALENDAR">
  398. <C:comp-filter name="VEVENT">
  399. <C:time-range start="{start}" end="{end}"/>
  400. </C:comp-filter>
  401. </C:comp-filter>
  402. </C:filter>
  403. </C:calendar-query>
  404. """
  405. status, responses = self.report("/test", request)
  406. assert status == 207
  407. assert len(responses) == 1
  408. response = responses["/test/calendar1.ics"]
  409. assert type(response) is dict
  410. status, element = response["C:calendar-data"]
  411. assert status == 200 and element.text
  412. assert "BEGIN:VEVENT" in element.text
  413. assert "RECURRENCE-ID:20060103T170000Z" in element.text
  414. assert "DTSTART:20060103T170000Z" in element.text
  415. def test_expand_report_for_recurring_and_non_recurring_events(self) -> None:
  416. """Test calendar-query with time-range filter, matches one VEVENT."""
  417. self.mkcalendar("/test/")
  418. self.put("/test/event.ics/", get_file_content("event_issue1812_2.ics"))
  419. self.put("/test/event2.ics/", get_file_content("event_issue1812_3.ics"))
  420. request = """
  421. <c:calendar-query xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
  422. <d:prop>
  423. <d:getetag/>
  424. <c:calendar-data>
  425. <c:expand start="20250629T220000Z" end="20250803T220000Z"/>
  426. </c:calendar-data>
  427. </d:prop>
  428. <c:filter>
  429. <c:comp-filter name="VCALENDAR">
  430. <c:comp-filter name="VEVENT">
  431. <c:time-range start="20250629T220000Z" end="20250803T220000Z"/>
  432. </c:comp-filter>
  433. </c:comp-filter>
  434. </c:filter>
  435. </c:calendar-query>
  436. """
  437. status, responses = self.report("/test", request)
  438. assert status == 207
  439. assert len(responses) == 2
  440. assert isinstance(responses, dict)
  441. assert "/test/event.ics" in responses
  442. assert "/test/event2.ics" in responses
  443. assert isinstance(responses["/test/event.ics"], dict)
  444. assert isinstance(responses["/test/event2.ics"], dict)
  445. assert "C:calendar-data" in responses["/test/event.ics"]
  446. status, event1_calendar_data = responses["/test/event.ics"]["C:calendar-data"]
  447. assert event1_calendar_data.text
  448. assert "UID:a07cfa8b-0ce6-4956-800d-c0bfe1f0730a" in event1_calendar_data.text
  449. assert "RECURRENCE-ID:20250716" in event1_calendar_data.text
  450. assert "RECURRENCE-ID:20250723" in event1_calendar_data.text
  451. assert "RECURRENCE-ID:20250730" in event1_calendar_data.text
  452. assert "C:calendar-data" in responses["/test/event2.ics"]
  453. status, event2_calendar_data = responses["/test/event2.ics"]["C:calendar-data"]
  454. assert event2_calendar_data.text
  455. assert "UID:c6be8b2c-3d72-453c-b698-4f25cdf1569e" in event2_calendar_data.text
  456. def test_report_getetag_expand_filter(self) -> None:
  457. """Test getetag with time-range filter and expand (example from #1880)."""
  458. self.mkcalendar("/test/")
  459. self.put("/test/event_issue1880_1.ics", get_file_content("event_issue1880_1.ics"))
  460. self.put("/test/event_issue1880_2.ics", get_file_content("event_issue1880_2.ics"))
  461. request = """
  462. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  463. <D:prop>
  464. <D:getetag>
  465. <C:expand start="20250921T220000Z" end="20250928T220000Z"/>
  466. </D:getetag>
  467. </D:prop>
  468. <C:filter>
  469. <C:comp-filter name="VCALENDAR">
  470. <C:comp-filter name="VEVENT">
  471. <C:time-range start="20250921T220000Z" end="20250928T220000Z"/>
  472. </C:comp-filter>
  473. </C:comp-filter>
  474. </C:filter>
  475. </C:calendar-query>
  476. """
  477. status, responses = self.report("/test", request)
  478. assert status == 207
  479. assert len(responses) == 2
  480. assert isinstance(responses["/test/event_issue1880_1.ics"], dict)
  481. assert isinstance(responses["/test/event_issue1880_2.ics"], dict)
  482. assert "D:getetag" in responses["/test/event_issue1880_1.ics"]
  483. assert "D:getetag" in responses["/test/event_issue1880_2.ics"]
  484. def test_report_getetag_expand_filter_positive1(self) -> None:
  485. """Test getetag with time-range filter and expand (not applicable), should return as matching filter range (example from #1812)."""
  486. self.mkcalendar("/test/")
  487. self.put("/test/event_issue1812_getetag.ics", get_file_content("event_issue1812_getetag.ics"))
  488. request = """
  489. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  490. <D:prop>
  491. <D:getetag>
  492. <C:expand start="20250706T220000Z" end="20250713T220000Z" />
  493. </D:getetag>
  494. </D:prop>
  495. <C:filter>
  496. <C:comp-filter name="VCALENDAR">
  497. <C:comp-filter name="VEVENT">
  498. <C:time-range start="20250716T220000Z" end="20250717T220000Z" />
  499. </C:comp-filter>
  500. </C:comp-filter>
  501. </C:filter>
  502. </C:calendar-query>
  503. """
  504. status, responses = self.report("/test", request)
  505. assert status == 207
  506. assert len(responses) == 1
  507. assert isinstance(responses["/test/event_issue1812_getetag.ics"], dict)
  508. assert "D:getetag" in responses["/test/event_issue1812_getetag.ics"]
  509. def test_report_getetag_expand_filter_positive2(self) -> None:
  510. """Test getetag with time-range filter and expand, should return as matching filter range (example from #1812)."""
  511. self.mkcalendar("/test/")
  512. self.put("/test/event_issue1812.ics", get_file_content("event_issue1812.ics"))
  513. request = """
  514. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  515. <D:prop>
  516. <D:getetag>
  517. <C:expand start="20250706T220000Z" end="20250730T220000Z" />
  518. </D:getetag>
  519. </D:prop>
  520. <C:filter>
  521. <C:comp-filter name="VCALENDAR">
  522. <C:comp-filter name="VEVENT">
  523. <C:time-range start="20250716T220000Z" end="20250723T220000Z" />
  524. </C:comp-filter>
  525. </C:comp-filter>
  526. </C:filter>
  527. </C:calendar-query>
  528. """
  529. status, responses = self.report("/test", request)
  530. assert status == 207
  531. assert len(responses) == 1
  532. assert isinstance(responses["/test/event_issue1812.ics"], dict)
  533. assert "D:getetag" in responses["/test/event_issue1812.ics"]
  534. def test_report_getetag_expand_filter_negative1(self) -> None:
  535. """Test getetag with time-range filter and expand, should not return anything (example from #1812)."""
  536. self.mkcalendar("/test/")
  537. self.put("/test/event_issue1812_getetag.ics", get_file_content("event_issue1812_getetag.ics"))
  538. request = """
  539. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  540. <D:prop>
  541. <D:getetag>
  542. <C:expand start="20250706T220000Z" end="20250713T220000Z" />
  543. </D:getetag>
  544. </D:prop>
  545. <C:filter>
  546. <C:comp-filter name="VCALENDAR">
  547. <C:comp-filter name="VEVENT">
  548. <C:time-range start="20250706T220000Z" end="20250713T220000Z" />
  549. </C:comp-filter>
  550. </C:comp-filter>
  551. </C:filter>
  552. </C:calendar-query>
  553. """
  554. status, responses = self.report("/test", request)
  555. assert status == 207
  556. assert len(responses) == 0
  557. def test_report_getetag_expand_filter_negative2(self) -> None:
  558. """Test getetag with time-range filter and expand, should not return anything (example from #1812)."""
  559. self.mkcalendar("/test/")
  560. self.put("/test/event_issue1812_getetag.ics", get_file_content("event_issue1812_getetag.ics"))
  561. request = """
  562. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  563. <D:prop>
  564. <D:getetag />
  565. <C:calendar-data>
  566. <C:expand start="20240706T220000Z" end="20240713T220000Z" />
  567. </C:calendar-data>
  568. </D:prop>
  569. <C:filter>
  570. <C:comp-filter name="VCALENDAR">
  571. <C:comp-filter name="VEVENT">
  572. <C:time-range start="20250706T220000Z" end="20250713T220000Z" />
  573. </C:comp-filter>
  574. </C:comp-filter>
  575. </C:filter>
  576. </C:calendar-query>
  577. """
  578. status, responses = self.report("/test", request)
  579. assert status == 207
  580. assert len(responses) == 0
  581. def test_report_getetag_expand_filter_negative3(self) -> None:
  582. """Test getetag with time-range filter and expand, should not return anything (example from #1812)."""
  583. self.mkcalendar("/test/")
  584. self.put("/test/event_issue1812_getetag.ics", get_file_content("event_issue1812_getetag.ics"))
  585. request = """
  586. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  587. <D:prop>
  588. <C:calendar-data>
  589. <C:expand start="20240706T220000Z" end="20240713T220000Z" />
  590. </C:calendar-data>
  591. </D:prop>
  592. <C:filter>
  593. <C:comp-filter name="VCALENDAR">
  594. <C:comp-filter name="VEVENT">
  595. <C:time-range start="20250706T220000Z" end="20250713T220000Z" />
  596. </C:comp-filter>
  597. </C:comp-filter>
  598. </C:filter>
  599. </C:calendar-query>
  600. """
  601. status, responses = self.report("/test", request)
  602. assert status == 207
  603. assert len(responses) == 0
  604. def test_report_getetag_expand_filter_negative4(self) -> None:
  605. """Test getetag with time-range filter and expand, nothing returned as filter is not matching (example from #1812)."""
  606. self.mkcalendar("/test/")
  607. self.put("/test/event_issue1812.ics", get_file_content("event_issue1812.ics"))
  608. request = """
  609. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  610. <D:prop>
  611. <D:getetag>
  612. <C:expand start="20250706T220000Z" end="20250730T220000Z" />
  613. </D:getetag>
  614. </D:prop>
  615. <C:filter>
  616. <C:comp-filter name="VCALENDAR">
  617. <C:comp-filter name="VEVENT">
  618. <C:time-range start="20240716T220000Z" end="20240723T220000Z" />
  619. </C:comp-filter>
  620. </C:comp-filter>
  621. </C:filter>
  622. </C:calendar-query>
  623. """
  624. status, responses = self.report("/test", request)
  625. assert status == 207
  626. assert len(responses) == 0