test_redis.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import pytest
  2. from unittest.mock import Mock, patch, AsyncMock
  3. import redis
  4. from open_webui.utils.redis import (
  5. SentinelRedisProxy,
  6. parse_redis_service_url,
  7. get_redis_connection,
  8. get_sentinels_from_env,
  9. MAX_RETRY_COUNT
  10. )
  11. class TestSentinelRedisProxy:
  12. """Test Redis Sentinel failover functionality"""
  13. def test_parse_redis_service_url_valid(self):
  14. """Test parsing valid Redis service URL"""
  15. url = "redis://user:pass@mymaster:6379/0"
  16. result = parse_redis_service_url(url)
  17. assert result["username"] == "user"
  18. assert result["password"] == "pass"
  19. assert result["service"] == "mymaster"
  20. assert result["port"] == 6379
  21. assert result["db"] == 0
  22. def test_parse_redis_service_url_defaults(self):
  23. """Test parsing Redis service URL with defaults"""
  24. url = "redis://mymaster"
  25. result = parse_redis_service_url(url)
  26. assert result["username"] is None
  27. assert result["password"] is None
  28. assert result["service"] == "mymaster"
  29. assert result["port"] == 6379
  30. assert result["db"] == 0
  31. def test_parse_redis_service_url_invalid_scheme(self):
  32. """Test parsing invalid URL scheme"""
  33. with pytest.raises(ValueError, match="Invalid Redis URL scheme"):
  34. parse_redis_service_url("http://invalid")
  35. def test_get_sentinels_from_env(self):
  36. """Test parsing sentinel hosts from environment"""
  37. hosts = "sentinel1,sentinel2,sentinel3"
  38. port = "26379"
  39. result = get_sentinels_from_env(hosts, port)
  40. expected = [("sentinel1", 26379), ("sentinel2", 26379), ("sentinel3", 26379)]
  41. assert result == expected
  42. def test_get_sentinels_from_env_empty(self):
  43. """Test empty sentinel hosts"""
  44. result = get_sentinels_from_env(None, "26379")
  45. assert result == []
  46. @patch('redis.sentinel.Sentinel')
  47. def test_sentinel_redis_proxy_sync_success(self, mock_sentinel_class):
  48. """Test successful sync operation with SentinelRedisProxy"""
  49. mock_sentinel = Mock()
  50. mock_master = Mock()
  51. mock_master.get.return_value = "test_value"
  52. mock_sentinel.master_for.return_value = mock_master
  53. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  54. # Test attribute access
  55. get_method = proxy.__getattr__("get")
  56. result = get_method("test_key")
  57. assert result == "test_value"
  58. mock_sentinel.master_for.assert_called_with("mymaster")
  59. mock_master.get.assert_called_with("test_key")
  60. @patch('redis.sentinel.Sentinel')
  61. @pytest.mark.asyncio
  62. async def test_sentinel_redis_proxy_async_success(self, mock_sentinel_class):
  63. """Test successful async operation with SentinelRedisProxy"""
  64. mock_sentinel = Mock()
  65. mock_master = Mock()
  66. mock_master.get = AsyncMock(return_value="test_value")
  67. mock_sentinel.master_for.return_value = mock_master
  68. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  69. # Test async attribute access
  70. get_method = proxy.__getattr__("get")
  71. result = await get_method("test_key")
  72. assert result == "test_value"
  73. mock_sentinel.master_for.assert_called_with("mymaster")
  74. mock_master.get.assert_called_with("test_key")
  75. @patch('redis.sentinel.Sentinel')
  76. def test_sentinel_redis_proxy_failover_retry(self, mock_sentinel_class):
  77. """Test retry mechanism during failover"""
  78. mock_sentinel = Mock()
  79. mock_master = Mock()
  80. # First call fails, second succeeds
  81. mock_master.get.side_effect = [
  82. redis.exceptions.ConnectionError("Master down"),
  83. "test_value"
  84. ]
  85. mock_sentinel.master_for.return_value = mock_master
  86. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  87. get_method = proxy.__getattr__("get")
  88. result = get_method("test_key")
  89. assert result == "test_value"
  90. assert mock_master.get.call_count == 2
  91. @patch('redis.sentinel.Sentinel')
  92. def test_sentinel_redis_proxy_max_retries_exceeded(self, mock_sentinel_class):
  93. """Test failure after max retries exceeded"""
  94. mock_sentinel = Mock()
  95. mock_master = Mock()
  96. # All calls fail
  97. mock_master.get.side_effect = redis.exceptions.ConnectionError("Master down")
  98. mock_sentinel.master_for.return_value = mock_master
  99. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  100. get_method = proxy.__getattr__("get")
  101. with pytest.raises(redis.exceptions.ConnectionError):
  102. get_method("test_key")
  103. assert mock_master.get.call_count == MAX_RETRY_COUNT
  104. @patch('redis.sentinel.Sentinel')
  105. def test_sentinel_redis_proxy_readonly_error_retry(self, mock_sentinel_class):
  106. """Test retry on ReadOnlyError"""
  107. mock_sentinel = Mock()
  108. mock_master = Mock()
  109. # First call gets ReadOnlyError (old master), second succeeds (new master)
  110. mock_master.get.side_effect = [
  111. redis.exceptions.ReadOnlyError("Read only"),
  112. "test_value"
  113. ]
  114. mock_sentinel.master_for.return_value = mock_master
  115. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  116. get_method = proxy.__getattr__("get")
  117. result = get_method("test_key")
  118. assert result == "test_value"
  119. assert mock_master.get.call_count == 2
  120. @patch('redis.sentinel.Sentinel')
  121. def test_sentinel_redis_proxy_factory_methods(self, mock_sentinel_class):
  122. """Test factory methods are passed through directly"""
  123. mock_sentinel = Mock()
  124. mock_master = Mock()
  125. mock_pipeline = Mock()
  126. mock_master.pipeline.return_value = mock_pipeline
  127. mock_sentinel.master_for.return_value = mock_master
  128. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  129. # Factory methods should be passed through without wrapping
  130. pipeline_method = proxy.__getattr__("pipeline")
  131. result = pipeline_method()
  132. assert result == mock_pipeline
  133. mock_master.pipeline.assert_called_once()
  134. @patch('redis.sentinel.Sentinel')
  135. @patch('redis.from_url')
  136. def test_get_redis_connection_with_sentinel(self, mock_from_url, mock_sentinel_class):
  137. """Test getting Redis connection with Sentinel"""
  138. mock_sentinel = Mock()
  139. mock_sentinel_class.return_value = mock_sentinel
  140. sentinels = [("sentinel1", 26379), ("sentinel2", 26379)]
  141. redis_url = "redis://user:pass@mymaster:6379/0"
  142. result = get_redis_connection(
  143. redis_url=redis_url,
  144. redis_sentinels=sentinels,
  145. async_mode=False
  146. )
  147. assert isinstance(result, SentinelRedisProxy)
  148. mock_sentinel_class.assert_called_once()
  149. mock_from_url.assert_not_called()
  150. @patch('redis.Redis.from_url')
  151. def test_get_redis_connection_without_sentinel(self, mock_from_url):
  152. """Test getting Redis connection without Sentinel"""
  153. mock_redis = Mock()
  154. mock_from_url.return_value = mock_redis
  155. redis_url = "redis://localhost:6379/0"
  156. result = get_redis_connection(
  157. redis_url=redis_url,
  158. redis_sentinels=None,
  159. async_mode=False
  160. )
  161. assert result == mock_redis
  162. mock_from_url.assert_called_once_with(redis_url, decode_responses=True)
  163. @patch('redis.asyncio.from_url')
  164. def test_get_redis_connection_without_sentinel_async(self, mock_from_url):
  165. """Test getting async Redis connection without Sentinel"""
  166. mock_redis = Mock()
  167. mock_from_url.return_value = mock_redis
  168. redis_url = "redis://localhost:6379/0"
  169. result = get_redis_connection(
  170. redis_url=redis_url,
  171. redis_sentinels=None,
  172. async_mode=True
  173. )
  174. assert result == mock_redis
  175. mock_from_url.assert_called_once_with(redis_url, decode_responses=True)