-
Notifications
You must be signed in to change notification settings - Fork 980
Expand file tree
/
Copy pathtest_upload_file.py
More file actions
195 lines (156 loc) · 6.66 KB
/
Copy pathtest_upload_file.py
File metadata and controls
195 lines (156 loc) · 6.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from unittest import mock
import httpx
from e2b.api.client.client import AuthenticatedClient
from e2b.template import utils as template_utils
from e2b.template.consts import FILE_UPLOAD_TIMEOUT_SECONDS
from e2b.template_sync.build_api import upload_file
# Regression test for e2b-dev/e2b#1243 — upload_file must set Content-Length
# and must not fall back to Transfer-Encoding: chunked. S3 presigned PUT URLs
# reject chunked encoding with 501 NotImplemented. The archive is streamed
# from a temporary file on disk with a known Content-Length instead of being
# buffered in memory; this test guards that contract.
def _make_server():
state = {"headers": None, "body_length": 0}
class Handler(BaseHTTPRequestHandler):
def do_PUT(self):
# hyper (pyqwest) sends lowercase header names where httpcore
# title-cased them; compare case-insensitively.
state["headers"] = {k.lower(): v for k, v in self.headers.items()}
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length) if length else b""
state["body_length"] = len(body)
self.send_response(200)
self.end_headers()
def log_message(self, *args, **kwargs):
return
server = HTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
return server, thread, state
def test_upload_file_sets_content_length_and_no_chunked_encoding(tmp_path):
(tmp_path / "hello.txt").write_text("hello world")
server, thread, state = _make_server()
host, port = server.server_address
url = f"http://{host}:{port}/upload"
try:
class UploadClient(AuthenticatedClient):
def get_httpx_client(self):
raise AssertionError("signed uploads should not use the API client")
client = UploadClient(base_url="http://test", token="test")
upload_file(
api_client=client,
file_name="*.txt",
context_path=str(tmp_path),
url=url,
ignore_patterns=[],
resolve_symlinks=False,
gzip=True,
stack_trace=None,
)
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
assert state["headers"] is not None
content_length = state["headers"].get("content-length")
assert content_length is not None
assert int(content_length) > 0
assert int(content_length) == state["body_length"]
transfer_encoding = state["headers"].get("transfer-encoding")
if transfer_encoding is not None:
assert "chunked" not in transfer_encoding.lower()
assert "authorization" not in state["headers"]
def _capture_upload_timeout(tmp_path, request_timeout=None):
"""Run upload_file against a local server, capturing the httpx timeout."""
(tmp_path / "hello.txt").write_text("hello world")
server, thread, state = _make_server()
host, port = server.server_address
url = f"http://{host}:{port}/upload"
captured = {}
real_client = httpx.Client
def spy_client(*args, **kwargs):
captured["timeout"] = kwargs.get("timeout")
return real_client(*args, **kwargs)
try:
client = AuthenticatedClient(base_url="http://test", token="test")
kwargs = {}
if request_timeout is not None:
kwargs["request_timeout"] = request_timeout
with mock.patch(
"e2b.template_sync.build_api.httpx.Client", side_effect=spy_client
):
upload_file(
api_client=client,
file_name="*.txt",
context_path=str(tmp_path),
url=url,
ignore_patterns=[],
resolve_symlinks=False,
gzip=True,
stack_trace=None,
**kwargs,
)
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
return captured["timeout"]
def test_upload_file_defaults_to_one_hour_timeout(tmp_path):
# Uploads of large archives need far longer than the 60s general API
# timeout, so the default upload timeout is 1 hour (matches the JS SDK).
timeout = _capture_upload_timeout(tmp_path)
assert timeout == httpx.Timeout(FILE_UPLOAD_TIMEOUT_SECONDS)
def test_upload_file_honors_explicit_request_timeout(tmp_path):
# An explicitly set request_timeout overrides the 1-hour upload default.
timeout = _capture_upload_timeout(tmp_path, request_timeout=5.0)
assert timeout == httpx.Timeout(5.0)
def test_upload_file_ignores_post_upload_close_failure(tmp_path):
# Regression test: once S3 has accepted the archive, closing the spooled
# temp file in the `finally` block can raise. That failure must not be
# wrapped as a FileUploadException — the upload already succeeded.
(tmp_path / "hello.txt").write_text("hello world")
server, thread, state = _make_server()
host, port = server.server_address
url = f"http://{host}:{port}/upload"
real_tar_file_stream = template_utils.tar_file_stream
class _FailingCloseFile:
# Proxies a real spooled temp file but raises on close(). The
# underlying file object is a C type whose `close` attribute can't
# be reassigned, so we wrap it instead.
def __init__(self, inner):
self._inner = inner
def __getattr__(self, name):
return getattr(self._inner, name)
def __iter__(self):
return iter(self._inner)
def close(self):
# Run the real close so we don't leak the temp file, then
# simulate a close failure surfacing from the `finally`.
self._inner.close()
raise OSError("close failed")
def failing_close_stream(*args, **kwargs):
return _FailingCloseFile(real_tar_file_stream(*args, **kwargs))
try:
client = AuthenticatedClient(base_url="http://test", token="test")
with mock.patch(
"e2b.template_sync.build_api.tar_file_stream",
side_effect=failing_close_stream,
):
# Must not raise despite close() failing after a 200 response.
upload_file(
api_client=client,
file_name="*.txt",
context_path=str(tmp_path),
url=url,
ignore_patterns=[],
resolve_symlinks=False,
gzip=True,
stack_trace=None,
)
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
assert state["headers"] is not None