"""DynamoDB table and item integration tests.""" import pytest class TestDynamoDBTable: """Test DynamoDB table operations.""" def test_create_table(self, dynamodb_client, unique_name): """Test CreateTable creates a table.""" table_name = f"pytest-ddb-{unique_name}" try: response = dynamodb_client.create_table( TableName=table_name, KeySchema=[ {"AttributeName": "pk", "KeyType": "HASH"}, {"AttributeName": "sk", "KeyType": "RANGE"}, ], AttributeDefinitions=[ {"AttributeName": "pk", "AttributeType": "S"}, {"AttributeName": "sk", "AttributeType": "S"}, ], BillingMode="PAY_PER_REQUEST", ) assert response["TableDescription"]["TableStatus"] == "ACTIVE" finally: dynamodb_client.delete_table(TableName=table_name) def test_describe_table(self, dynamodb_client, test_table): """Test DescribeTable returns table info.""" response = dynamodb_client.describe_table(TableName=test_table) assert response["Table"]["TableName"] == test_table def test_list_tables(self, dynamodb_client, test_table): """Test ListTables includes created table.""" response = dynamodb_client.list_tables() assert test_table in response["TableNames"] def test_update_table(self, dynamodb_client, unique_name): """Test UpdateTable modifies provisioned throughput.""" table_name = f"pytest-ddb-{unique_name}" dynamodb_client.create_table( TableName=table_name, KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}], ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, ) try: response = dynamodb_client.update_table( TableName=table_name, ProvisionedThroughput={"ReadCapacityUnits": 10, "WriteCapacityUnits": 10}, ) assert ( response["TableDescription"]["ProvisionedThroughput"]["ReadCapacityUnits"] == 10 ) finally: dynamodb_client.delete_table(TableName=table_name) def test_describe_time_to_live(self, dynamodb_client, test_table): """Test DescribeTimeToLive returns TTL status.""" response = dynamodb_client.describe_time_to_live(TableName=test_table) assert response["TimeToLiveDescription"]["TimeToLiveStatus"] == "DISABLED" def test_update_and_describe_continuous_backups(self, dynamodb_client, unique_name): """Test PITR can be enabled and described through the SDK.""" table_name = f"pytest-ddb-{unique_name}" dynamodb_client.create_table( TableName=table_name, KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}], BillingMode="PAY_PER_REQUEST", ) try: response = dynamodb_client.describe_continuous_backups(TableName=table_name) assert ( response["ContinuousBackupsDescription"]["ContinuousBackupsStatus"] == "ENABLED" ) assert ( response["ContinuousBackupsDescription"][ "PointInTimeRecoveryDescription" ]["PointInTimeRecoveryStatus"] == "DISABLED" ) assert ( "RecoveryPeriodInDays" not in response["ContinuousBackupsDescription"][ "PointInTimeRecoveryDescription" ] ) response = dynamodb_client.update_continuous_backups( TableName=table_name, PointInTimeRecoverySpecification={"PointInTimeRecoveryEnabled": True}, ) assert ( response["ContinuousBackupsDescription"][ "PointInTimeRecoveryDescription" ]["PointInTimeRecoveryStatus"] == "ENABLED" ) assert ( response["ContinuousBackupsDescription"][ "PointInTimeRecoveryDescription" ]["RecoveryPeriodInDays"] == 35 ) finally: dynamodb_client.delete_table(TableName=table_name) def test_delete_table(self, dynamodb_client, unique_name): """Test DeleteTable removes table.""" table_name = f"pytest-ddb-{unique_name}" dynamodb_client.create_table( TableName=table_name, KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}], BillingMode="PAY_PER_REQUEST", ) dynamodb_client.delete_table(TableName=table_name) response = dynamodb_client.list_tables() assert table_name not in response["TableNames"] class TestDynamoDBItem: """Test DynamoDB item operations.""" def test_put_item(self, dynamodb_client, test_table): """Test PutItem adds item to table.""" dynamodb_client.put_item( TableName=test_table, Item={"pk": {"S": "user-1"}, "data": {"S": "value-1"}}, ) response = dynamodb_client.get_item( TableName=test_table, Key={"pk": {"S": "user-1"}} ) assert response.get("Item", {}).get("data", {}).get("S") == "value-1" def test_get_item(self, dynamodb_client, test_table): """Test GetItem retrieves item.""" dynamodb_client.put_item( TableName=test_table, Item={"pk": {"S": "user-1"}, "data": {"S": "test-value"}}, ) response = dynamodb_client.get_item( TableName=test_table, Key={"pk": {"S": "user-1"}} ) assert response.get("Item", {}).get("data", {}).get("S") == "test-value" def test_update_item(self, dynamodb_client, test_table): """Test UpdateItem modifies item.""" dynamodb_client.put_item( TableName=test_table, Item={"pk": {"S": "user-1"}, "data": {"S": "original"}}, ) response = dynamodb_client.update_item( TableName=test_table, Key={"pk": {"S": "user-1"}}, UpdateExpression="SET #d = :newVal", ExpressionAttributeNames={"#d": "data"}, ExpressionAttributeValues={":newVal": {"S": "updated"}}, ReturnValues="ALL_NEW", ) assert response["Attributes"]["data"]["S"] == "updated" def test_delete_item(self, dynamodb_client, test_table): """Test DeleteItem removes item.""" dynamodb_client.put_item( TableName=test_table, Item={"pk": {"S": "to-delete"}, "data": {"S": "value"}}, ) dynamodb_client.delete_item( TableName=test_table, Key={"pk": {"S": "to-delete"}} ) response = dynamodb_client.get_item( TableName=test_table, Key={"pk": {"S": "to-delete"}} ) assert "Item" not in response class TestDynamoDBQuery: """Test DynamoDB query and scan operations.""" def test_query(self, dynamodb_client, unique_name): """Test Query returns matching items.""" table_name = f"pytest-ddb-{unique_name}" dynamodb_client.create_table( TableName=table_name, KeySchema=[ {"AttributeName": "pk", "KeyType": "HASH"}, {"AttributeName": "sk", "KeyType": "RANGE"}, ], AttributeDefinitions=[ {"AttributeName": "pk", "AttributeType": "S"}, {"AttributeName": "sk", "AttributeType": "S"}, ], BillingMode="PAY_PER_REQUEST", ) try: for i in range(1, 4): dynamodb_client.put_item( TableName=table_name, Item={ "pk": {"S": "user-1"}, "sk": {"S": f"item-{i}"}, "data": {"S": f"value-{i}"}, }, ) response = dynamodb_client.query( TableName=table_name, KeyConditionExpression="pk = :pk", ExpressionAttributeValues={":pk": {"S": "user-1"}}, ) assert response["Count"] == 3 finally: dynamodb_client.delete_table(TableName=table_name) def test_scan(self, dynamodb_client, test_table): """Test Scan returns all items.""" for i in range(1, 4): dynamodb_client.put_item( TableName=test_table, Item={"pk": {"S": f"user-{i}"}, "data": {"S": f"value-{i}"}}, ) response = dynamodb_client.scan(TableName=test_table) assert response["Count"] == 3 class TestDynamoDBBatch: """Test DynamoDB batch operations.""" def test_batch_write_item_put(self, dynamodb_client, test_table): """Test BatchWriteItem puts multiple items.""" dynamodb_client.batch_write_item( RequestItems={ test_table: [ { "PutRequest": { "Item": { "pk": {"S": "batch-1"}, "data": {"S": "value-1"}, } } }, { "PutRequest": { "Item": { "pk": {"S": "batch-2"}, "data": {"S": "value-2"}, } } }, ] } ) response = dynamodb_client.scan(TableName=test_table) assert response["Count"] == 2 def test_batch_write_item_delete(self, dynamodb_client, test_table): """Test BatchWriteItem deletes multiple items.""" # Setup items dynamodb_client.batch_write_item( RequestItems={ test_table: [ {"PutRequest": {"Item": {"pk": {"S": "del-1"}, "data": {"S": "v1"}}}}, {"PutRequest": {"Item": {"pk": {"S": "del-2"}, "data": {"S": "v2"}}}}, ] } ) # Delete items dynamodb_client.batch_write_item( RequestItems={ test_table: [ {"DeleteRequest": {"Key": {"pk": {"S": "del-1"}}}}, {"DeleteRequest": {"Key": {"pk": {"S": "del-2"}}}}, ] } ) response = dynamodb_client.scan(TableName=test_table) assert response["Count"] == 0 def test_batch_get_item(self, dynamodb_client, test_table): """Test BatchGetItem retrieves multiple items.""" dynamodb_client.batch_write_item( RequestItems={ test_table: [ {"PutRequest": {"Item": {"pk": {"S": "get-1"}, "data": {"S": "v1"}}}}, {"PutRequest": {"Item": {"pk": {"S": "get-2"}, "data": {"S": "v2"}}}}, ] } ) response = dynamodb_client.batch_get_item( RequestItems={ test_table: { "Keys": [ {"pk": {"S": "get-1"}}, {"pk": {"S": "get-2"}}, ] } } ) items = response["Responses"].get(test_table, []) assert len(items) == 2 class TestDynamoDBTagging: """Test DynamoDB table tagging operations.""" def test_tag_resource(self, dynamodb_client, unique_name): """Test TagResource adds tags to table.""" table_name = f"pytest-ddb-{unique_name}" response = dynamodb_client.create_table( TableName=table_name, KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}], BillingMode="PAY_PER_REQUEST", ) table_arn = response["TableDescription"]["TableArn"] try: dynamodb_client.tag_resource( ResourceArn=table_arn, Tags=[ {"Key": "env", "Value": "test"}, {"Key": "team", "Value": "backend"}, ], ) # If no exception, test passes finally: dynamodb_client.delete_table(TableName=table_name) def test_list_tags_of_resource(self, dynamodb_client, unique_name): """Test ListTagsOfResource returns tags.""" table_name = f"pytest-ddb-{unique_name}" response = dynamodb_client.create_table( TableName=table_name, KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}], BillingMode="PAY_PER_REQUEST", ) table_arn = response["TableDescription"]["TableArn"] dynamodb_client.tag_resource( ResourceArn=table_arn, Tags=[ {"Key": "env", "Value": "test"}, {"Key": "team", "Value": "backend"}, ], ) try: response = dynamodb_client.list_tags_of_resource(ResourceArn=table_arn) tags = {t["Key"]: t["Value"] for t in response["Tags"]} assert tags.get("env") == "test" assert tags.get("team") == "backend" finally: dynamodb_client.delete_table(TableName=table_name) def test_untag_resource(self, dynamodb_client, unique_name): """Test UntagResource removes tags.""" table_name = f"pytest-ddb-{unique_name}" response = dynamodb_client.create_table( TableName=table_name, KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}], BillingMode="PAY_PER_REQUEST", ) table_arn = response["TableDescription"]["TableArn"] dynamodb_client.tag_resource( ResourceArn=table_arn, Tags=[ {"Key": "env", "Value": "test"}, {"Key": "team", "Value": "backend"}, ], ) try: dynamodb_client.untag_resource(ResourceArn=table_arn, TagKeys=["team"]) response = dynamodb_client.list_tags_of_resource(ResourceArn=table_arn) tags = {t["Key"]: t["Value"] for t in response["Tags"]} assert tags.get("env") == "test" assert "team" not in tags finally: dynamodb_client.delete_table(TableName=table_name) class TestDynamoDBGSI: """Test DynamoDB Global Secondary Index and Local Secondary Index operations.""" def test_create_table_with_gsi_and_lsi(self, dynamodb_client, unique_name): """Test CreateTable with GSI and LSI.""" table_name = f"pytest-gsi-{unique_name}" try: dynamodb_client.create_table( TableName=table_name, KeySchema=[ {"AttributeName": "pk", "KeyType": "HASH"}, {"AttributeName": "sk", "KeyType": "RANGE"}, ], AttributeDefinitions=[ {"AttributeName": "pk", "AttributeType": "S"}, {"AttributeName": "sk", "AttributeType": "S"}, {"AttributeName": "gsiPk", "AttributeType": "S"}, {"AttributeName": "lsiSk", "AttributeType": "S"}, ], GlobalSecondaryIndexes=[ { "IndexName": "gsi-1", "KeySchema": [ {"AttributeName": "gsiPk", "KeyType": "HASH"}, {"AttributeName": "sk", "KeyType": "RANGE"}, ], "Projection": {"ProjectionType": "ALL"}, "ProvisionedThroughput": { "ReadCapacityUnits": 5, "WriteCapacityUnits": 5, }, } ], LocalSecondaryIndexes=[ { "IndexName": "lsi-1", "KeySchema": [ {"AttributeName": "pk", "KeyType": "HASH"}, {"AttributeName": "lsiSk", "KeyType": "RANGE"}, ], "Projection": {"ProjectionType": "KEYS_ONLY"}, } ], ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, ) # Verify indexes exist desc = dynamodb_client.describe_table(TableName=table_name)["Table"] gsis = desc.get("GlobalSecondaryIndexes", []) lsis = desc.get("LocalSecondaryIndexes", []) assert len(gsis) == 1 assert gsis[0]["IndexName"] == "gsi-1" assert gsis[0]["Projection"]["ProjectionType"] == "ALL" assert len(lsis) == 1 assert lsis[0]["IndexName"] == "lsi-1" assert lsis[0]["Projection"]["ProjectionType"] == "KEYS_ONLY" finally: dynamodb_client.delete_table(TableName=table_name) def test_query_gsi_sparse_index(self, dynamodb_client, unique_name): """Test querying GSI excludes items without GSI key (sparse index).""" table_name = f"pytest-gsi-{unique_name}" dynamodb_client.create_table( TableName=table_name, KeySchema=[ {"AttributeName": "pk", "KeyType": "HASH"}, {"AttributeName": "sk", "KeyType": "RANGE"}, ], AttributeDefinitions=[ {"AttributeName": "pk", "AttributeType": "S"}, {"AttributeName": "sk", "AttributeType": "S"}, {"AttributeName": "gsiPk", "AttributeType": "S"}, ], GlobalSecondaryIndexes=[ { "IndexName": "gsi-1", "KeySchema": [ {"AttributeName": "gsiPk", "KeyType": "HASH"}, {"AttributeName": "sk", "KeyType": "RANGE"}, ], "Projection": {"ProjectionType": "ALL"}, "ProvisionedThroughput": { "ReadCapacityUnits": 5, "WriteCapacityUnits": 5, }, } ], ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, ) try: # Put 2 items with gsiPk dynamodb_client.put_item( TableName=table_name, Item={ "pk": {"S": "item-1"}, "sk": {"S": "rev-1"}, "gsiPk": {"S": "group-A"}, }, ) dynamodb_client.put_item( TableName=table_name, Item={ "pk": {"S": "item-2"}, "sk": {"S": "rev-1"}, "gsiPk": {"S": "group-A"}, }, ) # Put 1 item without gsiPk (sparse) dynamodb_client.put_item( TableName=table_name, Item={"pk": {"S": "item-3"}, "sk": {"S": "rev-1"}, "data": {"S": "no-gsi"}}, ) # Query GSI - should return only the 2 items with gsiPk resp = dynamodb_client.query( TableName=table_name, IndexName="gsi-1", KeyConditionExpression="gsiPk = :gpk", ExpressionAttributeValues={":gpk": {"S": "group-A"}}, ) assert resp["Count"] == 2 pks = {item["pk"]["S"] for item in resp["Items"]} assert "item-3" not in pks finally: dynamodb_client.delete_table(TableName=table_name) def test_query_lsi(self, dynamodb_client, unique_name): """Test querying Local Secondary Index.""" table_name = f"pytest-lsi-{unique_name}" dynamodb_client.create_table( TableName=table_name, KeySchema=[ {"AttributeName": "pk", "KeyType": "HASH"}, {"AttributeName": "sk", "KeyType": "RANGE"}, ], AttributeDefinitions=[ {"AttributeName": "pk", "AttributeType": "S"}, {"AttributeName": "sk", "AttributeType": "S"}, {"AttributeName": "lsiSk", "AttributeType": "S"}, ], LocalSecondaryIndexes=[ { "IndexName": "lsi-1", "KeySchema": [ {"AttributeName": "pk", "KeyType": "HASH"}, {"AttributeName": "lsiSk", "KeyType": "RANGE"}, ], "Projection": {"ProjectionType": "KEYS_ONLY"}, } ], ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, ) try: dynamodb_client.put_item( TableName=table_name, Item={ "pk": {"S": "item-1"}, "sk": {"S": "rev-1"}, "lsiSk": {"S": "2024-01-01"}, }, ) dynamodb_client.put_item( TableName=table_name, Item={ "pk": {"S": "item-1"}, "sk": {"S": "rev-2"}, "lsiSk": {"S": "2024-01-02"}, }, ) # Query LSI with range condition resp = dynamodb_client.query( TableName=table_name, IndexName="lsi-1", KeyConditionExpression="pk = :pk AND lsiSk > :d", ExpressionAttributeValues={ ":pk": {"S": "item-1"}, ":d": {"S": "2024-01-00"}, }, ) assert resp["Count"] == 2 finally: dynamodb_client.delete_table(TableName=table_name)