test_server.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2018-2019 Unrud <unrud@outlook.com>
  3. #
  4. # This library is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This library is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. Test the internal server.
  18. """
  19. import os
  20. import shutil
  21. import socket
  22. import ssl
  23. import subprocess
  24. import sys
  25. import tempfile
  26. import threading
  27. import time
  28. from configparser import RawConfigParser
  29. from urllib import request
  30. from urllib.error import HTTPError, URLError
  31. import pytest
  32. from radicale import config, server
  33. from radicale.tests.helpers import configuration_to_dict, get_file_path
  34. try:
  35. import gunicorn
  36. except ImportError:
  37. gunicorn = None
  38. class DisabledRedirectHandler(request.HTTPRedirectHandler):
  39. def http_error_302(self, req, fp, code, msg, headers):
  40. raise HTTPError(req.full_url, code, msg, headers, fp)
  41. http_error_301 = http_error_303 = http_error_307 = http_error_302
  42. class TestBaseServerRequests:
  43. """Test the internal server."""
  44. def setup(self):
  45. self.configuration = config.load()
  46. self.colpath = tempfile.mkdtemp()
  47. self.shutdown_socket, shutdown_socket_out = socket.socketpair()
  48. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
  49. # Find available port
  50. sock.bind(("127.0.0.1", 0))
  51. self.sockname = sock.getsockname()
  52. self.configuration.update({
  53. "storage": {"filesystem_folder": self.colpath},
  54. "server": {"hosts": "[%s]:%d" % self.sockname},
  55. # Enable debugging for new processes
  56. "logging": {"level": "debug"},
  57. # Disable syncing to disk for better performance
  58. "internal": {"filesystem_fsync": "False"}}, "test", internal=True)
  59. self.thread = threading.Thread(target=server.serve, args=(
  60. self.configuration, shutdown_socket_out))
  61. ssl_context = ssl.create_default_context()
  62. ssl_context.check_hostname = False
  63. ssl_context.verify_mode = ssl.CERT_NONE
  64. self.opener = request.build_opener(
  65. request.HTTPSHandler(context=ssl_context),
  66. DisabledRedirectHandler)
  67. def teardown(self):
  68. self.shutdown_socket.sendall(b" ")
  69. try:
  70. self.thread.join()
  71. except RuntimeError: # Thread never started
  72. pass
  73. shutil.rmtree(self.colpath)
  74. def request(self, method, path, data=None, is_alive_fn=None, **headers):
  75. """Send a request."""
  76. if is_alive_fn is None:
  77. is_alive_fn = self.thread.is_alive
  78. scheme = ("https" if self.configuration.get("server", "ssl") else
  79. "http")
  80. req = request.Request(
  81. "%s://[%s]:%d%s" % (scheme, *self.sockname, path),
  82. data=data, headers=headers, method=method)
  83. while True:
  84. assert is_alive_fn()
  85. try:
  86. with self.opener.open(req) as f:
  87. return f.getcode(), f.info(), f.read().decode()
  88. except HTTPError as e:
  89. return e.code, e.headers, e.read().decode()
  90. except URLError as e:
  91. if not isinstance(e.reason, ConnectionRefusedError):
  92. raise
  93. time.sleep(0.1)
  94. def test_root(self):
  95. self.thread.start()
  96. status, _, _ = self.request("GET", "/")
  97. assert status == 302
  98. def test_ssl(self):
  99. self.configuration.update({
  100. "server": {"ssl": "True",
  101. "certificate": get_file_path("cert.pem"),
  102. "key": get_file_path("key.pem")}}, "test")
  103. self.thread.start()
  104. status, _, _ = self.request("GET", "/")
  105. assert status == 302
  106. @pytest.mark.skipif(not server.HAS_IPV6, reason="IPv6 not supported")
  107. def test_ipv6(self):
  108. with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock:
  109. sock.setsockopt(server.IPPROTO_IPV6, server.IPV6_V6ONLY, 1)
  110. try:
  111. # Find available port
  112. sock.bind(("::1", 0))
  113. except OSError:
  114. pytest.skip("IPv6 not supported")
  115. self.sockname = sock.getsockname()[:2]
  116. self.configuration.update({
  117. "server": {"hosts": "[%s]:%d" % self.sockname}}, "test")
  118. savedEaiAddrfamily = server.EAI_ADDRFAMILY
  119. if os.name == "nt" and server.EAI_ADDRFAMILY is None:
  120. # HACK: incomplete errno conversion in WINE
  121. server.EAI_ADDRFAMILY = -9
  122. try:
  123. self.thread.start()
  124. status, _, _ = self.request("GET", "/")
  125. finally:
  126. server.EAI_ADDRFAMILY = savedEaiAddrfamily
  127. assert status == 302
  128. def test_command_line_interface(self):
  129. config_args = []
  130. for section, values in config.DEFAULT_CONFIG_SCHEMA.items():
  131. if values.get("_internal", False):
  132. continue
  133. for option, data in values.items():
  134. if option.startswith("_"):
  135. continue
  136. long_name = "--{0}-{1}".format(
  137. section, option.replace("_", "-"))
  138. if data["type"] == bool:
  139. if not self.configuration.get(section, option):
  140. long_name = "--no{0}".format(long_name[1:])
  141. config_args.append(long_name)
  142. else:
  143. config_args.append(long_name)
  144. config_args.append(
  145. self.configuration.get_raw(section, option))
  146. env = os.environ.copy()
  147. env["PYTHONPATH"] = os.pathsep.join(sys.path)
  148. p = subprocess.Popen(
  149. [sys.executable, "-m", "radicale"] + config_args, env=env)
  150. try:
  151. status, _, _ = self.request(
  152. "GET", "/", is_alive_fn=lambda: p.poll() is None)
  153. assert status == 302
  154. finally:
  155. p.terminate()
  156. p.wait()
  157. if os.name == "posix":
  158. assert p.returncode == 0
  159. @pytest.mark.skipif(not gunicorn, reason="gunicorn module not found")
  160. def test_wsgi_server(self):
  161. config_path = os.path.join(self.colpath, "config")
  162. parser = RawConfigParser()
  163. parser.read_dict(configuration_to_dict(self.configuration))
  164. with open(config_path, "w") as f:
  165. parser.write(f)
  166. env = os.environ.copy()
  167. env["PYTHONPATH"] = os.pathsep.join(sys.path)
  168. p = subprocess.Popen([
  169. sys.executable,
  170. "-c", "from gunicorn.app.wsgiapp import run; run()",
  171. "--bind", self.configuration.get_raw("server", "hosts"),
  172. "--env", "RADICALE_CONFIG=%s" % config_path, "radicale"], env=env)
  173. try:
  174. status, _, _ = self.request(
  175. "GET", "/", is_alive_fn=lambda: p.poll() is None)
  176. assert status == 302
  177. finally:
  178. p.terminate()
  179. p.wait()
  180. assert p.returncode == 0