From 869bfc45acbc0b69aa7eb8537cc891c2d8660897 Mon Sep 17 00:00:00 2001 From: Ivan Golikov Date: Wed, 8 Jan 2025 21:44:41 +0100 Subject: [PATCH] Added support for all Redis versions (>=1.0.0) Previously support was provided for Redis>=6.2.0 --- pssecret_server/utils.py | 26 ++++++++++++++++++++++++ pyproject.toml | 3 +++ tests/integration/test_utils.py | 36 +++++++++++++++++++++++++++++++-- tests/unit/test_utils.py | 22 +++++++++++++++++++- 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/pssecret_server/utils.py b/pssecret_server/utils.py index e57b710..0fe0b23 100644 --- a/pssecret_server/utils.py +++ b/pssecret_server/utils.py @@ -1,7 +1,10 @@ +from functools import lru_cache from uuid import uuid4 from cryptography.fernet import Fernet from redis.asyncio import Redis +from redis.exceptions import ResponseError +from redis.typing import ResponseT from pssecret_server.models import Secret @@ -30,3 +33,26 @@ async def save_secret(data: Secret, redis: Redis) -> str: await redis.setex(new_key, 60 * 60 * 24, data.data) return new_key + + +@lru_cache +async def _is_getdel_available(redis: Redis) -> bool: + """GETDEL is not available in Redis prior to version 6.2""" + try: + await redis.getdel("test:getdel:availability") + except ResponseError: + return False + + return True + + +async def getdel(redis: Redis, key: str) -> ResponseT: + result: ResponseT + + if await _is_getdel_available(redis): + result = await redis.getdel(key) + else: + result = await redis.getset(key, "") + await redis.delete(key) + + return result diff --git a/pyproject.toml b/pyproject.toml index 3e746fb..8dc6c71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,3 +53,6 @@ reportUnusedCallResult = "none" [tool.pytest.ini_options] asyncio_mode = "auto" + +[tool.isort] +profile = "black" diff --git a/tests/integration/test_utils.py b/tests/integration/test_utils.py index bef19c4..0d9290d 100644 --- a/tests/integration/test_utils.py +++ b/tests/integration/test_utils.py @@ -1,8 +1,8 @@ -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch from redis.asyncio import Redis -from pssecret_server.utils import get_new_key, save_secret +from pssecret_server.utils import get_new_key, getdel, save_secret from ..factories import SecretFactory @@ -33,3 +33,35 @@ async def test_save_secret_data(redis_server: Redis) -> None: assert redis_data is not None assert redis_data.decode() == secret.data + + +@patch("pssecret_server.utils._is_getdel_available", side_effect=AsyncMock()) +async def test_getdel_when_available( + is_getdel_available: Mock, redis_server: Redis +) -> None: + is_getdel_available.side_effect.return_value = True + + test_value = "test_data" + test_key = "test_key" + await redis_server.set(test_key, test_value) + + result = await getdel(redis_server, test_key) + + assert result.decode() == test_value + assert not await redis_server.exists(test_key) + + +@patch("pssecret_server.utils._is_getdel_available", side_effect=AsyncMock()) +async def test_getdel_when_not_available( + is_getdel_available: Mock, redis_server: Redis +) -> None: + is_getdel_available.side_effect.return_value = False + + test_value = "test_data" + test_key = "test_key" + await redis_server.set(test_key, test_value) + + result = await getdel(redis_server, test_key) + + assert result.decode() == test_value + assert not await redis_server.exists(test_key) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index de2984b..321bb40 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,7 +1,10 @@ +from unittest.mock import AsyncMock + import pytest from cryptography.fernet import Fernet, InvalidToken +from redis.exceptions import ResponseError -from pssecret_server.utils import decrypt_secret, encrypt_secret +from pssecret_server.utils import _is_getdel_available, decrypt_secret, encrypt_secret from ..factories import SecretFactory @@ -29,3 +32,20 @@ def test_secret_is_not_decryptable_by_random_key(fernet: Fernet): with pytest.raises(InvalidToken): decrypt_secret(encrypted_secret.data.encode(), random_fernet) + + +async def test_is_getdel_available_when_supported(): + redis = AsyncMock() + + result = await _is_getdel_available(redis) + + assert result is True + + +async def test_is_getdel_available_when_not_supported(): + redis = AsyncMock() + redis.getdel.side_effect = ResponseError + + result = await _is_getdel_available(redis) + + assert result is False