1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-06-26 16:45:52 +00:00
Radicale/radicale/tests/test_base.py

1858 lines
80 KiB
Python
Raw Permalink Normal View History

2021-12-08 21:45:42 +01:00
# This file is part of Radicale - CalDAV and CardDAV server
2017-05-27 17:28:07 +02:00
# Copyright © 2012-2017 Guillaume Ayoub
2024-12-05 08:14:21 +01:00
# Copyright © 2017-2022 Unrud <unrud@outlook.com>
2025-04-22 21:31:25 +02:00
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
2012-09-15 10:00:13 +02:00
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
Radicale tests with simple requests.
"""
2025-05-16 06:31:39 +02:00
import logging
import os
2019-06-15 09:01:55 +02:00
import posixpath
2021-12-11 12:59:02 +01:00
from typing import Any, Callable, ClassVar, Iterable, List, Optional, Tuple
2015-07-24 16:01:03 +02:00
import defusedxml.ElementTree as DefusedET
2024-08-14 11:15:30 -06:00
import vobject
2019-06-15 09:01:55 +02:00
2021-12-11 12:59:02 +01:00
from radicale import storage, xmlutils
2021-07-26 20:56:47 +02:00
from radicale.tests import RESPONSES, BaseTest
2020-01-15 18:44:00 +01:00
from radicale.tests.helpers import get_file_content
2012-09-15 10:00:13 +02:00
2021-12-11 12:59:02 +01:00
class TestBaseRequests(BaseTest):
2012-09-15 10:00:13 +02:00
"""Tests with simple requests."""
2018-09-04 03:33:42 +02:00
# Allow skipping sync-token tests, when not fully supported by the backend
2021-07-26 20:56:47 +02:00
full_sync_token_support: ClassVar[bool] = True
2018-09-04 03:33:42 +02:00
def setup_method(self) -> None:
BaseTest.setup_method(self)
2021-12-11 12:59:02 +01:00
rights_file_path = os.path.join(self.colpath, "rights")
with open(rights_file_path, "w") as f:
f.write("""\
2024-09-29 19:54:54 +02:00
[permit delete collection]
user: .*
collection: test-permit-delete
permissions: RrWwD
[forbid delete collection]
user: .*
collection: test-forbid-delete
permissions: RrWwd
[permit overwrite collection]
user: .*
collection: test-permit-overwrite
permissions: RrWwO
[forbid overwrite collection]
user: .*
collection: test-forbid-overwrite
permissions: RrWwo
2021-12-11 12:59:02 +01:00
[allow all]
user: .*
collection: .*
permissions: RrWw""")
self.configure({"rights": {"file": rights_file_path,
"type": "from_file"}})
2021-07-26 20:56:47 +02:00
def test_root(self) -> None:
2015-07-24 14:23:11 +02:00
"""GET request at "/"."""
for path in ["", "/", "//"]:
_, headers, answer = self.request("GET", path, check=302)
assert headers.get("Location") == "/.web"
assert answer == "Redirected to /.web"
2013-09-05 15:13:31 +02:00
def test_root_script_name(self) -> None:
2017-05-31 13:18:40 +02:00
"""GET request at "/" with SCRIPT_NAME."""
for path in ["", "/", "//"]:
_, headers, _ = self.request("GET", path, check=302,
SCRIPT_NAME="/radicale")
assert headers.get("Location") == "/radicale/.web"
2022-01-21 19:56:57 +01:00
def test_root_broken_script_name(self) -> None:
"""GET request at "/" with SCRIPT_NAME ending with "/"."""
for script_name, prefix in [
("/", ""), ("//", ""), ("/radicale/", "/radicale"),
("radicale", None), ("radicale//", None)]:
_, headers, _ = self.request(
"GET", "/", check=500 if prefix is None else 302,
SCRIPT_NAME=script_name)
assert (prefix is None or
headers.get("Location") == prefix + "/.web")
2022-01-21 19:56:57 +01:00
2022-01-21 19:56:56 +01:00
def test_root_http_x_script_name(self) -> None:
"""GET request at "/" with HTTP_X_SCRIPT_NAME."""
for path in ["", "/", "//"]:
_, headers, _ = self.request("GET", path, check=302,
HTTP_X_SCRIPT_NAME="/radicale")
assert headers.get("Location") == "/radicale/.web"
2022-01-21 19:56:57 +01:00
def test_root_broken_http_x_script_name(self) -> None:
"""GET request at "/" with HTTP_X_SCRIPT_NAME ending with "/"."""
for script_name, prefix in [
("/", ""), ("//", ""), ("/radicale/", "/radicale"),
("radicale", None), ("radicale//", None)]:
_, headers, _ = self.request(
"GET", "/", check=400 if prefix is None else 302,
HTTP_X_SCRIPT_NAME=script_name)
assert (prefix is None or
headers.get("Location") == prefix + "/.web")
2022-01-21 19:56:57 +01:00
def test_sanitized_path(self) -> None:
"""GET request with unsanitized paths."""
for path, sane_path in [
("//.web", "/.web"), ("//.web/", "/.web/"),
("/.web//", "/.web/"), ("/.web/a//b", "/.web/a/b")]:
_, headers, _ = self.request("GET", path, check=301)
assert headers.get("Location") == sane_path
_, headers, _ = self.request("GET", path, check=301,
SCRIPT_NAME="/radicale")
assert headers.get("Location") == "/radicale%s" % sane_path
2022-01-21 19:56:56 +01:00
_, headers, _ = self.request("GET", path, check=301,
HTTP_X_SCRIPT_NAME="/radicale")
assert headers.get("Location") == "/radicale%s" % sane_path
2017-05-31 13:18:40 +02:00
2021-07-26 20:56:47 +02:00
def test_add_event(self) -> None:
2015-07-24 14:23:11 +02:00
"""Add an event."""
self.mkcalendar("/calendar.ics/")
event = get_file_content("event1.ics")
path = "/calendar.ics/event1.ics"
self.put(path, event)
2022-01-16 13:07:56 +01:00
_, headers, answer = self.request("GET", path, check=200)
2017-07-01 04:20:11 +02:00
assert "ETag" in headers
assert headers["Content-Type"] == "text/calendar; charset=utf-8"
assert "VEVENT" in answer
2015-07-24 14:23:11 +02:00
assert "Event" in answer
assert "UID:event" in answer
2021-07-26 20:56:47 +02:00
def test_add_event_without_uid(self) -> None:
2017-07-01 00:11:56 +02:00
"""Add an event without UID."""
self.mkcalendar("/calendar.ics/")
2017-07-01 00:11:56 +02:00
event = get_file_content("event1.ics").replace("UID:event1\n", "")
assert "\nUID:" not in event
path = "/calendar.ics/event.ics"
self.put(path, event, check=400)
2017-07-01 00:11:56 +02:00
2021-07-26 20:56:47 +02:00
def test_add_event_duplicate_uid(self) -> None:
"""Add an event with an existing UID."""
self.mkcalendar("/calendar.ics/")
event = get_file_content("event1.ics")
self.put("/calendar.ics/event1.ics", event)
status, answer = self.put(
2022-01-16 13:07:56 +01:00
"/calendar.ics/event1-duplicate.ics", event, check=None)
assert status in (403, 409)
xml = DefusedET.fromstring(answer)
assert xml.tag == xmlutils.make_clark("D:error")
assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
def test_add_event_with_mixed_datetime_and_date(self) -> None:
"""Test event with DTSTART as DATE-TIME and EXDATE as DATE."""
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_mixed_datetime_and_date.ics")
self.put("/calendar.ics/event.ics", event)
2024-12-05 08:14:06 +01:00
def test_add_event_with_exdate_without_rrule(self) -> None:
"""Test event with EXDATE but not having RRULE."""
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_exdate_without_rrule.ics")
self.put("/calendar.ics/event.ics", event)
2021-07-26 20:56:47 +02:00
def test_add_todo(self) -> None:
2015-07-24 14:23:11 +02:00
"""Add a todo."""
self.mkcalendar("/calendar.ics/")
todo = get_file_content("todo1.ics")
path = "/calendar.ics/todo1.ics"
self.put(path, todo)
2022-01-16 13:07:56 +01:00
_, headers, answer = self.request("GET", path, check=200)
2017-07-01 04:20:11 +02:00
assert "ETag" in headers
assert headers["Content-Type"] == "text/calendar; charset=utf-8"
assert "VTODO" in answer
2015-07-24 14:23:11 +02:00
assert "Todo" in answer
assert "UID:todo" in answer
2013-09-05 15:13:31 +02:00
2021-07-26 20:56:47 +02:00
def test_add_contact(self) -> None:
2017-07-01 00:11:07 +02:00
"""Add a contact."""
self.create_addressbook("/contacts.vcf/")
2017-07-01 00:11:07 +02:00
contact = get_file_content("contact1.vcf")
path = "/contacts.vcf/contact.vcf"
self.put(path, contact)
2022-01-16 13:07:56 +01:00
_, headers, answer = self.request("GET", path, check=200)
2017-07-01 04:20:11 +02:00
assert "ETag" in headers
assert headers["Content-Type"] == "text/vcard; charset=utf-8"
2017-07-01 00:11:07 +02:00
assert "VCARD" in answer
assert "UID:contact1" in answer
_, answer = self.get(path)
2017-07-01 00:11:07 +02:00
assert "UID:contact1" in answer
def test_add_contact_photo_with_data_uri(self) -> None:
"""Test workaround for broken PHOTO data from InfCloud"""
self.create_addressbook("/contacts.vcf/")
contact = get_file_content("contact_photo_with_data_uri.vcf")
self.put("/contacts.vcf/contact.vcf", contact)
2021-07-26 20:56:47 +02:00
def test_add_contact_without_uid(self) -> None:
"""Add a contact without UID."""
self.create_addressbook("/contacts.vcf/")
2017-07-01 00:11:56 +02:00
contact = get_file_content("contact1.vcf").replace("UID:contact1\n",
"")
assert "\nUID" not in contact
path = "/contacts.vcf/contact.vcf"
self.put(path, contact, check=400)
2017-07-01 00:11:56 +02:00
2021-07-26 20:56:47 +02:00
def test_update_event(self) -> None:
2016-07-12 18:12:42 +02:00
"""Update an event."""
self.mkcalendar("/calendar.ics/")
2016-07-12 18:08:01 +02:00
event = get_file_content("event1.ics")
2020-05-24 13:41:08 +02:00
event_modified = get_file_content("event1_modified.ics")
2016-07-12 18:08:01 +02:00
path = "/calendar.ics/event1.ics"
self.put(path, event)
self.put(path, event_modified, check=204)
_, answer = self.get("/calendar.ics/")
2016-07-12 18:08:01 +02:00
assert answer.count("BEGIN:VEVENT") == 1
_, answer = self.get(path)
2020-05-24 13:41:08 +02:00
assert "DTSTAMP:20130902T150159Z" in answer
2016-07-12 18:08:01 +02:00
2021-07-26 20:56:47 +02:00
def test_update_event_uid_event(self) -> None:
"""Update an event with a different UID."""
self.mkcalendar("/calendar.ics/")
event1 = get_file_content("event1.ics")
event2 = get_file_content("event2.ics")
path = "/calendar.ics/event1.ics"
self.put(path, event1)
2022-01-16 13:07:56 +01:00
status, answer = self.put(path, event2, check=None)
assert status in (403, 409)
xml = DefusedET.fromstring(answer)
assert xml.tag == xmlutils.make_clark("D:error")
assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
2021-07-26 20:56:47 +02:00
def test_put_whole_calendar(self) -> None:
"""Create and overwrite a whole calendar."""
self.put("/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
2017-07-01 04:20:11 +02:00
event1 = get_file_content("event1.ics")
self.put("/calendar.ics/test_event.ics", event1)
2016-08-08 06:09:24 +02:00
# Overwrite
events = get_file_content("event_multiple.ics")
self.put("/calendar.ics/", events)
self.get("/calendar.ics/test_event.ics", check=404)
_, answer = self.get("/calendar.ics/")
assert "\r\nUID:event\r\n" in answer and "\r\nUID:todo\r\n" in answer
assert "\r\nUID:event1\r\n" not in answer
2016-08-08 06:09:24 +02:00
2021-07-26 20:56:47 +02:00
def test_put_whole_calendar_without_uids(self) -> None:
2017-07-01 00:11:56 +02:00
"""Create a whole calendar without UID."""
event = get_file_content("event_multiple.ics")
event = event.replace("UID:event\n", "").replace("UID:todo\n", "")
assert "\nUID:" not in event
self.put("/calendar.ics/", event)
_, answer = self.get("/calendar.ics")
2017-07-01 00:11:56 +02:00
uids = []
for line in answer.split("\r\n"):
if line.startswith("UID:"):
uids.append(line[len("UID:"):])
assert len(uids) == 2
for i, uid1 in enumerate(uids):
assert uid1
for uid2 in uids[i + 1:]:
assert uid1 != uid2
2022-03-30 22:26:03 +02:00
def test_put_whole_calendar_case_sensitive_uids(self) -> None:
"""Create a whole calendar with case-sensitive UIDs."""
events = get_file_content("event_multiple_case_sensitive_uids.ics")
self.put("/calendar.ics/", events)
_, answer = self.get("/calendar.ics/")
assert "\r\nUID:event\r\n" in answer and "\r\nUID:EVENT\r\n" in answer
2021-07-26 20:56:47 +02:00
def test_put_whole_addressbook(self) -> None:
2017-07-01 00:11:25 +02:00
"""Create and overwrite a whole addressbook."""
contacts = get_file_content("contact_multiple.vcf")
self.put("/contacts.vcf/", contacts)
_, answer = self.get("/contacts.vcf/")
2021-07-26 20:56:47 +02:00
assert answer is not None
assert "\r\nUID:contact1\r\n" in answer
assert "\r\nUID:contact2\r\n" in answer
2017-07-01 00:11:25 +02:00
2021-07-26 20:56:47 +02:00
def test_put_whole_addressbook_without_uids(self) -> None:
2017-07-01 00:11:56 +02:00
"""Create a whole addressbook without UID."""
contacts = get_file_content("contact_multiple.vcf")
contacts = contacts.replace("UID:contact1\n", "").replace(
"UID:contact2\n", "")
assert "\nUID:" not in contacts
self.put("/contacts.vcf/", contacts)
_, answer = self.get("/contacts.vcf")
2017-07-01 00:11:56 +02:00
uids = []
for line in answer.split("\r\n"):
if line.startswith("UID:"):
uids.append(line[len("UID:"):])
assert len(uids) == 2
for i, uid1 in enumerate(uids):
assert uid1
for uid2 in uids[i + 1:]:
assert uid1 != uid2
2021-07-26 20:56:47 +02:00
def test_verify(self) -> None:
2018-09-09 14:58:44 +02:00
"""Verify the storage."""
contacts = get_file_content("contact_multiple.vcf")
self.put("/contacts.vcf/", contacts)
2018-09-09 14:58:44 +02:00
events = get_file_content("event_multiple.ics")
self.put("/calendar.ics/", events)
2018-09-09 14:58:44 +02:00
s = storage.load(self.configuration)
assert s.verify()
2021-07-26 20:56:47 +02:00
def test_delete(self) -> None:
2015-07-24 14:23:11 +02:00
"""Delete an event."""
self.mkcalendar("/calendar.ics/")
event = get_file_content("event1.ics")
path = "/calendar.ics/event1.ics"
self.put(path, event)
_, responses = self.delete(path)
assert responses[path] == 200
_, answer = self.get("/calendar.ics/")
assert "VEVENT" not in answer
2013-09-05 15:13:31 +02:00
2021-07-26 20:56:47 +02:00
def test_mkcalendar(self) -> None:
"""Make a calendar."""
self.mkcalendar("/calendar.ics/")
_, answer = self.get("/calendar.ics/")
2017-07-01 04:20:11 +02:00
assert "BEGIN:VCALENDAR" in answer
assert "END:VCALENDAR" in answer
2021-07-26 20:56:47 +02:00
def test_mkcalendar_overwrite(self) -> None:
"""Try to overwrite an existing calendar."""
self.mkcalendar("/calendar.ics/")
2022-01-16 13:07:56 +01:00
status, answer = self.mkcalendar("/calendar.ics/", check=None)
assert status in (403, 409)
xml = DefusedET.fromstring(answer)
assert xml.tag == xmlutils.make_clark("D:error")
assert xml.find(xmlutils.make_clark(
"D:resource-must-be-null")) is not None
2021-07-26 20:56:47 +02:00
def test_mkcalendar_intermediate(self) -> None:
"""Try make a calendar in a unmapped collection."""
2022-01-16 13:07:56 +01:00
self.mkcalendar("/unmapped/calendar.ics/", check=409)
2021-07-26 20:56:47 +02:00
def test_mkcol(self) -> None:
"""Make a collection."""
self.mkcol("/user/")
2021-07-26 20:56:47 +02:00
def test_mkcol_overwrite(self) -> None:
"""Try to overwrite an existing collection."""
self.mkcol("/user/")
2022-01-16 13:07:56 +01:00
self.mkcol("/user/", check=405)
2021-07-26 20:56:47 +02:00
def test_mkcol_intermediate(self) -> None:
"""Try make a collection in a unmapped collection."""
2022-01-16 13:07:56 +01:00
self.mkcol("/unmapped/user/", check=409)
2021-07-26 20:56:47 +02:00
def test_mkcol_make_calendar(self) -> None:
"""Make a calendar with additional props."""
mkcol_make_calendar = get_file_content("mkcol_make_calendar.xml")
self.mkcol("/calendar.ics/", mkcol_make_calendar)
_, answer = self.get("/calendar.ics/")
2021-07-26 20:56:47 +02:00
assert answer is not None
assert "BEGIN:VCALENDAR" in answer
assert "END:VCALENDAR" in answer
# Read additional properties
propfind = get_file_content("propfind_calendar_color.xml")
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 200 and prop.text == "#BADA55"
2021-07-26 20:56:47 +02:00
def test_move(self) -> None:
"""Move a item."""
self.mkcalendar("/calendar.ics/")
event = get_file_content("event1.ics")
path1 = "/calendar.ics/event1.ics"
path2 = "/calendar.ics/event2.ics"
self.put(path1, event)
2022-01-16 13:07:56 +01:00
self.request("MOVE", path1, check=201,
2023-03-08 15:49:45 +01:00
HTTP_DESTINATION="http://127.0.0.1/"+path2)
self.get(path1, check=404)
self.get(path2)
2018-11-03 21:02:28 +00:00
2024-07-24 11:22:49 +02:00
def test_move_between_collections(self) -> None:
2018-11-03 21:02:28 +00:00
"""Move a item."""
self.mkcalendar("/calendar1.ics/")
self.mkcalendar("/calendar2.ics/")
2018-11-03 21:02:28 +00:00
event = get_file_content("event1.ics")
path1 = "/calendar1.ics/event1.ics"
path2 = "/calendar2.ics/event2.ics"
self.put(path1, event)
2022-01-16 13:07:56 +01:00
self.request("MOVE", path1, check=201,
2023-03-08 15:49:45 +01:00
HTTP_DESTINATION="http://127.0.0.1/"+path2)
self.get(path1, check=404)
self.get(path2)
2024-07-24 11:22:49 +02:00
def test_move_between_collections_duplicate_uid(self) -> None:
"""Move a item to a collection which already contains the UID."""
self.mkcalendar("/calendar1.ics/")
self.mkcalendar("/calendar2.ics/")
event = get_file_content("event1.ics")
path1 = "/calendar1.ics/event1.ics"
path2 = "/calendar2.ics/event2.ics"
self.put(path1, event)
self.put("/calendar2.ics/event1.ics", event)
status, _, answer = self.request(
2023-03-08 15:49:45 +01:00
"MOVE", path1, HTTP_DESTINATION="http://127.0.0.1/"+path2)
assert status in (403, 409)
xml = DefusedET.fromstring(answer)
assert xml.tag == xmlutils.make_clark("D:error")
assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
2024-07-24 11:22:49 +02:00
def test_move_between_collections_overwrite(self) -> None:
"""Move a item to a collection which already contains the item."""
self.mkcalendar("/calendar1.ics/")
self.mkcalendar("/calendar2.ics/")
event = get_file_content("event1.ics")
path1 = "/calendar1.ics/event1.ics"
path2 = "/calendar2.ics/event1.ics"
self.put(path1, event)
self.put(path2, event)
2022-01-16 13:07:56 +01:00
self.request("MOVE", path1, check=412,
2023-03-08 15:49:45 +01:00
HTTP_DESTINATION="http://127.0.0.1/"+path2)
self.request("MOVE", path1, check=204, HTTP_OVERWRITE="T",
HTTP_DESTINATION="http://127.0.0.1/"+path2)
2024-07-24 11:22:49 +02:00
def test_move_between_collections_overwrite_uid_conflict(self) -> None:
"""Move an item to a collection which already contains the item with
a different UID."""
self.mkcalendar("/calendar1.ics/")
self.mkcalendar("/calendar2.ics/")
event1 = get_file_content("event1.ics")
event2 = get_file_content("event2.ics")
path1 = "/calendar1.ics/event1.ics"
path2 = "/calendar2.ics/event2.ics"
self.put(path1, event1)
self.put(path2, event2)
2023-03-08 15:49:45 +01:00
status, _, answer = self.request(
"MOVE", path1, HTTP_OVERWRITE="T",
HTTP_DESTINATION="http://127.0.0.1/"+path2)
assert status in (403, 409)
xml = DefusedET.fromstring(answer)
assert xml.tag == xmlutils.make_clark("D:error")
assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
2021-07-26 20:56:47 +02:00
def test_head(self) -> None:
2022-01-16 13:07:56 +01:00
_, headers, answer = self.request("HEAD", "/", check=302)
2022-01-15 22:32:38 +01:00
assert int(headers.get("Content-Length", "0")) > 0 and not answer
2021-07-26 20:56:47 +02:00
def test_options(self) -> None:
2022-01-16 13:07:56 +01:00
_, headers, _ = self.request("OPTIONS", "/", check=200)
assert "DAV" in headers
2021-07-26 20:56:47 +02:00
def test_delete_collection(self) -> None:
2016-08-01 13:43:43 +02:00
"""Delete a collection."""
self.mkcalendar("/calendar.ics/")
2016-08-01 13:43:43 +02:00
event = get_file_content("event1.ics")
self.put("/calendar.ics/event1.ics", event)
_, responses = self.delete("/calendar.ics/")
assert responses["/calendar.ics/"] == 200
self.get("/calendar.ics/", check=404)
2016-08-01 13:43:43 +02:00
2024-09-29 19:55:16 +02:00
def test_delete_collection_global_forbid(self) -> None:
"""Delete a collection (expect forbidden)."""
2024-09-29 18:10:53 +02:00
self.configure({"rights": {"permit_delete_collection": False}})
self.mkcalendar("/calendar.ics/")
event = get_file_content("event1.ics")
self.put("/calendar.ics/event1.ics", event)
_, responses = self.delete("/calendar.ics/", check=401)
self.get("/calendar.ics/", check=200)
2024-09-29 19:54:54 +02:00
def test_delete_collection_global_forbid_explicit_permit(self) -> None:
"""Delete a collection with permitted path (expect permit)."""
self.configure({"rights": {"permit_delete_collection": False}})
self.mkcalendar("/test-permit-delete/")
event = get_file_content("event1.ics")
self.put("/test-permit-delete/event1.ics", event)
_, responses = self.delete("/test-permit-delete/", check=200)
self.get("/test-permit-delete/", check=404)
def test_delete_collection_global_permit_explicit_forbid(self) -> None:
"""Delete a collection with permitted path (expect forbid)."""
self.configure({"rights": {"permit_delete_collection": True}})
self.mkcalendar("/test-forbid-delete/")
event = get_file_content("event1.ics")
self.put("/test-forbid-delete/event1.ics", event)
_, responses = self.delete("/test-forbid-delete/", check=401)
self.get("/test-forbid-delete/", check=200)
2021-07-26 20:56:47 +02:00
def test_delete_root_collection(self) -> None:
2016-08-02 19:31:49 +02:00
"""Delete the root collection."""
self.mkcalendar("/calendar.ics/")
2016-08-02 19:31:49 +02:00
event = get_file_content("event1.ics")
self.put("/event1.ics", event)
self.put("/calendar.ics/event1.ics", event)
_, responses = self.delete("/")
assert len(responses) == 1 and responses["/"] == 200
self.get("/calendar.ics/", check=404)
self.get("/event1.ics", 404)
2016-08-02 19:31:49 +02:00
def test_overwrite_collection_global_forbid(self) -> None:
"""Overwrite a collection (expect forbid)."""
self.configure({"rights": {"permit_overwrite_collection": False}})
event = get_file_content("event1.ics")
self.put("/calender.ics/", event, check=401)
def test_overwrite_collection_global_forbid_explict_permit(self) -> None:
"""Overwrite a collection with permitted path (expect permit)."""
self.configure({"rights": {"permit_overwrite_collection": False}})
event = get_file_content("event1.ics")
self.put("/test-permit-overwrite/", event, check=201)
def test_overwrite_collection_global_permit(self) -> None:
"""Overwrite a collection (expect permit)."""
self.configure({"rights": {"permit_overwrite_collection": True}})
event = get_file_content("event1.ics")
self.put("/calender.ics/", event, check=201)
def test_overwrite_collection_global_permit_explict_forbid(self) -> None:
"""Overwrite a collection with forbidden path (expect forbid)."""
self.configure({"rights": {"permit_overwrite_collection": True}})
event = get_file_content("event1.ics")
self.put("/test-forbid-overwrite/", event, check=401)
2021-07-26 20:56:47 +02:00
def test_propfind(self) -> None:
calendar_path = "/calendar.ics/"
self.mkcalendar("/calendar.ics/")
event = get_file_content("event1.ics")
event_path = posixpath.join(calendar_path, "event.ics")
self.put(event_path, event)
2021-07-26 20:56:46 +02:00
_, responses = self.propfind("/", HTTP_DEPTH="1")
assert len(responses) == 2
assert "/" in responses and calendar_path in responses
2021-07-26 20:56:46 +02:00
_, responses = self.propfind(calendar_path, HTTP_DEPTH="1")
assert len(responses) == 2
assert calendar_path in responses and event_path in responses
2021-07-26 20:56:47 +02:00
def test_propfind_propname(self) -> None:
self.mkcalendar("/calendar.ics/")
event = get_file_content("event1.ics")
self.put("/calendar.ics/event.ics", event)
propfind = get_file_content("propname.xml")
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int)
status, prop = response["D:sync-token"]
assert status == 200 and not prop.text
_, responses = self.propfind("/calendar.ics/event.ics", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/event.ics"]
assert not isinstance(response, int)
status, prop = response["D:getetag"]
assert status == 200 and not prop.text
2021-07-26 20:56:47 +02:00
def test_propfind_allprop(self) -> None:
self.mkcalendar("/calendar.ics/")
event = get_file_content("event1.ics")
self.put("/calendar.ics/event.ics", event)
propfind = get_file_content("allprop.xml")
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int)
status, prop = response["D:sync-token"]
assert status == 200 and prop.text
_, responses = self.propfind("/calendar.ics/event.ics", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/event.ics"]
assert not isinstance(response, int)
status, prop = response["D:getetag"]
assert status == 200 and prop.text
2021-07-26 20:56:47 +02:00
def test_propfind_nonexistent(self) -> None:
"""Read a property that does not exist."""
self.mkcalendar("/calendar.ics/")
propfind = get_file_content("propfind_calendar_color.xml")
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 404 and not prop.text
2021-07-26 20:56:47 +02:00
def test_proppatch(self) -> None:
"""Set/Remove a property and read it back."""
self.mkcalendar("/calendar.ics/")
proppatch = get_file_content("proppatch_set_calendar_color.xml")
_, responses = self.proppatch("/calendar.ics/", proppatch)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
2016-08-11 00:20:48 +02:00
# Read property back
propfind = get_file_content("propfind_calendar_color.xml")
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 200 and prop.text == "#BADA55"
propfind = get_file_content("allprop.xml")
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int)
status, prop = response["ICAL:calendar-color"]
assert status == 200 and prop.text == "#BADA55"
# Remove property
proppatch = get_file_content("proppatch_remove_calendar_color.xml")
_, responses = self.proppatch("/calendar.ics/", proppatch)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
# Read property back
propfind = get_file_content("propfind_calendar_color.xml")
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 404
2021-07-26 20:56:47 +02:00
def test_proppatch_multiple1(self) -> None:
"""Set/Remove a multiple properties and read them back."""
self.mkcalendar("/calendar.ics/")
propfind = get_file_content("propfind_multiple.xml")
proppatch = get_file_content("proppatch_set_multiple1.xml")
_, responses = self.proppatch("/calendar.ics/", proppatch)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
2021-07-26 20:56:47 +02:00
status, prop = response["C:calendar-description"]
assert status == 200 and not prop.text
# Read properties back
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 200 and prop.text == "#BADA55"
2021-07-26 20:56:47 +02:00
status, prop = response["C:calendar-description"]
assert status == 200 and prop.text == "test"
# Remove properties
proppatch = get_file_content("proppatch_remove_multiple1.xml")
_, responses = self.proppatch("/calendar.ics/", proppatch)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
2021-07-26 20:56:47 +02:00
status, prop = response["C:calendar-description"]
assert status == 200 and not prop.text
# Read properties back
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 404
2021-07-26 20:56:47 +02:00
status, prop = response["C:calendar-description"]
assert status == 404
2021-07-26 20:56:47 +02:00
def test_proppatch_multiple2(self) -> None:
"""Set/Remove a multiple properties and read them back."""
self.mkcalendar("/calendar.ics/")
propfind = get_file_content("propfind_multiple.xml")
proppatch = get_file_content("proppatch_set_multiple2.xml")
_, responses = self.proppatch("/calendar.ics/", proppatch)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
2021-07-26 20:56:47 +02:00
status, prop = response["C:calendar-description"]
assert status == 200 and not prop.text
# Read properties back
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 2
assert len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 200 and prop.text == "#BADA55"
2021-07-26 20:56:47 +02:00
status, prop = response["C:calendar-description"]
assert status == 200 and prop.text == "test"
# Remove properties
proppatch = get_file_content("proppatch_remove_multiple2.xml")
_, responses = self.proppatch("/calendar.ics/", proppatch)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
2021-07-26 20:56:47 +02:00
status, prop = response["C:calendar-description"]
assert status == 200 and not prop.text
# Read properties back
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 404
2021-07-26 20:56:47 +02:00
status, prop = response["C:calendar-description"]
assert status == 404
2021-07-26 20:56:47 +02:00
def test_proppatch_set_and_remove(self) -> None:
"""Set and remove multiple properties in single request."""
self.mkcalendar("/calendar.ics/")
propfind = get_file_content("propfind_multiple.xml")
# Prepare
proppatch = get_file_content("proppatch_set_multiple1.xml")
self.proppatch("/calendar.ics/", proppatch)
# Remove and set properties in single request
proppatch = get_file_content("proppatch_set_and_remove.xml")
_, responses = self.proppatch("/calendar.ics/", proppatch)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
2021-07-26 20:56:47 +02:00
status, prop = response["C:calendar-description"]
assert status == 200 and not prop.text
# Read properties back
_, responses = self.propfind("/calendar.ics/", propfind)
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/"]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 404
2021-07-26 20:56:47 +02:00
status, prop = response["C:calendar-description"]
assert status == 200 and prop.text == "test2"
2016-08-11 00:20:48 +02:00
2021-07-26 20:56:47 +02:00
def test_put_whole_calendar_multiple_events_with_same_uid(self) -> None:
2016-07-07 17:49:56 +02:00
"""Add two events with the same UID."""
self.put("/calendar.ics/", get_file_content("event2.ics"))
_, responses = self.report("/calendar.ics/", """\
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop xmlns:D="DAV:">
<D:getetag/>
</D:prop>
</C:calendar-query>""")
assert len(responses) == 1
2021-07-26 20:56:47 +02:00
response = responses["/calendar.ics/event2.ics"]
assert not isinstance(response, int)
status, prop = response["D:getetag"]
assert status == 200 and prop.text
_, answer = self.get("/calendar.ics/")
2016-07-07 17:49:56 +02:00
assert answer.count("BEGIN:VEVENT") == 2
2021-07-26 20:56:47 +02:00
def _test_filter(self, filters: Iterable[str], kind: str = "event",
test: Optional[str] = None, items: Iterable[int] = (1,)
) -> List[str]:
2020-01-19 18:13:05 +01:00
filter_template = "<C:filter>%s</C:filter>"
2021-07-26 20:56:47 +02:00
create_collection_fn: Callable[[str], Any]
2025-05-16 06:33:02 +02:00
if kind in ("event", "journal", "todo", "valarm"):
create_collection_fn = self.mkcalendar
2017-08-29 20:08:35 +02:00
path = "/calendar.ics/"
2020-01-19 18:13:05 +01:00
filename_template = "%s%d.ics"
2017-08-29 20:08:35 +02:00
namespace = "urn:ietf:params:xml:ns:caldav"
report = "calendar-query"
elif kind == "contact":
create_collection_fn = self.create_addressbook
2017-08-29 20:08:35 +02:00
if test:
2020-01-19 18:13:05 +01:00
filter_template = '<C:filter test="%s">%%s</C:filter>' % test
2017-08-29 20:08:35 +02:00
path = "/contacts.vcf/"
2020-01-19 18:13:05 +01:00
filename_template = "%s%d.vcf"
2017-08-29 20:08:35 +02:00
namespace = "urn:ietf:params:xml:ns:carddav"
report = "addressbook-query"
else:
raise ValueError("Unsupported kind: %r" % kind)
2022-01-16 13:07:56 +01:00
status, _, = self.delete(path, check=None)
2017-07-01 00:11:38 +02:00
assert status in (200, 404)
create_collection_fn(path)
2025-05-16 06:30:27 +02:00
logging.warning("Upload items %r", items)
for i in items:
2025-05-16 06:30:27 +02:00
logging.warning("Upload %d", i)
2020-01-19 18:13:05 +01:00
filename = filename_template % (kind, i)
2016-06-20 19:13:04 +02:00
event = get_file_content(filename)
self.put(posixpath.join(path, filename), event)
2025-05-16 06:30:27 +02:00
logging.warning("Upload items finished")
2020-01-19 18:13:05 +01:00
filters_text = "".join(filter_template % f for f in filters)
_, responses = self.report(path, """\
<?xml version="1.0" encoding="utf-8" ?>
<C:{1} xmlns:C="{0}">
<D:prop xmlns:D="DAV:">
<D:getetag/>
</D:prop>
{2}
</C:{1}>""".format(namespace, report, filters_text))
2021-07-26 20:56:47 +02:00
assert responses is not None
paths = []
for path, props in responses.items():
2021-07-26 20:56:47 +02:00
assert not isinstance(props, int) and len(props) == 1
status, prop = props["D:getetag"]
assert status == 200 and prop.text
paths.append(path)
return paths
2021-07-26 20:56:47 +02:00
def test_addressbook_empty_filter(self) -> None:
2017-08-29 20:08:35 +02:00
self._test_filter([""], kind="contact")
2021-07-26 20:56:47 +02:00
def test_addressbook_prop_filter(self) -> None:
assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap" match-type="contains"
>es</C:text-match>
</C:prop-filter>"""], "contact")
assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap">es</C:text-match>
</C:prop-filter>"""], "contact")
assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap" match-type="contains"
>a</C:text-match>
</C:prop-filter>"""], "contact")
assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap" match-type="equals"
>test</C:text-match>
</C:prop-filter>"""], "contact")
assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap" match-type="equals"
>tes</C:text-match>
</C:prop-filter>"""], "contact")
assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap" match-type="equals"
>est</C:text-match>
</C:prop-filter>"""], "contact")
assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap" match-type="starts-with"
>tes</C:text-match>
</C:prop-filter>"""], "contact")
assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap" match-type="starts-with"
>est</C:text-match>
</C:prop-filter>"""], "contact")
assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap" match-type="ends-with"
>est</C:text-match>
</C:prop-filter>"""], "contact")
assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap" match-type="ends-with"
>tes</C:text-match>
</C:prop-filter>"""], "contact")
2017-08-29 20:08:35 +02:00
2021-07-26 20:56:47 +02:00
def test_addressbook_prop_filter_any(self) -> None:
assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap">test</C:text-match>
</C:prop-filter>
<C:prop-filter name="EMAIL">
<C:text-match collation="i;unicode-casemap">test</C:text-match>
</C:prop-filter>"""], "contact", test="anyof")
assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap">a</C:text-match>
</C:prop-filter>
<C:prop-filter name="EMAIL">
<C:text-match collation="i;unicode-casemap">test</C:text-match>
</C:prop-filter>"""], "contact", test="anyof")
assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap">test</C:text-match>
</C:prop-filter>
<C:prop-filter name="EMAIL">
<C:text-match collation="i;unicode-casemap">test</C:text-match>
</C:prop-filter>"""], "contact")
2017-08-29 20:08:35 +02:00
2021-07-26 20:56:47 +02:00
def test_addressbook_prop_filter_all(self) -> None:
assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap">tes</C:text-match>
</C:prop-filter>
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap">est</C:text-match>
</C:prop-filter>"""], "contact", test="allof")
assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap">test</C:text-match>
</C:prop-filter>
<C:prop-filter name="EMAIL">
<C:text-match collation="i;unicode-casemap">test</C:text-match>
</C:prop-filter>"""], "contact", test="allof")
2017-08-29 20:08:35 +02:00
2021-07-26 20:56:47 +02:00
def test_calendar_empty_filter(self) -> None:
self._test_filter([""])
2021-07-26 20:56:47 +02:00
def test_calendar_tag_filter(self) -> None:
"""Report request with tag-based filter on calendar."""
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR"></C:comp-filter>"""])
2021-07-26 20:56:47 +02:00
def test_item_tag_filter(self) -> None:
"""Report request with tag-based filter on an item."""
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT"></C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO"></C:comp-filter>
</C:comp-filter>"""])
2021-07-26 20:56:47 +02:00
def test_item_not_tag_filter(self) -> None:
"""Report request with tag-based is-not filter on an item."""
assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:is-not-defined />
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:is-not-defined />
</C:comp-filter>
</C:comp-filter>"""])
2021-07-26 20:56:47 +02:00
def test_item_prop_filter(self) -> None:
"""Report request with prop-based filter on an item."""
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="SUMMARY"></C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="UNKNOWN"></C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
2021-07-26 20:56:47 +02:00
def test_item_not_prop_filter(self) -> None:
"""Report request with prop-based is-not filter on an item."""
assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="SUMMARY">
<C:is-not-defined />
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="UNKNOWN">
<C:is-not-defined />
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
2021-07-26 20:56:47 +02:00
def test_mutiple_filters(self) -> None:
"""Report request with multiple filters on an item."""
assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="SUMMARY">
<C:is-not-defined />
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>""", """
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="UNKNOWN">
<C:is-not-defined />
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="SUMMARY"></C:prop-filter>
</C:comp-filter>
</C:comp-filter>""", """
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="UNKNOWN">
<C:is-not-defined />
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="SUMMARY"></C:prop-filter>
<C:prop-filter name="UNKNOWN">
<C:is-not-defined />
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
2021-07-26 20:56:47 +02:00
def test_text_match_filter(self) -> None:
2016-05-27 14:44:59 +02:00
"""Report request with text-match filter on calendar."""
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="SUMMARY">
<C:text-match>event</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="CATEGORIES">
<C:text-match>some_category1</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="CATEGORIES">
<C:text-match collation="i;octet">some_category1</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="UNKNOWN">
<C:text-match>event</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="SUMMARY">
<C:text-match>unknown</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="SUMMARY">
<C:text-match negate-condition="yes">event</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
2016-05-27 14:44:59 +02:00
2021-07-26 20:56:47 +02:00
def test_param_filter(self) -> None:
2016-05-27 14:44:59 +02:00
"""Report request with param-filter on calendar."""
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="ATTENDEE">
<C:param-filter name="PARTSTAT">
<C:text-match collation="i;ascii-casemap"
2016-05-27 14:44:59 +02:00
>ACCEPTED</C:text-match>
</C:param-filter>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="ATTENDEE">
<C:param-filter name="PARTSTAT">
<C:text-match collation="i;ascii-casemap"
2016-05-27 14:44:59 +02:00
>UNKNOWN</C:text-match>
</C:param-filter>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" not in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="ATTENDEE">
<C:param-filter name="PARTSTAT">
<C:is-not-defined />
</C:param-filter>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
assert "/calendar.ics/event1.ics" in self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="ATTENDEE">
<C:param-filter name="UNKNOWN">
<C:is-not-defined />
</C:param-filter>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""])
2021-07-26 20:56:47 +02:00
def test_time_range_filter_events(self) -> None:
2016-06-20 19:13:04 +02:00
"""Report request with time-range filter on events."""
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "event", items=range(1, 6))
assert "/calendar.ics/event1.ics" in answer
assert "/calendar.ics/event2.ics" in answer
assert "/calendar.ics/event3.ics" in answer
assert "/calendar.ics/event4.ics" in answer
assert "/calendar.ics/event5.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "event", items=range(1, 6))
assert "/calendar.ics/event1.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="ATTENDEE">
<C:param-filter name="PARTSTAT">
<C:is-not-defined />
</C:param-filter>
</C:prop-filter>
<C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], items=range(1, 6))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" not in answer
assert "/calendar.ics/event3.ics" not in answer
assert "/calendar.ics/event4.ics" not in answer
assert "/calendar.ics/event5.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20130902T000000Z" end="20131001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], items=range(1, 6))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" in answer
assert "/calendar.ics/event3.ics" in answer
assert "/calendar.ics/event4.ics" in answer
assert "/calendar.ics/event5.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20130903T000000Z" end="20130908T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], items=range(1, 6))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" not in answer
assert "/calendar.ics/event3.ics" in answer
assert "/calendar.ics/event4.ics" in answer
assert "/calendar.ics/event5.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20130903T000000Z" end="20130904T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], items=range(1, 6))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" not in answer
assert "/calendar.ics/event3.ics" in answer
assert "/calendar.ics/event4.ics" not in answer
assert "/calendar.ics/event5.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20130805T000000Z" end="20130810T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], items=range(1, 6))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" not in answer
assert "/calendar.ics/event3.ics" not in answer
assert "/calendar.ics/event4.ics" not in answer
assert "/calendar.ics/event5.ics" not in answer
# HACK: VObject doesn't match RECURRENCE-ID to recurrences, the
# overwritten recurrence is still used for filtering.
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20170601T063000Z" end="20170601T070000Z"/>
</C:comp-filter>
</C:comp-filter>"""], items=(6, 7, 8, 9))
assert "/calendar.ics/event6.ics" in answer
assert "/calendar.ics/event7.ics" in answer
assert "/calendar.ics/event8.ics" in answer
assert "/calendar.ics/event9.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20170701T060000Z"/>
</C:comp-filter>
</C:comp-filter>"""], items=(6, 7, 8, 9))
assert "/calendar.ics/event6.ics" in answer
assert "/calendar.ics/event7.ics" in answer
assert "/calendar.ics/event8.ics" in answer
assert "/calendar.ics/event9.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20170702T070000Z" end="20170704T060000Z"/>
</C:comp-filter>
</C:comp-filter>"""], items=(6, 7, 8, 9))
assert "/calendar.ics/event6.ics" not in answer
assert "/calendar.ics/event7.ics" not in answer
assert "/calendar.ics/event8.ics" not in answer
assert "/calendar.ics/event9.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20170602T075959Z" end="20170602T080000Z"/>
</C:comp-filter>
</C:comp-filter>"""], items=(9,))
assert "/calendar.ics/event9.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20170602T080000Z" end="20170603T083000Z"/>
</C:comp-filter>
</C:comp-filter>"""], items=(9,))
assert "/calendar.ics/event9.ics" not in answer
2021-07-26 20:56:47 +02:00
def test_time_range_filter_events_rrule(self) -> None:
"""Report request with time-range filter on events with rrules."""
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "event", items=(1, 2))
assert "/calendar.ics/event1.ics" in answer
assert "/calendar.ics/event2.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "event", items=(1, 2))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20120801T000000Z" end="20121001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "event", items=(1, 2))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20130903T000000Z" end="20130907T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "event", items=(1, 2))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" not in answer
2021-07-26 20:56:47 +02:00
def test_time_range_filter_todos(self) -> None:
2016-06-20 19:13:04 +02:00
"""Report request with time-range filter on todos."""
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=range(1, 9))
assert "/calendar.ics/todo1.ics" in answer
assert "/calendar.ics/todo2.ics" in answer
assert "/calendar.ics/todo3.ics" in answer
assert "/calendar.ics/todo4.ics" in answer
assert "/calendar.ics/todo5.ics" in answer
assert "/calendar.ics/todo6.ics" in answer
assert "/calendar.ics/todo7.ics" in answer
assert "/calendar.ics/todo8.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130901T160000Z" end="20130901T183000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=range(1, 9))
assert "/calendar.ics/todo1.ics" not in answer
assert "/calendar.ics/todo2.ics" in answer
assert "/calendar.ics/todo3.ics" in answer
assert "/calendar.ics/todo4.ics" not in answer
assert "/calendar.ics/todo5.ics" not in answer
assert "/calendar.ics/todo6.ics" not in answer
assert "/calendar.ics/todo7.ics" in answer
assert "/calendar.ics/todo8.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130903T160000Z" end="20130901T183000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=range(1, 9))
assert "/calendar.ics/todo2.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130903T160000Z" end="20130901T173000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=range(1, 9))
assert "/calendar.ics/todo2.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130903T160000Z" end="20130903T173000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=range(1, 9))
assert "/calendar.ics/todo3.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130903T160000Z" end="20130803T203000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=range(1, 9))
assert "/calendar.ics/todo7.ics" in answer
2016-06-20 19:13:04 +02:00
2025-05-16 06:33:02 +02:00
def test_time_range_filter_events_valarm(self) -> None:
"""Report request with time-range filter on events having absolute VALARM."""
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:comp-filter name="VALARM">
<C:time-range start="20151010T030000Z" end="20151010T040000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:comp-filter>"""], "valarm", items=[1, 2])
assert "/calendar.ics/valarm1.ics" not in answer
2025-05-16 07:28:50 +02:00
assert "/calendar.ics/valarm2.ics" in answer # absolute date
2025-05-16 06:33:02 +02:00
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:comp-filter name="VALARM">
<C:time-range start="20151010T010000Z" end="20151010T020000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:comp-filter>"""], "valarm", items=[1, 2])
assert "/calendar.ics/valarm1.ics" not in answer
assert "/calendar.ics/valarm2.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:comp-filter name="VALARM">
<C:time-range start="20151010T080000Z" end="20151010T090000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:comp-filter>"""], "valarm", items=[1, 2])
assert "/calendar.ics/valarm1.ics" not in answer
assert "/calendar.ics/valarm2.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:comp-filter name="VALARM">
<C:time-range start="20151010T053000Z" end="20151010T055000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:comp-filter>"""], "valarm", items=[1, 2])
2025-05-16 07:28:50 +02:00
assert "/calendar.ics/valarm1.ics" in answer # -15 min offset
2025-05-16 06:33:02 +02:00
assert "/calendar.ics/valarm2.ics" not in answer
def test_time_range_filter_todos_completed(self) -> None:
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:prop-filter name="COMPLETED">
<C:time-range start="20130918T000000Z" end="20130922T000000Z"/>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=range(1, 9))
assert "/calendar.ics/todo6.ics" in answer
2021-07-26 20:56:47 +02:00
def test_time_range_filter_todos_rrule(self) -> None:
"""Report request with time-range filter on todos with rrules."""
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=(1, 2, 9))
assert "/calendar.ics/todo1.ics" in answer
assert "/calendar.ics/todo2.ics" in answer
assert "/calendar.ics/todo9.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=(1, 2, 9))
assert "/calendar.ics/todo1.ics" not in answer
assert "/calendar.ics/todo2.ics" in answer
assert "/calendar.ics/todo9.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20140902T000000Z" end="20140903T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=(1, 2))
assert "/calendar.ics/todo1.ics" not in answer
assert "/calendar.ics/todo2.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20140904T000000Z" end="20140914T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=(1, 2))
assert "/calendar.ics/todo1.ics" not in answer
assert "/calendar.ics/todo2.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130902T000000Z" end="20130906T235959Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=(9,))
assert "/calendar.ics/todo9.ics" not in answer
2021-07-26 20:56:47 +02:00
def test_time_range_filter_journals(self) -> None:
2016-06-20 19:13:04 +02:00
"""Report request with time-range filter on journals."""
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=(1, 2, 3))
assert "/calendar.ics/journal1.ics" not in answer
assert "/calendar.ics/journal2.ics" in answer
assert "/calendar.ics/journal3.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=(1, 2, 3))
assert "/calendar.ics/journal1.ics" not in answer
assert "/calendar.ics/journal2.ics" in answer
assert "/calendar.ics/journal3.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="19981229T000000Z" end="19991012T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=(1, 2, 3))
assert "/calendar.ics/journal1.ics" not in answer
assert "/calendar.ics/journal2.ics" not in answer
assert "/calendar.ics/journal3.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="20131229T000000Z" end="21520202T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=(1, 2, 3))
assert "/calendar.ics/journal1.ics" not in answer
assert "/calendar.ics/journal2.ics" in answer
assert "/calendar.ics/journal3.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="20000101T000000Z" end="20000202T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=(1, 2, 3))
assert "/calendar.ics/journal1.ics" not in answer
assert "/calendar.ics/journal2.ics" in answer
assert "/calendar.ics/journal3.ics" in answer
2016-06-20 19:13:04 +02:00
2021-07-26 20:56:47 +02:00
def test_time_range_filter_journals_rrule(self) -> None:
"""Report request with time-range filter on journals with rrules."""
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=(1, 2))
assert "/calendar.ics/journal1.ics" not in answer
assert "/calendar.ics/journal2.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="20051229T000000Z" end="20060202T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=(1, 2))
assert "/calendar.ics/journal1.ics" not in answer
assert "/calendar.ics/journal2.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="20060102T000000Z" end="20060202T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=(1, 2))
assert "/calendar.ics/journal1.ics" not in answer
assert "/calendar.ics/journal2.ics" not in answer
2021-07-26 20:56:47 +02:00
def test_report_item(self) -> None:
"""Test report request on an item"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
event = get_file_content("event1.ics")
event_path = posixpath.join(calendar_path, "event.ics")
self.put(event_path, event)
_, responses = self.report(event_path, """\
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop xmlns:D="DAV:">
<D:getetag />
</D:prop>
</C:calendar-query>""")
assert len(responses) == 1
2021-07-26 20:56:47 +02:00
response = responses[event_path]
2023-10-06 13:15:45 -06:00
assert isinstance(response, dict)
2021-07-26 20:56:47 +02:00
status, prop = response["D:getetag"]
assert status == 200 and prop.text
2023-10-06 13:15:45 -06:00
def test_report_free_busy(self) -> None:
"""Test free busy report on a few items"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
2024-08-14 11:15:30 -06:00
for i in (1, 2, 10):
2023-10-06 13:15:45 -06:00
filename = "event{}.ics".format(i)
event = get_file_content(filename)
self.put(posixpath.join(calendar_path, filename), event)
code, responses = self.report(calendar_path, """\
<?xml version="1.0" encoding="utf-8" ?>
<C:free-busy-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<C:time-range start="20130901T140000Z" end="20130908T220000Z"/>
2024-08-14 11:15:30 -06:00
</C:free-busy-query>""", 200, is_xml=False)
2023-10-06 13:15:45 -06:00
for response in responses.values():
assert isinstance(response, vobject.base.Component)
2023-10-11 12:09:11 -06:00
assert len(responses) == 1
vcalendar = list(responses.values())[0]
2024-08-14 11:15:30 -06:00
assert isinstance(vcalendar, vobject.base.Component)
2023-12-09 18:22:03 -07:00
assert len(vcalendar.vfreebusy_list) == 3
types = {}
2023-10-11 12:09:11 -06:00
for vfb in vcalendar.vfreebusy_list:
2024-08-14 11:15:30 -06:00
fbtype_val = vfb.fbtype.value
if fbtype_val not in types:
types[fbtype_val] = 0
types[fbtype_val] += 1
assert types == {'BUSY': 2, 'FREE': 1}
2023-10-06 13:15:45 -06:00
# Test max_freebusy_occurrence limit
self.configure({"reporting": {"max_freebusy_occurrence": 1}})
code, responses = self.report(calendar_path, """\
<?xml version="1.0" encoding="utf-8" ?>
<C:free-busy-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<C:time-range start="20130901T140000Z" end="20130908T220000Z"/>
</C:free-busy-query>""", 400, is_xml=False)
2021-07-26 20:56:47 +02:00
def _report_sync_token(
self, calendar_path: str, sync_token: Optional[str] = None
) -> Tuple[str, RESPONSES]:
2017-06-02 12:44:31 +02:00
sync_token_xml = (
"<sync-token><![CDATA[%s]]></sync-token>" % sync_token
if sync_token else "<sync-token />")
status, _, answer = self.request("REPORT", calendar_path, """\
<?xml version="1.0" encoding="utf-8" ?>
<sync-collection xmlns="DAV:">
<prop>
<getetag />
</prop>
%s
</sync-collection>""" % sync_token_xml)
2020-05-22 16:33:04 +02:00
xml = DefusedET.fromstring(answer)
if status in (403, 409):
assert xml.tag == xmlutils.make_clark("D:error")
assert sync_token and xml.find(
xmlutils.make_clark("D:valid-sync-token")) is not None
2021-07-26 20:56:47 +02:00
return "", {}
2017-06-02 12:44:31 +02:00
assert status == 207
assert xml.tag == xmlutils.make_clark("D:multistatus")
sync_token = xml.find(xmlutils.make_clark("D:sync-token")).text.strip()
2017-06-02 12:44:31 +02:00
assert sync_token
responses = self.parse_responses(answer)
for href, response in responses.items():
if not isinstance(response, int):
status, prop = response["D:getetag"]
assert status == 200 and prop.text and len(response) == 1
responses[href] = response = 200
assert response in (200, 404)
return sync_token, responses
2017-06-02 12:44:31 +02:00
2021-07-26 20:56:47 +02:00
def test_report_sync_collection_no_change(self) -> None:
2017-06-02 12:44:31 +02:00
"""Test sync-collection report without modifying the collection"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
2017-06-02 12:44:31 +02:00
event = get_file_content("event1.ics")
event_path = posixpath.join(calendar_path, "event.ics")
self.put(event_path, event)
sync_token, responses = self._report_sync_token(calendar_path)
assert len(responses) == 1 and responses[event_path] == 200
new_sync_token, responses = self._report_sync_token(
calendar_path, sync_token)
2018-09-04 03:33:42 +02:00
if not self.full_sync_token_support and not new_sync_token:
2020-01-15 01:23:43 +01:00
return
assert sync_token == new_sync_token and len(responses) == 0
2017-06-02 12:44:31 +02:00
2021-07-26 20:56:47 +02:00
def test_report_sync_collection_add(self) -> None:
2017-06-02 12:44:31 +02:00
"""Test sync-collection report with an added item"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
sync_token, responses = self._report_sync_token(calendar_path)
assert len(responses) == 0
2017-06-02 12:44:31 +02:00
event = get_file_content("event1.ics")
event_path = posixpath.join(calendar_path, "event.ics")
self.put(event_path, event)
sync_token, responses = self._report_sync_token(
calendar_path, sync_token)
2018-09-04 03:33:42 +02:00
if not self.full_sync_token_support and not sync_token:
2020-01-15 01:23:43 +01:00
return
assert len(responses) == 1 and responses[event_path] == 200
2017-06-02 12:44:31 +02:00
2021-07-26 20:56:47 +02:00
def test_report_sync_collection_delete(self) -> None:
2017-06-02 12:44:31 +02:00
"""Test sync-collection report with a deleted item"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
2017-06-02 12:44:31 +02:00
event = get_file_content("event1.ics")
event_path = posixpath.join(calendar_path, "event.ics")
self.put(event_path, event)
sync_token, responses = self._report_sync_token(calendar_path)
assert len(responses) == 1 and responses[event_path] == 200
self.delete(event_path)
sync_token, responses = self._report_sync_token(
calendar_path, sync_token)
2018-09-04 03:33:42 +02:00
if not self.full_sync_token_support and not sync_token:
2020-01-15 01:23:43 +01:00
return
assert len(responses) == 1 and responses[event_path] == 404
2017-06-02 12:44:31 +02:00
2021-07-26 20:56:47 +02:00
def test_report_sync_collection_create_delete(self) -> None:
2017-06-02 12:44:31 +02:00
"""Test sync-collection report with a created and deleted item"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
sync_token, responses = self._report_sync_token(calendar_path)
assert len(responses) == 0
2017-06-02 12:44:31 +02:00
event = get_file_content("event1.ics")
event_path = posixpath.join(calendar_path, "event.ics")
self.put(event_path, event)
self.delete(event_path)
sync_token, responses = self._report_sync_token(
calendar_path, sync_token)
2018-09-04 03:33:42 +02:00
if not self.full_sync_token_support and not sync_token:
2020-01-15 01:23:43 +01:00
return
assert len(responses) == 1 and responses[event_path] == 404
2017-06-02 12:44:31 +02:00
2021-07-26 20:56:47 +02:00
def test_report_sync_collection_modify_undo(self) -> None:
2017-06-02 12:44:31 +02:00
"""Test sync-collection report with a modified and changed back item"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
2017-06-02 12:44:31 +02:00
event1 = get_file_content("event1.ics")
event2 = get_file_content("event1_modified.ics")
event_path = posixpath.join(calendar_path, "event.ics")
self.put(event_path, event1)
sync_token, responses = self._report_sync_token(calendar_path)
assert len(responses) == 1 and responses[event_path] == 200
self.put(event_path, event2, check=204)
self.put(event_path, event1, check=204)
sync_token, responses = self._report_sync_token(
calendar_path, sync_token)
2018-09-04 03:33:42 +02:00
if not self.full_sync_token_support and not sync_token:
2020-01-15 01:23:43 +01:00
return
assert len(responses) == 1 and responses[event_path] == 200
2017-06-02 12:44:31 +02:00
2021-07-26 20:56:47 +02:00
def test_report_sync_collection_move(self) -> None:
2017-06-02 12:44:31 +02:00
"""Test sync-collection report a moved item"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
2017-06-02 12:44:31 +02:00
event = get_file_content("event1.ics")
event1_path = posixpath.join(calendar_path, "event1.ics")
event2_path = posixpath.join(calendar_path, "event2.ics")
self.put(event1_path, event)
sync_token, responses = self._report_sync_token(calendar_path)
assert len(responses) == 1 and responses[event1_path] == 200
2022-01-16 13:07:56 +01:00
self.request("MOVE", event1_path, check=201,
2023-03-08 15:49:45 +01:00
HTTP_DESTINATION="http://127.0.0.1/"+event2_path)
sync_token, responses = self._report_sync_token(
calendar_path, sync_token)
2018-09-04 03:33:42 +02:00
if not self.full_sync_token_support and not sync_token:
2020-01-15 01:23:43 +01:00
return
assert len(responses) == 2 and (responses[event1_path] == 404 and
responses[event2_path] == 200)
2017-06-02 12:44:31 +02:00
2021-07-26 20:56:47 +02:00
def test_report_sync_collection_move_undo(self) -> None:
2017-06-02 12:44:31 +02:00
"""Test sync-collection report with a moved and moved back item"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
2017-06-02 12:44:31 +02:00
event = get_file_content("event1.ics")
event1_path = posixpath.join(calendar_path, "event1.ics")
event2_path = posixpath.join(calendar_path, "event2.ics")
self.put(event1_path, event)
sync_token, responses = self._report_sync_token(calendar_path)
assert len(responses) == 1 and responses[event1_path] == 200
2022-01-16 13:07:56 +01:00
self.request("MOVE", event1_path, check=201,
2023-03-08 15:49:45 +01:00
HTTP_DESTINATION="http://127.0.0.1/"+event2_path)
2022-01-16 13:07:56 +01:00
self.request("MOVE", event2_path, check=201,
2023-03-08 15:49:45 +01:00
HTTP_DESTINATION="http://127.0.0.1/"+event1_path)
sync_token, responses = self._report_sync_token(
calendar_path, sync_token)
2018-09-04 03:33:42 +02:00
if not self.full_sync_token_support and not sync_token:
2020-01-15 01:23:43 +01:00
return
assert len(responses) == 2 and (responses[event1_path] == 200 and
responses[event2_path] == 404)
2017-06-02 12:44:31 +02:00
2021-07-26 20:56:47 +02:00
def test_report_sync_collection_invalid_sync_token(self) -> None:
2017-06-02 12:44:31 +02:00
"""Test sync-collection report with an invalid sync token"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
2020-01-17 12:45:01 +01:00
sync_token, _ = self._report_sync_token(
2017-06-02 12:44:31 +02:00
calendar_path, "http://radicale.org/ns/sync/INVALID")
assert not sync_token
2021-07-26 20:56:47 +02:00
def test_propfind_sync_token(self) -> None:
2017-06-02 12:44:31 +02:00
"""Retrieve the sync-token with a propfind request"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
propfind = get_file_content("allprop.xml")
_, responses = self.propfind(calendar_path, propfind)
2021-07-26 20:56:47 +02:00
response = responses[calendar_path]
assert not isinstance(response, int)
status, sync_token = response["D:sync-token"]
assert status == 200 and sync_token.text
2017-06-02 12:44:31 +02:00
event = get_file_content("event1.ics")
event_path = posixpath.join(calendar_path, "event.ics")
self.put(event_path, event)
_, responses = self.propfind(calendar_path, propfind)
2021-07-26 20:56:47 +02:00
response = responses[calendar_path]
assert not isinstance(response, int)
status, new_sync_token = response["D:sync-token"]
assert status == 200 and new_sync_token.text
assert sync_token.text != new_sync_token.text
2017-06-02 12:44:31 +02:00
2021-07-26 20:56:47 +02:00
def test_propfind_same_as_sync_collection_sync_token(self) -> None:
2017-06-02 12:44:31 +02:00
"""Compare sync-token property with sync-collection sync-token"""
calendar_path = "/calendar.ics/"
self.mkcalendar(calendar_path)
propfind = get_file_content("allprop.xml")
_, responses = self.propfind(calendar_path, propfind)
2021-07-26 20:56:47 +02:00
response = responses[calendar_path]
assert not isinstance(response, int)
status, sync_token = response["D:sync-token"]
assert status == 200 and sync_token.text
report_sync_token, _ = self._report_sync_token(calendar_path)
assert sync_token.text == report_sync_token
2017-06-02 12:44:31 +02:00
2021-07-26 20:56:47 +02:00
def test_calendar_getcontenttype(self) -> None:
"""Test report request on an item"""
self.mkcalendar("/test/")
for component in ("event", "todo", "journal"):
2020-01-19 18:13:05 +01:00
event = get_file_content("%s1.ics" % component)
2022-01-16 13:07:56 +01:00
status, _ = self.delete("/test/test.ics", check=None)
assert status in (200, 404)
self.put("/test/test.ics", event)
_, responses = self.report("/test/", """\
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop xmlns:D="DAV:">
<D:getcontenttype />
</D:prop>
</C:calendar-query>""")
2021-07-26 20:56:47 +02:00
assert len(responses) == 1
response = responses["/test/test.ics"]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["D:getcontenttype"]
assert status == 200 and prop.text == (
"text/calendar;charset=utf-8;component=V%s" %
component.upper())
2021-07-26 20:56:47 +02:00
def test_addressbook_getcontenttype(self) -> None:
"""Test report request on an item"""
self.create_addressbook("/test/")
contact = get_file_content("contact1.vcf")
self.put("/test/test.vcf", contact)
_, responses = self.report("/test/", """\
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop xmlns:D="DAV:">
<D:getcontenttype />
</D:prop>
</C:calendar-query>""")
2021-07-26 20:56:47 +02:00
assert len(responses) == 1
response = responses["/test/test.vcf"]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["D:getcontenttype"]
assert status == 200 and prop.text == "text/vcard;charset=utf-8"
2021-07-26 20:56:47 +02:00
def test_authorization(self) -> None:
self.configure({"auth": {"type": "none"}})
_, responses = self.propfind("/", """\
<?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:">
<prop>
<current-user-principal />
</prop>
2020-04-22 19:20:36 +02:00
</propfind>""", login="user:")
2021-07-26 20:56:47 +02:00
response = responses["/"]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["D:current-user-principal"]
assert status == 200 and len(prop) == 1
2021-07-26 20:56:47 +02:00
element = prop.find(xmlutils.make_clark("D:href"))
assert element is not None and element.text == "/user/"
2016-09-01 06:15:31 +02:00
2021-07-26 20:56:47 +02:00
def test_authentication(self) -> None:
"""Test if server sends authentication request."""
2021-12-10 20:54:04 +01:00
self.configure({"auth": {"type": "htpasswd",
"htpasswd_filename": os.devnull,
"htpasswd_encryption": "plain"},
"rights": {"type": "owner_only"}})
2017-07-01 04:20:11 +02:00
status, headers, _ = self.request("MKCOL", "/user/")
assert status in (401, 403)
assert headers.get("WWW-Authenticate")
2021-07-26 20:56:47 +02:00
def test_principal_collection_creation(self) -> None:
"""Verify existence of the principal collection."""
self.configure({"auth": {"type": "none"}})
2020-04-22 19:20:36 +02:00
self.propfind("/user/", login="user:")
2021-07-26 20:56:47 +02:00
def test_authentication_current_user_principal_hack(self) -> None:
"""Test if server sends authentication request when accessing
current-user-principal prop (workaround for DAVx5)."""
status, headers, _ = self.request("PROPFIND", "/", """\
<?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:">
<prop>
<current-user-principal />
</prop>
</propfind>""")
assert status in (401, 403)
assert headers.get("WWW-Authenticate")
2021-07-26 20:56:47 +02:00
def test_existence_of_root_collections(self) -> None:
"""Verify that the root collection always exists."""
# Use PROPFIND because GET returns message
self.propfind("/")
# it should still exist after deletion
self.delete("/")
self.propfind("/")
def test_well_known(self) -> None:
for path in ["/.well-known/caldav", "/.well-known/carddav"]:
for path in [path, "/foo" + path]:
_, headers, _ = self.request("GET", path, check=301)
assert headers.get("Location") == "/"
def test_well_known_script_name(self) -> None:
for path in ["/.well-known/caldav", "/.well-known/carddav"]:
for path in [path, "/foo" + path]:
_, headers, _ = self.request(
"GET", path, check=301, SCRIPT_NAME="/radicale")
assert headers.get("Location") == "/radicale/"
def test_well_known_not_found(self) -> None:
for path in ["/.well-known", "/.well-known/", "/.well-known/foo"]:
for path in [path, "/foo" + path]:
self.get(path, check=404)
2021-07-26 20:56:47 +02:00
def test_custom_headers(self) -> None:
2021-12-10 20:54:04 +01:00
self.configure({"headers": {"test": "123"}})
# Test if header is set on success
2022-01-16 13:07:56 +01:00
_, headers, _ = self.request("OPTIONS", "/", check=200)
assert headers.get("test") == "123"
# Test if header is set on failure
_, headers, _ = self.request("GET", "/.well-known/foo", check=404)
assert headers.get("test") == "123"
2021-07-26 20:56:47 +02:00
def test_timezone_seconds(self) -> None:
2018-04-21 10:44:59 +02:00
"""Verify that timezones with minutes and seconds work."""
self.mkcalendar("/calendar.ics/")
2018-04-21 10:44:59 +02:00
event = get_file_content("event_timezone_seconds.ics")
self.put("/calendar.ics/event.ics", event)