test_http.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import itertools
  2. import subprocess
  3. import sys
  4. import textwrap
  5. import pytest
  6. import requests
  7. from werkzeug.exceptions import ClientDisconnected
  8. from pytest_localserver import http
  9. from pytest_localserver import plugin
  10. # define test fixture here again in order to run tests without having to
  11. # install the plugin anew every single time
  12. httpserver = plugin.httpserver
  13. transfer_encoded = pytest.mark.parametrize(
  14. "transfer_encoding_header", ["Transfer-encoding", "Transfer-Encoding", "transfer-encoding", "TRANSFER-ENCODING"]
  15. )
  16. def test_httpserver_funcarg(httpserver):
  17. assert isinstance(httpserver, http.ContentServer)
  18. assert httpserver.is_alive()
  19. assert httpserver.server_address
  20. def test_server_does_not_serve_file_at_startup(httpserver):
  21. assert httpserver.code == 204
  22. assert httpserver.content == ""
  23. def test_some_content_retrieval(httpserver):
  24. httpserver.serve_content("TEST!")
  25. resp = requests.get(httpserver.url)
  26. assert resp.text == "TEST!"
  27. assert resp.status_code == 200
  28. def test_request_is_stored(httpserver):
  29. httpserver.serve_content("TEST!")
  30. assert len(httpserver.requests) == 0
  31. requests.get(httpserver.url)
  32. assert len(httpserver.requests) == 1
  33. def test_GET_request(httpserver):
  34. httpserver.serve_content("TEST!", headers={"Content-type": "text/plain"})
  35. resp = requests.get(httpserver.url, headers={"User-Agent": "Test method"})
  36. assert resp.text == "TEST!"
  37. assert resp.status_code == 200
  38. assert "text/plain" in resp.headers["Content-type"]
  39. # FIXME get compression working!
  40. # def test_gzipped_GET_request(httpserver):
  41. # httpserver.serve_content('TEST!', headers={'Content-type': 'text/plain'})
  42. # httpserver.compress = 'gzip'
  43. # resp = requests.get(httpserver.url, headers={
  44. # 'User-Agent': 'Test method',
  45. # 'Accept-encoding': 'gzip'
  46. # })
  47. # assert resp.text == 'TEST!'
  48. # assert resp.status_code == 200
  49. # assert resp.content_encoding == 'gzip'
  50. # assert resp.headers['Content-type'] == 'text/plain'
  51. # assert resp.headers['content-encoding'] == 'gzip'
  52. def test_HEAD_request(httpserver):
  53. httpserver.serve_content("TEST!", headers={"Content-type": "text/plain"})
  54. resp = requests.head(httpserver.url)
  55. assert resp.status_code == 200
  56. assert resp.headers["Content-type"] == "text/plain"
  57. # def test_POST_request(httpserver):
  58. # headers = {'Content-type': 'application/x-www-form-urlencoded',
  59. # 'set-cookie': 'some _cookie_content'}
  60. #
  61. # httpserver.serve_content('TEST!', headers=headers)
  62. # resp = requests.post(httpserver.url, data={'data': 'value'}, headers=headers)
  63. # assert resp.text == 'TEST!'
  64. # assert resp.status_code == 200
  65. #
  66. # httpserver.serve_content('TEST!', headers=headers, show_post_vars=True)
  67. # resp = requests.post(httpserver.url, data={'data': 'value'}, headers=headers)
  68. # assert resp.json() == {'data': 'value'}
  69. # assert resp.status_code == 200
  70. def test_POST_request_no_store_data(httpserver):
  71. headers = {"Content-type": "text/plain"}
  72. httpserver.serve_content("TEST!", store_request_data=False)
  73. requests.post(httpserver.url, data=b"testdata", headers=headers)
  74. request = httpserver.requests[-1]
  75. request.input_stream.close()
  76. with pytest.raises(ClientDisconnected):
  77. request.data
  78. def test_POST_request_store_data(httpserver):
  79. headers = {"Content-type": "text/plain"}
  80. httpserver.serve_content("TEST!", store_request_data=True)
  81. requests.post(httpserver.url, data=b"testdata", headers=headers)
  82. request = httpserver.requests[-1]
  83. request.input_stream.close()
  84. assert httpserver.requests[-1].data == b"testdata"
  85. @pytest.mark.parametrize("chunked_flag", [http.Chunked.YES, http.Chunked.AUTO, http.Chunked.NO])
  86. def test_chunked_attribute_without_header(httpserver, chunked_flag):
  87. """
  88. Test that passing the chunked attribute to serve_content() properly sets
  89. the chunked property of the server.
  90. """
  91. httpserver.serve_content(("TEST!", "test"), headers={"Content-type": "text/plain"}, chunked=chunked_flag)
  92. assert httpserver.chunked == chunked_flag
  93. @pytest.mark.parametrize("chunked_flag", [http.Chunked.YES, http.Chunked.AUTO, http.Chunked.NO])
  94. def test_chunked_attribute_with_header(httpserver, chunked_flag):
  95. """
  96. Test that passing the chunked attribute to serve_content() properly sets
  97. the chunked property of the server even when the transfer-encoding header is
  98. also set.
  99. """
  100. httpserver.serve_content(
  101. ("TEST!", "test"), headers={"Content-type": "text/plain", "Transfer-encoding": "chunked"}, chunked=chunked_flag
  102. )
  103. assert httpserver.chunked == chunked_flag
  104. @transfer_encoded
  105. @pytest.mark.parametrize("chunked_flag", [http.Chunked.YES, http.Chunked.AUTO])
  106. def test_GET_request_chunked_parameter(httpserver, transfer_encoding_header, chunked_flag):
  107. """
  108. Test that passing YES or AUTO as the chunked parameter to serve_content()
  109. causes the response to be sent using chunking when the Transfer-encoding
  110. header is also set.
  111. """
  112. httpserver.serve_content(
  113. ("TEST!", "test"),
  114. headers={"Content-type": "text/plain", transfer_encoding_header: "chunked"},
  115. chunked=chunked_flag,
  116. )
  117. resp = requests.get(httpserver.url, headers={"User-Agent": "Test method"})
  118. assert resp.text == "TEST!test"
  119. assert resp.status_code == 200
  120. assert "text/plain" in resp.headers["Content-type"]
  121. assert "chunked" in resp.headers["Transfer-encoding"]
  122. @transfer_encoded
  123. @pytest.mark.parametrize("chunked_flag", [http.Chunked.YES, http.Chunked.AUTO])
  124. def test_GET_request_chunked_attribute(httpserver, transfer_encoding_header, chunked_flag):
  125. """
  126. Test that setting the chunked attribute of httpserver to YES or AUTO
  127. causes the response to be sent using chunking when the Transfer-encoding
  128. header is also set.
  129. """
  130. httpserver.serve_content(
  131. ("TEST!", "test"), headers={"Content-type": "text/plain", transfer_encoding_header: "chunked"}
  132. )
  133. httpserver.chunked = chunked_flag
  134. resp = requests.get(httpserver.url, headers={"User-Agent": "Test method"})
  135. assert resp.text == "TEST!test"
  136. assert resp.status_code == 200
  137. assert "text/plain" in resp.headers["Content-type"]
  138. assert "chunked" in resp.headers["Transfer-encoding"]
  139. @transfer_encoded
  140. def test_GET_request_not_chunked(httpserver, transfer_encoding_header):
  141. """
  142. Test that setting the chunked attribute of httpserver to NO causes
  143. the response not to be sent using chunking even if the Transfer-encoding
  144. header is set.
  145. """
  146. httpserver.serve_content(
  147. ("TEST!", "test"),
  148. headers={"Content-type": "text/plain", transfer_encoding_header: "chunked"},
  149. chunked=http.Chunked.NO,
  150. )
  151. with pytest.raises(requests.exceptions.ChunkedEncodingError):
  152. requests.get(httpserver.url, headers={"User-Agent": "Test method"})
  153. @pytest.mark.parametrize("chunked_flag", [http.Chunked.NO, http.Chunked.AUTO])
  154. def test_GET_request_chunked_parameter_no_header(httpserver, chunked_flag):
  155. """
  156. Test that passing NO or AUTO as the chunked parameter to serve_content()
  157. causes the response not to be sent using chunking when the Transfer-encoding
  158. header is not set.
  159. """
  160. httpserver.serve_content(("TEST!", "test"), headers={"Content-type": "text/plain"}, chunked=chunked_flag)
  161. resp = requests.get(httpserver.url, headers={"User-Agent": "Test method"})
  162. assert resp.text == "TEST!test"
  163. assert resp.status_code == 200
  164. assert "text/plain" in resp.headers["Content-type"]
  165. assert "Transfer-encoding" not in resp.headers
  166. @pytest.mark.parametrize("chunked_flag", [http.Chunked.NO, http.Chunked.AUTO])
  167. def test_GET_request_chunked_attribute_no_header(httpserver, chunked_flag):
  168. """
  169. Test that setting the chunked attribute of httpserver to NO or AUTO
  170. causes the response not to be sent using chunking when the Transfer-encoding
  171. header is not set.
  172. """
  173. httpserver.serve_content(("TEST!", "test"), headers={"Content-type": "text/plain"})
  174. httpserver.chunked = chunked_flag
  175. resp = requests.get(httpserver.url, headers={"User-Agent": "Test method"})
  176. assert resp.text == "TEST!test"
  177. assert resp.status_code == 200
  178. assert "text/plain" in resp.headers["Content-type"]
  179. assert "Transfer-encoding" not in resp.headers
  180. def test_GET_request_chunked_no_header(httpserver):
  181. """
  182. Test that setting the chunked attribute of httpserver to YES causes
  183. the response to be sent using chunking even if the Transfer-encoding
  184. header is not set.
  185. """
  186. httpserver.serve_content(("TEST!", "test"), headers={"Content-type": "text/plain"}, chunked=http.Chunked.YES)
  187. resp = requests.get(httpserver.url, headers={"User-Agent": "Test method"})
  188. # Without the Transfer-encoding header set, requests does not undo the chunk
  189. # encoding so it comes through as "raw" chunks
  190. assert resp.text == "5\r\nTEST!\r\n4\r\ntest\r\n0\r\n\r\n"
  191. def _format_chunk(chunk):
  192. r = repr(chunk)
  193. if len(r) <= 40:
  194. return r
  195. else:
  196. return r[:13] + "..." + r[-14:] + " (length {})".format(len(chunk))
  197. def _compare_chunks(expected, actual):
  198. __tracebackhide__ = True
  199. if expected != actual:
  200. message = [_format_chunk(expected) + " != " + _format_chunk(actual)]
  201. if type(expected) is type(actual):
  202. for i, (e, a) in enumerate(itertools.zip_longest(expected, actual, fillvalue="<end>")):
  203. if e != a:
  204. message += [
  205. " Chunks differ at index {}:".format(i),
  206. " Expected: " + (repr(expected[i : i + 5]) + "..." if e != "<end>" else "<end>"),
  207. " Found: " + (repr(actual[i : i + 5]) + "..." if a != "<end>" else "<end>"),
  208. ]
  209. break
  210. pytest.fail("\n".join(message))
  211. @pytest.mark.parametrize("chunk_size", [400, 499, 500, 512, 750, 1024, 4096, 8192])
  212. def test_GET_request_large_chunks(httpserver, chunk_size):
  213. """
  214. Test that a response with large chunks comes through correctly
  215. """
  216. body = b"0123456789abcdef" * 1024 # 16 kb total
  217. # Split body into fixed-size chunks, from https://stackoverflow.com/a/18854817/56541
  218. chunks = [body[0 + i : chunk_size + i] for i in range(0, len(body), chunk_size)]
  219. httpserver.serve_content(
  220. chunks, headers={"Content-type": "text/plain", "Transfer-encoding": "chunked"}, chunked=http.Chunked.YES
  221. )
  222. resp = requests.get(httpserver.url, headers={"User-Agent": "Test method"}, stream=True)
  223. assert resp.status_code == 200
  224. text = b""
  225. for original_chunk, received_chunk in itertools.zip_longest(chunks, resp.iter_content(chunk_size=None)):
  226. _compare_chunks(original_chunk, received_chunk)
  227. text += received_chunk
  228. assert text == body
  229. assert "chunked" in resp.headers["Transfer-encoding"]
  230. @pytest.mark.parametrize("chunked_flag", [http.Chunked.YES, http.Chunked.AUTO])
  231. def test_GET_request_chunked_no_content_length(httpserver, chunked_flag):
  232. """
  233. Test that a chunked response does not include a Content-length header
  234. """
  235. httpserver.serve_content(
  236. ("TEST!", "test"), headers={"Content-type": "text/plain", "Transfer-encoding": "chunked"}, chunked=chunked_flag
  237. )
  238. resp = requests.get(httpserver.url, headers={"User-Agent": "Test method"})
  239. assert resp.status_code == 200
  240. assert "Transfer-encoding" in resp.headers
  241. assert "Content-length" not in resp.headers
  242. def test_httpserver_init_failure_no_stderr_during_cleanup(tmp_path):
  243. """
  244. Test that, when the server encounters an error during __init__, its cleanup
  245. does not raise an AttributeError in its __del__ method, which would emit a
  246. warning onto stderr.
  247. """
  248. script_path = tmp_path.joinpath("script.py")
  249. script_path.write_text(
  250. textwrap.dedent(
  251. """
  252. from pytest_localserver import http
  253. from unittest.mock import patch
  254. with patch("pytest_localserver.http.make_server", side_effect=RuntimeError("init failure")):
  255. server = http.ContentServer()
  256. """
  257. )
  258. )
  259. result = subprocess.run([sys.executable, str(script_path)], stderr=subprocess.PIPE)
  260. # We ensure that no warning about an AttributeError is printed on stderr
  261. # due to an error in the server's __del__ method. This AttributeError is
  262. # raised during cleanup and doesn't affect the exit code of the script, so
  263. # we can't use the exit code to tell whether the script did its job.
  264. assert b"AttributeError" not in result.stderr