test_base.py 66 KB

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