test_base.py 76 KB

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