Skip to content

Commit f8f485f

Browse files
Harden open-file helper default-prefix fallback behavior
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 01c054c commit f8f485f

9 files changed

Lines changed: 176 additions & 3 deletions

hyperbrowser/client/file_utils.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,20 @@ def build_file_path_error_message(
9090
return f"{normalized_prefix}: {file_path_display}"
9191

9292

93-
def build_open_file_error_message(file_path: object, *, prefix: str) -> str:
93+
def build_open_file_error_message(
94+
file_path: object,
95+
*,
96+
prefix: str,
97+
default_prefix: Optional[str] = None,
98+
) -> str:
9499
return build_file_path_error_message(
95100
file_path,
96101
prefix=prefix,
97-
default_prefix=_DEFAULT_OPEN_ERROR_MESSAGE_PREFIX,
102+
default_prefix=(
103+
_DEFAULT_OPEN_ERROR_MESSAGE_PREFIX
104+
if default_prefix is None
105+
else default_prefix
106+
),
98107
)
99108

100109

hyperbrowser/client/managers/async_manager/extension.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ async def create(self, params: CreateExtensionParams) -> ExtensionResponse:
3030
open_error_message=build_open_file_error_message(
3131
file_path,
3232
prefix=self._OPERATION_METADATA.open_file_error_prefix,
33+
default_prefix="Failed to open extension file at path",
3334
),
3435
) as extension_file:
3536
return await create_extension_resource_async(

hyperbrowser/client/managers/session_upload_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def open_upload_files_from_input(
8484
open_error_message=build_open_file_error_message(
8585
file_path,
8686
prefix=SESSION_OPERATION_METADATA.upload_open_file_error_prefix,
87+
default_prefix="Failed to open upload file at path",
8788
),
8889
) as opened_file:
8990
yield {"file": opened_file}

hyperbrowser/client/managers/sync_manager/extension.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def create(self, params: CreateExtensionParams) -> ExtensionResponse:
3030
open_error_message=build_open_file_error_message(
3131
file_path,
3232
prefix=self._OPERATION_METADATA.open_file_error_prefix,
33+
default_prefix="Failed to open extension file at path",
3334
),
3435
) as extension_file:
3536
return create_extension_resource(

tests/test_extension_manager.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,53 @@ def _open_binary_file_stub(file_path, *, open_error_message): # type: ignore[no
207207
assert captured["open_error_message"] == "Custom extension open prefix: bad?path.zip"
208208

209209

210+
def test_sync_extension_create_uses_default_open_file_prefix_when_metadata_invalid(
211+
monkeypatch: pytest.MonkeyPatch,
212+
):
213+
manager = SyncExtensionManager(_FakeClient(_SyncTransport()))
214+
manager._OPERATION_METADATA = type(
215+
"_Metadata",
216+
(),
217+
{
218+
"create_operation_name": "create extension",
219+
"open_file_error_prefix": 123,
220+
},
221+
)()
222+
params = CreateExtensionParams(name="my-extension", file_path="/tmp/ignored.zip")
223+
captured: dict[str, str] = {}
224+
225+
@contextmanager
226+
def _open_binary_file_stub(file_path, *, open_error_message): # type: ignore[no-untyped-def]
227+
captured["file_path"] = file_path
228+
captured["open_error_message"] = open_error_message
229+
yield io.BytesIO(b"content")
230+
231+
monkeypatch.setattr(
232+
sync_extension_module,
233+
"normalize_extension_create_input",
234+
lambda _: ("bad\tpath.zip", {"name": "my-extension"}),
235+
)
236+
monkeypatch.setattr(
237+
sync_extension_module,
238+
"open_binary_file",
239+
_open_binary_file_stub,
240+
)
241+
monkeypatch.setattr(
242+
sync_extension_module,
243+
"create_extension_resource",
244+
lambda **kwargs: SimpleNamespace(id="ext_sync_mock"),
245+
)
246+
247+
response = manager.create(params)
248+
249+
assert response.id == "ext_sync_mock"
250+
assert captured["file_path"] == "bad\tpath.zip"
251+
assert (
252+
captured["open_error_message"]
253+
== "Failed to open extension file at path: bad?path.zip"
254+
)
255+
256+
210257
def test_async_extension_create_does_not_mutate_params_and_closes_file(tmp_path):
211258
transport = _AsyncTransport()
212259
manager = AsyncExtensionManager(_FakeClient(transport))
@@ -323,6 +370,60 @@ async def run():
323370
assert captured["open_error_message"] == "Custom extension open prefix: bad?path.zip"
324371

325372

373+
def test_async_extension_create_uses_default_open_file_prefix_when_metadata_invalid(
374+
monkeypatch: pytest.MonkeyPatch,
375+
):
376+
manager = AsyncExtensionManager(_FakeClient(_AsyncTransport()))
377+
manager._OPERATION_METADATA = type(
378+
"_Metadata",
379+
(),
380+
{
381+
"create_operation_name": "create extension",
382+
"open_file_error_prefix": 123,
383+
},
384+
)()
385+
params = CreateExtensionParams(name="my-extension", file_path="/tmp/ignored.zip")
386+
captured: dict[str, str] = {}
387+
388+
@contextmanager
389+
def _open_binary_file_stub(file_path, *, open_error_message): # type: ignore[no-untyped-def]
390+
captured["file_path"] = file_path
391+
captured["open_error_message"] = open_error_message
392+
yield io.BytesIO(b"content")
393+
394+
async def _create_extension_resource_async_stub(**kwargs):
395+
_ = kwargs
396+
return SimpleNamespace(id="ext_async_mock")
397+
398+
monkeypatch.setattr(
399+
async_extension_module,
400+
"normalize_extension_create_input",
401+
lambda _: ("bad\tpath.zip", {"name": "my-extension"}),
402+
)
403+
monkeypatch.setattr(
404+
async_extension_module,
405+
"open_binary_file",
406+
_open_binary_file_stub,
407+
)
408+
monkeypatch.setattr(
409+
async_extension_module,
410+
"create_extension_resource_async",
411+
_create_extension_resource_async_stub,
412+
)
413+
414+
async def run():
415+
return await manager.create(params)
416+
417+
response = asyncio.run(run())
418+
419+
assert response.id == "ext_async_mock"
420+
assert captured["file_path"] == "bad\tpath.zip"
421+
assert (
422+
captured["open_error_message"]
423+
== "Failed to open extension file at path: bad?path.zip"
424+
)
425+
426+
326427
def test_sync_extension_create_raises_hyperbrowser_error_when_file_missing(tmp_path):
327428
transport = _SyncTransport()
328429
manager = SyncExtensionManager(_FakeClient(transport))

tests/test_extension_operation_metadata_usage.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from pathlib import Path
23

34
import pytest
@@ -19,4 +20,10 @@ def test_extension_managers_use_shared_operation_metadata():
1920
assert "operation_name=self._OPERATION_METADATA." in module_text
2021
assert "prefix=self._OPERATION_METADATA.open_file_error_prefix" in module_text
2122
assert 'operation_name="' not in module_text
22-
assert 'prefix="Failed to open extension file at path"' not in module_text
23+
assert (
24+
re.search(
25+
r'(?<!default_)prefix="Failed to open extension file at path"',
26+
module_text,
27+
)
28+
is None
29+
)

tests/test_file_open_error_helper_usage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ def test_file_open_error_messages_use_shared_helper():
1616
for module_path in OPEN_ERROR_HELPER_MODULES:
1717
module_text = Path(module_path).read_text(encoding="utf-8")
1818
assert "build_open_file_error_message(" in module_text
19+
assert "default_prefix=" in module_text
1920
assert "open_error_message=f\"" not in module_text

tests/test_file_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,16 @@ def test_build_open_file_error_message_uses_default_prefix_for_blank_string():
399399
assert message == "Failed to open file at path: /tmp/path.txt"
400400

401401

402+
def test_build_open_file_error_message_uses_explicit_default_prefix_when_prefix_invalid():
403+
message = build_open_file_error_message(
404+
"/tmp/path.txt",
405+
prefix=123, # type: ignore[arg-type]
406+
default_prefix="Failed to open upload file at path",
407+
)
408+
409+
assert message == "Failed to open upload file at path: /tmp/path.txt"
410+
411+
402412
def test_build_open_file_error_message_sanitizes_control_chars_in_prefix():
403413
message = build_open_file_error_message(
404414
"/tmp/path.txt",

tests/test_session_upload_utils.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,48 @@ def _open_binary_file_stub(file_path, *, open_error_message): # type: ignore[no
304304
assert captured["open_error_message"] == "Custom open prefix: bad?path.txt"
305305

306306

307+
def test_open_upload_files_from_input_uses_default_open_prefix_when_metadata_invalid(
308+
monkeypatch: pytest.MonkeyPatch,
309+
):
310+
captured: dict[str, str] = {}
311+
312+
@contextmanager
313+
def _open_binary_file_stub(file_path, *, open_error_message): # type: ignore[no-untyped-def]
314+
captured["file_path"] = file_path
315+
captured["open_error_message"] = open_error_message
316+
yield io.BytesIO(b"content")
317+
318+
monkeypatch.setattr(
319+
session_upload_utils,
320+
"normalize_upload_file_input",
321+
lambda file_input: ("bad\tpath.txt", None),
322+
)
323+
monkeypatch.setattr(
324+
session_upload_utils,
325+
"SESSION_OPERATION_METADATA",
326+
type(
327+
"_Metadata",
328+
(),
329+
{
330+
"upload_missing_file_message_prefix": "Custom missing prefix",
331+
"upload_not_file_message_prefix": "Custom not-file prefix",
332+
"upload_open_file_error_prefix": 123,
333+
},
334+
)(),
335+
)
336+
monkeypatch.setattr(
337+
session_upload_utils,
338+
"open_binary_file",
339+
_open_binary_file_stub,
340+
)
341+
342+
with open_upload_files_from_input("ignored-input") as files:
343+
assert files["file"].read() == b"content"
344+
345+
assert captured["file_path"] == "bad\tpath.txt"
346+
assert captured["open_error_message"] == "Failed to open upload file at path: bad?path.txt"
347+
348+
307349
def test_open_upload_files_from_input_rejects_missing_normalized_file_object(
308350
monkeypatch: pytest.MonkeyPatch,
309351
):

0 commit comments

Comments
 (0)