# This file is part of Radicale - CalDAV and CardDAV server
# Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2022 Unrud <unrud@outlook.com>
# Copyright © 2024-2026 Peter Bieringer <pb@bieringer.de>
#
# 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.

"""

import datetime
import logging
import os
import posixpath
import sys
import urllib
from typing import Any, Callable, ClassVar, Iterable, List, Optional, Tuple

import defusedxml.ElementTree as DefusedET
import pytest
import vobject

from radicale import pathutils, storage, utils, xmlutils
from radicale.tests import RESPONSES, BaseTest
from radicale.tests.helpers import get_file_content


class TestBaseRequests(BaseTest):
    """Tests with simple requests."""

    # Allow skipping sync-token tests, when not fully supported by the backend
    full_sync_token_support: ClassVar[bool] = True

    def setup_method(self) -> None:
        BaseTest.setup_method(self)
        rights_file_path = os.path.join(self.colpath, "rights")
        with open(rights_file_path, "w") as f:
            f.write("""\
[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

[allow all]
user: .*
collection: .*
permissions: RrWw""")
        self.configure({"rights": {"file": rights_file_path,
                                   "type": "from_file"},
                        "server": {"delay_on_error": 0},
                        "logging": {"request_header_on_debug": "True",
                                    "request_content_on_debug": "True",
                                    "response_header_on_debug": "True",
                                    "response_content_on_debug": "True"}})

    def test_root(self) -> None:
        """GET request at "/"."""
        for path in ["", "/", "//"]:
            _, headers, answer = self.request("GET", path, check=302)
            assert headers.get("Location") == "/.web"
            assert answer == "Redirected to /.web"

    def test_root_script_name(self) -> None:
        """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"

    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")

    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"

    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")

    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
            _, headers, _ = self.request("GET", path, check=301,
                                         HTTP_X_SCRIPT_NAME="/radicale")
            assert headers.get("Location") == "/radicale%s" % sane_path

    def test_add_event(self) -> None:
        """Add an event."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        path = "/calendar.ics/event1.ics"
        self.put(path, event)
        _, headers, answer = self.request("GET", path, check=200)
        assert "ETag" in headers
        assert headers["Content-Type"] == "text/calendar; charset=utf-8"
        assert "VEVENT" in answer
        assert "Event" in answer
        assert "UID:event" in answer

    @pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject <= 0.9.9 does not support PERIOD")
    def test_add_event_with_rdate_period_start_duration_single(self) -> None:
        """Add an event with RDATE/PERIOD with start+duration."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_rdate_period_start_duration.ics")
        path = "/calendar.ics/event_rdate_period_start_duration.ics"
        self.put(path, event)
        _, headers, answer = self.request("GET", path, check=200)
        assert "RDATE;VALUE=PERIOD:20000102T000000Z/PT2H" in answer

    @pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject <= 0.9.9 does not support PERIOD")
    def test_add_event_with_rdate_period_start_duration_multi(self) -> None:
        """Add an event with RDATE/PERIOD with multiple start+duration."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_rdate_period_start_duration_multi.ics")
        path = "/calendar.ics/event_rdate_period_start_duration_multi.ics"
        self.put(path, event)
        _, headers, answer = self.request("GET", path, check=200)
        assert "RDATE;VALUE=PERIOD:20000102T000000Z/PT2H,20010102T000000Z/PT2H" in answer

    @pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject <= 0.9.9 does not support PERIOD")
    def test_add_event_with_rdate_period_start_stop_single(self) -> None:
        """Add an event with RDATE/PERIOD with start+stop."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_rdate_period_start_stop.ics")
        path = "/calendar.ics/event_rdate_period_start_stop.ics"
        self.put(path, event)
        _, headers, answer = self.request("GET", path, check=200)
        assert "RDATE;VALUE=PERIOD:20000102T000000Z/20000402T000000Z" in answer

    @pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject <= 0.9.9 does not support PERIOD")
    def test_add_event_with_rdate_period_start_stop_multi(self) -> None:
        """Add an event with RDATE/PERIOD with multiple start+stop."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_rdate_period_start_stop_multi.ics")
        path = "/calendar.ics/event_rdate_period_start_stop_multi.ics"
        self.put(path, event)
        _, headers, answer = self.request("GET", path, check=200)
        # wrapped answer
        assert "RDATE;VALUE=PERIOD:20000102T000000Z/20000402T000000Z,20010102T000000Z/20010" in answer
        assert " 402T000000Z" in answer

    def test_add_event_with_tz_trailing_space(self) -> None:
        """Add an event."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_issue2172.ics")
        path = "/calendar.ics/event_issue2172.ics"
        self.put(path, event)
        _, headers, answer = self.request("GET", path, check=200)
        assert "ETag" in headers
        assert headers["Content-Type"] == "text/calendar; charset=utf-8"
        assert "DESCRIPTION" in answer
        assert "TZID:Mitteleuropäische Zeit " not in answer
        assert "TZID:Mitteleuropäische Zeit" in answer
        assert "TZNAME:Mitteleuropäische Zeit " not in answer
        assert "TZNAME:Mitteleuropäische Zeit" in answer
        assert "DTSTART;TZID=Mitteleuropäische Zeit :20260701T170000" not in answer
        assert "DTSTART;TZID=Mitteleuropäische Zeit:20260701T170000" in answer
        assert "DTEND;TZID=Mitteleuropäische Zeit :20260701T183000" not in answer
        assert "DTEND;TZID=Mitteleuropäische Zeit:20260701T183000" in answer

    def test_add_event_with_desc_ok(self) -> None:
        """Add an event."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_issue1970_ok.ics")
        path = "/calendar.ics/event_issue1970_ok.ics"
        self.put(path, event)
        _, headers, answer = self.request("GET", path, check=200)
        assert "ETag" in headers
        assert headers["Content-Type"] == "text/calendar; charset=utf-8"
        assert "DESCRIPTION" in answer
        assert "VEVENT" in answer
        assert "Event" in answer
        assert "UID:event" in answer

    def test_add_event_with_desc_problem(self) -> None:
        """Add an event."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_issue1970_problem.ics")
        path = "/calendar.ics/event_issue1970_problem.ics"
        self.put(path, event)
        _, headers, answer = self.request("GET", path, check=200)
        assert "ETag" in headers
        assert headers["Content-Type"] == "text/calendar; charset=utf-8"
        assert "DESCRIPTION" in answer
        assert "VEVENT" in answer
        assert "Event" in answer
        assert "UID:event" in answer

    def test_add_event_exceed_size(self) -> None:
        """Add an event which is exceeding max-resource-size."""
        self.configure({"server": {"max_resource_size": 20}})
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        path = "/calendar.ics/event1.ics"
        self.put(path, event, check=412)

    def test_add_events_exceed_size(self) -> None:
        """Add multipe events where last is exceeding max-resource-size."""
        self.configure({"server": {"max_resource_size": 603}})
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_multiple3.ics")
        path = "/calendar.ics/"
        self.put(path, event, check=412)

    def test_get_vcard_exceed_size(self) -> None:
        """Add vcards partially exceeding max-resource-size (adjusted after upload)."""
        path_base = "/contacts.vcf/"
        file1 = "contact1.vcf"
        file2 = "contact_photo_with_data_uri.vcf"
        self.create_addressbook(path_base)
        contact1 = get_file_content(file1)
        contact2 = get_file_content(file2)
        path1 = path_base + file1
        path2 = path_base + file2
        self.put(path1, contact1)
        self.put(path2, contact2)
        self.configure({"server": {"max_resource_size": 100}})
        # test "get"
        self.get(path1, check=200)
        self.get(path2, check=404)
        # test "report"
        _, responses = self.report(path_base, """\
<?xml version="1.0"?>
<CR:addressbook-multiget xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
   <prop>
     <getetag />
   </prop>
   <href>""" + path_base + """</href>
</CR:addressbook-multiget>""")
        logging.info("response: %r", responses)
        assert len(responses) == 1
        response = responses[path1]
        assert isinstance(response, dict)
        status, prop = response["D:getetag"]
        assert status == 200 and prop.text

    def test_add_event_broken(self) -> None:
        """Add a broken event."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("broken-vevent.ics")
        path = "/calendar.ics/broken-vevent.ics"
        self.put(path, event, check=400)

    def test_add_events_broken2(self) -> None:
        """Add a broken event (2nd one is broken)."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("broken-vevents.ics")
        path = "/calendar.ics/"
        self.put(path, event, check=400)

    def test_add_event_without_uid(self) -> None:
        """Add an event without UID."""
        self.mkcalendar("/calendar.ics/")
        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)

    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(
            "/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)

    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)

    def test_add_event_exdate_no_tz(self) -> None:
        """Test event where EXDATE has no tzinfo."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_issue2151.ics")
        self.put("/calendar.ics/event_issue2151.ics", event)

    def test_add_event_dtstart_no_tz_exdate_tz(self) -> None:
        """Test event where DTSTART has no tzinfo but EXDATE."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_mixed_datetime_and_date_exdate.ics")
        self.put("/calendar.ics/event_mixed_datetime_and_date_exdate.ics", event)

    def test_add_todo(self) -> None:
        """Add a todo."""
        self.mkcalendar("/calendar.ics/")
        todo = get_file_content("todo1.ics")
        path = "/calendar.ics/todo1.ics"
        self.put(path, todo)
        _, headers, answer = self.request("GET", path, check=200)
        assert "ETag" in headers
        assert headers["Content-Type"] == "text/calendar; charset=utf-8"
        assert "VTODO" in answer
        assert "Todo" in answer
        assert "UID:todo" in answer

    def test_add_contact(self) -> None:
        """Add a contact."""
        self.create_addressbook("/contacts.vcf/")
        contact = get_file_content("contact1.vcf")
        path = "/contacts.vcf/contact.vcf"
        self.put(path, contact)
        _, headers, answer = self.request("GET", path, check=200)
        assert "ETag" in headers
        assert headers["Content-Type"] == "text/vcard; charset=utf-8"
        assert "VCARD" in answer
        assert "UID:contact1" in answer
        _, answer = self.get(path)
        assert "UID:contact1" in answer

    def test_add_contact_broken(self) -> None:
        """Add a broken contact."""
        self.create_addressbook("/contacts.vcf/")
        contact = get_file_content("broken-vcard.vcf")
        path = "/contacts.vcf/broken-vcards.vcf"
        self.put(path, contact, check=400)

    def test_add_contacts_broken(self) -> None:
        """Add broken contacts."""
        self.create_addressbook("/contacts.vcf/")
        contact = get_file_content("broken-vcards.vcf")
        path = "/contacts.vcf/"
        self.put(path, contact, check=400)

    def test_add_contacts_broken2(self) -> None:
        """Add broken contacts (only 2nd one is broken)."""
        self.create_addressbook("/contacts.vcf/")
        contact = get_file_content("broken-vcards2.vcf")
        path = "/contacts.vcf/"
        self.put(path, contact, check=400)

    def test_add_contacts_broken2_no_uid(self) -> None:
        """Add broken contacts (only 2nd one is broken and has no UID)."""
        self.create_addressbook("/contacts.vcf/")
        contact = get_file_content("broken-vcards2-no_uid.vcf")
        path = "/contacts.vcf/"
        self.put(path, contact, check=400)

    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)

    def test_add_contact_without_uid(self) -> None:
        """Add a contact without UID."""
        self.create_addressbook("/contacts.vcf/")
        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)

    def test_add_contact_v3(self) -> None:
        """Add a vCard 3.0 contact."""
        self.create_addressbook("/contacts.vcf/")
        contact = get_file_content("contact1.vcf")
        path = "/contacts.vcf/contact.vcf"
        self.put(path, contact)
        _, headers, answer = self.request("GET", path, check=200)
        assert "ETag" in headers
        assert headers["Content-Type"] == "text/vcard; charset=utf-8"
        assert "VCARD" in answer
        assert "UID:contact1" in answer
        assert "VERSION:3.0" in answer

    @pytest.mark.skipif(not utils.vobject_supports_vcard4(),
                        reason="vobject < 1.0.0 does not support vCard 4.0")
    def test_add_contact_v4(self) -> None:
        """Add a vCard 4.0 contact (requires vobject >= 1.0.0)."""
        self.create_addressbook("/contacts.vcf/")
        contact = get_file_content("contact1_v4.vcf")
        path = "/contacts.vcf/contact.vcf"
        self.put(path, contact)
        _, headers, answer = self.request("GET", path, check=200)
        assert "ETag" in headers
        assert headers["Content-Type"] == "text/vcard; charset=utf-8"
        assert "VCARD" in answer
        assert "UID:contact1" in answer
        assert "VERSION:4.0" in answer

    def test_add_contact_photo_with_data_uri_v3(self) -> None:
        """Test vCard 3.0 PHOTO format"""
        self.create_addressbook("/contacts.vcf/")
        contact = get_file_content("contact_photo_with_data_uri.vcf")
        self.put("/contacts.vcf/contact.vcf", contact)

    @pytest.mark.skipif(not utils.vobject_supports_vcard4(),
                        reason="vobject < 1.0.0 does not support vCard 4.0")
    def test_add_contact_photo_with_data_uri_v4(self) -> None:
        """Test vCard 4.0 PHOTO data URI format (requires vobject >= 1.0.0)"""
        self.create_addressbook("/contacts.vcf/")
        contact = get_file_content("contact_photo_with_data_uri_v4.vcf")
        self.put("/contacts.vcf/contact.vcf", contact)

    def test_update_event(self) -> None:
        """Update an event."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        event_modified = get_file_content("event1_modified.ics")
        path = "/calendar.ics/event1.ics"
        self.put(path, event)
        self.put(path, event_modified, check=204)
        _, answer = self.get("/calendar.ics/")
        assert answer.count("BEGIN:VEVENT") == 1
        _, answer = self.get(path)
        assert "DTSTAMP:20130902T150159Z" in answer

    def test_update_event_no_etag_strict_preconditions_true(self) -> None:
        """Update an event without serving etag having strict_preconditions enabled (Precondition Failed)."""
        self.configure({"storage": {"strict_preconditions": True}})
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        event_modified = get_file_content("event1_modified.ics")
        path = "/calendar.ics/event1.ics"
        self.put(path, event, check=201)
        self.put(path, event_modified, check=412)

    def test_update_event_with_etag_strict_preconditions_true(self) -> None:
        """Update an event with serving equal etag having strict_preconditions enabled (OK)."""
        self.configure({"storage": {"strict_preconditions": True}})
        self.configure({"logging": {"response_content_on_debug": True}})
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        event_modified = get_file_content("event1_modified.ics")
        path = "/calendar.ics/event1.ics"
        self.put(path, event, check=201)
        # get etag
        _, 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
        response = responses["/calendar.ics/event1.ics"]
        assert not isinstance(response, int)
        status, prop = response["D:getetag"]
        assert status == 200 and prop.text
        self.put(path, event_modified, check=204, http_if_match=prop.text)

    def test_update_event_with_etag_mismatch(self) -> None:
        """Update an event with serving mismatch etag (Precondition Failed)."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        event_modified = get_file_content("event1_modified.ics")
        path = "/calendar.ics/event1.ics"
        self.put(path, event, check=201)
        self.put(path, event_modified, check=412, http_if_match="0000")

    def test_add_event_with_etag(self) -> None:
        """Add an event with serving etag (Precondition Failed)."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        path = "/calendar.ics/event1.ics"
        self.put(path, event, check=412, http_if_match="0000")

    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)
        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

    def test_put_whole_calendar(self) -> None:
        """Create and overwrite a whole calendar."""
        self.put("/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
        event1 = get_file_content("event1.ics")
        self.put("/calendar.ics/test_event.ics", event1)
        # 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

    def test_put_whole_calendar_without_uids(self) -> None:
        """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")
        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

    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

    def test_put_whole_addressbook(self) -> None:
        """Create and overwrite a whole addressbook."""
        contacts = get_file_content("contact_multiple.vcf")
        self.put("/contacts.vcf/", contacts)
        _, answer = self.get("/contacts.vcf/")
        assert answer is not None
        assert "\r\nUID:contact1\r\n" in answer
        assert "\r\nUID:contact2\r\n" in answer

    def test_put_whole_addressbook_without_uids(self) -> None:
        """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")
        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

    def test_add_event_tz_dtend_only(self) -> None:
        """Add an event having TZ only on DTEND."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_issue1847_1.ics")
        path = "/calendar.ics/event_issue1847_1.ics"
        self.put(path, event)
        _, headers, answer = self.request("GET", path, check=200)

    def test_add_event_tz_dtstart_only(self) -> None:
        """Add an event having TZ only on DTSTART."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_issue1847_2.ics")
        path = "/calendar.ics/event_issue1847_2.ics"
        self.put(path, event)
        _, headers, answer = self.request("GET", path, check=200)

    def test_verify(self) -> None:
        """Verify the storage."""
        contacts = get_file_content("contact_multiple.vcf")
        self.put("/contacts.vcf/", contacts)
        events = get_file_content("event_multiple.ics")
        self.put("/calendar.ics/", events)
        s = storage.load(self.configuration)
        assert s.verify()

    def test_delete(self) -> None:
        """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

    def test_mkcalendar(self) -> None:
        """Make a calendar."""
        self.mkcalendar("/calendar.ics/")
        _, answer = self.get("/calendar.ics/")
        assert "BEGIN:VCALENDAR" in answer
        assert "END:VCALENDAR" in answer

    def test_mkcalendar_overwrite(self) -> None:
        """Try to overwrite an existing calendar."""
        self.mkcalendar("/calendar.ics/")
        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

    def test_mkcalendar_intermediate(self) -> None:
        """Try make a calendar in a unmapped collection."""
        self.mkcalendar("/unmapped/calendar.ics/", check=409)

    def test_mkcol(self) -> None:
        """Make a collection."""
        self.mkcol("/user/")

    def test_mkcol_overwrite(self) -> None:
        """Try to overwrite an existing collection."""
        self.mkcol("/user/")
        self.mkcol("/user/", check=405)

    def test_mkcol_intermediate(self) -> None:
        """Try make a collection in a unmapped collection."""
        self.mkcol("/unmapped/user/", check=409)

    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/")
        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)
        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"

    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)
        self.request("MOVE", path1, check=201,
                     HTTP_DESTINATION="http://127.0.0.1/"+path2)
        self.get(path1, check=404)
        self.get(path2)

    def test_move_unicode(self) -> None:
        """Move a item."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        path1 = "/calendar.ics/event😀1.ics"
        path2 = "/calendar.ics/event😁2.ics"
        if not pathutils.path_supports_unicode(self.colpath):
            check_put = 400
            check_move = 400
            check_get1 = 400
            check_get2 = 400
        else:
            check_put = 201
            check_move = 201
            check_get1 = 200
            check_get2 = 404
        self.put(path1, event, check=check_put)
        self.get(path1, check=check_get1)
        self.get(path2, check=check_get2)
        self.request("MOVE", path1, check=check_move,
                     HTTP_DESTINATION="http://127.0.0.1/"+path2)
        self.get(path1, check=check_get2)
        self.get(path2, check=check_get1)

    def test_move_letter_unicode_dst_reject(self) -> None:
        """Move a item."""
        self.configure({"server": {"validate_path_value": "unicode-letter"}})
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        path1 = "/calendar.ics/event1.ics"
        path2 = "/calendar.ics/event😁2.ics"
        self.put(path1, event)
        self.request("MOVE", path1, check=400,
                     HTTP_DESTINATION="http://127.0.0.1/"+path2)
        self.get(path1, check=200)
        self.get(path2, check=400)

    def test_move_letter_unicode_dst_pass(self) -> None:
        """Move a item."""
        self.configure({"server": {"validate_path_value": "unicode-letter"}})
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        path1 = "/calendar.ics/event1.ics"
        path2 = "/calendar.ics/event2.ics"
        self.put(path1, event)
        self.request("MOVE", path1, check=201,
                     HTTP_DESTINATION="http://127.0.0.1/"+path2)
        self.get(path1, check=404)
        self.get(path2, check=200)

    def test_move_strict_unicode_dst(self) -> None:
        """Move a item."""
        self.configure({"server": {"validate_path_value": "strict"}})
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        path1 = "/calendar.ics/event1.ics"
        path2 = "/calendar.ics/event😁2.ics"
        self.put(path1, event)
        self.request("MOVE", path1, check=400,
                     HTTP_DESTINATION="http://127.0.0.1/"+path2)
        self.get(path1, check=200)
        self.get(path2, check=400)

    def test_move_strict_unicode_src(self) -> None:
        """Move a item."""
        self.configure({"server": {"validate_path_value": "strict"}})
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        path1 = "/calendar.ics/event😀1.ics"
        path2 = "/calendar.ics/event2.ics"
        self.put(path1, event, check=400)
        self.request("MOVE", path1, check=400,
                     HTTP_DESTINATION="http://127.0.0.1/"+path2)
        self.get(path1, check=400)
        self.get(path2, check=404)

    def test_move_between_collections(self) -> None:
        """Move a item."""
        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.request("MOVE", path1, check=201,
                     HTTP_DESTINATION="http://127.0.0.1/"+path2)
        self.get(path1, check=404)
        self.get(path2)

    def test_move_between_collections_with_at_native(self) -> None:
        """Move a item."""
        self.mkcalendar("/calendar1@domain.ics/")
        self.mkcalendar("/calendar2@domain.ics/")
        event = get_file_content("event1.ics")
        path1 = "/calendar1@domain.ics/event1.ics"
        path2 = "/calendar2@domain.ics/event2.ics"
        self.put(path1, event)
        self.request("MOVE", path1, check=201,
                     HTTP_DESTINATION="http://127.0.0.1/"+path2)
        self.get(path1, check=404)
        self.get(path2)

    def test_move_between_collections_with_at_encoded(self) -> None:
        """Move a item."""
        self.mkcalendar("/calendar1@domain.ics/")
        self.mkcalendar("/calendar2@domain.ics/")
        event = get_file_content("event1.ics")
        path1 = "/calendar1@domain.ics/event1.ics"
        path2 = "/calendar2@domain.ics/event2.ics"
        path2_encoded = urllib.parse.quote(path2)
        self.put(path1, event)
        self.request("MOVE", path1, check=201,
                     HTTP_DESTINATION="http://127.0.0.1/"+path2_encoded)
        self.get(path1, check=404)
        self.get(path2)

    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(
            "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

    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)
        self.request("MOVE", path1, check=412,
                     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)

    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)
        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

    def test_head(self) -> None:
        _, headers, answer = self.request("HEAD", "/", check=302)
        assert int(headers.get("Content-Length", "0")) > 0 and not answer

    def test_options(self) -> None:
        _, headers, _ = self.request("OPTIONS", "/", check=200)
        assert "DAV" in headers

    def test_server_error_delay_basic(self) -> None:
        """Trigger a server error and check delay."""
        delay = .3
        delay_min = delay * 0.9  # no random jitter during test
        delay_max = delay + 0.2  # no random jitter during test
        if sys.platform == "darwin" or sys.platform == "win32":  # no reliable sleep times
            delay_max = delay_max * 1.5

        self.configure({"server": {"delay_on_error": delay}})
        os.environ["PYTEST_RADICALE_RAISE_GENERIC_ERROR"] = "1"
        time_begin = datetime.datetime.now()
        _, headers, answer = self.request("OPTIONS", "/", check=500)
        time_end = datetime.datetime.now()
        time_delta = (time_end - time_begin).total_seconds()
        assert time_delta > delay_min
        assert time_delta < delay_max
        del os.environ["PYTEST_RADICALE_RAISE_GENERIC_ERROR"]

    def test_delete_collection(self) -> None:
        """Delete a collection."""
        self.mkcalendar("/calendar.ics/")
        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)

    def test_delete_collection_global_forbid_base(self) -> None:
        """Delete a collection (expect forbidden)."""
        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)

    def test_delete_collection_global_forbid_delay(self) -> None:
        """Delete a collection (expect forbidden, check delay)."""
        delay = .3
        delay_min = delay * 0.9  # no random jitter during test
        delay_max = delay + 0.2  # no random jitter during test
        if sys.platform == "darwin" or sys.platform == "win32":  # no reliable sleep times
            delay_max = delay_max * 1.5

        self.configure({"rights": {"permit_delete_collection": False}, "auth": {"delay": delay}})
        self.mkcalendar("/calendar.ics/")
        time_begin = datetime.datetime.now()
        _, responses = self.delete("/calendar.ics/", check=401)
        time_end = datetime.datetime.now()
        time_delta = (time_end - time_begin).total_seconds()
        assert time_delta > delay_min
        assert time_delta < delay_max

    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)

    def test_delete_root_collection(self) -> None:
        """Delete the root collection."""
        self.mkcalendar("/calendar.ics/")
        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)

    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)

    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)
        _, responses = self.propfind("/", HTTP_DEPTH="1")
        assert len(responses) == 2
        assert "/" in responses and calendar_path in responses
        _, responses = self.propfind(calendar_path, HTTP_DEPTH="1")
        assert len(responses) == 2
        assert calendar_path in responses and event_path in responses

    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)
        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)
        response = responses["/calendar.ics/event.ics"]
        assert not isinstance(response, int)
        status, prop = response["D:getetag"]
        assert status == 200 and not prop.text

    def test_propfind_vcalendar_allprop(self) -> None:
        self.configure({"logging": {"limit_content": 5000}})
        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)
        response = responses["/calendar.ics/"]
        assert not isinstance(response, int)
        status, prop = response["D:sync-token"]
        assert status == 200 and prop.text
        assert "C:max-resource-size" not in response
        assert "C:supported-calendar-component-set" in response
        assert "CR:supported-address-data" not in response
        assert "D:resourcetype" in response
        status, resourcetype = response["D:resourcetype"]
        resourcetypes = resourcetype.find(xmlutils.make_clark("C:calendar"))
        assert resourcetypes is not None
        assert "{urn:ietf:params:xml:ns:caldav}calendar" in resourcetypes.tag
        assert "{urn:ietf:params:xml:ns:carddav}addressbook" not in resourcetypes.tag
        status, sup_cal_comp_set = response["C:supported-calendar-component-set"]
        sup_cal_comp_sets = sup_cal_comp_set.findall(xmlutils.make_clark("C:comp"))
        comp_attr = []
        for comp in sup_cal_comp_sets:
            comp_attr.append(comp.attrib)
            logging.debug("comp: %r", comp.attrib)
        assert {'name': 'VEVENT'} in comp_attr
        assert {'name': 'VTODO'} in comp_attr
        assert {'name': 'VJOURNAL'} in comp_attr
        _, responses = self.propfind("/calendar.ics/event.ics", propfind)
        response = responses["/calendar.ics/event.ics"]
        assert not isinstance(response, int)
        status, prop = response["D:getetag"]
        assert status == 200 and prop.text
        assert "C:max-resource-size" not in response
        _, responses = self.propfind("/calendar.ics/event-not-exists.ics", propfind, check=404)

    def test_propfind_vaddressbook_allprop(self) -> None:
        self.configure({"logging": {"limit_content": 5000}})
        self.create_addressbook("/addressbook.vcf/")
        contact = get_file_content("contact1.vcf")
        self.put("/addressbook.vcf/contact1.vcf", contact)
        propfind = get_file_content("allprop.xml")
        _, responses = self.propfind("/addressbook.vcf/", propfind)
        response = responses["/addressbook.vcf/"]
        assert not isinstance(response, int)
        status, prop = response["D:sync-token"]
        assert status == 200 and prop.text
        assert "C:max-resource-size" not in response
        assert "C:supported-calendar-component-set" not in response
        assert "CR:supported-address-data" in response
        assert "D:resourcetype" in response
        status, resourcetype = response["D:resourcetype"]
        resourcetypes = resourcetype.find(xmlutils.make_clark("CR:addressbook"))
        assert resourcetypes is not None
        assert "{urn:ietf:params:xml:ns:carddav}addressbook" in resourcetypes.tag
        assert "{urn:ietf:params:xml:ns:caldav}calendar" not in resourcetypes.tag
        _, responses = self.propfind("/addressbook.vcf/contact1.vcf", propfind)
        response = responses["/addressbook.vcf/contact1.vcf"]
        assert not isinstance(response, int)
        status, prop = response["D:getetag"]
        assert status == 200 and prop.text
        assert "C:max-resource-size" not in response

    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)
        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

    def test_propfind_max_resource_size(self) -> None:
        """Read property C:max-resource-size"""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        self.put("/calendar.ics/event.ics", event)
        _, responses = self.propfind("/calendar.ics/", """\
<?xml version="1.0"?>
 <propfind xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
   <prop>
     <C:max-resource-size />
   </prop>
 </propfind>""")
        response = responses["/calendar.ics/"]
        assert not isinstance(response, int)
        status, prop = response["C:max-resource-size"]
        assert status == 200 and prop.text

    def test_propfind_getctag(self) -> None:
        """Read property CS:getctag"""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event1.ics")
        self.put("/calendar.ics/event.ics", event)
        _, responses = self.propfind("/calendar.ics/", """\
<?xml version="1.0"?>
<propfind xmlns="DAV:" xmlns:CS="http://calendarserver.org/ns/">
  <prop>
    <CS:getctag />
  </prop>
</propfind>""")
        response = responses["/calendar.ics/"]
        assert not isinstance(response, int)
        status, prop = response["CS:getctag"]
        assert status == 200 and prop.text

    def test_propfind_supported_address_data(self) -> None:
        """Read property CR:supported-address-data on addressbook"""
        self.create_addressbook("/addressbook.vcf/")
        contact = get_file_content("contact1.vcf")
        self.put("/addressbook.vcf/contact.vcf", contact)
        _, responses = self.propfind("/addressbook.vcf/", """\
<?xml version="1.0"?>
<propfind xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
  <prop>
    <CR:supported-address-data />
  </prop>
</propfind>""")
        response = responses["/addressbook.vcf/"]
        assert not isinstance(response, int)
        status, prop = response["CR:supported-address-data"]
        assert status == 200
        # Should have at least one address-data-type element
        address_data_types = prop.findall(
            xmlutils.make_clark("CR:address-data-type"))
        assert len(address_data_types) >= 1
        # Check that 3.0 is always supported
        versions = [e.get("version") for e in address_data_types]
        assert "3.0" in versions
        # Check content-type is text/vcard for all
        for e in address_data_types:
            assert e.get("content-type") == "text/vcard"
        # If vobject >= 1.0.0, should also support 4.0
        if utils.vobject_supports_vcard4():
            assert "4.0" in versions
            # vCard 4.0 should be listed first (preferred)
            assert versions[0] == "4.0"

    def test_propfind_supported_address_data_on_calendar(self) -> None:
        """Read property CR:supported-address-data on calendar (should 404)"""
        self.mkcalendar("/calendar.ics/")
        _, responses = self.propfind("/calendar.ics/", """\
<?xml version="1.0"?>
<propfind xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
  <prop>
    <CR:supported-address-data />
  </prop>
</propfind>""")
        response = responses["/calendar.ics/"]
        assert not isinstance(response, int)
        status, prop = response["CR:supported-address-data"]
        assert status == 404

    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)
        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)
        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)
        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)
        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)
        response = responses["/calendar.ics/"]
        assert not isinstance(response, int) and len(response) == 1
        status, prop = response["ICAL:calendar-color"]
        assert status == 404

    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)
        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
        status, prop = response["C:calendar-description"]
        assert status == 200 and not prop.text
        # Read properties back
        _, responses = self.propfind("/calendar.ics/", propfind)
        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"
        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)
        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
        status, prop = response["C:calendar-description"]
        assert status == 200 and not prop.text
        # Read properties back
        _, responses = self.propfind("/calendar.ics/", propfind)
        response = responses["/calendar.ics/"]
        assert not isinstance(response, int) and len(response) == 2
        status, prop = response["ICAL:calendar-color"]
        assert status == 404
        status, prop = response["C:calendar-description"]
        assert status == 404

    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)
        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
        status, prop = response["C:calendar-description"]
        assert status == 200 and not prop.text
        # Read properties back
        _, responses = self.propfind("/calendar.ics/", propfind)
        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"
        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)
        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
        status, prop = response["C:calendar-description"]
        assert status == 200 and not prop.text
        # Read properties back
        _, responses = self.propfind("/calendar.ics/", propfind)
        response = responses["/calendar.ics/"]
        assert not isinstance(response, int) and len(response) == 2
        status, prop = response["ICAL:calendar-color"]
        assert status == 404
        status, prop = response["C:calendar-description"]
        assert status == 404

    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)
        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
        status, prop = response["C:calendar-description"]
        assert status == 200 and not prop.text
        # Read properties back
        _, responses = self.propfind("/calendar.ics/", propfind)
        response = responses["/calendar.ics/"]
        assert not isinstance(response, int) and len(response) == 2
        status, prop = response["ICAL:calendar-color"]
        assert status == 404
        status, prop = response["C:calendar-description"]
        assert status == 200 and prop.text == "test2"

    def test_put_whole_calendar_multiple_events_with_same_uid(self) -> None:
        """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
        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/")
        assert answer.count("BEGIN:VEVENT") == 2

    def _test_filter(self, filters: Iterable[str], kind: str = "event",
                     test: Optional[str] = None, items: Iterable[int] = (1,)
                     ) -> List[str]:
        filter_template = "<C:filter>%s</C:filter>"
        create_collection_fn: Callable[[str], Any]
        if kind in ("event", "journal", "todo", "valarm"):
            create_collection_fn = self.mkcalendar
            path = "/calendar.ics/"
            filename_template = "%s%d.ics"
            namespace = "urn:ietf:params:xml:ns:caldav"
            report = "calendar-query"
        elif kind == "contact":
            create_collection_fn = self.create_addressbook
            if test:
                filter_template = '<C:filter test="%s">%%s</C:filter>' % test
            path = "/contacts.vcf/"
            filename_template = "%s%d.vcf"
            namespace = "urn:ietf:params:xml:ns:carddav"
            report = "addressbook-query"
        else:
            raise ValueError("Unsupported kind: %r" % kind)
        status, _, = self.delete(path, check=None)
        assert status in (200, 404)
        create_collection_fn(path)
        logging.warning("Upload items %r", items)
        for i in items:
            logging.warning("Upload %d", i)
            filename = filename_template % (kind, i)
            event = get_file_content(filename)
            self.put(posixpath.join(path, filename), event)
        logging.warning("Upload items finished")
        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))
        assert responses is not None
        paths = []
        for path, props in responses.items():
            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

    def test_addressbook_empty_filter(self) -> None:
        self._test_filter([""], kind="contact")

    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")

    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")

    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")

    def test_addressbook_prop_filter_structured(self) -> None:
        """text-match on a structured property (N) that vobject parses into a
        non-string value must not crash the REPORT."""
        assert "/contacts.vcf/contact1.vcf" in self._test_filter(["""\
<C:prop-filter name="N">
    <C:text-match collation="i;unicode-casemap">contact</C:text-match>
</C:prop-filter>"""], "contact")
        assert "/contacts.vcf/contact1.vcf" not in self._test_filter(["""\
<C:prop-filter name="N">
    <C:text-match collation="i;unicode-casemap">nonexistent</C:text-match>
</C:prop-filter>"""], "contact")

    def test_calendar_empty_filter(self) -> None:
        self._test_filter([""])

    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>"""])

    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>"""])

    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>"""])

    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>"""])

    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>"""])

    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>"""])

    def test_text_match_filter(self) -> None:
        """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>"""])

    def test_param_filter(self) -> None:
        """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"
                    >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"
                    >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>"""])

    def test_time_range_filter_events(self) -> None:
        """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

    def test_time_range_filter_events_whole_day_duration(self) -> None:
        """Report time-range filter on an event with a whole-day DURATION.

        event11 starts 2013-09-01T00:00:00Z and lasts DURATION:P2D, i.e. it is
        ongoing until 2013-09-03T00:00:00Z. A time-range that falls inside the
        span (but after DTSTART) must match it, exactly as it would for an
        equivalent event expressed with DTEND.
        """
        # Time-range fully inside the 2-day event, after DTSTART.
        answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
    <C:comp-filter name="VEVENT">
        <C:time-range start="20130902T000000Z" end="20130902T120000Z"/>
    </C:comp-filter>
</C:comp-filter>"""], items=(11,))
        assert "/calendar.ics/event11.ics" in answer
        # Time-range fully after the event must not match.
        answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
    <C:comp-filter name="VEVENT">
        <C:time-range start="20130904T000000Z" end="20130905T000000Z"/>
    </C:comp-filter>
</C:comp-filter>"""], items=(11,))
        assert "/calendar.ics/event11.ics" not in answer

    def test_time_range_filter_without_comp_filter(self) -> None:
        """Report request with time-range filter without comp-filter on events."""
        answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
        <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
</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:time-range start="20130902T000000Z" end="20131001T000000Z"/>
</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:time-range start="20130903T000000Z" end="20130908T000000Z"/>
</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:time-range start="20130903T000000Z" end="20130904T000000Z"/>
</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:time-range start="20130805T000000Z" end="20130810T000000Z"/>
</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:time-range start="20170601T063000Z" end="20170601T070000Z"/>
</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:time-range start="20170701T060000Z"/>
</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:time-range start="20170702T070000Z" end="20170704T060000Z"/>
</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:time-range start="20170602T075959Z" end="20170602T080000Z"/>
</C:comp-filter>"""], items=(9,))
        assert "/calendar.ics/event9.ics" in answer
        answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
        <C:time-range start="20170602T080000Z" end="20170603T083000Z"/>
</C:comp-filter>"""], items=(9,))
        assert "/calendar.ics/event9.ics" not in answer

    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

    def test_time_range_filter_todos(self) -> None:
        """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

    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
        assert "/calendar.ics/valarm2.ics" in answer  # absolute date
        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])
        assert "/calendar.ics/valarm1.ics" in answer  # -15 min offset
        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

    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

    def test_time_range_filter_journals(self) -> None:
        """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

    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

    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
        response = responses[event_path]
        assert isinstance(response, dict)
        status, prop = response["D:getetag"]
        assert status == 200 and prop.text

    def test_report_free_busy(self) -> None:
        """Test free busy report on a few items"""
        calendar_path = "/calendar.ics/"
        self.mkcalendar(calendar_path)
        for i in (1, 2, 10):
            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"/>
</C:free-busy-query>""", 200, is_xml=False)
        for response in responses.values():
            assert isinstance(response, vobject.base.Component)
        assert len(responses) == 1
        vcalendar = list(responses.values())[0]
        assert isinstance(vcalendar, vobject.base.Component)
        assert len(vcalendar.vfreebusy_list) == 3
        types = {}
        for vfb in vcalendar.vfreebusy_list:
            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}

        # 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)

        # Test max_freebusy_occurrence set to 0 (limit disabled)
        self.configure({"reporting": {"max_freebusy_occurrence": 0}})
        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>""", 200, is_xml=False)
        assert len(responses) == 1
        vcalendar = list(responses.values())[0]
        assert isinstance(vcalendar, vobject.base.Component)
        assert len(vcalendar.vfreebusy_list) == 3

    def _report_sync_token(
            self, calendar_path: str, sync_token: Optional[str] = None, **kwargs
            ) -> Tuple[str, RESPONSES]:
        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, **kwargs)
        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
            return "", {}
        assert status == 207
        assert xml.tag == xmlutils.make_clark("D:multistatus")
        sync_token = xml.find(xmlutils.make_clark("D:sync-token")).text.strip()
        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

    def test_report_sync_collection_no_change(self) -> None:
        """Test sync-collection report without modifying the collection"""
        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)
        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)
        if not self.full_sync_token_support and not new_sync_token:
            return
        assert sync_token == new_sync_token and len(responses) == 0

    def test_report_sync_collection_add(self) -> None:
        """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
        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)
        if not self.full_sync_token_support and not sync_token:
            return
        assert len(responses) == 1 and responses[event_path] == 200

    def test_report_sync_collection_delete(self) -> None:
        """Test sync-collection report with a deleted 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)
        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)
        if not self.full_sync_token_support and not sync_token:
            return
        assert len(responses) == 1 and responses[event_path] == 404

    def test_report_sync_collection_create_delete(self) -> None:
        """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
        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)
        if not self.full_sync_token_support and not sync_token:
            return
        assert len(responses) == 1 and responses[event_path] == 404

    def test_report_sync_collection_modify_undo(self) -> None:
        """Test sync-collection report with a modified and changed back item"""
        calendar_path = "/calendar.ics/"
        self.mkcalendar(calendar_path)
        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)
        if not self.full_sync_token_support and not sync_token:
            return
        assert len(responses) == 1 and responses[event_path] == 200

    def test_report_sync_collection_move(self) -> None:
        """Test sync-collection report a moved item"""
        calendar_path = "/calendar.ics/"
        self.mkcalendar(calendar_path)
        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
        self.request("MOVE", event1_path, check=201,
                     HTTP_DESTINATION="http://127.0.0.1/"+event2_path)
        sync_token, responses = self._report_sync_token(
            calendar_path, sync_token)
        if not self.full_sync_token_support and not sync_token:
            return
        assert len(responses) == 2 and (responses[event1_path] == 404 and
                                        responses[event2_path] == 200)

    def test_report_sync_collection_move_undo(self) -> None:
        """Test sync-collection report with a moved and moved back item"""
        calendar_path = "/calendar.ics/"
        self.mkcalendar(calendar_path)
        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
        self.request("MOVE", event1_path, check=201,
                     HTTP_DESTINATION="http://127.0.0.1/"+event2_path)
        self.request("MOVE", event2_path, check=201,
                     HTTP_DESTINATION="http://127.0.0.1/"+event1_path)
        sync_token, responses = self._report_sync_token(
            calendar_path, sync_token)
        if not self.full_sync_token_support and not sync_token:
            return
        assert len(responses) == 2 and (responses[event1_path] == 200 and
                                        responses[event2_path] == 404)

    def test_report_sync_collection_invalid_sync_token(self) -> None:
        """Test sync-collection report with an invalid sync token"""
        calendar_path = "/calendar.ics/"
        self.mkcalendar(calendar_path)
        sync_token, _ = self._report_sync_token(
            calendar_path, "http://radicale.org/ns/sync/INVALID")
        assert not sync_token

    def test_report_sync_collection_invalid_sync_token_with_user(self) -> None:
        """Test sync-collection report with an invalid sync token and user+host+useragent"""
        self.configure({"auth": {"type": "none"}})
        calendar_path = "/calendar.ics/"
        self.mkcalendar(calendar_path)
        sync_token, _ = self._report_sync_token(
            calendar_path, "http://radicale.org/ns/sync/INVALID", login="testuser:", remote_host="192.0.2.1", remote_useragent="Testclient/1.0")
        assert not sync_token

    def test_propfind_sync_token(self) -> None:
        """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)
        response = responses[calendar_path]
        assert not isinstance(response, int)
        status, sync_token = response["D:sync-token"]
        assert status == 200 and sync_token.text
        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)
        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

    def test_propfind_same_as_sync_collection_sync_token(self) -> None:
        """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)
        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

    def test_calendar_getcontenttype(self) -> None:
        """Test report request on an item"""
        self.mkcalendar("/test/")
        for component in ("event", "todo", "journal"):
            event = get_file_content("%s1.ics" % component)
            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>""")
            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())

    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>""")
        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"

    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>
</propfind>""", login="user:")
        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
        element = prop.find(xmlutils.make_clark("D:href"))
        assert element is not None and element.text == "/user/"

    def test_version_exposure_authenticated(self) -> None:
        """Test if the version is returned for authenticated users."""
        self.configure({"auth": {"type": "none"}})
        status, responses = self.propfind("/", """\
<?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:" xmlns:R="http://radicale.org/ns/">
    <prop>
        <R:version />
    </prop>
</propfind>""", login="user:")
        assert status == 207
        response = responses["/"]
        assert not isinstance(response, int)
        status_code, prop = response["RADICALE:version"]
        assert status_code == 200
        assert prop.text == utils.package_version("radicale")

    def test_version_exposure_unauthenticated(self) -> None:
        """Test if the version is hidden for unauthenticated users."""
        self.configure({"auth": {"type": "none"}})
        # Without login, 'user' will be empty
        status, responses = self.propfind("/", """\
<?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:" xmlns:R="http://radicale.org/ns/">
    <prop>
        <R:version />
    </prop>
</propfind>""")
        assert status == 207
        response = responses["/"]
        assert not isinstance(response, int)
        status_code, _ = response["RADICALE:version"]
        assert status_code == 404

    def test_authentication(self) -> None:
        """Test if server sends authentication request."""
        self.configure({"auth": {"type": "htpasswd",
                                 "htpasswd_filename": os.devnull,
                                 "htpasswd_encryption": "plain"},
                        "rights": {"type": "owner_only"}})
        status, headers, _ = self.request("MKCOL", "/user/")
        assert status in (401, 403)
        assert headers.get("WWW-Authenticate")

    def test_principal_collection_creation(self) -> None:
        """Verify existence of the principal collection."""
        self.configure({"auth": {"type": "none"}})
        self.propfind("/user/", login="user:")

    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")

    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)

    def test_custom_headers(self) -> None:
        self.configure({"headers": {"test": "123"}})
        # Test if header is set on success
        _, 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"

    def test_timezone_seconds(self) -> None:
        """Verify that timezones with minutes and seconds work."""
        self.mkcalendar("/calendar.ics/")
        event = get_file_content("event_timezone_seconds.ics")
        self.put("/calendar.ics/event.ics", event)

    def test_logging_conditional_basic(self, caplog) -> None:
        caplog.set_level(logging.INFO)
        self.configure({"logging": {"request_header_on_debug": "False",
                                    "request_content_on_debug": "False",
                                    "response_header_on_debug": "False",
                                    "response_content_on_debug": "False",
                                    "request_header_on_notice_condition": '{"method": {"match": "equal", "value": "GET"}, "login": {"match": "equal", "value": "owner"}}',
                                    "request_content_on_notice_condition": '{"method": {"match": "equal", "value": "PUT"}, "useragent": {"match": "startswith", "value": "caldavsync"}}',
                                    "response_header_on_notice_condition": '{"method": {"match": "equal", "value": "PUT"}, "status": {"match": ">=", "value": "201"}, "host": {"match": "included", "value": "127.0.0.0/8"}}',
                                    "response_content_on_notice_condition": '{"method": {"match": "equal", "value": "GET"}, "path": {"match": "endswith", "value": ".ics"}}',
                                    }})
        self.mkcalendar("/test/")
        event = get_file_content("event1.ics")
        path = "/test/event1.ics"
        path2 = "/test/event2.ics"

        logging.info("\n*** check log condition: Response header (found)")
        caplog.clear()
        self.put(path, event, remote_host='127.0.0.1')
        assert "Response header (log condition passed)" in "\n".join(caplog.messages)

        logging.info("\n*** check log condition: Response header (not found)")
        caplog.clear()
        self.put(path, event, remote_host='192.0.2.1', check=204)
        assert "Response header (log condition passed)" not in "\n".join(caplog.messages)

        logging.info("\n*** check log condition: Response content (found)")
        caplog.clear()
        self.get(path)
        assert "Response content (nonXML, log condition passed)" in "\n".join(caplog.messages)

        logging.info("\n*** check log condition: Response content (not found)")
        caplog.clear()
        self.get(path2, check=404)
        assert "Response content (log condition passed)" not in "\n".join(caplog.messages)

        logging.info("\n*** check log condition: Request header (found)")
        caplog.clear()
        self.get(path2, check=401, login="owner:ownerpw")
        assert "Request header (log condition passed)" in "\n".join(caplog.messages)

        logging.info("\n*** check log condition: Request content (found)")
        caplog.clear()
        self.put(path, event, remote_useragent='caldavsync', check=204)
        assert "Request content (log condition passed)" in "\n".join(caplog.messages)

    def test_logging_conditional_ip_included(self, caplog) -> None:
        self.configure({"logging": {"request_header_on_debug": "False",
                                    "request_content_on_debug": "False",
                                    "response_header_on_debug": "False",
                                    "response_content_on_debug": "False",
                                    "response_header_on_notice_condition": '{"host": {"match": "included", "value": "127.0.0.0/8"}}',
                                    }})

        self.mkcalendar("/test/")
        event = get_file_content("event1.ics")
        path = "/test/event1.ics"

        logging.info("\n*** check log condition: Response header (not found)")
        caplog.clear()
        self.put(path, event, remote_host='192.0.2.1')
        assert "Response header (log condition passed)" not in "\n".join(caplog.messages)

        logging.info("\n*** check log condition: Response header (found)")
        caplog.clear()
        self.put(path, event, remote_host='127.0.0.1', check=204)
        assert "Response header (log condition passed)" in "\n".join(caplog.messages)

        logging.info("\n*** check log condition: Response header (not found, no remote_host)")
        caplog.clear()
        self.put(path, event, check=204)
        assert "Response header (log condition passed)" not in "\n".join(caplog.messages)

        logging.info("\n*** check log condition: Response header (IPv6, not found)")
        caplog.clear()
        self.put(path, event, check=204, remote_host='2001:db8::1')
        assert "Response header (log condition passed)" not in "\n".join(caplog.messages)

    def test_logging_conditional_report(self, caplog) -> None:
        caplog.set_level(logging.INFO)
        self.configure({"logging": {"request_header_on_debug": "False",
                                    "request_content_on_debug": "False",
                                    "response_header_on_debug": "False",
                                    "response_content_on_debug": "False",
                                    "response_content_on_notice_condition": '{"status": {"match": "equal", "value": "207"}, "method": {"match": "equal", "value": "REPORT"}}',
                                    }})
        self.mkcalendar("/test/")

        logging.info("\n*** check log condition: Response content (found)")
        caplog.clear()
        _, 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:getetag/>
    </D:prop>
</C:calendar-query>""")
        assert "Response content (XML, log condition passed)" in "\n".join(caplog.messages)

    def test_logging_conditional_propfind(self, caplog) -> None:
        caplog.set_level(logging.INFO)
        self.configure({"logging": {"request_header_on_debug": "False",
                                    "request_content_on_debug": "False",
                                    "response_header_on_debug": "False",
                                    "response_content_on_debug": "False",
                                    "response_content_on_notice_condition": '{"status": {"match": "equal", "value": "207"}, "method": {"match": "equal", "value": "PROPFIND"}}',
                                    }})
        self.mkcalendar("/test/")

        logging.info("\n*** check log condition: Response content (found)")
        caplog.clear()
        _, responses = self.propfind("/test/")
        assert "Response content (XML, log condition passed)" in "\n".join(caplog.messages)

    def test_logging_conditional_proppatch(self, caplog) -> None:
        caplog.set_level(logging.INFO)
        self.configure({"logging": {"request_header_on_debug": "False",
                                    "request_content_on_debug": "False",
                                    "response_header_on_debug": "False",
                                    "response_content_on_debug": "False",
                                    "response_content_on_notice_condition": '{"status": {"match": "equal", "value": "207"}, "method": {"match": "equal", "value": "PROPPATCH"}}',
                                    }})
        self.mkcalendar("/test/")
        proppatch = get_file_content("proppatch_set_calendar_color.xml")

        logging.info("\n*** check log condition: Response content (found)")
        caplog.clear()
        _, responses = self.proppatch("/test/", proppatch)
        assert "Response content (XML, log condition passed)" in "\n".join(caplog.messages)

    def test_logging_conditional_mkcalendar(self, caplog) -> None:
        caplog.set_level(logging.INFO)
        self.configure({"logging": {"request_header_on_debug": "False",
                                    "request_content_on_debug": "False",
                                    "response_header_on_debug": "False",
                                    "response_content_on_debug": "False",
                                    "request_header_on_notice_condition": '{"method": {"match": "equal", "value": "MKCALENDAR"}}',
                                    }})

        logging.info("\n*** check log condition: Request content (found)")
        caplog.clear()
        self.mkcalendar("/test/")
        assert "Request header (log condition passed)" in "\n".join(caplog.messages)

    def test_logging_conditional_mkcol(self, caplog) -> None:
        caplog.set_level(logging.INFO)
        self.configure({"logging": {"request_header_on_debug": "False",
                                    "request_content_on_debug": "False",
                                    "response_header_on_debug": "False",
                                    "response_content_on_debug": "True",
                                    "request_content_on_notice_condition": '{"method": {"match": "equal", "value": "MKCOL"}}',
                                    }})

        logging.info("\n*** check log condition: Request content (found)")
        caplog.clear()
        self.create_addressbook("/test/")

    def test_logging_conditional_regex_basic(self, caplog) -> None:
        caplog.set_level(logging.INFO)
        self.configure({"logging": {"request_header_on_debug": "False",
                                    "request_content_on_debug": "False",
                                    "response_header_on_debug": "False",
                                    "response_content_on_debug": "False",
                                    "response_header_on_notice_condition": '{"method": {"match": "equal", "value": "PUT"}, "path": {"match": "re", "value": "ev?nt[01]"}}',
                                    }})
        self.mkcalendar("/test/")
        event = get_file_content("event1.ics")
        event2 = get_file_content("event2.ics")
        path = "/test/event1.ics"
        path2 = "/test/event2.ics"

        logging.info("\n*** check log condition: Response header (found/re)")
        caplog.clear()
        self.put(path, event, remote_host='127.0.0.1')
        assert "Response header (log condition passed)" in "\n".join(caplog.messages)

        logging.info("\n*** check log condition: Response header (found/re)")
        caplog.clear()
        self.put(path2, event2, remote_host='127.0.0.1')
        assert "Response header (log condition passed)" not in "\n".join(caplog.messages)

    def test_logging_conditional_problems(self, caplog) -> None:
        logging.info("\n*** check log condition config: re ok")
        self.configure({"logging": {"request_header_on_debug": "False",
                                    "request_content_on_debug": "False",
                                    "response_header_on_debug": "False",
                                    "response_content_on_debug": "False",
                                    "request_header_on_notice_condition": '{"path": {"match": "re", "value": "ev?.*nt[01]"}}',
                                    }})
        self.mkcalendar("/test1/")

        logging.info("\n*** check log condition config: re broken")
        try:
            self.configure({"logging": {"request_header_on_debug": "False",
                                        "request_content_on_debug": "False",
                                        "response_header_on_debug": "False",
                                        "response_content_on_debug": "False",
                                        "request_header_on_notice_condition": '{"path": {"match": "re", "value": "*.ics"}}',
                                        }})
        except RuntimeError:
            pass
        else:
            raise

        logging.info("\n*** check log condition config: status not supported <100")
        try:
            self.configure({"logging": {"request_header_on_debug": "False",
                                        "request_content_on_debug": "False",
                                        "response_header_on_debug": "False",
                                        "response_content_on_debug": "False",
                                        "response_header_on_notice_condition": '{"status": {"match": "<=", "value": "99"}}',
                                        }})
        except RuntimeError:
            pass
        else:
            raise

        logging.info("\n*** check log condition config: status not supported >599")
        try:
            self.configure({"logging": {"request_header_on_debug": "False",
                                        "request_content_on_debug": "False",
                                        "response_header_on_debug": "False",
                                        "response_content_on_debug": "False",
                                        "response_header_on_notice_condition": '{"status": {"match": "<=", "value": "600"}}',
                                        }})
        except RuntimeError:
            pass
        else:
            raise
