test_http.py 12 KB

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