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

238 lines
9.5 KiB
Python
Raw Permalink Normal View History

2021-12-08 21:45:42 +01:00
# This file is part of Radicale - CalDAV and CardDAV server
2017-05-27 17:28:07 +02:00
# Copyright © 2012-2017 Guillaume Ayoub
2019-06-17 04:13:24 +02:00
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
2012-09-15 10:00:13 +02:00
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
Tests for Radicale.
"""
2020-04-22 19:20:36 +02:00
import base64
2018-08-21 18:43:44 +02:00
import logging
2021-07-26 20:56:47 +02:00
import shutil
2012-09-15 10:00:13 +02:00
import sys
2021-07-26 20:56:47 +02:00
import tempfile
2023-03-08 15:49:45 +01:00
import wsgiref.util
2021-07-26 20:56:47 +02:00
import xml.etree.ElementTree as ET
2013-09-05 15:13:31 +02:00
from io import BytesIO
2021-07-26 20:56:47 +02:00
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.parse import quote
2012-09-15 10:00:13 +02:00
import defusedxml.ElementTree as DefusedET
2024-08-14 11:15:30 -06:00
import vobject
2019-06-15 09:01:55 +02:00
import radicale
2021-12-10 20:54:04 +01:00
from radicale import app, config, types, xmlutils
2021-07-26 20:56:47 +02:00
2023-10-06 13:15:45 -06:00
RESPONSES = Dict[str, Union[int, Dict[str, Tuple[int, ET.Element]], vobject.base.Component]]
2018-09-09 07:28:36 +02:00
2018-08-21 18:43:44 +02:00
# Enable debug output
radicale.log.logger.setLevel(logging.DEBUG)
2012-09-15 10:00:13 +02:00
class BaseTest:
2012-09-15 10:00:13 +02:00
"""Base class for tests."""
2021-07-26 20:56:47 +02:00
colpath: str
configuration: config.Configuration
application: app.Application
def setup_method(self) -> None:
2021-07-26 20:56:47 +02:00
self.configuration = config.load()
self.colpath = tempfile.mkdtemp()
2021-12-10 20:54:04 +01:00
self.configure({
2021-07-26 20:56:47 +02:00
"storage": {"filesystem_folder": self.colpath,
# Disable syncing to disk for better performance
"_filesystem_fsync": "False"},
# Set incorrect authentication delay to a short duration
2021-12-10 20:54:04 +01:00
"auth": {"delay": "0.001"}})
def configure(self, config_: types.CONFIG) -> None:
self.configuration.update(config_, "test", privileged=True)
2021-07-26 20:56:47 +02:00
self.application = app.Application(self.configuration)
def teardown_method(self) -> None:
2021-07-26 20:56:47 +02:00
shutil.rmtree(self.colpath)
def request(self, method: str, path: str, data: Optional[str] = None,
2022-01-16 13:07:56 +01:00
check: Optional[int] = None, **kwargs
) -> Tuple[int, Dict[str, str], str]:
2012-09-15 10:00:13 +02:00
"""Send a request."""
2021-07-26 20:56:47 +02:00
login = kwargs.pop("login", None)
if login is not None and not isinstance(login, str):
raise TypeError("login argument must be %r, not %r" %
(str, type(login)))
environ: Dict[str, Any] = {k.upper(): v for k, v in kwargs.items()}
for k, v in environ.items():
if not isinstance(v, str):
raise TypeError("type of %r is %r, expected %r" %
(k, type(v), str))
encoding: str = self.configuration.get("encoding", "request")
2020-04-22 19:20:36 +02:00
if login:
2021-07-26 20:56:47 +02:00
environ["HTTP_AUTHORIZATION"] = "Basic " + base64.b64encode(
login.encode(encoding)).decode()
environ["REQUEST_METHOD"] = method.upper()
environ["PATH_INFO"] = path
2023-03-08 15:49:45 +01:00
if data is not None:
2021-07-26 20:56:47 +02:00
data_bytes = data.encode(encoding)
environ["wsgi.input"] = BytesIO(data_bytes)
environ["CONTENT_LENGTH"] = str(len(data_bytes))
environ["wsgi.errors"] = sys.stderr
2023-03-08 15:49:45 +01:00
wsgiref.util.setup_testing_defaults(environ)
2018-04-29 23:06:18 +02:00
status = headers = None
2021-07-26 20:56:47 +02:00
def start_response(status_: str, headers_: List[Tuple[str, str]]
) -> None:
2018-04-29 23:06:18 +02:00
nonlocal status, headers
2022-01-16 13:07:56 +01:00
status = int(status_.split()[0])
headers = dict(headers_)
2021-07-26 20:56:47 +02:00
answers = list(self.application(environ, start_response))
assert status is not None and headers is not None
2022-01-16 13:07:56 +01:00
assert check is None or status == check, "%d != %d" % (status, check)
2018-04-29 23:06:18 +02:00
2022-01-16 13:07:56 +01:00
return status, headers, answers[0].decode() if answers else ""
@staticmethod
2021-07-26 20:56:47 +02:00
def parse_responses(text: str) -> RESPONSES:
xml = DefusedET.fromstring(text)
assert xml.tag == xmlutils.make_clark("D:multistatus")
2023-10-06 13:15:45 -06:00
path_responses: RESPONSES = {}
for response in xml.findall(xmlutils.make_clark("D:response")):
href = response.find(xmlutils.make_clark("D:href"))
assert href.text not in path_responses
2024-07-24 11:22:49 +02:00
prop_responses: Dict[str, Tuple[int, ET.Element]] = {}
for propstat in response.findall(
xmlutils.make_clark("D:propstat")):
status = propstat.find(xmlutils.make_clark("D:status"))
assert status.text.startswith("HTTP/1.1 ")
status_code = int(status.text.split(" ")[1])
2020-09-26 22:08:21 +02:00
for element in propstat.findall(
"./%s/*" % xmlutils.make_clark("D:prop")):
human_tag = xmlutils.make_human_tag(element.tag)
2024-07-24 11:22:49 +02:00
assert human_tag not in prop_responses
prop_responses[human_tag] = (status_code, element)
status = response.find(xmlutils.make_clark("D:status"))
if status is not None:
2024-07-24 11:22:49 +02:00
assert not prop_responses
assert status.text.startswith("HTTP/1.1 ")
status_code = int(status.text.split(" ")[1])
path_responses[href.text] = status_code
else:
2024-07-24 11:22:49 +02:00
path_responses[href.text] = prop_responses
return path_responses
2023-10-06 13:15:45 -06:00
@staticmethod
def parse_free_busy(text: str) -> RESPONSES:
path_responses: RESPONSES = {}
path_responses[""] = vobject.readOne(text)
return path_responses
2022-01-16 13:07:56 +01:00
def get(self, path: str, check: Optional[int] = 200, **kwargs
2021-07-26 20:56:47 +02:00
) -> Tuple[int, str]:
assert "data" not in kwargs
2022-01-16 13:07:56 +01:00
status, _, answer = self.request("GET", path, check=check, **kwargs)
return status, answer
2023-03-05 16:52:07 +01:00
def post(self, path: str, data: Optional[str] = None,
check: Optional[int] = 200, **kwargs) -> Tuple[int, str]:
2022-01-16 13:07:56 +01:00
status, _, answer = self.request("POST", path, data, check=check,
**kwargs)
return status, answer
2022-01-16 13:07:56 +01:00
def put(self, path: str, data: str, check: Optional[int] = 201,
2021-07-26 20:56:47 +02:00
**kwargs) -> Tuple[int, str]:
2022-01-16 13:07:56 +01:00
status, _, answer = self.request("PUT", path, data, check=check,
**kwargs)
return status, answer
2021-07-26 20:56:47 +02:00
def propfind(self, path: str, data: Optional[str] = None,
2022-01-16 13:07:56 +01:00
check: Optional[int] = 207, **kwargs
2021-07-26 20:56:47 +02:00
) -> Tuple[int, RESPONSES]:
2022-01-16 13:07:56 +01:00
status, _, answer = self.request("PROPFIND", path, data, check=check,
**kwargs)
if status < 200 or 300 <= status:
2021-07-26 20:56:47 +02:00
return status, {}
assert answer is not None
responses = self.parse_responses(answer)
2021-07-26 20:56:47 +02:00
if kwargs.get("HTTP_DEPTH", "0") == "0":
assert len(responses) == 1 and quote(path) in responses
return status, responses
2021-07-26 20:56:47 +02:00
def proppatch(self, path: str, data: Optional[str] = None,
2022-01-16 13:07:56 +01:00
check: Optional[int] = 207, **kwargs
2021-07-26 20:56:47 +02:00
) -> Tuple[int, RESPONSES]:
2022-01-16 13:07:56 +01:00
status, _, answer = self.request("PROPPATCH", path, data, check=check,
**kwargs)
if status < 200 or 300 <= status:
2021-07-26 20:56:47 +02:00
return status, {}
assert answer is not None
responses = self.parse_responses(answer)
assert len(responses) == 1 and path in responses
return status, responses
2022-01-16 13:07:56 +01:00
def report(self, path: str, data: str, check: Optional[int] = 207,
2023-10-06 13:15:45 -06:00
is_xml: Optional[bool] = True,
2021-07-26 20:56:47 +02:00
**kwargs) -> Tuple[int, RESPONSES]:
2022-01-16 13:07:56 +01:00
status, _, answer = self.request("REPORT", path, data, check=check,
**kwargs)
if status < 200 or 300 <= status:
2021-07-26 20:56:47 +02:00
return status, {}
assert answer is not None
2023-10-06 13:15:45 -06:00
if is_xml:
parsed = self.parse_responses(answer)
else:
parsed = self.parse_free_busy(answer)
return status, parsed
2022-01-16 13:07:56 +01:00
def delete(self, path: str, check: Optional[int] = 200, **kwargs
2021-07-26 20:56:47 +02:00
) -> Tuple[int, RESPONSES]:
assert "data" not in kwargs
2022-01-16 13:07:56 +01:00
status, _, answer = self.request("DELETE", path, check=check, **kwargs)
if status < 200 or 300 <= status:
2021-07-26 20:56:47 +02:00
return status, {}
assert answer is not None
responses = self.parse_responses(answer)
assert len(responses) == 1 and path in responses
return status, responses
2021-07-26 20:56:47 +02:00
def mkcalendar(self, path: str, data: Optional[str] = None,
2022-01-16 13:07:56 +01:00
check: Optional[int] = 201, **kwargs
2021-07-26 20:56:47 +02:00
) -> Tuple[int, str]:
2022-01-16 13:07:56 +01:00
status, _, answer = self.request("MKCALENDAR", path, data, check=check,
**kwargs)
return status, answer
2021-07-26 20:56:47 +02:00
def mkcol(self, path: str, data: Optional[str] = None,
2022-01-16 13:07:56 +01:00
check: Optional[int] = 201, **kwargs) -> int:
status, _, _ = self.request("MKCOL", path, data, check=check, **kwargs)
return status
2022-01-16 13:07:56 +01:00
def create_addressbook(self, path: str, check: Optional[int] = 201,
2021-07-26 20:56:47 +02:00
**kwargs) -> int:
assert "data" not in kwargs
return self.mkcol(path, """\
<?xml version="1.0" encoding="UTF-8" ?>
<create xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
<resourcetype>
<collection />
<CR:addressbook />
</resourcetype>
</prop>
</set>
2021-07-26 20:56:47 +02:00
</create>""", check=check, **kwargs)