test_base.py 86 KB

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