* 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>
171 lines
6.3 KiB
Python
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)
|