diff --git a/moto/dynamodb2/exceptions.py b/moto/dynamodb2/exceptions.py index 334cd913a..01b98b35d 100644 --- a/moto/dynamodb2/exceptions.py +++ b/moto/dynamodb2/exceptions.py @@ -164,3 +164,10 @@ class TransactionCanceledException(ValueError): def __init__(self, errors): msg = self.cancel_reason_msg.format(", ".join([str(err) for err in errors])) super(TransactionCanceledException, self).__init__(msg) + + +class EmptyKeyAttributeException(MockValidationException): + empty_str_msg = "One or more parameter values were invalid: An AttributeValue may not contain an empty string" + + def __init__(self): + super(EmptyKeyAttributeException, self).__init__(self.empty_str_msg) diff --git a/moto/dynamodb2/models/__init__.py b/moto/dynamodb2/models/__init__.py index 6b3583010..18b0b918f 100644 --- a/moto/dynamodb2/models/__init__.py +++ b/moto/dynamodb2/models/__init__.py @@ -20,6 +20,7 @@ from moto.dynamodb2.exceptions import ( ItemSizeToUpdateTooLarge, ConditionalCheckFailed, TransactionCanceledException, + EmptyKeyAttributeException, ) from moto.dynamodb2.models.utilities import bytesize from moto.dynamodb2.models.dynamo_type import DynamoType @@ -107,6 +108,13 @@ class Item(BaseModel): included = self.attrs return {"Item": included} + def validate_no_empty_key_values(self, attribute_updates, key_attributes): + for attribute_name, update_action in attribute_updates.items(): + action = update_action.get("Action") or "PUT" # PUT is default + new_value = next(iter(update_action["Value"].values())) + if action == "PUT" and new_value == "" and attribute_name in key_attributes: + raise EmptyKeyAttributeException + def update_with_attribute_updates(self, attribute_updates): for attribute_name, update_action in attribute_updates.items(): # Use default Action value, if no explicit Action is passed. @@ -434,6 +442,18 @@ class Table(CloudFormationModel): def physical_resource_id(self): return self.name + @property + def key_attributes(self): + # A set of all the hash or range attributes for all indexes + def keys_from_index(idx): + schema = idx.schema + return [attr["AttributeName"] for attr in schema] + + fieldnames = copy.copy(self.table_key_attrs) + for idx in self.indexes + self.global_indexes: + fieldnames += keys_from_index(idx) + return fieldnames + @staticmethod def cloudformation_name_type(): return "TableName" @@ -1273,12 +1293,16 @@ class DynamoDBBackend(BaseBackend): table.put_item(data) item = table.get_item(hash_value, range_value) + if attribute_updates: + item.validate_no_empty_key_values(attribute_updates, table.key_attributes) + if update_expression: validated_ast = UpdateExpressionValidator( update_expression_ast, expression_attribute_names=expression_attribute_names, expression_attribute_values=expression_attribute_values, item=item, + table=table, ).validate() try: UpdateExpressionExecutor( diff --git a/moto/dynamodb2/parsing/validators.py b/moto/dynamodb2/parsing/validators.py index f924a713c..79849e538 100644 --- a/moto/dynamodb2/parsing/validators.py +++ b/moto/dynamodb2/parsing/validators.py @@ -12,6 +12,7 @@ from moto.dynamodb2.exceptions import ( IncorrectOperandType, InvalidUpdateExpressionInvalidDocumentPath, ProvidedKeyDoesNotExist, + EmptyKeyAttributeException, ) from moto.dynamodb2.models import DynamoType from moto.dynamodb2.parsing.ast_nodes import ( @@ -318,13 +319,36 @@ class ExecuteOperations(DepthFirstTraverser): raise IncorrectOperandType("-", left_operand.type) +class EmptyStringKeyValueValidator(DepthFirstTraverser): + def __init__(self, key_attributes): + self.key_attributes = key_attributes + + def _processing_map(self): + return {UpdateExpressionSetAction: self.check_for_empty_string_key_value} + + def check_for_empty_string_key_value(self, node): + """A node representing a SET action. Check that keys are not being assigned empty strings""" + assert isinstance(node, UpdateExpressionSetAction) + assert len(node.children) == 2 + key = node.children[0].children[0].children[0] + val_node = node.children[1].children[0] + if val_node.type in ["S", "B"] and key in self.key_attributes: + raise EmptyKeyAttributeException + return node + + class Validator(object): """ A validator is used to validate expressions which are passed in as an AST. """ def __init__( - self, expression, expression_attribute_names, expression_attribute_values, item + self, + expression, + expression_attribute_names, + expression_attribute_values, + item, + table, ): """ Besides validation the Validator should also replace referenced parts of an item which is cheapest upon @@ -339,6 +363,7 @@ class Validator(object): self.expression_attribute_names = expression_attribute_names self.expression_attribute_values = expression_attribute_values self.item = item + self.table = table self.processors = self.get_ast_processors() self.node_to_validate = deepcopy(expression) @@ -364,5 +389,6 @@ class UpdateExpressionValidator(Validator): UpdateExpressionFunctionEvaluator(), NoneExistingPathChecker(), ExecuteOperations(), + EmptyStringKeyValueValidator(self.table.key_attributes), ] return processors diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index d67994ced..85d265f6d 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -21,15 +21,18 @@ from moto.dynamodb2.models import dynamodb_backends, dynamo_json_dump TRANSACTION_MAX_ITEMS = 25 -def has_empty_keys_or_values(_dict): - if _dict == "": - return True - if not isinstance(_dict, dict): - return False - return any( - key == "" or value == "" or has_empty_keys_or_values(value) - for key, value in _dict.items() - ) +def put_has_empty_keys(field_updates, table): + if table: + key_names = table.key_attributes + + # string/binary fields with empty string as value + empty_str_fields = [ + key + for (key, val) in field_updates.items() + if next(iter(val.keys())) in ["S", "B"] and next(iter(val.values())) == "" + ] + return any([keyname in empty_str_fields for keyname in key_names]) + return False def get_empty_str_error(): @@ -257,7 +260,7 @@ class DynamoHandler(BaseResponse): er = "com.amazonaws.dynamodb.v20111205#ValidationException" return self.error(er, "Return values set to invalid value") - if has_empty_keys_or_values(item): + if put_has_empty_keys(item, self.dynamodb_backend.get_table(name)): return get_empty_str_error() overwrite = "Expected" not in self.body @@ -751,9 +754,6 @@ class DynamoHandler(BaseResponse): er = "com.amazonaws.dynamodb.v20111205#ValidationException" return self.error(er, "Return values set to invalid value") - if has_empty_keys_or_values(expression_attribute_values): - return get_empty_str_error() - if "Expected" in self.body: expected = self.body["Expected"] else: diff --git a/tests/test_dynamodb2/conftest.py b/tests/test_dynamodb2/conftest.py new file mode 100644 index 000000000..5f523db96 --- /dev/null +++ b/tests/test_dynamodb2/conftest.py @@ -0,0 +1,13 @@ +import pytest +from moto.dynamodb2.models import Table + + +@pytest.fixture +def table(): + return Table( + "Forums", + schema=[ + {"KeyType": "HASH", "AttributeName": "forum_name"}, + {"KeyType": "RANGE", "AttributeName": "subject"}, + ], + ) diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 731f4466d..3571239e2 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -186,7 +186,7 @@ def test_list_not_found_table_tags(): @requires_boto_gte("2.9") @mock_dynamodb2 -def test_item_add_empty_string_exception(): +def test_item_add_empty_string_in_key_exception(): name = "TestTable" conn = boto3.client( "dynamodb", @@ -205,10 +205,10 @@ def test_item_add_empty_string_exception(): conn.put_item( TableName=name, Item={ - "forum_name": {"S": "LOLCat Forum"}, + "forum_name": {"S": ""}, "subject": {"S": "Check this out!"}, "Body": {"S": "http://url_to_lolcat.gif"}, - "SentBy": {"S": ""}, + "SentBy": {"S": "someone@somewhere.edu"}, "ReceivedTime": {"S": "12/9/2011 11:36:03 PM"}, }, ) @@ -222,7 +222,36 @@ def test_item_add_empty_string_exception(): @requires_boto_gte("2.9") @mock_dynamodb2 -def test_update_item_with_empty_string_exception(): +def test_item_add_empty_string_no_exception(): + name = "TestTable" + conn = boto3.client( + "dynamodb", + region_name="us-west-2", + aws_access_key_id="ak", + aws_secret_access_key="sk", + ) + conn.create_table( + TableName=name, + KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + + conn.put_item( + TableName=name, + Item={ + "forum_name": {"S": "LOLCat Forum"}, + "subject": {"S": "Check this out!"}, + "Body": {"S": "http://url_to_lolcat.gif"}, + "SentBy": {"S": ""}, + "ReceivedTime": {"S": "12/9/2011 11:36:03 PM"}, + }, + ) + + +@requires_boto_gte("2.9") +@mock_dynamodb2 +def test_update_item_with_empty_string_in_key_exception(): name = "TestTable" conn = boto3.client( "dynamodb", @@ -252,8 +281,8 @@ def test_update_item_with_empty_string_exception(): conn.update_item( TableName=name, Key={"forum_name": {"S": "LOLCat Forum"}}, - UpdateExpression="set Body=:Body", - ExpressionAttributeValues={":Body": {"S": ""}}, + UpdateExpression="set forum_name=:NewName", + ExpressionAttributeValues={":NewName": {"S": ""}}, ) ex.value.response["Error"]["Code"].should.equal("ValidationException") @@ -263,6 +292,42 @@ def test_update_item_with_empty_string_exception(): ) +@requires_boto_gte("2.9") +@mock_dynamodb2 +def test_update_item_with_empty_string_no_exception(): + name = "TestTable" + conn = boto3.client( + "dynamodb", + region_name="us-west-2", + aws_access_key_id="ak", + aws_secret_access_key="sk", + ) + conn.create_table( + TableName=name, + KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + + conn.put_item( + TableName=name, + Item={ + "forum_name": {"S": "LOLCat Forum"}, + "subject": {"S": "Check this out!"}, + "Body": {"S": "http://url_to_lolcat.gif"}, + "SentBy": {"S": "test"}, + "ReceivedTime": {"S": "12/9/2011 11:36:03 PM"}, + }, + ) + + conn.update_item( + TableName=name, + Key={"forum_name": {"S": "LOLCat Forum"}}, + UpdateExpression="set Body=:Body", + ExpressionAttributeValues={":Body": {"S": ""}}, + ) + + @requires_boto_gte("2.9") @mock_dynamodb2 def test_query_invalid_table(): diff --git a/tests/test_dynamodb2/test_dynamodb_executor.py b/tests/test_dynamodb2/test_dynamodb_executor.py index 892d2715c..577a5bae0 100644 --- a/tests/test_dynamodb2/test_dynamodb_executor.py +++ b/tests/test_dynamodb2/test_dynamodb_executor.py @@ -7,7 +7,7 @@ from moto.dynamodb2.parsing.expressions import UpdateExpressionParser from moto.dynamodb2.parsing.validators import UpdateExpressionValidator -def test_execution_of_if_not_exists_not_existing_value(): +def test_execution_of_if_not_exists_not_existing_value(table): update_expression = "SET a = if_not_exists(b, a)" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -22,6 +22,7 @@ def test_execution_of_if_not_exists_not_existing_value(): expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -34,7 +35,9 @@ def test_execution_of_if_not_exists_not_existing_value(): assert expected_item == item -def test_execution_of_if_not_exists_with_existing_attribute_should_return_attribute(): +def test_execution_of_if_not_exists_with_existing_attribute_should_return_attribute( + table, +): update_expression = "SET a = if_not_exists(b, a)" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -49,6 +52,7 @@ def test_execution_of_if_not_exists_with_existing_attribute_should_return_attrib expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -61,7 +65,7 @@ def test_execution_of_if_not_exists_with_existing_attribute_should_return_attrib assert expected_item == item -def test_execution_of_if_not_exists_with_existing_attribute_should_return_value(): +def test_execution_of_if_not_exists_with_existing_attribute_should_return_value(table): update_expression = "SET a = if_not_exists(b, :val)" update_expression_values = {":val": {"N": "4"}} update_expression_ast = UpdateExpressionParser.make(update_expression) @@ -77,6 +81,7 @@ def test_execution_of_if_not_exists_with_existing_attribute_should_return_value( expression_attribute_names=None, expression_attribute_values=update_expression_values, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -89,7 +94,9 @@ def test_execution_of_if_not_exists_with_existing_attribute_should_return_value( assert expected_item == item -def test_execution_of_if_not_exists_with_non_existing_attribute_should_return_value(): +def test_execution_of_if_not_exists_with_non_existing_attribute_should_return_value( + table, +): update_expression = "SET a = if_not_exists(b, :val)" update_expression_values = {":val": {"N": "4"}} update_expression_ast = UpdateExpressionParser.make(update_expression) @@ -105,6 +112,7 @@ def test_execution_of_if_not_exists_with_non_existing_attribute_should_return_va expression_attribute_names=None, expression_attribute_values=update_expression_values, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -117,7 +125,7 @@ def test_execution_of_if_not_exists_with_non_existing_attribute_should_return_va assert expected_item == item -def test_execution_of_sum_operation(): +def test_execution_of_sum_operation(table): update_expression = "SET a = a + b" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -132,6 +140,7 @@ def test_execution_of_sum_operation(): expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -144,7 +153,7 @@ def test_execution_of_sum_operation(): assert expected_item == item -def test_execution_of_remove(): +def test_execution_of_remove(table): update_expression = "Remove a" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -159,6 +168,7 @@ def test_execution_of_remove(): expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -171,7 +181,7 @@ def test_execution_of_remove(): assert expected_item == item -def test_execution_of_remove_in_map(): +def test_execution_of_remove_in_map(table): update_expression = "Remove itemmap.itemlist[1].foo11" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -198,6 +208,7 @@ def test_execution_of_remove_in_map(): expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -222,7 +233,7 @@ def test_execution_of_remove_in_map(): assert expected_item == item -def test_execution_of_remove_in_list(): +def test_execution_of_remove_in_list(table): update_expression = "Remove itemmap.itemlist[1]" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -249,6 +260,7 @@ def test_execution_of_remove_in_list(): expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -270,7 +282,7 @@ def test_execution_of_remove_in_list(): assert expected_item == item -def test_execution_of_delete_element_from_set(): +def test_execution_of_delete_element_from_set(table): update_expression = "delete s :value" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -285,6 +297,7 @@ def test_execution_of_delete_element_from_set(): expression_attribute_names=None, expression_attribute_values={":value": {"SS": ["value2", "value5"]}}, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -297,7 +310,7 @@ def test_execution_of_delete_element_from_set(): assert expected_item == item -def test_execution_of_add_number(): +def test_execution_of_add_number(table): update_expression = "add s :value" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -312,6 +325,7 @@ def test_execution_of_add_number(): expression_attribute_names=None, expression_attribute_values={":value": {"N": "10"}}, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -324,7 +338,7 @@ def test_execution_of_add_number(): assert expected_item == item -def test_execution_of_add_set_to_a_number(): +def test_execution_of_add_set_to_a_number(table): update_expression = "add s :value" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -340,6 +354,7 @@ def test_execution_of_add_set_to_a_number(): expression_attribute_names=None, expression_attribute_values={":value": {"SS": ["s1"]}}, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -355,7 +370,7 @@ def test_execution_of_add_set_to_a_number(): assert True -def test_execution_of_add_to_a_set(): +def test_execution_of_add_to_a_set(table): update_expression = "ADD s :value" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -370,6 +385,7 @@ def test_execution_of_add_to_a_set(): expression_attribute_names=None, expression_attribute_values={":value": {"SS": ["value2", "value5"]}}, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() expected_item = Item( @@ -398,7 +414,7 @@ def test_execution_of_add_to_a_set(): ], ) def test_execution_of__delete_element_from_set_invalid_value( - expression_attribute_values, unexpected_data_type + expression_attribute_values, unexpected_data_type, table ): """A delete statement must use a value of type SS in order to delete elements from a set.""" update_expression = "delete s :value" @@ -416,6 +432,7 @@ def test_execution_of__delete_element_from_set_invalid_value( expression_attribute_names=None, expression_attribute_values=expression_attribute_values, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() assert False, "Must raise exception" @@ -424,7 +441,7 @@ def test_execution_of__delete_element_from_set_invalid_value( assert e.operand_type == unexpected_data_type -def test_execution_of_delete_element_from_a_string_attribute(): +def test_execution_of_delete_element_from_a_string_attribute(table): """A delete statement must use a value of type SS in order to delete elements from a set.""" update_expression = "delete s :value" update_expression_ast = UpdateExpressionParser.make(update_expression) @@ -441,6 +458,7 @@ def test_execution_of_delete_element_from_a_string_attribute(): expression_attribute_names=None, expression_attribute_values={":value": {"SS": ["value2"]}}, item=item, + table=table, ).validate() UpdateExpressionExecutor(validated_ast, item, None).execute() assert False, "Must raise exception" diff --git a/tests/test_dynamodb2/test_dynamodb_validation.py b/tests/test_dynamodb2/test_dynamodb_validation.py index 8761d2cd2..c966efc14 100644 --- a/tests/test_dynamodb2/test_dynamodb_validation.py +++ b/tests/test_dynamodb2/test_dynamodb_validation.py @@ -7,6 +7,7 @@ from moto.dynamodb2.exceptions import ( ExpressionAttributeNameNotDefined, IncorrectOperandType, InvalidUpdateExpressionInvalidDocumentPath, + EmptyKeyAttributeException, ) from moto.dynamodb2.models import Item, DynamoType from moto.dynamodb2.parsing.ast_nodes import ( @@ -18,7 +19,28 @@ from moto.dynamodb2.parsing.expressions import UpdateExpressionParser from moto.dynamodb2.parsing.validators import UpdateExpressionValidator -def test_validation_of_update_expression_with_keyword(): +def test_validation_of_empty_string_key_val(table): + with pytest.raises(EmptyKeyAttributeException): + update_expression = "set forum_name=:NewName" + update_expression_values = {":NewName": {"S": ""}} + update_expression_ast = UpdateExpressionParser.make(update_expression) + item = Item( + hash_key=DynamoType({"S": "forum_name"}), + hash_key_type="TYPE", + range_key=None, + range_key_type=None, + attrs={"forum_name": {"S": "hello"}}, + ) + UpdateExpressionValidator( + update_expression_ast, + expression_attribute_names=None, + expression_attribute_values=update_expression_values, + item=item, + table=table, + ).validate() + + +def test_validation_of_update_expression_with_keyword(table): try: update_expression = "SET myNum = path + :val" update_expression_values = {":val": {"N": "3"}} @@ -35,6 +57,7 @@ def test_validation_of_update_expression_with_keyword(): expression_attribute_names=None, expression_attribute_values=update_expression_values, item=item, + table=table, ).validate() assert False, "No exception raised" except AttributeIsReservedKeyword as e: @@ -44,7 +67,9 @@ def test_validation_of_update_expression_with_keyword(): @pytest.mark.parametrize( "update_expression", ["SET a = #b + :val2", "SET a = :val2 + #b",] ) -def test_validation_of_a_set_statement_with_incorrect_passed_value(update_expression): +def test_validation_of_a_set_statement_with_incorrect_passed_value( + update_expression, table +): """ By running permutations it shows that values are replaced prior to resolving attributes. @@ -65,12 +90,15 @@ def test_validation_of_a_set_statement_with_incorrect_passed_value(update_expres expression_attribute_names={"#b": "ok"}, expression_attribute_values={":val": {"N": "3"}}, item=item, + table=table, ).validate() except ExpressionAttributeValueNotDefined as e: assert e.attribute_value == ":val2" -def test_validation_of_update_expression_with_attribute_that_does_not_exist_in_item(): +def test_validation_of_update_expression_with_attribute_that_does_not_exist_in_item( + table, +): """ When an update expression tries to get an attribute that does not exist it must throw the appropriate exception. @@ -92,6 +120,7 @@ def test_validation_of_update_expression_with_attribute_that_does_not_exist_in_i expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() assert False, "No exception raised" except AttributeDoesNotExist: @@ -100,7 +129,7 @@ def test_validation_of_update_expression_with_attribute_that_does_not_exist_in_i @pytest.mark.parametrize("update_expression", ["SET a = #c", "SET a = #c + #d",]) def test_validation_of_update_expression_with_attribute_name_that_is_not_defined( - update_expression, + update_expression, table, ): """ When an update expression tries to get an attribute name that is not provided it must throw an exception. @@ -122,13 +151,14 @@ def test_validation_of_update_expression_with_attribute_name_that_is_not_defined expression_attribute_names={"#b": "ok"}, expression_attribute_values=None, item=item, + table=table, ).validate() assert False, "No exception raised" except ExpressionAttributeNameNotDefined as e: assert e.not_defined_attribute_name == "#c" -def test_validation_of_if_not_exists_not_existing_invalid_replace_value(): +def test_validation_of_if_not_exists_not_existing_invalid_replace_value(table): try: update_expression = "SET a = if_not_exists(b, a.c)" update_expression_ast = UpdateExpressionParser.make(update_expression) @@ -144,6 +174,7 @@ def test_validation_of_if_not_exists_not_existing_invalid_replace_value(): expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() assert False, "No exception raised" except AttributeDoesNotExist: @@ -172,7 +203,7 @@ def get_set_action_value(ast): return dynamo_value -def test_validation_of_if_not_exists_not_existing_value(): +def test_validation_of_if_not_exists_not_existing_value(table): update_expression = "SET a = if_not_exists(b, a)" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -187,12 +218,15 @@ def test_validation_of_if_not_exists_not_existing_value(): expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() dynamo_value = get_set_action_value(validated_ast) assert dynamo_value == DynamoType({"S": "A"}) -def test_validation_of_if_not_exists_with_existing_attribute_should_return_attribute(): +def test_validation_of_if_not_exists_with_existing_attribute_should_return_attribute( + table, +): update_expression = "SET a = if_not_exists(b, a)" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -207,12 +241,13 @@ def test_validation_of_if_not_exists_with_existing_attribute_should_return_attri expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() dynamo_value = get_set_action_value(validated_ast) assert dynamo_value == DynamoType({"S": "B"}) -def test_validation_of_if_not_exists_with_existing_attribute_should_return_value(): +def test_validation_of_if_not_exists_with_existing_attribute_should_return_value(table): update_expression = "SET a = if_not_exists(b, :val)" update_expression_values = {":val": {"N": "4"}} update_expression_ast = UpdateExpressionParser.make(update_expression) @@ -228,12 +263,15 @@ def test_validation_of_if_not_exists_with_existing_attribute_should_return_value expression_attribute_names=None, expression_attribute_values=update_expression_values, item=item, + table=table, ).validate() dynamo_value = get_set_action_value(validated_ast) assert dynamo_value == DynamoType({"N": "3"}) -def test_validation_of_if_not_exists_with_non_existing_attribute_should_return_value(): +def test_validation_of_if_not_exists_with_non_existing_attribute_should_return_value( + table, +): update_expression = "SET a = if_not_exists(b, :val)" update_expression_values = {":val": {"N": "4"}} update_expression_ast = UpdateExpressionParser.make(update_expression) @@ -249,12 +287,13 @@ def test_validation_of_if_not_exists_with_non_existing_attribute_should_return_v expression_attribute_names=None, expression_attribute_values=update_expression_values, item=item, + table=table, ).validate() dynamo_value = get_set_action_value(validated_ast) assert dynamo_value == DynamoType({"N": "4"}) -def test_validation_of_sum_operation(): +def test_validation_of_sum_operation(table): update_expression = "SET a = a + b" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -269,12 +308,13 @@ def test_validation_of_sum_operation(): expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() dynamo_value = get_set_action_value(validated_ast) assert dynamo_value == DynamoType({"N": "7"}) -def test_validation_homogeneous_list_append_function(): +def test_validation_homogeneous_list_append_function(table): update_expression = "SET ri = list_append(ri, :vals)" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -289,6 +329,7 @@ def test_validation_homogeneous_list_append_function(): expression_attribute_names=None, expression_attribute_values={":vals": {"L": [{"S": "i3"}, {"S": "i4"}]}}, item=item, + table=table, ).validate() dynamo_value = get_set_action_value(validated_ast) assert dynamo_value == DynamoType( @@ -296,7 +337,7 @@ def test_validation_homogeneous_list_append_function(): ) -def test_validation_hetereogenous_list_append_function(): +def test_validation_hetereogenous_list_append_function(table): update_expression = "SET ri = list_append(ri, :vals)" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -311,12 +352,13 @@ def test_validation_hetereogenous_list_append_function(): expression_attribute_names=None, expression_attribute_values={":vals": {"L": [{"N": "3"}]}}, item=item, + table=table, ).validate() dynamo_value = get_set_action_value(validated_ast) assert dynamo_value == DynamoType({"L": [{"S": "i1"}, {"S": "i2"}, {"N": "3"}]}) -def test_validation_list_append_function_with_non_list_arg(): +def test_validation_list_append_function_with_non_list_arg(table): """ Must error out: Invalid UpdateExpression: Incorrect operand type for operator or function; @@ -339,13 +381,14 @@ def test_validation_list_append_function_with_non_list_arg(): expression_attribute_names=None, expression_attribute_values={":vals": {"S": "N"}}, item=item, + table=table, ).validate() except IncorrectOperandType as e: assert e.operand_type == "S" assert e.operator_or_function == "list_append" -def test_sum_with_incompatible_types(): +def test_sum_with_incompatible_types(table): """ Must error out: Invalid UpdateExpression: Incorrect operand type for operator or function; operator or function: +, operand type: S' @@ -367,13 +410,14 @@ def test_sum_with_incompatible_types(): expression_attribute_names=None, expression_attribute_values={":val": {"S": "N"}, ":val2": {"N": "3"}}, item=item, + table=table, ).validate() except IncorrectOperandType as e: assert e.operand_type == "S" assert e.operator_or_function == "+" -def test_validation_of_subraction_operation(): +def test_validation_of_subraction_operation(table): update_expression = "SET ri = :val - :val2" update_expression_ast = UpdateExpressionParser.make(update_expression) item = Item( @@ -388,12 +432,13 @@ def test_validation_of_subraction_operation(): expression_attribute_names=None, expression_attribute_values={":val": {"N": "1"}, ":val2": {"N": "3"}}, item=item, + table=table, ).validate() dynamo_value = get_set_action_value(validated_ast) assert dynamo_value == DynamoType({"N": "-2"}) -def test_cannot_index_into_a_string(): +def test_cannot_index_into_a_string(table): """ Must error out: The document path provided in the update expression is invalid for update' @@ -413,13 +458,16 @@ def test_cannot_index_into_a_string(): expression_attribute_names=None, expression_attribute_values={":Item": {"S": "string_update"}}, item=item, + table=table, ).validate() assert False, "Must raise exception" except InvalidUpdateExpressionInvalidDocumentPath: assert True -def test_validation_set_path_does_not_need_to_be_resolvable_when_setting_a_new_attribute(): +def test_validation_set_path_does_not_need_to_be_resolvable_when_setting_a_new_attribute( + table, +): """If this step just passes we are happy enough""" update_expression = "set d=a" update_expression_ast = UpdateExpressionParser.make(update_expression) @@ -435,12 +483,15 @@ def test_validation_set_path_does_not_need_to_be_resolvable_when_setting_a_new_a expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() dynamo_value = get_set_action_value(validated_ast) assert dynamo_value == DynamoType({"N": "3"}) -def test_validation_set_path_does_not_need_to_be_resolvable_but_must_be_creatable_when_setting_a_new_attribute(): +def test_validation_set_path_does_not_need_to_be_resolvable_but_must_be_creatable_when_setting_a_new_attribute( + table, +): try: update_expression = "set d.e=a" update_expression_ast = UpdateExpressionParser.make(update_expression) @@ -456,6 +507,7 @@ def test_validation_set_path_does_not_need_to_be_resolvable_but_must_be_creatabl expression_attribute_names=None, expression_attribute_values=None, item=item, + table=table, ).validate() assert False, "Must raise exception" except InvalidUpdateExpressionInvalidDocumentPath: