def test_update_ttl_errors(dynamodb): client = dynamodb.meta.client # Can't set TTL on a non-existent table nonexistent_table = unique_table_name() with pytest.raises(ClientError, match='ResourceNotFoundException'): client.update_time_to_live(TableName=nonexistent_table, TimeToLiveSpecification={ 'AttributeName': 'expiration', 'Enabled': True }) with new_test_table(dynamodb, KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, ], AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]) as table: # AttributeName must be between 1 and 255 characters long. with pytest.raises(ClientError, match='ValidationException.*length'): client.update_time_to_live(TableName=table.name, TimeToLiveSpecification={ 'AttributeName': 'x' * 256, 'Enabled': True }) with pytest.raises(ClientError, match='ValidationException.*length'): client.update_time_to_live(TableName=table.name, TimeToLiveSpecification={ 'AttributeName': '', 'Enabled': True }) # Missing mandatory UpdateTimeToLive parameters - AttributeName or Enabled with pytest.raises(ClientError, match='ValidationException.*[aA]ttributeName'): client.update_time_to_live( TableName=table.name, TimeToLiveSpecification={'Enabled': True}) with pytest.raises(ClientError, match='ValidationException.*[eE]nabled'): client.update_time_to_live( TableName=table.name, TimeToLiveSpecification={'AttributeName': 'hello'}) # Wrong types for these mandatory parameters (e.g., string for Enabled) # The error type is currently a bit different in Alternator # (ValidationException) and in DynamoDB (SerializationException). with pytest.raises(ClientError): client.update_time_to_live(TableName=table.name, TimeToLiveSpecification={ 'AttributeName': 'hello', 'Enabled': 'dog' }) with pytest.raises(ClientError): client.update_time_to_live(TableName=table.name, TimeToLiveSpecification={ 'AttributeName': 3, 'Enabled': True })
def test_forbidden_tags_from_creation(scylla_only, dynamodb): # The feature of creating a table already with tags was only added to # DynamoDB in April 2019, and to the botocore library in version 1.12.136 # so older versions of the library cannot run this test. import botocore from distutils.version import LooseVersion if (LooseVersion(botocore.__version__) < LooseVersion('1.12.136')): pytest.skip( "Botocore version 1.12.136 or above required to run this test") name = unique_table_name() # It is not allowed to set the system:write_isolation to "dog", so the # following table creation should fail: with pytest.raises(ClientError, match='ValidationException'): dynamodb.create_table(TableName=name, BillingMode='PAY_PER_REQUEST', KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }], AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }], Tags=[{ 'Key': 'system:write_isolation', 'Value': 'dog' }]) # After the table creation failed, the table should not exist. with pytest.raises(ClientError, match='ResourceNotFoundException'): dynamodb.meta.client.describe_table(TableName=name)
def test_create_table_billing_mode_errors(dynamodb, test_table): with pytest.raises(ClientError, match='ValidationException'): create_table(dynamodb, unique_table_name(), BillingMode='unknown') # billing mode is case-sensitive with pytest.raises(ClientError, match='ValidationException'): create_table(dynamodb, unique_table_name(), BillingMode='pay_per_request') # PAY_PER_REQUEST cannot come with a ProvisionedThroughput: with pytest.raises(ClientError, match='ValidationException'): create_table(dynamodb, unique_table_name(), BillingMode='PAY_PER_REQUEST', ProvisionedThroughput={ 'ReadCapacityUnits': 10, 'WriteCapacityUnits': 10 }) # On the other hand, PROVISIONED requires ProvisionedThroughput: # By the way, ProvisionedThroughput not only needs to appear, it must # have both ReadCapacityUnits and WriteCapacityUnits - but we can't test # this with boto3, because boto3 has its own verification that if # ProvisionedThroughput is given, it must have the correct form. with pytest.raises(ClientError, match='ValidationException'): create_table(dynamodb, unique_table_name(), BillingMode='PROVISIONED') # If BillingMode is completely missing, it defaults to PROVISIONED, so # ProvisionedThroughput is required with pytest.raises(ClientError, match='ValidationException'): dynamodb.create_table(TableName=unique_table_name(), KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }], AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }])
def test_too_long_tags_from_creation(dynamodb): # The feature of creating a table already with tags was only added to # DynamoDB in April 2019, and to the botocore library in version 1.12.136 # so older versions of the library cannot run this test. import botocore if (Version(botocore.__version__) < Version('1.12.136')): pytest.skip("Botocore version 1.12.136 or above required to run this test") name = unique_table_name() # Setting 100 tags is not allowed, the following table creation should fail: with pytest.raises(ClientError, match='ValidationException'): dynamodb.create_table(TableName=name, BillingMode='PAY_PER_REQUEST', KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }], AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }], Tags=[{'Key': str(i), 'Value': str(i)} for i in range(100)]) # After the table creation failed, the table should not exist. with pytest.raises(ClientError, match='ResourceNotFoundException'): dynamodb.meta.client.describe_table(TableName=name)
def test_concurrent_create_and_delete_table(dynamodb, table_def, fails_without_raft): # According to boto3 documentation, "Unlike Resources and Sessions, # clients are generally thread-safe.". So because we have two threads # in this test, we must not use "dynamodb" (containing the boto3 # "resource") - we should only the boto3 "client": client = dynamodb.meta.client # Unfortunately by default Python threads print their exceptions # (e.g., assertion failures) but don't propagate them to the join(), # so the overall test doesn't fail. The following Thread wrapper # causes join() to rethrow the exception, so the test will fail. class ThreadWrapper(threading.Thread): def run(self): try: self.ret = self._target(*self._args, **self._kwargs) except BaseException as e: self.exception = e def join(self, timeout=None): super().join(timeout) if hasattr(self, 'exception'): raise self.exception return self.ret table_name = unique_table_name() # The more iterations we do, the higher the chance of reproducing # this issue. On my laptop, count = 10 reproduces the bug every time. # Lower numbers have some chance of not catching the bug. If this # issue starts to xpass, we may need to increase the count. count = 10 def deletes(): for i in range(count): try: client.delete_table(TableName=table_name) except Exception as e: # Expect either success or a ResourceNotFoundException. # On DynamoDB we can also get a ResourceInUseException # if we try to delete a table while it's in the middle # of being created. # Anything else (e.g., InternalServerError) is a bug. assert isinstance( e, ClientError) and ('ResourceNotFoundException' in str(e) or 'ResourceInUseException' in str(e)) else: print("delete successful") def creates(): for i in range(count): try: client.create_table(TableName=table_name, BillingMode='PAY_PER_REQUEST', **table_def) except Exception as e: # Expect either success or a ResourceInUseException. # Anything else (e.g., InternalServerError) is a bug. assert isinstance( e, ClientError) and 'ResourceInUseException' in str(e) else: print("create successful") t1 = ThreadWrapper(target=deletes) t2 = ThreadWrapper(target=creates) t1.start() t2.start() try: t1.join() t2.join() finally: # Make sure that in any case, the table is deleted before the # test finishes. On DynamoDB, we can't just call DeleteTable - # if some CreateTable is still in progress we can't call # DeleteTable until it finishes... timeout = time.time() + 120 while time.time() < timeout: try: client.delete_table(TableName=table_name) break except ClientError as e: if 'ResourceNotFoundException' in str(e): # The table was already deleted by the deletion thread, # nothing left to do :-) break if 'ResourceInUseException' in str(e): # A CreateTable opereration is still in progress, # we can't delete the table yet. time.sleep(1) continue raise