test_base.py 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2012-2017 Guillaume Ayoub
  3. #
  4. # This library is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This library is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. Radicale tests with simple requests.
  18. """
  19. import base64
  20. import os
  21. import posixpath
  22. import shutil
  23. import tempfile
  24. import xml.etree.ElementTree as ET
  25. import pytest
  26. from radicale import Application, config
  27. from . import BaseTest
  28. from .helpers import get_file_content
  29. class BaseRequestsMixIn:
  30. """Tests with simple requests."""
  31. def test_root(self):
  32. """GET request at "/"."""
  33. status, _, answer = self.request("GET", "/")
  34. assert status == 302
  35. assert answer == "Redirected to .web"
  36. def test_script_name(self):
  37. """GET request at "/" with SCRIPT_NAME."""
  38. status, _, answer = self.request("GET", "/", SCRIPT_NAME="/radicale")
  39. assert status == 302
  40. assert answer == "Redirected to .web"
  41. status, _, answer = self.request("GET", "", SCRIPT_NAME="/radicale")
  42. assert status == 302
  43. assert answer == "Redirected to radicale/.web"
  44. def test_add_event(self):
  45. """Add an event."""
  46. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  47. assert status == 201
  48. event = get_file_content("event1.ics")
  49. path = "/calendar.ics/event1.ics"
  50. status, _, _ = self.request("PUT", path, event)
  51. assert status == 201
  52. status, headers, answer = self.request("GET", path)
  53. assert status == 200
  54. assert "ETag" in headers
  55. assert "VEVENT" in answer
  56. assert "Event" in answer
  57. assert "UID:event" in answer
  58. def test_add_event_without_uid(self):
  59. """Add an event without UID."""
  60. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  61. assert status == 201
  62. event = get_file_content("event1.ics").replace("UID:event1\n", "")
  63. assert "\nUID:" not in event
  64. path = "/calendar.ics/event.ics"
  65. status, _, _ = self.request("PUT", path, event)
  66. assert status == 201
  67. status, _, answer = self.request("GET", path)
  68. assert status == 200
  69. uids = []
  70. for line in answer.split("\r\n"):
  71. if line.startswith("UID:"):
  72. uids.append(line[len("UID:"):])
  73. assert len(uids) == 1 and uids[0]
  74. # Overwrite the event with an event without UID and check that the UID
  75. # is still the same
  76. status, _, _ = self.request("PUT", path, event)
  77. assert status == 201
  78. status, _, answer = self.request("GET", path)
  79. assert status == 200
  80. assert "\r\nUID:%s\r\n" % uids[0] in answer
  81. def test_add_todo(self):
  82. """Add a todo."""
  83. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  84. assert status == 201
  85. todo = get_file_content("todo1.ics")
  86. path = "/calendar.ics/todo1.ics"
  87. status, _, _ = self.request("PUT", path, todo)
  88. assert status == 201
  89. status, headers, answer = self.request("GET", path)
  90. assert status == 200
  91. assert "ETag" in headers
  92. assert "VTODO" in answer
  93. assert "Todo" in answer
  94. assert "UID:todo" in answer
  95. def _create_addressbook(self, path):
  96. return self.request(
  97. "MKCOL", path, """\
  98. <?xml version="1.0" encoding="UTF-8" ?>
  99. <create xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
  100. <set>
  101. <prop>
  102. <resourcetype>
  103. <collection />
  104. <CR:addressbook />
  105. </resourcetype>
  106. </prop>
  107. </set>
  108. </create>""")
  109. def test_add_contact(self):
  110. """Add a contact."""
  111. status, _, _ = self._create_addressbook("/contacts.vcf/")
  112. assert status == 201
  113. contact = get_file_content("contact1.vcf")
  114. path = "/contacts.vcf/contact.vcf"
  115. status, _, _ = self.request("PUT", path, contact)
  116. assert status == 201
  117. status, headers, answer = self.request("GET", path)
  118. assert status == 200
  119. assert "ETag" in headers
  120. assert "VCARD" in answer
  121. assert "UID:contact1" in answer
  122. status, _, answer = self.request("GET", path)
  123. assert status == 200
  124. assert "UID:contact1" in answer
  125. def test_add_contact_without_uid(self):
  126. """Add a contact."""
  127. status, _, _ = self._create_addressbook("/contacts.vcf/")
  128. assert status == 201
  129. contact = get_file_content("contact1.vcf").replace("UID:contact1\n",
  130. "")
  131. assert "\nUID" not in contact
  132. path = "/contacts.vcf/contact.vcf"
  133. status, _, _ = self.request("PUT", path, contact)
  134. assert status == 201
  135. status, _, answer = self.request("GET", path)
  136. assert status == 200
  137. uids = []
  138. for line in answer.split("\r\n"):
  139. if line.startswith("UID:"):
  140. uids.append(line[len("UID:"):])
  141. assert len(uids) == 1 and uids[0]
  142. # Overwrite the contact with an contact without UID and check that the
  143. # UID is still the same
  144. status, _, _ = self.request("PUT", path, contact)
  145. assert status == 201
  146. status, _, answer = self.request("GET", path)
  147. assert status == 200
  148. assert "\r\nUID:%s\r\n" % uids[0] in answer
  149. def test_update(self):
  150. """Update an event."""
  151. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  152. assert status == 201
  153. event = get_file_content("event1.ics")
  154. path = "/calendar.ics/event1.ics"
  155. status, _, _ = self.request("PUT", path, event)
  156. assert status == 201
  157. status, headers, answer = self.request("GET", path)
  158. assert "ETag" in headers
  159. assert status == 200
  160. assert "VEVENT" in answer
  161. assert "Event" in answer
  162. assert "UID:event" in answer
  163. assert "DTSTART;TZID=Europe/Paris:20130901T180000" in answer
  164. assert "DTEND;TZID=Europe/Paris:20130901T190000" in answer
  165. # Then we send another PUT request
  166. event = get_file_content("event1-prime.ics")
  167. status, _, _ = self.request("PUT", path, event)
  168. assert status == 201
  169. status, _, answer = self.request("GET", "/calendar.ics/")
  170. assert status == 200
  171. assert answer.count("BEGIN:VEVENT") == 1
  172. status, headers, answer = self.request("GET", path)
  173. assert status == 200
  174. assert "ETag" in headers
  175. assert "VEVENT" in answer
  176. assert "Event" in answer
  177. assert "UID:event" in answer
  178. assert "DTSTART;TZID=Europe/Paris:20130901T180000" not in answer
  179. assert "DTEND;TZID=Europe/Paris:20130901T190000" not in answer
  180. assert "DTSTART;TZID=Europe/Paris:20140901T180000" in answer
  181. assert "DTEND;TZID=Europe/Paris:20140901T210000" in answer
  182. def test_put_whole_calendar(self):
  183. """Create and overwrite a whole calendar."""
  184. status, _, _ = self.request(
  185. "PUT", "/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
  186. assert status == 201
  187. event1 = get_file_content("event1.ics")
  188. status, _, _ = self.request(
  189. "PUT", "/calendar.ics/test_event.ics", event1)
  190. assert status == 201
  191. # Overwrite
  192. events = get_file_content("event_multiple.ics")
  193. status, _, _ = self.request("PUT", "/calendar.ics/", events)
  194. assert status == 201
  195. status, _, _ = self.request("GET", "/calendar.ics/test_event.ics")
  196. assert status == 404
  197. status, _, answer = self.request("GET", "/calendar.ics/")
  198. assert status == 200
  199. assert "\r\nUID:event\r\n" in answer and "\r\nUID:todo\r\n" in answer
  200. assert "\r\nUID:event1\r\n" not in answer
  201. def test_put_whole_calendar_without_uids(self):
  202. """Create a whole calendar without UID."""
  203. event = get_file_content("event_multiple.ics")
  204. event = event.replace("UID:event\n", "").replace("UID:todo\n", "")
  205. assert "\nUID:" not in event
  206. status, _, _ = self.request("PUT", "/calendar.ics/", event)
  207. assert status == 201
  208. status, _, answer = self.request("GET", "/calendar.ics")
  209. assert status == 200
  210. uids = []
  211. for line in answer.split("\r\n"):
  212. if line.startswith("UID:"):
  213. uids.append(line[len("UID:"):])
  214. assert len(uids) == 2
  215. for i, uid1 in enumerate(uids):
  216. assert uid1
  217. for uid2 in uids[i + 1:]:
  218. assert uid1 != uid2
  219. def test_put_whole_addressbook(self):
  220. """Create and overwrite a whole addressbook."""
  221. contacts = get_file_content("contact_multiple.vcf")
  222. status, _, _ = self.request("PUT", "/contacts.vcf/", contacts)
  223. assert status == 201
  224. status, _, answer = self.request("GET", "/contacts.vcf/")
  225. assert status == 200
  226. assert ("\r\nUID:contact1\r\n" in answer and
  227. "\r\nUID:contact2\r\n" in answer)
  228. def test_put_whole_addressbook_without_uids(self):
  229. """Create a whole addressbook without UID."""
  230. contacts = get_file_content("contact_multiple.vcf")
  231. contacts = contacts.replace("UID:contact1\n", "").replace(
  232. "UID:contact2\n", "")
  233. assert "\nUID:" not in contacts
  234. status, _, _ = self.request("PUT", "/contacts.vcf/", contacts)
  235. assert status == 201
  236. status, _, answer = self.request("GET", "/contacts.vcf")
  237. assert status == 200
  238. uids = []
  239. for line in answer.split("\r\n"):
  240. if line.startswith("UID:"):
  241. uids.append(line[len("UID:"):])
  242. assert len(uids) == 2
  243. for i, uid1 in enumerate(uids):
  244. assert uid1
  245. for uid2 in uids[i + 1:]:
  246. assert uid1 != uid2
  247. def test_delete(self):
  248. """Delete an event."""
  249. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  250. assert status == 201
  251. event = get_file_content("event1.ics")
  252. path = "/calendar.ics/event1.ics"
  253. status, _, _ = self.request("PUT", path, event)
  254. assert status == 201
  255. # Then we send a DELETE request
  256. status, _, answer = self.request("DELETE", path)
  257. assert status == 200
  258. assert "href>%s</" % path in answer
  259. status, _, answer = self.request("GET", "/calendar.ics/")
  260. assert status == 200
  261. assert "VEVENT" not in answer
  262. def test_mkcalendar(self):
  263. """Make a calendar."""
  264. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  265. assert status == 201
  266. status, _, answer = self.request("GET", "/calendar.ics/")
  267. assert status == 200
  268. assert "BEGIN:VCALENDAR" in answer
  269. assert "END:VCALENDAR" in answer
  270. def test_move(self):
  271. """Move a item."""
  272. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  273. assert status == 201
  274. event = get_file_content("event1.ics")
  275. path1 = "/calendar.ics/event1.ics"
  276. path2 = "/calendar.ics/event2.ics"
  277. status, _, _ = self.request("PUT", path1, event)
  278. assert status == 201
  279. status, _, _ = self.request(
  280. "MOVE", path1, HTTP_DESTINATION=path2, HTTP_HOST="")
  281. assert status == 201
  282. status, _, _ = self.request("GET", path1)
  283. assert status == 404
  284. status, _, _ = self.request("GET", path2)
  285. assert status == 200
  286. def test_head(self):
  287. status, _, _ = self.request("HEAD", "/")
  288. assert status == 302
  289. def test_options(self):
  290. status, headers, _ = self.request("OPTIONS", "/")
  291. assert status == 200
  292. assert "DAV" in headers
  293. def test_delete_collection(self):
  294. """Delete a collection."""
  295. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  296. assert status == 201
  297. event = get_file_content("event1.ics")
  298. self.request("PUT", "/calendar.ics/event1.ics", event)
  299. status, _, answer = self.request("DELETE", "/calendar.ics/")
  300. assert status == 200
  301. assert "href>/calendar.ics/</" in answer
  302. status, _, _ = self.request("GET", "/calendar.ics/")
  303. assert status == 404
  304. def test_delete_root_collection(self):
  305. """Delete the root collection."""
  306. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  307. assert status == 201
  308. event = get_file_content("event1.ics")
  309. status, _, _ = self.request("PUT", "/event1.ics", event)
  310. assert status == 201
  311. status, _, _ = self.request("PUT", "/calendar.ics/event1.ics", event)
  312. assert status == 201
  313. status, _, answer = self.request("DELETE", "/")
  314. assert status == 200
  315. assert "href>/</" in answer
  316. status, _, _ = self.request("GET", "/calendar.ics/")
  317. assert status == 404
  318. status, _, _ = self.request("GET", "/event1.ics")
  319. assert status == 404
  320. def test_propfind(self):
  321. calendar_path = "/calendar.ics/"
  322. status, _, _ = self.request("MKCALENDAR", calendar_path)
  323. assert status == 201
  324. event = get_file_content("event1.ics")
  325. event_path = posixpath.join(calendar_path, "event.ics")
  326. status, _, _ = self.request("PUT", event_path, event)
  327. assert status == 201
  328. status, _, answer = self.request("PROPFIND", "/", HTTP_DEPTH="1")
  329. assert status == 207
  330. assert "href>/</" in answer
  331. assert "href>%s</" % calendar_path in answer
  332. status, _, answer = self.request(
  333. "PROPFIND", calendar_path, HTTP_DEPTH="1")
  334. assert status == 207
  335. assert "href>%s</" % calendar_path in answer
  336. assert "href>%s</" % event_path in answer
  337. def test_proppatch(self):
  338. """Write a property and read it back."""
  339. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  340. assert status == 201
  341. proppatch = get_file_content("proppatch1.xml")
  342. status, _, answer = self.request(
  343. "PROPPATCH", "/calendar.ics/", proppatch)
  344. assert status == 207
  345. assert "calendar-color" in answer
  346. assert "200 OK</status" in answer
  347. # Read property back
  348. propfind = get_file_content("propfind1.xml")
  349. status, _, answer = self.request(
  350. "PROPFIND", "/calendar.ics/", propfind)
  351. assert status == 207
  352. assert ":calendar-color>#BADA55</" in answer
  353. assert "200 OK</status" in answer
  354. def test_put_whole_calendar_multiple_events_with_same_uid(self):
  355. """Add two events with the same UID."""
  356. status, _, _ = self.request(
  357. "PUT", "/calendar.ics/", get_file_content("event2.ics"))
  358. assert status == 201
  359. status, _, answer = self.request(
  360. "REPORT", "/calendar.ics/",
  361. """<?xml version="1.0" encoding="utf-8" ?>
  362. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  363. <D:prop xmlns:D="DAV:"><D:getetag/></D:prop>
  364. </C:calendar-query>""")
  365. assert status == 207
  366. assert answer.count("<getetag>") == 1
  367. status, _, answer = self.request("GET", "/calendar.ics/")
  368. assert status == 200
  369. assert answer.count("BEGIN:VEVENT") == 2
  370. def _test_filter(self, filters, kind="event", items=(1,)):
  371. filters_text = "".join(
  372. "<C:filter>%s</C:filter>" % filter_ for filter_ in filters)
  373. status, _, _ = self.request("DELETE", "/calendar.ics/")
  374. assert status in (200, 404)
  375. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  376. assert status == 201
  377. for i in items:
  378. filename = "{}{}.ics".format(kind, i)
  379. event = get_file_content(filename)
  380. status, _, _ = self.request(
  381. "PUT", "/calendar.ics/%s" % filename, event)
  382. assert status == 201
  383. status, _, answer = self.request(
  384. "REPORT", "/calendar.ics",
  385. """<?xml version="1.0" encoding="utf-8" ?>
  386. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  387. <D:prop xmlns:D="DAV:">
  388. <D:getetag/>
  389. </D:prop>
  390. %s
  391. </C:calendar-query>""" % filters_text)
  392. assert status == 207
  393. return answer
  394. def test_calendar_empty_filter(self):
  395. self._test_filter([""])
  396. def test_calendar_tag_filter(self):
  397. """Report request with tag-based filter on calendar."""
  398. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  399. <C:comp-filter name="VCALENDAR"></C:comp-filter>"""])
  400. def test_item_tag_filter(self):
  401. """Report request with tag-based filter on an item."""
  402. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  403. <C:comp-filter name="VCALENDAR">
  404. <C:comp-filter name="VEVENT"></C:comp-filter>
  405. </C:comp-filter>"""])
  406. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  407. <C:comp-filter name="VCALENDAR">
  408. <C:comp-filter name="VTODO"></C:comp-filter>
  409. </C:comp-filter>"""])
  410. def test_item_not_tag_filter(self):
  411. """Report request with tag-based is-not filter on an item."""
  412. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  413. <C:comp-filter name="VCALENDAR">
  414. <C:comp-filter name="VEVENT">
  415. <C:is-not-defined />
  416. </C:comp-filter>
  417. </C:comp-filter>"""])
  418. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  419. <C:comp-filter name="VCALENDAR">
  420. <C:comp-filter name="VTODO">
  421. <C:is-not-defined />
  422. </C:comp-filter>
  423. </C:comp-filter>"""])
  424. def test_item_prop_filter(self):
  425. """Report request with prop-based filter on an item."""
  426. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  427. <C:comp-filter name="VCALENDAR">
  428. <C:comp-filter name="VEVENT">
  429. <C:prop-filter name="SUMMARY"></C:prop-filter>
  430. </C:comp-filter>
  431. </C:comp-filter>"""])
  432. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  433. <C:comp-filter name="VCALENDAR">
  434. <C:comp-filter name="VEVENT">
  435. <C:prop-filter name="UNKNOWN"></C:prop-filter>
  436. </C:comp-filter>
  437. </C:comp-filter>"""])
  438. def test_item_not_prop_filter(self):
  439. """Report request with prop-based is-not filter on an item."""
  440. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  441. <C:comp-filter name="VCALENDAR">
  442. <C:comp-filter name="VEVENT">
  443. <C:prop-filter name="SUMMARY">
  444. <C:is-not-defined />
  445. </C:prop-filter>
  446. </C:comp-filter>
  447. </C:comp-filter>"""])
  448. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  449. <C:comp-filter name="VCALENDAR">
  450. <C:comp-filter name="VEVENT">
  451. <C:prop-filter name="UNKNOWN">
  452. <C:is-not-defined />
  453. </C:prop-filter>
  454. </C:comp-filter>
  455. </C:comp-filter>"""])
  456. def test_mutiple_filters(self):
  457. """Report request with multiple filters on an item."""
  458. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  459. <C:comp-filter name="VCALENDAR">
  460. <C:comp-filter name="VEVENT">
  461. <C:prop-filter name="SUMMARY">
  462. <C:is-not-defined />
  463. </C:prop-filter>
  464. </C:comp-filter>
  465. </C:comp-filter>""", """
  466. <C:comp-filter name="VCALENDAR">
  467. <C:comp-filter name="VEVENT">
  468. <C:prop-filter name="UNKNOWN">
  469. <C:is-not-defined />
  470. </C:prop-filter>
  471. </C:comp-filter>
  472. </C:comp-filter>"""])
  473. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  474. <C:comp-filter name="VCALENDAR">
  475. <C:comp-filter name="VEVENT">
  476. <C:prop-filter name="SUMMARY"></C:prop-filter>
  477. </C:comp-filter>
  478. </C:comp-filter>""", """
  479. <C:comp-filter name="VCALENDAR">
  480. <C:comp-filter name="VEVENT">
  481. <C:prop-filter name="UNKNOWN">
  482. <C:is-not-defined />
  483. </C:prop-filter>
  484. </C:comp-filter>
  485. </C:comp-filter>"""])
  486. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  487. <C:comp-filter name="VCALENDAR">
  488. <C:comp-filter name="VEVENT">
  489. <C:prop-filter name="SUMMARY"></C:prop-filter>
  490. <C:prop-filter name="UNKNOWN">
  491. <C:is-not-defined />
  492. </C:prop-filter>
  493. </C:comp-filter>
  494. </C:comp-filter>"""])
  495. def test_text_match_filter(self):
  496. """Report request with text-match filter on calendar."""
  497. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  498. <C:comp-filter name="VCALENDAR">
  499. <C:comp-filter name="VEVENT">
  500. <C:prop-filter name="SUMMARY">
  501. <C:text-match>event</C:text-match>
  502. </C:prop-filter>
  503. </C:comp-filter>
  504. </C:comp-filter>"""])
  505. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  506. <C:comp-filter name="VCALENDAR">
  507. <C:comp-filter name="VEVENT">
  508. <C:prop-filter name="UNKNOWN">
  509. <C:text-match>event</C:text-match>
  510. </C:prop-filter>
  511. </C:comp-filter>
  512. </C:comp-filter>"""])
  513. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  514. <C:comp-filter name="VCALENDAR">
  515. <C:comp-filter name="VEVENT">
  516. <C:prop-filter name="SUMMARY">
  517. <C:text-match>unknown</C:text-match>
  518. </C:prop-filter>
  519. </C:comp-filter>
  520. </C:comp-filter>"""])
  521. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  522. <C:comp-filter name="VCALENDAR">
  523. <C:comp-filter name="VEVENT">
  524. <C:prop-filter name="SUMMARY">
  525. <C:text-match negate-condition="yes">event</C:text-match>
  526. </C:prop-filter>
  527. </C:comp-filter>
  528. </C:comp-filter>"""])
  529. def test_param_filter(self):
  530. """Report request with param-filter on calendar."""
  531. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  532. <C:comp-filter name="VCALENDAR">
  533. <C:comp-filter name="VEVENT">
  534. <C:prop-filter name="ATTENDEE">
  535. <C:param-filter name="PARTSTAT">
  536. <C:text-match collation="i;ascii-casemap"
  537. >ACCEPTED</C:text-match>
  538. </C:param-filter>
  539. </C:prop-filter>
  540. </C:comp-filter>
  541. </C:comp-filter>"""])
  542. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  543. <C:comp-filter name="VCALENDAR">
  544. <C:comp-filter name="VEVENT">
  545. <C:prop-filter name="ATTENDEE">
  546. <C:param-filter name="PARTSTAT">
  547. <C:text-match collation="i;ascii-casemap"
  548. >UNKNOWN</C:text-match>
  549. </C:param-filter>
  550. </C:prop-filter>
  551. </C:comp-filter>
  552. </C:comp-filter>"""])
  553. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  554. <C:comp-filter name="VCALENDAR">
  555. <C:comp-filter name="VEVENT">
  556. <C:prop-filter name="ATTENDEE">
  557. <C:param-filter name="PARTSTAT">
  558. <C:is-not-defined />
  559. </C:param-filter>
  560. </C:prop-filter>
  561. </C:comp-filter>
  562. </C:comp-filter>"""])
  563. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  564. <C:comp-filter name="VCALENDAR">
  565. <C:comp-filter name="VEVENT">
  566. <C:prop-filter name="ATTENDEE">
  567. <C:param-filter name="UNKNOWN">
  568. <C:is-not-defined />
  569. </C:param-filter>
  570. </C:prop-filter>
  571. </C:comp-filter>
  572. </C:comp-filter>"""])
  573. def test_time_range_filter_events(self):
  574. """Report request with time-range filter on events."""
  575. answer = self._test_filter(["""
  576. <C:comp-filter name="VCALENDAR">
  577. <C:comp-filter name="VEVENT">
  578. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  579. </C:comp-filter>
  580. </C:comp-filter>"""], "event", items=range(1, 6))
  581. assert "href>/calendar.ics/event1.ics</" in answer
  582. assert "href>/calendar.ics/event2.ics</" in answer
  583. assert "href>/calendar.ics/event3.ics</" in answer
  584. assert "href>/calendar.ics/event4.ics</" in answer
  585. assert "href>/calendar.ics/event5.ics</" in answer
  586. answer = self._test_filter(["""
  587. <C:comp-filter name="VCALENDAR">
  588. <C:comp-filter name="VEVENT">
  589. <C:prop-filter name="ATTENDEE">
  590. <C:param-filter name="PARTSTAT">
  591. <C:is-not-defined />
  592. </C:param-filter>
  593. </C:prop-filter>
  594. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  595. </C:comp-filter>
  596. </C:comp-filter>"""], items=range(1, 6))
  597. assert "href>/calendar.ics/event1.ics</" not in answer
  598. assert "href>/calendar.ics/event2.ics</" not in answer
  599. assert "href>/calendar.ics/event3.ics</" not in answer
  600. assert "href>/calendar.ics/event4.ics</" not in answer
  601. assert "href>/calendar.ics/event5.ics</" not in answer
  602. answer = self._test_filter(["""
  603. <C:comp-filter name="VCALENDAR">
  604. <C:comp-filter name="VEVENT">
  605. <C:time-range start="20130902T000000Z" end="20131001T000000Z"/>
  606. </C:comp-filter>
  607. </C:comp-filter>"""], items=range(1, 6))
  608. assert "href>/calendar.ics/event1.ics</" not in answer
  609. assert "href>/calendar.ics/event2.ics</" in answer
  610. assert "href>/calendar.ics/event3.ics</" in answer
  611. assert "href>/calendar.ics/event4.ics</" in answer
  612. assert "href>/calendar.ics/event5.ics</" in answer
  613. answer = self._test_filter(["""
  614. <C:comp-filter name="VCALENDAR">
  615. <C:comp-filter name="VEVENT">
  616. <C:time-range start="20130903T000000Z" end="20130908T000000Z"/>
  617. </C:comp-filter>
  618. </C:comp-filter>"""], items=range(1, 6))
  619. assert "href>/calendar.ics/event1.ics</" not in answer
  620. assert "href>/calendar.ics/event2.ics</" not in answer
  621. assert "href>/calendar.ics/event3.ics</" in answer
  622. assert "href>/calendar.ics/event4.ics</" in answer
  623. assert "href>/calendar.ics/event5.ics</" in answer
  624. answer = self._test_filter(["""
  625. <C:comp-filter name="VCALENDAR">
  626. <C:comp-filter name="VEVENT">
  627. <C:time-range start="20130903T000000Z" end="20130904T000000Z"/>
  628. </C:comp-filter>
  629. </C:comp-filter>"""], items=range(1, 6))
  630. assert "href>/calendar.ics/event1.ics</" not in answer
  631. assert "href>/calendar.ics/event2.ics</" not in answer
  632. assert "href>/calendar.ics/event3.ics</" in answer
  633. assert "href>/calendar.ics/event4.ics</" not in answer
  634. assert "href>/calendar.ics/event5.ics</" not in answer
  635. answer = self._test_filter(["""
  636. <C:comp-filter name="VCALENDAR">
  637. <C:comp-filter name="VEVENT">
  638. <C:time-range start="20130805T000000Z" end="20130810T000000Z"/>
  639. </C:comp-filter>
  640. </C:comp-filter>"""], items=range(1, 6))
  641. assert "href>/calendar.ics/event1.ics</" not in answer
  642. assert "href>/calendar.ics/event2.ics</" not in answer
  643. assert "href>/calendar.ics/event3.ics</" not in answer
  644. assert "href>/calendar.ics/event4.ics</" not in answer
  645. assert "href>/calendar.ics/event5.ics</" not in answer
  646. # HACK: VObject doesn't match RECURRENCE-ID to recurrences, the
  647. # overwritten recurrence is still used for filtering.
  648. answer = self._test_filter(["""
  649. <C:comp-filter name="VCALENDAR">
  650. <C:comp-filter name="VEVENT">
  651. <C:time-range start="20170601T063000Z" end="20170601T070000Z"/>
  652. </C:comp-filter>
  653. </C:comp-filter>"""], items=(6, 7, 8))
  654. assert "href>/calendar.ics/event6.ics</" in answer
  655. assert "href>/calendar.ics/event7.ics</" in answer
  656. assert "href>/calendar.ics/event8.ics</" in answer
  657. answer = self._test_filter(["""
  658. <C:comp-filter name="VCALENDAR">
  659. <C:comp-filter name="VEVENT">
  660. <C:time-range start="20170701T060000Z"/>
  661. </C:comp-filter>
  662. </C:comp-filter>"""], items=(6, 7, 8))
  663. assert "href>/calendar.ics/event6.ics</" in answer
  664. assert "href>/calendar.ics/event7.ics</" in answer
  665. assert "href>/calendar.ics/event8.ics</" in answer
  666. answer = self._test_filter(["""
  667. <C:comp-filter name="VCALENDAR">
  668. <C:comp-filter name="VEVENT">
  669. <C:time-range start="20170701T080000Z"/>
  670. </C:comp-filter>
  671. </C:comp-filter>"""], items=(6, 7, 8))
  672. assert "href>/calendar.ics/event6.ics</" not in answer
  673. assert "href>/calendar.ics/event7.ics</" in answer
  674. assert "href>/calendar.ics/event8.ics</" not in answer
  675. def test_time_range_filter_events_rrule(self):
  676. """Report request with time-range filter on events with rrules."""
  677. answer = self._test_filter(["""
  678. <C:comp-filter name="VCALENDAR">
  679. <C:comp-filter name="VEVENT">
  680. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  681. </C:comp-filter>
  682. </C:comp-filter>"""], "event", items=(1, 2))
  683. assert "href>/calendar.ics/event1.ics</" in answer
  684. assert "href>/calendar.ics/event2.ics</" in answer
  685. answer = self._test_filter(["""
  686. <C:comp-filter name="VCALENDAR">
  687. <C:comp-filter name="VEVENT">
  688. <C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
  689. </C:comp-filter>
  690. </C:comp-filter>"""], "event", items=(1, 2))
  691. assert "href>/calendar.ics/event1.ics</" not in answer
  692. assert "href>/calendar.ics/event2.ics</" in answer
  693. answer = self._test_filter(["""
  694. <C:comp-filter name="VCALENDAR">
  695. <C:comp-filter name="VEVENT">
  696. <C:time-range start="20120801T000000Z" end="20121001T000000Z"/>
  697. </C:comp-filter>
  698. </C:comp-filter>"""], "event", items=(1, 2))
  699. assert "href>/calendar.ics/event1.ics</" not in answer
  700. assert "href>/calendar.ics/event2.ics</" not in answer
  701. answer = self._test_filter(["""
  702. <C:comp-filter name="VCALENDAR">
  703. <C:comp-filter name="VEVENT">
  704. <C:time-range start="20130903T000000Z" end="20130907T000000Z"/>
  705. </C:comp-filter>
  706. </C:comp-filter>"""], "event", items=(1, 2))
  707. assert "href>/calendar.ics/event1.ics</" not in answer
  708. assert "href>/calendar.ics/event2.ics</" not in answer
  709. def test_time_range_filter_todos(self):
  710. """Report request with time-range filter on todos."""
  711. answer = self._test_filter(["""
  712. <C:comp-filter name="VCALENDAR">
  713. <C:comp-filter name="VTODO">
  714. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  715. </C:comp-filter>
  716. </C:comp-filter>"""], "todo", items=range(1, 9))
  717. assert "href>/calendar.ics/todo1.ics</" in answer
  718. assert "href>/calendar.ics/todo2.ics</" in answer
  719. assert "href>/calendar.ics/todo3.ics</" in answer
  720. assert "href>/calendar.ics/todo4.ics</" in answer
  721. assert "href>/calendar.ics/todo5.ics</" in answer
  722. assert "href>/calendar.ics/todo6.ics</" in answer
  723. assert "href>/calendar.ics/todo7.ics</" in answer
  724. assert "href>/calendar.ics/todo8.ics</" in answer
  725. answer = self._test_filter(["""
  726. <C:comp-filter name="VCALENDAR">
  727. <C:comp-filter name="VTODO">
  728. <C:time-range start="20130901T160000Z" end="20130901T183000Z"/>
  729. </C:comp-filter>
  730. </C:comp-filter>"""], "todo", items=range(1, 9))
  731. assert "href>/calendar.ics/todo1.ics</" not in answer
  732. assert "href>/calendar.ics/todo2.ics</" in answer
  733. assert "href>/calendar.ics/todo3.ics</" in answer
  734. assert "href>/calendar.ics/todo4.ics</" not in answer
  735. assert "href>/calendar.ics/todo5.ics</" not in answer
  736. assert "href>/calendar.ics/todo6.ics</" not in answer
  737. assert "href>/calendar.ics/todo7.ics</" in answer
  738. assert "href>/calendar.ics/todo8.ics</" in answer
  739. answer = self._test_filter(["""
  740. <C:comp-filter name="VCALENDAR">
  741. <C:comp-filter name="VTODO">
  742. <C:time-range start="20130903T160000Z" end="20130901T183000Z"/>
  743. </C:comp-filter>
  744. </C:comp-filter>"""], "todo", items=range(1, 9))
  745. assert "href>/calendar.ics/todo2.ics</" not in answer
  746. answer = self._test_filter(["""
  747. <C:comp-filter name="VCALENDAR">
  748. <C:comp-filter name="VTODO">
  749. <C:time-range start="20130903T160000Z" end="20130901T173000Z"/>
  750. </C:comp-filter>
  751. </C:comp-filter>"""], "todo", items=range(1, 9))
  752. assert "href>/calendar.ics/todo2.ics</" not in answer
  753. answer = self._test_filter(["""
  754. <C:comp-filter name="VCALENDAR">
  755. <C:comp-filter name="VTODO">
  756. <C:time-range start="20130903T160000Z" end="20130903T173000Z"/>
  757. </C:comp-filter>
  758. </C:comp-filter>"""], "todo", items=range(1, 9))
  759. assert "href>/calendar.ics/todo3.ics</" not in answer
  760. answer = self._test_filter(["""
  761. <C:comp-filter name="VCALENDAR">
  762. <C:comp-filter name="VTODO">
  763. <C:time-range start="20130903T160000Z" end="20130803T203000Z"/>
  764. </C:comp-filter>
  765. </C:comp-filter>"""], "todo", items=range(1, 9))
  766. assert "href>/calendar.ics/todo7.ics</" in answer
  767. def test_time_range_filter_todos_rrule(self):
  768. """Report request with time-range filter on todos with rrules."""
  769. answer = self._test_filter(["""
  770. <C:comp-filter name="VCALENDAR">
  771. <C:comp-filter name="VTODO">
  772. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  773. </C:comp-filter>
  774. </C:comp-filter>"""], "todo", items=(1, 2))
  775. assert "href>/calendar.ics/todo1.ics</" in answer
  776. assert "href>/calendar.ics/todo2.ics</" in answer
  777. answer = self._test_filter(["""
  778. <C:comp-filter name="VCALENDAR">
  779. <C:comp-filter name="VTODO">
  780. <C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
  781. </C:comp-filter>
  782. </C:comp-filter>"""], "todo", items=(1, 2))
  783. assert "href>/calendar.ics/todo1.ics</" not in answer
  784. assert "href>/calendar.ics/todo2.ics</" in answer
  785. answer = self._test_filter(["""
  786. <C:comp-filter name="VCALENDAR">
  787. <C:comp-filter name="VTODO">
  788. <C:time-range start="20140902T000000Z" end="20140903T000000Z"/>
  789. </C:comp-filter>
  790. </C:comp-filter>"""], "todo", items=(1, 2))
  791. assert "href>/calendar.ics/todo1.ics</" not in answer
  792. assert "href>/calendar.ics/todo2.ics</" in answer
  793. answer = self._test_filter(["""
  794. <C:comp-filter name="VCALENDAR">
  795. <C:comp-filter name="VTODO">
  796. <C:time-range start="20140904T000000Z" end="20140914T000000Z"/>
  797. </C:comp-filter>
  798. </C:comp-filter>"""], "todo", items=(1, 2))
  799. assert "href>/calendar.ics/todo1.ics</" not in answer
  800. assert "href>/calendar.ics/todo2.ics</" not in answer
  801. def test_time_range_filter_journals(self):
  802. """Report request with time-range filter on journals."""
  803. answer = self._test_filter(["""
  804. <C:comp-filter name="VCALENDAR">
  805. <C:comp-filter name="VJOURNAL">
  806. <C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
  807. </C:comp-filter>
  808. </C:comp-filter>"""], "journal", items=(1, 2, 3))
  809. assert "href>/calendar.ics/journal1.ics</" not in answer
  810. assert "href>/calendar.ics/journal2.ics</" in answer
  811. assert "href>/calendar.ics/journal3.ics</" in answer
  812. answer = self._test_filter(["""
  813. <C:comp-filter name="VCALENDAR">
  814. <C:comp-filter name="VJOURNAL">
  815. <C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
  816. </C:comp-filter>
  817. </C:comp-filter>"""], "journal", items=(1, 2, 3))
  818. assert "href>/calendar.ics/journal1.ics</" not in answer
  819. assert "href>/calendar.ics/journal2.ics</" in answer
  820. assert "href>/calendar.ics/journal3.ics</" in answer
  821. answer = self._test_filter(["""
  822. <C:comp-filter name="VCALENDAR">
  823. <C:comp-filter name="VJOURNAL">
  824. <C:time-range start="19981229T000000Z" end="19991012T000000Z"/>
  825. </C:comp-filter>
  826. </C:comp-filter>"""], "journal", items=(1, 2, 3))
  827. assert "href>/calendar.ics/journal1.ics</" not in answer
  828. assert "href>/calendar.ics/journal2.ics</" not in answer
  829. assert "href>/calendar.ics/journal3.ics</" not in answer
  830. answer = self._test_filter(["""
  831. <C:comp-filter name="VCALENDAR">
  832. <C:comp-filter name="VJOURNAL">
  833. <C:time-range start="20131229T000000Z" end="21520202T000000Z"/>
  834. </C:comp-filter>
  835. </C:comp-filter>"""], "journal", items=(1, 2, 3))
  836. assert "href>/calendar.ics/journal1.ics</" not in answer
  837. assert "href>/calendar.ics/journal2.ics</" in answer
  838. assert "href>/calendar.ics/journal3.ics</" not in answer
  839. answer = self._test_filter(["""
  840. <C:comp-filter name="VCALENDAR">
  841. <C:comp-filter name="VJOURNAL">
  842. <C:time-range start="20000101T000000Z" end="20000202T000000Z"/>
  843. </C:comp-filter>
  844. </C:comp-filter>"""], "journal", items=(1, 2, 3))
  845. assert "href>/calendar.ics/journal1.ics</" not in answer
  846. assert "href>/calendar.ics/journal2.ics</" in answer
  847. assert "href>/calendar.ics/journal3.ics</" in answer
  848. def test_time_range_filter_journals_rrule(self):
  849. """Report request with time-range filter on journals with rrules."""
  850. answer = self._test_filter(["""
  851. <C:comp-filter name="VCALENDAR">
  852. <C:comp-filter name="VJOURNAL">
  853. <C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
  854. </C:comp-filter>
  855. </C:comp-filter>"""], "journal", items=(1, 2))
  856. assert "href>/calendar.ics/journal1.ics</" not in answer
  857. assert "href>/calendar.ics/journal2.ics</" in answer
  858. answer = self._test_filter(["""
  859. <C:comp-filter name="VCALENDAR">
  860. <C:comp-filter name="VJOURNAL">
  861. <C:time-range start="20051229T000000Z" end="20060202T000000Z"/>
  862. </C:comp-filter>
  863. </C:comp-filter>"""], "journal", items=(1, 2))
  864. assert "href>/calendar.ics/journal1.ics</" not in answer
  865. assert "href>/calendar.ics/journal2.ics</" in answer
  866. answer = self._test_filter(["""
  867. <C:comp-filter name="VCALENDAR">
  868. <C:comp-filter name="VJOURNAL">
  869. <C:time-range start="20060102T000000Z" end="20060202T000000Z"/>
  870. </C:comp-filter>
  871. </C:comp-filter>"""], "journal", items=(1, 2))
  872. assert "href>/calendar.ics/journal1.ics</" not in answer
  873. assert "href>/calendar.ics/journal2.ics</" not in answer
  874. def test_report_item(self):
  875. """Test report request on an item"""
  876. calendar_path = "/calendar.ics/"
  877. status, _, _ = self.request("MKCALENDAR", calendar_path)
  878. assert status == 201
  879. event = get_file_content("event1.ics")
  880. event_path = posixpath.join(calendar_path, "event.ics")
  881. status, _, _ = self.request("PUT", event_path, event)
  882. assert status == 201
  883. status, _, answer = self.request(
  884. "REPORT", event_path,
  885. """<?xml version="1.0" encoding="utf-8" ?>
  886. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  887. <D:prop xmlns:D="DAV:">
  888. <D:getetag />
  889. </D:prop>
  890. </C:calendar-query>""")
  891. assert status == 207
  892. assert "href>%s<" % event_path in answer
  893. def _report_sync_token(self, calendar_path, sync_token=None):
  894. sync_token_xml = (
  895. "<sync-token><![CDATA[%s]]></sync-token>" % sync_token
  896. if sync_token else "<sync-token />")
  897. status, _, answer = self.request(
  898. "REPORT", calendar_path,
  899. """<?xml version="1.0" encoding="utf-8" ?>
  900. <sync-collection xmlns="DAV:">
  901. <prop>
  902. <getetag />
  903. </prop>
  904. %s
  905. </sync-collection>""" % sync_token_xml)
  906. if sync_token and status == 412:
  907. return None, None
  908. assert status == 207
  909. xml = ET.fromstring(answer)
  910. sync_token = xml.find("{DAV:}sync-token").text.strip()
  911. assert sync_token
  912. return sync_token, xml
  913. def test_report_sync_collection_no_change(self):
  914. """Test sync-collection report without modifying the collection"""
  915. calendar_path = "/calendar.ics/"
  916. status, _, _ = self.request("MKCALENDAR", calendar_path)
  917. assert status == 201
  918. event = get_file_content("event1.ics")
  919. event_path = posixpath.join(calendar_path, "event.ics")
  920. status, _, _ = self.request("PUT", event_path, event)
  921. assert status == 201
  922. sync_token, xml = self._report_sync_token(calendar_path)
  923. assert xml.find("{DAV:}response") is not None
  924. new_sync_token, xml = self._report_sync_token(calendar_path,
  925. sync_token)
  926. assert sync_token == new_sync_token
  927. assert xml.find("{DAV:}response") is None
  928. def test_report_sync_collection_add(self):
  929. """Test sync-collection report with an added item"""
  930. calendar_path = "/calendar.ics/"
  931. status, _, _ = self.request("MKCALENDAR", calendar_path)
  932. assert status == 201
  933. sync_token, xml = self._report_sync_token(calendar_path)
  934. event = get_file_content("event1.ics")
  935. event_path = posixpath.join(calendar_path, "event.ics")
  936. status, _, _ = self.request("PUT", event_path, event)
  937. assert status == 201
  938. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  939. if not sync_token:
  940. pytest.skip("storage backend does not support sync-token")
  941. assert xml.find("{DAV:}response") is not None
  942. assert xml.find("{DAV:}response/{DAV:}status") is None
  943. def test_report_sync_collection_delete(self):
  944. """Test sync-collection report with a deleted item"""
  945. calendar_path = "/calendar.ics/"
  946. status, _, _ = self.request("MKCALENDAR", calendar_path)
  947. assert status == 201
  948. event = get_file_content("event1.ics")
  949. event_path = posixpath.join(calendar_path, "event.ics")
  950. status, _, _ = self.request("PUT", event_path, event)
  951. assert status == 201
  952. sync_token, xml = self._report_sync_token(calendar_path)
  953. status, _, _ = self.request("DELETE", event_path)
  954. assert status == 200
  955. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  956. if not sync_token:
  957. pytest.skip("storage backend does not support sync-token")
  958. assert "404" in xml.find("{DAV:}response/{DAV:}status").text
  959. def test_report_sync_collection_create_delete(self):
  960. """Test sync-collection report with a created and deleted item"""
  961. calendar_path = "/calendar.ics/"
  962. status, _, _ = self.request("MKCALENDAR", calendar_path)
  963. assert status == 201
  964. sync_token, xml = self._report_sync_token(calendar_path)
  965. event = get_file_content("event1.ics")
  966. event_path = posixpath.join(calendar_path, "event.ics")
  967. status, _, _ = self.request("PUT", event_path, event)
  968. assert status == 201
  969. status, _, _ = self.request("DELETE", event_path)
  970. assert status == 200
  971. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  972. if not sync_token:
  973. pytest.skip("storage backend does not support sync-token")
  974. assert "404" in xml.find("{DAV:}response/{DAV:}status").text
  975. def test_report_sync_collection_modify_undo(self):
  976. """Test sync-collection report with a modified and changed back item"""
  977. calendar_path = "/calendar.ics/"
  978. status, _, _ = self.request("MKCALENDAR", calendar_path)
  979. assert status == 201
  980. event1 = get_file_content("event1.ics")
  981. event2 = get_file_content("event2.ics")
  982. event_path = posixpath.join(calendar_path, "event1.ics")
  983. status, _, _ = self.request("PUT", event_path, event1)
  984. assert status == 201
  985. sync_token, xml = self._report_sync_token(calendar_path)
  986. status, _, _ = self.request("PUT", event_path, event2)
  987. assert status == 201
  988. status, _, _ = self.request("PUT", event_path, event1)
  989. assert status == 201
  990. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  991. if not sync_token:
  992. pytest.skip("storage backend does not support sync-token")
  993. assert xml.find("{DAV:}response") is not None
  994. assert xml.find("{DAV:}response/{DAV:}status") is None
  995. def test_report_sync_collection_move(self):
  996. """Test sync-collection report a moved item"""
  997. calendar_path = "/calendar.ics/"
  998. status, _, _ = self.request("MKCALENDAR", calendar_path)
  999. assert status == 201
  1000. event = get_file_content("event1.ics")
  1001. event1_path = posixpath.join(calendar_path, "event1.ics")
  1002. event2_path = posixpath.join(calendar_path, "event2.ics")
  1003. status, _, _ = self.request("PUT", event1_path, event)
  1004. assert status == 201
  1005. sync_token, xml = self._report_sync_token(calendar_path)
  1006. status, _, _ = self.request(
  1007. "MOVE", event1_path, HTTP_DESTINATION=event2_path, HTTP_HOST="")
  1008. assert status == 201
  1009. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  1010. if not sync_token:
  1011. pytest.skip("storage backend does not support sync-token")
  1012. for response in xml.findall("{DAV:}response"):
  1013. if response.find("{DAV:}status") is None:
  1014. assert response.find("{DAV:}href").text == event2_path
  1015. else:
  1016. assert "404" in response.find("{DAV:}status").text
  1017. assert response.find("{DAV:}href").text == event1_path
  1018. def test_report_sync_collection_move_undo(self):
  1019. """Test sync-collection report with a moved and moved back item"""
  1020. calendar_path = "/calendar.ics/"
  1021. status, _, _ = self.request("MKCALENDAR", calendar_path)
  1022. assert status == 201
  1023. event = get_file_content("event1.ics")
  1024. event1_path = posixpath.join(calendar_path, "event1.ics")
  1025. event2_path = posixpath.join(calendar_path, "event2.ics")
  1026. status, _, _ = self.request("PUT", event1_path, event)
  1027. assert status == 201
  1028. sync_token, xml = self._report_sync_token(calendar_path)
  1029. status, _, _ = self.request(
  1030. "MOVE", event1_path, HTTP_DESTINATION=event2_path, HTTP_HOST="")
  1031. assert status == 201
  1032. status, _, _ = self.request(
  1033. "MOVE", event2_path, HTTP_DESTINATION=event1_path, HTTP_HOST="")
  1034. assert status == 201
  1035. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  1036. if not sync_token:
  1037. pytest.skip("storage backend does not support sync-token")
  1038. created = deleted = 0
  1039. for response in xml.findall("{DAV:}response"):
  1040. if response.find("{DAV:}status") is None:
  1041. assert response.find("{DAV:}href").text == event1_path
  1042. created += 1
  1043. else:
  1044. assert "404" in response.find("{DAV:}status").text
  1045. assert response.find("{DAV:}href").text == event2_path
  1046. deleted += 1
  1047. assert created == 1 and deleted == 1
  1048. def test_report_sync_collection_invalid_sync_token(self):
  1049. """Test sync-collection report with an invalid sync token"""
  1050. calendar_path = "/calendar.ics/"
  1051. status, _, _ = self.request("MKCALENDAR", calendar_path)
  1052. assert status == 201
  1053. sync_token, xml = self._report_sync_token(
  1054. calendar_path, "http://radicale.org/ns/sync/INVALID")
  1055. assert not sync_token
  1056. def test_propfind_sync_token(self):
  1057. """Retrieve the sync-token with a propfind request"""
  1058. calendar_path = "/calendar.ics/"
  1059. status, _, _ = self.request("MKCALENDAR", calendar_path)
  1060. assert status == 201
  1061. sync_token, xml = self._report_sync_token(calendar_path)
  1062. event = get_file_content("event1.ics")
  1063. event_path = posixpath.join(calendar_path, "event.ics")
  1064. status, _, _ = self.request("PUT", event_path, event)
  1065. assert status == 201
  1066. new_sync_token, xml = self._report_sync_token(calendar_path,
  1067. sync_token)
  1068. assert sync_token != new_sync_token
  1069. def test_propfind_same_as_sync_collection_sync_token(self):
  1070. """Compare sync-token property with sync-collection sync-token"""
  1071. calendar_path = "/calendar.ics/"
  1072. status, _, _ = self.request("MKCALENDAR", calendar_path)
  1073. assert status == 201
  1074. sync_token, xml = self._report_sync_token(calendar_path)
  1075. new_sync_token, xml = self._report_sync_token(calendar_path,
  1076. sync_token)
  1077. assert sync_token == new_sync_token
  1078. def test_authorization(self):
  1079. authorization = "Basic " + base64.b64encode(b"user:").decode()
  1080. status, _, answer = self.request(
  1081. "PROPFIND", "/",
  1082. """<?xml version="1.0" encoding="utf-8"?>
  1083. <propfind xmlns="DAV:">
  1084. <prop>
  1085. <current-user-principal />
  1086. </prop>
  1087. </propfind>""",
  1088. HTTP_AUTHORIZATION=authorization)
  1089. assert status == 207
  1090. assert "href>/user/<" in answer
  1091. def test_authentication(self):
  1092. """Test if server sends authentication request."""
  1093. self.configuration["auth"]["type"] = "htpasswd"
  1094. self.configuration["auth"]["htpasswd_filename"] = os.devnull
  1095. self.configuration["auth"]["htpasswd_encryption"] = "plain"
  1096. self.configuration["rights"]["type"] = "owner_only"
  1097. self.application = Application(self.configuration, self.logger)
  1098. status, headers, _ = self.request("MKCOL", "/user/")
  1099. assert status in (401, 403)
  1100. assert headers.get("WWW-Authenticate")
  1101. def test_principal_collection_creation(self):
  1102. """Verify existence of the principal collection."""
  1103. status, _, _ = self.request("PROPFIND", "/user/", HTTP_AUTHORIZATION=(
  1104. "Basic " + base64.b64encode(b"user:").decode()))
  1105. assert status == 207
  1106. def test_existence_of_root_collections(self):
  1107. """Verify that the root collection always exists."""
  1108. # Use PROPFIND because GET returns message
  1109. status, _, _ = self.request("PROPFIND", "/")
  1110. assert status == 207
  1111. # it should still exist after deletion
  1112. status, _, _ = self.request("DELETE", "/")
  1113. assert status == 200
  1114. status, _, _ = self.request("PROPFIND", "/")
  1115. assert status == 207
  1116. def test_custom_headers(self):
  1117. if not self.configuration.has_section("headers"):
  1118. self.configuration.add_section("headers")
  1119. self.configuration.set("headers", "test", "123")
  1120. # Test if header is set on success
  1121. status, headers, _ = self.request("OPTIONS", "/")
  1122. assert status == 200
  1123. assert headers.get("test") == "123"
  1124. # Test if header is set on failure
  1125. status, headers, _ = self.request(
  1126. "GET", "/.well-known/does not exist")
  1127. assert status == 404
  1128. assert headers.get("test") == "123"
  1129. def test_missing_uid(self):
  1130. """Verify that missing UIDs are added in a stable manner."""
  1131. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1132. assert status == 201
  1133. event_without_uid = get_file_content("event1.ics").replace(
  1134. "UID:event1\n", "")
  1135. assert "UID" not in event_without_uid
  1136. path = "/calendar.ics/event1.ics"
  1137. status, _, _ = self.request("PUT", path, event_without_uid)
  1138. assert status == 201
  1139. status, _, answer = self.request("GET", path)
  1140. assert status == 200
  1141. uid = None
  1142. for line in answer.split("\r\n"):
  1143. if line.startswith("UID:"):
  1144. uid = line[len("UID:"):]
  1145. assert uid
  1146. status, _, _ = self.request("PUT", path, event_without_uid)
  1147. assert status == 201
  1148. status, _, answer = self.request("GET", path)
  1149. assert status == 200
  1150. assert "UID:%s\r\n" % uid in answer
  1151. class BaseFileSystemTest(BaseTest):
  1152. """Base class for filesystem backend tests."""
  1153. storage_type = None
  1154. def setup(self):
  1155. self.configuration = config.load()
  1156. self.configuration["storage"]["type"] = self.storage_type
  1157. self.colpath = tempfile.mkdtemp()
  1158. self.configuration["storage"]["filesystem_folder"] = self.colpath
  1159. # Disable syncing to disk for better performance
  1160. self.configuration["storage"]["filesystem_fsync"] = "False"
  1161. # Required on Windows, doesn't matter on Unix
  1162. self.configuration["storage"]["filesystem_close_lock_file"] = "True"
  1163. self.application = Application(self.configuration, self.logger)
  1164. def teardown(self):
  1165. shutil.rmtree(self.colpath)
  1166. class TestMultiFileSystem(BaseFileSystemTest, BaseRequestsMixIn):
  1167. """Test BaseRequests on multifilesystem."""
  1168. storage_type = "multifilesystem"
  1169. def test_fsync(self):
  1170. """Create a directory and file with syncing enabled."""
  1171. self.configuration["storage"]["filesystem_fsync"] = "True"
  1172. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1173. assert status == 201
  1174. def test_hook(self):
  1175. """Run hook."""
  1176. self.configuration["storage"]["hook"] = (
  1177. "mkdir %s" % os.path.join("collection-root", "created_by_hook"))
  1178. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1179. assert status == 201
  1180. status, _, _ = self.request("PROPFIND", "/created_by_hook/")
  1181. assert status == 207
  1182. def test_hook_read_access(self):
  1183. """Verify that hook is not run for read accesses."""
  1184. self.configuration["storage"]["hook"] = (
  1185. "mkdir %s" % os.path.join("collection-root", "created_by_hook"))
  1186. status, _, _ = self.request("PROPFIND", "/")
  1187. assert status == 207
  1188. status, _, _ = self.request("PROPFIND", "/created_by_hook/")
  1189. assert status == 404
  1190. @pytest.mark.skipif(os.system("type flock") != 0,
  1191. reason="flock command not found")
  1192. def test_hook_storage_locked(self):
  1193. """Verify that the storage is locked when the hook runs."""
  1194. self.configuration["storage"]["hook"] = (
  1195. "flock -n .Radicale.lock || exit 0; exit 1")
  1196. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1197. assert status == 201
  1198. def test_hook_principal_collection_creation(self):
  1199. """Verify that the hooks runs when a new user is created."""
  1200. self.configuration["storage"]["hook"] = (
  1201. "mkdir %s" % os.path.join("collection-root", "created_by_hook"))
  1202. status, _, _ = self.request("PROPFIND", "/", HTTP_AUTHORIZATION=(
  1203. "Basic " + base64.b64encode(b"user:").decode()))
  1204. assert status == 207
  1205. status, _, _ = self.request("PROPFIND", "/created_by_hook/")
  1206. assert status == 207
  1207. def test_hook_fail(self):
  1208. """Verify that a request fails if the hook fails."""
  1209. self.configuration["storage"]["hook"] = "exit 1"
  1210. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1211. assert status != 201
  1212. def test_item_cache_rebuild(self):
  1213. """Delete the item cache and verify that it is rebuild."""
  1214. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1215. assert status == 201
  1216. event = get_file_content("event1.ics")
  1217. path = "/calendar.ics/event1.ics"
  1218. status, _, _ = self.request("PUT", path, event)
  1219. assert status == 201
  1220. status, _, answer1 = self.request("GET", path)
  1221. assert status == 200
  1222. cache_folder = os.path.join(self.colpath, "collection-root",
  1223. "calendar.ics", ".Radicale.cache", "item")
  1224. assert os.path.exists(os.path.join(cache_folder, "event1.ics"))
  1225. shutil.rmtree(cache_folder)
  1226. status, _, answer2 = self.request("GET", path)
  1227. assert status == 200
  1228. assert answer1 == answer2
  1229. assert os.path.exists(os.path.join(cache_folder, "event1.ics"))
  1230. class TestCustomStorageSystem(BaseFileSystemTest):
  1231. """Test custom backend loading."""
  1232. storage_type = "tests.custom.storage"
  1233. def test_root(self):
  1234. """A simple test to verify that the custom backend works."""
  1235. BaseRequestsMixIn.test_root(self)