test_sync_files.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import io
  2. from unittest import mock
  3. import pytest
  4. from urllib3.response import HTTPResponse
  5. from urllib3.util.request import set_file_position
  6. from clean_python.api_client import ApiException
  7. from clean_python.api_client import download_file
  8. from clean_python.api_client import download_fileobj
  9. from clean_python.api_client import upload_file
  10. from clean_python.api_client import upload_fileobj
  11. from clean_python.api_client.files import _SeekableChunkIterator
  12. from clean_python.api_client.files import DEFAULT_UPLOAD_TIMEOUT
  13. MODULE = "clean_python.api_client.files"
  14. @pytest.fixture
  15. def pool():
  16. pool = mock.Mock()
  17. return pool
  18. @pytest.fixture
  19. def responses_single():
  20. return [
  21. HTTPResponse(
  22. body=b"X" * 42,
  23. headers={"Content-Range": "bytes 0-41/42"},
  24. status=206,
  25. )
  26. ]
  27. @pytest.fixture
  28. def responses_double():
  29. return [
  30. HTTPResponse(
  31. body=b"X" * 64,
  32. headers={"Content-Range": "bytes 0-63/65"},
  33. status=206,
  34. ),
  35. HTTPResponse(
  36. body=b"X",
  37. headers={"Content-Range": "bytes 64-64/65"},
  38. status=206,
  39. ),
  40. ]
  41. def test_download_fileobj(pool, responses_single):
  42. stream = io.BytesIO()
  43. pool.request.side_effect = responses_single
  44. download_fileobj("some-url", stream, chunk_size=64, pool=pool)
  45. pool.request.assert_called_with(
  46. "GET",
  47. "some-url",
  48. headers={"Range": "bytes=0-63"},
  49. timeout=5.0,
  50. )
  51. assert stream.tell() == 42
  52. def test_download_fileobj_two_chunks(pool, responses_double):
  53. stream = io.BytesIO()
  54. pool.request.side_effect = responses_double
  55. callback_func = mock.Mock()
  56. download_fileobj(
  57. "some-url", stream, chunk_size=64, pool=pool, callback_func=callback_func
  58. )
  59. (_, kwargs1), (_, kwargs2) = pool.request.call_args_list
  60. assert kwargs1["headers"] == {"Range": "bytes=0-63"}
  61. assert kwargs2["headers"] == {"Range": "bytes=64-127"}
  62. assert stream.tell() == 65
  63. # Check callback func
  64. (args1, _), (args2, _) = callback_func.call_args_list
  65. assert args1 == (63, 65)
  66. assert args2 == (65, 65)
  67. def test_download_fileobj_no_multipart(pool, responses_single):
  68. """The remote server does not support range requests"""
  69. responses_single[0].status = 200
  70. pool.request.side_effect = responses_single
  71. with pytest.raises(ApiException) as e:
  72. download_fileobj("some-url", None, chunk_size=64, pool=pool)
  73. assert e.value.status == 200
  74. assert str(e.value) == "The file server does not support multipart downloads."
  75. def test_download_fileobj_forbidden(pool, responses_single):
  76. """The remote server does not support range requests"""
  77. responses_single[0].status = 403
  78. pool.request.side_effect = responses_single
  79. with pytest.raises(ApiException) as e:
  80. download_fileobj("some-url", None, chunk_size=64, pool=pool)
  81. assert e.value.status == 403
  82. @mock.patch(MODULE + ".download_fileobj")
  83. def test_download_file(download_fileobj, tmp_path):
  84. download_file(
  85. "http://domain/a.b", tmp_path / "c.d", chunk_size=64, timeout=3.0, pool="foo"
  86. )
  87. args, kwargs = download_fileobj.call_args
  88. assert args[0] == "http://domain/a.b"
  89. assert isinstance(args[1], io.IOBase)
  90. assert args[1].mode == "wb"
  91. assert args[1].name == str(tmp_path / "c.d")
  92. assert kwargs["chunk_size"] == 64
  93. assert kwargs["timeout"] == 3.0
  94. assert kwargs["pool"] == "foo"
  95. @mock.patch(MODULE + ".download_fileobj")
  96. def test_download_file_directory(download_fileobj, tmp_path):
  97. # if a target directory is specified, a filename is created from the url
  98. download_file("http://domain/a.b", tmp_path, chunk_size=64, timeout=3.0, pool="foo")
  99. args, kwargs = download_fileobj.call_args
  100. assert args[1].name == str(tmp_path / "a.b")
  101. @pytest.fixture
  102. def upload_response():
  103. return HTTPResponse(status=200)
  104. @pytest.fixture
  105. def fileobj():
  106. stream = io.BytesIO()
  107. stream.write(b"X" * 39)
  108. stream.seek(0)
  109. return stream
  110. @pytest.mark.parametrize(
  111. "chunk_size,expected_body",
  112. [
  113. (64, [b"X" * 39]),
  114. (39, [b"X" * 39]),
  115. (38, [b"X" * 38, b"X"]),
  116. (16, [b"X" * 16, b"X" * 16, b"X" * 7]),
  117. ],
  118. )
  119. def test_upload_fileobj(pool, fileobj, upload_response, chunk_size, expected_body):
  120. pool.request.return_value = upload_response
  121. upload_fileobj("some-url", fileobj, chunk_size=chunk_size, pool=pool)
  122. args, kwargs = pool.request.call_args
  123. assert args == ("PUT", "some-url")
  124. assert list(kwargs["body"]) == expected_body
  125. assert kwargs["headers"] == {"Content-Length": "39"}
  126. assert kwargs["timeout"] == DEFAULT_UPLOAD_TIMEOUT
  127. def test_upload_fileobj_callback(pool, fileobj, upload_response):
  128. expected_body = [b"X" * 16, b"X" * 16, b"X" * 7]
  129. chunk_size = 16
  130. pool.request.return_value = upload_response
  131. callback_func = mock.Mock()
  132. upload_fileobj(
  133. "some-url",
  134. fileobj,
  135. chunk_size=chunk_size,
  136. pool=pool,
  137. callback_func=callback_func,
  138. )
  139. args, kwargs = pool.request.call_args
  140. assert args == ("PUT", "some-url")
  141. assert list(kwargs["body"]) == expected_body
  142. assert kwargs["headers"] == {"Content-Length": "39"}
  143. assert kwargs["timeout"] == DEFAULT_UPLOAD_TIMEOUT
  144. # Check callback_func
  145. (args1, _), (args2, _), (args3, _) = callback_func.call_args_list
  146. assert args1 == (16, 39)
  147. assert args2 == (32, 39)
  148. assert args3 == (39, 39)
  149. def test_upload_fileobj_with_md5(pool, fileobj, upload_response):
  150. pool.request.return_value = upload_response
  151. upload_fileobj("some-url", fileobj, pool=pool, md5=b"abcd")
  152. # base64.b64encode(b"abcd")).decode()
  153. expected_md5 = "YWJjZA=="
  154. args, kwargs = pool.request.call_args
  155. assert kwargs["headers"] == {"Content-Length": "39", "Content-MD5": expected_md5}
  156. def test_upload_fileobj_empty_file():
  157. with pytest.raises(IOError, match="The file object is empty."):
  158. upload_fileobj("some-url", io.BytesIO())
  159. def test_upload_fileobj_non_binary_file():
  160. with pytest.raises(IOError, match="The file object is not in binary*"):
  161. upload_fileobj("some-url", io.StringIO())
  162. def test_upload_fileobj_errors(pool, fileobj, upload_response):
  163. upload_response.status = 400
  164. pool.request.return_value = upload_response
  165. with pytest.raises(ApiException) as e:
  166. upload_fileobj("some-url", fileobj, pool=pool)
  167. assert e.value.status == 400
  168. @mock.patch(MODULE + ".upload_fileobj")
  169. def test_upload_file(upload_fileobj, tmp_path):
  170. path = tmp_path / "myfile"
  171. with path.open("wb") as f:
  172. f.write(b"X")
  173. upload_file(
  174. "http://domain/a.b", path, chunk_size=1234, timeout=3.0, pool="foo", md5=b"abcd"
  175. )
  176. args, kwargs = upload_fileobj.call_args
  177. assert args[0] == "http://domain/a.b"
  178. assert isinstance(args[1], io.IOBase)
  179. assert args[1].mode == "rb"
  180. assert args[1].name == str(path)
  181. assert kwargs["timeout"] == 3.0
  182. assert kwargs["chunk_size"] == 1234
  183. assert kwargs["pool"] == "foo"
  184. assert kwargs["md5"] == b"abcd"
  185. def test_seekable_chunk_iterator():
  186. data = b"XYZ"
  187. body = _SeekableChunkIterator(io.BytesIO(data), chunk_size=4)
  188. pos = set_file_position(body, pos=0)
  189. assert pos == 0
  190. assert list(body) == [data]
  191. assert list(body) == []
  192. set_file_position(body, pos)
  193. assert list(body) == [data]