test_base.py 98 KB

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