test_base.py 73 KB

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