test_base.py 49 KB

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