test_base.py 70 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2012-2017 Guillaume Ayoub
  3. # Copyright © 2017-2018 Unrud <unrud@outlook.com>
  4. #
  5. # This library is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This library is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  17. """
  18. Radicale tests with simple requests.
  19. """
  20. import base64
  21. import os
  22. import posixpath
  23. import shutil
  24. import sys
  25. import tempfile
  26. import xml.etree.ElementTree as ET
  27. from functools import partial
  28. import pytest
  29. from radicale import Application, config
  30. from . import BaseTest
  31. from .helpers import get_file_content
  32. class BaseRequestsMixIn:
  33. """Tests with simple requests."""
  34. def test_root(self):
  35. """GET request at "/"."""
  36. status, _, answer = self.request("GET", "/")
  37. assert status == 302
  38. assert answer == "Redirected to .web"
  39. def test_script_name(self):
  40. """GET request at "/" with SCRIPT_NAME."""
  41. status, _, answer = self.request("GET", "/", SCRIPT_NAME="/radicale")
  42. assert status == 302
  43. assert answer == "Redirected to .web"
  44. status, _, answer = self.request("GET", "", SCRIPT_NAME="/radicale")
  45. assert status == 302
  46. assert answer == "Redirected to radicale/.web"
  47. def test_add_event(self):
  48. """Add an event."""
  49. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  50. assert status == 201
  51. event = get_file_content("event1.ics")
  52. path = "/calendar.ics/event1.ics"
  53. status, _, _ = self.request("PUT", path, event)
  54. assert status == 201
  55. status, headers, answer = self.request("GET", path)
  56. assert status == 200
  57. assert "ETag" in headers
  58. assert headers["Content-Type"] == "text/calendar; charset=utf-8"
  59. assert "VEVENT" in answer
  60. assert "Event" in answer
  61. assert "UID:event" in answer
  62. def test_add_event_without_uid(self):
  63. """Add an event without UID."""
  64. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  65. assert status == 201
  66. event = get_file_content("event1.ics").replace("UID:event1\n", "")
  67. assert "\nUID:" not in event
  68. path = "/calendar.ics/event.ics"
  69. status, _, _ = self.request("PUT", path, event)
  70. assert status == 400
  71. def test_add_todo(self):
  72. """Add a todo."""
  73. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  74. assert status == 201
  75. todo = get_file_content("todo1.ics")
  76. path = "/calendar.ics/todo1.ics"
  77. status, _, _ = self.request("PUT", path, todo)
  78. assert status == 201
  79. status, headers, answer = self.request("GET", path)
  80. assert status == 200
  81. assert "ETag" in headers
  82. assert headers["Content-Type"] == "text/calendar; charset=utf-8"
  83. assert "VTODO" in answer
  84. assert "Todo" in answer
  85. assert "UID:todo" in answer
  86. def _create_addressbook(self, path):
  87. return self.request(
  88. "MKCOL", path, """\
  89. <?xml version="1.0" encoding="UTF-8" ?>
  90. <create xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
  91. <set>
  92. <prop>
  93. <resourcetype>
  94. <collection />
  95. <CR:addressbook />
  96. </resourcetype>
  97. </prop>
  98. </set>
  99. </create>""")
  100. def test_add_contact(self):
  101. """Add a contact."""
  102. status, _, _ = self._create_addressbook("/contacts.vcf/")
  103. assert status == 201
  104. contact = get_file_content("contact1.vcf")
  105. path = "/contacts.vcf/contact.vcf"
  106. status, _, _ = self.request("PUT", path, contact)
  107. assert status == 201
  108. status, headers, answer = self.request("GET", path)
  109. assert status == 200
  110. assert "ETag" in headers
  111. assert headers["Content-Type"] == "text/vcard; charset=utf-8"
  112. assert "VCARD" in answer
  113. assert "UID:contact1" in answer
  114. status, _, answer = self.request("GET", path)
  115. assert status == 200
  116. assert "UID:contact1" in answer
  117. def test_add_contact_without_uid(self):
  118. """Add a contact without UID."""
  119. status, _, _ = self._create_addressbook("/contacts.vcf/")
  120. assert status == 201
  121. contact = get_file_content("contact1.vcf").replace("UID:contact1\n",
  122. "")
  123. assert "\nUID" not in contact
  124. path = "/contacts.vcf/contact.vcf"
  125. status, _, _ = self.request("PUT", path, contact)
  126. assert status == 400
  127. def test_update(self):
  128. """Update an event."""
  129. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  130. assert status == 201
  131. event = get_file_content("event1.ics")
  132. path = "/calendar.ics/event1.ics"
  133. status, _, _ = self.request("PUT", path, event)
  134. assert status == 201
  135. status, headers, answer = self.request("GET", path)
  136. assert "ETag" in headers
  137. assert status == 200
  138. assert "VEVENT" in answer
  139. assert "Event" in answer
  140. assert "UID:event" in answer
  141. assert "DTSTART;TZID=Europe/Paris:20130901T180000" in answer
  142. assert "DTEND;TZID=Europe/Paris:20130901T190000" in answer
  143. # Then we send another PUT request
  144. event = get_file_content("event1-prime.ics")
  145. status, _, _ = self.request("PUT", path, event)
  146. assert status == 201
  147. status, _, answer = self.request("GET", "/calendar.ics/")
  148. assert status == 200
  149. assert answer.count("BEGIN:VEVENT") == 1
  150. status, headers, answer = self.request("GET", path)
  151. assert status == 200
  152. assert "ETag" in headers
  153. assert "VEVENT" in answer
  154. assert "Event" in answer
  155. assert "UID:event" in answer
  156. assert "DTSTART;TZID=Europe/Paris:20130901T180000" not in answer
  157. assert "DTEND;TZID=Europe/Paris:20130901T190000" not in answer
  158. assert "DTSTART;TZID=Europe/Paris:20140901T180000" in answer
  159. assert "DTEND;TZID=Europe/Paris:20140901T210000" in answer
  160. def test_put_whole_calendar(self):
  161. """Create and overwrite a whole calendar."""
  162. status, _, _ = self.request(
  163. "PUT", "/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
  164. assert status == 201
  165. event1 = get_file_content("event1.ics")
  166. status, _, _ = self.request(
  167. "PUT", "/calendar.ics/test_event.ics", event1)
  168. assert status == 201
  169. # Overwrite
  170. events = get_file_content("event_multiple.ics")
  171. status, _, _ = self.request("PUT", "/calendar.ics/", events)
  172. assert status == 201
  173. status, _, _ = self.request("GET", "/calendar.ics/test_event.ics")
  174. assert status == 404
  175. status, _, answer = self.request("GET", "/calendar.ics/")
  176. assert status == 200
  177. assert "\r\nUID:event\r\n" in answer and "\r\nUID:todo\r\n" in answer
  178. assert "\r\nUID:event1\r\n" not in answer
  179. def test_put_whole_calendar_without_uids(self):
  180. """Create a whole calendar without UID."""
  181. event = get_file_content("event_multiple.ics")
  182. event = event.replace("UID:event\n", "").replace("UID:todo\n", "")
  183. assert "\nUID:" not in event
  184. status, _, _ = self.request("PUT", "/calendar.ics/", event)
  185. assert status == 201
  186. status, _, answer = self.request("GET", "/calendar.ics")
  187. assert status == 200
  188. uids = []
  189. for line in answer.split("\r\n"):
  190. if line.startswith("UID:"):
  191. uids.append(line[len("UID:"):])
  192. assert len(uids) == 2
  193. for i, uid1 in enumerate(uids):
  194. assert uid1
  195. for uid2 in uids[i + 1:]:
  196. assert uid1 != uid2
  197. def test_put_whole_addressbook(self):
  198. """Create and overwrite a whole addressbook."""
  199. contacts = get_file_content("contact_multiple.vcf")
  200. status, _, _ = self.request("PUT", "/contacts.vcf/", contacts)
  201. assert status == 201
  202. status, _, answer = self.request("GET", "/contacts.vcf/")
  203. assert status == 200
  204. assert ("\r\nUID:contact1\r\n" in answer and
  205. "\r\nUID:contact2\r\n" in answer)
  206. def test_put_whole_addressbook_without_uids(self):
  207. """Create a whole addressbook without UID."""
  208. contacts = get_file_content("contact_multiple.vcf")
  209. contacts = contacts.replace("UID:contact1\n", "").replace(
  210. "UID:contact2\n", "")
  211. assert "\nUID:" not in contacts
  212. status, _, _ = self.request("PUT", "/contacts.vcf/", contacts)
  213. assert status == 201
  214. status, _, answer = self.request("GET", "/contacts.vcf")
  215. assert status == 200
  216. uids = []
  217. for line in answer.split("\r\n"):
  218. if line.startswith("UID:"):
  219. uids.append(line[len("UID:"):])
  220. assert len(uids) == 2
  221. for i, uid1 in enumerate(uids):
  222. assert uid1
  223. for uid2 in uids[i + 1:]:
  224. assert uid1 != uid2
  225. def test_delete(self):
  226. """Delete an event."""
  227. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  228. assert status == 201
  229. event = get_file_content("event1.ics")
  230. path = "/calendar.ics/event1.ics"
  231. status, _, _ = self.request("PUT", path, event)
  232. assert status == 201
  233. # Then we send a DELETE request
  234. status, _, answer = self.request("DELETE", path)
  235. assert status == 200
  236. assert "href>%s</" % path in answer
  237. status, _, answer = self.request("GET", "/calendar.ics/")
  238. assert status == 200
  239. assert "VEVENT" not in answer
  240. def test_mkcalendar(self):
  241. """Make a calendar."""
  242. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  243. assert status == 201
  244. status, _, answer = self.request("GET", "/calendar.ics/")
  245. assert status == 200
  246. assert "BEGIN:VCALENDAR" in answer
  247. assert "END:VCALENDAR" in answer
  248. def test_move(self):
  249. """Move a item."""
  250. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  251. assert status == 201
  252. event = get_file_content("event1.ics")
  253. path1 = "/calendar.ics/event1.ics"
  254. path2 = "/calendar.ics/event2.ics"
  255. status, _, _ = self.request("PUT", path1, event)
  256. assert status == 201
  257. status, _, _ = self.request(
  258. "MOVE", path1, HTTP_DESTINATION=path2, HTTP_HOST="")
  259. assert status == 201
  260. status, _, _ = self.request("GET", path1)
  261. assert status == 404
  262. status, _, _ = self.request("GET", path2)
  263. assert status == 200
  264. def test_head(self):
  265. status, _, _ = self.request("HEAD", "/")
  266. assert status == 302
  267. def test_options(self):
  268. status, headers, _ = self.request("OPTIONS", "/")
  269. assert status == 200
  270. assert "DAV" in headers
  271. def test_delete_collection(self):
  272. """Delete a collection."""
  273. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  274. assert status == 201
  275. event = get_file_content("event1.ics")
  276. self.request("PUT", "/calendar.ics/event1.ics", event)
  277. status, _, answer = self.request("DELETE", "/calendar.ics/")
  278. assert status == 200
  279. assert "href>/calendar.ics/</" in answer
  280. status, _, _ = self.request("GET", "/calendar.ics/")
  281. assert status == 404
  282. def test_delete_root_collection(self):
  283. """Delete the root collection."""
  284. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  285. assert status == 201
  286. event = get_file_content("event1.ics")
  287. status, _, _ = self.request("PUT", "/event1.ics", event)
  288. assert status == 201
  289. status, _, _ = self.request("PUT", "/calendar.ics/event1.ics", event)
  290. assert status == 201
  291. status, _, answer = self.request("DELETE", "/")
  292. assert status == 200
  293. assert "href>/</" in answer
  294. status, _, _ = self.request("GET", "/calendar.ics/")
  295. assert status == 404
  296. status, _, _ = self.request("GET", "/event1.ics")
  297. assert status == 404
  298. def test_propfind(self):
  299. calendar_path = "/calendar.ics/"
  300. status, _, _ = self.request("MKCALENDAR", calendar_path)
  301. assert status == 201
  302. event = get_file_content("event1.ics")
  303. event_path = posixpath.join(calendar_path, "event.ics")
  304. status, _, _ = self.request("PUT", event_path, event)
  305. assert status == 201
  306. status, _, answer = self.request("PROPFIND", "/", HTTP_DEPTH="1")
  307. assert status == 207
  308. assert "href>/</" in answer
  309. assert "href>%s</" % calendar_path in answer
  310. status, _, answer = self.request(
  311. "PROPFIND", calendar_path, HTTP_DEPTH="1")
  312. assert status == 207
  313. assert "href>%s</" % calendar_path in answer
  314. assert "href>%s</" % event_path in answer
  315. def test_propfind_propname(self):
  316. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  317. assert status == 201
  318. event = get_file_content("event1.ics")
  319. status, _, _ = self.request("PUT", "/calendar.ics/event.ics", event)
  320. assert status == 201
  321. propfind = get_file_content("propname.xml")
  322. status, _, answer = self.request(
  323. "PROPFIND", "/calendar.ics/", propfind)
  324. assert "<sync-token />" in answer
  325. status, _, answer = self.request(
  326. "PROPFIND", "/calendar.ics/event.ics", propfind)
  327. assert "<getetag />" in answer
  328. def test_propfind_allprop(self):
  329. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  330. assert status == 201
  331. event = get_file_content("event1.ics")
  332. status, _, _ = self.request("PUT", "/calendar.ics/event.ics", event)
  333. assert status == 201
  334. propfind = get_file_content("allprop.xml")
  335. status, _, answer = self.request(
  336. "PROPFIND", "/calendar.ics/", propfind)
  337. assert "<sync-token>" in answer
  338. status, _, answer = self.request(
  339. "PROPFIND", "/calendar.ics/event.ics", propfind)
  340. assert "<getetag>" in answer
  341. def test_proppatch(self):
  342. """Write a property and read it back."""
  343. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  344. assert status == 201
  345. proppatch = get_file_content("proppatch1.xml")
  346. status, _, answer = self.request(
  347. "PROPPATCH", "/calendar.ics/", proppatch)
  348. assert status == 207
  349. assert "calendar-color" in answer
  350. assert "200 OK</status" in answer
  351. # Read property back
  352. propfind = get_file_content("propfind1.xml")
  353. status, _, answer = self.request(
  354. "PROPFIND", "/calendar.ics/", propfind)
  355. assert status == 207
  356. assert "<ICAL:calendar-color>#BADA55</" in answer
  357. assert "200 OK</status" in answer
  358. propfind = get_file_content("allprop.xml")
  359. status, _, answer = self.request(
  360. "PROPFIND", "/calendar.ics/", propfind)
  361. assert "<ICAL:calendar-color>" in answer
  362. def test_put_whole_calendar_multiple_events_with_same_uid(self):
  363. """Add two events with the same UID."""
  364. status, _, _ = self.request(
  365. "PUT", "/calendar.ics/", get_file_content("event2.ics"))
  366. assert status == 201
  367. status, _, answer = self.request(
  368. "REPORT", "/calendar.ics/",
  369. """<?xml version="1.0" encoding="utf-8" ?>
  370. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  371. <D:prop xmlns:D="DAV:"><D:getetag/></D:prop>
  372. </C:calendar-query>""")
  373. assert status == 207
  374. assert answer.count("<getetag>") == 1
  375. status, _, answer = self.request("GET", "/calendar.ics/")
  376. assert status == 200
  377. assert answer.count("BEGIN:VEVENT") == 2
  378. def _test_filter(self, filters, kind="event", test=None, items=(1,)):
  379. filter_template = "<C:filter>{}</C:filter>"
  380. if kind in ("event", "journal", "todo"):
  381. create_collection_fn = partial(self.request, "MKCALENDAR")
  382. path = "/calendar.ics/"
  383. filename_template = "{}{}.ics"
  384. namespace = "urn:ietf:params:xml:ns:caldav"
  385. report = "calendar-query"
  386. elif kind == "contact":
  387. create_collection_fn = self._create_addressbook
  388. if test:
  389. filter_template = '<C:filter test="{}">{{}}</C:filter>'.format(
  390. test)
  391. path = "/contacts.vcf/"
  392. filename_template = "{}{}.vcf"
  393. namespace = "urn:ietf:params:xml:ns:carddav"
  394. report = "addressbook-query"
  395. else:
  396. raise ValueError("Unsupported kind: %r" % kind)
  397. status, _, _ = self.request("DELETE", path)
  398. assert status in (200, 404)
  399. status, _, _ = create_collection_fn(path)
  400. assert status == 201
  401. for i in items:
  402. filename = filename_template.format(kind, i)
  403. event = get_file_content(filename)
  404. status, _, _ = self.request(
  405. "PUT", posixpath.join(path, filename), event)
  406. assert status == 201
  407. filters_text = "".join(
  408. filter_template.format(filter_) for filter_ in filters)
  409. status, _, answer = self.request(
  410. "REPORT", path,
  411. """<?xml version="1.0" encoding="utf-8" ?>
  412. <C:{1} xmlns:C="{0}">
  413. <D:prop xmlns:D="DAV:">
  414. <D:getetag/>
  415. </D:prop>
  416. {2}
  417. </C:{1}>""".format(namespace, report, filters_text))
  418. assert status == 207
  419. return answer
  420. def test_addressbook_empty_filter(self):
  421. self._test_filter([""], kind="contact")
  422. def test_addressbook_prop_filter(self):
  423. assert "href>/contacts.vcf/contact1.vcf</" in self._test_filter(["""
  424. <C:prop-filter name="NICKNAME">
  425. <C:text-match collation="i;unicode-casemap"
  426. match-type="contains"
  427. >es</C:text-match>
  428. </C:prop-filter>"""], "contact")
  429. assert "href>/contacts.vcf/contact1.vcf</" in self._test_filter(["""
  430. <C:prop-filter name="NICKNAME">
  431. <C:text-match collation="i;unicode-casemap"
  432. >es</C:text-match>
  433. </C:prop-filter>"""], "contact")
  434. assert "href>/contacts.vcf/contact1.vcf</" not in self._test_filter(["""
  435. <C:prop-filter name="NICKNAME">
  436. <C:text-match collation="i;unicode-casemap"
  437. match-type="contains"
  438. >a</C:text-match>
  439. </C:prop-filter>"""], "contact")
  440. assert "href>/contacts.vcf/contact1.vcf</" in self._test_filter(["""
  441. <C:prop-filter name="NICKNAME">
  442. <C:text-match collation="i;unicode-casemap"
  443. match-type="equals"
  444. >test</C:text-match>
  445. </C:prop-filter>"""], "contact")
  446. assert "href>/contacts.vcf/contact1.vcf</" not in self._test_filter(["""
  447. <C:prop-filter name="NICKNAME">
  448. <C:text-match collation="i;unicode-casemap"
  449. match-type="equals"
  450. >tes</C:text-match>
  451. </C:prop-filter>"""], "contact")
  452. assert "href>/contacts.vcf/contact1.vcf</" not in self._test_filter(["""
  453. <C:prop-filter name="NICKNAME">
  454. <C:text-match collation="i;unicode-casemap"
  455. match-type="equals"
  456. >est</C:text-match>
  457. </C:prop-filter>"""], "contact")
  458. assert "href>/contacts.vcf/contact1.vcf</" in self._test_filter(["""
  459. <C:prop-filter name="NICKNAME">
  460. <C:text-match collation="i;unicode-casemap"
  461. match-type="starts-with"
  462. >tes</C:text-match>
  463. </C:prop-filter>"""], "contact")
  464. assert "href>/contacts.vcf/contact1.vcf</" not in self._test_filter(["""
  465. <C:prop-filter name="NICKNAME">
  466. <C:text-match collation="i;unicode-casemap"
  467. match-type="starts-with"
  468. >est</C:text-match>
  469. </C:prop-filter>"""], "contact")
  470. assert "href>/contacts.vcf/contact1.vcf</" in self._test_filter(["""
  471. <C:prop-filter name="NICKNAME">
  472. <C:text-match collation="i;unicode-casemap"
  473. match-type="ends-with"
  474. >est</C:text-match>
  475. </C:prop-filter>"""], "contact")
  476. assert "href>/contacts.vcf/contact1.vcf</" not in self._test_filter(["""
  477. <C:prop-filter name="NICKNAME">
  478. <C:text-match collation="i;unicode-casemap"
  479. match-type="ends-with"
  480. >tes</C:text-match>
  481. </C:prop-filter>"""], "contact")
  482. def test_addressbook_prop_filter_any(self):
  483. assert "href>/contacts.vcf/contact1.vcf</" in self._test_filter(["""
  484. <C:prop-filter name="NICKNAME">
  485. <C:text-match collation="i;unicode-casemap"
  486. >test</C:text-match>
  487. </C:prop-filter>
  488. <C:prop-filter name="EMAIL">
  489. <C:text-match collation="i;unicode-casemap"
  490. >test</C:text-match>
  491. </C:prop-filter>"""], "contact", test="anyof")
  492. assert "href>/contacts.vcf/contact1.vcf</" not in self._test_filter(["""
  493. <C:prop-filter name="NICKNAME">
  494. <C:text-match collation="i;unicode-casemap"
  495. >a</C:text-match>
  496. </C:prop-filter>
  497. <C:prop-filter name="EMAIL">
  498. <C:text-match collation="i;unicode-casemap"
  499. >test</C:text-match>
  500. </C:prop-filter>"""], "contact", test="anyof")
  501. assert "href>/contacts.vcf/contact1.vcf</" in self._test_filter(["""
  502. <C:prop-filter name="NICKNAME">
  503. <C:text-match collation="i;unicode-casemap"
  504. >test</C:text-match>
  505. </C:prop-filter>
  506. <C:prop-filter name="EMAIL">
  507. <C:text-match collation="i;unicode-casemap"
  508. >test</C:text-match>
  509. </C:prop-filter>"""], "contact")
  510. def test_addressbook_prop_filter_all(self):
  511. assert "href>/contacts.vcf/contact1.vcf</" in self._test_filter(["""
  512. <C:prop-filter name="NICKNAME">
  513. <C:text-match collation="i;unicode-casemap"
  514. >tes</C:text-match>
  515. </C:prop-filter>
  516. <C:prop-filter name="NICKNAME">
  517. <C:text-match collation="i;unicode-casemap"
  518. >est</C:text-match>
  519. </C:prop-filter>"""], "contact", test="allof")
  520. assert "href>/contacts.vcf/contact1.vcf</" not in self._test_filter(["""
  521. <C:prop-filter name="NICKNAME">
  522. <C:text-match collation="i;unicode-casemap"
  523. >test</C:text-match>
  524. </C:prop-filter>
  525. <C:prop-filter name="EMAIL">
  526. <C:text-match collation="i;unicode-casemap"
  527. >test</C:text-match>
  528. </C:prop-filter>"""], "contact", test="allof")
  529. def test_calendar_empty_filter(self):
  530. self._test_filter([""])
  531. def test_calendar_tag_filter(self):
  532. """Report request with tag-based filter on calendar."""
  533. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  534. <C:comp-filter name="VCALENDAR"></C:comp-filter>"""])
  535. def test_item_tag_filter(self):
  536. """Report request with tag-based filter on an item."""
  537. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  538. <C:comp-filter name="VCALENDAR">
  539. <C:comp-filter name="VEVENT"></C:comp-filter>
  540. </C:comp-filter>"""])
  541. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  542. <C:comp-filter name="VCALENDAR">
  543. <C:comp-filter name="VTODO"></C:comp-filter>
  544. </C:comp-filter>"""])
  545. def test_item_not_tag_filter(self):
  546. """Report request with tag-based is-not filter on an item."""
  547. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  548. <C:comp-filter name="VCALENDAR">
  549. <C:comp-filter name="VEVENT">
  550. <C:is-not-defined />
  551. </C:comp-filter>
  552. </C:comp-filter>"""])
  553. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  554. <C:comp-filter name="VCALENDAR">
  555. <C:comp-filter name="VTODO">
  556. <C:is-not-defined />
  557. </C:comp-filter>
  558. </C:comp-filter>"""])
  559. def test_item_prop_filter(self):
  560. """Report request with prop-based filter on an item."""
  561. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  562. <C:comp-filter name="VCALENDAR">
  563. <C:comp-filter name="VEVENT">
  564. <C:prop-filter name="SUMMARY"></C:prop-filter>
  565. </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="VEVENT">
  570. <C:prop-filter name="UNKNOWN"></C:prop-filter>
  571. </C:comp-filter>
  572. </C:comp-filter>"""])
  573. def test_item_not_prop_filter(self):
  574. """Report request with prop-based is-not filter on an item."""
  575. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  576. <C:comp-filter name="VCALENDAR">
  577. <C:comp-filter name="VEVENT">
  578. <C:prop-filter name="SUMMARY">
  579. <C:is-not-defined />
  580. </C:prop-filter>
  581. </C:comp-filter>
  582. </C:comp-filter>"""])
  583. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  584. <C:comp-filter name="VCALENDAR">
  585. <C:comp-filter name="VEVENT">
  586. <C:prop-filter name="UNKNOWN">
  587. <C:is-not-defined />
  588. </C:prop-filter>
  589. </C:comp-filter>
  590. </C:comp-filter>"""])
  591. def test_mutiple_filters(self):
  592. """Report request with multiple filters on an item."""
  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="SUMMARY">
  597. <C:is-not-defined />
  598. </C:prop-filter>
  599. </C:comp-filter>
  600. </C:comp-filter>""", """
  601. <C:comp-filter name="VCALENDAR">
  602. <C:comp-filter name="VEVENT">
  603. <C:prop-filter name="UNKNOWN">
  604. <C:is-not-defined />
  605. </C:prop-filter>
  606. </C:comp-filter>
  607. </C:comp-filter>"""])
  608. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  609. <C:comp-filter name="VCALENDAR">
  610. <C:comp-filter name="VEVENT">
  611. <C:prop-filter name="SUMMARY"></C:prop-filter>
  612. </C:comp-filter>
  613. </C:comp-filter>""", """
  614. <C:comp-filter name="VCALENDAR">
  615. <C:comp-filter name="VEVENT">
  616. <C:prop-filter name="UNKNOWN">
  617. <C:is-not-defined />
  618. </C:prop-filter>
  619. </C:comp-filter>
  620. </C:comp-filter>"""])
  621. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  622. <C:comp-filter name="VCALENDAR">
  623. <C:comp-filter name="VEVENT">
  624. <C:prop-filter name="SUMMARY"></C:prop-filter>
  625. <C:prop-filter name="UNKNOWN">
  626. <C:is-not-defined />
  627. </C:prop-filter>
  628. </C:comp-filter>
  629. </C:comp-filter>"""])
  630. def test_text_match_filter(self):
  631. """Report request with text-match filter on calendar."""
  632. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  633. <C:comp-filter name="VCALENDAR">
  634. <C:comp-filter name="VEVENT">
  635. <C:prop-filter name="SUMMARY">
  636. <C:text-match>event</C:text-match>
  637. </C:prop-filter>
  638. </C:comp-filter>
  639. </C:comp-filter>"""])
  640. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  641. <C:comp-filter name="VCALENDAR">
  642. <C:comp-filter name="VEVENT">
  643. <C:prop-filter name="UNKNOWN">
  644. <C:text-match>event</C:text-match>
  645. </C:prop-filter>
  646. </C:comp-filter>
  647. </C:comp-filter>"""])
  648. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  649. <C:comp-filter name="VCALENDAR">
  650. <C:comp-filter name="VEVENT">
  651. <C:prop-filter name="SUMMARY">
  652. <C:text-match>unknown</C:text-match>
  653. </C:prop-filter>
  654. </C:comp-filter>
  655. </C:comp-filter>"""])
  656. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  657. <C:comp-filter name="VCALENDAR">
  658. <C:comp-filter name="VEVENT">
  659. <C:prop-filter name="SUMMARY">
  660. <C:text-match negate-condition="yes">event</C:text-match>
  661. </C:prop-filter>
  662. </C:comp-filter>
  663. </C:comp-filter>"""])
  664. def test_param_filter(self):
  665. """Report request with param-filter on calendar."""
  666. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  667. <C:comp-filter name="VCALENDAR">
  668. <C:comp-filter name="VEVENT">
  669. <C:prop-filter name="ATTENDEE">
  670. <C:param-filter name="PARTSTAT">
  671. <C:text-match collation="i;ascii-casemap"
  672. >ACCEPTED</C:text-match>
  673. </C:param-filter>
  674. </C:prop-filter>
  675. </C:comp-filter>
  676. </C:comp-filter>"""])
  677. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  678. <C:comp-filter name="VCALENDAR">
  679. <C:comp-filter name="VEVENT">
  680. <C:prop-filter name="ATTENDEE">
  681. <C:param-filter name="PARTSTAT">
  682. <C:text-match collation="i;ascii-casemap"
  683. >UNKNOWN</C:text-match>
  684. </C:param-filter>
  685. </C:prop-filter>
  686. </C:comp-filter>
  687. </C:comp-filter>"""])
  688. assert "href>/calendar.ics/event1.ics</" not in self._test_filter(["""
  689. <C:comp-filter name="VCALENDAR">
  690. <C:comp-filter name="VEVENT">
  691. <C:prop-filter name="ATTENDEE">
  692. <C:param-filter name="PARTSTAT">
  693. <C:is-not-defined />
  694. </C:param-filter>
  695. </C:prop-filter>
  696. </C:comp-filter>
  697. </C:comp-filter>"""])
  698. assert "href>/calendar.ics/event1.ics</" in self._test_filter(["""
  699. <C:comp-filter name="VCALENDAR">
  700. <C:comp-filter name="VEVENT">
  701. <C:prop-filter name="ATTENDEE">
  702. <C:param-filter name="UNKNOWN">
  703. <C:is-not-defined />
  704. </C:param-filter>
  705. </C:prop-filter>
  706. </C:comp-filter>
  707. </C:comp-filter>"""])
  708. def test_time_range_filter_events(self):
  709. """Report request with time-range filter on events."""
  710. answer = self._test_filter(["""
  711. <C:comp-filter name="VCALENDAR">
  712. <C:comp-filter name="VEVENT">
  713. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  714. </C:comp-filter>
  715. </C:comp-filter>"""], "event", items=range(1, 6))
  716. assert "href>/calendar.ics/event1.ics</" in answer
  717. assert "href>/calendar.ics/event2.ics</" in answer
  718. assert "href>/calendar.ics/event3.ics</" in answer
  719. assert "href>/calendar.ics/event4.ics</" in answer
  720. assert "href>/calendar.ics/event5.ics</" in answer
  721. answer = self._test_filter(["""
  722. <C:comp-filter name="VCALENDAR">
  723. <C:comp-filter name="VTODO">
  724. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  725. </C:comp-filter>
  726. </C:comp-filter>"""], "event", items=range(1, 6))
  727. assert "href>/calendar.ics/event1.ics</" not in answer
  728. answer = self._test_filter(["""
  729. <C:comp-filter name="VCALENDAR">
  730. <C:comp-filter name="VEVENT">
  731. <C:prop-filter name="ATTENDEE">
  732. <C:param-filter name="PARTSTAT">
  733. <C:is-not-defined />
  734. </C:param-filter>
  735. </C:prop-filter>
  736. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  737. </C:comp-filter>
  738. </C:comp-filter>"""], items=range(1, 6))
  739. assert "href>/calendar.ics/event1.ics</" not in answer
  740. assert "href>/calendar.ics/event2.ics</" not in answer
  741. assert "href>/calendar.ics/event3.ics</" not in answer
  742. assert "href>/calendar.ics/event4.ics</" not in answer
  743. assert "href>/calendar.ics/event5.ics</" not in answer
  744. answer = self._test_filter(["""
  745. <C:comp-filter name="VCALENDAR">
  746. <C:comp-filter name="VEVENT">
  747. <C:time-range start="20130902T000000Z" end="20131001T000000Z"/>
  748. </C:comp-filter>
  749. </C:comp-filter>"""], items=range(1, 6))
  750. assert "href>/calendar.ics/event1.ics</" not in answer
  751. assert "href>/calendar.ics/event2.ics</" in answer
  752. assert "href>/calendar.ics/event3.ics</" in answer
  753. assert "href>/calendar.ics/event4.ics</" in answer
  754. assert "href>/calendar.ics/event5.ics</" in answer
  755. answer = self._test_filter(["""
  756. <C:comp-filter name="VCALENDAR">
  757. <C:comp-filter name="VEVENT">
  758. <C:time-range start="20130903T000000Z" end="20130908T000000Z"/>
  759. </C:comp-filter>
  760. </C:comp-filter>"""], items=range(1, 6))
  761. assert "href>/calendar.ics/event1.ics</" not in answer
  762. assert "href>/calendar.ics/event2.ics</" not in answer
  763. assert "href>/calendar.ics/event3.ics</" in answer
  764. assert "href>/calendar.ics/event4.ics</" in answer
  765. assert "href>/calendar.ics/event5.ics</" in answer
  766. answer = self._test_filter(["""
  767. <C:comp-filter name="VCALENDAR">
  768. <C:comp-filter name="VEVENT">
  769. <C:time-range start="20130903T000000Z" end="20130904T000000Z"/>
  770. </C:comp-filter>
  771. </C:comp-filter>"""], items=range(1, 6))
  772. assert "href>/calendar.ics/event1.ics</" not in answer
  773. assert "href>/calendar.ics/event2.ics</" not in answer
  774. assert "href>/calendar.ics/event3.ics</" in answer
  775. assert "href>/calendar.ics/event4.ics</" not in answer
  776. assert "href>/calendar.ics/event5.ics</" not in answer
  777. answer = self._test_filter(["""
  778. <C:comp-filter name="VCALENDAR">
  779. <C:comp-filter name="VEVENT">
  780. <C:time-range start="20130805T000000Z" end="20130810T000000Z"/>
  781. </C:comp-filter>
  782. </C:comp-filter>"""], items=range(1, 6))
  783. assert "href>/calendar.ics/event1.ics</" not in answer
  784. assert "href>/calendar.ics/event2.ics</" not in answer
  785. assert "href>/calendar.ics/event3.ics</" not in answer
  786. assert "href>/calendar.ics/event4.ics</" not in answer
  787. assert "href>/calendar.ics/event5.ics</" not in answer
  788. # HACK: VObject doesn't match RECURRENCE-ID to recurrences, the
  789. # overwritten recurrence is still used for filtering.
  790. answer = self._test_filter(["""
  791. <C:comp-filter name="VCALENDAR">
  792. <C:comp-filter name="VEVENT">
  793. <C:time-range start="20170601T063000Z" end="20170601T070000Z"/>
  794. </C:comp-filter>
  795. </C:comp-filter>"""], items=(6, 7, 8, 9))
  796. assert "href>/calendar.ics/event6.ics</" in answer
  797. assert "href>/calendar.ics/event7.ics</" in answer
  798. assert "href>/calendar.ics/event8.ics</" in answer
  799. assert "href>/calendar.ics/event9.ics</" in answer
  800. answer = self._test_filter(["""
  801. <C:comp-filter name="VCALENDAR">
  802. <C:comp-filter name="VEVENT">
  803. <C:time-range start="20170701T060000Z"/>
  804. </C:comp-filter>
  805. </C:comp-filter>"""], items=(6, 7, 8, 9))
  806. assert "href>/calendar.ics/event6.ics</" in answer
  807. assert "href>/calendar.ics/event7.ics</" in answer
  808. assert "href>/calendar.ics/event8.ics</" in answer
  809. assert "href>/calendar.ics/event9.ics</" not in answer
  810. answer = self._test_filter(["""
  811. <C:comp-filter name="VCALENDAR">
  812. <C:comp-filter name="VEVENT">
  813. <C:time-range start="20170702T070000Z" end="20170704T060000Z"/>
  814. </C:comp-filter>
  815. </C:comp-filter>"""], items=(6, 7, 8, 9))
  816. assert "href>/calendar.ics/event6.ics</" not in answer
  817. assert "href>/calendar.ics/event7.ics</" not in answer
  818. assert "href>/calendar.ics/event8.ics</" not in answer
  819. assert "href>/calendar.ics/event9.ics</" not in answer
  820. answer = self._test_filter(["""
  821. <C:comp-filter name="VCALENDAR">
  822. <C:comp-filter name="VEVENT">
  823. <C:time-range start="20170602T075959Z" end="20170602T080000Z"/>
  824. </C:comp-filter>
  825. </C:comp-filter>"""], items=(9,))
  826. assert "href>/calendar.ics/event9.ics</" in answer
  827. answer = self._test_filter(["""
  828. <C:comp-filter name="VCALENDAR">
  829. <C:comp-filter name="VEVENT">
  830. <C:time-range start="20170602T080000Z" end="20170603T083000Z"/>
  831. </C:comp-filter>
  832. </C:comp-filter>"""], items=(9,))
  833. assert "href>/calendar.ics/event9.ics</" not in answer
  834. def test_time_range_filter_events_rrule(self):
  835. """Report request with time-range filter on events with rrules."""
  836. answer = self._test_filter(["""
  837. <C:comp-filter name="VCALENDAR">
  838. <C:comp-filter name="VEVENT">
  839. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  840. </C:comp-filter>
  841. </C:comp-filter>"""], "event", items=(1, 2))
  842. assert "href>/calendar.ics/event1.ics</" in answer
  843. assert "href>/calendar.ics/event2.ics</" in answer
  844. answer = self._test_filter(["""
  845. <C:comp-filter name="VCALENDAR">
  846. <C:comp-filter name="VEVENT">
  847. <C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
  848. </C:comp-filter>
  849. </C:comp-filter>"""], "event", items=(1, 2))
  850. assert "href>/calendar.ics/event1.ics</" not in answer
  851. assert "href>/calendar.ics/event2.ics</" in answer
  852. answer = self._test_filter(["""
  853. <C:comp-filter name="VCALENDAR">
  854. <C:comp-filter name="VEVENT">
  855. <C:time-range start="20120801T000000Z" end="20121001T000000Z"/>
  856. </C:comp-filter>
  857. </C:comp-filter>"""], "event", items=(1, 2))
  858. assert "href>/calendar.ics/event1.ics</" not in answer
  859. assert "href>/calendar.ics/event2.ics</" not in answer
  860. answer = self._test_filter(["""
  861. <C:comp-filter name="VCALENDAR">
  862. <C:comp-filter name="VEVENT">
  863. <C:time-range start="20130903T000000Z" end="20130907T000000Z"/>
  864. </C:comp-filter>
  865. </C:comp-filter>"""], "event", items=(1, 2))
  866. assert "href>/calendar.ics/event1.ics</" not in answer
  867. assert "href>/calendar.ics/event2.ics</" not in answer
  868. def test_time_range_filter_todos(self):
  869. """Report request with time-range filter on todos."""
  870. answer = self._test_filter(["""
  871. <C:comp-filter name="VCALENDAR">
  872. <C:comp-filter name="VTODO">
  873. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  874. </C:comp-filter>
  875. </C:comp-filter>"""], "todo", items=range(1, 9))
  876. assert "href>/calendar.ics/todo1.ics</" in answer
  877. assert "href>/calendar.ics/todo2.ics</" in answer
  878. assert "href>/calendar.ics/todo3.ics</" in answer
  879. assert "href>/calendar.ics/todo4.ics</" in answer
  880. assert "href>/calendar.ics/todo5.ics</" in answer
  881. assert "href>/calendar.ics/todo6.ics</" in answer
  882. assert "href>/calendar.ics/todo7.ics</" in answer
  883. assert "href>/calendar.ics/todo8.ics</" in answer
  884. answer = self._test_filter(["""
  885. <C:comp-filter name="VCALENDAR">
  886. <C:comp-filter name="VTODO">
  887. <C:time-range start="20130901T160000Z" end="20130901T183000Z"/>
  888. </C:comp-filter>
  889. </C:comp-filter>"""], "todo", items=range(1, 9))
  890. assert "href>/calendar.ics/todo1.ics</" not in answer
  891. assert "href>/calendar.ics/todo2.ics</" in answer
  892. assert "href>/calendar.ics/todo3.ics</" in answer
  893. assert "href>/calendar.ics/todo4.ics</" not in answer
  894. assert "href>/calendar.ics/todo5.ics</" not in answer
  895. assert "href>/calendar.ics/todo6.ics</" not in answer
  896. assert "href>/calendar.ics/todo7.ics</" in answer
  897. assert "href>/calendar.ics/todo8.ics</" in answer
  898. answer = self._test_filter(["""
  899. <C:comp-filter name="VCALENDAR">
  900. <C:comp-filter name="VTODO">
  901. <C:time-range start="20130903T160000Z" end="20130901T183000Z"/>
  902. </C:comp-filter>
  903. </C:comp-filter>"""], "todo", items=range(1, 9))
  904. assert "href>/calendar.ics/todo2.ics</" not in answer
  905. answer = self._test_filter(["""
  906. <C:comp-filter name="VCALENDAR">
  907. <C:comp-filter name="VTODO">
  908. <C:time-range start="20130903T160000Z" end="20130901T173000Z"/>
  909. </C:comp-filter>
  910. </C:comp-filter>"""], "todo", items=range(1, 9))
  911. assert "href>/calendar.ics/todo2.ics</" not in answer
  912. answer = self._test_filter(["""
  913. <C:comp-filter name="VCALENDAR">
  914. <C:comp-filter name="VTODO">
  915. <C:time-range start="20130903T160000Z" end="20130903T173000Z"/>
  916. </C:comp-filter>
  917. </C:comp-filter>"""], "todo", items=range(1, 9))
  918. assert "href>/calendar.ics/todo3.ics</" not in answer
  919. answer = self._test_filter(["""
  920. <C:comp-filter name="VCALENDAR">
  921. <C:comp-filter name="VTODO">
  922. <C:time-range start="20130903T160000Z" end="20130803T203000Z"/>
  923. </C:comp-filter>
  924. </C:comp-filter>"""], "todo", items=range(1, 9))
  925. assert "href>/calendar.ics/todo7.ics</" in answer
  926. def test_time_range_filter_todos_rrule(self):
  927. """Report request with time-range filter on todos with rrules."""
  928. answer = self._test_filter(["""
  929. <C:comp-filter name="VCALENDAR">
  930. <C:comp-filter name="VTODO">
  931. <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
  932. </C:comp-filter>
  933. </C:comp-filter>"""], "todo", items=(1, 2, 9))
  934. assert "href>/calendar.ics/todo1.ics</" in answer
  935. assert "href>/calendar.ics/todo2.ics</" in answer
  936. assert "href>/calendar.ics/todo9.ics</" in answer
  937. answer = self._test_filter(["""
  938. <C:comp-filter name="VCALENDAR">
  939. <C:comp-filter name="VTODO">
  940. <C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
  941. </C:comp-filter>
  942. </C:comp-filter>"""], "todo", items=(1, 2, 9))
  943. assert "href>/calendar.ics/todo1.ics</" not in answer
  944. assert "href>/calendar.ics/todo2.ics</" in answer
  945. assert "href>/calendar.ics/todo9.ics</" in answer
  946. answer = self._test_filter(["""
  947. <C:comp-filter name="VCALENDAR">
  948. <C:comp-filter name="VTODO">
  949. <C:time-range start="20140902T000000Z" end="20140903T000000Z"/>
  950. </C:comp-filter>
  951. </C:comp-filter>"""], "todo", items=(1, 2))
  952. assert "href>/calendar.ics/todo1.ics</" not in answer
  953. assert "href>/calendar.ics/todo2.ics</" in answer
  954. answer = self._test_filter(["""
  955. <C:comp-filter name="VCALENDAR">
  956. <C:comp-filter name="VTODO">
  957. <C:time-range start="20140904T000000Z" end="20140914T000000Z"/>
  958. </C:comp-filter>
  959. </C:comp-filter>"""], "todo", items=(1, 2))
  960. assert "href>/calendar.ics/todo1.ics</" not in answer
  961. assert "href>/calendar.ics/todo2.ics</" not in answer
  962. answer = self._test_filter(["""
  963. <C:comp-filter name="VCALENDAR">
  964. <C:comp-filter name="VTODO">
  965. <C:time-range start="20130902T000000Z" end="20130906T235959Z"/>
  966. </C:comp-filter>
  967. </C:comp-filter>"""], "todo", items=(9,))
  968. assert "href>/calendar.ics/todo9.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 == 409:
  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("event1_modified.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("DELETE", "/test/test.ics")
  1253. assert status in (200, 404)
  1254. status, _, _ = self.request("PUT", "/test/test.ics", event)
  1255. assert status == 201
  1256. status, _, answer = self.request(
  1257. "REPORT", "/test/",
  1258. """<?xml version="1.0" encoding="utf-8" ?>
  1259. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  1260. <D:prop xmlns:D="DAV:">
  1261. <D:getcontenttype />
  1262. </D:prop>
  1263. </C:calendar-query>""")
  1264. assert status == 207
  1265. assert ">text/calendar;charset=utf-8;component=V{}<".format(
  1266. component.upper()) in answer
  1267. def test_addressbook_getcontenttype(self):
  1268. """Test report request on an item"""
  1269. status, _, _ = self._create_addressbook("/test/")
  1270. assert status == 201
  1271. contact = get_file_content("contact1.vcf")
  1272. status, _, _ = self.request("PUT", "/test/test.vcf", contact)
  1273. assert status == 201
  1274. status, _, answer = self.request(
  1275. "REPORT", "/test/",
  1276. """<?xml version="1.0" encoding="utf-8" ?>
  1277. <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  1278. <D:prop xmlns:D="DAV:">
  1279. <D:getcontenttype />
  1280. </D:prop>
  1281. </C:calendar-query>""")
  1282. assert status == 207
  1283. assert ">text/vcard;charset=utf-8<" in answer
  1284. def test_authorization(self):
  1285. authorization = "Basic " + base64.b64encode(b"user:").decode()
  1286. status, _, answer = self.request(
  1287. "PROPFIND", "/",
  1288. """<?xml version="1.0" encoding="utf-8"?>
  1289. <propfind xmlns="DAV:">
  1290. <prop>
  1291. <current-user-principal />
  1292. </prop>
  1293. </propfind>""",
  1294. HTTP_AUTHORIZATION=authorization)
  1295. assert status == 207
  1296. assert "href>/user/<" in answer
  1297. def test_authentication(self):
  1298. """Test if server sends authentication request."""
  1299. self.configuration["auth"]["type"] = "htpasswd"
  1300. self.configuration["auth"]["htpasswd_filename"] = os.devnull
  1301. self.configuration["auth"]["htpasswd_encryption"] = "plain"
  1302. self.configuration["rights"]["type"] = "owner_only"
  1303. self.application = Application(self.configuration)
  1304. status, headers, _ = self.request("MKCOL", "/user/")
  1305. assert status in (401, 403)
  1306. assert headers.get("WWW-Authenticate")
  1307. def test_principal_collection_creation(self):
  1308. """Verify existence of the principal collection."""
  1309. status, _, _ = self.request("PROPFIND", "/user/", HTTP_AUTHORIZATION=(
  1310. "Basic " + base64.b64encode(b"user:").decode()))
  1311. assert status == 207
  1312. def test_existence_of_root_collections(self):
  1313. """Verify that the root collection always exists."""
  1314. # Use PROPFIND because GET returns message
  1315. status, _, _ = self.request("PROPFIND", "/")
  1316. assert status == 207
  1317. # it should still exist after deletion
  1318. status, _, _ = self.request("DELETE", "/")
  1319. assert status == 200
  1320. status, _, _ = self.request("PROPFIND", "/")
  1321. assert status == 207
  1322. def test_custom_headers(self):
  1323. if not self.configuration.has_section("headers"):
  1324. self.configuration.add_section("headers")
  1325. self.configuration.set("headers", "test", "123")
  1326. # Test if header is set on success
  1327. status, headers, _ = self.request("OPTIONS", "/")
  1328. assert status == 200
  1329. assert headers.get("test") == "123"
  1330. # Test if header is set on failure
  1331. status, headers, _ = self.request(
  1332. "GET", "/.well-known/does not exist")
  1333. assert status == 404
  1334. assert headers.get("test") == "123"
  1335. @pytest.mark.skipif(sys.version_info < (3, 6),
  1336. reason="Unsupported in Python < 3.6")
  1337. def test_timezone_seconds(self):
  1338. """Verify that timezones with minutes and seconds work."""
  1339. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1340. assert status == 201
  1341. event = get_file_content("event_timezone_seconds.ics")
  1342. status, _, _ = self.request("PUT", "/calendar.ics/event.ics", event)
  1343. assert status == 201
  1344. class BaseFileSystemTest(BaseTest):
  1345. """Base class for filesystem backend tests."""
  1346. storage_type = None
  1347. def setup(self):
  1348. self.configuration = config.load()
  1349. self.configuration["storage"]["type"] = self.storage_type
  1350. self.colpath = tempfile.mkdtemp()
  1351. self.configuration["storage"]["filesystem_folder"] = self.colpath
  1352. # Disable syncing to disk for better performance
  1353. self.configuration["internal"]["filesystem_fsync"] = "False"
  1354. # Allow access to anything for tests
  1355. rights_file_path = os.path.join(self.colpath, "rights")
  1356. with open(rights_file_path, "w") as f:
  1357. f.write("""\
  1358. [allow all]
  1359. user: .*
  1360. collection: .*
  1361. permissions: RrWw""")
  1362. self.configuration["rights"]["file"] = rights_file_path
  1363. self.configuration["rights"]["type"] = "from_file"
  1364. self.application = Application(self.configuration)
  1365. def teardown(self):
  1366. shutil.rmtree(self.colpath)
  1367. class TestMultiFileSystem(BaseFileSystemTest, BaseRequestsMixIn):
  1368. """Test BaseRequests on multifilesystem."""
  1369. storage_type = "multifilesystem"
  1370. def test_fsync(self):
  1371. """Create a directory and file with syncing enabled."""
  1372. self.configuration["internal"]["filesystem_fsync"] = "True"
  1373. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1374. assert status == 201
  1375. def test_hook(self):
  1376. """Run hook."""
  1377. self.configuration["storage"]["hook"] = (
  1378. "mkdir %s" % os.path.join("collection-root", "created_by_hook"))
  1379. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1380. assert status == 201
  1381. status, _, _ = self.request("PROPFIND", "/created_by_hook/")
  1382. assert status == 207
  1383. def test_hook_read_access(self):
  1384. """Verify that hook is not run for read accesses."""
  1385. self.configuration["storage"]["hook"] = (
  1386. "mkdir %s" % os.path.join("collection-root", "created_by_hook"))
  1387. status, _, _ = self.request("PROPFIND", "/")
  1388. assert status == 207
  1389. status, _, _ = self.request("PROPFIND", "/created_by_hook/")
  1390. assert status == 404
  1391. @pytest.mark.skipif(os.system("type flock") != 0,
  1392. reason="flock command not found")
  1393. def test_hook_storage_locked(self):
  1394. """Verify that the storage is locked when the hook runs."""
  1395. self.configuration["storage"]["hook"] = (
  1396. "flock -n .Radicale.lock || exit 0; exit 1")
  1397. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1398. assert status == 201
  1399. def test_hook_principal_collection_creation(self):
  1400. """Verify that the hooks runs when a new user is created."""
  1401. self.configuration["storage"]["hook"] = (
  1402. "mkdir %s" % os.path.join("collection-root", "created_by_hook"))
  1403. status, _, _ = self.request("PROPFIND", "/", HTTP_AUTHORIZATION=(
  1404. "Basic " + base64.b64encode(b"user:").decode()))
  1405. assert status == 207
  1406. status, _, _ = self.request("PROPFIND", "/created_by_hook/")
  1407. assert status == 207
  1408. def test_hook_fail(self):
  1409. """Verify that a request fails if the hook fails."""
  1410. self.configuration["storage"]["hook"] = "exit 1"
  1411. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1412. assert status != 201
  1413. def test_item_cache_rebuild(self):
  1414. """Delete the item cache and verify that it is rebuild."""
  1415. status, _, _ = self.request("MKCALENDAR", "/calendar.ics/")
  1416. assert status == 201
  1417. event = get_file_content("event1.ics")
  1418. path = "/calendar.ics/event1.ics"
  1419. status, _, _ = self.request("PUT", path, event)
  1420. assert status == 201
  1421. status, _, answer1 = self.request("GET", path)
  1422. assert status == 200
  1423. cache_folder = os.path.join(self.colpath, "collection-root",
  1424. "calendar.ics", ".Radicale.cache", "item")
  1425. assert os.path.exists(os.path.join(cache_folder, "event1.ics"))
  1426. shutil.rmtree(cache_folder)
  1427. status, _, answer2 = self.request("GET", path)
  1428. assert status == 200
  1429. assert answer1 == answer2
  1430. assert os.path.exists(os.path.join(cache_folder, "event1.ics"))
  1431. @pytest.mark.skipif(os.name not in ("nt", "posix"),
  1432. reason="Only supported on 'nt' and 'posix'")
  1433. def test_put_whole_calendar_uids_used_as_file_names(self):
  1434. """Test if UIDs are used as file names."""
  1435. BaseRequestsMixIn.test_put_whole_calendar(self)
  1436. for uid in ("todo", "event"):
  1437. status, _, answer = self.request(
  1438. "GET", "/calendar.ics/%s.ics" % uid)
  1439. assert status == 200
  1440. assert "\r\nUID:%s\r\n" % uid in answer
  1441. @pytest.mark.skipif(os.name not in ("nt", "posix"),
  1442. reason="Only supported on 'nt' and 'posix'")
  1443. def test_put_whole_calendar_random_uids_used_as_file_names(self):
  1444. """Test if UIDs are used as file names."""
  1445. BaseRequestsMixIn.test_put_whole_calendar_without_uids(self)
  1446. status, _, answer = self.request("GET", "/calendar.ics")
  1447. assert status == 200
  1448. uids = []
  1449. for line in answer.split("\r\n"):
  1450. if line.startswith("UID:"):
  1451. uids.append(line[len("UID:"):])
  1452. for uid in uids:
  1453. status, _, answer = self.request(
  1454. "GET", "/calendar.ics/%s.ics" % uid)
  1455. assert status == 200
  1456. assert "\r\nUID:%s\r\n" % uid in answer
  1457. @pytest.mark.skipif(os.name not in ("nt", "posix"),
  1458. reason="Only supported on 'nt' and 'posix'")
  1459. def test_put_whole_addressbook_uids_used_as_file_names(self):
  1460. """Test if UIDs are used as file names."""
  1461. BaseRequestsMixIn.test_put_whole_addressbook(self)
  1462. for uid in ("contact1", "contact2"):
  1463. status, _, answer = self.request(
  1464. "GET", "/contacts.vcf/%s.vcf" % uid)
  1465. assert status == 200
  1466. assert "\r\nUID:%s\r\n" % uid in answer
  1467. @pytest.mark.skipif(os.name not in ("nt", "posix"),
  1468. reason="Only supported on 'nt' and 'posix'")
  1469. def test_put_whole_addressbook_random_uids_used_as_file_names(self):
  1470. """Test if UIDs are used as file names."""
  1471. BaseRequestsMixIn.test_put_whole_addressbook_without_uids(self)
  1472. status, _, answer = self.request("GET", "/contacts.vcf")
  1473. assert status == 200
  1474. uids = []
  1475. for line in answer.split("\r\n"):
  1476. if line.startswith("UID:"):
  1477. uids.append(line[len("UID:"):])
  1478. for uid in uids:
  1479. status, _, answer = self.request(
  1480. "GET", "/contacts.vcf/%s.vcf" % uid)
  1481. assert status == 200
  1482. assert "\r\nUID:%s\r\n" % uid in answer
  1483. class TestCustomStorageSystem(BaseFileSystemTest):
  1484. """Test custom backend loading."""
  1485. storage_type = "tests.custom.storage"
  1486. def test_root(self):
  1487. """A simple test to verify that the custom backend works."""
  1488. BaseRequestsMixIn.test_root(self)