test_base.py 62 KB

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