test_expand.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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. #
  7. # This library is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This library is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  19. """
  20. Radicale tests with expand requests.
  21. """
  22. import os
  23. from typing import ClassVar, List
  24. from xml.etree import ElementTree
  25. from radicale.log import logger
  26. from radicale.tests import BaseTest
  27. from radicale.tests.helpers import get_file_content
  28. ONLY_DATES = True
  29. CONTAINS_TIMES = False
  30. class TestExpandRequests(BaseTest):
  31. """Tests with expand requests."""
  32. # Allow skipping sync-token tests, when not fully supported by the backend
  33. full_sync_token_support: ClassVar[bool] = True
  34. def setup_method(self) -> None:
  35. BaseTest.setup_method(self)
  36. rights_file_path = os.path.join(self.colpath, "rights")
  37. with open(rights_file_path, "w") as f:
  38. f.write("""\
  39. [permit delete collection]
  40. user: .*
  41. collection: test-permit-delete
  42. permissions: RrWwD
  43. [forbid delete collection]
  44. user: .*
  45. collection: test-forbid-delete
  46. permissions: RrWwd
  47. [permit overwrite collection]
  48. user: .*
  49. collection: test-permit-overwrite
  50. permissions: RrWwO
  51. [forbid overwrite collection]
  52. user: .*
  53. collection: test-forbid-overwrite
  54. permissions: RrWwo
  55. [allow all]
  56. user: .*
  57. collection: .*
  58. permissions: RrWw""")
  59. self.configure({"rights": {"file": rights_file_path,
  60. "type": "from_file"}})
  61. def _test_expand(self,
  62. expected_uid: str,
  63. start: str,
  64. end: str,
  65. expected_recurrence_ids: List[str],
  66. expected_start_times: List[str],
  67. expected_end_times: List[str],
  68. only_dates: bool,
  69. nr_uids: int) -> None:
  70. self.put("/calendar.ics/", get_file_content(f"{expected_uid}.ics"))
  71. req_body_without_expand = \
  72. f"""<?xml version="1.0" encoding="utf-8" ?>
  73. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  74. <D:prop>
  75. <C:calendar-data>
  76. </C:calendar-data>
  77. </D:prop>
  78. <C:filter>
  79. <C:comp-filter name="VCALENDAR">
  80. <C:comp-filter name="VEVENT">
  81. <C:time-range start="{start}" end="{end}"/>
  82. </C:comp-filter>
  83. </C:comp-filter>
  84. </C:filter>
  85. </C:calendar-query>
  86. """
  87. _, responses = self.report("/calendar.ics/", req_body_without_expand)
  88. assert len(responses) == 1
  89. response_without_expand = responses[f'/calendar.ics/{expected_uid}.ics']
  90. assert not isinstance(response_without_expand, int)
  91. status, element = response_without_expand["C:calendar-data"]
  92. assert status == 200 and element.text
  93. assert "RRULE" in element.text
  94. if not only_dates:
  95. assert "BEGIN:VTIMEZONE" in element.text
  96. if nr_uids == 1:
  97. assert "RECURRENCE-ID" not in element.text
  98. uids: List[str] = []
  99. for line in element.text.split("\n"):
  100. if line.startswith("UID:"):
  101. uid = line[len("UID:"):]
  102. assert uid == expected_uid
  103. uids.append(uid)
  104. assert len(uids) == nr_uids
  105. req_body_with_expand = \
  106. f"""<?xml version="1.0" encoding="utf-8" ?>
  107. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  108. <D:prop>
  109. <C:calendar-data>
  110. <C:expand start="{start}" end="{end}"/>
  111. </C:calendar-data>
  112. </D:prop>
  113. <C:filter>
  114. <C:comp-filter name="VCALENDAR">
  115. <C:comp-filter name="VEVENT">
  116. <C:time-range start="{start}" end="{end}"/>
  117. </C:comp-filter>
  118. </C:comp-filter>
  119. </C:filter>
  120. </C:calendar-query>
  121. """
  122. _, responses = self.report("/calendar.ics/", req_body_with_expand)
  123. assert len(responses) == 1
  124. response_with_expand = responses[f'/calendar.ics/{expected_uid}.ics']
  125. assert not isinstance(response_with_expand, int)
  126. status, element = response_with_expand["C:calendar-data"]
  127. logger.debug("lbt: element is %s",
  128. ElementTree.tostring(element, encoding='unicode'))
  129. assert status == 200 and element.text
  130. assert "RRULE" not in element.text
  131. assert "BEGIN:VTIMEZONE" not in element.text
  132. uids = []
  133. recurrence_ids = []
  134. for line in element.text.split("\n"):
  135. if line.startswith("UID:"):
  136. assert line == f"UID:{expected_uid}"
  137. uids.append(line)
  138. if line.startswith("RECURRENCE-ID:"):
  139. assert line in expected_recurrence_ids
  140. recurrence_ids.append(line)
  141. if line.startswith("DTSTART:"):
  142. assert line in expected_start_times
  143. if line.startswith("DTEND:"):
  144. assert line in expected_end_times
  145. assert len(uids) == len(expected_recurrence_ids)
  146. assert len(set(recurrence_ids)) == len(expected_recurrence_ids)
  147. def test_report_with_expand_property(self) -> None:
  148. """Test report with expand property"""
  149. self._test_expand(
  150. "event_daily_rrule",
  151. "20060103T000000Z",
  152. "20060105T000000Z",
  153. ["RECURRENCE-ID:20060103T170000Z", "RECURRENCE-ID:20060104T170000Z"],
  154. ["DTSTART:20060103T170000Z", "DTSTART:20060104T170000Z"],
  155. [],
  156. CONTAINS_TIMES,
  157. 1
  158. )
  159. def test_report_with_expand_property_start_inside(self) -> None:
  160. """Test report with expand property start inside"""
  161. self._test_expand(
  162. "event_daily_rrule",
  163. "20060103T171500Z",
  164. "20060105T000000Z",
  165. ["RECURRENCE-ID:20060103T170000Z", "RECURRENCE-ID:20060104T170000Z"],
  166. ["DTSTART:20060103T170000Z", "DTSTART:20060104T170000Z"],
  167. [],
  168. CONTAINS_TIMES,
  169. 1
  170. )
  171. def test_report_with_expand_property_just_inside(self) -> None:
  172. """Test report with expand property start and end inside event"""
  173. self._test_expand(
  174. "event_daily_rrule",
  175. "20060103T171500Z",
  176. "20060103T171501Z",
  177. ["RECURRENCE-ID:20060103T170000Z"],
  178. ["DTSTART:20060103T170000Z"],
  179. [],
  180. CONTAINS_TIMES,
  181. 1
  182. )
  183. def test_report_with_expand_property_issue1812(self) -> None:
  184. """Test report with expand property for issue 1812"""
  185. self._test_expand(
  186. "event_issue1812",
  187. "20250127T183000Z",
  188. "20250127T183001Z",
  189. ["RECURRENCE-ID:20250127T180000Z"],
  190. ["DTSTART:20250127T180000Z"],
  191. ["DTEND:20250127T233000Z"],
  192. CONTAINS_TIMES,
  193. 11
  194. )
  195. def test_report_with_expand_property_issue1812_DS(self) -> None:
  196. """Test report with expand property for issue 1812 - DS active"""
  197. self._test_expand(
  198. "event_issue1812",
  199. "20250627T183000Z",
  200. "20250627T183001Z",
  201. ["RECURRENCE-ID:20250627T170000Z"],
  202. ["DTSTART:20250627T170000Z"],
  203. ["DTEND:20250627T223000Z"],
  204. CONTAINS_TIMES,
  205. 11
  206. )
  207. def test_report_with_expand_property_all_day_event(self) -> None:
  208. """Test report with expand property for all day events"""
  209. self._test_expand(
  210. "event_full_day_rrule",
  211. "20060103T000000Z",
  212. "20060105T000000Z",
  213. ["RECURRENCE-ID:20060103", "RECURRENCE-ID:20060104", "RECURRENCE-ID:20060105"],
  214. ["DTSTART:20060103", "DTSTART:20060104", "DTSTART:20060105"],
  215. ["DTEND:20060104", "DTEND:20060105", "DTEND:20060106"],
  216. ONLY_DATES,
  217. 1
  218. )
  219. def test_report_with_expand_property_overridden(self) -> None:
  220. """Test report with expand property with overridden events"""
  221. self._test_expand(
  222. "event_daily_rrule_overridden",
  223. "20060103T000000Z",
  224. "20060105T000000Z",
  225. ["RECURRENCE-ID:20060103T170000Z", "RECURRENCE-ID:20060104T170000Z"],
  226. ["DTSTART:20060103T170000Z", "DTSTART:20060104T190000Z"],
  227. [],
  228. CONTAINS_TIMES,
  229. 2
  230. )
  231. def test_report_with_expand_property_timezone(self):
  232. self._test_expand(
  233. "event_weekly_rrule",
  234. "20060320T000000Z",
  235. "20060414T000000Z",
  236. [
  237. "RECURRENCE-ID:20060321T200000Z",
  238. "RECURRENCE-ID:20060328T200000Z",
  239. "RECURRENCE-ID:20060404T190000Z",
  240. "RECURRENCE-ID:20060411T190000Z",
  241. ],
  242. [
  243. "DTSTART:20060321T200000Z",
  244. "DTSTART:20060328T200000Z",
  245. "DTSTART:20060404T190000Z",
  246. "DTSTART:20060411T190000Z",
  247. ],
  248. [],
  249. CONTAINS_TIMES,
  250. 1
  251. )