def test_sanitize_drop_empty_indexes(): expected = expected_table_description(SimpleModel) # Start from the same base, but inject an unnecessary NonKeyAttributes description = expected_table_description(SimpleModel) description["GlobalSecondaryIndexes"] = [] assert_unordered(expected, sanitize_table_description(description))
def test_sanitize_drop_empty_lists(): expected = expected_table_description(ComplexModel) # Start from the same base, but inject an unnecessary NonKeyAttributes description = expected_table_description(ComplexModel) index = description["GlobalSecondaryIndexes"][0] index["Projection"]["NonKeyAttributes"] = [] assert_unordered(expected, sanitize_table_description(description))
def test_validate_unspecified_gsi_throughput(session, dynamodb, caplog): """Model doesn't care what GSI read/write units are""" class MyModel(BaseModel): class Meta: read_units = 1 write_units = 1 id = Column(String, hash_key=True) other = Column(String) by_other = GlobalSecondaryIndex(projection="keys", hash_key=other) description = expected_table_description(MyModel) description["TableStatus"] = "ACTIVE" description["GlobalSecondaryIndexes"][0]["IndexStatus"] = "ACTIVE" throughput = description["GlobalSecondaryIndexes"][0]["ProvisionedThroughput"] throughput["ReadCapacityUnits"] = 15 throughput["WriteCapacityUnits"] = 20 dynamodb.describe_table.return_value = {"Table": description} assert MyModel.by_other.read_units is None assert MyModel.by_other.write_units is None caplog.handler.records.clear() session.validate_table(MyModel) assert MyModel.by_other.read_units == 15 assert MyModel.by_other.write_units == 20 assert caplog.record_tuples == [ ("bloop.session", logging.DEBUG, "validate_table: table \"MyModel\" was in ACTIVE state after 1 calls"), ("bloop.session", logging.DEBUG, "MyModel.by_other does not specify read_units, set to 15 from DescribeTable response"), ("bloop.session", logging.DEBUG, "MyModel.by_other does not specify write_units, set to 20 from DescribeTable response") ]
def test_sanitize_expected(): expected = expected_table_description(User) # Add some extra fields description = { 'AttributeDefinitions': [ {'AttributeType': 'S', 'AttributeName': 'email'}, {'AttributeType': 'S', 'AttributeName': 'id'}], 'CreationDateTime': 'EXTRA_FIELD', 'ItemCount': 'EXTRA_FIELD', 'KeySchema': [{'AttributeName': 'id', 'KeyType': 'HASH'}], 'GlobalSecondaryIndexes': [{ 'IndexArn': 'EXTRA_FIELD', 'IndexName': 'by_email', 'IndexSizeBytes': 'EXTRA_FIELD', 'IndexStatus': 'EXTRA_FIELD', 'KeySchema': [{'AttributeName': 'email', 'KeyType': 'HASH'}], 'Projection': {'ProjectionType': 'ALL'}, 'ProvisionedThroughput': { 'NumberOfDecreasesToday': 'EXTRA_FIELD', 'ReadCapacityUnits': 1, 'WriteCapacityUnits': 1}}], 'ProvisionedThroughput': { 'LastDecreaseDateTime': 'EXTRA_FIELD', 'LastIncreaseDateTime': 'EXTRA_FIELD', 'NumberOfDecreasesToday': 'EXTRA_FIELD', 'ReadCapacityUnits': 1, 'WriteCapacityUnits': 1}, 'TableArn': 'EXTRA_FIELD', 'TableName': 'User', 'TableSizeBytes': 'EXTRA_FIELD', 'TableStatus': 'EXTRA_FIELD'} sanitized = sanitize_table_description(description) assert_unordered(expected, sanitized)
def test_validate_compares_tables(session, dynamodb): description = expected_table_description(User) description["TableStatus"] = "ACTIVE" description["GlobalSecondaryIndexes"][0]["IndexStatus"] = "ACTIVE" dynamodb.describe_table.return_value = {"Table": description} session.validate_table(User) dynamodb.describe_table.assert_called_once_with(TableName="User")
def test_validate_missing_index(session, dynamodb): """Required GSI is missing""" description = expected_table_description(ProjectedIndexes) description["TableStatus"] = "ACTIVE" dynamodb.describe_table.return_value = {"Table": description} del description["GlobalSecondaryIndexes"] with pytest.raises(TableMismatch): session.validate_table(ProjectedIndexes)
def test_validate_wrong_table(session, dynamodb): """dynamo returns a valid document but it doesn't match""" full = expected_table_description(SimpleModel) full["TableStatus"] = "ACTIVE" full["TableName"] = "wrong table name" dynamodb.describe_table.return_value = {"Table": full} with pytest.raises(TableMismatch): session.validate_table(SimpleModel)
def test_validate_unknown_projection_type(session, dynamodb): """DynamoDB starts returning a new projection type""" description = expected_table_description(ProjectedIndexes) description["TableStatus"] = "ACTIVE" description["GlobalSecondaryIndexes"][0]["IndexStatus"] = "ACTIVE" dynamodb.describe_table.return_value = {"Table": description} description["GlobalSecondaryIndexes"][0]["Projection"][ "ProjectionType"] = "NewProjectionType" with pytest.raises(TableMismatch): session.validate_table(ProjectedIndexes)
def test_validate_bad_index_provisioned_throughput(session, dynamodb): """KeySchema doesn't match""" description = expected_table_description(ProjectedIndexes) description["TableStatus"] = "ACTIVE" description["GlobalSecondaryIndexes"][0]["IndexStatus"] = "ACTIVE" dynamodb.describe_table.return_value = {"Table": description} description["GlobalSecondaryIndexes"][0]["ProvisionedThroughput"][ "WriteCapacityUnits"] = -2 with pytest.raises(TableMismatch): session.validate_table(ProjectedIndexes)
def test_validate_bad_index_key_schema(session, dynamodb, caplog): """KeySchema doesn't match""" description = expected_table_description(ProjectedIndexes) description["TableStatus"] = "ACTIVE" description["GlobalSecondaryIndexes"][0]["IndexStatus"] = "ACTIVE" dynamodb.describe_table.return_value = {"Table": description} description["GlobalSecondaryIndexes"][0]["KeySchema"] = [{"KeyType": "HASH", "AttributeName": "unknown"}] with pytest.raises(TableMismatch): session.validate_table(ProjectedIndexes) assert "key schema mismatch for \"by_gsi\"" in caplog.text
def test_validate_bad_index_projection_type(session, dynamodb, caplog): """Required GSI is missing""" description = expected_table_description(ProjectedIndexes) description["TableStatus"] = "ACTIVE" description["GlobalSecondaryIndexes"][0]["IndexStatus"] = "ACTIVE" dynamodb.describe_table.return_value = {"Table": description} description["GlobalSecondaryIndexes"][0]["Projection"] = {"ProjectionType": "KEYS_ONLY"} with pytest.raises(TableMismatch): session.validate_table(ProjectedIndexes) assert "actual projection for index \"by_gsi\" is missing expected columns" in caplog.text
def test_validate_superset_index(session, dynamodb): """Validation passes if an Index's projection is a superset of the required projection""" description = expected_table_description(ProjectedIndexes) description["TableStatus"] = "ACTIVE" description["GlobalSecondaryIndexes"][0]["IndexStatus"] = "ACTIVE" # projection is ALL in DynamoDB, not the exact ["both", "gsi_only"] from the model description["GlobalSecondaryIndexes"][0]["Projection"] = {"ProjectionType": "ALL"} dynamodb.describe_table.return_value = {"Table": description} session.validate_table(ProjectedIndexes) dynamodb.describe_table.assert_called_once_with(TableName="ProjectedIndexes")
def test_validate_checks_status(session, dynamodb): # Don't care about the value checking, just want to observe retries # based on busy tables or indexes full = expected_table_description(ProjectedIndexes) full["TableStatus"] = "ACTIVE" full["GlobalSecondaryIndexes"][0]["IndexStatus"] = "ACTIVE" dynamodb.describe_table.side_effect = [ {"Table": {"TableStatus": "CREATING"}}, {"Table": {"TableStatus": "ACTIVE", "GlobalSecondaryIndexes": [ {"IndexStatus": "CREATING"}]}}, {"Table": full} ] session.validate_table(ProjectedIndexes) dynamodb.describe_table.assert_called_with(TableName="ProjectedIndexes") assert dynamodb.describe_table.call_count == 3
def test_validate_unexpected_index(session, dynamodb): """Validation doesn't fail when the backing table has an extra GSI""" full = expected_table_description(ComplexModel) full["GlobalSecondaryIndexes"].append({ "IndexName": "extra_gsi", "Projection": { "ProjectionType": "KEYS_ONLY" }, "KeySchema": [{ "KeyType": "HASH", "AttributeName": "date" }], "ProvisionedThroughput": { "WriteCapacityUnits": 1, "ReadCapacityUnits": 1 }, }) full["LocalSecondaryIndexes"].append({ "IndexName": "extra_lsi", "Projection": { "ProjectionType": "KEYS_ONLY" }, "KeySchema": [{ "KeyType": "RANGE", "AttributeName": "date" }], "ProvisionedThroughput": { "WriteCapacityUnits": 1, "ReadCapacityUnits": 1 } }) dynamodb.describe_table.return_value = {"Table": full} full["TableStatus"] = "ACTIVE" for gsi in full["GlobalSecondaryIndexes"]: gsi["IndexStatus"] = "ACTIVE" # Validation passes even though there are extra Indexes and AttributeDefinitions session.validate_table(ComplexModel)
def test_expected_description(): # Eventually expected_table_description will probably diverge from create_table # This will guard against (or coverage should show) if there's drift create = create_table_request(ComplexModel) expected = expected_table_description(ComplexModel) assert_unordered(create, expected)