test_base.py 69 KB

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