From 63ce3f4d745117498c0f629b200790fa897c2ece Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Sun, 1 Nov 2026 03:37:38 +0530 Subject: [PATCH 1/6] fix(serialization-text): preserve empty string and bytes values and handle malformed inputs Allow empty strings and byte arrays to be serialized and deserialized in TextSerializationWriter and TextParseNode. Prevent silent duplicate value overwrites when the existing writer value is empty, and safely return None for malformed scalar primitives, invalid base64 bytes, and unknown enum members. Signed-off-by: Hardik Kaurani --- .../text_parse_node.py | 50 ++++++++++++++----- .../text_serialization_writer.py | 8 +-- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/serialization/text/kiota_serialization_text/text_parse_node.py b/packages/serialization/text/kiota_serialization_text/text_parse_node.py index 3b1f3cb7..b02c9c3c 100644 --- a/packages/serialization/text/kiota_serialization_text/text_parse_node.py +++ b/packages/serialization/text/kiota_serialization_text/text_parse_node.py @@ -8,7 +8,9 @@ from uuid import UUID from kiota_abstractions.date_utils import ( - parse_timedelta_string, datetime_from_iso_format_compat, time_from_iso_format_compat + datetime_from_iso_format_compat, + parse_timedelta_string, + time_from_iso_format_compat, ) from kiota_abstractions.serialization import Parsable, ParsableFactory, ParseNode @@ -40,7 +42,7 @@ def get_str_value(self) -> Optional[str]: Returns: str: The string value of the node """ - if self._text: + if self._text is not None: return str(self._text) return None @@ -68,7 +70,10 @@ def get_int_value(self) -> Optional[int]: int: The integer value of the node """ if self._text: - return int(self._text) + try: + return int(self._text) + except ValueError: + return None return None def get_float_value(self) -> Optional[float]: @@ -77,7 +82,10 @@ def get_float_value(self) -> Optional[float]: float: The floating point value of the node """ if self._text: - return float(self._text) + try: + return float(self._text) + except ValueError: + return None return None def get_uuid_value(self) -> Optional[UUID]: @@ -86,7 +94,10 @@ def get_uuid_value(self) -> Optional[UUID]: UUID: The UUID value of the node """ if self._text: - return UUID(self._text) + try: + return UUID(self._text) + except ValueError: + return None return None def get_datetime_value(self) -> Optional[datetime]: @@ -96,7 +107,10 @@ def get_datetime_value(self) -> Optional[datetime]: """ datetime_str = self.get_str_value() if datetime_str: - return datetime_from_iso_format_compat(datetime_str) + try: + return datetime_from_iso_format_compat(datetime_str) + except (ValueError, TypeError): + return None return None def get_timedelta_value(self) -> Optional[timedelta]: @@ -106,7 +120,10 @@ def get_timedelta_value(self) -> Optional[timedelta]: """ datetime_str = self.get_str_value() if datetime_str: - return parse_timedelta_string(datetime_str) + try: + return parse_timedelta_string(datetime_str) + except (ValueError, TypeError): + return None return None def get_date_value(self) -> Optional[date]: @@ -116,7 +133,10 @@ def get_date_value(self) -> Optional[date]: """ datetime_str = self.get_str_value() if datetime_str: - return date.fromisoformat(datetime_str) + try: + return date.fromisoformat(datetime_str) + except (ValueError, TypeError): + return None return None def get_time_value(self) -> Optional[time]: @@ -126,7 +146,10 @@ def get_time_value(self) -> Optional[time]: """ datetime_str = self.get_str_value() if datetime_str: - return time_from_iso_format_compat(datetime_str) + try: + return time_from_iso_format_compat(datetime_str) + except (ValueError, TypeError): + return None return None def get_collection_of_primitive_values(self, primitive_type: type[T]) -> list[T]: @@ -169,7 +192,7 @@ def get_enum_value(self, enum_class: K) -> Optional[K]: try: return enum_class[camel_case_key] # type: ignore except KeyError: - raise Exception(f'Invalid key: {camel_case_key} for enum {enum_class}.') + return None return None def get_object_value(self, factory: ParsableFactory[U]) -> U: @@ -187,8 +210,11 @@ def get_bytes_value(self) -> Optional[bytes]: base64_string = self.get_str_value() if not base64_string: return None - base64_bytes = base64_string.encode("utf-8") - return base64.b64decode(base64_bytes) + try: + base64_bytes = base64_string.encode("utf-8") + return base64.b64decode(base64_bytes) + except Exception: + return None def get_on_before_assign_field_values(self) -> Optional[Callable[[Parsable], None]]: """Gets the callback called before the node is deserialized. diff --git a/packages/serialization/text/kiota_serialization_text/text_serialization_writer.py b/packages/serialization/text/kiota_serialization_text/text_serialization_writer.py index a200080f..2d5deba7 100644 --- a/packages/serialization/text/kiota_serialization_text/text_serialization_writer.py +++ b/packages/serialization/text/kiota_serialization_text/text_serialization_writer.py @@ -34,7 +34,7 @@ def write_str_value(self, key: Optional[str], value: Optional[str]) -> None: if isinstance(value, str): if key: raise Exception(self.NO_STRUCTURED_DATA_MESSAGE) - if self._writer: + if self._writer is not None: raise Exception( 'A value was already written for this serialization writer,' 'text content only supports a single value' @@ -152,9 +152,9 @@ def write_bytes_value(self, key: Optional[str], value: Optional[bytes]) -> None: key (Optional[str]): The key to be used for the written value. May be null. value (bytes): The byte array to be written. """ - if key and value: + if key and value is not None: raise Exception(self.NO_STRUCTURED_DATA_MESSAGE) - if value and not key: + if value is not None and not key: base64_bytes = base64.b64encode(value) base64_string = base64_bytes.decode('utf-8') self.write_str_value(key, base64_string) @@ -201,7 +201,7 @@ def get_serialized_content(self) -> bytes: """ text_string = self._writer self._writer = None - if text_string: + if text_string is not None: stream = text_string.encode('utf-8') return stream From 246e70c093e37c35b682cf7b8df02dc74eb888a1 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Sun, 1 Nov 2026 03:39:12 +0530 Subject: [PATCH 2/6] test(serialization-text): add unit tests for empty content and malformed scalars Add unit tests verifying serialization and deserialization of empty strings and empty bytes, assertion of duplicate write prevention on empty string, and safe None returns on malformed scalar primitives and unknown enums. Signed-off-by: Hardik Kaurani --- .../text/tests/unit/test_text_parse_node.py | 50 +++++++++++++++++++ .../unit/test_text_serialization_writer.py | 29 +++++++++++ 2 files changed, 79 insertions(+) diff --git a/packages/serialization/text/tests/unit/test_text_parse_node.py b/packages/serialization/text/tests/unit/test_text_parse_node.py index 0480261b..087df9a7 100644 --- a/packages/serialization/text/tests/unit/test_text_parse_node.py +++ b/packages/serialization/text/tests/unit/test_text_parse_node.py @@ -135,3 +135,53 @@ def test_get_collection_of_object_values(sample_structured_data): with pytest.raises(Exception) as e_info: parse_node = TextParseNode(sample_structured_data) result = parse_node.get_collection_of_object_values(User) + + +def test_get_empty_str_value(): + parse_node = TextParseNode("") + assert parse_node.get_str_value() == "" + + +def test_get_bytes_value_invalid_base64_returns_none(): + parse_node = TextParseNode("not-base64!!!") + assert parse_node.get_bytes_value() is None + + +def test_get_int_value_invalid_returns_none(): + parse_node = TextParseNode("not-an-int") + assert parse_node.get_int_value() is None + + +def test_get_float_value_invalid_returns_none(): + parse_node = TextParseNode("not-a-float") + assert parse_node.get_float_value() is None + + +def test_get_uuid_value_invalid_returns_none(): + parse_node = TextParseNode("not-a-uuid") + assert parse_node.get_uuid_value() is None + + +def test_get_datetime_value_invalid_returns_none(): + parse_node = TextParseNode("not-a-datetime") + assert parse_node.get_datetime_value() is None + + +def test_get_timedelta_value_invalid_returns_none(): + parse_node = TextParseNode("not-a-timedelta") + assert parse_node.get_timedelta_value() is None + + +def test_get_date_value_invalid_returns_none(): + parse_node = TextParseNode("not-a-date") + assert parse_node.get_date_value() is None + + +def test_get_time_value_invalid_returns_none(): + parse_node = TextParseNode("not-a-time") + assert parse_node.get_time_value() is None + + +def test_get_enum_value_unknown_returns_none(): + parse_node = TextParseNode("unknown_location") + assert parse_node.get_enum_value(OfficeLocation) is None diff --git a/packages/serialization/text/tests/unit/test_text_serialization_writer.py b/packages/serialization/text/tests/unit/test_text_serialization_writer.py index b54efd59..dc46c900 100644 --- a/packages/serialization/text/tests/unit/test_text_serialization_writer.py +++ b/packages/serialization/text/tests/unit/test_text_serialization_writer.py @@ -249,3 +249,32 @@ def test_write_additional_data_value(): "businessPhones": ["+1 205 555 0108"], } ) + + +def test_write_empty_str_value(): + text_serialization_writer = TextSerializationWriter() + text_serialization_writer.write_str_value(None, "") + content = text_serialization_writer.get_serialized_content() + assert content == b"" + + +def test_write_empty_str_value_duplicate_fails(): + text_serialization_writer = TextSerializationWriter() + text_serialization_writer.write_str_value(None, "") + with pytest.raises(Exception) as exc_info: + text_serialization_writer.write_str_value(None, "second") + assert "A value was already written" in str(exc_info.value) + + +def test_write_empty_bytes_value(): + text_serialization_writer = TextSerializationWriter() + text_serialization_writer.write_bytes_value(None, b"") + content = text_serialization_writer.get_serialized_content() + assert content == b"" + + +def test_write_empty_bytes_value_with_key(): + text_serialization_writer = TextSerializationWriter() + with pytest.raises(Exception) as exc_info: + text_serialization_writer.write_bytes_value("key", b"") + assert "Text does not support structured data" in str(exc_info.value) From 8631a405e4dea20a66efd7fb152a1907fbb8d710 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Mon, 2 Nov 2026 19:06:46 +0530 Subject: [PATCH 3/6] fix(serialization): allow empty content in factory guards and empty bytes deserialization Signed-off-by: Hardik Kaurani --- .../serialization/parse_node_factory_registry.py | 2 +- .../text/kiota_serialization_text/text_parse_node.py | 2 +- .../text/kiota_serialization_text/text_parse_node_factory.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/abstractions/kiota_abstractions/serialization/parse_node_factory_registry.py b/packages/abstractions/kiota_abstractions/serialization/parse_node_factory_registry.py index 563f04b4..5fb99009 100644 --- a/packages/abstractions/kiota_abstractions/serialization/parse_node_factory_registry.py +++ b/packages/abstractions/kiota_abstractions/serialization/parse_node_factory_registry.py @@ -33,7 +33,7 @@ def get_valid_content_type(self) -> str: def get_root_parse_node(self, content_type: str, content: bytes) -> ParseNode: if not content_type: raise Exception("Content type cannot be null") - if not content: + if content is None: raise Exception("Content cannot be null") vendor_specific_content_type = content_type.split(';')[0] diff --git a/packages/serialization/text/kiota_serialization_text/text_parse_node.py b/packages/serialization/text/kiota_serialization_text/text_parse_node.py index b02c9c3c..f8c24ff6 100644 --- a/packages/serialization/text/kiota_serialization_text/text_parse_node.py +++ b/packages/serialization/text/kiota_serialization_text/text_parse_node.py @@ -208,7 +208,7 @@ def get_bytes_value(self) -> Optional[bytes]: bytes: The bytes value from the node """ base64_string = self.get_str_value() - if not base64_string: + if base64_string is None: return None try: base64_bytes = base64_string.encode("utf-8") diff --git a/packages/serialization/text/kiota_serialization_text/text_parse_node_factory.py b/packages/serialization/text/kiota_serialization_text/text_parse_node_factory.py index 39db6d66..be6ad43b 100644 --- a/packages/serialization/text/kiota_serialization_text/text_parse_node_factory.py +++ b/packages/serialization/text/kiota_serialization_text/text_parse_node_factory.py @@ -28,7 +28,7 @@ def get_root_parse_node(self, content_type: str, content: bytes) -> ParseNode: if valid_content_type.casefold() != content_type.casefold(): raise TypeError(f"Expected {valid_content_type} as content type") - if not content: + if content is None: raise TypeError("Content cannot be null") content_as_str = content.decode('utf-8') From 9d1d7de44b95cdc7eac20d292373aa0176610741 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Mon, 2 Nov 2026 19:06:51 +0530 Subject: [PATCH 4/6] test(serialization): add tests for empty content factory dispatch and empty bytes round-trip Signed-off-by: Hardik Kaurani --- .../tests/test_parse_node_factory_registry.py | 38 +++++++++++++++++++ .../text/tests/unit/test_text_parse_node.py | 10 +++++ .../unit/test_text_parse_node_factory.py | 33 ++++++++++++++-- 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 packages/abstractions/tests/test_parse_node_factory_registry.py diff --git a/packages/abstractions/tests/test_parse_node_factory_registry.py b/packages/abstractions/tests/test_parse_node_factory_registry.py new file mode 100644 index 00000000..f34e9d36 --- /dev/null +++ b/packages/abstractions/tests/test_parse_node_factory_registry.py @@ -0,0 +1,38 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All Rights Reserved. +# Licensed under the MIT License. +# See License in the project root for license information. +# ------------------------------------------------------------------------------ + +from unittest.mock import Mock + +import pytest + +from kiota_abstractions.serialization import ParseNode, ParseNodeFactory, ParseNodeFactoryRegistry + + +def test_registry_dispatches_empty_content(): + registry = ParseNodeFactoryRegistry() + mock_node = Mock(spec=ParseNode) + mock_factory = Mock(spec=ParseNodeFactory) + mock_factory.get_root_parse_node.return_value = mock_node + + registry.CONTENT_TYPE_ASSOCIATED_FACTORIES["text/plain"] = mock_factory + + result = registry.get_root_parse_node("text/plain", b"") + assert result is mock_node + mock_factory.get_root_parse_node.assert_called_once_with("text/plain", b"") + + +def test_registry_raises_on_none_content(): + registry = ParseNodeFactoryRegistry() + with pytest.raises(Exception) as exc_info: + registry.get_root_parse_node("text/plain", None) + assert "Content cannot be null" in str(exc_info.value) + + +def test_registry_raises_on_empty_content_type(): + registry = ParseNodeFactoryRegistry() + with pytest.raises(Exception) as exc_info: + registry.get_root_parse_node("", b"hello") + assert "Content type cannot be null" in str(exc_info.value) diff --git a/packages/serialization/text/tests/unit/test_text_parse_node.py b/packages/serialization/text/tests/unit/test_text_parse_node.py index 087df9a7..df249a36 100644 --- a/packages/serialization/text/tests/unit/test_text_parse_node.py +++ b/packages/serialization/text/tests/unit/test_text_parse_node.py @@ -142,6 +142,16 @@ def test_get_empty_str_value(): assert parse_node.get_str_value() == "" +def test_get_empty_bytes_value(): + parse_node = TextParseNode("") + assert parse_node.get_bytes_value() == b"" + + +def test_get_bytes_value_none(): + parse_node = TextParseNode(None) + assert parse_node.get_bytes_value() is None + + def test_get_bytes_value_invalid_base64_returns_none(): parse_node = TextParseNode("not-base64!!!") assert parse_node.get_bytes_value() is None diff --git a/packages/serialization/text/tests/unit/test_text_parse_node_factory.py b/packages/serialization/text/tests/unit/test_text_parse_node_factory.py index ad3ea5f6..a5c5f23d 100644 --- a/packages/serialization/text/tests/unit/test_text_parse_node_factory.py +++ b/packages/serialization/text/tests/unit/test_text_parse_node_factory.py @@ -2,6 +2,7 @@ from kiota_serialization_text.text_parse_node import TextParseNode from kiota_serialization_text.text_parse_node_factory import TextParseNodeFactory +from kiota_serialization_text.text_serialization_writer import TextSerializationWriter @pytest.fixture @@ -31,13 +32,39 @@ def test_get_root_parse_node_unsupported_content_type(sample_text_string): def test_get_root_parse_node_empty_text(): - with pytest.raises(TypeError) as e_info: + factory = TextParseNodeFactory() + sample_string_bytes = ''.encode('utf-8') + root = factory.get_root_parse_node('text/plain', sample_string_bytes) + assert isinstance(root, TextParseNode) + assert root.get_str_value() == '' + assert root.get_bytes_value() == b'' + + +def test_get_root_parse_node_none_content(): + with pytest.raises(TypeError): factory = TextParseNodeFactory() - sample_string_bytes = ''.encode('utf-8') - root = factory.get_root_parse_node('text/plain', sample_string_bytes) + factory.get_root_parse_node('text/plain', None) def test_get_valid_content_type(): factory = TextParseNodeFactory() content_type = factory.get_valid_content_type() assert content_type == 'text/plain' + + +def test_empty_string_round_trip(): + writer = TextSerializationWriter() + writer.write_str_value(None, '') + content = writer.get_serialized_content() + factory = TextParseNodeFactory() + node = factory.get_root_parse_node('text/plain', content) + assert node.get_str_value() == '' + + +def test_empty_bytes_round_trip(): + writer = TextSerializationWriter() + writer.write_bytes_value(None, b'') + content = writer.get_serialized_content() + factory = TextParseNodeFactory() + node = factory.get_root_parse_node('text/plain', content) + assert node.get_bytes_value() == b'' From 03686347d4ecedcae1b412de415f676249875efe Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Mon, 2 Nov 2026 20:31:55 +0530 Subject: [PATCH 5/6] fix(serialization-text): validate base64 decoding and isolate factory registry test Signed-off-by: Hardik Kaurani --- .../abstractions/tests/test_parse_node_factory_registry.py | 4 ++-- .../text/kiota_serialization_text/text_parse_node.py | 2 +- .../serialization/text/tests/unit/test_text_parse_node.py | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/abstractions/tests/test_parse_node_factory_registry.py b/packages/abstractions/tests/test_parse_node_factory_registry.py index f34e9d36..60c92d72 100644 --- a/packages/abstractions/tests/test_parse_node_factory_registry.py +++ b/packages/abstractions/tests/test_parse_node_factory_registry.py @@ -11,13 +11,13 @@ from kiota_abstractions.serialization import ParseNode, ParseNodeFactory, ParseNodeFactoryRegistry -def test_registry_dispatches_empty_content(): +def test_registry_dispatches_empty_content(monkeypatch): registry = ParseNodeFactoryRegistry() mock_node = Mock(spec=ParseNode) mock_factory = Mock(spec=ParseNodeFactory) mock_factory.get_root_parse_node.return_value = mock_node - registry.CONTENT_TYPE_ASSOCIATED_FACTORIES["text/plain"] = mock_factory + monkeypatch.setitem(registry.CONTENT_TYPE_ASSOCIATED_FACTORIES, "text/plain", mock_factory) result = registry.get_root_parse_node("text/plain", b"") assert result is mock_node diff --git a/packages/serialization/text/kiota_serialization_text/text_parse_node.py b/packages/serialization/text/kiota_serialization_text/text_parse_node.py index f8c24ff6..04713620 100644 --- a/packages/serialization/text/kiota_serialization_text/text_parse_node.py +++ b/packages/serialization/text/kiota_serialization_text/text_parse_node.py @@ -212,7 +212,7 @@ def get_bytes_value(self) -> Optional[bytes]: return None try: base64_bytes = base64_string.encode("utf-8") - return base64.b64decode(base64_bytes) + return base64.b64decode(base64_bytes, validate=True) except Exception: return None diff --git a/packages/serialization/text/tests/unit/test_text_parse_node.py b/packages/serialization/text/tests/unit/test_text_parse_node.py index df249a36..a631939b 100644 --- a/packages/serialization/text/tests/unit/test_text_parse_node.py +++ b/packages/serialization/text/tests/unit/test_text_parse_node.py @@ -4,7 +4,6 @@ from uuid import UUID import pytest - from kiota_serialization_text.text_parse_node import TextParseNode from ..helpers import OfficeLocation, User @@ -116,6 +115,7 @@ def test_get_enum_value(): def test_get_enum_value_none_member(): + class RiskLevel(Enum): Low = "low" None_ = "none" @@ -152,8 +152,9 @@ def test_get_bytes_value_none(): assert parse_node.get_bytes_value() is None -def test_get_bytes_value_invalid_base64_returns_none(): - parse_node = TextParseNode("not-base64!!!") +@pytest.mark.parametrize("invalid_b64", ["not-base64!!!", "!!!!", "T!Q=="]) +def test_get_bytes_value_invalid_base64_returns_none(invalid_b64): + parse_node = TextParseNode(invalid_b64) assert parse_node.get_bytes_value() is None From b90a04381de11b87b6b3763ff027d689c65420cd Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Tue, 3 Nov 2026 01:20:06 +0530 Subject: [PATCH 6/6] fix(serialization-text): propagate errors on malformed scalar parsing Do not swallow parsing errors behind None for malformed primitives, invalid base64 payloads, or unknown enum keys in TextParseNode. Return None only when the underlying text is None or empty, allowing unexpected payloads to raise descriptive errors. Signed-off-by: Hardik Kaurani --- .../text_parse_node.py | 44 +++--------- .../text/tests/unit/test_text_parse_node.py | 72 ++++++++++++++----- 2 files changed, 63 insertions(+), 53 deletions(-) diff --git a/packages/serialization/text/kiota_serialization_text/text_parse_node.py b/packages/serialization/text/kiota_serialization_text/text_parse_node.py index 04713620..a5af45c2 100644 --- a/packages/serialization/text/kiota_serialization_text/text_parse_node.py +++ b/packages/serialization/text/kiota_serialization_text/text_parse_node.py @@ -70,10 +70,7 @@ def get_int_value(self) -> Optional[int]: int: The integer value of the node """ if self._text: - try: - return int(self._text) - except ValueError: - return None + return int(self._text) return None def get_float_value(self) -> Optional[float]: @@ -82,10 +79,7 @@ def get_float_value(self) -> Optional[float]: float: The floating point value of the node """ if self._text: - try: - return float(self._text) - except ValueError: - return None + return float(self._text) return None def get_uuid_value(self) -> Optional[UUID]: @@ -94,10 +88,7 @@ def get_uuid_value(self) -> Optional[UUID]: UUID: The UUID value of the node """ if self._text: - try: - return UUID(self._text) - except ValueError: - return None + return UUID(self._text) return None def get_datetime_value(self) -> Optional[datetime]: @@ -107,10 +98,7 @@ def get_datetime_value(self) -> Optional[datetime]: """ datetime_str = self.get_str_value() if datetime_str: - try: - return datetime_from_iso_format_compat(datetime_str) - except (ValueError, TypeError): - return None + return datetime_from_iso_format_compat(datetime_str) return None def get_timedelta_value(self) -> Optional[timedelta]: @@ -120,10 +108,7 @@ def get_timedelta_value(self) -> Optional[timedelta]: """ datetime_str = self.get_str_value() if datetime_str: - try: - return parse_timedelta_string(datetime_str) - except (ValueError, TypeError): - return None + return parse_timedelta_string(datetime_str) return None def get_date_value(self) -> Optional[date]: @@ -133,10 +118,7 @@ def get_date_value(self) -> Optional[date]: """ datetime_str = self.get_str_value() if datetime_str: - try: - return date.fromisoformat(datetime_str) - except (ValueError, TypeError): - return None + return date.fromisoformat(datetime_str) return None def get_time_value(self) -> Optional[time]: @@ -146,10 +128,7 @@ def get_time_value(self) -> Optional[time]: """ datetime_str = self.get_str_value() if datetime_str: - try: - return time_from_iso_format_compat(datetime_str) - except (ValueError, TypeError): - return None + return time_from_iso_format_compat(datetime_str) return None def get_collection_of_primitive_values(self, primitive_type: type[T]) -> list[T]: @@ -192,7 +171,7 @@ def get_enum_value(self, enum_class: K) -> Optional[K]: try: return enum_class[camel_case_key] # type: ignore except KeyError: - return None + raise Exception(f'Invalid key: {camel_case_key} for enum {enum_class}.') return None def get_object_value(self, factory: ParsableFactory[U]) -> U: @@ -210,11 +189,8 @@ def get_bytes_value(self) -> Optional[bytes]: base64_string = self.get_str_value() if base64_string is None: return None - try: - base64_bytes = base64_string.encode("utf-8") - return base64.b64decode(base64_bytes, validate=True) - except Exception: - return None + base64_bytes = base64_string.encode("utf-8") + return base64.b64decode(base64_bytes) def get_on_before_assign_field_values(self) -> Optional[Callable[[Parsable], None]]: """Gets the callback called before the node is deserialized. diff --git a/packages/serialization/text/tests/unit/test_text_parse_node.py b/packages/serialization/text/tests/unit/test_text_parse_node.py index a631939b..27b76c32 100644 --- a/packages/serialization/text/tests/unit/test_text_parse_node.py +++ b/packages/serialization/text/tests/unit/test_text_parse_node.py @@ -152,47 +152,81 @@ def test_get_bytes_value_none(): assert parse_node.get_bytes_value() is None -@pytest.mark.parametrize("invalid_b64", ["not-base64!!!", "!!!!", "T!Q=="]) -def test_get_bytes_value_invalid_base64_returns_none(invalid_b64): - parse_node = TextParseNode(invalid_b64) - assert parse_node.get_bytes_value() is None +def test_get_bytes_value_invalid_base64_raises(): + parse_node = TextParseNode("not-base64!!!") + with pytest.raises(Exception): + parse_node.get_bytes_value() -def test_get_int_value_invalid_returns_none(): +def test_get_int_value_invalid_raises(): parse_node = TextParseNode("not-an-int") - assert parse_node.get_int_value() is None + with pytest.raises(ValueError): + parse_node.get_int_value() -def test_get_float_value_invalid_returns_none(): +def test_get_float_value_invalid_raises(): parse_node = TextParseNode("not-a-float") - assert parse_node.get_float_value() is None + with pytest.raises(ValueError): + parse_node.get_float_value() -def test_get_uuid_value_invalid_returns_none(): +def test_get_uuid_value_invalid_raises(): parse_node = TextParseNode("not-a-uuid") - assert parse_node.get_uuid_value() is None + with pytest.raises(ValueError): + parse_node.get_uuid_value() -def test_get_datetime_value_invalid_returns_none(): +def test_get_datetime_value_invalid_raises(): parse_node = TextParseNode("not-a-datetime") - assert parse_node.get_datetime_value() is None + with pytest.raises(ValueError): + parse_node.get_datetime_value() -def test_get_timedelta_value_invalid_returns_none(): +def test_get_timedelta_value_invalid_raises(): parse_node = TextParseNode("not-a-timedelta") - assert parse_node.get_timedelta_value() is None + with pytest.raises(Exception): + parse_node.get_timedelta_value() -def test_get_date_value_invalid_returns_none(): +def test_get_date_value_invalid_raises(): parse_node = TextParseNode("not-a-date") - assert parse_node.get_date_value() is None + with pytest.raises(ValueError): + parse_node.get_date_value() -def test_get_time_value_invalid_returns_none(): +def test_get_time_value_invalid_raises(): parse_node = TextParseNode("not-a-time") - assert parse_node.get_time_value() is None + with pytest.raises(ValueError): + parse_node.get_time_value() -def test_get_enum_value_unknown_returns_none(): +def test_get_enum_value_unknown_raises(): parse_node = TextParseNode("unknown_location") + with pytest.raises(Exception): + parse_node.get_enum_value(OfficeLocation) + + +def test_get_scalars_empty_text_returns_none(): + parse_node = TextParseNode("") + assert parse_node.get_int_value() is None + assert parse_node.get_float_value() is None + assert parse_node.get_uuid_value() is None + assert parse_node.get_datetime_value() is None + assert parse_node.get_timedelta_value() is None + assert parse_node.get_date_value() is None + assert parse_node.get_time_value() is None + assert parse_node.get_enum_value(OfficeLocation) is None + + +def test_get_scalars_none_text_returns_none(): + parse_node = TextParseNode(None) + assert parse_node.get_str_value() is None + assert parse_node.get_bytes_value() is None + assert parse_node.get_int_value() is None + assert parse_node.get_float_value() is None + assert parse_node.get_uuid_value() is None + assert parse_node.get_datetime_value() is None + assert parse_node.get_timedelta_value() is None + assert parse_node.get_date_value() is None + assert parse_node.get_time_value() is None assert parse_node.get_enum_value(OfficeLocation) is None