11import asyncio
2+ from contextlib import contextmanager
3+ import io
24from pathlib import Path
5+ from types import SimpleNamespace
36import pytest
47
8+ import hyperbrowser .client .managers .async_manager .extension as async_extension_module
9+ import hyperbrowser .client .managers .sync_manager .extension as sync_extension_module
510from hyperbrowser .client .managers .async_manager .extension import (
611 ExtensionManager as AsyncExtensionManager ,
712)
@@ -119,6 +124,45 @@ def test_sync_extension_create_does_not_mutate_params_and_closes_file(tmp_path):
119124 assert transport .received_data == {"name" : "my-extension" }
120125
121126
127+ def test_sync_extension_create_uses_sanitized_open_error_message (
128+ monkeypatch : pytest .MonkeyPatch ,
129+ ):
130+ manager = SyncExtensionManager (_FakeClient (_SyncTransport ()))
131+ params = CreateExtensionParams (name = "my-extension" , file_path = "/tmp/ignored.zip" )
132+ captured : dict [str , str ] = {}
133+
134+ @contextmanager
135+ def _open_binary_file_stub (file_path , * , open_error_message ): # type: ignore[no-untyped-def]
136+ captured ["file_path" ] = file_path
137+ captured ["open_error_message" ] = open_error_message
138+ yield io .BytesIO (b"content" )
139+
140+ monkeypatch .setattr (
141+ sync_extension_module ,
142+ "normalize_extension_create_input" ,
143+ lambda _ : ("bad\t path.zip" , {"name" : "my-extension" }),
144+ )
145+ monkeypatch .setattr (
146+ sync_extension_module ,
147+ "open_binary_file" ,
148+ _open_binary_file_stub ,
149+ )
150+ monkeypatch .setattr (
151+ sync_extension_module ,
152+ "create_extension_resource" ,
153+ lambda ** kwargs : SimpleNamespace (id = "ext_sync_mock" ),
154+ )
155+
156+ response = manager .create (params )
157+
158+ assert response .id == "ext_sync_mock"
159+ assert captured ["file_path" ] == "bad\t path.zip"
160+ assert (
161+ captured ["open_error_message" ]
162+ == "Failed to open extension file at path: bad?path.zip"
163+ )
164+
165+
122166def test_async_extension_create_does_not_mutate_params_and_closes_file (tmp_path ):
123167 transport = _AsyncTransport ()
124168 manager = AsyncExtensionManager (_FakeClient (transport ))
@@ -138,6 +182,52 @@ async def run():
138182 assert transport .received_data == {"name" : "my-extension" }
139183
140184
185+ def test_async_extension_create_uses_sanitized_open_error_message (
186+ monkeypatch : pytest .MonkeyPatch ,
187+ ):
188+ manager = AsyncExtensionManager (_FakeClient (_AsyncTransport ()))
189+ params = CreateExtensionParams (name = "my-extension" , file_path = "/tmp/ignored.zip" )
190+ captured : dict [str , str ] = {}
191+
192+ @contextmanager
193+ def _open_binary_file_stub (file_path , * , open_error_message ): # type: ignore[no-untyped-def]
194+ captured ["file_path" ] = file_path
195+ captured ["open_error_message" ] = open_error_message
196+ yield io .BytesIO (b"content" )
197+
198+ async def _create_extension_resource_async_stub (** kwargs ):
199+ _ = kwargs
200+ return SimpleNamespace (id = "ext_async_mock" )
201+
202+ monkeypatch .setattr (
203+ async_extension_module ,
204+ "normalize_extension_create_input" ,
205+ lambda _ : ("bad\t path.zip" , {"name" : "my-extension" }),
206+ )
207+ monkeypatch .setattr (
208+ async_extension_module ,
209+ "open_binary_file" ,
210+ _open_binary_file_stub ,
211+ )
212+ monkeypatch .setattr (
213+ async_extension_module ,
214+ "create_extension_resource_async" ,
215+ _create_extension_resource_async_stub ,
216+ )
217+
218+ async def run ():
219+ return await manager .create (params )
220+
221+ response = asyncio .run (run ())
222+
223+ assert response .id == "ext_async_mock"
224+ assert captured ["file_path" ] == "bad\t path.zip"
225+ assert (
226+ captured ["open_error_message" ]
227+ == "Failed to open extension file at path: bad?path.zip"
228+ )
229+
230+
141231def test_sync_extension_create_raises_hyperbrowser_error_when_file_missing (tmp_path ):
142232 transport = _SyncTransport ()
143233 manager = SyncExtensionManager (_FakeClient (transport ))
0 commit comments