python-boto3/tests/unit/dynamodb/test_transform.py
2022-05-25 16:13:54 -07:00

654 lines
21 KiB
Python

# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# https://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from botocore.model import OperationModel, ServiceModel
from boto3.dynamodb.conditions import Attr, Key
from boto3.dynamodb.transform import (
DynamoDBHighLevelResource,
ParameterTransformer,
TransformationInjector,
copy_dynamodb_params,
register_high_level_interface,
)
from boto3.resources.base import ResourceMeta, ServiceResource
from tests import mock, unittest
class BaseTransformationTest(unittest.TestCase):
def setUp(self):
self.target_shape = 'MyShape'
self.original_value = 'orginal'
self.transformed_value = 'transformed'
self.transformer = ParameterTransformer()
self.json_model = {}
self.nested_json_model = {}
self.setup_models()
self.build_models()
def setup_models(self):
self.json_model = {
'operations': {
'SampleOperation': {
'name': 'SampleOperation',
'input': {'shape': 'SampleOperationInputOutput'},
'output': {'shape': 'SampleOperationInputOutput'},
}
},
'shapes': {
'SampleOperationInputOutput': {
'type': 'structure',
'members': {},
},
'String': {'type': 'string'},
},
}
def build_models(self):
self.service_model = ServiceModel(self.json_model)
self.operation_model = OperationModel(
self.json_model['operations']['SampleOperation'],
self.service_model,
)
def add_input_shape(self, shape):
self.add_shape(shape)
params_shape = self.json_model['shapes']['SampleOperationInputOutput']
shape_name = list(shape.keys())[0]
params_shape['members'][shape_name] = {'shape': shape_name}
def add_shape(self, shape):
shape_name = list(shape.keys())[0]
self.json_model['shapes'][shape_name] = shape[shape_name]
class TestInputOutputTransformer(BaseTransformationTest):
def setUp(self):
super().setUp()
self.transformation = lambda params: self.transformed_value
self.add_shape({self.target_shape: {'type': 'string'}})
def test_transform_structure(self):
input_params = {
'Structure': {
'TransformMe': self.original_value,
'LeaveAlone': self.original_value,
}
}
input_shape = {
'Structure': {
'type': 'structure',
'members': {
'TransformMe': {'shape': self.target_shape},
'LeaveAlone': {'shape': 'String'},
},
}
}
self.add_input_shape(input_shape)
self.transformer.transform(
params=input_params,
model=self.operation_model.input_shape,
transformation=self.transformation,
target_shape=self.target_shape,
)
assert input_params == {
'Structure': {
'TransformMe': self.transformed_value,
'LeaveAlone': self.original_value,
}
}
def test_transform_map(self):
input_params = {
'TransformMe': {'foo': self.original_value},
'LeaveAlone': {'foo': self.original_value},
}
targeted_input_shape = {
'TransformMe': {
'type': 'map',
'key': {'shape': 'String'},
'value': {'shape': self.target_shape},
}
}
untargeted_input_shape = {
'LeaveAlone': {
'type': 'map',
'key': {'shape': 'String'},
'value': {'shape': 'String'},
}
}
self.add_input_shape(targeted_input_shape)
self.add_input_shape(untargeted_input_shape)
self.transformer.transform(
params=input_params,
model=self.operation_model.input_shape,
transformation=self.transformation,
target_shape=self.target_shape,
)
assert input_params == {
'TransformMe': {'foo': self.transformed_value},
'LeaveAlone': {'foo': self.original_value},
}
def test_transform_list(self):
input_params = {
'TransformMe': [self.original_value, self.original_value],
'LeaveAlone': [self.original_value, self.original_value],
}
targeted_input_shape = {
'TransformMe': {
'type': 'list',
'member': {'shape': self.target_shape},
}
}
untargeted_input_shape = {
'LeaveAlone': {'type': 'list', 'member': {'shape': 'String'}}
}
self.add_input_shape(targeted_input_shape)
self.add_input_shape(untargeted_input_shape)
self.transformer.transform(
params=input_params,
model=self.operation_model.input_shape,
transformation=self.transformation,
target_shape=self.target_shape,
)
assert input_params == {
'TransformMe': [self.transformed_value, self.transformed_value],
'LeaveAlone': [self.original_value, self.original_value],
}
def test_transform_nested_structure(self):
input_params = {
'WrapperStructure': {
'Structure': {
'TransformMe': self.original_value,
'LeaveAlone': self.original_value,
}
}
}
structure_shape = {
'Structure': {
'type': 'structure',
'members': {
'TransformMe': {'shape': self.target_shape},
'LeaveAlone': {'shape': 'String'},
},
}
}
input_shape = {
'WrapperStructure': {
'type': 'structure',
'members': {'Structure': {'shape': 'Structure'}},
}
}
self.add_shape(structure_shape)
self.add_input_shape(input_shape)
self.transformer.transform(
params=input_params,
model=self.operation_model.input_shape,
transformation=self.transformation,
target_shape=self.target_shape,
)
assert input_params == {
'WrapperStructure': {
'Structure': {
'TransformMe': self.transformed_value,
'LeaveAlone': self.original_value,
}
}
}
def test_transform_nested_map(self):
input_params = {
'TargetedWrapperMap': {'foo': {'bar': self.original_value}},
'UntargetedWrapperMap': {'foo': {'bar': self.original_value}},
}
targeted_map_shape = {
'TransformMeMap': {
'type': 'map',
'key': {'shape': 'String'},
'value': {'shape': self.target_shape},
}
}
targeted_wrapper_shape = {
'TargetedWrapperMap': {
'type': 'map',
'key': {'shape': 'Name'},
'value': {'shape': 'TransformMeMap'},
}
}
self.add_shape(targeted_map_shape)
self.add_input_shape(targeted_wrapper_shape)
untargeted_map_shape = {
'LeaveAloneMap': {
'type': 'map',
'key': {'shape': 'String'},
'value': {'shape': 'String'},
}
}
untargeted_wrapper_shape = {
'UntargetedWrapperMap': {
'type': 'map',
'key': {'shape': 'Name'},
'value': {'shape': 'LeaveAloneMap'},
}
}
self.add_shape(untargeted_map_shape)
self.add_input_shape(untargeted_wrapper_shape)
self.transformer.transform(
params=input_params,
model=self.operation_model.input_shape,
transformation=self.transformation,
target_shape=self.target_shape,
)
assert input_params == {
'TargetedWrapperMap': {'foo': {'bar': self.transformed_value}},
'UntargetedWrapperMap': {'foo': {'bar': self.original_value}},
}
def test_transform_nested_list(self):
input_params = {
'TargetedWrapperList': [
[self.original_value, self.original_value]
],
'UntargetedWrapperList': [
[self.original_value, self.original_value]
],
}
targeted_list_shape = {
'TransformMe': {
'type': 'list',
'member': {'shape': self.target_shape},
}
}
targeted_wrapper_shape = {
'TargetedWrapperList': {
'type': 'list',
'member': {'shape': 'TransformMe'},
}
}
self.add_shape(targeted_list_shape)
self.add_input_shape(targeted_wrapper_shape)
untargeted_list_shape = {
'LeaveAlone': {'type': 'list', 'member': {'shape': 'String'}}
}
untargeted_wrapper_shape = {
'UntargetedWrapperList': {
'type': 'list',
'member': {'shape': 'LeaveAlone'},
}
}
self.add_shape(untargeted_list_shape)
self.add_input_shape(untargeted_wrapper_shape)
self.transformer.transform(
params=input_params,
model=self.operation_model.input_shape,
transformation=self.transformation,
target_shape=self.target_shape,
)
assert input_params == {
'TargetedWrapperList': [
[self.transformed_value, self.transformed_value]
],
'UntargetedWrapperList': [
[self.original_value, self.original_value]
],
}
def test_transform_incorrect_type_for_structure(self):
input_params = {'Structure': 'foo'}
input_shape = {
'Structure': {
'type': 'structure',
'members': {
'TransformMe': {'shape': self.target_shape},
},
}
}
self.add_input_shape(input_shape)
self.transformer.transform(
params=input_params,
model=self.operation_model.input_shape,
transformation=self.transformation,
target_shape=self.target_shape,
)
assert input_params == {'Structure': 'foo'}
def test_transform_incorrect_type_for_map(self):
input_params = {'Map': 'foo'}
input_shape = {
'Map': {
'type': 'map',
'key': {'shape': 'String'},
'value': {'shape': self.target_shape},
}
}
self.add_input_shape(input_shape)
self.transformer.transform(
params=input_params,
model=self.operation_model.input_shape,
transformation=self.transformation,
target_shape=self.target_shape,
)
assert input_params == {'Map': 'foo'}
def test_transform_incorrect_type_for_list(self):
input_params = {'List': 'foo'}
input_shape = {
'List': {'type': 'list', 'member': {'shape': self.target_shape}}
}
self.add_input_shape(input_shape)
self.transformer.transform(
params=input_params,
model=self.operation_model.input_shape,
transformation=self.transformation,
target_shape=self.target_shape,
)
assert input_params == {'List': 'foo'}
class BaseTransformAttributeValueTest(BaseTransformationTest):
def setUp(self):
self.target_shape = 'AttributeValue'
self.setup_models()
self.build_models()
self.python_value = 'mystring'
self.dynamodb_value = {'S': self.python_value}
self.injector = TransformationInjector()
self.add_shape({self.target_shape: {'type': 'string'}})
class TestTransformAttributeValueInput(BaseTransformAttributeValueTest):
def test_handler(self):
input_params = {
'Structure': {
'TransformMe': self.python_value,
'LeaveAlone': 'unchanged',
}
}
input_shape = {
'Structure': {
'type': 'structure',
'members': {
'TransformMe': {'shape': self.target_shape},
'LeaveAlone': {'shape': 'String'},
},
}
}
self.add_input_shape(input_shape)
self.injector.inject_attribute_value_input(
params=input_params, model=self.operation_model
)
assert input_params == {
'Structure': {
'TransformMe': self.dynamodb_value,
'LeaveAlone': 'unchanged',
}
}
class TestTransformAttributeValueOutput(BaseTransformAttributeValueTest):
def test_handler(self):
parsed = {
'Structure': {
'TransformMe': self.dynamodb_value,
'LeaveAlone': 'unchanged',
}
}
input_shape = {
'Structure': {
'type': 'structure',
'members': {
'TransformMe': {'shape': self.target_shape},
'LeaveAlone': {'shape': 'String'},
},
}
}
self.add_input_shape(input_shape)
self.injector.inject_attribute_value_output(
parsed=parsed, model=self.operation_model
)
assert parsed == {
'Structure': {
'TransformMe': self.python_value,
'LeaveAlone': 'unchanged',
}
}
def test_no_output(self):
service_model = ServiceModel(
{
'operations': {
'SampleOperation': {
'name': 'SampleOperation',
'input': {'shape': 'SampleOperationInputOutput'},
}
},
'shapes': {
'SampleOperationInput': {
'type': 'structure',
'members': {},
},
'String': {'type': 'string'},
},
}
)
operation_model = service_model.operation_model('SampleOperation')
parsed = {}
self.injector.inject_attribute_value_output(
parsed=parsed, model=operation_model
)
assert parsed == {}
class TestTransformConditionExpression(BaseTransformationTest):
def setUp(self):
super().setUp()
self.add_shape({'ConditionExpression': {'type': 'string'}})
self.add_shape({'KeyExpression': {'type': 'string'}})
shapes = self.json_model['shapes']
input_members = shapes['SampleOperationInputOutput']['members']
input_members['KeyCondition'] = {'shape': 'KeyExpression'}
input_members['AttrCondition'] = {'shape': 'ConditionExpression'}
self.injector = TransformationInjector()
self.build_models()
def test_non_condition_input(self):
params = {'KeyCondition': 'foo', 'AttrCondition': 'bar'}
self.injector.inject_condition_expressions(
params, self.operation_model
)
assert params == {'KeyCondition': 'foo', 'AttrCondition': 'bar'}
def test_single_attr_condition_expression(self):
params = {'AttrCondition': Attr('foo').eq('bar')}
self.injector.inject_condition_expressions(
params, self.operation_model
)
assert params == {
'AttrCondition': '#n0 = :v0',
'ExpressionAttributeNames': {'#n0': 'foo'},
'ExpressionAttributeValues': {':v0': 'bar'},
}
def test_single_key_conditon_expression(self):
params = {'KeyCondition': Key('foo').eq('bar')}
self.injector.inject_condition_expressions(
params, self.operation_model
)
assert params == {
'KeyCondition': '#n0 = :v0',
'ExpressionAttributeNames': {'#n0': 'foo'},
'ExpressionAttributeValues': {':v0': 'bar'},
}
def test_key_and_attr_conditon_expression(self):
params = {
'KeyCondition': Key('foo').eq('bar'),
'AttrCondition': Attr('biz').eq('baz'),
}
self.injector.inject_condition_expressions(
params, self.operation_model
)
assert params == {
'KeyCondition': '#n1 = :v1',
'AttrCondition': '#n0 = :v0',
'ExpressionAttributeNames': {'#n0': 'biz', '#n1': 'foo'},
'ExpressionAttributeValues': {':v0': 'baz', ':v1': 'bar'},
}
def test_key_and_attr_conditon_expression_with_placeholders(self):
params = {
'KeyCondition': Key('foo').eq('bar'),
'AttrCondition': Attr('biz').eq('baz'),
'ExpressionAttributeNames': {'#a': 'b'},
'ExpressionAttributeValues': {':c': 'd'},
}
self.injector.inject_condition_expressions(
params, self.operation_model
)
assert params == {
'KeyCondition': '#n1 = :v1',
'AttrCondition': '#n0 = :v0',
'ExpressionAttributeNames': {
'#n0': 'biz',
'#n1': 'foo',
'#a': 'b',
},
'ExpressionAttributeValues': {
':v0': 'baz',
':v1': 'bar',
':c': 'd',
},
}
class TestCopyDynamoDBParams(unittest.TestCase):
def test_copy_dynamodb_params(self):
params = {'foo': 'bar'}
new_params = copy_dynamodb_params(params)
assert params == new_params
assert new_params is not params
class TestDynamoDBHighLevelResource(unittest.TestCase):
def setUp(self):
self.events = mock.Mock()
self.client = mock.Mock()
self.client.meta.events = self.events
self.meta = ResourceMeta('dynamodb')
def test_instantiation(self):
# Instantiate the class.
dynamodb_class = type(
'dynamodb',
(DynamoDBHighLevelResource, ServiceResource),
{'meta': self.meta},
)
with mock.patch(
'boto3.dynamodb.transform.TransformationInjector'
) as mock_injector:
with mock.patch(
'boto3.dynamodb.transform.DocumentModifiedShape.'
'replace_documentation_for_matching_shape'
) as mock_modify_documentation_method:
dynamodb_class(client=self.client)
# It should have fired the following events upon instantiation.
event_call_args = self.events.register.call_args_list
assert event_call_args == [
mock.call(
'provide-client-params.dynamodb',
copy_dynamodb_params,
unique_id='dynamodb-create-params-copy',
),
mock.call(
'before-parameter-build.dynamodb',
mock_injector.return_value.inject_condition_expressions,
unique_id='dynamodb-condition-expression',
),
mock.call(
'before-parameter-build.dynamodb',
mock_injector.return_value.inject_attribute_value_input,
unique_id='dynamodb-attr-value-input',
),
mock.call(
'after-call.dynamodb',
mock_injector.return_value.inject_attribute_value_output,
unique_id='dynamodb-attr-value-output',
),
mock.call(
'docs.*.dynamodb.*.complete-section',
mock_modify_documentation_method,
unique_id='dynamodb-attr-value-docs',
),
mock.call(
'docs.*.dynamodb.*.complete-section',
mock_modify_documentation_method,
unique_id='dynamodb-key-expression-docs',
),
mock.call(
'docs.*.dynamodb.*.complete-section',
mock_modify_documentation_method,
unique_id='dynamodb-cond-expression-docs',
),
]
class TestRegisterHighLevelInterface(unittest.TestCase):
def test_register(self):
base_classes = [object]
register_high_level_interface(base_classes)
# Check that the base classes are as expected
assert base_classes == [DynamoDBHighLevelResource, object]