test_base.py 69 KB

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