Files
Radicale/radicale/tests/__init__.py
Tom Hacohen d3bb19800c Web: add support for the POST HTTP method. (#1097)
* Web: add support for the POST HTTP method.

This patch adds support for POST in addition to the already supported GET.

This is needed for implementing more complex web modules that also
support configuration modifications and advanced queries.

* Base web: return METHOD_NOT_ALLOWED when method isn't implemenetd.

Co-authored-by: Unrud <Unrud@users.noreply.github.com>
2020-09-14 18:17:45 +02:00

171 lines
6.3 KiB
Python

# This file is part of Radicale Server - Calendar Server
# Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
#
# 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.
"""
import base64
import logging
import sys
from io import BytesIO
import defusedxml.ElementTree as DefusedET
import radicale
from radicale import xmlutils
# Enable debug output
radicale.log.logger.setLevel(logging.DEBUG)
class BaseTest:
"""Base class for tests."""
def request(self, method, path, data=None, login=None, **args):
"""Send a request."""
for key in args:
args[key.upper()] = args[key]
if login:
args["HTTP_AUTHORIZATION"] = "Basic " + base64.b64encode(
login.encode()).decode()
args["REQUEST_METHOD"] = method.upper()
args["PATH_INFO"] = path
if data:
data = data.encode()
args["wsgi.input"] = BytesIO(data)
args["CONTENT_LENGTH"] = str(len(data))
args["wsgi.errors"] = sys.stderr
status = headers = None
def start_response(status_, headers_):
nonlocal status, headers
status = status_
headers = headers_
answer = self.application(args, start_response)
return (int(status.split()[0]), dict(headers),
answer[0].decode() if answer else None)
@staticmethod
def parse_responses(text):
xml = DefusedET.fromstring(text)
assert xml.tag == xmlutils.make_clark("D:multistatus")
path_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
prop_respones = {}
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])
for prop in propstat.findall(xmlutils.make_clark("D:prop")):
for element in prop:
human_tag = xmlutils.make_human_tag(element.tag)
assert human_tag not in prop_respones
prop_respones[human_tag] = (status_code, element)
status = response.find(xmlutils.make_clark("D:status"))
if status is not None:
assert not prop_respones
assert status.text.startswith("HTTP/1.1 ")
status_code = int(status.text.split(" ")[1])
path_responses[href.text] = status_code
else:
path_responses[href.text] = prop_respones
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
return status == good_status
def get(self, path, check=True, **args):
status, _, answer = self.request("GET", path, **args)
self._check_status(status, 200, check)
return status, answer
def post(self, path, check=True, **args):
status, _, answer = self.request("POST", path, **args)
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)
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)
if not self._check_status(status, 207, check):
return status, None
responses = self.parse_responses(answer)
if args.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)
if not self._check_status(status, 207, check):
return status, 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)
if not self._check_status(status, 207, check):
return status, None
return status, self.parse_responses(answer)
def delete(self, path, check=True, **args):
status, _, answer = self.request("DELETE", path, **args)
if not self._check_status(status, 200, check):
return status, 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)
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)
self._check_status(status, 201, check)
return status
def create_addressbook(self, path, check=True, **args):
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>
</create>""", check=check, **args)