123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793 |
- 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")
|