first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

View File

@@ -0,0 +1,573 @@
"""
Unit tests for Redis Service.
These tests use mocking to test the RedisService without requiring
a real Redis connection.
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
import json
from redis.exceptions import RedisError, ConnectionError as RedisConnectionError
from app.services.redis_service import (
RedisService,
RedisServiceError,
RedisConnectionFailed
)
class TestRedisServiceInit:
"""Test RedisService initialization."""
@patch('app.services.redis_service.redis.ConnectionPool.from_url')
@patch('app.services.redis_service.redis.Redis')
def test_init_success(self, mock_redis_class, mock_pool_from_url):
"""Test successful initialization."""
# Setup mocks
mock_pool = MagicMock()
mock_pool_from_url.return_value = mock_pool
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_redis_class.return_value = mock_client
# Create service
service = RedisService(redis_url='redis://localhost:6379/0')
# Verify
mock_pool_from_url.assert_called_once()
mock_redis_class.assert_called_once_with(connection_pool=mock_pool)
mock_client.ping.assert_called_once()
assert service.client == mock_client
@patch('app.services.redis_service.redis.ConnectionPool.from_url')
def test_init_connection_failed(self, mock_pool_from_url):
"""Test initialization fails when Redis is unavailable."""
mock_pool_from_url.side_effect = RedisConnectionError("Connection refused")
with pytest.raises(RedisConnectionFailed) as exc_info:
RedisService(redis_url='redis://localhost:6379/0')
assert "Could not connect to Redis" in str(exc_info.value)
def test_init_missing_url(self):
"""Test initialization fails with missing URL."""
# Clear environment variable and pass empty string
with patch.dict('os.environ', {'REDIS_URL': ''}, clear=True):
with pytest.raises(ValueError) as exc_info:
RedisService(redis_url='')
assert "Redis URL not configured" in str(exc_info.value)
class TestRedisServiceOperations:
"""Test Redis operations (get, set, delete, exists)."""
@pytest.fixture
def redis_service(self):
"""Create a RedisService with mocked client."""
with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
mock_pool = MagicMock()
mock_pool_from_url.return_value = mock_pool
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_redis_class.return_value = mock_client
service = RedisService(redis_url='redis://localhost:6379/0')
yield service
def test_get_existing_key(self, redis_service):
"""Test getting an existing key."""
redis_service.client.get.return_value = "test_value"
result = redis_service.get("test_key")
redis_service.client.get.assert_called_once_with("test_key")
assert result == "test_value"
def test_get_nonexistent_key(self, redis_service):
"""Test getting a non-existent key returns None."""
redis_service.client.get.return_value = None
result = redis_service.get("nonexistent_key")
assert result is None
def test_get_error(self, redis_service):
"""Test get raises RedisServiceError on failure."""
redis_service.client.get.side_effect = RedisError("Connection lost")
with pytest.raises(RedisServiceError) as exc_info:
redis_service.get("test_key")
assert "Failed to get key" in str(exc_info.value)
def test_set_basic(self, redis_service):
"""Test basic set operation."""
redis_service.client.set.return_value = True
result = redis_service.set("test_key", "test_value")
redis_service.client.set.assert_called_once_with(
"test_key", "test_value", ex=None, nx=False, xx=False
)
assert result is True
def test_set_with_ttl(self, redis_service):
"""Test set with TTL."""
redis_service.client.set.return_value = True
result = redis_service.set("test_key", "test_value", ttl=3600)
redis_service.client.set.assert_called_once_with(
"test_key", "test_value", ex=3600, nx=False, xx=False
)
assert result is True
def test_set_nx_success(self, redis_service):
"""Test set with NX (only if not exists) - success."""
redis_service.client.set.return_value = True
result = redis_service.set("test_key", "test_value", nx=True)
redis_service.client.set.assert_called_once_with(
"test_key", "test_value", ex=None, nx=True, xx=False
)
assert result is True
def test_set_nx_failure(self, redis_service):
"""Test set with NX fails when key exists."""
redis_service.client.set.return_value = None # NX returns None if key exists
result = redis_service.set("test_key", "test_value", nx=True)
assert result is False
def test_set_error(self, redis_service):
"""Test set raises RedisServiceError on failure."""
redis_service.client.set.side_effect = RedisError("Connection lost")
with pytest.raises(RedisServiceError) as exc_info:
redis_service.set("test_key", "test_value")
assert "Failed to set key" in str(exc_info.value)
def test_delete_single_key(self, redis_service):
"""Test deleting a single key."""
redis_service.client.delete.return_value = 1
result = redis_service.delete("test_key")
redis_service.client.delete.assert_called_once_with("test_key")
assert result == 1
def test_delete_multiple_keys(self, redis_service):
"""Test deleting multiple keys."""
redis_service.client.delete.return_value = 3
result = redis_service.delete("key1", "key2", "key3")
redis_service.client.delete.assert_called_once_with("key1", "key2", "key3")
assert result == 3
def test_delete_no_keys(self, redis_service):
"""Test delete with no keys returns 0."""
result = redis_service.delete()
redis_service.client.delete.assert_not_called()
assert result == 0
def test_delete_error(self, redis_service):
"""Test delete raises RedisServiceError on failure."""
redis_service.client.delete.side_effect = RedisError("Connection lost")
with pytest.raises(RedisServiceError) as exc_info:
redis_service.delete("test_key")
assert "Failed to delete keys" in str(exc_info.value)
def test_exists_single_key(self, redis_service):
"""Test checking existence of a single key."""
redis_service.client.exists.return_value = 1
result = redis_service.exists("test_key")
redis_service.client.exists.assert_called_once_with("test_key")
assert result == 1
def test_exists_multiple_keys(self, redis_service):
"""Test checking existence of multiple keys."""
redis_service.client.exists.return_value = 2
result = redis_service.exists("key1", "key2", "key3")
redis_service.client.exists.assert_called_once_with("key1", "key2", "key3")
assert result == 2
def test_exists_no_keys(self, redis_service):
"""Test exists with no keys returns 0."""
result = redis_service.exists()
redis_service.client.exists.assert_not_called()
assert result == 0
def test_exists_error(self, redis_service):
"""Test exists raises RedisServiceError on failure."""
redis_service.client.exists.side_effect = RedisError("Connection lost")
with pytest.raises(RedisServiceError) as exc_info:
redis_service.exists("test_key")
assert "Failed to check existence" in str(exc_info.value)
class TestRedisServiceJSON:
"""Test JSON serialization methods."""
@pytest.fixture
def redis_service(self):
"""Create a RedisService with mocked client."""
with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
mock_pool = MagicMock()
mock_pool_from_url.return_value = mock_pool
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_redis_class.return_value = mock_client
service = RedisService(redis_url='redis://localhost:6379/0')
yield service
def test_get_json_success(self, redis_service):
"""Test getting and deserializing JSON."""
test_data = {"name": "test", "value": 123, "nested": {"key": "value"}}
redis_service.client.get.return_value = json.dumps(test_data)
result = redis_service.get_json("test_key")
assert result == test_data
def test_get_json_none(self, redis_service):
"""Test get_json returns None for non-existent key."""
redis_service.client.get.return_value = None
result = redis_service.get_json("nonexistent_key")
assert result is None
def test_get_json_invalid(self, redis_service):
"""Test get_json raises error for invalid JSON."""
redis_service.client.get.return_value = "not valid json {"
with pytest.raises(RedisServiceError) as exc_info:
redis_service.get_json("test_key")
assert "Failed to decode JSON" in str(exc_info.value)
def test_set_json_success(self, redis_service):
"""Test serializing and setting JSON."""
redis_service.client.set.return_value = True
test_data = {"name": "test", "value": 123}
result = redis_service.set_json("test_key", test_data, ttl=3600)
# Verify the value was serialized
call_args = redis_service.client.set.call_args
stored_value = call_args[0][1]
assert json.loads(stored_value) == test_data
assert result is True
def test_set_json_non_serializable(self, redis_service):
"""Test set_json raises error for non-serializable data."""
non_serializable = {"func": lambda x: x}
with pytest.raises(RedisServiceError) as exc_info:
redis_service.set_json("test_key", non_serializable)
assert "Failed to serialize value" in str(exc_info.value)
class TestRedisServiceTTL:
"""Test TTL-related operations."""
@pytest.fixture
def redis_service(self):
"""Create a RedisService with mocked client."""
with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
mock_pool = MagicMock()
mock_pool_from_url.return_value = mock_pool
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_redis_class.return_value = mock_client
service = RedisService(redis_url='redis://localhost:6379/0')
yield service
def test_expire_success(self, redis_service):
"""Test setting expiration on existing key."""
redis_service.client.expire.return_value = True
result = redis_service.expire("test_key", 3600)
redis_service.client.expire.assert_called_once_with("test_key", 3600)
assert result is True
def test_expire_nonexistent_key(self, redis_service):
"""Test expire returns False for non-existent key."""
redis_service.client.expire.return_value = False
result = redis_service.expire("nonexistent_key", 3600)
assert result is False
def test_ttl_existing_key(self, redis_service):
"""Test getting TTL of existing key."""
redis_service.client.ttl.return_value = 3500
result = redis_service.ttl("test_key")
redis_service.client.ttl.assert_called_once_with("test_key")
assert result == 3500
def test_ttl_no_expiry(self, redis_service):
"""Test TTL returns -1 for key without expiry."""
redis_service.client.ttl.return_value = -1
result = redis_service.ttl("test_key")
assert result == -1
def test_ttl_nonexistent_key(self, redis_service):
"""Test TTL returns -2 for non-existent key."""
redis_service.client.ttl.return_value = -2
result = redis_service.ttl("test_key")
assert result == -2
class TestRedisServiceIncrement:
"""Test increment/decrement operations."""
@pytest.fixture
def redis_service(self):
"""Create a RedisService with mocked client."""
with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
mock_pool = MagicMock()
mock_pool_from_url.return_value = mock_pool
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_redis_class.return_value = mock_client
service = RedisService(redis_url='redis://localhost:6379/0')
yield service
def test_incr_default(self, redis_service):
"""Test incrementing by default amount (1)."""
redis_service.client.incrby.return_value = 5
result = redis_service.incr("counter")
redis_service.client.incrby.assert_called_once_with("counter", 1)
assert result == 5
def test_incr_custom_amount(self, redis_service):
"""Test incrementing by custom amount."""
redis_service.client.incrby.return_value = 15
result = redis_service.incr("counter", 10)
redis_service.client.incrby.assert_called_once_with("counter", 10)
assert result == 15
def test_decr_default(self, redis_service):
"""Test decrementing by default amount (1)."""
redis_service.client.decrby.return_value = 4
result = redis_service.decr("counter")
redis_service.client.decrby.assert_called_once_with("counter", 1)
assert result == 4
def test_decr_custom_amount(self, redis_service):
"""Test decrementing by custom amount."""
redis_service.client.decrby.return_value = 0
result = redis_service.decr("counter", 5)
redis_service.client.decrby.assert_called_once_with("counter", 5)
assert result == 0
class TestRedisServiceHealth:
"""Test health check and info operations."""
@pytest.fixture
def redis_service(self):
"""Create a RedisService with mocked client."""
with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
mock_pool = MagicMock()
mock_pool_from_url.return_value = mock_pool
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_redis_class.return_value = mock_client
service = RedisService(redis_url='redis://localhost:6379/0')
yield service
def test_health_check_success(self, redis_service):
"""Test health check when Redis is healthy."""
redis_service.client.ping.return_value = True
result = redis_service.health_check()
assert result is True
def test_health_check_failure(self, redis_service):
"""Test health check when Redis is unhealthy."""
redis_service.client.ping.side_effect = RedisError("Connection lost")
result = redis_service.health_check()
assert result is False
def test_info_success(self, redis_service):
"""Test getting Redis info."""
mock_info = {
'redis_version': '7.0.0',
'used_memory': 1000000,
'connected_clients': 5
}
redis_service.client.info.return_value = mock_info
result = redis_service.info()
assert result == mock_info
def test_info_error(self, redis_service):
"""Test info raises error on failure."""
redis_service.client.info.side_effect = RedisError("Connection lost")
with pytest.raises(RedisServiceError) as exc_info:
redis_service.info()
assert "Failed to get Redis info" in str(exc_info.value)
class TestRedisServiceUtility:
"""Test utility methods."""
@pytest.fixture
def redis_service(self):
"""Create a RedisService with mocked client."""
with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
mock_pool = MagicMock()
mock_pool_from_url.return_value = mock_pool
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_redis_class.return_value = mock_client
service = RedisService(redis_url='redis://localhost:6379/0')
yield service
def test_flush_db(self, redis_service):
"""Test flushing database."""
redis_service.client.flushdb.return_value = True
result = redis_service.flush_db()
redis_service.client.flushdb.assert_called_once()
assert result is True
def test_close(self, redis_service):
"""Test closing connection pool."""
redis_service.close()
redis_service.pool.disconnect.assert_called_once()
def test_context_manager(self, redis_service):
"""Test using service as context manager."""
with redis_service as service:
assert service is not None
redis_service.pool.disconnect.assert_called_once()
def test_sanitize_url_with_password(self, redis_service):
"""Test URL sanitization masks password."""
url = "redis://user:secretpassword@localhost:6379/0"
result = redis_service._sanitize_url(url)
assert "secretpassword" not in result
assert "***" in result
assert "localhost:6379/0" in result
def test_sanitize_url_without_password(self, redis_service):
"""Test URL sanitization with no password."""
url = "redis://localhost:6379/0"
result = redis_service._sanitize_url(url)
assert result == url
class TestRedisServiceIntegration:
"""Integration-style tests that verify the flow of operations."""
@pytest.fixture
def redis_service(self):
"""Create a RedisService with mocked client."""
with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
mock_pool = MagicMock()
mock_pool_from_url.return_value = mock_pool
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_redis_class.return_value = mock_client
service = RedisService(redis_url='redis://localhost:6379/0')
yield service
def test_set_then_get(self, redis_service):
"""Test setting and then getting a value."""
# Set
redis_service.client.set.return_value = True
redis_service.set("test_key", "test_value")
# Get
redis_service.client.get.return_value = "test_value"
result = redis_service.get("test_key")
assert result == "test_value"
def test_json_roundtrip(self, redis_service):
"""Test JSON serialization roundtrip."""
test_data = {
"user_id": "user_123",
"tokens_used": 450,
"model": "claude-3-5-haiku"
}
# Set JSON
redis_service.client.set.return_value = True
redis_service.set_json("job_result", test_data, ttl=3600)
# Get JSON
redis_service.client.get.return_value = json.dumps(test_data)
result = redis_service.get_json("job_result")
assert result == test_data