__init__.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2012-2017 Guillaume Ayoub
  3. # Copyright © 2017-2018 Unrud <unrud@outlook.com>
  4. #
  5. # This library is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This library is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  17. """
  18. Tests for Radicale.
  19. """
  20. import base64
  21. import logging
  22. import sys
  23. from io import BytesIO
  24. import defusedxml.ElementTree as DefusedET
  25. import radicale
  26. from radicale import xmlutils
  27. # Enable debug output
  28. radicale.log.logger.setLevel(logging.DEBUG)
  29. class BaseTest:
  30. """Base class for tests."""
  31. def request(self, method, path, data=None, login=None, **args):
  32. """Send a request."""
  33. for key in args:
  34. args[key.upper()] = args[key]
  35. if login:
  36. args["HTTP_AUTHORIZATION"] = "Basic " + base64.b64encode(
  37. login.encode()).decode()
  38. args["REQUEST_METHOD"] = method.upper()
  39. args["PATH_INFO"] = path
  40. if data:
  41. data = data.encode()
  42. args["wsgi.input"] = BytesIO(data)
  43. args["CONTENT_LENGTH"] = str(len(data))
  44. args["wsgi.errors"] = sys.stderr
  45. status = headers = None
  46. def start_response(status_, headers_):
  47. nonlocal status, headers
  48. status = status_
  49. headers = headers_
  50. answer = self.application(args, start_response)
  51. return (int(status.split()[0]), dict(headers),
  52. answer[0].decode() if answer else None)
  53. @staticmethod
  54. def parse_responses(text):
  55. xml = DefusedET.fromstring(text)
  56. assert xml.tag == xmlutils.make_clark("D:multistatus")
  57. path_responses = {}
  58. for response in xml.findall(xmlutils.make_clark("D:response")):
  59. href = response.find(xmlutils.make_clark("D:href"))
  60. assert href.text not in path_responses
  61. prop_respones = {}
  62. for propstat in response.findall(
  63. xmlutils.make_clark("D:propstat")):
  64. status = propstat.find(xmlutils.make_clark("D:status"))
  65. assert status.text.startswith("HTTP/1.1 ")
  66. status_code = int(status.text.split(" ")[1])
  67. for prop in propstat.findall(xmlutils.make_clark("D:prop")):
  68. for element in prop:
  69. human_tag = xmlutils.make_human_tag(element.tag)
  70. assert human_tag not in prop_respones
  71. prop_respones[human_tag] = (status_code, element)
  72. status = response.find(xmlutils.make_clark("D:status"))
  73. if status is not None:
  74. assert not prop_respones
  75. assert status.text.startswith("HTTP/1.1 ")
  76. status_code = int(status.text.split(" ")[1])
  77. path_responses[href.text] = status_code
  78. else:
  79. path_responses[href.text] = prop_respones
  80. return path_responses
  81. @staticmethod
  82. def _check_status(status, good_status, check=True):
  83. if check is True:
  84. assert status == good_status
  85. elif check is not False:
  86. assert status == check
  87. return status == good_status
  88. def get(self, path, check=True, **args):
  89. status, _, answer = self.request("GET", path, **args)
  90. self._check_status(status, 200, check)
  91. return status, answer
  92. def put(self, path, data, check=True, **args):
  93. status, _, answer = self.request("PUT", path, data, **args)
  94. self._check_status(status, 201, check)
  95. return status
  96. def propfind(self, path, data=None, check=True, **args):
  97. status, _, answer = self.request("PROPFIND", path, data, **args)
  98. if not self._check_status(status, 207, check):
  99. return status, None
  100. responses = self.parse_responses(answer)
  101. if args.get("HTTP_DEPTH", 0) == 0:
  102. assert len(responses) == 1 and path in responses
  103. return status, responses
  104. def proppatch(self, path, data=None, check=True, **args):
  105. status, _, answer = self.request("PROPPATCH", path, data, **args)
  106. if not self._check_status(status, 207, check):
  107. return status, None
  108. responses = self.parse_responses(answer)
  109. assert len(responses) == 1 and path in responses
  110. return status, responses
  111. def report(self, path, data, check=True, **args):
  112. status, _, answer = self.request("REPORT", path, data, **args)
  113. if not self._check_status(status, 207, check):
  114. return status, None
  115. return status, self.parse_responses(answer)
  116. def delete(self, path, check=True, **args):
  117. status, _, answer = self.request("DELETE", path, **args)
  118. if not self._check_status(status, 200, check):
  119. return status, None
  120. responses = self.parse_responses(answer)
  121. assert len(responses) == 1 and path in responses
  122. return status, responses
  123. def mkcalendar(self, path, data=None, check=True, **args):
  124. status, _, _ = self.request("MKCALENDAR", path, data, **args)
  125. self._check_status(status, 201, check)
  126. return status
  127. def mkcol(self, path, data=None, check=True, **args):
  128. status, _, _ = self.request("MKCOL", path, data, **args)
  129. self._check_status(status, 201, check)
  130. return status
  131. def create_addressbook(self, path, check=True, **args):
  132. return self.mkcol(path, """\
  133. <?xml version="1.0" encoding="UTF-8" ?>
  134. <create xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
  135. <set>
  136. <prop>
  137. <resourcetype>
  138. <collection />
  139. <CR:addressbook />
  140. </resourcetype>
  141. </prop>
  142. </set>
  143. </create>""", check=check, **args)