794 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			794 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Python
		
	
	
	
| import pytest
 | |
| from unittest.mock import Mock, patch, AsyncMock
 | |
| import redis
 | |
| from open_webui.utils.redis import (
 | |
|     SentinelRedisProxy,
 | |
|     parse_redis_service_url,
 | |
|     get_redis_connection,
 | |
|     get_sentinels_from_env,
 | |
|     MAX_RETRY_COUNT,
 | |
| )
 | |
| import inspect
 | |
| 
 | |
| 
 | |
| class TestSentinelRedisProxy:
 | |
|     """Test Redis Sentinel failover functionality"""
 | |
| 
 | |
|     def test_parse_redis_service_url_valid(self):
 | |
|         """Test parsing valid Redis service URL"""
 | |
|         url = "redis://user:pass@mymaster:6379/0"
 | |
|         result = parse_redis_service_url(url)
 | |
| 
 | |
|         assert result["username"] == "user"
 | |
|         assert result["password"] == "pass"
 | |
|         assert result["service"] == "mymaster"
 | |
|         assert result["port"] == 6379
 | |
|         assert result["db"] == 0
 | |
| 
 | |
|     def test_parse_redis_service_url_defaults(self):
 | |
|         """Test parsing Redis service URL with defaults"""
 | |
|         url = "redis://mymaster"
 | |
|         result = parse_redis_service_url(url)
 | |
| 
 | |
|         assert result["username"] is None
 | |
|         assert result["password"] is None
 | |
|         assert result["service"] == "mymaster"
 | |
|         assert result["port"] == 6379
 | |
|         assert result["db"] == 0
 | |
| 
 | |
|     def test_parse_redis_service_url_invalid_scheme(self):
 | |
|         """Test parsing invalid URL scheme"""
 | |
|         with pytest.raises(ValueError, match="Invalid Redis URL scheme"):
 | |
|             parse_redis_service_url("http://invalid")
 | |
| 
 | |
|     def test_get_sentinels_from_env(self):
 | |
|         """Test parsing sentinel hosts from environment"""
 | |
|         hosts = "sentinel1,sentinel2,sentinel3"
 | |
|         port = "26379"
 | |
| 
 | |
|         result = get_sentinels_from_env(hosts, port)
 | |
|         expected = [("sentinel1", 26379), ("sentinel2", 26379), ("sentinel3", 26379)]
 | |
| 
 | |
|         assert result == expected
 | |
| 
 | |
|     def test_get_sentinels_from_env_empty(self):
 | |
|         """Test empty sentinel hosts"""
 | |
|         result = get_sentinels_from_env(None, "26379")
 | |
|         assert result == []
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_sentinel_redis_proxy_sync_success(self, mock_sentinel_class):
 | |
|         """Test successful sync operation with SentinelRedisProxy"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
|         mock_master.get.return_value = "test_value"
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         # Test attribute access
 | |
|         get_method = proxy.__getattr__("get")
 | |
|         result = get_method("test_key")
 | |
| 
 | |
|         assert result == "test_value"
 | |
|         mock_sentinel.master_for.assert_called_with("mymaster")
 | |
|         mock_master.get.assert_called_with("test_key")
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_sentinel_redis_proxy_async_success(self, mock_sentinel_class):
 | |
|         """Test successful async operation with SentinelRedisProxy"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
|         mock_master.get = AsyncMock(return_value="test_value")
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test async attribute access
 | |
|         get_method = proxy.__getattr__("get")
 | |
|         result = await get_method("test_key")
 | |
| 
 | |
|         assert result == "test_value"
 | |
|         mock_sentinel.master_for.assert_called_with("mymaster")
 | |
|         mock_master.get.assert_called_with("test_key")
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_sentinel_redis_proxy_failover_retry(self, mock_sentinel_class):
 | |
|         """Test retry mechanism during failover"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # First call fails, second succeeds
 | |
|         mock_master.get.side_effect = [
 | |
|             redis.exceptions.ConnectionError("Master down"),
 | |
|             "test_value",
 | |
|         ]
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         get_method = proxy.__getattr__("get")
 | |
|         result = get_method("test_key")
 | |
| 
 | |
|         assert result == "test_value"
 | |
|         assert mock_master.get.call_count == 2
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_sentinel_redis_proxy_max_retries_exceeded(self, mock_sentinel_class):
 | |
|         """Test failure after max retries exceeded"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # All calls fail
 | |
|         mock_master.get.side_effect = redis.exceptions.ConnectionError("Master down")
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         get_method = proxy.__getattr__("get")
 | |
| 
 | |
|         with pytest.raises(redis.exceptions.ConnectionError):
 | |
|             get_method("test_key")
 | |
| 
 | |
|         assert mock_master.get.call_count == MAX_RETRY_COUNT
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_sentinel_redis_proxy_readonly_error_retry(self, mock_sentinel_class):
 | |
|         """Test retry on ReadOnlyError"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # First call gets ReadOnlyError (old master), second succeeds (new master)
 | |
|         mock_master.get.side_effect = [
 | |
|             redis.exceptions.ReadOnlyError("Read only"),
 | |
|             "test_value",
 | |
|         ]
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         get_method = proxy.__getattr__("get")
 | |
|         result = get_method("test_key")
 | |
| 
 | |
|         assert result == "test_value"
 | |
|         assert mock_master.get.call_count == 2
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_sentinel_redis_proxy_factory_methods(self, mock_sentinel_class):
 | |
|         """Test factory methods are passed through directly"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
|         mock_pipeline = Mock()
 | |
|         mock_master.pipeline.return_value = mock_pipeline
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         # Factory methods should be passed through without wrapping
 | |
|         pipeline_method = proxy.__getattr__("pipeline")
 | |
|         result = pipeline_method()
 | |
| 
 | |
|         assert result == mock_pipeline
 | |
|         mock_master.pipeline.assert_called_once()
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @patch("redis.from_url")
 | |
|     def test_get_redis_connection_with_sentinel(
 | |
|         self, mock_from_url, mock_sentinel_class
 | |
|     ):
 | |
|         """Test getting Redis connection with Sentinel"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_sentinel_class.return_value = mock_sentinel
 | |
| 
 | |
|         sentinels = [("sentinel1", 26379), ("sentinel2", 26379)]
 | |
|         redis_url = "redis://user:pass@mymaster:6379/0"
 | |
| 
 | |
|         result = get_redis_connection(
 | |
|             redis_url=redis_url, redis_sentinels=sentinels, async_mode=False
 | |
|         )
 | |
| 
 | |
|         assert isinstance(result, SentinelRedisProxy)
 | |
|         mock_sentinel_class.assert_called_once()
 | |
|         mock_from_url.assert_not_called()
 | |
| 
 | |
|     @patch("redis.Redis.from_url")
 | |
|     def test_get_redis_connection_without_sentinel(self, mock_from_url):
 | |
|         """Test getting Redis connection without Sentinel"""
 | |
|         mock_redis = Mock()
 | |
|         mock_from_url.return_value = mock_redis
 | |
| 
 | |
|         redis_url = "redis://localhost:6379/0"
 | |
| 
 | |
|         result = get_redis_connection(
 | |
|             redis_url=redis_url, redis_sentinels=None, async_mode=False
 | |
|         )
 | |
| 
 | |
|         assert result == mock_redis
 | |
|         mock_from_url.assert_called_once_with(redis_url, decode_responses=True)
 | |
| 
 | |
|     @patch("redis.asyncio.from_url")
 | |
|     def test_get_redis_connection_without_sentinel_async(self, mock_from_url):
 | |
|         """Test getting async Redis connection without Sentinel"""
 | |
|         mock_redis = Mock()
 | |
|         mock_from_url.return_value = mock_redis
 | |
| 
 | |
|         redis_url = "redis://localhost:6379/0"
 | |
| 
 | |
|         result = get_redis_connection(
 | |
|             redis_url=redis_url, redis_sentinels=None, async_mode=True
 | |
|         )
 | |
| 
 | |
|         assert result == mock_redis
 | |
|         mock_from_url.assert_called_once_with(redis_url, decode_responses=True)
 | |
| 
 | |
| 
 | |
| class TestSentinelRedisProxyCommands:
 | |
|     """Test Redis commands through SentinelRedisProxy"""
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_hash_commands_sync(self, mock_sentinel_class):
 | |
|         """Test Redis hash commands in sync mode"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # Mock hash command responses
 | |
|         mock_master.hset.return_value = 1
 | |
|         mock_master.hget.return_value = "test_value"
 | |
|         mock_master.hgetall.return_value = {"key1": "value1", "key2": "value2"}
 | |
|         mock_master.hdel.return_value = 1
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         # Test hset
 | |
|         hset_method = proxy.__getattr__("hset")
 | |
|         result = hset_method("test_hash", "field1", "value1")
 | |
|         assert result == 1
 | |
|         mock_master.hset.assert_called_with("test_hash", "field1", "value1")
 | |
| 
 | |
|         # Test hget
 | |
|         hget_method = proxy.__getattr__("hget")
 | |
|         result = hget_method("test_hash", "field1")
 | |
|         assert result == "test_value"
 | |
|         mock_master.hget.assert_called_with("test_hash", "field1")
 | |
| 
 | |
|         # Test hgetall
 | |
|         hgetall_method = proxy.__getattr__("hgetall")
 | |
|         result = hgetall_method("test_hash")
 | |
|         assert result == {"key1": "value1", "key2": "value2"}
 | |
|         mock_master.hgetall.assert_called_with("test_hash")
 | |
| 
 | |
|         # Test hdel
 | |
|         hdel_method = proxy.__getattr__("hdel")
 | |
|         result = hdel_method("test_hash", "field1")
 | |
|         assert result == 1
 | |
|         mock_master.hdel.assert_called_with("test_hash", "field1")
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_hash_commands_async(self, mock_sentinel_class):
 | |
|         """Test Redis hash commands in async mode"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # Mock async hash command responses
 | |
|         mock_master.hset = AsyncMock(return_value=1)
 | |
|         mock_master.hget = AsyncMock(return_value="test_value")
 | |
|         mock_master.hgetall = AsyncMock(
 | |
|             return_value={"key1": "value1", "key2": "value2"}
 | |
|         )
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test hset
 | |
|         hset_method = proxy.__getattr__("hset")
 | |
|         result = await hset_method("test_hash", "field1", "value1")
 | |
|         assert result == 1
 | |
|         mock_master.hset.assert_called_with("test_hash", "field1", "value1")
 | |
| 
 | |
|         # Test hget
 | |
|         hget_method = proxy.__getattr__("hget")
 | |
|         result = await hget_method("test_hash", "field1")
 | |
|         assert result == "test_value"
 | |
|         mock_master.hget.assert_called_with("test_hash", "field1")
 | |
| 
 | |
|         # Test hgetall
 | |
|         hgetall_method = proxy.__getattr__("hgetall")
 | |
|         result = await hgetall_method("test_hash")
 | |
|         assert result == {"key1": "value1", "key2": "value2"}
 | |
|         mock_master.hgetall.assert_called_with("test_hash")
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_string_commands_sync(self, mock_sentinel_class):
 | |
|         """Test Redis string commands in sync mode"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # Mock string command responses
 | |
|         mock_master.set.return_value = True
 | |
|         mock_master.get.return_value = "test_value"
 | |
|         mock_master.delete.return_value = 1
 | |
|         mock_master.exists.return_value = True
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         # Test set
 | |
|         set_method = proxy.__getattr__("set")
 | |
|         result = set_method("test_key", "test_value")
 | |
|         assert result is True
 | |
|         mock_master.set.assert_called_with("test_key", "test_value")
 | |
| 
 | |
|         # Test get
 | |
|         get_method = proxy.__getattr__("get")
 | |
|         result = get_method("test_key")
 | |
|         assert result == "test_value"
 | |
|         mock_master.get.assert_called_with("test_key")
 | |
| 
 | |
|         # Test delete
 | |
|         delete_method = proxy.__getattr__("delete")
 | |
|         result = delete_method("test_key")
 | |
|         assert result == 1
 | |
|         mock_master.delete.assert_called_with("test_key")
 | |
| 
 | |
|         # Test exists
 | |
|         exists_method = proxy.__getattr__("exists")
 | |
|         result = exists_method("test_key")
 | |
|         assert result is True
 | |
|         mock_master.exists.assert_called_with("test_key")
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_list_commands_sync(self, mock_sentinel_class):
 | |
|         """Test Redis list commands in sync mode"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # Mock list command responses
 | |
|         mock_master.lpush.return_value = 1
 | |
|         mock_master.rpop.return_value = "test_value"
 | |
|         mock_master.llen.return_value = 5
 | |
|         mock_master.lrange.return_value = ["item1", "item2", "item3"]
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         # Test lpush
 | |
|         lpush_method = proxy.__getattr__("lpush")
 | |
|         result = lpush_method("test_list", "item1")
 | |
|         assert result == 1
 | |
|         mock_master.lpush.assert_called_with("test_list", "item1")
 | |
| 
 | |
|         # Test rpop
 | |
|         rpop_method = proxy.__getattr__("rpop")
 | |
|         result = rpop_method("test_list")
 | |
|         assert result == "test_value"
 | |
|         mock_master.rpop.assert_called_with("test_list")
 | |
| 
 | |
|         # Test llen
 | |
|         llen_method = proxy.__getattr__("llen")
 | |
|         result = llen_method("test_list")
 | |
|         assert result == 5
 | |
|         mock_master.llen.assert_called_with("test_list")
 | |
| 
 | |
|         # Test lrange
 | |
|         lrange_method = proxy.__getattr__("lrange")
 | |
|         result = lrange_method("test_list", 0, -1)
 | |
|         assert result == ["item1", "item2", "item3"]
 | |
|         mock_master.lrange.assert_called_with("test_list", 0, -1)
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_pubsub_commands_sync(self, mock_sentinel_class):
 | |
|         """Test Redis pubsub commands in sync mode"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
|         mock_pubsub = Mock()
 | |
| 
 | |
|         # Mock pubsub responses
 | |
|         mock_master.pubsub.return_value = mock_pubsub
 | |
|         mock_master.publish.return_value = 1
 | |
|         mock_pubsub.subscribe.return_value = None
 | |
|         mock_pubsub.get_message.return_value = {"type": "message", "data": "test_data"}
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         # Test pubsub (factory method - should pass through)
 | |
|         pubsub_method = proxy.__getattr__("pubsub")
 | |
|         result = pubsub_method()
 | |
|         assert result == mock_pubsub
 | |
|         mock_master.pubsub.assert_called_once()
 | |
| 
 | |
|         # Test publish
 | |
|         publish_method = proxy.__getattr__("publish")
 | |
|         result = publish_method("test_channel", "test_message")
 | |
|         assert result == 1
 | |
|         mock_master.publish.assert_called_with("test_channel", "test_message")
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_pipeline_commands_sync(self, mock_sentinel_class):
 | |
|         """Test Redis pipeline commands in sync mode"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
|         mock_pipeline = Mock()
 | |
| 
 | |
|         # Mock pipeline responses
 | |
|         mock_master.pipeline.return_value = mock_pipeline
 | |
|         mock_pipeline.set.return_value = mock_pipeline
 | |
|         mock_pipeline.get.return_value = mock_pipeline
 | |
|         mock_pipeline.execute.return_value = [True, "test_value"]
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         # Test pipeline (factory method - should pass through)
 | |
|         pipeline_method = proxy.__getattr__("pipeline")
 | |
|         result = pipeline_method()
 | |
|         assert result == mock_pipeline
 | |
|         mock_master.pipeline.assert_called_once()
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_commands_with_failover_retry(self, mock_sentinel_class):
 | |
|         """Test Redis commands with failover retry mechanism"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # First call fails with connection error, second succeeds
 | |
|         mock_master.hget.side_effect = [
 | |
|             redis.exceptions.ConnectionError("Connection failed"),
 | |
|             "recovered_value",
 | |
|         ]
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         # Test hget with retry
 | |
|         hget_method = proxy.__getattr__("hget")
 | |
|         result = hget_method("test_hash", "field1")
 | |
| 
 | |
|         assert result == "recovered_value"
 | |
|         assert mock_master.hget.call_count == 2
 | |
| 
 | |
|         # Verify both calls were made with same parameters
 | |
|         expected_calls = [(("test_hash", "field1"),), (("test_hash", "field1"),)]
 | |
|         actual_calls = [call.args for call in mock_master.hget.call_args_list]
 | |
|         assert actual_calls == expected_calls
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     def test_commands_with_readonly_error_retry(self, mock_sentinel_class):
 | |
|         """Test Redis commands with ReadOnlyError retry mechanism"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # First call fails with ReadOnlyError, second succeeds
 | |
|         mock_master.hset.side_effect = [
 | |
|             redis.exceptions.ReadOnlyError(
 | |
|                 "READONLY You can't write against a read only replica"
 | |
|             ),
 | |
|             1,
 | |
|         ]
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
 | |
| 
 | |
|         # Test hset with retry
 | |
|         hset_method = proxy.__getattr__("hset")
 | |
|         result = hset_method("test_hash", "field1", "value1")
 | |
| 
 | |
|         assert result == 1
 | |
|         assert mock_master.hset.call_count == 2
 | |
| 
 | |
|         # Verify both calls were made with same parameters
 | |
|         expected_calls = [
 | |
|             (("test_hash", "field1", "value1"),),
 | |
|             (("test_hash", "field1", "value1"),),
 | |
|         ]
 | |
|         actual_calls = [call.args for call in mock_master.hset.call_args_list]
 | |
|         assert actual_calls == expected_calls
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_async_commands_with_failover_retry(self, mock_sentinel_class):
 | |
|         """Test async Redis commands with failover retry mechanism"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # First call fails with connection error, second succeeds
 | |
|         mock_master.hget = AsyncMock(
 | |
|             side_effect=[
 | |
|                 redis.exceptions.ConnectionError("Connection failed"),
 | |
|                 "recovered_value",
 | |
|             ]
 | |
|         )
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test async hget with retry
 | |
|         hget_method = proxy.__getattr__("hget")
 | |
|         result = await hget_method("test_hash", "field1")
 | |
| 
 | |
|         assert result == "recovered_value"
 | |
|         assert mock_master.hget.call_count == 2
 | |
| 
 | |
|         # Verify both calls were made with same parameters
 | |
|         expected_calls = [(("test_hash", "field1"),), (("test_hash", "field1"),)]
 | |
|         actual_calls = [call.args for call in mock_master.hget.call_args_list]
 | |
|         assert actual_calls == expected_calls
 | |
| 
 | |
| 
 | |
| class TestSentinelRedisProxyFactoryMethods:
 | |
|     """Test Redis factory methods in async mode - these are special cases that remain sync"""
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_pubsub_factory_method_async(self, mock_sentinel_class):
 | |
|         """Test pubsub factory method in async mode - should pass through without wrapping"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
|         mock_pubsub = Mock()
 | |
| 
 | |
|         # Mock pubsub factory method
 | |
|         mock_master.pubsub.return_value = mock_pubsub
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test pubsub factory method - should NOT be wrapped as async
 | |
|         pubsub_method = proxy.__getattr__("pubsub")
 | |
|         result = pubsub_method()
 | |
| 
 | |
|         assert result == mock_pubsub
 | |
|         mock_master.pubsub.assert_called_once()
 | |
| 
 | |
|         # Verify it's not wrapped as async (no await needed)
 | |
|         assert not inspect.iscoroutine(result)
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_pipeline_factory_method_async(self, mock_sentinel_class):
 | |
|         """Test pipeline factory method in async mode - should pass through without wrapping"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
|         mock_pipeline = Mock()
 | |
| 
 | |
|         # Mock pipeline factory method
 | |
|         mock_master.pipeline.return_value = mock_pipeline
 | |
|         mock_pipeline.set.return_value = mock_pipeline
 | |
|         mock_pipeline.get.return_value = mock_pipeline
 | |
|         mock_pipeline.execute.return_value = [True, "test_value"]
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test pipeline factory method - should NOT be wrapped as async
 | |
|         pipeline_method = proxy.__getattr__("pipeline")
 | |
|         result = pipeline_method()
 | |
| 
 | |
|         assert result == mock_pipeline
 | |
|         mock_master.pipeline.assert_called_once()
 | |
| 
 | |
|         # Verify it's not wrapped as async (no await needed)
 | |
|         assert not inspect.iscoroutine(result)
 | |
| 
 | |
|         # Test pipeline usage (these should also be sync)
 | |
|         pipeline_result = result.set("key", "value").get("key").execute()
 | |
|         assert pipeline_result == [True, "test_value"]
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_factory_methods_vs_regular_commands_async(self, mock_sentinel_class):
 | |
|         """Test that factory methods behave differently from regular commands in async mode"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # Mock both factory method and regular command
 | |
|         mock_pubsub = Mock()
 | |
|         mock_master.pubsub.return_value = mock_pubsub
 | |
|         mock_master.get = AsyncMock(return_value="test_value")
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test factory method - should NOT be wrapped
 | |
|         pubsub_method = proxy.__getattr__("pubsub")
 | |
|         pubsub_result = pubsub_method()
 | |
| 
 | |
|         # Test regular command - should be wrapped as async
 | |
|         get_method = proxy.__getattr__("get")
 | |
|         get_result = get_method("test_key")
 | |
| 
 | |
|         # Factory method returns directly
 | |
|         assert pubsub_result == mock_pubsub
 | |
|         assert not inspect.iscoroutine(pubsub_result)
 | |
| 
 | |
|         # Regular command returns coroutine
 | |
|         assert inspect.iscoroutine(get_result)
 | |
| 
 | |
|         # Regular command needs await
 | |
|         actual_value = await get_result
 | |
|         assert actual_value == "test_value"
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_factory_methods_with_failover_async(self, mock_sentinel_class):
 | |
|         """Test factory methods with failover in async mode"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # First call fails, second succeeds
 | |
|         mock_pubsub = Mock()
 | |
|         mock_master.pubsub.side_effect = [
 | |
|             redis.exceptions.ConnectionError("Connection failed"),
 | |
|             mock_pubsub,
 | |
|         ]
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test pubsub factory method with failover
 | |
|         pubsub_method = proxy.__getattr__("pubsub")
 | |
|         result = pubsub_method()
 | |
| 
 | |
|         assert result == mock_pubsub
 | |
|         assert mock_master.pubsub.call_count == 2  # Retry happened
 | |
| 
 | |
|         # Verify it's still not wrapped as async after retry
 | |
|         assert not inspect.iscoroutine(result)
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_monitor_factory_method_async(self, mock_sentinel_class):
 | |
|         """Test monitor factory method in async mode - should pass through without wrapping"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
|         mock_monitor = Mock()
 | |
| 
 | |
|         # Mock monitor factory method
 | |
|         mock_master.monitor.return_value = mock_monitor
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test monitor factory method - should NOT be wrapped as async
 | |
|         monitor_method = proxy.__getattr__("monitor")
 | |
|         result = monitor_method()
 | |
| 
 | |
|         assert result == mock_monitor
 | |
|         mock_master.monitor.assert_called_once()
 | |
| 
 | |
|         # Verify it's not wrapped as async (no await needed)
 | |
|         assert not inspect.iscoroutine(result)
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_client_factory_method_async(self, mock_sentinel_class):
 | |
|         """Test client factory method in async mode - should pass through without wrapping"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
|         mock_client = Mock()
 | |
| 
 | |
|         # Mock client factory method
 | |
|         mock_master.client.return_value = mock_client
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test client factory method - should NOT be wrapped as async
 | |
|         client_method = proxy.__getattr__("client")
 | |
|         result = client_method()
 | |
| 
 | |
|         assert result == mock_client
 | |
|         mock_master.client.assert_called_once()
 | |
| 
 | |
|         # Verify it's not wrapped as async (no await needed)
 | |
|         assert not inspect.iscoroutine(result)
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_transaction_factory_method_async(self, mock_sentinel_class):
 | |
|         """Test transaction factory method in async mode - should pass through without wrapping"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
|         mock_transaction = Mock()
 | |
| 
 | |
|         # Mock transaction factory method
 | |
|         mock_master.transaction.return_value = mock_transaction
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test transaction factory method - should NOT be wrapped as async
 | |
|         transaction_method = proxy.__getattr__("transaction")
 | |
|         result = transaction_method()
 | |
| 
 | |
|         assert result == mock_transaction
 | |
|         mock_master.transaction.assert_called_once()
 | |
| 
 | |
|         # Verify it's not wrapped as async (no await needed)
 | |
|         assert not inspect.iscoroutine(result)
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_all_factory_methods_async(self, mock_sentinel_class):
 | |
|         """Test all factory methods in async mode - comprehensive test"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # Mock all factory methods
 | |
|         mock_objects = {
 | |
|             "pipeline": Mock(),
 | |
|             "pubsub": Mock(),
 | |
|             "monitor": Mock(),
 | |
|             "client": Mock(),
 | |
|             "transaction": Mock(),
 | |
|         }
 | |
| 
 | |
|         for method_name, mock_obj in mock_objects.items():
 | |
|             setattr(mock_master, method_name, Mock(return_value=mock_obj))
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Test all factory methods
 | |
|         for method_name, expected_obj in mock_objects.items():
 | |
|             method = proxy.__getattr__(method_name)
 | |
|             result = method()
 | |
| 
 | |
|             assert result == expected_obj
 | |
|             assert not inspect.iscoroutine(result)
 | |
|             getattr(mock_master, method_name).assert_called_once()
 | |
| 
 | |
|             # Reset mock for next iteration
 | |
|             getattr(mock_master, method_name).reset_mock()
 | |
| 
 | |
|     @patch("redis.sentinel.Sentinel")
 | |
|     @pytest.mark.asyncio
 | |
|     async def test_mixed_factory_and_regular_commands_async(self, mock_sentinel_class):
 | |
|         """Test using both factory methods and regular commands in async mode"""
 | |
|         mock_sentinel = Mock()
 | |
|         mock_master = Mock()
 | |
| 
 | |
|         # Mock pipeline factory and regular commands
 | |
|         mock_pipeline = Mock()
 | |
|         mock_master.pipeline.return_value = mock_pipeline
 | |
|         mock_pipeline.set.return_value = mock_pipeline
 | |
|         mock_pipeline.get.return_value = mock_pipeline
 | |
|         mock_pipeline.execute.return_value = [True, "pipeline_value"]
 | |
| 
 | |
|         mock_master.get = AsyncMock(return_value="regular_value")
 | |
| 
 | |
|         mock_sentinel.master_for.return_value = mock_master
 | |
| 
 | |
|         proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
 | |
| 
 | |
|         # Use factory method (sync)
 | |
|         pipeline = proxy.__getattr__("pipeline")()
 | |
|         pipeline_result = pipeline.set("key1", "value1").get("key1").execute()
 | |
| 
 | |
|         # Use regular command (async)
 | |
|         get_method = proxy.__getattr__("get")
 | |
|         regular_result = await get_method("key2")
 | |
| 
 | |
|         # Verify both work correctly
 | |
|         assert pipeline_result == [True, "pipeline_value"]
 | |
|         assert regular_result == "regular_value"
 | |
| 
 | |
|         # Verify calls
 | |
|         mock_master.pipeline.assert_called_once()
 | |
|         mock_master.get.assert_called_with("key2")
 |