test_base.py 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2012-2017 Guillaume Ayoub
  3. #
  4. # This library is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This library is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. Radicale tests with simple requests.
  18. """
  19. import base64
  20. import os
  21. import posixpath
  22. import shutil
  23. import tempfile
  24. import xml.etree.ElementTree as ET
  25. import pytest
  26. from radicale import Application, config
  27. from . import BaseTest
  28. from .helpers import get_file_content
  29. class BaseRequestsMixIn:
  30. """Tests with simple requests."""
  31. def test_root(self):
  32. """GET request at "/"."""
  33. status, headers, answer = self.request("GET", "/")
  34. assert status == 303
  35. assert answer == "Redirected to .web"
  36. # Test the creation of the collection
  37. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  38. assert status == 201
  39. status, headers, answer = self.request("GET", "/calendar.ics/")
  40. assert "BEGIN:VCALENDAR" in answer
  41. assert "END:VCALENDAR" in answer
  42. def test_script_name(self):
  43. """GET request at "/" with SCRIPT_NAME."""
  44. status, headers, answer = self.request(
  45. "GET", "/", SCRIPT_NAME="/radicale")
  46. assert status == 303
  47. assert answer == "Redirected to .web"
  48. status, headers, answer = self.request(
  49. "GET", "", SCRIPT_NAME="/radicale")
  50. assert status == 303
  51. assert answer == "Redirected to radicale/.web"
  52. def test_add_event(self):
  53. """Add an event."""
  54. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  55. assert status == 201
  56. event = get_file_content("event1.ics")
  57. path = "/calendar.ics/event1.ics"
  58. status, headers, answer = self.request("PUT", path, event)
  59. assert status == 201
  60. status, headers, answer = self.request("GET", path)
  61. assert "ETag" in headers.keys()
  62. assert status == 200
  63. assert "VEVENT" in answer
  64. assert "Event" in answer
  65. assert "UID:event" in answer
  66. def test_add_todo(self):
  67. """Add a todo."""
  68. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  69. assert status == 201
  70. todo = get_file_content("todo1.ics")
  71. path = "/calendar.ics/todo1.ics"
  72. status, headers, answer = self.request("PUT", path, todo)
  73. assert status == 201
  74. status, headers, answer = self.request("GET", path)
  75. assert "ETag" in headers.keys()
  76. assert "VTODO" in answer
  77. assert "Todo" in answer
  78. assert "UID:todo" in answer
  79. def _create_addressbook(self, path):
  80. return self.request(
  81. "MKCOL", path, """\
  82. <?xml version="1.0" encoding="UTF-8" ?>
  83. <create xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
  84. <set>
  85. <prop>
  86. <resourcetype>
  87. <collection />
  88. <CR:addressbook />
  89. </resourcetype>
  90. </prop>
  91. </set>
  92. </create>""")
  93. def test_add_contact(self):
  94. """Add a contact."""
  95. status, _, _ = self._create_addressbook("/contacts.vcf/")
  96. assert status == 201
  97. contact = get_file_content("contact1.vcf")
  98. path = "/contacts.vcf/contact.vcf"
  99. status, _, _ = self.request("PUT", path, contact)
  100. assert status == 201
  101. status, headers, answer = self.request("GET", path)
  102. assert status == 200
  103. assert "ETag" in headers.keys()
  104. assert "VCARD" in answer
  105. assert "UID:contact1" in answer
  106. status, _, answer = self.request("GET", path)
  107. assert status == 200
  108. assert "UID:contact1" in answer
  109. def test_update(self):
  110. """Update an event."""
  111. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  112. assert status == 201
  113. event = get_file_content("event1.ics")
  114. path = "/calendar.ics/event1.ics"
  115. status, headers, answer = self.request("PUT", path, event)
  116. assert status == 201
  117. status, headers, answer = self.request("GET", path)
  118. assert "ETag" in headers.keys()
  119. assert status == 200
  120. assert "VEVENT" in answer
  121. assert "Event" in answer
  122. assert "UID:event" in answer
  123. assert "DTSTART;TZID=Europe/Paris:20130901T180000" in answer
  124. assert "DTEND;TZID=Europe/Paris:20130901T190000" in answer
  125. # Then we send another PUT request
  126. event = get_file_content("event1-prime.ics")
  127. status, headers, answer = self.request("PUT", path, event)
  128. assert status == 201
  129. status, headers, answer = self.request("GET", "/calendar.ics/")
  130. assert answer.count("BEGIN:VEVENT") == 1
  131. status, headers, answer = self.request("GET", path)
  132. assert "ETag" in headers.keys()
  133. assert status == 200
  134. assert "VEVENT" in answer
  135. assert "Event" in answer
  136. assert "UID:event" in answer
  137. assert "DTSTART;TZID=Europe/Paris:20130901T180000" not in answer
  138. assert "DTEND;TZID=Europe/Paris:20130901T190000" not in answer
  139. assert "DTSTART;TZID=Europe/Paris:20140901T180000" in answer
  140. assert "DTEND;TZID=Europe/Paris:20140901T210000" in answer
  141. def test_put_whole_calendar(self):
  142. """Create and overwrite a whole calendar."""
  143. status, _, _ = self.request(
  144. "PUT", "/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
  145. event1 = get_file_content("event1.ics")
  146. assert status == 201
  147. status, _, _ = self.request(
  148. "PUT", "/calendar.ics/test_event.ics", event1)
  149. assert status == 201
  150. # Overwrite
  151. events = get_file_content("event_multiple.ics")
  152. status, _, _ = self.request("PUT", "/calendar.ics/", events)
  153. assert status == 201
  154. status, _, _ = self.request("GET", "/calendar.ics/test_event.ics")
  155. assert status == 404
  156. status, _, answer = self.request("GET", "/calendar.ics/")
  157. assert status == 200
  158. assert "\r\nUID:event\r\n" in answer and "\r\nUID:todo\r\n" in answer
  159. assert "\r\nUID:event1\r\n" not in answer
  160. def test_put_whole_addressbook(self):
  161. """Create and overwrite a whole addressbook."""
  162. contacts = get_file_content("contact_multiple.vcf")
  163. status, _, _ = self.request("PUT", "/contacts.vcf/", contacts)
  164. assert status == 201
  165. status, _, answer = self.request("GET", "/contacts.vcf/")
  166. assert status == 200
  167. assert ("\r\nUID:contact1\r\n" in answer and
  168. "\r\nUID:contact2\r\n" in answer)
  169. def test_delete(self):
  170. """Delete an event."""
  171. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  172. assert status == 201
  173. event = get_file_content("event1.ics")
  174. path = "/calendar.ics/event1.ics"
  175. status, headers, answer = self.request("PUT", path, event)
  176. # Then we send a DELETE request
  177. status, headers, answer = self.request("DELETE", path)
  178. assert status == 200
  179. assert "href>%s</" % path in answer
  180. status, headers, answer = self.request("GET", "/calendar.ics/")
  181. assert "VEVENT" not in answer
  182. def test_mkcalendar(self):
  183. """Make a calendar."""
  184. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  185. assert status == 201
  186. status, headers, answer = self.request("GET", "/calendar.ics/")
  187. assert status == 200
  188. def test_move(self):
  189. """Move a item."""
  190. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  191. assert status == 201
  192. event = get_file_content("event1.ics")
  193. path1 = "/calendar.ics/event1.ics"
  194. path2 = "/calendar.ics/event2.ics"
  195. status, headers, answer = self.request("PUT", path1, event)
  196. status, headers, answer = self.request(
  197. "MOVE", path1, HTTP_DESTINATION=path2, HTTP_HOST="")
  198. assert status == 201
  199. status, headers, answer = self.request("GET", path1)
  200. assert status == 404
  201. status, headers, answer = self.request("GET", path2)
  202. assert status == 200
  203. def test_head(self):
  204. status, headers, answer = self.request("HEAD", "/")
  205. assert status == 303
  206. def test_options(self):
  207. status, headers, answer = self.request("OPTIONS", "/")
  208. assert status == 200
  209. assert "DAV" in headers
  210. def test_delete_collection(self):
  211. """Delete a collection."""
  212. self.request("MKCALENDAR", "/calendar.ics/")
  213. event = get_file_content("event1.ics")
  214. self.request("PUT", "/calendar.ics/event1.ics", event)
  215. status, headers, answer = self.request("DELETE", "/calendar.ics/")
  216. assert status == 200
  217. assert "href>/calendar.ics/</" in answer
  218. status, headers, answer = self.request("GET", "/calendar.ics/")
  219. assert status == 404
  220. def test_delete_root_collection(self):
  221. """Delete the root collection."""
  222. self.request("MKCALENDAR", "/calendar.ics/")
  223. event = get_file_content("event1.ics")
  224. self.request("PUT", "/event1.ics", event)
  225. self.request("PUT", "/calendar.ics/event1.ics", event)
  226. status, headers, answer = self.request("DELETE", "/")
  227. assert status == 200
  228. assert "href>/</" in answer
  229. status, headers, answer = self.request("GET", "/calendar.ics/")
  230. assert status == 404
  231. status, headers, answer = self.request("GET", "/event1.ics")
  232. assert status == 404
  233. def test_propfind(self):
  234. calendar_path = "/calendar.ics/"
  235. self.request("MKCALENDAR", calendar_path)
  236. event = get_file_content("event1.ics")
  237. event_path = posixpath.join(calendar_path, "event.ics")
  238. self.request("PUT", event_path, event)
  239. status, headers, answer = self.request("PROPFIND", "/", HTTP_DEPTH="1")
  240. assert status == 207
  241. assert "href>/</" in answer
  242. assert "href>%s</" % calendar_path in answer
  243. status, headers, answer = self.request(
  244. "PROPFIND", calendar_path, HTTP_DEPTH="1")
  245. assert status == 207
  246. assert "href>%s</" % calendar_path in answer
  247. assert "href>%s</" % event_path in answer
  248. def test_proppatch(self):
  249. """Write a property and read it back."""
  250. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  251. assert status == 201
  252. proppatch = get_file_content("proppatch1.xml")
  253. status, headers, answer = self.request(
  254. "PROPPATCH", "/calendar.ics/", proppatch)
  255. assert status == 207
  256. assert "calendar-color" in answer
  257. assert "200 OK</status" in answer
  258. # Read property back
  259. propfind = get_file_content("propfind1.xml")
  260. status, headers, answer = self.request(
  261. "PROPFIND", "/calendar.ics/", propfind)
  262. assert status == 207
  263. assert ":calendar-color>#BADA55</" in answer
  264. assert "200 OK</status" in answer
  265. def test_put_whole_calendar_multiple_events_with_same_uid(self):
  266. """Add two events with the same UID."""
  267. self.request("PUT", "/calendar.ics/", get_file_content("event2.ics"))
  268. status, headers, answer = self.request(
  269. "REPORT", "/calendar.ics/",
  270. """<?xml version="1.0" encoding="utf-8" ?>
  271. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  272. <D:prop xmlns:D="DAV:"><D:getetag/></D:prop>
  273. </C:calendar-query>""")
  274. assert answer.count("<getetag>") == 1
  275. status, headers, answer = self.request("GET", "/calendar.ics/")
  276. assert answer.count("BEGIN:VEVENT") == 2
  277. def _test_filter(self, filters, kind="event", items=1):
  278. filters_text = "".join(
  279. "<C:filter>%s</C:filter>" % filter_ for filter_ in filters)
  280. status, _, _ = self.request("DELETE", "/calendar.ics/")
  281. assert status in (200, 404)
  282. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  283. assert status == 201
  284. for i in range(items):
  285. filename = "{}{}.ics".format(kind, i + 1)
  286. event = get_file_content(filename)
  287. self.request("PUT", "/calendar.ics/{}".format(filename), event)
  288. status, headers, answer = self.request(
  289. "REPORT", "/calendar.ics",
  290. """<?xml version="1.0" encoding="utf-8" ?>
  291. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  292. <D:prop xmlns:D="DAV:">
  293. <D:getetag/>
  294. </D:prop>
  295. %s
  296. </C:calendar-query>""" % filters_text)
  297. return answer
  298. def test_calendar_empty_filter(self):
  299. self._test_filter([""])
  300. def test_calendar_tag_filter(self):
  301. """Report request with tag-based filter on calendar."""
  302. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  303. <C:comp-filter name="VCALENDAR"></C:comp-filter>"""])
  304. def test_item_tag_filter(self):
  305. """Report request with tag-based filter on an item."""
  306. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  307. <C:comp-filter name="VCALENDAR">
  308. <C:comp-filter name="VEVENT"></C:comp-filter>
  309. </C:comp-filter>"""])
  310. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  311. <C:comp-filter name="VCALENDAR">
  312. <C:comp-filter name="VTODO"></C:comp-filter>
  313. </C:comp-filter>"""])
  314. def test_item_not_tag_filter(self):
  315. """Report request with tag-based is-not filter on an item."""
  316. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  317. <C:comp-filter name="VCALENDAR">
  318. <C:comp-filter name="VEVENT">
  319. <C:is-not-defined />
  320. </C:comp-filter>
  321. </C:comp-filter>"""])
  322. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  323. <C:comp-filter name="VCALENDAR">
  324. <C:comp-filter name="VTODO">
  325. <C:is-not-defined />
  326. </C:comp-filter>
  327. </C:comp-filter>"""])
  328. def test_item_prop_filter(self):
  329. """Report request with prop-based filter on an item."""
  330. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  331. <C:comp-filter name="VCALENDAR">
  332. <C:comp-filter name="VEVENT">
  333. <C:prop-filter name="SUMMARY"></C:prop-filter>
  334. </C:comp-filter>
  335. </C:comp-filter>"""])
  336. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  337. <C:comp-filter name="VCALENDAR">
  338. <C:comp-filter name="VEVENT">
  339. <C:prop-filter name="UNKNOWN"></C:prop-filter>
  340. </C:comp-filter>
  341. </C:comp-filter>"""])
  342. def test_item_not_prop_filter(self):
  343. """Report request with prop-based is-not filter on an item."""
  344. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  345. <C:comp-filter name="VCALENDAR">
  346. <C:comp-filter name="VEVENT">
  347. <C:prop-filter name="SUMMARY">
  348. <C:is-not-defined />
  349. </C:prop-filter>
  350. </C:comp-filter>
  351. </C:comp-filter>"""])
  352. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  353. <C:comp-filter name="VCALENDAR">
  354. <C:comp-filter name="VEVENT">
  355. <C:prop-filter name="UNKNOWN">
  356. <C:is-not-defined />
  357. </C:prop-filter>
  358. </C:comp-filter>
  359. </C:comp-filter>"""])
  360. def test_mutiple_filters(self):
  361. """Report request with multiple filters on an item."""
  362. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  363. <C:comp-filter name="VCALENDAR">
  364. <C:comp-filter name="VEVENT">
  365. <C:prop-filter name="SUMMARY">
  366. <C:is-not-defined />
  367. </C:prop-filter>
  368. </C:comp-filter>
  369. </C:comp-filter>""", """
  370. <C:comp-filter name="VCALENDAR">
  371. <C:comp-filter name="VEVENT">
  372. <C:prop-filter name="UNKNOWN">
  373. <C:is-not-defined />
  374. </C:prop-filter>
  375. </C:comp-filter>
  376. </C:comp-filter>"""])
  377. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  378. <C:comp-filter name="VCALENDAR">
  379. <C:comp-filter name="VEVENT">
  380. <C:prop-filter name="SUMMARY"></C:prop-filter>
  381. </C:comp-filter>
  382. </C:comp-filter>""", """
  383. <C:comp-filter name="VCALENDAR">
  384. <C:comp-filter name="VEVENT">
  385. <C:prop-filter name="UNKNOWN">
  386. <C:is-not-defined />
  387. </C:prop-filter>
  388. </C:comp-filter>
  389. </C:comp-filter>"""])
  390. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  391. <C:comp-filter name="VCALENDAR">
  392. <C:comp-filter name="VEVENT">
  393. <C:prop-filter name="SUMMARY"></C:prop-filter>
  394. <C:prop-filter name="UNKNOWN">
  395. <C:is-not-defined />
  396. </C:prop-filter>
  397. </C:comp-filter>
  398. </C:comp-filter>"""])
  399. def test_text_match_filter(self):
  400. """Report request with text-match filter on calendar."""
  401. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  402. <C:comp-filter name="VCALENDAR">
  403. <C:comp-filter name="VEVENT">
  404. <C:prop-filter name="SUMMARY">
  405. <C:text-match>event</C:text-match>
  406. </C:prop-filter>
  407. </C:comp-filter>
  408. </C:comp-filter>"""])
  409. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  410. <C:comp-filter name="VCALENDAR">
  411. <C:comp-filter name="VEVENT">
  412. <C:prop-filter name="UNKNOWN">
  413. <C:text-match>event</C:text-match>
  414. </C:prop-filter>
  415. </C:comp-filter>
  416. </C:comp-filter>"""])
  417. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  418. <C:comp-filter name="VCALENDAR">
  419. <C:comp-filter name="VEVENT">
  420. <C:prop-filter name="SUMMARY">
  421. <C:text-match>unknown</C:text-match>
  422. </C:prop-filter>
  423. </C:comp-filter>
  424. </C:comp-filter>"""])
  425. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  426. <C:comp-filter name="VCALENDAR">
  427. <C:comp-filter name="VEVENT">
  428. <C:prop-filter name="SUMMARY">
  429. <C:text-match negate-condition="yes">event</C:text-match>
  430. </C:prop-filter>
  431. </C:comp-filter>
  432. </C:comp-filter>"""])
  433. def test_param_filter(self):
  434. """Report request with param-filter on calendar."""
  435. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  436. <C:comp-filter name="VCALENDAR">
  437. <C:comp-filter name="VEVENT">
  438. <C:prop-filter name="ATTENDEE">
  439. <C:param-filter name="PARTSTAT">
  440. <C:text-match collation="i;ascii-casemap"
  441. >ACCEPTED</C:text-match>
  442. </C:param-filter>
  443. </C:prop-filter>
  444. </C:comp-filter>
  445. </C:comp-filter>"""])
  446. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  447. <C:comp-filter name="VCALENDAR">
  448. <C:comp-filter name="VEVENT">
  449. <C:prop-filter name="ATTENDEE">
  450. <C:param-filter name="PARTSTAT">
  451. <C:text-match collation="i;ascii-casemap"
  452. >UNKNOWN</C:text-match>
  453. </C:param-filter>
  454. </C:prop-filter>
  455. </C:comp-filter>
  456. </C:comp-filter>"""])
  457. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  458. <C:comp-filter name="VCALENDAR">
  459. <C:comp-filter name="VEVENT">
  460. <C:prop-filter name="ATTENDEE">
  461. <C:param-filter name="PARTSTAT">
  462. <C:is-not-defined />
  463. </C:param-filter>
  464. </C:prop-filter>
  465. </C:comp-filter>
  466. </C:comp-filter>"""])
  467. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  468. <C:comp-filter name="VCALENDAR">
  469. <C:comp-filter name="VEVENT">
  470. <C:prop-filter name="ATTENDEE">
  471. <C:param-filter name="UNKNOWN">
  472. <C:is-not-defined />
  473. </C:param-filter>
  474. </C:prop-filter>
  475. </C:comp-filter>
  476. </C:comp-filter>"""])
  477. def test_time_range_filter_events(self):
  478. """Report request with time-range filter on events."""
  479. answer = self._test_filter(["""
  480. <C:comp-filter name="VCALENDAR">
  481. <C:comp-filter name="VEVENT">
  482. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  483. </C:comp-filter>
  484. </C:comp-filter>"""], "event", items=5)
  485. assert "href>/calendar.ics/event1.ics</" in answer
  486. assert "href>/calendar.ics/event2.ics</" in answer
  487. assert "href>/calendar.ics/event3.ics</" in answer
  488. assert "href>/calendar.ics/event4.ics</" in answer
  489. assert "href>/calendar.ics/event5.ics</" in answer
  490. answer = self._test_filter(["""
  491. <C:comp-filter name="VCALENDAR">
  492. <C:comp-filter name="VEVENT">
  493. <C:prop-filter name="ATTENDEE">
  494. <C:param-filter name="PARTSTAT">
  495. <C:is-not-defined />
  496. </C:param-filter>
  497. </C:prop-filter>
  498. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  499. </C:comp-filter>
  500. </C:comp-filter>"""], items=5)
  501. assert "href>/calendar.ics/event1.ics</" not in answer
  502. assert "href>/calendar.ics/event2.ics</" not in answer
  503. assert "href>/calendar.ics/event3.ics</" not in answer
  504. assert "href>/calendar.ics/event4.ics</" not in answer
  505. assert "href>/calendar.ics/event5.ics</" not in answer
  506. answer = self._test_filter(["""
  507. <C:comp-filter name="VCALENDAR">
  508. <C:comp-filter name="VEVENT">
  509. <C:time-range start="20130902T000000Z" end="20131001T000000Z"/>
  510. </C:comp-filter>
  511. </C:comp-filter>"""], items=5)
  512. assert "href>/calendar.ics/event1.ics</" not in answer
  513. assert "href>/calendar.ics/event2.ics</" in answer
  514. assert "href>/calendar.ics/event3.ics</" in answer
  515. assert "href>/calendar.ics/event4.ics</" in answer
  516. assert "href>/calendar.ics/event5.ics</" in answer
  517. answer = self._test_filter(["""
  518. <C:comp-filter name="VCALENDAR">
  519. <C:comp-filter name="VEVENT">
  520. <C:time-range start="20130903T000000Z" end="20130908T000000Z"/>
  521. </C:comp-filter>
  522. </C:comp-filter>"""], items=5)
  523. assert "href>/calendar.ics/event1.ics</" not in answer
  524. assert "href>/calendar.ics/event2.ics</" not in answer
  525. assert "href>/calendar.ics/event3.ics</" in answer
  526. assert "href>/calendar.ics/event4.ics</" in answer
  527. assert "href>/calendar.ics/event5.ics</" in answer
  528. answer = self._test_filter(["""
  529. <C:comp-filter name="VCALENDAR">
  530. <C:comp-filter name="VEVENT">
  531. <C:time-range start="20130903T000000Z" end="20130904T000000Z"/>
  532. </C:comp-filter>
  533. </C:comp-filter>"""], items=5)
  534. assert "href>/calendar.ics/event1.ics</" not in answer
  535. assert "href>/calendar.ics/event2.ics</" not in answer
  536. assert "href>/calendar.ics/event3.ics</" in answer
  537. assert "href>/calendar.ics/event4.ics</" not in answer
  538. assert "href>/calendar.ics/event5.ics</" not in answer
  539. answer = self._test_filter(["""
  540. <C:comp-filter name="VCALENDAR">
  541. <C:comp-filter name="VEVENT">
  542. <C:time-range start="20130805T000000Z" end="20130810T000000Z"/>
  543. </C:comp-filter>
  544. </C:comp-filter>"""], items=5)
  545. assert "href>/calendar.ics/event1.ics</" not in answer
  546. assert "href>/calendar.ics/event2.ics</" not in answer
  547. assert "href>/calendar.ics/event3.ics</" not in answer
  548. assert "href>/calendar.ics/event4.ics</" not in answer
  549. assert "href>/calendar.ics/event5.ics</" not in answer
  550. answer = self._test_filter(["""
  551. <C:comp-filter name="VCALENDAR">
  552. <C:comp-filter name="VEVENT">
  553. <C:time-range start="20170701T060000Z"/>
  554. </C:comp-filter>
  555. </C:comp-filter>"""], items=7)
  556. # HACK: VObject doesn't match RECURRENCE-ID to recurrences, the
  557. # overwritten recurrence is still used for filtering.
  558. assert "href>/calendar.ics/event6.ics</" in answer
  559. assert "href>/calendar.ics/event7.ics</" in answer
  560. answer = self._test_filter(["""
  561. <C:comp-filter name="VCALENDAR">
  562. <C:comp-filter name="VEVENT">
  563. <C:time-range start="20170701T080000Z"/>
  564. </C:comp-filter>
  565. </C:comp-filter>"""], items=7)
  566. assert "href>/calendar.ics/event6.ics</" not in answer
  567. assert "href>/calendar.ics/event7.ics</" not in answer
  568. def test_time_range_filter_events_rrule(self):
  569. """Report request with time-range filter on events with rrules."""
  570. answer = self._test_filter(["""
  571. <C:comp-filter name="VCALENDAR">
  572. <C:comp-filter name="VEVENT">
  573. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  574. </C:comp-filter>
  575. </C:comp-filter>"""], "event", items=2)
  576. assert "href>/calendar.ics/event1.ics</" in answer
  577. assert "href>/calendar.ics/event2.ics</" in answer
  578. answer = self._test_filter(["""
  579. <C:comp-filter name="VCALENDAR">
  580. <C:comp-filter name="VEVENT">
  581. <C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
  582. </C:comp-filter>
  583. </C:comp-filter>"""], "event", items=2)
  584. assert "href>/calendar.ics/event1.ics</" not in answer
  585. assert "href>/calendar.ics/event2.ics</" in answer
  586. answer = self._test_filter(["""
  587. <C:comp-filter name="VCALENDAR">
  588. <C:comp-filter name="VEVENT">
  589. <C:time-range start="20120801T000000Z" end="20121001T000000Z"/>
  590. </C:comp-filter>
  591. </C:comp-filter>"""], "event", items=2)
  592. assert "href>/calendar.ics/event1.ics</" not in answer
  593. assert "href>/calendar.ics/event2.ics</" not in answer
  594. answer = self._test_filter(["""
  595. <C:comp-filter name="VCALENDAR">
  596. <C:comp-filter name="VEVENT">
  597. <C:time-range start="20130903T000000Z" end="20130907T000000Z"/>
  598. </C:comp-filter>
  599. </C:comp-filter>"""], "event", items=2)
  600. assert "href>/calendar.ics/event1.ics</" not in answer
  601. assert "href>/calendar.ics/event2.ics</" not in answer
  602. def test_time_range_filter_todos(self):
  603. """Report request with time-range filter on todos."""
  604. answer = self._test_filter(["""
  605. <C:comp-filter name="VCALENDAR">
  606. <C:comp-filter name="VTODO">
  607. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  608. </C:comp-filter>
  609. </C:comp-filter>"""], "todo", items=8)
  610. assert "href>/calendar.ics/todo1.ics</" in answer
  611. assert "href>/calendar.ics/todo2.ics</" in answer
  612. assert "href>/calendar.ics/todo3.ics</" in answer
  613. assert "href>/calendar.ics/todo4.ics</" in answer
  614. assert "href>/calendar.ics/todo5.ics</" in answer
  615. assert "href>/calendar.ics/todo6.ics</" in answer
  616. assert "href>/calendar.ics/todo7.ics</" in answer
  617. assert "href>/calendar.ics/todo8.ics</" in answer
  618. answer = self._test_filter(["""
  619. <C:comp-filter name="VCALENDAR">
  620. <C:comp-filter name="VTODO">
  621. <C:time-range start="20130901T160000Z" end="20130901T183000Z"/>
  622. </C:comp-filter>
  623. </C:comp-filter>"""], "todo", items=8)
  624. assert "href>/calendar.ics/todo1.ics</" not in answer
  625. assert "href>/calendar.ics/todo2.ics</" in answer
  626. assert "href>/calendar.ics/todo3.ics</" in answer
  627. assert "href>/calendar.ics/todo4.ics</" not in answer
  628. assert "href>/calendar.ics/todo5.ics</" not in answer
  629. assert "href>/calendar.ics/todo6.ics</" not in answer
  630. assert "href>/calendar.ics/todo7.ics</" in answer
  631. assert "href>/calendar.ics/todo8.ics</" in answer
  632. answer = self._test_filter(["""
  633. <C:comp-filter name="VCALENDAR">
  634. <C:comp-filter name="VTODO">
  635. <C:time-range start="20130903T160000Z" end="20130901T183000Z"/>
  636. </C:comp-filter>
  637. </C:comp-filter>"""], "todo", items=8)
  638. assert "href>/calendar.ics/todo2.ics</" not in answer
  639. answer = self._test_filter(["""
  640. <C:comp-filter name="VCALENDAR">
  641. <C:comp-filter name="VTODO">
  642. <C:time-range start="20130903T160000Z" end="20130901T173000Z"/>
  643. </C:comp-filter>
  644. </C:comp-filter>"""], "todo", items=8)
  645. assert "href>/calendar.ics/todo2.ics</" not in answer
  646. answer = self._test_filter(["""
  647. <C:comp-filter name="VCALENDAR">
  648. <C:comp-filter name="VTODO">
  649. <C:time-range start="20130903T160000Z" end="20130903T173000Z"/>
  650. </C:comp-filter>
  651. </C:comp-filter>"""], "todo", items=8)
  652. assert "href>/calendar.ics/todo3.ics</" not in answer
  653. answer = self._test_filter(["""
  654. <C:comp-filter name="VCALENDAR">
  655. <C:comp-filter name="VTODO">
  656. <C:time-range start="20130903T160000Z" end="20130803T203000Z"/>
  657. </C:comp-filter>
  658. </C:comp-filter>"""], "todo", items=8)
  659. assert "href>/calendar.ics/todo7.ics</" in answer
  660. def test_time_range_filter_todos_rrule(self):
  661. """Report request with time-range filter on todos with rrules."""
  662. answer = self._test_filter(["""
  663. <C:comp-filter name="VCALENDAR">
  664. <C:comp-filter name="VTODO">
  665. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  666. </C:comp-filter>
  667. </C:comp-filter>"""], "todo", items=2)
  668. assert "href>/calendar.ics/todo1.ics</" in answer
  669. assert "href>/calendar.ics/todo2.ics</" in answer
  670. answer = self._test_filter(["""
  671. <C:comp-filter name="VCALENDAR">
  672. <C:comp-filter name="VTODO">
  673. <C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
  674. </C:comp-filter>
  675. </C:comp-filter>"""], "todo", items=2)
  676. assert "href>/calendar.ics/todo1.ics</" not in answer
  677. assert "href>/calendar.ics/todo2.ics</" in answer
  678. answer = self._test_filter(["""
  679. <C:comp-filter name="VCALENDAR">
  680. <C:comp-filter name="VTODO">
  681. <C:time-range start="20140902T000000Z" end="20140903T000000Z"/>
  682. </C:comp-filter>
  683. </C:comp-filter>"""], "todo", items=2)
  684. assert "href>/calendar.ics/todo1.ics</" not in answer
  685. assert "href>/calendar.ics/todo2.ics</" in answer
  686. answer = self._test_filter(["""
  687. <C:comp-filter name="VCALENDAR">
  688. <C:comp-filter name="VTODO">
  689. <C:time-range start="20140904T000000Z" end="20140914T000000Z"/>
  690. </C:comp-filter>
  691. </C:comp-filter>"""], "todo", items=2)
  692. assert "href>/calendar.ics/todo1.ics</" not in answer
  693. assert "href>/calendar.ics/todo2.ics</" not in answer
  694. def test_time_range_filter_journals(self):
  695. """Report request with time-range filter on journals."""
  696. answer = self._test_filter(["""
  697. <C:comp-filter name="VCALENDAR">
  698. <C:comp-filter name="VJOURNAL">
  699. <C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
  700. </C:comp-filter>
  701. </C:comp-filter>"""], "journal", items=3)
  702. assert "href>/calendar.ics/journal1.ics</" not in answer
  703. assert "href>/calendar.ics/journal2.ics</" in answer
  704. assert "href>/calendar.ics/journal3.ics</" in answer
  705. answer = self._test_filter(["""
  706. <C:comp-filter name="VCALENDAR">
  707. <C:comp-filter name="VJOURNAL">
  708. <C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
  709. </C:comp-filter>
  710. </C:comp-filter>"""], "journal", items=3)
  711. assert "href>/calendar.ics/journal1.ics</" not in answer
  712. assert "href>/calendar.ics/journal2.ics</" in answer
  713. assert "href>/calendar.ics/journal3.ics</" in answer
  714. answer = self._test_filter(["""
  715. <C:comp-filter name="VCALENDAR">
  716. <C:comp-filter name="VJOURNAL">
  717. <C:time-range start="19981229T000000Z" end="19991012T000000Z"/>
  718. </C:comp-filter>
  719. </C:comp-filter>"""], "journal", items=3)
  720. assert "href>/calendar.ics/journal1.ics</" not in answer
  721. assert "href>/calendar.ics/journal2.ics</" not in answer
  722. assert "href>/calendar.ics/journal3.ics</" not in answer
  723. answer = self._test_filter(["""
  724. <C:comp-filter name="VCALENDAR">
  725. <C:comp-filter name="VJOURNAL">
  726. <C:time-range start="20131229T000000Z" end="21520202T000000Z"/>
  727. </C:comp-filter>
  728. </C:comp-filter>"""], "journal", items=3)
  729. assert "href>/calendar.ics/journal1.ics</" not in answer
  730. assert "href>/calendar.ics/journal2.ics</" in answer
  731. assert "href>/calendar.ics/journal3.ics</" not in answer
  732. answer = self._test_filter(["""
  733. <C:comp-filter name="VCALENDAR">
  734. <C:comp-filter name="VJOURNAL">
  735. <C:time-range start="20000101T000000Z" end="20000202T000000Z"/>
  736. </C:comp-filter>
  737. </C:comp-filter>"""], "journal", items=3)
  738. assert "href>/calendar.ics/journal1.ics</" not in answer
  739. assert "href>/calendar.ics/journal2.ics</" in answer
  740. assert "href>/calendar.ics/journal3.ics</" in answer
  741. def test_time_range_filter_journals_rrule(self):
  742. """Report request with time-range filter on journals with rrules."""
  743. answer = self._test_filter(["""
  744. <C:comp-filter name="VCALENDAR">
  745. <C:comp-filter name="VJOURNAL">
  746. <C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
  747. </C:comp-filter>
  748. </C:comp-filter>"""], "journal", items=2)
  749. assert "href>/calendar.ics/journal1.ics</" not in answer
  750. assert "href>/calendar.ics/journal2.ics</" in answer
  751. answer = self._test_filter(["""
  752. <C:comp-filter name="VCALENDAR">
  753. <C:comp-filter name="VJOURNAL">
  754. <C:time-range start="20051229T000000Z" end="20060202T000000Z"/>
  755. </C:comp-filter>
  756. </C:comp-filter>"""], "journal", items=2)
  757. assert "href>/calendar.ics/journal1.ics</" not in answer
  758. assert "href>/calendar.ics/journal2.ics</" in answer
  759. answer = self._test_filter(["""
  760. <C:comp-filter name="VCALENDAR">
  761. <C:comp-filter name="VJOURNAL">
  762. <C:time-range start="20060102T000000Z" end="20060202T000000Z"/>
  763. </C:comp-filter>
  764. </C:comp-filter>"""], "journal", items=2)
  765. assert "href>/calendar.ics/journal1.ics</" not in answer
  766. assert "href>/calendar.ics/journal2.ics</" not in answer
  767. def test_report_item(self):
  768. """Test report request on an item"""
  769. calendar_path = "/calendar.ics/"
  770. self.request("MKCALENDAR", calendar_path)
  771. event = get_file_content("event1.ics")
  772. event_path = posixpath.join(calendar_path, "event.ics")
  773. self.request("PUT", event_path, event)
  774. status, headers, answer = self.request(
  775. "REPORT", event_path,
  776. """<?xml version="1.0" encoding="utf-8" ?>
  777. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  778. <D:prop xmlns:D="DAV:">
  779. <D:getetag />
  780. </D:prop>
  781. </C:calendar-query>""")
  782. assert status == 207
  783. assert "href>%s<" % event_path in answer
  784. def _report_sync_token(self, calendar_path, sync_token=None):
  785. sync_token_xml = (
  786. "<sync-token><![CDATA[%s]]></sync-token>" % sync_token
  787. if sync_token else "<sync-token />")
  788. status, headers, answer = self.request(
  789. "REPORT", calendar_path,
  790. """<?xml version="1.0" encoding="utf-8" ?>
  791. <sync-collection xmlns="DAV:">
  792. <prop>
  793. <getetag />
  794. </prop>
  795. %s
  796. </sync-collection>""" % sync_token_xml)
  797. if sync_token and status == 412:
  798. return None, None
  799. assert status == 207
  800. xml = ET.fromstring(answer)
  801. sync_token = xml.find("{DAV:}sync-token").text.strip()
  802. assert sync_token
  803. return sync_token, xml
  804. def test_report_sync_collection_no_change(self):
  805. """Test sync-collection report without modifying the collection"""
  806. calendar_path = "/calendar.ics/"
  807. self.request("MKCALENDAR", calendar_path)
  808. event = get_file_content("event1.ics")
  809. event_path = posixpath.join(calendar_path, "event.ics")
  810. self.request("PUT", event_path, event)
  811. sync_token, xml = self._report_sync_token(calendar_path)
  812. assert xml.find("{DAV:}response") is not None
  813. new_sync_token, xml = self._report_sync_token(calendar_path,
  814. sync_token)
  815. assert sync_token == new_sync_token
  816. assert xml.find("{DAV:}response") is None
  817. def test_report_sync_collection_add(self):
  818. """Test sync-collection report with an added item"""
  819. calendar_path = "/calendar.ics/"
  820. self.request("MKCALENDAR", calendar_path)
  821. sync_token, xml = self._report_sync_token(calendar_path)
  822. event = get_file_content("event1.ics")
  823. event_path = posixpath.join(calendar_path, "event.ics")
  824. self.request("PUT", event_path, event)
  825. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  826. if not sync_token:
  827. pytest.skip("storage backend does not support sync-token")
  828. assert xml.find("{DAV:}response") is not None
  829. assert xml.find("{DAV:}response/{DAV:}status") is None
  830. def test_report_sync_collection_delete(self):
  831. """Test sync-collection report with a deleted item"""
  832. calendar_path = "/calendar.ics/"
  833. self.request("MKCALENDAR", calendar_path)
  834. event = get_file_content("event1.ics")
  835. event_path = posixpath.join(calendar_path, "event.ics")
  836. self.request("PUT", event_path, event)
  837. sync_token, xml = self._report_sync_token(calendar_path)
  838. self.request("DELETE", event_path)
  839. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  840. if not sync_token:
  841. pytest.skip("storage backend does not support sync-token")
  842. assert "404" in xml.find("{DAV:}response/{DAV:}status").text
  843. def test_report_sync_collection_create_delete(self):
  844. """Test sync-collection report with a created and deleted item"""
  845. calendar_path = "/calendar.ics/"
  846. self.request("MKCALENDAR", calendar_path)
  847. sync_token, xml = self._report_sync_token(calendar_path)
  848. event = get_file_content("event1.ics")
  849. event_path = posixpath.join(calendar_path, "event.ics")
  850. self.request("PUT", event_path, event)
  851. self.request("DELETE", event_path)
  852. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  853. if not sync_token:
  854. pytest.skip("storage backend does not support sync-token")
  855. assert "404" in xml.find("{DAV:}response/{DAV:}status").text
  856. def test_report_sync_collection_modify_undo(self):
  857. """Test sync-collection report with a modified and changed back item"""
  858. calendar_path = "/calendar.ics/"
  859. self.request("MKCALENDAR", calendar_path)
  860. event1 = get_file_content("event1.ics")
  861. event2 = get_file_content("event2.ics")
  862. event_path = posixpath.join(calendar_path, "event1.ics")
  863. self.request("PUT", event_path, event1)
  864. sync_token, xml = self._report_sync_token(calendar_path)
  865. self.request("PUT", event_path, event2)
  866. self.request("PUT", event_path, event1)
  867. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  868. if not sync_token:
  869. pytest.skip("storage backend does not support sync-token")
  870. assert xml.find("{DAV:}response") is not None
  871. assert xml.find("{DAV:}response/{DAV:}status") is None
  872. def test_report_sync_collection_move(self):
  873. """Test sync-collection report a moved item"""
  874. calendar_path = "/calendar.ics/"
  875. self.request("MKCALENDAR", calendar_path)
  876. event = get_file_content("event1.ics")
  877. event1_path = posixpath.join(calendar_path, "event1.ics")
  878. event2_path = posixpath.join(calendar_path, "event2.ics")
  879. self.request("PUT", event1_path, event)
  880. sync_token, xml = self._report_sync_token(calendar_path)
  881. status, headers, answer = self.request(
  882. "MOVE", event1_path, HTTP_DESTINATION=event2_path, HTTP_HOST="")
  883. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  884. if not sync_token:
  885. pytest.skip("storage backend does not support sync-token")
  886. for response in xml.findall("{DAV:}response"):
  887. if response.find("{DAV:}status") is None:
  888. assert response.find("{DAV:}href").text == event2_path
  889. else:
  890. assert "404" in response.find("{DAV:}status").text
  891. assert response.find("{DAV:}href").text == event1_path
  892. def test_report_sync_collection_move_undo(self):
  893. """Test sync-collection report with a moved and moved back item"""
  894. calendar_path = "/calendar.ics/"
  895. self.request("MKCALENDAR", calendar_path)
  896. event = get_file_content("event1.ics")
  897. event1_path = posixpath.join(calendar_path, "event1.ics")
  898. event2_path = posixpath.join(calendar_path, "event2.ics")
  899. self.request("PUT", event1_path, event)
  900. sync_token, xml = self._report_sync_token(calendar_path)
  901. status, headers, answer = self.request(
  902. "MOVE", event1_path, HTTP_DESTINATION=event2_path, HTTP_HOST="")
  903. status, headers, answer = self.request(
  904. "MOVE", event2_path, HTTP_DESTINATION=event1_path, HTTP_HOST="")
  905. sync_token, xml = self._report_sync_token(calendar_path, sync_token)
  906. if not sync_token:
  907. pytest.skip("storage backend does not support sync-token")
  908. created = deleted = 0
  909. for response in xml.findall("{DAV:}response"):
  910. if response.find("{DAV:}status") is None:
  911. assert response.find("{DAV:}href").text == event1_path
  912. created += 1
  913. else:
  914. assert "404" in response.find("{DAV:}status").text
  915. assert response.find("{DAV:}href").text == event2_path
  916. deleted += 1
  917. assert created == 1 and deleted == 1
  918. def test_report_sync_collection_invalid_sync_token(self):
  919. """Test sync-collection report with an invalid sync token"""
  920. calendar_path = "/calendar.ics/"
  921. self.request("MKCALENDAR", calendar_path)
  922. sync_token, xml = self._report_sync_token(
  923. calendar_path, "http://radicale.org/ns/sync/INVALID")
  924. assert not sync_token
  925. def test_propfind_sync_token(self):
  926. """Retrieve the sync-token with a propfind request"""
  927. calendar_path = "/calendar.ics/"
  928. self.request("MKCALENDAR", calendar_path)
  929. sync_token, xml = self._report_sync_token(calendar_path)
  930. event = get_file_content("event1.ics")
  931. event_path = posixpath.join(calendar_path, "event.ics")
  932. self.request("PUT", event_path, event)
  933. new_sync_token, xml = self._report_sync_token(calendar_path,
  934. sync_token)
  935. assert sync_token != new_sync_token
  936. def test_propfind_same_as_sync_collection_sync_token(self):
  937. """Compare sync-token property with sync-collection sync-token"""
  938. calendar_path = "/calendar.ics/"
  939. self.request("MKCALENDAR", calendar_path)
  940. sync_token, xml = self._report_sync_token(calendar_path)
  941. new_sync_token, xml = self._report_sync_token(calendar_path,
  942. sync_token)
  943. assert sync_token == new_sync_token
  944. def test_authorization(self):
  945. authorization = "Basic " + base64.b64encode(b"user:").decode()
  946. status, headers, answer = self.request(
  947. "PROPFIND", "/",
  948. """<?xml version="1.0" encoding="utf-8"?>
  949. <propfind xmlns="DAV:">
  950. <prop>
  951. <current-user-principal />
  952. </prop>
  953. </propfind>""",
  954. HTTP_AUTHORIZATION=authorization)
  955. assert status == 207
  956. assert "href>/user/<" in answer
  957. def test_authentication(self):
  958. """Test if server sends authentication request."""
  959. self.configuration["auth"]["type"] = "htpasswd"
  960. self.configuration["auth"]["htpasswd_filename"] = os.devnull
  961. self.configuration["auth"]["htpasswd_encryption"] = "plain"
  962. self.configuration["rights"]["type"] = "owner_only"
  963. self.application = Application(self.configuration, self.logger)
  964. status, headers, answer = self.request("MKCOL", "/user/")
  965. assert status in (401, 403)
  966. assert headers.get("WWW-Authenticate")
  967. def test_principal_collection_creation(self):
  968. """Verify existence of the principal collection."""
  969. status, headers, answer = self.request(
  970. "PROPFIND", "/user/", HTTP_AUTHORIZATION=(
  971. "Basic " + base64.b64encode(b"user:").decode()))
  972. assert status == 207
  973. def test_existence_of_root_collections(self):
  974. """Verify that the root collection always exists."""
  975. # Use PROPFIND because GET returns message
  976. status, headers, answer = self.request("PROPFIND", "/")
  977. assert status == 207
  978. # it should still exist after deletion
  979. self.request("DELETE", "/")
  980. status, headers, answer = self.request("PROPFIND", "/")
  981. assert status == 207
  982. def test_fsync(self):
  983. """Create a directory and file with syncing enabled."""
  984. self.configuration["storage"]["filesystem_fsync"] = "True"
  985. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  986. assert status == 201
  987. def test_hook(self):
  988. """Run hook."""
  989. self.configuration["storage"]["hook"] = (
  990. "mkdir %s" % os.path.join("collection-root", "created_by_hook"))
  991. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  992. assert status == 201
  993. status, _, _ = self.request("PROPFIND", "/created_by_hook/")
  994. assert status == 207
  995. def test_hook_read_access(self):
  996. """Verify that hook is not run for read accesses."""
  997. self.configuration["storage"]["hook"] = (
  998. "mkdir %s" % os.path.join("collection-root", "created_by_hook"))
  999. status, headers, answer = self.request("GET", "/")
  1000. assert status == 303
  1001. status, headers, answer = self.request("GET", "/created_by_hook/")
  1002. assert status == 404
  1003. @pytest.mark.skipif(os.system("type flock") != 0,
  1004. reason="flock command not found")
  1005. def test_hook_storage_locked(self):
  1006. """Verify that the storage is locked when the hook runs."""
  1007. self.configuration["storage"]["hook"] = (
  1008. "flock -n .Radicale.lock || exit 0; exit 1")
  1009. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1010. assert status == 201
  1011. def test_hook_principal_collection_creation(self):
  1012. """Verify that the hooks runs when a new user is created."""
  1013. self.configuration["storage"]["hook"] = (
  1014. "mkdir %s" % os.path.join("collection-root", "created_by_hook"))
  1015. status, headers, answer = self.request(
  1016. "GET", "/", HTTP_AUTHORIZATION=(
  1017. "Basic " + base64.b64encode(b"user:").decode()))
  1018. assert status == 303
  1019. status, headers, answer = self.request("PROPFIND", "/created_by_hook/")
  1020. assert status == 207
  1021. def test_hook_fail(self):
  1022. """Verify that a request fails if the hook fails."""
  1023. self.configuration["storage"]["hook"] = "exit 1"
  1024. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1025. assert status != 201
  1026. def test_custom_headers(self):
  1027. if not self.configuration.has_section("headers"):
  1028. self.configuration.add_section("headers")
  1029. self.configuration.set("headers", "test", "123")
  1030. # Test if header is set on success
  1031. status, headers, answer = self.request("GET", "/")
  1032. assert headers.get("test") == "123"
  1033. # Test if header is set on failure
  1034. status, headers, answer = self.request(
  1035. "GET", "/.well-known/does not exist")
  1036. assert headers.get("test") == "123"
  1037. class BaseFileSystemTest(BaseTest):
  1038. """Base class for filesystem backend tests."""
  1039. storage_type = None
  1040. def setup(self):
  1041. self.configuration = config.load()
  1042. self.configuration["storage"]["type"] = self.storage_type
  1043. self.colpath = tempfile.mkdtemp()
  1044. self.configuration["storage"]["filesystem_folder"] = self.colpath
  1045. # Disable syncing to disk for better performance
  1046. self.configuration["storage"]["filesystem_fsync"] = "False"
  1047. # Required on Windows, doesn't matter on Unix
  1048. self.configuration["storage"]["filesystem_close_lock_file"] = "True"
  1049. self.application = Application(self.configuration, self.logger)
  1050. def teardown(self):
  1051. shutil.rmtree(self.colpath)
  1052. class TestMultiFileSystem(BaseFileSystemTest, BaseRequestsMixIn):
  1053. """Test BaseRequests on multifilesystem."""
  1054. storage_type = "multifilesystem"
  1055. class TestCustomStorageSystem(BaseFileSystemTest):
  1056. """Test custom backend loading."""
  1057. storage_type = "tests.custom.storage"
  1058. def test_root(self):
  1059. """A simple test to verify that the custom backend works."""
  1060. BaseRequestsMixIn.test_root(self)