test_base.py 84 KB

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