test_base.py 69 KB

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