Spaces:
Build error
Build error
| 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 == [] | |
| 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') | |
| 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') | |
| 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 | |
| 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 | |
| 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 | |
| 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() | |
| 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() | |
| 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) | |
| 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""" | |
| 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') | |
| 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') | |
| 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') | |
| 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) | |
| 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') | |
| 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() | |
| 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 | |
| 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 | |
| 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""" | |
| 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) | |
| 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'] | |
| 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' | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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() | |
| 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') | |