test_base.py 73 KB

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