Skip to content

Commit 0432052

Browse files
Enforce concrete key types in shared response parsers
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent ad5b876 commit 0432052

File tree

8 files changed

+79
-18
lines changed

8 files changed

+79
-18
lines changed

hyperbrowser/client/managers/extension_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def parse_extension_list_response_data(response_data: Any) -> List[ExtensionResp
125125
original_error=exc,
126126
) from exc
127127
for key in extension_keys:
128-
if isinstance(key, str):
128+
if type(key) is str:
129129
continue
130130
raise HyperbrowserError(
131131
f"Expected extension object keys to be strings at index {index}"

hyperbrowser/client/managers/response_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def parse_response_model(
8989
original_error=exc,
9090
) from exc
9191
for key in response_keys:
92-
if isinstance(key, str):
92+
if type(key) is str:
9393
continue
9494
raise HyperbrowserError(
9595
f"Expected {normalized_operation_name} response object keys to be strings"

hyperbrowser/client/managers/session_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def parse_session_recordings_response_data(
7676
original_error=exc,
7777
) from exc
7878
for key in recording_keys:
79-
if isinstance(key, str):
79+
if type(key) is str:
8080
continue
8181
raise HyperbrowserError(
8282
f"Expected session recording object keys to be strings at index {index}"

hyperbrowser/transport/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def from_json(
146146
original_error=exc,
147147
) from exc
148148
for key in response_keys:
149-
if isinstance(key, str):
149+
if type(key) is str:
150150
continue
151151
key_type_name = type(key).__name__
152152
raise HyperbrowserError(

tests/test_extension_utils.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,23 @@ def test_parse_extension_list_response_data_rejects_non_string_extension_keys():
321321
)
322322

323323

324+
def test_parse_extension_list_response_data_rejects_string_subclass_extension_keys():
325+
class _Key(str):
326+
pass
327+
328+
with pytest.raises(
329+
HyperbrowserError,
330+
match="Expected extension object keys to be strings at index 0",
331+
):
332+
parse_extension_list_response_data(
333+
{
334+
"extensions": [
335+
{_Key("name"): "invalid-key-type"},
336+
]
337+
}
338+
)
339+
340+
324341
def test_parse_extension_list_response_data_wraps_extension_value_read_failures():
325342
class _BrokenValueLookupMapping(Mapping[str, object]):
326343
def __iter__(self) -> Iterator[str]:
@@ -367,7 +384,7 @@ def __getitem__(self, key: str) -> object:
367384
assert exc_info.value.original_error is not None
368385

369386

370-
def test_parse_extension_list_response_data_falls_back_for_unreadable_value_read_keys():
387+
def test_parse_extension_list_response_data_rejects_string_subclass_value_read_keys():
371388
class _BrokenKey(str):
372389
class _BrokenRenderedKey(str):
373390
def __iter__(self):
@@ -389,13 +406,13 @@ def __getitem__(self, key: str) -> object:
389406

390407
with pytest.raises(
391408
HyperbrowserError,
392-
match="Failed to read extension object value for key '<unprintable _BrokenKey>' at index 0",
409+
match="Expected extension object keys to be strings at index 0",
393410
) as exc_info:
394411
parse_extension_list_response_data(
395412
{"extensions": [_BrokenValueLookupMapping()]}
396413
)
397414

398-
assert exc_info.value.original_error is not None
415+
assert exc_info.value.original_error is None
399416

400417

401418
def test_parse_extension_list_response_data_preserves_hyperbrowser_value_read_errors():

tests/test_response_utils.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,21 @@ def test_parse_response_model_rejects_non_string_keys():
194194
)
195195

196196

197+
def test_parse_response_model_rejects_string_subclass_keys():
198+
class _Key(str):
199+
pass
200+
201+
with pytest.raises(
202+
HyperbrowserError,
203+
match="Expected basic operation response object keys to be strings",
204+
):
205+
parse_response_model(
206+
{_Key("success"): True},
207+
model=BasicResponse,
208+
operation_name="basic operation",
209+
)
210+
211+
197212
def test_parse_response_model_sanitizes_operation_name_in_errors():
198213
with pytest.raises(
199214
HyperbrowserError,
@@ -303,7 +318,7 @@ def test_parse_response_model_truncates_operation_name_in_errors():
303318
)
304319

305320

306-
def test_parse_response_model_falls_back_for_unreadable_key_display():
321+
def test_parse_response_model_rejects_string_subclass_keys_before_value_reads():
307322
class _BrokenKey(str):
308323
def __iter__(self):
309324
raise RuntimeError("key iteration exploded")
@@ -321,15 +336,15 @@ def __getitem__(self, key: str) -> object:
321336

322337
with pytest.raises(
323338
HyperbrowserError,
324-
match="Failed to read basic operation response value for key '<unreadable key>'",
339+
match="Expected basic operation response object keys to be strings",
325340
) as exc_info:
326341
parse_response_model(
327342
_BrokenValueLookupMapping(),
328343
model=BasicResponse,
329344
operation_name="basic operation",
330345
)
331346

332-
assert isinstance(exc_info.value.original_error, RuntimeError)
347+
assert exc_info.value.original_error is None
333348

334349

335350
def test_parse_response_model_wraps_mapping_read_failures():

tests/test_session_recording_utils.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,21 @@ def test_parse_session_recordings_response_data_rejects_non_string_recording_key
237237
)
238238

239239

240+
def test_parse_session_recordings_response_data_rejects_string_subclass_recording_keys():
241+
class _Key(str):
242+
pass
243+
244+
with pytest.raises(
245+
HyperbrowserError,
246+
match="Expected session recording object keys to be strings at index 0",
247+
):
248+
parse_session_recordings_response_data(
249+
[
250+
{_Key("type"): "bad-key"},
251+
]
252+
)
253+
254+
240255
def test_parse_session_recordings_response_data_wraps_recording_value_read_failures():
241256
class _BrokenValueLookupMapping(Mapping[str, object]):
242257
def __iter__(self) -> Iterator[str]:
@@ -282,7 +297,7 @@ def __getitem__(self, key: str) -> object:
282297
assert exc_info.value.original_error is not None
283298

284299

285-
def test_parse_session_recordings_response_data_falls_back_for_unreadable_recording_keys():
300+
def test_parse_session_recordings_response_data_rejects_string_subclass_recording_keys_before_value_reads():
286301
class _BrokenKey(str):
287302
def __iter__(self):
288303
raise RuntimeError("cannot iterate recording key")
@@ -300,14 +315,11 @@ def __getitem__(self, key: str) -> object:
300315

301316
with pytest.raises(
302317
HyperbrowserError,
303-
match=(
304-
"Failed to read session recording object value "
305-
"for key '<unreadable key>' at index 0"
306-
),
318+
match="Expected session recording object keys to be strings at index 0",
307319
) as exc_info:
308320
parse_session_recordings_response_data([_BrokenValueLookupMapping()])
309321

310-
assert exc_info.value.original_error is not None
322+
assert exc_info.value.original_error is None
311323

312324

313325
def test_parse_session_recordings_response_data_preserves_hyperbrowser_value_read_errors():

tests/test_transport_base.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,23 @@ def test_api_response_from_json_rejects_non_string_mapping_keys() -> None:
184184
)
185185

186186

187+
def test_api_response_from_json_rejects_string_subclass_mapping_keys() -> None:
188+
class _Key(str):
189+
pass
190+
191+
with pytest.raises(
192+
HyperbrowserError,
193+
match=(
194+
"Failed to parse response data for _SampleResponseModel: "
195+
"expected string keys but received _Key"
196+
),
197+
):
198+
APIResponse.from_json(
199+
{_Key("name"): "job-1"},
200+
_SampleResponseModel,
201+
)
202+
203+
187204
def test_api_response_from_json_wraps_non_hyperbrowser_errors() -> None:
188205
with pytest.raises(
189206
HyperbrowserError,
@@ -298,14 +315,14 @@ def test_api_response_from_json_sanitizes_and_truncates_mapping_keys_in_errors()
298315
APIResponse.from_json(_BrokenLongKeyValueMapping(), _SampleResponseModel)
299316

300317

301-
def test_api_response_from_json_falls_back_for_unreadable_mapping_keys_in_errors() -> (
318+
def test_api_response_from_json_rejects_string_subclass_mapping_keys_before_value_reads() -> (
302319
None
303320
):
304321
with pytest.raises(
305322
HyperbrowserError,
306323
match=(
307324
"Failed to parse response data for _SampleResponseModel: "
308-
"unable to read value for key '<unreadable key>'"
325+
"expected string keys but received _BrokenRenderedMappingKey"
309326
),
310327
):
311328
APIResponse.from_json(_BrokenRenderedKeyValueMapping(), _SampleResponseModel)

0 commit comments

Comments
 (0)