test_base.py 54 KB

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