test_base.py 70 KB

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