python-boto3/tests/unit/dynamodb/test_transform.py
2021-11-03 10:27:47 -07:00

623 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 tests import unittest, mock
from botocore.model import ServiceModel, OperationModel
from boto3.resources.base import ResourceMeta, ServiceResource
from boto3.dynamodb.transform import ParameterTransformer
from boto3.dynamodb.transform import TransformationInjector
from boto3.dynamodb.transform import DynamoDBHighLevelResource
from boto3.dynamodb.transform import register_high_level_interface
from boto3.dynamodb.transform import copy_dynamodb_params
from boto3.dynamodb.conditions import Attr, Key
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(TestInputOutputTransformer, self).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(TestTransformConditionExpression, self).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]