mirror of
https://github.com/Kozea/Radicale.git
synced 2025-08-01 18:18:31 +00:00
Type hints for tests
This commit is contained in:
parent
698ae875ce
commit
60f25bf19a
12 changed files with 501 additions and 379 deletions
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue