test_base.py 71 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659
  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. #
  5. # This library is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This library is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  17. """
  18. Radicale tests with simple requests.
  19. """
  20. import os
  21. import posixpath
  22. from typing import Any, Callable, ClassVar, Iterable, List, Optional, Tuple
  23. import defusedxml.ElementTree as DefusedET
  24. from radicale import storage, xmlutils
  25. from radicale.tests import RESPONSES, BaseTest
  26. from radicale.tests.helpers import get_file_content
  27. class TestBaseRequests(BaseTest):
  28. """Tests with simple requests."""
  29. # Allow skipping sync-token tests, when not fully supported by the backend
  30. full_sync_token_support: ClassVar[bool] = True
  31. def setup(self) -> None:
  32. BaseTest.setup(self)
  33. rights_file_path = os.path.join(self.colpath, "rights")
  34. with open(rights_file_path, "w") as f:
  35. f.write("""\
  36. [allow all]
  37. user: .*
  38. collection: .*
  39. permissions: RrWw""")
  40. self.configure({"rights": {"file": rights_file_path,
  41. "type": "from_file"}})
  42. def test_root(self) -> None:
  43. """GET request at "/"."""
  44. for path in ["", "/", "//"]:
  45. _, headers, answer = self.request("GET", path, check=302)
  46. assert headers.get("Location") == "/.web"
  47. assert answer == "Redirected to /.web"
  48. def test_root_script_name(self) -> None:
  49. """GET request at "/" with SCRIPT_NAME."""
  50. for path in ["", "/", "//"]:
  51. _, headers, _ = self.request("GET", path, check=302,
  52. SCRIPT_NAME="/radicale")
  53. assert headers.get("Location") == "/radicale/.web"
  54. def test_root_broken_script_name(self) -> None:
  55. """GET request at "/" with SCRIPT_NAME ending with "/"."""
  56. for script_name, prefix in [
  57. ("/", ""), ("//", ""), ("/radicale/", "/radicale"),
  58. ("radicale", None), ("radicale//", None)]:
  59. _, headers, _ = self.request(
  60. "GET", "/", check=500 if prefix is None else 302,
  61. SCRIPT_NAME=script_name)
  62. assert (prefix is None or
  63. headers.get("Location") == prefix + "/.web")
  64. def test_root_http_x_script_name(self) -> None:
  65. """GET request at "/" with HTTP_X_SCRIPT_NAME."""
  66. for path in ["", "/", "//"]:
  67. _, headers, _ = self.request("GET", path, check=302,
  68. HTTP_X_SCRIPT_NAME="/radicale")
  69. assert headers.get("Location") == "/radicale/.web"
  70. def test_root_broken_http_x_script_name(self) -> None:
  71. """GET request at "/" with HTTP_X_SCRIPT_NAME ending with "/"."""
  72. for script_name, prefix in [
  73. ("/", ""), ("//", ""), ("/radicale/", "/radicale"),
  74. ("radicale", None), ("radicale//", None)]:
  75. _, headers, _ = self.request(
  76. "GET", "/", check=400 if prefix is None else 302,
  77. HTTP_X_SCRIPT_NAME=script_name)
  78. assert (prefix is None or
  79. headers.get("Location") == prefix + "/.web")
  80. def test_sanitized_path(self) -> None:
  81. """GET request with unsanitized paths."""
  82. for path, sane_path in [
  83. ("//.web", "/.web"), ("//.web/", "/.web/"),
  84. ("/.web//", "/.web/"), ("/.web/a//b", "/.web/a/b")]:
  85. _, headers, _ = self.request("GET", path, check=301)
  86. assert headers.get("Location") == sane_path
  87. _, headers, _ = self.request("GET", path, check=301,
  88. SCRIPT_NAME="/radicale")
  89. assert headers.get("Location") == "/radicale%s" % sane_path
  90. _, headers, _ = self.request("GET", path, check=301,
  91. HTTP_X_SCRIPT_NAME="/radicale")
  92. assert headers.get("Location") == "/radicale%s" % sane_path
  93. def test_add_event(self) -> None:
  94. """Add an event."""
  95. self.mkcalendar("/calendar.ics/")
  96. event = get_file_content("event1.ics")
  97. path = "/calendar.ics/event1.ics"
  98. self.put(path, event)
  99. _, headers, answer = self.request("GET", path, check=200)
  100. assert "ETag" in headers
  101. assert headers["Content-Type"] == "text/calendar; charset=utf-8"
  102. assert "VEVENT" in answer
  103. assert "Event" in answer
  104. assert "UID:event" in answer
  105. def test_add_event_without_uid(self) -> None:
  106. """Add an event without UID."""
  107. self.mkcalendar("/calendar.ics/")
  108. event = get_file_content("event1.ics").replace("UID:event1\n", "")
  109. assert "\nUID:" not in event
  110. path = "/calendar.ics/event.ics"
  111. self.put(path, event, check=400)
  112. def test_add_event_duplicate_uid(self) -> None:
  113. """Add an event with an existing UID."""
  114. self.mkcalendar("/calendar.ics/")
  115. event = get_file_content("event1.ics")
  116. self.put("/calendar.ics/event1.ics", event)
  117. status, answer = self.put(
  118. "/calendar.ics/event1-duplicate.ics", event, check=None)
  119. assert status in (403, 409)
  120. xml = DefusedET.fromstring(answer)
  121. assert xml.tag == xmlutils.make_clark("D:error")
  122. assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
  123. def test_add_event_with_mixed_datetime_and_date(self) -> None:
  124. """Test event with DTSTART as DATE-TIME and EXDATE as DATE."""
  125. self.mkcalendar("/calendar.ics/")
  126. event = get_file_content("event_mixed_datetime_and_date.ics")
  127. self.put("/calendar.ics/event.ics", event)
  128. def test_add_todo(self) -> None:
  129. """Add a todo."""
  130. self.mkcalendar("/calendar.ics/")
  131. todo = get_file_content("todo1.ics")
  132. path = "/calendar.ics/todo1.ics"
  133. self.put(path, todo)
  134. _, headers, answer = self.request("GET", path, check=200)
  135. assert "ETag" in headers
  136. assert headers["Content-Type"] == "text/calendar; charset=utf-8"
  137. assert "VTODO" in answer
  138. assert "Todo" in answer
  139. assert "UID:todo" in answer
  140. def test_add_contact(self) -> None:
  141. """Add a contact."""
  142. self.create_addressbook("/contacts.vcf/")
  143. contact = get_file_content("contact1.vcf")
  144. path = "/contacts.vcf/contact.vcf"
  145. self.put(path, contact)
  146. _, headers, answer = self.request("GET", path, check=200)
  147. assert "ETag" in headers
  148. assert headers["Content-Type"] == "text/vcard; charset=utf-8"
  149. assert "VCARD" in answer
  150. assert "UID:contact1" in answer
  151. _, answer = self.get(path)
  152. assert "UID:contact1" in answer
  153. def test_add_contact_photo_with_data_uri(self) -> None:
  154. """Test workaround for broken PHOTO data from InfCloud"""
  155. self.create_addressbook("/contacts.vcf/")
  156. contact = get_file_content("contact_photo_with_data_uri.vcf")
  157. self.put("/contacts.vcf/contact.vcf", contact)
  158. def test_add_contact_without_uid(self) -> None:
  159. """Add a contact without UID."""
  160. self.create_addressbook("/contacts.vcf/")
  161. contact = get_file_content("contact1.vcf").replace("UID:contact1\n",
  162. "")
  163. assert "\nUID" not in contact
  164. path = "/contacts.vcf/contact.vcf"
  165. self.put(path, contact, check=400)
  166. def test_update_event(self) -> None:
  167. """Update an event."""
  168. self.mkcalendar("/calendar.ics/")
  169. event = get_file_content("event1.ics")
  170. event_modified = get_file_content("event1_modified.ics")
  171. path = "/calendar.ics/event1.ics"
  172. self.put(path, event)
  173. self.put(path, event_modified)
  174. _, answer = self.get("/calendar.ics/")
  175. assert answer.count("BEGIN:VEVENT") == 1
  176. _, answer = self.get(path)
  177. assert "DTSTAMP:20130902T150159Z" in answer
  178. def test_update_event_uid_event(self) -> None:
  179. """Update an event with a different UID."""
  180. self.mkcalendar("/calendar.ics/")
  181. event1 = get_file_content("event1.ics")
  182. event2 = get_file_content("event2.ics")
  183. path = "/calendar.ics/event1.ics"
  184. self.put(path, event1)
  185. status, answer = self.put(path, event2, check=None)
  186. assert status in (403, 409)
  187. xml = DefusedET.fromstring(answer)
  188. assert xml.tag == xmlutils.make_clark("D:error")
  189. assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
  190. def test_put_whole_calendar(self) -> None:
  191. """Create and overwrite a whole calendar."""
  192. self.put("/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
  193. event1 = get_file_content("event1.ics")
  194. self.put("/calendar.ics/test_event.ics", event1)
  195. # Overwrite
  196. events = get_file_content("event_multiple.ics")
  197. self.put("/calendar.ics/", events)
  198. self.get("/calendar.ics/test_event.ics", check=404)
  199. _, answer = self.get("/calendar.ics/")
  200. assert "\r\nUID:event\r\n" in answer and "\r\nUID:todo\r\n" in answer
  201. assert "\r\nUID:event1\r\n" not in answer
  202. def test_put_whole_calendar_without_uids(self) -> None:
  203. """Create a whole calendar without UID."""
  204. event = get_file_content("event_multiple.ics")
  205. event = event.replace("UID:event\n", "").replace("UID:todo\n", "")
  206. assert "\nUID:" not in event
  207. self.put("/calendar.ics/", event)
  208. _, answer = self.get("/calendar.ics")
  209. uids = []
  210. for line in answer.split("\r\n"):
  211. if line.startswith("UID:"):
  212. uids.append(line[len("UID:"):])
  213. assert len(uids) == 2
  214. for i, uid1 in enumerate(uids):
  215. assert uid1
  216. for uid2 in uids[i + 1:]:
  217. assert uid1 != uid2
  218. def test_put_whole_addressbook(self) -> None:
  219. """Create and overwrite a whole addressbook."""
  220. contacts = get_file_content("contact_multiple.vcf")
  221. self.put("/contacts.vcf/", contacts)
  222. _, answer = self.get("/contacts.vcf/")
  223. assert answer is not None
  224. assert "\r\nUID:contact1\r\n" in answer
  225. assert "\r\nUID:contact2\r\n" in answer
  226. def test_put_whole_addressbook_without_uids(self) -> None:
  227. """Create a whole addressbook without UID."""
  228. contacts = get_file_content("contact_multiple.vcf")
  229. contacts = contacts.replace("UID:contact1\n", "").replace(
  230. "UID:contact2\n", "")
  231. assert "\nUID:" not in contacts
  232. self.put("/contacts.vcf/", contacts)
  233. _, answer = self.get("/contacts.vcf")
  234. uids = []
  235. for line in answer.split("\r\n"):
  236. if line.startswith("UID:"):
  237. uids.append(line[len("UID:"):])
  238. assert len(uids) == 2
  239. for i, uid1 in enumerate(uids):
  240. assert uid1
  241. for uid2 in uids[i + 1:]:
  242. assert uid1 != uid2
  243. def test_verify(self) -> None:
  244. """Verify the storage."""
  245. contacts = get_file_content("contact_multiple.vcf")
  246. self.put("/contacts.vcf/", contacts)
  247. events = get_file_content("event_multiple.ics")
  248. self.put("/calendar.ics/", events)
  249. s = storage.load(self.configuration)
  250. assert s.verify()
  251. def test_delete(self) -> None:
  252. """Delete an event."""
  253. self.mkcalendar("/calendar.ics/")
  254. event = get_file_content("event1.ics")
  255. path = "/calendar.ics/event1.ics"
  256. self.put(path, event)
  257. _, responses = self.delete(path)
  258. assert responses[path] == 200
  259. _, answer = self.get("/calendar.ics/")
  260. assert "VEVENT" not in answer
  261. def test_mkcalendar(self) -> None:
  262. """Make a calendar."""
  263. self.mkcalendar("/calendar.ics/")
  264. _, answer = self.get("/calendar.ics/")
  265. assert "BEGIN:VCALENDAR" in answer
  266. assert "END:VCALENDAR" in answer
  267. def test_mkcalendar_overwrite(self) -> None:
  268. """Try to overwrite an existing calendar."""
  269. self.mkcalendar("/calendar.ics/")
  270. status, answer = self.mkcalendar("/calendar.ics/", check=None)
  271. assert status in (403, 409)
  272. xml = DefusedET.fromstring(answer)
  273. assert xml.tag == xmlutils.make_clark("D:error")
  274. assert xml.find(xmlutils.make_clark(
  275. "D:resource-must-be-null")) is not None
  276. def test_mkcalendar_intermediate(self) -> None:
  277. """Try make a calendar in a unmapped collection."""
  278. self.mkcalendar("/unmapped/calendar.ics/", check=409)
  279. def test_mkcol(self) -> None:
  280. """Make a collection."""
  281. self.mkcol("/user/")
  282. def test_mkcol_overwrite(self) -> None:
  283. """Try to overwrite an existing collection."""
  284. self.mkcol("/user/")
  285. self.mkcol("/user/", check=405)
  286. def test_mkcol_intermediate(self) -> None:
  287. """Try make a collection in a unmapped collection."""
  288. self.mkcol("/unmapped/user/", check=409)
  289. def test_mkcol_make_calendar(self) -> None:
  290. """Make a calendar with additional props."""
  291. mkcol_make_calendar = get_file_content("mkcol_make_calendar.xml")
  292. self.mkcol("/calendar.ics/", mkcol_make_calendar)
  293. _, answer = self.get("/calendar.ics/")
  294. assert answer is not None
  295. assert "BEGIN:VCALENDAR" in answer
  296. assert "END:VCALENDAR" in answer
  297. # Read additional properties
  298. propfind = get_file_content("propfind_calendar_color.xml")
  299. _, responses = self.propfind("/calendar.ics/", propfind)
  300. response = responses["/calendar.ics/"]
  301. assert not isinstance(response, int) and len(response) == 1
  302. status, prop = response["ICAL:calendar-color"]
  303. assert status == 200 and prop.text == "#BADA55"
  304. def test_move(self) -> None:
  305. """Move a item."""
  306. self.mkcalendar("/calendar.ics/")
  307. event = get_file_content("event1.ics")
  308. path1 = "/calendar.ics/event1.ics"
  309. path2 = "/calendar.ics/event2.ics"
  310. self.put(path1, event)
  311. self.request("MOVE", path1, check=201,
  312. HTTP_DESTINATION=path2, HTTP_HOST="")
  313. self.get(path1, check=404)
  314. self.get(path2)
  315. def test_move_between_colections(self) -> None:
  316. """Move a item."""
  317. self.mkcalendar("/calendar1.ics/")
  318. self.mkcalendar("/calendar2.ics/")
  319. event = get_file_content("event1.ics")
  320. path1 = "/calendar1.ics/event1.ics"
  321. path2 = "/calendar2.ics/event2.ics"
  322. self.put(path1, event)
  323. self.request("MOVE", path1, check=201,
  324. HTTP_DESTINATION=path2, HTTP_HOST="")
  325. self.get(path1, check=404)
  326. self.get(path2)
  327. def test_move_between_colections_duplicate_uid(self) -> None:
  328. """Move a item to a collection which already contains the UID."""
  329. self.mkcalendar("/calendar1.ics/")
  330. self.mkcalendar("/calendar2.ics/")
  331. event = get_file_content("event1.ics")
  332. path1 = "/calendar1.ics/event1.ics"
  333. path2 = "/calendar2.ics/event2.ics"
  334. self.put(path1, event)
  335. self.put("/calendar2.ics/event1.ics", event)
  336. status, _, answer = self.request(
  337. "MOVE", path1, HTTP_DESTINATION=path2, HTTP_HOST="")
  338. assert status in (403, 409)
  339. xml = DefusedET.fromstring(answer)
  340. assert xml.tag == xmlutils.make_clark("D:error")
  341. assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
  342. def test_move_between_colections_overwrite(self) -> None:
  343. """Move a item to a collection which already contains the item."""
  344. self.mkcalendar("/calendar1.ics/")
  345. self.mkcalendar("/calendar2.ics/")
  346. event = get_file_content("event1.ics")
  347. path1 = "/calendar1.ics/event1.ics"
  348. path2 = "/calendar2.ics/event1.ics"
  349. self.put(path1, event)
  350. self.put(path2, event)
  351. self.request("MOVE", path1, check=412,
  352. HTTP_DESTINATION=path2, HTTP_HOST="")
  353. self.request("MOVE", path1, check=204,
  354. HTTP_DESTINATION=path2, HTTP_HOST="", HTTP_OVERWRITE="T")
  355. def test_move_between_colections_overwrite_uid_conflict(self) -> None:
  356. """Move a item to a collection which already contains the item with
  357. a different UID."""
  358. self.mkcalendar("/calendar1.ics/")
  359. self.mkcalendar("/calendar2.ics/")
  360. event1 = get_file_content("event1.ics")
  361. event2 = get_file_content("event2.ics")
  362. path1 = "/calendar1.ics/event1.ics"
  363. path2 = "/calendar2.ics/event2.ics"
  364. self.put(path1, event1)
  365. self.put(path2, event2)
  366. status, _, answer = self.request("MOVE", path1, HTTP_DESTINATION=path2,
  367. HTTP_HOST="", HTTP_OVERWRITE="T")
  368. assert status in (403, 409)
  369. xml = DefusedET.fromstring(answer)
  370. assert xml.tag == xmlutils.make_clark("D:error")
  371. assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
  372. def test_head(self) -> None:
  373. _, headers, answer = self.request("HEAD", "/", check=302)
  374. assert int(headers.get("Content-Length", "0")) > 0 and not answer
  375. def test_options(self) -> None:
  376. _, headers, _ = self.request("OPTIONS", "/", check=200)
  377. assert "DAV" in headers
  378. def test_delete_collection(self) -> None:
  379. """Delete a collection."""
  380. self.mkcalendar("/calendar.ics/")
  381. event = get_file_content("event1.ics")
  382. self.put("/calendar.ics/event1.ics", event)
  383. _, responses = self.delete("/calendar.ics/")
  384. assert responses["/calendar.ics/"] == 200
  385. self.get("/calendar.ics/", check=404)
  386. def test_delete_root_collection(self) -> None:
  387. """Delete the root collection."""
  388. self.mkcalendar("/calendar.ics/")
  389. event = get_file_content("event1.ics")
  390. self.put("/event1.ics", event)
  391. self.put("/calendar.ics/event1.ics", event)
  392. _, responses = self.delete("/")
  393. assert len(responses) == 1 and responses["/"] == 200
  394. self.get("/calendar.ics/", check=404)
  395. self.get("/event1.ics", 404)
  396. def test_propfind(self) -> None:
  397. calendar_path = "/calendar.ics/"
  398. self.mkcalendar("/calendar.ics/")
  399. event = get_file_content("event1.ics")
  400. event_path = posixpath.join(calendar_path, "event.ics")
  401. self.put(event_path, event)
  402. _, responses = self.propfind("/", HTTP_DEPTH="1")
  403. assert len(responses) == 2
  404. assert "/" in responses and calendar_path in responses
  405. _, responses = self.propfind(calendar_path, HTTP_DEPTH="1")
  406. assert len(responses) == 2
  407. assert calendar_path in responses and event_path in responses
  408. def test_propfind_propname(self) -> None:
  409. self.mkcalendar("/calendar.ics/")
  410. event = get_file_content("event1.ics")
  411. self.put("/calendar.ics/event.ics", event)
  412. propfind = get_file_content("propname.xml")
  413. _, responses = self.propfind("/calendar.ics/", propfind)
  414. response = responses["/calendar.ics/"]
  415. assert not isinstance(response, int)
  416. status, prop = response["D:sync-token"]
  417. assert status == 200 and not prop.text
  418. _, responses = self.propfind("/calendar.ics/event.ics", propfind)
  419. response = responses["/calendar.ics/event.ics"]
  420. assert not isinstance(response, int)
  421. status, prop = response["D:getetag"]
  422. assert status == 200 and not prop.text
  423. def test_propfind_allprop(self) -> None:
  424. self.mkcalendar("/calendar.ics/")
  425. event = get_file_content("event1.ics")
  426. self.put("/calendar.ics/event.ics", event)
  427. propfind = get_file_content("allprop.xml")
  428. _, responses = self.propfind("/calendar.ics/", propfind)
  429. response = responses["/calendar.ics/"]
  430. assert not isinstance(response, int)
  431. status, prop = response["D:sync-token"]
  432. assert status == 200 and prop.text
  433. _, responses = self.propfind("/calendar.ics/event.ics", propfind)
  434. response = responses["/calendar.ics/event.ics"]
  435. assert not isinstance(response, int)
  436. status, prop = response["D:getetag"]
  437. assert status == 200 and prop.text
  438. def test_propfind_nonexistent(self) -> None:
  439. """Read a property that does not exist."""
  440. self.mkcalendar("/calendar.ics/")
  441. propfind = get_file_content("propfind_calendar_color.xml")
  442. _, responses = self.propfind("/calendar.ics/", propfind)
  443. response = responses["/calendar.ics/"]
  444. assert not isinstance(response, int) and len(response) == 1
  445. status, prop = response["ICAL:calendar-color"]
  446. assert status == 404 and not prop.text
  447. def test_proppatch(self) -> None:
  448. """Set/Remove a property and read it back."""
  449. self.mkcalendar("/calendar.ics/")
  450. proppatch = get_file_content("proppatch_set_calendar_color.xml")
  451. _, responses = self.proppatch("/calendar.ics/", proppatch)
  452. response = responses["/calendar.ics/"]
  453. assert not isinstance(response, int) and len(response) == 1
  454. status, prop = response["ICAL:calendar-color"]
  455. assert status == 200 and not prop.text
  456. # Read property back
  457. propfind = get_file_content("propfind_calendar_color.xml")
  458. _, responses = self.propfind("/calendar.ics/", propfind)
  459. response = responses["/calendar.ics/"]
  460. assert not isinstance(response, int) and len(response) == 1
  461. status, prop = response["ICAL:calendar-color"]
  462. assert status == 200 and prop.text == "#BADA55"
  463. propfind = get_file_content("allprop.xml")
  464. _, responses = self.propfind("/calendar.ics/", propfind)
  465. response = responses["/calendar.ics/"]
  466. assert not isinstance(response, int)
  467. status, prop = response["ICAL:calendar-color"]
  468. assert status == 200 and prop.text == "#BADA55"
  469. # Remove property
  470. proppatch = get_file_content("proppatch_remove_calendar_color.xml")
  471. _, responses = self.proppatch("/calendar.ics/", proppatch)
  472. response = responses["/calendar.ics/"]
  473. assert not isinstance(response, int) and len(response) == 1
  474. status, prop = response["ICAL:calendar-color"]
  475. assert status == 200 and not prop.text
  476. # Read property back
  477. propfind = get_file_content("propfind_calendar_color.xml")
  478. _, responses = self.propfind("/calendar.ics/", propfind)
  479. response = responses["/calendar.ics/"]
  480. assert not isinstance(response, int) and len(response) == 1
  481. status, prop = response["ICAL:calendar-color"]
  482. assert status == 404
  483. def test_proppatch_multiple1(self) -> None:
  484. """Set/Remove a multiple properties and read them back."""
  485. self.mkcalendar("/calendar.ics/")
  486. propfind = get_file_content("propfind_multiple.xml")
  487. proppatch = get_file_content("proppatch_set_multiple1.xml")
  488. _, responses = self.proppatch("/calendar.ics/", proppatch)
  489. response = responses["/calendar.ics/"]
  490. assert not isinstance(response, int) and len(response) == 2
  491. status, prop = response["ICAL:calendar-color"]
  492. assert status == 200 and not prop.text
  493. status, prop = response["C:calendar-description"]
  494. assert status == 200 and not prop.text
  495. # Read properties back
  496. _, responses = self.propfind("/calendar.ics/", propfind)
  497. response = responses["/calendar.ics/"]
  498. assert not isinstance(response, int) and len(response) == 2
  499. status, prop = response["ICAL:calendar-color"]
  500. assert status == 200 and prop.text == "#BADA55"
  501. status, prop = response["C:calendar-description"]
  502. assert status == 200 and prop.text == "test"
  503. # Remove properties
  504. proppatch = get_file_content("proppatch_remove_multiple1.xml")
  505. _, responses = self.proppatch("/calendar.ics/", proppatch)
  506. response = responses["/calendar.ics/"]
  507. assert not isinstance(response, int) and len(response) == 2
  508. status, prop = response["ICAL:calendar-color"]
  509. assert status == 200 and not prop.text
  510. status, prop = response["C:calendar-description"]
  511. assert status == 200 and not prop.text
  512. # Read properties back
  513. _, responses = self.propfind("/calendar.ics/", propfind)
  514. response = responses["/calendar.ics/"]
  515. assert not isinstance(response, int) and len(response) == 2
  516. status, prop = response["ICAL:calendar-color"]
  517. assert status == 404
  518. status, prop = response["C:calendar-description"]
  519. assert status == 404
  520. def test_proppatch_multiple2(self) -> None:
  521. """Set/Remove a multiple properties and read them back."""
  522. self.mkcalendar("/calendar.ics/")
  523. propfind = get_file_content("propfind_multiple.xml")
  524. proppatch = get_file_content("proppatch_set_multiple2.xml")
  525. _, responses = self.proppatch("/calendar.ics/", proppatch)
  526. response = responses["/calendar.ics/"]
  527. assert not isinstance(response, int) and len(response) == 2
  528. status, prop = response["ICAL:calendar-color"]
  529. assert status == 200 and not prop.text
  530. status, prop = response["C:calendar-description"]
  531. assert status == 200 and not prop.text
  532. # Read properties back
  533. _, responses = self.propfind("/calendar.ics/", propfind)
  534. response = responses["/calendar.ics/"]
  535. assert not isinstance(response, int) and len(response) == 2
  536. assert len(response) == 2
  537. status, prop = response["ICAL:calendar-color"]
  538. assert status == 200 and prop.text == "#BADA55"
  539. status, prop = response["C:calendar-description"]
  540. assert status == 200 and prop.text == "test"
  541. # Remove properties
  542. proppatch = get_file_content("proppatch_remove_multiple2.xml")
  543. _, responses = self.proppatch("/calendar.ics/", proppatch)
  544. response = responses["/calendar.ics/"]
  545. assert not isinstance(response, int) and len(response) == 2
  546. status, prop = response["ICAL:calendar-color"]
  547. assert status == 200 and not prop.text
  548. status, prop = response["C:calendar-description"]
  549. assert status == 200 and not prop.text
  550. # Read properties back
  551. _, responses = self.propfind("/calendar.ics/", propfind)
  552. response = responses["/calendar.ics/"]
  553. assert not isinstance(response, int) and len(response) == 2
  554. status, prop = response["ICAL:calendar-color"]
  555. assert status == 404
  556. status, prop = response["C:calendar-description"]
  557. assert status == 404
  558. def test_proppatch_set_and_remove(self) -> None:
  559. """Set and remove multiple properties in single request."""
  560. self.mkcalendar("/calendar.ics/")
  561. propfind = get_file_content("propfind_multiple.xml")
  562. # Prepare
  563. proppatch = get_file_content("proppatch_set_multiple1.xml")
  564. self.proppatch("/calendar.ics/", proppatch)
  565. # Remove and set properties in single request
  566. proppatch = get_file_content("proppatch_set_and_remove.xml")
  567. _, responses = self.proppatch("/calendar.ics/", proppatch)
  568. response = responses["/calendar.ics/"]
  569. assert not isinstance(response, int) and len(response) == 2
  570. status, prop = response["ICAL:calendar-color"]
  571. assert status == 200 and not prop.text
  572. status, prop = response["C:calendar-description"]
  573. assert status == 200 and not prop.text
  574. # Read properties back
  575. _, responses = self.propfind("/calendar.ics/", propfind)
  576. response = responses["/calendar.ics/"]
  577. assert not isinstance(response, int) and len(response) == 2
  578. status, prop = response["ICAL:calendar-color"]
  579. assert status == 404
  580. status, prop = response["C:calendar-description"]
  581. assert status == 200 and prop.text == "test2"
  582. def test_put_whole_calendar_multiple_events_with_same_uid(self) -> None:
  583. """Add two events with the same UID."""
  584. self.put("/calendar.ics/", get_file_content("event2.ics"))
  585. _, responses = self.report("/calendar.ics/", """\
  586. <?xml version="1.0" encoding="utf-8" ?>
  587. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  588. <D:prop xmlns:D="DAV:">
  589. <D:getetag/>
  590. </D:prop>
  591. </C:calendar-query>""")
  592. assert len(responses) == 1
  593. response = responses["/calendar.ics/event2.ics"]
  594. assert not isinstance(response, int)
  595. status, prop = response["D:getetag"]
  596. assert status == 200 and prop.text
  597. _, answer = self.get("/calendar.ics/")
  598. assert answer.count("BEGIN:VEVENT") == 2
  599. def _test_filter(self, filters: Iterable[str], kind: str = "event",
  600. test: Optional[str] = None, items: Iterable[int] = (1,)
  601. ) -> List[str]:
  602. filter_template = "<C:filter>%s</C:filter>"
  603. create_collection_fn: Callable[[str], Any]
  604. if kind in ("event", "journal", "todo"):
  605. create_collection_fn = self.mkcalendar
  606. path = "/calendar.ics/"
  607. filename_template = "%s%d.ics"
  608. namespace = "urn:ietf:params:xml:ns:caldav"
  609. report = "calendar-query"
  610. elif kind == "contact":
  611. create_collection_fn = self.create_addressbook
  612. if test:
  613. filter_template = '<C:filter test="%s">%%s</C:filter>' % test
  614. path = "/contacts.vcf/"
  615. filename_template = "%s%d.vcf"
  616. namespace = "urn:ietf:params:xml:ns:carddav"
  617. report = "addressbook-query"
  618. else:
  619. raise ValueError("Unsupported kind: %r" % kind)
  620. status, _, = self.delete(path, check=None)
  621. assert status in (200, 404)
  622. create_collection_fn(path)
  623. for i in items:
  624. filename = filename_template % (kind, i)
  625. event = get_file_content(filename)
  626. self.put(posixpath.join(path, filename), event)
  627. filters_text = "".join(filter_template % f for f in filters)
  628. _, responses = self.report(path, """\
  629. <?xml version="1.0" encoding="utf-8" ?>
  630. <C:{1} xmlns:C="{0}">
  631. <D:prop xmlns:D="DAV:">
  632. <D:getetag/>
  633. </D:prop>
  634. {2}
  635. </C:{1}>""".format(namespace, report, filters_text))
  636. assert responses is not None
  637. paths = []
  638. for path, props in responses.items():
  639. assert not isinstance(props, int) and len(props) == 1
  640. status, prop = props["D:getetag"]
  641. assert status == 200 and prop.text
  642. paths.append(path)
  643. return paths
  644. def test_addressbook_empty_filter(self) -> None:
  645. self._test_filter([""], kind="contact")
  646. def test_addressbook_prop_filter(self) -> None:
  647. assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
  648. <C:prop-filter name="NICKNAME">
  649. <C:text-match collation="i;unicode-casemap" match-type="contains"
  650. >es</C:text-match>
  651. </C:prop-filter>"""], "contact")
  652. assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
  653. <C:prop-filter name="NICKNAME">
  654. <C:text-match collation="i;unicode-casemap">es</C:text-match>
  655. </C:prop-filter>"""], "contact")
  656. assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
  657. <C:prop-filter name="NICKNAME">
  658. <C:text-match collation="i;unicode-casemap" match-type="contains"
  659. >a</C:text-match>
  660. </C:prop-filter>"""], "contact")
  661. assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
  662. <C:prop-filter name="NICKNAME">
  663. <C:text-match collation="i;unicode-casemap" match-type="equals"
  664. >test</C:text-match>
  665. </C:prop-filter>"""], "contact")
  666. assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
  667. <C:prop-filter name="NICKNAME">
  668. <C:text-match collation="i;unicode-casemap" match-type="equals"
  669. >tes</C:text-match>
  670. </C:prop-filter>"""], "contact")
  671. assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
  672. <C:prop-filter name="NICKNAME">
  673. <C:text-match collation="i;unicode-casemap" match-type="equals"
  674. >est</C:text-match>
  675. </C:prop-filter>"""], "contact")
  676. assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
  677. <C:prop-filter name="NICKNAME">
  678. <C:text-match collation="i;unicode-casemap" match-type="starts-with"
  679. >tes</C:text-match>
  680. </C:prop-filter>"""], "contact")
  681. assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
  682. <C:prop-filter name="NICKNAME">
  683. <C:text-match collation="i;unicode-casemap" match-type="starts-with"
  684. >est</C:text-match>
  685. </C:prop-filter>"""], "contact")
  686. assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
  687. <C:prop-filter name="NICKNAME">
  688. <C:text-match collation="i;unicode-casemap" match-type="ends-with"
  689. >est</C:text-match>
  690. </C:prop-filter>"""], "contact")
  691. assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
  692. <C:prop-filter name="NICKNAME">
  693. <C:text-match collation="i;unicode-casemap" match-type="ends-with"
  694. >tes</C:text-match>
  695. </C:prop-filter>"""], "contact")
  696. def test_addressbook_prop_filter_any(self) -> None:
  697. assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
  698. <C:prop-filter name="NICKNAME">
  699. <C:text-match collation="i;unicode-casemap">test</C:text-match>
  700. </C:prop-filter>
  701. <C:prop-filter name="EMAIL">
  702. <C:text-match collation="i;unicode-casemap">test</C:text-match>
  703. </C:prop-filter>"""], "contact", test="anyof")
  704. assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
  705. <C:prop-filter name="NICKNAME">
  706. <C:text-match collation="i;unicode-casemap">a</C:text-match>
  707. </C:prop-filter>
  708. <C:prop-filter name="EMAIL">
  709. <C:text-match collation="i;unicode-casemap">test</C:text-match>
  710. </C:prop-filter>"""], "contact", test="anyof")
  711. assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
  712. <C:prop-filter name="NICKNAME">
  713. <C:text-match collation="i;unicode-casemap">test</C:text-match>
  714. </C:prop-filter>
  715. <C:prop-filter name="EMAIL">
  716. <C:text-match collation="i;unicode-casemap">test</C:text-match>
  717. </C:prop-filter>"""], "contact")
  718. def test_addressbook_prop_filter_all(self) -> None:
  719. assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
  720. <C:prop-filter name="NICKNAME">
  721. <C:text-match collation="i;unicode-casemap">tes</C:text-match>
  722. </C:prop-filter>
  723. <C:prop-filter name="NICKNAME">
  724. <C:text-match collation="i;unicode-casemap">est</C:text-match>
  725. </C:prop-filter>"""], "contact", test="allof")
  726. assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
  727. <C:prop-filter name="NICKNAME">
  728. <C:text-match collation="i;unicode-casemap">test</C:text-match>
  729. </C:prop-filter>
  730. <C:prop-filter name="EMAIL">
  731. <C:text-match collation="i;unicode-casemap">test</C:text-match>
  732. </C:prop-filter>"""], "contact", test="allof")
  733. def test_calendar_empty_filter(self) -> None:
  734. self._test_filter([""])
  735. def test_calendar_tag_filter(self) -> None:
  736. """Report request with tag-based filter on calendar."""
  737. assert "/calendar.ics/event1.ics" in self._test_filter(["""\
  738. <C:comp-filter name="VCALENDAR"></C:comp-filter>"""])
  739. def test_item_tag_filter(self) -> None:
  740. """Report request with tag-based filter on an item."""
  741. assert "/calendar.ics/event1.ics" in self._test_filter(["""\
  742. <C:comp-filter name="VCALENDAR">
  743. <C:comp-filter name="VEVENT"></C:comp-filter>
  744. </C:comp-filter>"""])
  745. assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
  746. <C:comp-filter name="VCALENDAR">
  747. <C:comp-filter name="VTODO"></C:comp-filter>
  748. </C:comp-filter>"""])
  749. def test_item_not_tag_filter(self) -> None:
  750. """Report request with tag-based is-not filter on an item."""
  751. assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
  752. <C:comp-filter name="VCALENDAR">
  753. <C:comp-filter name="VEVENT">
  754. <C:is-not-defined />
  755. </C:comp-filter>
  756. </C:comp-filter>"""])
  757. assert "/calendar.ics/event1.ics" in self._test_filter(["""\
  758. <C:comp-filter name="VCALENDAR">
  759. <C:comp-filter name="VTODO">
  760. <C:is-not-defined />
  761. </C:comp-filter>
  762. </C:comp-filter>"""])
  763. def test_item_prop_filter(self) -> None:
  764. """Report request with prop-based filter on an item."""
  765. assert "/calendar.ics/event1.ics" in self._test_filter(["""\
  766. <C:comp-filter name="VCALENDAR">
  767. <C:comp-filter name="VEVENT">
  768. <C:prop-filter name="SUMMARY"></C:prop-filter>
  769. </C:comp-filter>
  770. </C:comp-filter>"""])
  771. assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
  772. <C:comp-filter name="VCALENDAR">
  773. <C:comp-filter name="VEVENT">
  774. <C:prop-filter name="UNKNOWN"></C:prop-filter>
  775. </C:comp-filter>
  776. </C:comp-filter>"""])
  777. def test_item_not_prop_filter(self) -> None:
  778. """Report request with prop-based is-not filter on an item."""
  779. assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
  780. <C:comp-filter name="VCALENDAR">
  781. <C:comp-filter name="VEVENT">
  782. <C:prop-filter name="SUMMARY">
  783. <C:is-not-defined />
  784. </C:prop-filter>
  785. </C:comp-filter>
  786. </C:comp-filter>"""])
  787. assert "/calendar.ics/event1.ics" in self._test_filter(["""\
  788. <C:comp-filter name="VCALENDAR">
  789. <C:comp-filter name="VEVENT">
  790. <C:prop-filter name="UNKNOWN">
  791. <C:is-not-defined />
  792. </C:prop-filter>
  793. </C:comp-filter>
  794. </C:comp-filter>"""])
  795. def test_mutiple_filters(self) -> None:
  796. """Report request with multiple filters on an item."""
  797. assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
  798. <C:comp-filter name="VCALENDAR">
  799. <C:comp-filter name="VEVENT">
  800. <C:prop-filter name="SUMMARY">
  801. <C:is-not-defined />
  802. </C:prop-filter>
  803. </C:comp-filter>
  804. </C:comp-filter>""", """
  805. <C:comp-filter name="VCALENDAR">
  806. <C:comp-filter name="VEVENT">
  807. <C:prop-filter name="UNKNOWN">
  808. <C:is-not-defined />
  809. </C:prop-filter>
  810. </C:comp-filter>
  811. </C:comp-filter>"""])
  812. assert "/calendar.ics/event1.ics" in self._test_filter(["""\
  813. <C:comp-filter name="VCALENDAR">
  814. <C:comp-filter name="VEVENT">
  815. <C:prop-filter name="SUMMARY"></C:prop-filter>
  816. </C:comp-filter>
  817. </C:comp-filter>""", """
  818. <C:comp-filter name="VCALENDAR">
  819. <C:comp-filter name="VEVENT">
  820. <C:prop-filter name="UNKNOWN">
  821. <C:is-not-defined />
  822. </C:prop-filter>
  823. </C:comp-filter>
  824. </C:comp-filter>"""])
  825. assert "/calendar.ics/event1.ics" in self._test_filter(["""\
  826. <C:comp-filter name="VCALENDAR">
  827. <C:comp-filter name="VEVENT">
  828. <C:prop-filter name="SUMMARY"></C:prop-filter>
  829. <C:prop-filter name="UNKNOWN">
  830. <C:is-not-defined />
  831. </C:prop-filter>
  832. </C:comp-filter>
  833. </C:comp-filter>"""])
  834. def test_text_match_filter(self) -> None:
  835. """Report request with text-match filter on calendar."""
  836. assert "/calendar.ics/event1.ics" in self._test_filter(["""\
  837. <C:comp-filter name="VCALENDAR">
  838. <C:comp-filter name="VEVENT">
  839. <C:prop-filter name="SUMMARY">
  840. <C:text-match>event</C:text-match>
  841. </C:prop-filter>
  842. </C:comp-filter>
  843. </C:comp-filter>"""])
  844. assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
  845. <C:comp-filter name="VCALENDAR">
  846. <C:comp-filter name="VEVENT">
  847. <C:prop-filter name="UNKNOWN">
  848. <C:text-match>event</C:text-match>
  849. </C:prop-filter>
  850. </C:comp-filter>
  851. </C:comp-filter>"""])
  852. assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
  853. <C:comp-filter name="VCALENDAR">
  854. <C:comp-filter name="VEVENT">
  855. <C:prop-filter name="SUMMARY">
  856. <C:text-match>unknown</C:text-match>
  857. </C:prop-filter>
  858. </C:comp-filter>
  859. </C:comp-filter>"""])
  860. assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
  861. <C:comp-filter name="VCALENDAR">
  862. <C:comp-filter name="VEVENT">
  863. <C:prop-filter name="SUMMARY">
  864. <C:text-match negate-condition="yes">event</C:text-match>
  865. </C:prop-filter>
  866. </C:comp-filter>
  867. </C:comp-filter>"""])
  868. def test_param_filter(self) -> None:
  869. """Report request with param-filter on calendar."""
  870. assert "/calendar.ics/event1.ics" in self._test_filter(["""\
  871. <C:comp-filter name="VCALENDAR">
  872. <C:comp-filter name="VEVENT">
  873. <C:prop-filter name="ATTENDEE">
  874. <C:param-filter name="PARTSTAT">
  875. <C:text-match collation="i;ascii-casemap"
  876. >ACCEPTED</C:text-match>
  877. </C:param-filter>
  878. </C:prop-filter>
  879. </C:comp-filter>
  880. </C:comp-filter>"""])
  881. assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
  882. <C:comp-filter name="VCALENDAR">
  883. <C:comp-filter name="VEVENT">
  884. <C:prop-filter name="ATTENDEE">
  885. <C:param-filter name="PARTSTAT">
  886. <C:text-match collation="i;ascii-casemap"
  887. >UNKNOWN</C:text-match>
  888. </C:param-filter>
  889. </C:prop-filter>
  890. </C:comp-filter>
  891. </C:comp-filter>"""])
  892. assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
  893. <C:comp-filter name="VCALENDAR">
  894. <C:comp-filter name="VEVENT">
  895. <C:prop-filter name="ATTENDEE">
  896. <C:param-filter name="PARTSTAT">
  897. <C:is-not-defined />
  898. </C:param-filter>
  899. </C:prop-filter>
  900. </C:comp-filter>
  901. </C:comp-filter>"""])
  902. assert "/calendar.ics/event1.ics" in self._test_filter(["""\
  903. <C:comp-filter name="VCALENDAR">
  904. <C:comp-filter name="VEVENT">
  905. <C:prop-filter name="ATTENDEE">
  906. <C:param-filter name="UNKNOWN">
  907. <C:is-not-defined />
  908. </C:param-filter>
  909. </C:prop-filter>
  910. </C:comp-filter>
  911. </C:comp-filter>"""])
  912. def test_time_range_filter_events(self) -> None:
  913. """Report request with time-range filter on events."""
  914. answer = self._test_filter(["""\
  915. <C:comp-filter name="VCALENDAR">
  916. <C:comp-filter name="VEVENT">
  917. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  918. </C:comp-filter>
  919. </C:comp-filter>"""], "event", items=range(1, 6))
  920. assert "/calendar.ics/event1.ics" in answer
  921. assert "/calendar.ics/event2.ics" in answer
  922. assert "/calendar.ics/event3.ics" in answer
  923. assert "/calendar.ics/event4.ics" in answer
  924. assert "/calendar.ics/event5.ics" in answer
  925. answer = self._test_filter(["""\
  926. <C:comp-filter name="VCALENDAR">
  927. <C:comp-filter name="VTODO">
  928. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  929. </C:comp-filter>
  930. </C:comp-filter>"""], "event", items=range(1, 6))
  931. assert "/calendar.ics/event1.ics" not in answer
  932. answer = self._test_filter(["""\
  933. <C:comp-filter name="VCALENDAR">
  934. <C:comp-filter name="VEVENT">
  935. <C:prop-filter name="ATTENDEE">
  936. <C:param-filter name="PARTSTAT">
  937. <C:is-not-defined />
  938. </C:param-filter>
  939. </C:prop-filter>
  940. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  941. </C:comp-filter>
  942. </C:comp-filter>"""], items=range(1, 6))
  943. assert "/calendar.ics/event1.ics" not in answer
  944. assert "/calendar.ics/event2.ics" not in answer
  945. assert "/calendar.ics/event3.ics" not in answer
  946. assert "/calendar.ics/event4.ics" not in answer
  947. assert "/calendar.ics/event5.ics" not in answer
  948. answer = self._test_filter(["""\
  949. <C:comp-filter name="VCALENDAR">
  950. <C:comp-filter name="VEVENT">
  951. <C:time-range start="20130902T000000Z" end="20131001T000000Z"/>
  952. </C:comp-filter>
  953. </C:comp-filter>"""], items=range(1, 6))
  954. assert "/calendar.ics/event1.ics" not in answer
  955. assert "/calendar.ics/event2.ics" in answer
  956. assert "/calendar.ics/event3.ics" in answer
  957. assert "/calendar.ics/event4.ics" in answer
  958. assert "/calendar.ics/event5.ics" in answer
  959. answer = self._test_filter(["""\
  960. <C:comp-filter name="VCALENDAR">
  961. <C:comp-filter name="VEVENT">
  962. <C:time-range start="20130903T000000Z" end="20130908T000000Z"/>
  963. </C:comp-filter>
  964. </C:comp-filter>"""], items=range(1, 6))
  965. assert "/calendar.ics/event1.ics" not in answer
  966. assert "/calendar.ics/event2.ics" not in answer
  967. assert "/calendar.ics/event3.ics" in answer
  968. assert "/calendar.ics/event4.ics" in answer
  969. assert "/calendar.ics/event5.ics" in answer
  970. answer = self._test_filter(["""\
  971. <C:comp-filter name="VCALENDAR">
  972. <C:comp-filter name="VEVENT">
  973. <C:time-range start="20130903T000000Z" end="20130904T000000Z"/>
  974. </C:comp-filter>
  975. </C:comp-filter>"""], items=range(1, 6))
  976. assert "/calendar.ics/event1.ics" not in answer
  977. assert "/calendar.ics/event2.ics" not in answer
  978. assert "/calendar.ics/event3.ics" in answer
  979. assert "/calendar.ics/event4.ics" not in answer
  980. assert "/calendar.ics/event5.ics" not in answer
  981. answer = self._test_filter(["""\
  982. <C:comp-filter name="VCALENDAR">
  983. <C:comp-filter name="VEVENT">
  984. <C:time-range start="20130805T000000Z" end="20130810T000000Z"/>
  985. </C:comp-filter>
  986. </C:comp-filter>"""], items=range(1, 6))
  987. assert "/calendar.ics/event1.ics" not in answer
  988. assert "/calendar.ics/event2.ics" not in answer
  989. assert "/calendar.ics/event3.ics" not in answer
  990. assert "/calendar.ics/event4.ics" not in answer
  991. assert "/calendar.ics/event5.ics" not in answer
  992. # HACK: VObject doesn't match RECURRENCE-ID to recurrences, the
  993. # overwritten recurrence is still used for filtering.
  994. answer = self._test_filter(["""\
  995. <C:comp-filter name="VCALENDAR">
  996. <C:comp-filter name="VEVENT">
  997. <C:time-range start="20170601T063000Z" end="20170601T070000Z"/>
  998. </C:comp-filter>
  999. </C:comp-filter>"""], items=(6, 7, 8, 9))
  1000. assert "/calendar.ics/event6.ics" in answer
  1001. assert "/calendar.ics/event7.ics" in answer
  1002. assert "/calendar.ics/event8.ics" in answer
  1003. assert "/calendar.ics/event9.ics" in answer
  1004. answer = self._test_filter(["""\
  1005. <C:comp-filter name="VCALENDAR">
  1006. <C:comp-filter name="VEVENT">
  1007. <C:time-range start="20170701T060000Z"/>
  1008. </C:comp-filter>
  1009. </C:comp-filter>"""], items=(6, 7, 8, 9))
  1010. assert "/calendar.ics/event6.ics" in answer
  1011. assert "/calendar.ics/event7.ics" in answer
  1012. assert "/calendar.ics/event8.ics" in answer
  1013. assert "/calendar.ics/event9.ics" not in answer
  1014. answer = self._test_filter(["""\
  1015. <C:comp-filter name="VCALENDAR">
  1016. <C:comp-filter name="VEVENT">
  1017. <C:time-range start="20170702T070000Z" end="20170704T060000Z"/>
  1018. </C:comp-filter>
  1019. </C:comp-filter>"""], items=(6, 7, 8, 9))
  1020. assert "/calendar.ics/event6.ics" not in answer
  1021. assert "/calendar.ics/event7.ics" not in answer
  1022. assert "/calendar.ics/event8.ics" not in answer
  1023. assert "/calendar.ics/event9.ics" not in answer
  1024. answer = self._test_filter(["""\
  1025. <C:comp-filter name="VCALENDAR">
  1026. <C:comp-filter name="VEVENT">
  1027. <C:time-range start="20170602T075959Z" end="20170602T080000Z"/>
  1028. </C:comp-filter>
  1029. </C:comp-filter>"""], items=(9,))
  1030. assert "/calendar.ics/event9.ics" in answer
  1031. answer = self._test_filter(["""\
  1032. <C:comp-filter name="VCALENDAR">
  1033. <C:comp-filter name="VEVENT">
  1034. <C:time-range start="20170602T080000Z" end="20170603T083000Z"/>
  1035. </C:comp-filter>
  1036. </C:comp-filter>"""], items=(9,))
  1037. assert "/calendar.ics/event9.ics" not in answer
  1038. def test_time_range_filter_events_rrule(self) -> None:
  1039. """Report request with time-range filter on events with rrules."""
  1040. answer = self._test_filter(["""\
  1041. <C:comp-filter name="VCALENDAR">
  1042. <C:comp-filter name="VEVENT">
  1043. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  1044. </C:comp-filter>
  1045. </C:comp-filter>"""], "event", items=(1, 2))
  1046. assert "/calendar.ics/event1.ics" in answer
  1047. assert "/calendar.ics/event2.ics" in answer
  1048. answer = self._test_filter(["""\
  1049. <C:comp-filter name="VCALENDAR">
  1050. <C:comp-filter name="VEVENT">
  1051. <C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
  1052. </C:comp-filter>
  1053. </C:comp-filter>"""], "event", items=(1, 2))
  1054. assert "/calendar.ics/event1.ics" not in answer
  1055. assert "/calendar.ics/event2.ics" in answer
  1056. answer = self._test_filter(["""\
  1057. <C:comp-filter name="VCALENDAR">
  1058. <C:comp-filter name="VEVENT">
  1059. <C:time-range start="20120801T000000Z" end="20121001T000000Z"/>
  1060. </C:comp-filter>
  1061. </C:comp-filter>"""], "event", items=(1, 2))
  1062. assert "/calendar.ics/event1.ics" not in answer
  1063. assert "/calendar.ics/event2.ics" not in answer
  1064. answer = self._test_filter(["""\
  1065. <C:comp-filter name="VCALENDAR">
  1066. <C:comp-filter name="VEVENT">
  1067. <C:time-range start="20130903T000000Z" end="20130907T000000Z"/>
  1068. </C:comp-filter>
  1069. </C:comp-filter>"""], "event", items=(1, 2))
  1070. assert "/calendar.ics/event1.ics" not in answer
  1071. assert "/calendar.ics/event2.ics" not in answer
  1072. def test_time_range_filter_todos(self) -> None:
  1073. """Report request with time-range filter on todos."""
  1074. answer = self._test_filter(["""\
  1075. <C:comp-filter name="VCALENDAR">
  1076. <C:comp-filter name="VTODO">
  1077. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  1078. </C:comp-filter>
  1079. </C:comp-filter>"""], "todo", items=range(1, 9))
  1080. assert "/calendar.ics/todo1.ics" in answer
  1081. assert "/calendar.ics/todo2.ics" in answer
  1082. assert "/calendar.ics/todo3.ics" in answer
  1083. assert "/calendar.ics/todo4.ics" in answer
  1084. assert "/calendar.ics/todo5.ics" in answer
  1085. assert "/calendar.ics/todo6.ics" in answer
  1086. assert "/calendar.ics/todo7.ics" in answer
  1087. assert "/calendar.ics/todo8.ics" in answer
  1088. answer = self._test_filter(["""\
  1089. <C:comp-filter name="VCALENDAR">
  1090. <C:comp-filter name="VTODO">
  1091. <C:time-range start="20130901T160000Z" end="20130901T183000Z"/>
  1092. </C:comp-filter>
  1093. </C:comp-filter>"""], "todo", items=range(1, 9))
  1094. assert "/calendar.ics/todo1.ics" not in answer
  1095. assert "/calendar.ics/todo2.ics" in answer
  1096. assert "/calendar.ics/todo3.ics" in answer
  1097. assert "/calendar.ics/todo4.ics" not in answer
  1098. assert "/calendar.ics/todo5.ics" not in answer
  1099. assert "/calendar.ics/todo6.ics" not in answer
  1100. assert "/calendar.ics/todo7.ics" in answer
  1101. assert "/calendar.ics/todo8.ics" in answer
  1102. answer = self._test_filter(["""\
  1103. <C:comp-filter name="VCALENDAR">
  1104. <C:comp-filter name="VTODO">
  1105. <C:time-range start="20130903T160000Z" end="20130901T183000Z"/>
  1106. </C:comp-filter>
  1107. </C:comp-filter>"""], "todo", items=range(1, 9))
  1108. assert "/calendar.ics/todo2.ics" not in answer
  1109. answer = self._test_filter(["""\
  1110. <C:comp-filter name="VCALENDAR">
  1111. <C:comp-filter name="VTODO">
  1112. <C:time-range start="20130903T160000Z" end="20130901T173000Z"/>
  1113. </C:comp-filter>
  1114. </C:comp-filter>"""], "todo", items=range(1, 9))
  1115. assert "/calendar.ics/todo2.ics" not in answer
  1116. answer = self._test_filter(["""\
  1117. <C:comp-filter name="VCALENDAR">
  1118. <C:comp-filter name="VTODO">
  1119. <C:time-range start="20130903T160000Z" end="20130903T173000Z"/>
  1120. </C:comp-filter>
  1121. </C:comp-filter>"""], "todo", items=range(1, 9))
  1122. assert "/calendar.ics/todo3.ics" not in answer
  1123. answer = self._test_filter(["""\
  1124. <C:comp-filter name="VCALENDAR">
  1125. <C:comp-filter name="VTODO">
  1126. <C:time-range start="20130903T160000Z" end="20130803T203000Z"/>
  1127. </C:comp-filter>
  1128. </C:comp-filter>"""], "todo", items=range(1, 9))
  1129. assert "/calendar.ics/todo7.ics" in answer
  1130. def test_time_range_filter_todos_rrule(self) -> None:
  1131. """Report request with time-range filter on todos with rrules."""
  1132. answer = self._test_filter(["""\
  1133. <C:comp-filter name="VCALENDAR">
  1134. <C:comp-filter name="VTODO">
  1135. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  1136. </C:comp-filter>
  1137. </C:comp-filter>"""], "todo", items=(1, 2, 9))
  1138. assert "/calendar.ics/todo1.ics" in answer
  1139. assert "/calendar.ics/todo2.ics" in answer
  1140. assert "/calendar.ics/todo9.ics" in answer
  1141. answer = self._test_filter(["""\
  1142. <C:comp-filter name="VCALENDAR">
  1143. <C:comp-filter name="VTODO">
  1144. <C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
  1145. </C:comp-filter>
  1146. </C:comp-filter>"""], "todo", items=(1, 2, 9))
  1147. assert "/calendar.ics/todo1.ics" not in answer
  1148. assert "/calendar.ics/todo2.ics" in answer
  1149. assert "/calendar.ics/todo9.ics" in answer
  1150. answer = self._test_filter(["""\
  1151. <C:comp-filter name="VCALENDAR">
  1152. <C:comp-filter name="VTODO">
  1153. <C:time-range start="20140902T000000Z" end="20140903T000000Z"/>
  1154. </C:comp-filter>
  1155. </C:comp-filter>"""], "todo", items=(1, 2))
  1156. assert "/calendar.ics/todo1.ics" not in answer
  1157. assert "/calendar.ics/todo2.ics" in answer
  1158. answer = self._test_filter(["""\
  1159. <C:comp-filter name="VCALENDAR">
  1160. <C:comp-filter name="VTODO">
  1161. <C:time-range start="20140904T000000Z" end="20140914T000000Z"/>
  1162. </C:comp-filter>
  1163. </C:comp-filter>"""], "todo", items=(1, 2))
  1164. assert "/calendar.ics/todo1.ics" not in answer
  1165. assert "/calendar.ics/todo2.ics" not in answer
  1166. answer = self._test_filter(["""\
  1167. <C:comp-filter name="VCALENDAR">
  1168. <C:comp-filter name="VTODO">
  1169. <C:time-range start="20130902T000000Z" end="20130906T235959Z"/>
  1170. </C:comp-filter>
  1171. </C:comp-filter>"""], "todo", items=(9,))
  1172. assert "/calendar.ics/todo9.ics" not in answer
  1173. def test_time_range_filter_journals(self) -> None:
  1174. """Report request with time-range filter on journals."""
  1175. answer = self._test_filter(["""\
  1176. <C:comp-filter name="VCALENDAR">
  1177. <C:comp-filter name="VJOURNAL">
  1178. <C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
  1179. </C:comp-filter>
  1180. </C:comp-filter>"""], "journal", items=(1, 2, 3))
  1181. assert "/calendar.ics/journal1.ics" not in answer
  1182. assert "/calendar.ics/journal2.ics" in answer
  1183. assert "/calendar.ics/journal3.ics" in answer
  1184. answer = self._test_filter(["""\
  1185. <C:comp-filter name="VCALENDAR">
  1186. <C:comp-filter name="VJOURNAL">
  1187. <C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
  1188. </C:comp-filter>
  1189. </C:comp-filter>"""], "journal", items=(1, 2, 3))
  1190. assert "/calendar.ics/journal1.ics" not in answer
  1191. assert "/calendar.ics/journal2.ics" in answer
  1192. assert "/calendar.ics/journal3.ics" in answer
  1193. answer = self._test_filter(["""\
  1194. <C:comp-filter name="VCALENDAR">
  1195. <C:comp-filter name="VJOURNAL">
  1196. <C:time-range start="19981229T000000Z" end="19991012T000000Z"/>
  1197. </C:comp-filter>
  1198. </C:comp-filter>"""], "journal", items=(1, 2, 3))
  1199. assert "/calendar.ics/journal1.ics" not in answer
  1200. assert "/calendar.ics/journal2.ics" not in answer
  1201. assert "/calendar.ics/journal3.ics" not in answer
  1202. answer = self._test_filter(["""\
  1203. <C:comp-filter name="VCALENDAR">
  1204. <C:comp-filter name="VJOURNAL">
  1205. <C:time-range start="20131229T000000Z" end="21520202T000000Z"/>
  1206. </C:comp-filter>
  1207. </C:comp-filter>"""], "journal", items=(1, 2, 3))
  1208. assert "/calendar.ics/journal1.ics" not in answer
  1209. assert "/calendar.ics/journal2.ics" in answer
  1210. assert "/calendar.ics/journal3.ics" not in answer
  1211. answer = self._test_filter(["""\
  1212. <C:comp-filter name="VCALENDAR">
  1213. <C:comp-filter name="VJOURNAL">
  1214. <C:time-range start="20000101T000000Z" end="20000202T000000Z"/>
  1215. </C:comp-filter>
  1216. </C:comp-filter>"""], "journal", items=(1, 2, 3))
  1217. assert "/calendar.ics/journal1.ics" not in answer
  1218. assert "/calendar.ics/journal2.ics" in answer
  1219. assert "/calendar.ics/journal3.ics" in answer
  1220. def test_time_range_filter_journals_rrule(self) -> None:
  1221. """Report request with time-range filter on journals with rrules."""
  1222. answer = self._test_filter(["""\
  1223. <C:comp-filter name="VCALENDAR">
  1224. <C:comp-filter name="VJOURNAL">
  1225. <C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
  1226. </C:comp-filter>
  1227. </C:comp-filter>"""], "journal", items=(1, 2))
  1228. assert "/calendar.ics/journal1.ics" not in answer
  1229. assert "/calendar.ics/journal2.ics" in answer
  1230. answer = self._test_filter(["""\
  1231. <C:comp-filter name="VCALENDAR">
  1232. <C:comp-filter name="VJOURNAL">
  1233. <C:time-range start="20051229T000000Z" end="20060202T000000Z"/>
  1234. </C:comp-filter>
  1235. </C:comp-filter>"""], "journal", items=(1, 2))
  1236. assert "/calendar.ics/journal1.ics" not in answer
  1237. assert "/calendar.ics/journal2.ics" in answer
  1238. answer = self._test_filter(["""\
  1239. <C:comp-filter name="VCALENDAR">
  1240. <C:comp-filter name="VJOURNAL">
  1241. <C:time-range start="20060102T000000Z" end="20060202T000000Z"/>
  1242. </C:comp-filter>
  1243. </C:comp-filter>"""], "journal", items=(1, 2))
  1244. assert "/calendar.ics/journal1.ics" not in answer
  1245. assert "/calendar.ics/journal2.ics" not in answer
  1246. def test_report_item(self) -> None:
  1247. """Test report request on an item"""
  1248. calendar_path = "/calendar.ics/"
  1249. self.mkcalendar(calendar_path)
  1250. event = get_file_content("event1.ics")
  1251. event_path = posixpath.join(calendar_path, "event.ics")
  1252. self.put(event_path, event)
  1253. _, responses = self.report(event_path, """\
  1254. <?xml version="1.0" encoding="utf-8" ?>
  1255. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  1256. <D:prop xmlns:D="DAV:">
  1257. <D:getetag />
  1258. </D:prop>
  1259. </C:calendar-query>""")
  1260. assert len(responses) == 1
  1261. response = responses[event_path]
  1262. assert not isinstance(response, int)
  1263. status, prop = response["D:getetag"]
  1264. assert status == 200 and prop.text
  1265. def _report_sync_token(
  1266. self, calendar_path: str, sync_token: Optional[str] = None
  1267. ) -> Tuple[str, RESPONSES]:
  1268. sync_token_xml = (
  1269. "<sync-token><![CDATA[%s]]></sync-token>" % sync_token
  1270. if sync_token else "<sync-token />")
  1271. status, _, answer = self.request("REPORT", calendar_path, """\
  1272. <?xml version="1.0" encoding="utf-8" ?>
  1273. <sync-collection xmlns="DAV:">
  1274. <prop>
  1275. <getetag />
  1276. </prop>
  1277. %s
  1278. </sync-collection>""" % sync_token_xml)
  1279. xml = DefusedET.fromstring(answer)
  1280. if status in (403, 409):
  1281. assert xml.tag == xmlutils.make_clark("D:error")
  1282. assert sync_token and xml.find(
  1283. xmlutils.make_clark("D:valid-sync-token")) is not None
  1284. return "", {}
  1285. assert status == 207
  1286. assert xml.tag == xmlutils.make_clark("D:multistatus")
  1287. sync_token = xml.find(xmlutils.make_clark("D:sync-token")).text.strip()
  1288. assert sync_token
  1289. responses = self.parse_responses(answer)
  1290. for href, response in responses.items():
  1291. if not isinstance(response, int):
  1292. status, prop = response["D:getetag"]
  1293. assert status == 200 and prop.text and len(response) == 1
  1294. responses[href] = response = 200
  1295. assert response in (200, 404)
  1296. return sync_token, responses
  1297. def test_report_sync_collection_no_change(self) -> None:
  1298. """Test sync-collection report without modifying the collection"""
  1299. calendar_path = "/calendar.ics/"
  1300. self.mkcalendar(calendar_path)
  1301. event = get_file_content("event1.ics")
  1302. event_path = posixpath.join(calendar_path, "event.ics")
  1303. self.put(event_path, event)
  1304. sync_token, responses = self._report_sync_token(calendar_path)
  1305. assert len(responses) == 1 and responses[event_path] == 200
  1306. new_sync_token, responses = self._report_sync_token(
  1307. calendar_path, sync_token)
  1308. if not self.full_sync_token_support and not new_sync_token:
  1309. return
  1310. assert sync_token == new_sync_token and len(responses) == 0
  1311. def test_report_sync_collection_add(self) -> None:
  1312. """Test sync-collection report with an added item"""
  1313. calendar_path = "/calendar.ics/"
  1314. self.mkcalendar(calendar_path)
  1315. sync_token, responses = self._report_sync_token(calendar_path)
  1316. assert len(responses) == 0
  1317. event = get_file_content("event1.ics")
  1318. event_path = posixpath.join(calendar_path, "event.ics")
  1319. self.put(event_path, event)
  1320. sync_token, responses = self._report_sync_token(
  1321. calendar_path, sync_token)
  1322. if not self.full_sync_token_support and not sync_token:
  1323. return
  1324. assert len(responses) == 1 and responses[event_path] == 200
  1325. def test_report_sync_collection_delete(self) -> None:
  1326. """Test sync-collection report with a deleted item"""
  1327. calendar_path = "/calendar.ics/"
  1328. self.mkcalendar(calendar_path)
  1329. event = get_file_content("event1.ics")
  1330. event_path = posixpath.join(calendar_path, "event.ics")
  1331. self.put(event_path, event)
  1332. sync_token, responses = self._report_sync_token(calendar_path)
  1333. assert len(responses) == 1 and responses[event_path] == 200
  1334. self.delete(event_path)
  1335. sync_token, responses = self._report_sync_token(
  1336. calendar_path, sync_token)
  1337. if not self.full_sync_token_support and not sync_token:
  1338. return
  1339. assert len(responses) == 1 and responses[event_path] == 404
  1340. def test_report_sync_collection_create_delete(self) -> None:
  1341. """Test sync-collection report with a created and deleted item"""
  1342. calendar_path = "/calendar.ics/"
  1343. self.mkcalendar(calendar_path)
  1344. sync_token, responses = self._report_sync_token(calendar_path)
  1345. assert len(responses) == 0
  1346. event = get_file_content("event1.ics")
  1347. event_path = posixpath.join(calendar_path, "event.ics")
  1348. self.put(event_path, event)
  1349. self.delete(event_path)
  1350. sync_token, responses = self._report_sync_token(
  1351. calendar_path, sync_token)
  1352. if not self.full_sync_token_support and not sync_token:
  1353. return
  1354. assert len(responses) == 1 and responses[event_path] == 404
  1355. def test_report_sync_collection_modify_undo(self) -> None:
  1356. """Test sync-collection report with a modified and changed back item"""
  1357. calendar_path = "/calendar.ics/"
  1358. self.mkcalendar(calendar_path)
  1359. event1 = get_file_content("event1.ics")
  1360. event2 = get_file_content("event1_modified.ics")
  1361. event_path = posixpath.join(calendar_path, "event.ics")
  1362. self.put(event_path, event1)
  1363. sync_token, responses = self._report_sync_token(calendar_path)
  1364. assert len(responses) == 1 and responses[event_path] == 200
  1365. self.put(event_path, event2)
  1366. self.put(event_path, event1)
  1367. sync_token, responses = self._report_sync_token(
  1368. calendar_path, sync_token)
  1369. if not self.full_sync_token_support and not sync_token:
  1370. return
  1371. assert len(responses) == 1 and responses[event_path] == 200
  1372. def test_report_sync_collection_move(self) -> None:
  1373. """Test sync-collection report a moved item"""
  1374. calendar_path = "/calendar.ics/"
  1375. self.mkcalendar(calendar_path)
  1376. event = get_file_content("event1.ics")
  1377. event1_path = posixpath.join(calendar_path, "event1.ics")
  1378. event2_path = posixpath.join(calendar_path, "event2.ics")
  1379. self.put(event1_path, event)
  1380. sync_token, responses = self._report_sync_token(calendar_path)
  1381. assert len(responses) == 1 and responses[event1_path] == 200
  1382. self.request("MOVE", event1_path, check=201,
  1383. HTTP_DESTINATION=event2_path, HTTP_HOST="")
  1384. sync_token, responses = self._report_sync_token(
  1385. calendar_path, sync_token)
  1386. if not self.full_sync_token_support and not sync_token:
  1387. return
  1388. assert len(responses) == 2 and (responses[event1_path] == 404 and
  1389. responses[event2_path] == 200)
  1390. def test_report_sync_collection_move_undo(self) -> None:
  1391. """Test sync-collection report with a moved and moved back item"""
  1392. calendar_path = "/calendar.ics/"
  1393. self.mkcalendar(calendar_path)
  1394. event = get_file_content("event1.ics")
  1395. event1_path = posixpath.join(calendar_path, "event1.ics")
  1396. event2_path = posixpath.join(calendar_path, "event2.ics")
  1397. self.put(event1_path, event)
  1398. sync_token, responses = self._report_sync_token(calendar_path)
  1399. assert len(responses) == 1 and responses[event1_path] == 200
  1400. self.request("MOVE", event1_path, check=201,
  1401. HTTP_DESTINATION=event2_path, HTTP_HOST="")
  1402. self.request("MOVE", event2_path, check=201,
  1403. HTTP_DESTINATION=event1_path, HTTP_HOST="")
  1404. sync_token, responses = self._report_sync_token(
  1405. calendar_path, sync_token)
  1406. if not self.full_sync_token_support and not sync_token:
  1407. return
  1408. assert len(responses) == 2 and (responses[event1_path] == 200 and
  1409. responses[event2_path] == 404)
  1410. def test_report_sync_collection_invalid_sync_token(self) -> None:
  1411. """Test sync-collection report with an invalid sync token"""
  1412. calendar_path = "/calendar.ics/"
  1413. self.mkcalendar(calendar_path)
  1414. sync_token, _ = self._report_sync_token(
  1415. calendar_path, "http://radicale.org/ns/sync/INVALID")
  1416. assert not sync_token
  1417. def test_propfind_sync_token(self) -> None:
  1418. """Retrieve the sync-token with a propfind request"""
  1419. calendar_path = "/calendar.ics/"
  1420. self.mkcalendar(calendar_path)
  1421. propfind = get_file_content("allprop.xml")
  1422. _, responses = self.propfind(calendar_path, propfind)
  1423. response = responses[calendar_path]
  1424. assert not isinstance(response, int)
  1425. status, sync_token = response["D:sync-token"]
  1426. assert status == 200 and sync_token.text
  1427. event = get_file_content("event1.ics")
  1428. event_path = posixpath.join(calendar_path, "event.ics")
  1429. self.put(event_path, event)
  1430. _, responses = self.propfind(calendar_path, propfind)
  1431. response = responses[calendar_path]
  1432. assert not isinstance(response, int)
  1433. status, new_sync_token = response["D:sync-token"]
  1434. assert status == 200 and new_sync_token.text
  1435. assert sync_token.text != new_sync_token.text
  1436. def test_propfind_same_as_sync_collection_sync_token(self) -> None:
  1437. """Compare sync-token property with sync-collection sync-token"""
  1438. calendar_path = "/calendar.ics/"
  1439. self.mkcalendar(calendar_path)
  1440. propfind = get_file_content("allprop.xml")
  1441. _, responses = self.propfind(calendar_path, propfind)
  1442. response = responses[calendar_path]
  1443. assert not isinstance(response, int)
  1444. status, sync_token = response["D:sync-token"]
  1445. assert status == 200 and sync_token.text
  1446. report_sync_token, _ = self._report_sync_token(calendar_path)
  1447. assert sync_token.text == report_sync_token
  1448. def test_calendar_getcontenttype(self) -> None:
  1449. """Test report request on an item"""
  1450. self.mkcalendar("/test/")
  1451. for component in ("event", "todo", "journal"):
  1452. event = get_file_content("%s1.ics" % component)
  1453. status, _ = self.delete("/test/test.ics", check=None)
  1454. assert status in (200, 404)
  1455. self.put("/test/test.ics", event)
  1456. _, responses = self.report("/test/", """\
  1457. <?xml version="1.0" encoding="utf-8" ?>
  1458. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  1459. <D:prop xmlns:D="DAV:">
  1460. <D:getcontenttype />
  1461. </D:prop>
  1462. </C:calendar-query>""")
  1463. assert len(responses) == 1
  1464. response = responses["/test/test.ics"]
  1465. assert not isinstance(response, int) and len(response) == 1
  1466. status, prop = response["D:getcontenttype"]
  1467. assert status == 200 and prop.text == (
  1468. "text/calendar;charset=utf-8;component=V%s" %
  1469. component.upper())
  1470. def test_addressbook_getcontenttype(self) -> None:
  1471. """Test report request on an item"""
  1472. self.create_addressbook("/test/")
  1473. contact = get_file_content("contact1.vcf")
  1474. self.put("/test/test.vcf", contact)
  1475. _, responses = self.report("/test/", """\
  1476. <?xml version="1.0" encoding="utf-8" ?>
  1477. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  1478. <D:prop xmlns:D="DAV:">
  1479. <D:getcontenttype />
  1480. </D:prop>
  1481. </C:calendar-query>""")
  1482. assert len(responses) == 1
  1483. response = responses["/test/test.vcf"]
  1484. assert not isinstance(response, int) and len(response) == 1
  1485. status, prop = response["D:getcontenttype"]
  1486. assert status == 200 and prop.text == "text/vcard;charset=utf-8"
  1487. def test_authorization(self) -> None:
  1488. _, responses = self.propfind("/", """\
  1489. <?xml version="1.0" encoding="utf-8"?>
  1490. <propfind xmlns="DAV:">
  1491. <prop>
  1492. <current-user-principal />
  1493. </prop>
  1494. </propfind>""", login="user:")
  1495. response = responses["/"]
  1496. assert not isinstance(response, int) and len(response) == 1
  1497. status, prop = response["D:current-user-principal"]
  1498. assert status == 200 and len(prop) == 1
  1499. element = prop.find(xmlutils.make_clark("D:href"))
  1500. assert element is not None and element.text == "/user/"
  1501. def test_authentication(self) -> None:
  1502. """Test if server sends authentication request."""
  1503. self.configure({"auth": {"type": "htpasswd",
  1504. "htpasswd_filename": os.devnull,
  1505. "htpasswd_encryption": "plain"},
  1506. "rights": {"type": "owner_only"}})
  1507. status, headers, _ = self.request("MKCOL", "/user/")
  1508. assert status in (401, 403)
  1509. assert headers.get("WWW-Authenticate")
  1510. def test_principal_collection_creation(self) -> None:
  1511. """Verify existence of the principal collection."""
  1512. self.propfind("/user/", login="user:")
  1513. def test_authentication_current_user_principal_hack(self) -> None:
  1514. """Test if server sends authentication request when accessing
  1515. current-user-principal prop (workaround for DAVx5)."""
  1516. status, headers, _ = self.request("PROPFIND", "/", """\
  1517. <?xml version="1.0" encoding="utf-8"?>
  1518. <propfind xmlns="DAV:">
  1519. <prop>
  1520. <current-user-principal />
  1521. </prop>
  1522. </propfind>""")
  1523. assert status in (401, 403)
  1524. assert headers.get("WWW-Authenticate")
  1525. def test_existence_of_root_collections(self) -> None:
  1526. """Verify that the root collection always exists."""
  1527. # Use PROPFIND because GET returns message
  1528. self.propfind("/")
  1529. # it should still exist after deletion
  1530. self.delete("/")
  1531. self.propfind("/")
  1532. def test_well_known(self) -> None:
  1533. for path in ["/.well-known/caldav", "/.well-known/carddav"]:
  1534. for path in [path, "/foo" + path]:
  1535. _, headers, _ = self.request("GET", path, check=301)
  1536. assert headers.get("Location") == "/"
  1537. def test_well_known_script_name(self) -> None:
  1538. for path in ["/.well-known/caldav", "/.well-known/carddav"]:
  1539. for path in [path, "/foo" + path]:
  1540. _, headers, _ = self.request(
  1541. "GET", path, check=301, SCRIPT_NAME="/radicale")
  1542. assert headers.get("Location") == "/radicale/"
  1543. def test_well_known_not_found(self) -> None:
  1544. for path in ["/.well-known", "/.well-known/", "/.well-known/foo"]:
  1545. for path in [path, "/foo" + path]:
  1546. self.get(path, check=404)
  1547. def test_custom_headers(self) -> None:
  1548. self.configure({"headers": {"test": "123"}})
  1549. # Test if header is set on success
  1550. _, headers, _ = self.request("OPTIONS", "/", check=200)
  1551. assert headers.get("test") == "123"
  1552. # Test if header is set on failure
  1553. _, headers, _ = self.request("GET", "/.well-known/foo", check=404)
  1554. assert headers.get("test") == "123"
  1555. def test_timezone_seconds(self) -> None:
  1556. """Verify that timezones with minutes and seconds work."""
  1557. self.mkcalendar("/calendar.ics/")
  1558. event = get_file_content("event_timezone_seconds.ics")
  1559. self.put("/calendar.ics/event.ics", event)