|
|
@@ -22,13 +22,19 @@ Tests for Radicale.
|
|
|
|
|
|
import base64
|
|
|
import logging
|
|
|
+import shutil
|
|
|
import sys
|
|
|
+import tempfile
|
|
|
+import xml.etree.ElementTree as ET
|
|
|
from io import BytesIO
|
|
|
+from typing import Any, Dict, List, Optional, Tuple, Union
|
|
|
|
|
|
import defusedxml.ElementTree as DefusedET
|
|
|
|
|
|
import radicale
|
|
|
-from radicale import xmlutils
|
|
|
+from radicale import app, config, xmlutils
|
|
|
+
|
|
|
+RESPONSES = Dict[str, Union[int, Dict[str, Tuple[int, ET.Element]]]]
|
|
|
|
|
|
# Enable debug output
|
|
|
radicale.log.logger.setLevel(logging.DEBUG)
|
|
|
@@ -37,40 +43,70 @@ radicale.log.logger.setLevel(logging.DEBUG)
|
|
|
class BaseTest:
|
|
|
"""Base class for tests."""
|
|
|
|
|
|
- def request(self, method, path, data=None, login=None, **args):
|
|
|
+ colpath: str
|
|
|
+ configuration: config.Configuration
|
|
|
+ application: app.Application
|
|
|
+
|
|
|
+ def setup(self) -> None:
|
|
|
+ self.configuration = config.load()
|
|
|
+ self.colpath = tempfile.mkdtemp()
|
|
|
+ self.configuration.update({
|
|
|
+ "storage": {"filesystem_folder": self.colpath,
|
|
|
+ # Disable syncing to disk for better performance
|
|
|
+ "_filesystem_fsync": "False"},
|
|
|
+ # Set incorrect authentication delay to a short duration
|
|
|
+ "auth": {"delay": "0.001"}}, "test", privileged=True)
|
|
|
+ self.application = app.Application(self.configuration)
|
|
|
+
|
|
|
+ def teardown(self) -> None:
|
|
|
+ shutil.rmtree(self.colpath)
|
|
|
+
|
|
|
+ def request(self, method: str, path: str, data: Optional[str] = None,
|
|
|
+ **kwargs) -> Tuple[int, Dict[str, str], str]:
|
|
|
"""Send a request."""
|
|
|
- for key in args:
|
|
|
- args[key.upper()] = args[key]
|
|
|
+ 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")
|
|
|
if login:
|
|
|
- args["HTTP_AUTHORIZATION"] = "Basic " + base64.b64encode(
|
|
|
- login.encode()).decode()
|
|
|
- args["REQUEST_METHOD"] = method.upper()
|
|
|
- args["PATH_INFO"] = path
|
|
|
+ environ["HTTP_AUTHORIZATION"] = "Basic " + base64.b64encode(
|
|
|
+ login.encode(encoding)).decode()
|
|
|
+ environ["REQUEST_METHOD"] = method.upper()
|
|
|
+ environ["PATH_INFO"] = path
|
|
|
if data:
|
|
|
- data = data.encode()
|
|
|
- args["wsgi.input"] = BytesIO(data)
|
|
|
- args["CONTENT_LENGTH"] = str(len(data))
|
|
|
- args["wsgi.errors"] = sys.stderr
|
|
|
+ data_bytes = data.encode(encoding)
|
|
|
+ environ["wsgi.input"] = BytesIO(data_bytes)
|
|
|
+ environ["CONTENT_LENGTH"] = str(len(data_bytes))
|
|
|
+ environ["wsgi.errors"] = sys.stderr
|
|
|
status = headers = None
|
|
|
|
|
|
- def start_response(status_, headers_):
|
|
|
+ def start_response(status_: str, headers_: List[Tuple[str, str]]
|
|
|
+ ) -> None:
|
|
|
nonlocal status, headers
|
|
|
status = status_
|
|
|
headers = headers_
|
|
|
- answer = self.application(args, start_response)
|
|
|
+ answers = list(self.application(environ, start_response))
|
|
|
+ assert status is not None and headers is not None
|
|
|
|
|
|
return (int(status.split()[0]), dict(headers),
|
|
|
- answer[0].decode() if answer else None)
|
|
|
+ answers[0].decode() if answers else "")
|
|
|
|
|
|
@staticmethod
|
|
|
- def parse_responses(text):
|
|
|
+ def parse_responses(text: str) -> RESPONSES:
|
|
|
xml = DefusedET.fromstring(text)
|
|
|
assert xml.tag == xmlutils.make_clark("D:multistatus")
|
|
|
- path_responses = {}
|
|
|
+ path_responses: Dict[str, Union[
|
|
|
+ int, Dict[str, Tuple[int, ET.Element]]]] = {}
|
|
|
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
|
|
|
- prop_respones = {}
|
|
|
+ prop_respones: Dict[str, Tuple[int, ET.Element]] = {}
|
|
|
for propstat in response.findall(
|
|
|
xmlutils.make_clark("D:propstat")):
|
|
|
status = propstat.find(xmlutils.make_clark("D:status"))
|
|
|
@@ -92,70 +128,90 @@ class BaseTest:
|
|
|
return path_responses
|
|
|
|
|
|
@staticmethod
|
|
|
- def _check_status(status, good_status, check=True):
|
|
|
- if check is True:
|
|
|
- assert status == good_status
|
|
|
- elif check is not False:
|
|
|
- assert status == check
|
|
|
+ def _check_status(status: int, good_status: int,
|
|
|
+ check: Union[bool, int] = True) -> bool:
|
|
|
+ if check is not False:
|
|
|
+ expected = good_status if check is True else check
|
|
|
+ assert status == expected, "%d != %d" % (status, expected)
|
|
|
return status == good_status
|
|
|
|
|
|
- def get(self, path, check=True, **args):
|
|
|
- status, _, answer = self.request("GET", path, **args)
|
|
|
+ def get(self, path: str, check: Union[bool, int] = True, **kwargs
|
|
|
+ ) -> Tuple[int, str]:
|
|
|
+ assert "data" not in kwargs
|
|
|
+ status, _, answer = self.request("GET", path, **kwargs)
|
|
|
self._check_status(status, 200, check)
|
|
|
return status, answer
|
|
|
|
|
|
- def post(self, path, data=None, check=True, **args):
|
|
|
- status, _, answer = self.request("POST", path, data, **args)
|
|
|
+ def post(self, path: str, data: str = None, check: Union[bool, int] = True,
|
|
|
+ **kwargs) -> Tuple[int, str]:
|
|
|
+ status, _, answer = self.request("POST", path, data, **kwargs)
|
|
|
self._check_status(status, 200, check)
|
|
|
return status, answer
|
|
|
|
|
|
- def put(self, path, data, check=True, **args):
|
|
|
- status, _, answer = self.request("PUT", path, data, **args)
|
|
|
+ def put(self, path: str, data: str, check: Union[bool, int] = True,
|
|
|
+ **kwargs) -> Tuple[int, str]:
|
|
|
+ status, _, answer = self.request("PUT", path, data, **kwargs)
|
|
|
self._check_status(status, 201, check)
|
|
|
return status, answer
|
|
|
|
|
|
- def propfind(self, path, data=None, check=True, **args):
|
|
|
- status, _, answer = self.request("PROPFIND", path, data, **args)
|
|
|
+ def propfind(self, path: str, data: Optional[str] = None,
|
|
|
+ check: Union[bool, int] = True, **kwargs
|
|
|
+ ) -> Tuple[int, RESPONSES]:
|
|
|
+ status, _, answer = self.request("PROPFIND", path, data, **kwargs)
|
|
|
if not self._check_status(status, 207, check):
|
|
|
- return status, None
|
|
|
+ return status, {}
|
|
|
+ assert answer is not None
|
|
|
responses = self.parse_responses(answer)
|
|
|
- if args.get("HTTP_DEPTH", "0") == "0":
|
|
|
+ if kwargs.get("HTTP_DEPTH", "0") == "0":
|
|
|
assert len(responses) == 1 and path in responses
|
|
|
return status, responses
|
|
|
|
|
|
- def proppatch(self, path, data=None, check=True, **args):
|
|
|
- status, _, answer = self.request("PROPPATCH", path, data, **args)
|
|
|
+ def proppatch(self, path: str, data: Optional[str] = None,
|
|
|
+ check: Union[bool, int] = True, **kwargs
|
|
|
+ ) -> Tuple[int, RESPONSES]:
|
|
|
+ status, _, answer = self.request("PROPPATCH", path, data, **kwargs)
|
|
|
if not self._check_status(status, 207, check):
|
|
|
- return status, None
|
|
|
+ return status, {}
|
|
|
+ assert answer is not None
|
|
|
responses = self.parse_responses(answer)
|
|
|
assert len(responses) == 1 and path in responses
|
|
|
return status, responses
|
|
|
|
|
|
- def report(self, path, data, check=True, **args):
|
|
|
- status, _, answer = self.request("REPORT", path, data, **args)
|
|
|
+ def report(self, path: str, data: str, check: Union[bool, int] = True,
|
|
|
+ **kwargs) -> Tuple[int, RESPONSES]:
|
|
|
+ status, _, answer = self.request("REPORT", path, data, **kwargs)
|
|
|
if not self._check_status(status, 207, check):
|
|
|
- return status, None
|
|
|
+ return status, {}
|
|
|
+ assert answer is not None
|
|
|
return status, self.parse_responses(answer)
|
|
|
|
|
|
- def delete(self, path, check=True, **args):
|
|
|
- status, _, answer = self.request("DELETE", path, **args)
|
|
|
+ def delete(self, path: str, check: Union[bool, int] = True, **kwargs
|
|
|
+ ) -> Tuple[int, RESPONSES]:
|
|
|
+ assert "data" not in kwargs
|
|
|
+ status, _, answer = self.request("DELETE", path, **kwargs)
|
|
|
if not self._check_status(status, 200, check):
|
|
|
- return status, None
|
|
|
+ return status, {}
|
|
|
+ assert answer is not None
|
|
|
responses = self.parse_responses(answer)
|
|
|
assert len(responses) == 1 and path in responses
|
|
|
return status, responses
|
|
|
|
|
|
- def mkcalendar(self, path, data=None, check=True, **args):
|
|
|
- status, _, answer = self.request("MKCALENDAR", path, data, **args)
|
|
|
+ def mkcalendar(self, path: str, data: Optional[str] = None,
|
|
|
+ check: Union[bool, int] = True, **kwargs
|
|
|
+ ) -> Tuple[int, str]:
|
|
|
+ status, _, answer = self.request("MKCALENDAR", path, data, **kwargs)
|
|
|
self._check_status(status, 201, check)
|
|
|
return status, answer
|
|
|
|
|
|
- def mkcol(self, path, data=None, check=True, **args):
|
|
|
- status, _, _ = self.request("MKCOL", path, data, **args)
|
|
|
+ def mkcol(self, path: str, data: Optional[str] = None,
|
|
|
+ check: Union[bool, int] = True, **kwargs) -> int:
|
|
|
+ status, _, _ = self.request("MKCOL", path, data, **kwargs)
|
|
|
self._check_status(status, 201, check)
|
|
|
return status
|
|
|
|
|
|
- def create_addressbook(self, path, check=True, **args):
|
|
|
+ def create_addressbook(self, path: str, check: Union[bool, int] = True,
|
|
|
+ **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">
|
|
|
@@ -167,4 +223,4 @@ class BaseTest:
|
|
|
</resourcetype>
|
|
|
</prop>
|
|
|
</set>
|
|
|
-</create>""", check=check, **args)
|
|
|
+</create>""", check=check, **kwargs)
|