"""Tests for the dynamo connector.""" import time from typing import Any, Dict import pytest from botocore.exceptions import ClientError, ParamValidationError from mypy_boto3_dynamodb import DynamoDBClient from pdp.connectors.dynamo import DynamoDbConnector from pdp.models.identity import HASH_KEY as IDENTITY_HASH_KEY from pdp.models.identity import RANGE_KEY as IDENTITY_RANGE_KEY @pytest.fixture def query_response() -> Dict[str, Any]: """Fixture response for dynamodb.""" return { "Items": [ { IDENTITY_HASH_KEY: {"S": "hello-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "howdy-test-uuid"}, }, { IDENTITY_HASH_KEY: {"S": "hello-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "bonjour-test-uuid"}, }, ], "Count": 1, "ScannedCount": 1, "ResponseMetadata": {}, } def test_connector_query_by_hash_key( default_boto_client: DynamoDBClient, local_dynamo_connector: DynamoDbConnector, seed_test_table: None, query_response: Dict[str, Any], ) -> None: """Test connector queries by hash key.""" result = local_dynamo_connector.query_by_hash_key("hello-test-uuid") items = result.get("items") assert items for item in items: assert item in query_response["Items"] def test_connector_query_by_hash_and_range_key( default_boto_client: DynamoDBClient, local_dynamo_connector_with_range: DynamoDbConnector, seed_test_table: None, ) -> None: """Test connector queries by hash and range key.""" result = local_dynamo_connector_with_range.query_by_hash_key( "hello-test-uuid", "bonjour-test-uuid" ) items = result.get("items") assert items assert { IDENTITY_HASH_KEY: {"S": "hello-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "bonjour-test-uuid"}, } in items assert { IDENTITY_HASH_KEY: {"S": "hello-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "howdy-test-uuid"}, } not in items @pytest.mark.parametrize( "hash_key, range_key, item, expect_exception, exception", [ pytest.param( "hello-test-uuid", "howdy-test-uuid", { "tenant_type": {"S": "account"}, "roles": {"L": [{"M": {"role": {"S": "audience_development_client"}}}]}, }, False, None, id="Test adding a role to existing identity-tenant pair.", ), pytest.param( "hello-test-uuid", "howdy-test-uuid", { "tenant_type": {"S": "account"}, "roles": {"L": [{"M": {"role": {"S": "audience_development_admin"}}}]}, }, False, None, id="Test adding an additional role to existing identity-tenant pair.", ), pytest.param( None, "howdy-test-uuid", { "tenant_type": {"S": "account"}, "roles": {}, }, True, ParamValidationError, id="Test validation error when missing hash key.", ), pytest.param( "hello-test-uuid", None, { "tenant_type": {"S": "account"}, "roles": {}, }, True, ClientError, id="Test client error when missing range key.", ), ], ) def test_connector_update_item( hash_key: str, range_key: str, item: Dict[str, Any], expect_exception: bool, exception: type[BaseException], default_boto_client: DynamoDBClient, local_dynamo_connector_with_range: DynamoDbConnector, seed_test_table: None, ) -> None: """Test connector updates an item.""" # Update an item if expect_exception: with pytest.raises(exception): local_dynamo_connector_with_range.update_item( hash_key, item, range_key, ) else: local_dynamo_connector_with_range.update_item( hash_key, item, range_key, ) # Query for that item query_result = local_dynamo_connector_with_range.query_by_hash_key( hash_key, range_key ) items = query_result.get("items") assert items assert { "identity_uuid": {"S": hash_key}, **item, "tenant_uuid": {"S": range_key}, } in items def test_connector_update_item_not_self_range_key( default_boto_client: DynamoDBClient, local_dynamo_connector: DynamoDbConnector, seed_test_table: None, ) -> None: """Test update_item throws range key param validation error.""" # This test uses a Dynamo connector with no self._range_key hash_key = "hello-test-uuid" range_key = "unexpected-range-key" item = { "tenant_type": {"S": "account"}, "roles": {"L": [{"M": {"role": {"S": "audience_development_admin"}}}]}, } with pytest.raises(ClientError): local_dynamo_connector.update_item( hash_key, item, range_key, ) def test_connector_update_item_same_role_new_attr( default_boto_client: DynamoDBClient, local_dynamo_connector_with_range: DynamoDbConnector, seed_test_table: None, ) -> None: """Test adding an existing role, with additional attributes.""" hash_key = "hello-test-uuid" range_key = "never-before-seen-range-key" item: Dict[str, Any] = { "tenant_type": {"S": "account"}, "roles": {"L": [{"M": {"role": {"S": "zookeeper"}}}]}, } # Add the role local_dynamo_connector_with_range.update_item( hash_key, item, range_key, ) # Add the same role, but with additional attributes item = { "tenant_type": {"S": "account"}, "roles": { "L": [{"M": {"role": {"S": "zookeeper"}, "animal_type": {"S": "big cats"}}}] }, } local_dynamo_connector_with_range.update_item( hash_key, item, range_key, ) # Query for that identity-tenant roles query_result = local_dynamo_connector_with_range.query_by_hash_key( hash_key, range_key ) items = query_result.get("items") assert items assert { "identity_uuid": {"S": hash_key}, **item, "tenant_uuid": {"S": range_key}, } in items def test_connector_update_item_same_role_fewer_attr( default_boto_client: DynamoDBClient, local_dynamo_connector_with_range: DynamoDbConnector, seed_test_table: None, ) -> None: """Test adding an existing role, with fewer attributes.""" hash_key = "hello-test-uuid" range_key = "never-before-seen-range-key" item: Dict[str, Any] = { "tenant_type": {"S": "account"}, "roles": { "L": [{"M": {"role": {"S": "zookeeper"}, "animal_type": {"S": "big cats"}}}] }, } # Add the role local_dynamo_connector_with_range.update_item( hash_key, item, range_key, ) # Add the same role, but with additional attributes item = { "tenant_type": {"S": "account"}, "roles": {"L": [{"M": {"role": {"S": "zookeeper"}}}]}, } local_dynamo_connector_with_range.update_item( hash_key, item, range_key, ) # Query for that identity-tenant roles query_result = local_dynamo_connector_with_range.query_by_hash_key( hash_key, range_key ) items = query_result.get("items") assert items assert { "identity_uuid": {"S": hash_key}, **item, "tenant_uuid": {"S": range_key}, } in items def test_connector_update_item_with_ttl( default_boto_client: DynamoDBClient, local_dynamo_connector_with_range: DynamoDbConnector, seed_test_table: None, ) -> None: """Test adding expires_at attribute to an item.""" hash_key = "hello-test-uuid" range_key = "never-before-seen-range-key" item: Dict[str, Any] = { "tenant_type": {"S": "account"}, "roles": { "L": [{"M": {"role": {"S": "zookeeper"}, "animal_type": {"S": "big cats"}}}] }, "expires_at": { "N": str(int(time.time()) + 120) }, # Epoch format timestamp + 2 minutes } # Add the role local_dynamo_connector_with_range.update_item( hash_key, item, range_key, ) # Query for that identity-tenant roles query_result = local_dynamo_connector_with_range.query_by_hash_key( hash_key, range_key ) items = query_result.get("items") assert items assert { "identity_uuid": {"S": hash_key}, **item, "tenant_uuid": {"S": range_key}, } in items async def test_connector_delete_items( default_boto_client: DynamoDBClient, local_dynamo_connector_with_range: DynamoDbConnector, seed_test_table: None, ) -> None: """Test connector deletes items.""" delete_result = await local_dynamo_connector_with_range.delete_items( [ { IDENTITY_HASH_KEY: "hello-test-uuid", IDENTITY_RANGE_KEY: "bonjour-test-uuid", }, { IDENTITY_HASH_KEY: "hello-test-uuid", IDENTITY_RANGE_KEY: "not-howdy-test-uuid", }, ] ) assert delete_result == [] query_result = local_dynamo_connector_with_range.query_by_hash_key( "hello-test-uuid", ) items = query_result.get("items") assert items assert { IDENTITY_HASH_KEY: {"S": "hello-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "bonjour-test-uuid"}, } not in items assert { IDENTITY_HASH_KEY: {"S": "hello-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "howdy-test-uuid"}, } in items async def test_connector_delete_items_missing_range_key( default_boto_client: DynamoDBClient, local_dynamo_connector_with_range: DynamoDbConnector, seed_test_table: None, ) -> None: """Test connector delete_items fails if range key is missing.""" with pytest.raises(Exception) as exception_info: await local_dynamo_connector_with_range.delete_items( [ { IDENTITY_HASH_KEY: "hello-test-uuid", }, ] ) assert ( str(exception_info.value) == f"An error occurred (ValidationException) when calling the BatchWriteItem operation: The number of conditions on the keys is invalid" # noqa E501 ) async def test_connector_put_items( default_boto_client: DynamoDBClient, local_dynamo_connector_with_range: DynamoDbConnector, seed_test_table: None, ) -> None: """Test connector put-items.""" create_result = await local_dynamo_connector_with_range.put_items( [ { IDENTITY_HASH_KEY: "new-test-uuid", IDENTITY_RANGE_KEY: "bonjour-test-uuid", "this": "is crazy", }, { IDENTITY_HASH_KEY: "new-test-uuid", IDENTITY_RANGE_KEY: "howdy-test-uuid", "heres": "my number", }, ] ) assert create_result == [] query_result = local_dynamo_connector_with_range.query_by_hash_key( "new-test-uuid", ) items = query_result.get("items") assert items assert { IDENTITY_HASH_KEY: {"S": "new-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "bonjour-test-uuid"}, "this": {"S": "is crazy"}, } in items assert { IDENTITY_HASH_KEY: {"S": "new-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "howdy-test-uuid"}, "heres": {"S": "my number"}, } in items async def test_connector_put_items_when_existing( default_boto_client: DynamoDBClient, local_dynamo_connector_with_range: DynamoDbConnector, seed_test_table: None, ) -> None: """Test connector put_items overwrites when item already exists.""" local_dynamo_connector_with_range.update_item( hash_key="dododo-test-uuid", item={ "all": {"S": "attributes"}, "that": {"S": "will be overwritten"}, }, range_key="bonjour-test-uuid", ) seeded_data = local_dynamo_connector_with_range.query_by_hash_key( "dododo-test-uuid", ) seeded_items = seeded_data.get("items") assert seeded_items assert { IDENTITY_HASH_KEY: {"S": "dododo-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "bonjour-test-uuid"}, "all": {"S": "attributes"}, "that": {"S": "will be overwritten"}, } in seeded_items, "Test data should be seeded" create_result = await local_dynamo_connector_with_range.put_items( [ { IDENTITY_HASH_KEY: "dododo-test-uuid", IDENTITY_RANGE_KEY: "bonjour-test-uuid", "this": "is crazy", }, ] ) assert create_result == [] query_result = local_dynamo_connector_with_range.query_by_hash_key( "dododo-test-uuid", ) items = query_result.get("items") assert items assert { IDENTITY_HASH_KEY: {"S": "dododo-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "bonjour-test-uuid"}, "this": {"S": "is crazy"}, } in items, "item attributes are overwritten, not updated" async def test_connector_put_items_missing_range_key( default_boto_client: DynamoDBClient, local_dynamo_connector_with_range: DynamoDbConnector, seed_test_table: None, ) -> None: """Test connector put_items fails if range key is missing.""" with pytest.raises(Exception) as exception_info: await local_dynamo_connector_with_range.put_items( [ { IDENTITY_HASH_KEY: "hello-test-uuid", "music": "pop", }, ] ) assert ( str(exception_info.value) == f"An error occurred (ValidationException) when calling the BatchWriteItem operation: One of the required keys was not given a value" # noqa E501 )