test_base.py 73 KB

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