diff --git a/moto/dynamodb/models/dynamo_type.py b/moto/dynamodb/models/dynamo_type.py index 33bad7d48..d6554a25c 100644 --- a/moto/dynamodb/models/dynamo_type.py +++ b/moto/dynamodb/models/dynamo_type.py @@ -1,3 +1,4 @@ +import base64 import copy import decimal @@ -195,6 +196,10 @@ class DynamoType(object): def to_json(self) -> Dict[str, Any]: # Returns a regular JSON object where the value can still be/contain a DynamoType + if self.is_binary(): + # Binary data cannot be represented in JSON + # AWS returns a base64-encoded value - the SDK's then convert that back + return {self.type: base64.b64encode(self.value).decode("utf-8")} return {self.type: self.value} def to_regular_json(self) -> Dict[str, Any]: @@ -212,6 +217,8 @@ class DynamoType(object): val.to_regular_json() if isinstance(val, DynamoType) else val for val in value ] + if self.is_binary(): + value = base64.b64decode(value) return {self.type: value} def compare(self, range_comparison: str, range_objs: List[Any]) -> bool: @@ -236,6 +243,9 @@ class DynamoType(object): def is_map(self) -> bool: return self.type == DDBType.MAP + def is_binary(self) -> bool: + return self.type == DDBType.BINARY + def same_type(self, other: "DynamoType") -> bool: return self.type == other.type diff --git a/moto/dynamodb/models/utilities.py b/moto/dynamodb/models/utilities.py index 227f34bc0..6d4636e78 100644 --- a/moto/dynamodb/models/utilities.py +++ b/moto/dynamodb/models/utilities.py @@ -14,7 +14,7 @@ def dynamo_json_dump(dynamo_object: Any) -> str: def bytesize(val: str) -> int: - return len(val.encode("utf-8")) + return len(val if isinstance(val, bytes) else val.encode("utf-8")) def find_nested_key( diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py index 07c405071..667c0dfa4 100644 --- a/tests/test_dynamodb/test_dynamodb.py +++ b/tests/test_dynamodb/test_dynamodb.py @@ -4,6 +4,7 @@ from decimal import Decimal import boto3 from boto3.dynamodb.conditions import Attr, Key +from boto3.dynamodb.types import Binary import re from moto import mock_dynamodb, settings from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID @@ -5727,6 +5728,30 @@ def test_projection_expression_execution_order(): ) +@mock_dynamodb +def test_projection_expression_with_binary_attr(): + dynamo_resource = boto3.resource("dynamodb", region_name="us-east-1") + dynamo_resource.create_table( + TableName="test", + AttributeDefinitions=[ + {"AttributeName": "pk", "AttributeType": "S"}, + {"AttributeName": "sk", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "pk", "KeyType": "HASH"}, + {"AttributeName": "sk", "KeyType": "RANGE"}, + ], + BillingMode="PAY_PER_REQUEST", + ) + table = dynamo_resource.Table("test") + table.put_item(Item={"pk": "pk", "sk": "sk", "key": b"value\xbf"}) + assert table.get_item( + Key={"pk": "pk", "sk": "sk"}, + ExpressionAttributeNames={"#key": "key"}, + ProjectionExpression="#key", + )["Item"] == {"key": Binary(b"value\xbf")} + + @mock_dynamodb def test_invalid_projection_expressions(): table_name = "test-projection-expressions-table"