test_base.py 68 KB

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