diff --git a/moto/dynamodb2/exceptions.py b/moto/dynamodb2/exceptions.py index 31514923f..d5e595e64 100644 --- a/moto/dynamodb2/exceptions.py +++ b/moto/dynamodb2/exceptions.py @@ -205,6 +205,13 @@ class MultipleTransactionsException(MockValidationException): super().__init__(self.msg) +class TooManyTransactionsException(MockValidationException): + msg = "Validation error at transactItems: Member must have length less than or equal to 25." + + def __init__(self): + super().__init__(self.msg) + + class EmptyKeyAttributeException(MockValidationException): empty_str_msg = "One or more parameter values were invalid: An AttributeValue may not contain an empty string" # AWS has a different message for empty index keys diff --git a/moto/dynamodb2/models/__init__.py b/moto/dynamodb2/models/__init__.py index 2e9284f3c..892cfa2af 100644 --- a/moto/dynamodb2/models/__init__.py +++ b/moto/dynamodb2/models/__init__.py @@ -24,6 +24,7 @@ from moto.dynamodb2.exceptions import ( EmptyKeyAttributeException, InvalidAttributeTypeError, MultipleTransactionsException, + TooManyTransactionsException, ) from moto.dynamodb2.models.utilities import bytesize from moto.dynamodb2.models.dynamo_type import DynamoType @@ -388,12 +389,16 @@ class Table(CloudFormationModel): def __init__( self, table_name, + region, schema=None, attr=None, throughput=None, + billing_mode=None, indexes=None, global_indexes=None, streams=None, + sse_specification=None, + tags=None, ): self.name = table_name self.attr = attr @@ -417,8 +422,9 @@ class Table(CloudFormationModel): self.table_key_attrs = [ key for key in (self.hash_key_attr, self.range_key_attr) if key ] + self.billing_mode = billing_mode if throughput is None: - self.throughput = {"WriteCapacityUnits": 10, "ReadCapacityUnits": 10} + self.throughput = {"WriteCapacityUnits": 0, "ReadCapacityUnits": 0} else: self.throughput = throughput self.throughput["NumberOfDecreasesToday"] = 0 @@ -433,11 +439,14 @@ class Table(CloudFormationModel): self.created_at = datetime.datetime.utcnow() self.items = defaultdict(dict) self.table_arn = self._generate_arn(table_name) - self.tags = [] + self.tags = tags or [] self.ttl = { "TimeToLiveStatus": "DISABLED" # One of 'ENABLING'|'DISABLING'|'ENABLED'|'DISABLED', # 'AttributeName': 'string' # Can contain this } + self.stream_specification = {"StreamEnabled": False} + self.latest_stream_label = None + self.stream_shard = None self.set_stream_specification(streams) self.lambda_event_source_mappings = {} self.continuous_backups = { @@ -446,6 +455,32 @@ class Table(CloudFormationModel): "PointInTimeRecoveryStatus": "DISABLED" # One of 'ENABLED'|'DISABLED' }, } + self.sse_specification = sse_specification + if sse_specification and "KMSMasterKeyId" not in self.sse_specification: + self.sse_specification["KMSMasterKeyId"] = self._get_default_encryption_key( + region + ) + + def _get_default_encryption_key(self, region): + from moto.kms import kms_backends + + # https://aws.amazon.com/kms/features/#AWS_Service_Integration + # An AWS managed CMK is created automatically when you first create + # an encrypted resource using an AWS service integrated with KMS. + kms = kms_backends[region] + ddb_alias = "alias/aws/dynamodb" + if not kms.alias_exists(ddb_alias): + key = kms.create_key( + policy="", + key_usage="ENCRYPT_DECRYPT", + customer_master_key_spec="SYMMETRIC_DEFAULT", + description="Default master key that protects my DynamoDB table storage", + tags=None, + region=region, + ) + kms.add_alias(key.id, ddb_alias) + ebs_key = kms.describe_key(ddb_alias) + return ebs_key.arn @classmethod def has_cfn_attr(cls, attribute): @@ -466,7 +501,7 @@ class Table(CloudFormationModel): return self.name @property - def key_attributes(self): + def attribute_keys(self): # A set of all the hash or range attributes for all indexes def keys_from_index(idx): schema = idx.schema @@ -519,7 +554,7 @@ class Table(CloudFormationModel): return table def _generate_arn(self, name): - return "arn:aws:dynamodb:us-east-1:123456789011:table/" + name + return f"arn:aws:dynamodb:us-east-1:{ACCOUNT_ID}:table/{name}" def set_stream_specification(self, streams): self.stream_specification = streams @@ -529,14 +564,13 @@ class Table(CloudFormationModel): self.stream_shard = StreamShard(self) else: self.stream_specification = {"StreamEnabled": False} - self.latest_stream_label = None - self.stream_shard = None def describe(self, base_key="TableDescription"): results = { base_key: { "AttributeDefinitions": self.attr, "ProvisionedThroughput": self.throughput, + "BillingModeSummary": {"BillingMode": self.billing_mode}, "TableSizeBytes": 0, "TableName": self.name, "TableStatus": "ACTIVE", @@ -550,13 +584,19 @@ class Table(CloudFormationModel): "LocalSecondaryIndexes": [index.describe() for index in self.indexes], } } + if self.latest_stream_label: + results[base_key]["LatestStreamLabel"] = self.latest_stream_label + results[base_key][ + "LatestStreamArn" + ] = f"{self.table_arn}/stream/{self.latest_stream_label}" if self.stream_specification and self.stream_specification["StreamEnabled"]: results[base_key]["StreamSpecification"] = self.stream_specification - if self.latest_stream_label: - results[base_key]["LatestStreamLabel"] = self.latest_stream_label - results[base_key]["LatestStreamArn"] = ( - self.table_arn + "/stream/" + self.latest_stream_label - ) + if self.sse_specification and self.sse_specification.get("Enabled") is True: + results[base_key]["SSEDescription"] = { + "Status": "ENABLED", + "SSEType": "KMS", + "KMSMasterKeyArn": self.sse_specification.get("KMSMasterKeyId"), + } return results def __len__(self): @@ -988,9 +1028,9 @@ class Table(CloudFormationModel): class RestoredTable(Table): - def __init__(self, name, backup): + def __init__(self, name, region, backup): params = self._parse_params_from_backup(backup) - super().__init__(name, **params) + super().__init__(name, region=region, **params) self.indexes = copy.deepcopy(backup.table.indexes) self.global_indexes = copy.deepcopy(backup.table.global_indexes) self.items = copy.deepcopy(backup.table.items) @@ -1020,9 +1060,9 @@ class RestoredTable(Table): class RestoredPITTable(Table): - def __init__(self, name, source): + def __init__(self, name, region, source): params = self._parse_params_from_table(source) - super().__init__(name, **params) + super().__init__(name, region=region, **params) self.indexes = copy.deepcopy(source.indexes) self.global_indexes = copy.deepcopy(source.global_indexes) self.items = copy.deepcopy(source.items) @@ -1145,7 +1185,7 @@ class DynamoDBBackend(BaseBackend): def create_table(self, name, **params): if name in self.tables: return None - table = Table(name, **params) + table = Table(name, region=self.region_name, **params) self.tables[name] = table return table @@ -1205,12 +1245,24 @@ class DynamoDBBackend(BaseBackend): table = self.tables[name] return table.describe(base_key="Table") - def update_table(self, name, global_index, throughput, stream_spec): + def update_table( + self, + name, + attr_definitions, + global_index, + throughput, + billing_mode, + stream_spec, + ): table = self.get_table(name) + if attr_definitions: + table.attr = attr_definitions if global_index: table = self.update_table_global_indexes(name, global_index) if throughput: table = self.update_table_throughput(name, throughput) + if billing_mode: + table = self.update_table_billing_mode(name, billing_mode) if stream_spec: table = self.update_table_streams(name, stream_spec) return table @@ -1220,6 +1272,11 @@ class DynamoDBBackend(BaseBackend): table.throughput = throughput return table + def update_table_billing_mode(self, name, billing_mode): + table = self.tables[name] + table.billing_mode = billing_mode + return table + def update_table_streams(self, name, stream_specification): table = self.tables[name] if ( @@ -1495,7 +1552,7 @@ class DynamoDBBackend(BaseBackend): item = table.get_item(hash_value, range_value) if attribute_updates: - item.validate_no_empty_key_values(attribute_updates, table.key_attributes) + item.validate_no_empty_key_values(attribute_updates, table.attribute_keys) if update_expression: validator = UpdateExpressionValidator( @@ -1568,6 +1625,8 @@ class DynamoDBBackend(BaseBackend): return table.ttl def transact_write_items(self, transact_items): + if len(transact_items) > 25: + raise TooManyTransactionsException() # Create a backup in case any of the transactions fail original_table_state = copy.deepcopy(self.tables) target_items = set() @@ -1739,7 +1798,9 @@ class DynamoDBBackend(BaseBackend): existing_table = self.get_table(target_table_name) if existing_table is not None: raise ValueError() - new_table = RestoredTable(target_table_name, backup) + new_table = RestoredTable( + target_table_name, region=self.region_name, backup=backup + ) self.tables[target_table_name] = new_table return new_table @@ -1755,7 +1816,9 @@ class DynamoDBBackend(BaseBackend): existing_table = self.get_table(target_table_name) if existing_table is not None: raise ValueError() - new_table = RestoredPITTable(target_table_name, source) + new_table = RestoredPITTable( + target_table_name, region=self.region_name, source=source + ) self.tables[target_table_name] = new_table return new_table diff --git a/moto/dynamodb2/parsing/validators.py b/moto/dynamodb2/parsing/validators.py index 3d958b917..a03658188 100644 --- a/moto/dynamodb2/parsing/validators.py +++ b/moto/dynamodb2/parsing/validators.py @@ -410,6 +410,6 @@ class UpdateExpressionValidator(Validator): UpdateExpressionFunctionEvaluator(), NoneExistingPathChecker(), ExecuteOperations(), - EmptyStringKeyValueValidator(self.table.key_attributes), + EmptyStringKeyValueValidator(self.table.attribute_keys), ] return processors diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 56115ba4b..b9689c570 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -69,7 +69,7 @@ def include_consumed_capacity(val=1.0): def put_has_empty_keys(field_updates, table): if table: - key_names = table.key_attributes + key_names = table.attribute_keys # string/binary fields with empty string as value empty_str_fields = [ @@ -168,12 +168,20 @@ class DynamoHandler(BaseResponse): er = "com.amazonaws.dynamodb.v20111205#ValidationException" return self.error( er, - "ProvisionedThroughput cannot be specified \ - when BillingMode is PAY_PER_REQUEST", + "ProvisionedThroughput cannot be specified when BillingMode is PAY_PER_REQUEST", ) throughput = None + billing_mode = "PAY_PER_REQUEST" else: # Provisioned (default billing mode) throughput = body.get("ProvisionedThroughput") + if throughput is None: + return self.error( + "ValidationException", + "One or more parameter values were invalid: ReadCapacityUnits and WriteCapacityUnits must both be specified when BillingMode is PROVISIONED", + ) + billing_mode = "PROVISIONED" + # getting ServerSideEncryption details + sse_spec = body.get("SSESpecification") # getting the schema key_schema = body["KeySchema"] # getting attribute definition @@ -218,6 +226,8 @@ class DynamoHandler(BaseResponse): ) # get the stream specification streams = body.get("StreamSpecification") + # Get any tags + tags = body.get("Tags", []) table = self.dynamodb_backend.create_table( table_name, @@ -227,6 +237,9 @@ class DynamoHandler(BaseResponse): global_indexes=global_indexes, indexes=local_secondary_indexes, streams=streams, + billing_mode=billing_mode, + sse_specification=sse_spec, + tags=tags, ) if table is not None: return dynamo_json_dump(table.describe()) @@ -339,14 +352,18 @@ class DynamoHandler(BaseResponse): def update_table(self): name = self.body["TableName"] + attr_definitions = self.body.get("AttributeDefinitions", None) global_index = self.body.get("GlobalSecondaryIndexUpdates", None) throughput = self.body.get("ProvisionedThroughput", None) + billing_mode = self.body.get("BillingMode", None) stream_spec = self.body.get("StreamSpecification", None) try: table = self.dynamodb_backend.update_table( name=name, + attr_definitions=attr_definitions, global_index=global_index, throughput=throughput, + billing_mode=billing_mode, stream_spec=stream_spec, ) return dynamo_json_dump(table.describe()) diff --git a/tests/test_awslambda/test_lambda_eventsourcemapping.py b/tests/test_awslambda/test_lambda_eventsourcemapping.py index ed045a469..39ebfe528 100644 --- a/tests/test_awslambda/test_lambda_eventsourcemapping.py +++ b/tests/test_awslambda/test_lambda_eventsourcemapping.py @@ -115,6 +115,7 @@ def test_invoke_function_from_dynamodb_put(): "StreamEnabled": True, "StreamViewType": "NEW_AND_OLD_IMAGES", }, + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5}, ) conn = boto3.client("lambda", region_name="us-east-1") @@ -165,6 +166,7 @@ def test_invoke_function_from_dynamodb_update(): "StreamEnabled": True, "StreamViewType": "NEW_AND_OLD_IMAGES", }, + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5}, ) conn = boto3.client("lambda", region_name="us-east-1") diff --git a/tests/test_dynamodb2/conftest.py b/tests/test_dynamodb2/conftest.py index 372882d78..08bef17f1 100644 --- a/tests/test_dynamodb2/conftest.py +++ b/tests/test_dynamodb2/conftest.py @@ -6,6 +6,7 @@ from moto.dynamodb2.models import Table def table(): return Table( "Forums", + region="us-east-1", schema=[ {"KeyType": "HASH", "AttributeName": "forum_name"}, {"KeyType": "RANGE", "AttributeName": "subject"}, diff --git a/tests/test_dynamodb2/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb2/exceptions/test_dynamodb_exceptions.py index 7f0b623b9..be73b9b07 100644 --- a/tests/test_dynamodb2/exceptions/test_dynamodb_exceptions.py +++ b/tests/test_dynamodb2/exceptions/test_dynamodb_exceptions.py @@ -502,3 +502,35 @@ def test_multiple_transactions_on_same_item(): err["Message"].should.equal( "Transaction request cannot include multiple operations on one item" ) + + +@mock_dynamodb2 +def test_transact_write_items__too_many_transactions(): + table_schema = { + "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}], + "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"},], + } + dynamodb = boto3.client("dynamodb", region_name="us-east-1") + dynamodb.create_table( + TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema + ) + + def update_email_transact(email): + return { + "Put": { + "TableName": "test-table", + "Item": {"pk": {"S": ":v"}}, + "ExpressionAttributeValues": {":v": {"S": email}}, + } + } + + update_email_transact(f"test1@moto.com") + with pytest.raises(ClientError) as exc: + dynamodb.transact_write_items( + TransactItems=[ + update_email_transact(f"test{idx}@moto.com") for idx in range(26) + ] + ) + err = exc.value.response["Error"] + err["Code"].should.equal("ValidationException") + err["Message"].should.match("Member must have length less than or equal to 25") diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 594fdf3ad..7eef4b13f 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -1605,45 +1605,6 @@ def test_bad_scan_filter(): raise RuntimeError("Should have raised ResourceInUseException") -@mock_dynamodb2 -def test_create_table_pay_per_request(): - client = boto3.client("dynamodb", region_name="us-east-1") - client.create_table( - TableName="test1", - AttributeDefinitions=[ - {"AttributeName": "client", "AttributeType": "S"}, - {"AttributeName": "app", "AttributeType": "S"}, - ], - KeySchema=[ - {"AttributeName": "client", "KeyType": "HASH"}, - {"AttributeName": "app", "KeyType": "RANGE"}, - ], - BillingMode="PAY_PER_REQUEST", - ) - - -@mock_dynamodb2 -def test_create_table_error_pay_per_request_with_provisioned_param(): - client = boto3.client("dynamodb", region_name="us-east-1") - - try: - client.create_table( - TableName="test1", - AttributeDefinitions=[ - {"AttributeName": "client", "AttributeType": "S"}, - {"AttributeName": "app", "AttributeType": "S"}, - ], - KeySchema=[ - {"AttributeName": "client", "KeyType": "HASH"}, - {"AttributeName": "app", "KeyType": "RANGE"}, - ], - ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123}, - BillingMode="PAY_PER_REQUEST", - ) - except ClientError as err: - err.response["Error"]["Code"].should.equal("ValidationException") - - @mock_dynamodb2 def test_duplicate_create(): client = boto3.client("dynamodb", region_name="us-east-1") @@ -2356,61 +2317,6 @@ def test_query_global_secondary_index_when_created_via_update_table_resource(): } -@mock_dynamodb2 -def test_dynamodb_streams_1(): - conn = boto3.client("dynamodb", region_name="us-east-1") - - resp = conn.create_table( - TableName="test-streams", - KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], - AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], - ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, - StreamSpecification={ - "StreamEnabled": True, - "StreamViewType": "NEW_AND_OLD_IMAGES", - }, - ) - - assert "StreamSpecification" in resp["TableDescription"] - assert resp["TableDescription"]["StreamSpecification"] == { - "StreamEnabled": True, - "StreamViewType": "NEW_AND_OLD_IMAGES", - } - assert "LatestStreamLabel" in resp["TableDescription"] - assert "LatestStreamArn" in resp["TableDescription"] - - resp = conn.delete_table(TableName="test-streams") - - assert "StreamSpecification" in resp["TableDescription"] - - -@mock_dynamodb2 -def test_dynamodb_streams_2(): - conn = boto3.client("dynamodb", region_name="us-east-1") - - resp = conn.create_table( - TableName="test-stream-update", - KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], - AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], - ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, - ) - - assert "StreamSpecification" not in resp["TableDescription"] - - resp = conn.update_table( - TableName="test-stream-update", - StreamSpecification={"StreamEnabled": True, "StreamViewType": "NEW_IMAGE"}, - ) - - assert "StreamSpecification" in resp["TableDescription"] - assert resp["TableDescription"]["StreamSpecification"] == { - "StreamEnabled": True, - "StreamViewType": "NEW_IMAGE", - } - assert "LatestStreamLabel" in resp["TableDescription"] - assert "LatestStreamArn" in resp["TableDescription"] - - @mock_dynamodb2 def test_query_gsi_with_range_key(): dynamodb = boto3.client("dynamodb", region_name="us-east-1") @@ -4314,6 +4220,7 @@ def test_update_expression_with_numeric_literal_instead_of_value(): TableName="moto-test", KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + BillingMode="PAY_PER_REQUEST", ) try: @@ -4338,6 +4245,7 @@ def test_update_expression_with_multiple_set_clauses_must_be_comma_separated(): TableName="moto-test", KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + BillingMode="PAY_PER_REQUEST", ) try: @@ -5123,6 +5031,7 @@ def test_attribute_item_delete(): TableName=name, AttributeDefinitions=[{"AttributeName": "name", "AttributeType": "S"}], KeySchema=[{"AttributeName": "name", "KeyType": "HASH"}], + BillingMode="PAY_PER_REQUEST", ) item_name = "foo" diff --git a/tests/test_dynamodb2/test_dynamodb_condition_expressions.py b/tests/test_dynamodb2/test_dynamodb_condition_expressions.py index d96d7ce9b..e4637ce80 100644 --- a/tests/test_dynamodb2/test_dynamodb_condition_expressions.py +++ b/tests/test_dynamodb2/test_dynamodb_condition_expressions.py @@ -15,6 +15,7 @@ def test_condition_expression_with_dot_in_attr_name(): TableName=table_name, KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + BillingMode="PAY_PER_REQUEST", ) table = dynamodb.Table(table_name) @@ -247,6 +248,7 @@ def test_condition_expression_numerical_attribute(): TableName="my-table", KeySchema=[{"AttributeName": "partitionKey", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "partitionKey", "AttributeType": "S"}], + BillingMode="PAY_PER_REQUEST", ) table = dynamodb.Table("my-table") table.put_item(Item={"partitionKey": "pk-pos", "myAttr": 5}) @@ -376,6 +378,7 @@ def test_condition_expression_with_reserved_keyword_as_attr_name(): TableName=table_name, KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + BillingMode="PAY_PER_REQUEST", ) table = dynamodb.Table(table_name) diff --git a/tests/test_dynamodb2/test_dynamodb_consumedcapacity.py b/tests/test_dynamodb2/test_dynamodb_consumedcapacity.py index a284b9a64..055e24d3f 100644 --- a/tests/test_dynamodb2/test_dynamodb_consumedcapacity.py +++ b/tests/test_dynamodb2/test_dynamodb_consumedcapacity.py @@ -37,6 +37,7 @@ def test_consumed_capacity_get_unknown_item(): TableName="test_table", KeySchema=[{"AttributeName": "u", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "u", "AttributeType": "S"}], + BillingMode="PAY_PER_REQUEST", ) response = conn.get_item( TableName="test_table", diff --git a/tests/test_dynamodb2/test_dynamodb_create_table.py b/tests/test_dynamodb2/test_dynamodb_create_table.py new file mode 100644 index 000000000..999c5a065 --- /dev/null +++ b/tests/test_dynamodb2/test_dynamodb_create_table.py @@ -0,0 +1,406 @@ +import boto3 +from botocore.exceptions import ClientError +import sure # noqa # pylint: disable=unused-import +from datetime import datetime +import pytest + +from moto import mock_dynamodb2 +from moto.core import ACCOUNT_ID + + +@mock_dynamodb2 +def test_create_table_standard(): + client = boto3.client("dynamodb", region_name="us-east-1") + client.create_table( + TableName="messages", + KeySchema=[ + {"AttributeName": "id", "KeyType": "HASH"}, + {"AttributeName": "subject", "KeyType": "RANGE"}, + ], + AttributeDefinitions=[ + {"AttributeName": "id", "AttributeType": "S"}, + {"AttributeName": "subject", "AttributeType": "S"}, + ], + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5}, + ) + actual = client.describe_table(TableName="messages")["Table"] + + actual.should.have.key("AttributeDefinitions").equal( + [ + {"AttributeName": "id", "AttributeType": "S"}, + {"AttributeName": "subject", "AttributeType": "S"}, + ] + ) + actual.should.have.key("CreationDateTime").be.a(datetime) + actual.should.have.key("GlobalSecondaryIndexes").equal([]) + actual.should.have.key("LocalSecondaryIndexes").equal([]) + actual.should.have.key("ProvisionedThroughput").equal( + {"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 1, "WriteCapacityUnits": 5} + ) + actual.should.have.key("TableSizeBytes").equal(0) + actual.should.have.key("TableName").equal("messages") + actual.should.have.key("TableStatus").equal("ACTIVE") + actual.should.have.key("TableArn").equal( + f"arn:aws:dynamodb:us-east-1:{ACCOUNT_ID}:table/messages" + ) + actual.should.have.key("KeySchema").equal( + [ + {"AttributeName": "id", "KeyType": "HASH"}, + {"AttributeName": "subject", "KeyType": "RANGE"}, + ] + ) + actual.should.have.key("ItemCount").equal(0) + + +@mock_dynamodb2 +def test_create_table_with_local_index(): + client = boto3.client("dynamodb", region_name="us-east-1") + client.create_table( + TableName="messages", + KeySchema=[ + {"AttributeName": "id", "KeyType": "HASH"}, + {"AttributeName": "subject", "KeyType": "RANGE"}, + ], + AttributeDefinitions=[ + {"AttributeName": "id", "AttributeType": "S"}, + {"AttributeName": "subject", "AttributeType": "S"}, + {"AttributeName": "threads", "AttributeType": "S"}, + ], + LocalSecondaryIndexes=[ + { + "IndexName": "threads_index", + "KeySchema": [ + {"AttributeName": "id", "KeyType": "HASH"}, + {"AttributeName": "threads", "KeyType": "RANGE"}, + ], + "Projection": {"ProjectionType": "ALL"}, + } + ], + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5}, + ) + actual = client.describe_table(TableName="messages")["Table"] + + actual.should.have.key("AttributeDefinitions").equal( + [ + {"AttributeName": "id", "AttributeType": "S"}, + {"AttributeName": "subject", "AttributeType": "S"}, + {"AttributeName": "threads", "AttributeType": "S"}, + ] + ) + actual.should.have.key("CreationDateTime").be.a(datetime) + actual.should.have.key("GlobalSecondaryIndexes").equal([]) + actual.should.have.key("LocalSecondaryIndexes").equal( + [ + { + "IndexName": "threads_index", + "KeySchema": [ + {"AttributeName": "id", "KeyType": "HASH"}, + {"AttributeName": "threads", "KeyType": "RANGE"}, + ], + "Projection": {"ProjectionType": "ALL"}, + } + ] + ) + actual.should.have.key("ProvisionedThroughput").equal( + {"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 1, "WriteCapacityUnits": 5} + ) + actual.should.have.key("TableSizeBytes").equal(0) + actual.should.have.key("TableName").equal("messages") + actual.should.have.key("TableStatus").equal("ACTIVE") + actual.should.have.key("TableArn").equal( + f"arn:aws:dynamodb:us-east-1:{ACCOUNT_ID}:table/messages" + ) + actual.should.have.key("KeySchema").equal( + [ + {"AttributeName": "id", "KeyType": "HASH"}, + {"AttributeName": "subject", "KeyType": "RANGE"}, + ] + ) + actual.should.have.key("ItemCount").equal(0) + + +@mock_dynamodb2 +def test_create_table_with_gsi(): + dynamodb = boto3.client("dynamodb", region_name="us-east-1") + + table = dynamodb.create_table( + TableName="users", + KeySchema=[ + {"AttributeName": "forum_name", "KeyType": "HASH"}, + {"AttributeName": "subject", "KeyType": "RANGE"}, + ], + AttributeDefinitions=[ + {"AttributeName": "forum_name", "AttributeType": "S"}, + {"AttributeName": "subject", "AttributeType": "S"}, + ], + BillingMode="PAY_PER_REQUEST", + GlobalSecondaryIndexes=[ + { + "IndexName": "test_gsi", + "KeySchema": [{"AttributeName": "subject", "KeyType": "HASH"}], + "Projection": {"ProjectionType": "ALL"}, + } + ], + ) + table["TableDescription"]["GlobalSecondaryIndexes"].should.equal( + [ + { + "KeySchema": [{"KeyType": "HASH", "AttributeName": "subject"}], + "IndexName": "test_gsi", + "Projection": {"ProjectionType": "ALL"}, + "IndexStatus": "ACTIVE", + "ProvisionedThroughput": { + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0, + }, + } + ] + ) + + table = dynamodb.create_table( + TableName="users2", + KeySchema=[ + {"AttributeName": "forum_name", "KeyType": "HASH"}, + {"AttributeName": "subject", "KeyType": "RANGE"}, + ], + AttributeDefinitions=[ + {"AttributeName": "forum_name", "AttributeType": "S"}, + {"AttributeName": "subject", "AttributeType": "S"}, + ], + BillingMode="PAY_PER_REQUEST", + GlobalSecondaryIndexes=[ + { + "IndexName": "test_gsi", + "KeySchema": [{"AttributeName": "subject", "KeyType": "HASH"}], + "Projection": {"ProjectionType": "ALL"}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 3, + "WriteCapacityUnits": 5, + }, + } + ], + ) + table["TableDescription"]["GlobalSecondaryIndexes"].should.equal( + [ + { + "KeySchema": [{"KeyType": "HASH", "AttributeName": "subject"}], + "IndexName": "test_gsi", + "Projection": {"ProjectionType": "ALL"}, + "IndexStatus": "ACTIVE", + "ProvisionedThroughput": { + "ReadCapacityUnits": 3, + "WriteCapacityUnits": 5, + }, + } + ] + ) + + +@mock_dynamodb2 +def test_create_table_with_stream_specification(): + conn = boto3.client("dynamodb", region_name="us-east-1") + + resp = conn.create_table( + TableName="test-streams", + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, + StreamSpecification={ + "StreamEnabled": True, + "StreamViewType": "NEW_AND_OLD_IMAGES", + }, + ) + + resp["TableDescription"].should.have.key("StreamSpecification") + resp["TableDescription"]["StreamSpecification"].should.equal( + {"StreamEnabled": True, "StreamViewType": "NEW_AND_OLD_IMAGES",} + ) + resp["TableDescription"].should.contain("LatestStreamLabel") + resp["TableDescription"].should.contain("LatestStreamArn") + + resp = conn.delete_table(TableName="test-streams") + + resp["TableDescription"].should.contain("StreamSpecification") + + +@mock_dynamodb2 +def test_create_table_with_tags(): + client = boto3.client("dynamodb", region_name="us-east-1") + + resp = client.create_table( + TableName="test-streams", + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, + Tags=[{"Key": "tk", "Value": "tv",}], + ) + + resp = client.list_tags_of_resource( + ResourceArn=resp["TableDescription"]["TableArn"] + ) + resp.should.have.key("Tags").equals([{"Key": "tk", "Value": "tv"}]) + + +@mock_dynamodb2 +def test_create_table_pay_per_request(): + client = boto3.client("dynamodb", region_name="us-east-1") + client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + BillingMode="PAY_PER_REQUEST", + ) + + actual = client.describe_table(TableName="test1")["Table"] + actual.should.have.key("BillingModeSummary").equals( + {"BillingMode": "PAY_PER_REQUEST"} + ) + actual.should.have.key("ProvisionedThroughput").equals( + {"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 0, "WriteCapacityUnits": 0} + ) + + +@mock_dynamodb2 +def test_create_table__provisioned_throughput(): + client = boto3.client("dynamodb", region_name="us-east-1") + client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + ProvisionedThroughput={"ReadCapacityUnits": 2, "WriteCapacityUnits": 3}, + ) + + actual = client.describe_table(TableName="test1")["Table"] + actual.should.have.key("BillingModeSummary").equals({"BillingMode": "PROVISIONED"}) + actual.should.have.key("ProvisionedThroughput").equals( + {"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 2, "WriteCapacityUnits": 3} + ) + + +@mock_dynamodb2 +def test_create_table_without_specifying_throughput(): + dynamodb_client = boto3.client("dynamodb", region_name="us-east-1") + + with pytest.raises(ClientError) as exc: + dynamodb_client.create_table( + TableName="my-table", + AttributeDefinitions=[ + {"AttributeName": "some_field", "AttributeType": "S"} + ], + KeySchema=[{"AttributeName": "some_field", "KeyType": "HASH"}], + BillingMode="PROVISIONED", + StreamSpecification={"StreamEnabled": False, "StreamViewType": "NEW_IMAGE"}, + ) + err = exc.value.response["Error"] + err["Code"].should.equal("ValidationException") + err["Message"].should.equal( + "One or more parameter values were invalid: ReadCapacityUnits and WriteCapacityUnits must both be specified when BillingMode is PROVISIONED" + ) + + +@mock_dynamodb2 +def test_create_table_error_pay_per_request_with_provisioned_param(): + client = boto3.client("dynamodb", region_name="us-east-1") + + with pytest.raises(ClientError) as exc: + client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123}, + BillingMode="PAY_PER_REQUEST", + ) + err = exc.value.response["Error"] + err["Code"].should.equal("ValidationException") + err["Message"].should.equal( + "ProvisionedThroughput cannot be specified when BillingMode is PAY_PER_REQUEST" + ) + + +@mock_dynamodb2 +def test_create_table_with_ssespecification__false(): + client = boto3.client("dynamodb", region_name="us-east-1") + client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + BillingMode="PAY_PER_REQUEST", + SSESpecification={"Enabled": False}, + ) + + actual = client.describe_table(TableName="test1")["Table"] + actual.shouldnt.have.key("SSEDescription") + + +@mock_dynamodb2 +def test_create_table_with_ssespecification__true(): + client = boto3.client("dynamodb", region_name="us-east-1") + client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + BillingMode="PAY_PER_REQUEST", + SSESpecification={"Enabled": True}, + ) + + actual = client.describe_table(TableName="test1")["Table"] + actual.should.have.key("SSEDescription") + actual["SSEDescription"].should.have.key("Status").equals("ENABLED") + actual["SSEDescription"].should.have.key("SSEType").equals("KMS") + actual["SSEDescription"].should.have.key("KMSMasterKeyArn").match( + "^arn:aws:kms" + ) # Default KMS key for DynamoDB + + +@mock_dynamodb2 +def test_create_table_with_ssespecification__custom_kms_key(): + client = boto3.client("dynamodb", region_name="us-east-1") + client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + BillingMode="PAY_PER_REQUEST", + SSESpecification={"Enabled": True, "KMSMasterKeyId": "custom-kms-key"}, + ) + + actual = client.describe_table(TableName="test1")["Table"] + actual.should.have.key("SSEDescription") + actual["SSEDescription"].should.have.key("Status").equals("ENABLED") + actual["SSEDescription"].should.have.key("SSEType").equals("KMS") + actual["SSEDescription"].should.have.key("KMSMasterKeyArn").equals("custom-kms-key") diff --git a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py index 53e1d576b..43b34981b 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py @@ -4,124 +4,12 @@ import boto3 from boto3.dynamodb.conditions import Key from botocore.exceptions import ClientError import sure # noqa # pylint: disable=unused-import -from datetime import datetime import pytest from moto import mock_dynamodb2 from uuid import uuid4 -@mock_dynamodb2 -def test_create_table_boto3(): - client = boto3.client("dynamodb", region_name="us-east-1") - client.create_table( - TableName="messages", - KeySchema=[ - {"AttributeName": "id", "KeyType": "HASH"}, - {"AttributeName": "subject", "KeyType": "RANGE"}, - ], - AttributeDefinitions=[ - {"AttributeName": "id", "AttributeType": "S"}, - {"AttributeName": "subject", "AttributeType": "S"}, - ], - ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5}, - ) - actual = client.describe_table(TableName="messages")["Table"] - - actual.should.have.key("AttributeDefinitions").equal( - [ - {"AttributeName": "id", "AttributeType": "S"}, - {"AttributeName": "subject", "AttributeType": "S"}, - ] - ) - actual.should.have.key("CreationDateTime").be.a(datetime) - actual.should.have.key("GlobalSecondaryIndexes").equal([]) - actual.should.have.key("LocalSecondaryIndexes").equal([]) - actual.should.have.key("ProvisionedThroughput").equal( - {"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 1, "WriteCapacityUnits": 5} - ) - actual.should.have.key("TableSizeBytes").equal(0) - actual.should.have.key("TableName").equal("messages") - actual.should.have.key("TableStatus").equal("ACTIVE") - actual.should.have.key("TableArn").equal( - "arn:aws:dynamodb:us-east-1:123456789011:table/messages" - ) - actual.should.have.key("KeySchema").equal( - [ - {"AttributeName": "id", "KeyType": "HASH"}, - {"AttributeName": "subject", "KeyType": "RANGE"}, - ] - ) - actual.should.have.key("ItemCount").equal(0) - - -@mock_dynamodb2 -def test_create_table_with_local_index_boto3(): - client = boto3.client("dynamodb", region_name="us-east-1") - client.create_table( - TableName="messages", - KeySchema=[ - {"AttributeName": "id", "KeyType": "HASH"}, - {"AttributeName": "subject", "KeyType": "RANGE"}, - ], - AttributeDefinitions=[ - {"AttributeName": "id", "AttributeType": "S"}, - {"AttributeName": "subject", "AttributeType": "S"}, - {"AttributeName": "threads", "AttributeType": "S"}, - ], - LocalSecondaryIndexes=[ - { - "IndexName": "threads_index", - "KeySchema": [ - {"AttributeName": "id", "KeyType": "HASH"}, - {"AttributeName": "threads", "KeyType": "RANGE"}, - ], - "Projection": {"ProjectionType": "ALL"}, - } - ], - ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5}, - ) - actual = client.describe_table(TableName="messages")["Table"] - - actual.should.have.key("AttributeDefinitions").equal( - [ - {"AttributeName": "id", "AttributeType": "S"}, - {"AttributeName": "subject", "AttributeType": "S"}, - {"AttributeName": "threads", "AttributeType": "S"}, - ] - ) - actual.should.have.key("CreationDateTime").be.a(datetime) - actual.should.have.key("GlobalSecondaryIndexes").equal([]) - actual.should.have.key("LocalSecondaryIndexes").equal( - [ - { - "IndexName": "threads_index", - "KeySchema": [ - {"AttributeName": "id", "KeyType": "HASH"}, - {"AttributeName": "threads", "KeyType": "RANGE"}, - ], - "Projection": {"ProjectionType": "ALL"}, - } - ] - ) - actual.should.have.key("ProvisionedThroughput").equal( - {"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 1, "WriteCapacityUnits": 5} - ) - actual.should.have.key("TableSizeBytes").equal(0) - actual.should.have.key("TableName").equal("messages") - actual.should.have.key("TableStatus").equal("ACTIVE") - actual.should.have.key("TableArn").equal( - "arn:aws:dynamodb:us-east-1:123456789011:table/messages" - ) - actual.should.have.key("KeySchema").equal( - [ - {"AttributeName": "id", "KeyType": "HASH"}, - {"AttributeName": "subject", "KeyType": "RANGE"}, - ] - ) - actual.should.have.key("ItemCount").equal(0) - - @mock_dynamodb2 def test_get_item_without_range_key_boto3(): client = boto3.resource("dynamodb", region_name="us-east-1") @@ -192,83 +80,6 @@ def test_query_filter_boto3(): res["Items"].should.equal([{"pk": "pk", "sk": "sk-1"}, {"pk": "pk", "sk": "sk-2"}]) -@mock_dynamodb2 -def test_boto3_create_table_with_gsi(): - dynamodb = boto3.client("dynamodb", region_name="us-east-1") - - table = dynamodb.create_table( - TableName="users", - KeySchema=[ - {"AttributeName": "forum_name", "KeyType": "HASH"}, - {"AttributeName": "subject", "KeyType": "RANGE"}, - ], - AttributeDefinitions=[ - {"AttributeName": "forum_name", "AttributeType": "S"}, - {"AttributeName": "subject", "AttributeType": "S"}, - ], - BillingMode="PAY_PER_REQUEST", - GlobalSecondaryIndexes=[ - { - "IndexName": "test_gsi", - "KeySchema": [{"AttributeName": "subject", "KeyType": "HASH"}], - "Projection": {"ProjectionType": "ALL"}, - } - ], - ) - table["TableDescription"]["GlobalSecondaryIndexes"].should.equal( - [ - { - "KeySchema": [{"KeyType": "HASH", "AttributeName": "subject"}], - "IndexName": "test_gsi", - "Projection": {"ProjectionType": "ALL"}, - "IndexStatus": "ACTIVE", - "ProvisionedThroughput": { - "ReadCapacityUnits": 0, - "WriteCapacityUnits": 0, - }, - } - ] - ) - - table = dynamodb.create_table( - TableName="users2", - KeySchema=[ - {"AttributeName": "forum_name", "KeyType": "HASH"}, - {"AttributeName": "subject", "KeyType": "RANGE"}, - ], - AttributeDefinitions=[ - {"AttributeName": "forum_name", "AttributeType": "S"}, - {"AttributeName": "subject", "AttributeType": "S"}, - ], - BillingMode="PAY_PER_REQUEST", - GlobalSecondaryIndexes=[ - { - "IndexName": "test_gsi", - "KeySchema": [{"AttributeName": "subject", "KeyType": "HASH"}], - "Projection": {"ProjectionType": "ALL"}, - "ProvisionedThroughput": { - "ReadCapacityUnits": 3, - "WriteCapacityUnits": 5, - }, - } - ], - ) - table["TableDescription"]["GlobalSecondaryIndexes"].should.equal( - [ - { - "KeySchema": [{"KeyType": "HASH", "AttributeName": "subject"}], - "IndexName": "test_gsi", - "Projection": {"ProjectionType": "ALL"}, - "IndexStatus": "ACTIVE", - "ProvisionedThroughput": { - "ReadCapacityUnits": 3, - "WriteCapacityUnits": 5, - }, - } - ] - ) - - @mock_dynamodb2 def test_boto3_conditions(): dynamodb = boto3.resource("dynamodb", region_name="us-east-1") @@ -1103,8 +914,15 @@ def test_update_table_gsi_create(): table = dynamodb.Table("users") table.global_secondary_indexes.should.have.length_of(0) + table.attribute_definitions.should.have.length_of(2) table.update( + AttributeDefinitions=[ + {"AttributeName": "forum_name", "AttributeType": "S"}, + {"AttributeName": "subject", "AttributeType": "S"}, + {"AttributeName": "username", "AttributeType": "S"}, + {"AttributeName": "created", "AttributeType": "N"}, + ], GlobalSecondaryIndexUpdates=[ { "Create": { @@ -1120,11 +938,13 @@ def test_update_table_gsi_create(): }, } } - ] + ], ) table = dynamodb.Table("users") + table.reload() table.global_secondary_indexes.should.have.length_of(1) + table.attribute_definitions.should.have.length_of(4) gsi_throughput = table.global_secondary_indexes[0]["ProvisionedThroughput"] assert gsi_throughput["ReadCapacityUnits"].should.equal(3) @@ -1353,6 +1173,7 @@ def test_update_item_throws_exception_when_updating_hash_or_range_key( {"AttributeName": "h", "AttributeType": "S"}, {"AttributeName": "r", "AttributeType": "S"}, ], + BillingMode="PAY_PER_REQUEST", ) initial_val = str(uuid4()) diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py index b94bd688a..264e25645 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -5,6 +5,7 @@ import pytest from datetime import datetime from botocore.exceptions import ClientError from moto import mock_dynamodb2 +from moto.core import ACCOUNT_ID import botocore @@ -63,7 +64,7 @@ def test_create_table_boto3(): actual.should.have.key("TableName").equal("messages") actual.should.have.key("TableStatus").equal("ACTIVE") actual.should.have.key("TableArn").equal( - "arn:aws:dynamodb:us-east-1:123456789011:table/messages" + f"arn:aws:dynamodb:us-east-1:{ACCOUNT_ID}:table/messages" ) actual.should.have.key("KeySchema").equal( [{"AttributeName": "id", "KeyType": "HASH"}] @@ -93,26 +94,6 @@ def test_delete_table_boto3(): ex.value.response["Error"]["Message"].should.equal("Requested resource not found") -@mock_dynamodb2 -def test_update_table_throughput_boto3(): - conn = boto3.resource("dynamodb", region_name="us-west-2") - table = conn.create_table( - TableName="messages", - KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], - AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], - ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, - ) - table.provisioned_throughput["ReadCapacityUnits"].should.equal(5) - table.provisioned_throughput["WriteCapacityUnits"].should.equal(5) - - table.update( - ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 6} - ) - - table.provisioned_throughput["ReadCapacityUnits"].should.equal(5) - table.provisioned_throughput["WriteCapacityUnits"].should.equal(6) - - @mock_dynamodb2 def test_item_add_and_describe_and_update_boto3(): conn = boto3.resource("dynamodb", region_name="us-west-2") diff --git a/tests/test_dynamodb2/test_dynamodb_update_table.py b/tests/test_dynamodb2/test_dynamodb_update_table.py new file mode 100644 index 000000000..dbae7b552 --- /dev/null +++ b/tests/test_dynamodb2/test_dynamodb_update_table.py @@ -0,0 +1,80 @@ +import boto3 +import sure # noqa # pylint: disable=unused-import + +from moto import mock_dynamodb2 + + +@mock_dynamodb2 +def test_update_table__billing_mode(): + client = boto3.client("dynamodb", region_name="us-east-1") + client.create_table( + TableName="test", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + BillingMode="PAY_PER_REQUEST", + ) + + client.update_table( + TableName="test", + BillingMode="PROVISIONED", + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, + ) + + actual = client.describe_table(TableName="test")["Table"] + actual.should.have.key("BillingModeSummary").equals({"BillingMode": "PROVISIONED"}) + actual.should.have.key("ProvisionedThroughput").equals( + {"ReadCapacityUnits": 1, "WriteCapacityUnits": 1} + ) + + +@mock_dynamodb2 +def test_update_table_throughput(): + conn = boto3.resource("dynamodb", region_name="us-west-2") + table = conn.create_table( + TableName="messages", + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table.provisioned_throughput["ReadCapacityUnits"].should.equal(5) + table.provisioned_throughput["WriteCapacityUnits"].should.equal(5) + + table.update( + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 6} + ) + + table.provisioned_throughput["ReadCapacityUnits"].should.equal(5) + table.provisioned_throughput["WriteCapacityUnits"].should.equal(6) + + +@mock_dynamodb2 +def test_update_table__enable_stream(): + conn = boto3.client("dynamodb", region_name="us-east-1") + + resp = conn.create_table( + TableName="test-stream-update", + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, + ) + + assert "StreamSpecification" not in resp["TableDescription"] + + resp = conn.update_table( + TableName="test-stream-update", + StreamSpecification={"StreamEnabled": True, "StreamViewType": "NEW_IMAGE"}, + ) + + assert "StreamSpecification" in resp["TableDescription"] + assert resp["TableDescription"]["StreamSpecification"] == { + "StreamEnabled": True, + "StreamViewType": "NEW_IMAGE", + } + assert "LatestStreamLabel" in resp["TableDescription"] + assert "LatestStreamArn" in resp["TableDescription"]