test_base.py 68 KB

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