Skip to content

Commit aa974a0

Browse files
RonaldJENRonaldJEN
authored andcommitted
fix(tests): update error handling for host path creation in Docker volume validation
1 parent 1970410 commit aa974a0

3 files changed

Lines changed: 64 additions & 33 deletions

File tree

sdks/sandbox/python/src/opensandbox/adapters/filesystem_adapter.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from collections.abc import AsyncIterator
2626
from io import IOBase, TextIOBase
2727
from typing import TypedDict
28+
from urllib.parse import quote
2829

2930
import httpx
3031

@@ -52,7 +53,7 @@
5253

5354
class _DownloadRequest(TypedDict):
5455
url: str
55-
params: dict[str, str]
56+
params: dict[str, str] | None
5657
headers: dict[str, str]
5758

5859

@@ -158,11 +159,13 @@ async def read_bytes(
158159
request_data = self._build_download_request(path, range_header)
159160
client = await self._get_httpx_client()
160161

161-
response = await client.get(
162-
request_data["url"],
163-
params=request_data["params"],
164-
headers=request_data["headers"],
165-
)
162+
request_kwargs: dict[str, dict[str, str]] = {
163+
"headers": request_data["headers"],
164+
}
165+
if request_data["params"] is not None:
166+
request_kwargs["params"] = request_data["params"]
167+
168+
response = await client.get(request_data["url"], **request_kwargs)
166169
response.raise_for_status()
167170
return response.content
168171
except Exception as e:
@@ -186,12 +189,13 @@ async def read_bytes_stream(
186189
params = request_data["params"]
187190
headers = request_data["headers"]
188191

189-
request = client.build_request(
190-
"GET",
191-
url,
192-
params=params,
193-
headers=headers,
194-
)
192+
request_kwargs: dict[str, dict[str, str]] = {
193+
"headers": headers,
194+
}
195+
if params is not None:
196+
request_kwargs["params"] = params
197+
198+
request = client.build_request("GET", url, **request_kwargs)
195199

196200
response = await client.send(request, stream=True)
197201

@@ -485,6 +489,6 @@ def _build_download_request(
485489

486490
return {
487491
"url": url,
488-
"params": {},
492+
"params": None,
489493
"headers": headers,
490494
}

sdks/sandbox/python/src/opensandbox/sync/adapters/filesystem_adapter.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from collections.abc import Iterator
2323
from io import IOBase, TextIOBase
2424
from typing import TypedDict
25+
from urllib.parse import quote
2526

2627
import httpx
2728

@@ -49,7 +50,7 @@
4950

5051
class _DownloadRequest(TypedDict):
5152
url: str
52-
params: dict[str, str]
53+
params: dict[str, str] | None
5354
headers: dict[str, str]
5455

5556

@@ -92,7 +93,7 @@ def _build_download_request(self, path: str, range_header: str | None = None) ->
9293
headers: dict[str, str] = {}
9394
if range_header:
9495
headers["Range"] = range_header
95-
return {"url": url, "params": {}, "headers": headers}
96+
return {"url": url, "params": None, "headers": headers}
9697

9798
def read_file(
9899
self,
@@ -108,11 +109,13 @@ def read_bytes(self, path: str, *, range_header: str | None = None) -> bytes:
108109
logger.debug("Reading file as bytes: %s", path)
109110
try:
110111
request_data = self._build_download_request(path, range_header)
111-
response = self._httpx_client.get(
112-
request_data["url"],
113-
params=request_data["params"],
114-
headers=request_data["headers"],
115-
)
112+
request_kwargs: dict[str, dict[str, str]] = {
113+
"headers": request_data["headers"],
114+
}
115+
if request_data["params"] is not None:
116+
request_kwargs["params"] = request_data["params"]
117+
118+
response = self._httpx_client.get(request_data["url"], **request_kwargs)
116119
response.raise_for_status()
117120
return response.content
118121
except Exception as e:
@@ -128,7 +131,11 @@ def read_bytes_stream(
128131
params = request_data["params"]
129132
headers = request_data["headers"]
130133

131-
request = self._httpx_client.build_request("GET", url, params=params, headers=headers)
134+
request_kwargs: dict[str, dict[str, str]] = {"headers": headers}
135+
if params is not None:
136+
request_kwargs["params"] = params
137+
138+
request = self._httpx_client.build_request("GET", url, **request_kwargs)
132139
response = self._httpx_client.send(request, stream=True)
133140

134141
if response.status_code >= 300:

server/tests/test_docker_service.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ def test_pvc_subpath_binds_resolved_to_mountpoint(self, mock_docker):
988988
assert binds[0] == "/var/lib/docker/volumes/my-vol/_data/datasets/train:/mnt/train:ro"
989989

990990
def test_host_path_not_found_rejected(self, mock_docker):
991-
"""Host path that does not exist should be rejected."""
991+
"""Host path create failure should return 500 with HOST_PATH_CREATE_FAILED."""
992992
mock_client = MagicMock()
993993
mock_client.containers.list.return_value = []
994994
mock_docker.from_env.return_value = mock_client
@@ -1012,11 +1012,12 @@ def test_host_path_not_found_rejected(self, mock_docker):
10121012
],
10131013
)
10141014

1015-
with pytest.raises(HTTPException) as exc_info:
1016-
service.create_sandbox(request)
1015+
with patch("src.services.docker.os.makedirs", side_effect=PermissionError("denied")):
1016+
with pytest.raises(HTTPException) as exc_info:
1017+
service.create_sandbox(request)
10171018

1018-
assert exc_info.value.status_code == status.HTTP_400_BAD_REQUEST
1019-
assert exc_info.value.detail["code"] == SandboxErrorCodes.HOST_PATH_NOT_FOUND
1019+
assert exc_info.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
1020+
assert exc_info.value.detail["code"] == SandboxErrorCodes.HOST_PATH_CREATE_FAILED
10201021

10211022
def test_host_path_not_in_allowlist_rejected(self, mock_docker):
10221023
"""Host path not in allowlist should be rejected."""
@@ -1167,17 +1168,21 @@ def test_host_volume_with_subpath_resolved_correctly(self, mock_docker):
11671168
assert len(binds) == 1
11681169
assert binds[0] == f"{sub_dir}:/mnt/work:ro"
11691170

1170-
def test_host_subpath_not_found_rejected(self, mock_docker):
1171-
"""Host volume with non-existent subPath should be rejected."""
1171+
def test_host_subpath_auto_created(self, mock_docker):
1172+
"""Host volume with non-existent subPath should be auto-created."""
11721173
mock_client = MagicMock()
11731174
mock_client.containers.list.return_value = []
1175+
mock_client.api.create_host_config.return_value = {}
1176+
mock_client.api.create_container.return_value = {"Id": "cid"}
1177+
mock_client.containers.get.return_value = MagicMock()
11741178
mock_docker.from_env.return_value = mock_client
11751179

11761180
service = DockerSandboxService(config=_app_config())
11771181

11781182
import tempfile
11791183

11801184
with tempfile.TemporaryDirectory() as tmpdir:
1185+
sub = "auto-created-sub"
11811186
request = CreateSandboxRequest(
11821187
image=ImageSpec(uri="python:3.11"),
11831188
timeout=120,
@@ -1191,16 +1196,31 @@ def test_host_subpath_not_found_rejected(self, mock_docker):
11911196
host=Host(path=tmpdir),
11921197
mount_path="/mnt/work",
11931198
read_only=False,
1194-
sub_path="nonexistent-sub",
1199+
sub_path=sub,
11951200
)
11961201
],
11971202
)
11981203

1199-
with pytest.raises(HTTPException) as exc_info:
1200-
service.create_sandbox(request)
1204+
import os
1205+
1206+
resolved = os.path.join(tmpdir, sub)
1207+
assert not os.path.exists(resolved)
12011208

1202-
assert exc_info.value.status_code == status.HTTP_400_BAD_REQUEST
1203-
assert exc_info.value.detail["code"] == SandboxErrorCodes.HOST_PATH_NOT_FOUND
1209+
# create_sandbox will proceed past volume validation (subpath
1210+
# auto-created) but will fail later during container provisioning
1211+
# (mock doesn't cover the full flow). We only care that the
1212+
# directory was created — NOT that it raised HOST_PATH_CREATE_FAILED.
1213+
try:
1214+
service.create_sandbox(request)
1215+
except HTTPException as e:
1216+
# If it's our own create-failed error, the auto-create didn't
1217+
# work — let the test fail explicitly.
1218+
if e.detail.get("code") == SandboxErrorCodes.HOST_PATH_CREATE_FAILED:
1219+
raise
1220+
except Exception:
1221+
pass # other provisioning errors are expected
1222+
1223+
assert os.path.isdir(resolved)
12041224

12051225
def test_empty_allowlist_permits_any_host_path(self, mock_docker):
12061226
"""Empty allowed_host_paths (default) should permit any valid host path."""

0 commit comments

Comments
 (0)