"""Integration Tests for Redis Connection methods.""" from typing import Any, Collection, Dict, List, TypeVar import pytest from pdp.connectors.redis_client import ( JSONSerializer, PPCacheItemSerializer, PydanticSchemaSerializer, RedisConnector, TypeAdapterSerializer, ) from pdp.fastapi.schemas.identity import RoleListValidator, RolesResponse T = TypeVar("T") @pytest.fixture() async def default_keys_redis_connector( local_redis_connector: RedisConnector, ) -> RedisConnector: """Redis connector with default entries written to cache.""" test_key_values = { "prefix_key1": "value1", "prefix_key2": "value2", "someother_key1": "value3", } await local_redis_connector.client.mset(test_key_values) return local_redis_connector async def test_ping(local_redis_connector: RedisConnector) -> None: """Test the PING endpoint.""" ret = await local_redis_connector.ping() assert ret is True @pytest.mark.parametrize( "cache_key, cached_objects, serializer", [ pytest.param( "foo#identity_uuid", {"key": "value"}, JSONSerializer(), id="GET should return a Python dictionary object.", ), pytest.param( "roles_response#identity_uuid", RolesResponse.model_validate( { "tenants": { "1e3a0a9b-289d-4a3a-a165-d82a6dbe144d": { "tenant_type": "account", "tenant_uuid": "1e3a0a9b-289d-4a3a-a165-d82a6dbe144d", "roles": [{"role": "cat_scratcher"}], } }, "cursor": {"cursor": None}, } ), PydanticSchemaSerializer(RolesResponse), id="GET should return a RolesResponse schema object", ), pytest.param( "roles_list#identity_uuid", RoleListValidator.validate_python( [{"role": "zookeeper"}, {"role": "beekeeper"}] ), TypeAdapterSerializer(RoleListValidator), id="GET should return a List[Role] object", ), pytest.param( "no.such.key", None, JSONSerializer(), id="GET should handle cache misses.", ), ], ) async def test_set_and_get( cache_key: str, cached_objects: T, serializer: PPCacheItemSerializer, local_redis_connector: RedisConnector, ) -> None: """Test the cache SET and GET methods.""" # NOTE: SET will ignore null items. await local_redis_connector.set( key=cache_key, item=cached_objects, serializer=serializer ) # Read the key / value pair result = await local_redis_connector.get(key=cache_key, serializer=serializer) # type: ignore assert result == cached_objects @pytest.mark.parametrize( "key_value_objects_to_cache, expected_return_values, serializer", [ pytest.param( { "foo#identity_uuid_01": {"key1": "value1"}, "foo#identity_uuid_02": {"key2": "value2"}, }, [{"key1": "value1"}, {"key2": "value2"}], JSONSerializer(), id="MGET should return Python dictionary objects for multiple keys.", ), pytest.param( { "roles_response#identity_uuid_01": RolesResponse.model_validate( { "tenants": { "04b48f72-5b47-425f-8b49-21f1ebc3f0cd": { "tenant_type": "account", "tenant_uuid": "04b48f72-5b47-425f-8b49-21f1ebc3f0cd", "roles": [{"role": "dog_whisperer"}], } }, "cursor": {"cursor": None}, } ), "roles_response#identity_uuid_02": RolesResponse.model_validate( { "tenants": { "1e3a0a9b-289d-4a3a-a165-d82a6dbe144d": { "tenant_type": "account", "tenant_uuid": "1e3a0a9b-289d-4a3a-a165-d82a6dbe144d", "roles": [{"role": "cat_scratcher"}], } }, "cursor": {"cursor": None}, } ), }, [ RolesResponse.model_validate( { "tenants": { "04b48f72-5b47-425f-8b49-21f1ebc3f0cd": { "tenant_type": "account", "tenant_uuid": "04b48f72-5b47-425f-8b49-21f1ebc3f0cd", "roles": [{"role": "dog_whisperer"}], } }, "cursor": {"cursor": None}, } ), RolesResponse.model_validate( { "tenants": { "1e3a0a9b-289d-4a3a-a165-d82a6dbe144d": { "tenant_type": "account", "tenant_uuid": "1e3a0a9b-289d-4a3a-a165-d82a6dbe144d", "roles": [{"role": "cat_scratcher"}], } }, "cursor": {"cursor": None}, } ), ], PydanticSchemaSerializer(RolesResponse), id="MGET should return a RolesResponse for multiple keys.", ), pytest.param( { "roles_list#identity_uuid_01": RoleListValidator.validate_python( [{"role": "zookeeper"}, {"role": "beekeeper"}] ), "roles_list#identity_uuid_02": RoleListValidator.validate_python( [{"role": "beast_titan"}, {"role": "armor_titan"}] ), }, [ RoleListValidator.validate_python( [{"role": "zookeeper"}, {"role": "beekeeper"}] ), RoleListValidator.validate_python( [{"role": "beast_titan"}, {"role": "armor_titan"}] ), ], TypeAdapterSerializer(RoleListValidator), id="MGET should return a List[Role] object for multiple keys.", ), pytest.param( { "foo#identity_uuid_01": {"key1": "value1"}, "no.such.key": None, }, [{"key1": "value1"}, None], JSONSerializer(), id="MGET should handle cache misses.", ), ], ) async def test_set_and_mget( key_value_objects_to_cache: Dict[str, Any], expected_return_values: List[Any], serializer: PPCacheItemSerializer, local_redis_connector: RedisConnector, ) -> None: """Test the cache SET and MGET methods.""" keys = [k for k in key_value_objects_to_cache.keys()] for k, v in key_value_objects_to_cache.items(): # SET will ignore null items await local_redis_connector.set(k, v, serializer=serializer) result = await local_redis_connector.mget(keys=keys, serializer=serializer) assert result == expected_return_values @pytest.mark.parametrize( "key_value_objects_to_cache, expected_return_values, serializer", [ pytest.param( { "foo#identity_uuid_01": {"key1": "value1"}, "foo#identity_uuid_02": {"key2": "value2"}, }, [{"key1": "value1"}, {"key2": "value2"}], JSONSerializer(), id="MSET should set multiple keys.", ), pytest.param( { "roles_response#identity_uuid_01": RolesResponse.model_validate( { "tenants": { "04b48f72-5b47-425f-8b49-21f1ebc3f0cd": { "tenant_type": "account", "tenant_uuid": "04b48f72-5b47-425f-8b49-21f1ebc3f0cd", "roles": [{"role": "dog_whisperer"}], } }, "cursor": {"cursor": None}, } ), "roles_response#identity_uuid_02": RolesResponse.model_validate( { "tenants": { "1e3a0a9b-289d-4a3a-a165-d82a6dbe144d": { "tenant_type": "account", "tenant_uuid": "1e3a0a9b-289d-4a3a-a165-d82a6dbe144d", "roles": [{"role": "cat_scratcher"}], } }, "cursor": {"cursor": None}, } ), }, [ RolesResponse.model_validate( { "tenants": { "04b48f72-5b47-425f-8b49-21f1ebc3f0cd": { "tenant_type": "account", "tenant_uuid": "04b48f72-5b47-425f-8b49-21f1ebc3f0cd", "roles": [{"role": "dog_whisperer"}], } }, "cursor": {"cursor": None}, } ), RolesResponse.model_validate( { "tenants": { "1e3a0a9b-289d-4a3a-a165-d82a6dbe144d": { "tenant_type": "account", "tenant_uuid": "1e3a0a9b-289d-4a3a-a165-d82a6dbe144d", "roles": [{"role": "cat_scratcher"}], } }, "cursor": {"cursor": None}, } ), ], PydanticSchemaSerializer(RolesResponse), id="MSET should set RolesResponses for multiple keys.", ), pytest.param( { "roles_list#identity_uuid_01": RoleListValidator.validate_python( [{"role": "zookeeper"}, {"role": "beekeeper"}] ), "roles_list#identity_uuid_02": RoleListValidator.validate_python( [{"role": "beast_titan"}, {"role": "armor_titan"}] ), }, [ RoleListValidator.validate_python( [{"role": "zookeeper"}, {"role": "beekeeper"}] ), RoleListValidator.validate_python( [{"role": "beast_titan"}, {"role": "armor_titan"}] ), ], TypeAdapterSerializer(RoleListValidator), id="MSET should set Role lists for multiple keys.", ), ], ) @pytest.mark.parametrize( "mset_method_name", [ ("mset"), ("mset_with_pipeline"), ], ) async def test_mset_and_mget( mset_method_name: str, key_value_objects_to_cache: Dict[str, Any], expected_return_values: List[Any], serializer: PPCacheItemSerializer, local_redis_connector: RedisConnector, ) -> None: """Test the cache MSET and MGET methods.""" keys = [k for k in key_value_objects_to_cache.keys()] mset_method = getattr(local_redis_connector, mset_method_name) await mset_method(key_item_dict=key_value_objects_to_cache, serializer=serializer) result = await local_redis_connector.mget(keys=keys, serializer=serializer) assert result == expected_return_values @pytest.mark.parametrize( "keys_to_delete, expected_num_keys_deleted, expected_remaining_keys", [ pytest.param( ["prefix_key1", "prefix_key2"], 2, {b"someother_key1"}, id="Delete should succeed when the cache keys exist", ), pytest.param( ["no.such.keys"], 0, {b"someother_key1", b"prefix_key1", b"prefix_key2"}, id="Delete should succeed when the cache keys exist", ), pytest.param( [None], 0, {b"someother_key1", b"prefix_key1", b"prefix_key2"}, id="Delete should skip null keys.", ), ], ) async def test_delete( keys_to_delete: List[str], expected_num_keys_deleted: int, expected_remaining_keys: Collection[bytes], default_keys_redis_connector: RedisConnector, ) -> None: """Test the cache DELETE method.""" # Delete the keys: result = await default_keys_redis_connector.delete(*keys_to_delete) assert result == expected_num_keys_deleted # Verify remaining keys after deletion. result_keys = await default_keys_redis_connector.client.keys("*") assert set(result_keys) == expected_remaining_keys @pytest.mark.parametrize( "key_pattern, expected_result", [ pytest.param( "prefix_*", {"prefix_key1", "prefix_key2"}, id="list should return keys for a matching pattern", ), pytest.param( "no.such.prefix_*", set(), id="list should return 0 keys for a non-matching pattern", ), pytest.param( None, set(), id="list should return 0 keys for a null pattern", ), ], ) async def test_list( key_pattern: str, expected_result: Collection[str], default_keys_redis_connector: RedisConnector, ) -> None: """Test the KEYS list method.""" result = await default_keys_redis_connector.list(key_pattern) assert set(result) == expected_result @pytest.mark.parametrize( "key_pattern, expected_result, expected_remaining_keys", [ pytest.param( "prefix_*", {"prefix_key1", "prefix_key2"}, {b"someother_key1"}, id="The method should return keys for a matching pattern", ), pytest.param( "no.such.prefix_*", set(), {b"someother_key1", b"prefix_key1", b"prefix_key2"}, id="The method should return 0 keys for a non-matching pattern", ), pytest.param( None, set(), {b"someother_key1", b"prefix_key1", b"prefix_key2"}, id="The method should return 0 keys for a null pattern", ), ], ) async def test_delete_all_matching_pattern( key_pattern: str, expected_result: Collection[str], expected_remaining_keys: Collection[bytes], default_keys_redis_connector: RedisConnector, ) -> None: """Test combining list and delete.""" result = await default_keys_redis_connector.delete_all_matching_pattern(key_pattern) assert set(result) == expected_result remaining_keys = await default_keys_redis_connector.client.keys("*") assert set(remaining_keys) == expected_remaining_keys @pytest.mark.parametrize( "key_value_objects_to_cache, " "expected_return_values, expected_remaining_keys, serializer", [ pytest.param( { "foo#identity_uuid_01": {"key1": "value1"}, "foo#identity_uuid_02": {"key2": "value2"}, }, [{"key1": "value1"}, {"key2": "value2"}], {b"someother_key1", b"prefix_key1", b"prefix_key2"}, JSONSerializer(), id="GETDEL should return Python dictionary objects for multiple keys.", ), pytest.param( { "foo#identity_uuid_01": {"key1": "value1"}, "no.such.key": None, }, [{"key1": "value1"}, None], {b"someother_key1", b"prefix_key1", b"prefix_key2"}, JSONSerializer(), id="GETDEL should handle cache misses.", ), ], ) async def test_getdel( key_value_objects_to_cache: Dict[str, Any], expected_return_values: List[Any], expected_remaining_keys: Collection[bytes], serializer: PPCacheItemSerializer, default_keys_redis_connector: RedisConnector, ) -> None: """Test the cache GETDEL methods.""" keys = [k for k in key_value_objects_to_cache.keys()] for k, v in key_value_objects_to_cache.items(): # SET will ignore null items await default_keys_redis_connector.set(k, v, serializer=serializer) result = await default_keys_redis_connector.getdel(keys=keys, serializer=serializer) assert result == expected_return_values remaining_keys = await default_keys_redis_connector.client.keys("*") assert set(remaining_keys) == expected_remaining_keys