test_redis.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  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. import inspect
  12. class TestSentinelRedisProxy:
  13. """Test Redis Sentinel failover functionality"""
  14. def test_parse_redis_service_url_valid(self):
  15. """Test parsing valid Redis service URL"""
  16. url = "redis://user:pass@mymaster:6379/0"
  17. result = parse_redis_service_url(url)
  18. assert result["username"] == "user"
  19. assert result["password"] == "pass"
  20. assert result["service"] == "mymaster"
  21. assert result["port"] == 6379
  22. assert result["db"] == 0
  23. def test_parse_redis_service_url_defaults(self):
  24. """Test parsing Redis service URL with defaults"""
  25. url = "redis://mymaster"
  26. result = parse_redis_service_url(url)
  27. assert result["username"] is None
  28. assert result["password"] is None
  29. assert result["service"] == "mymaster"
  30. assert result["port"] == 6379
  31. assert result["db"] == 0
  32. def test_parse_redis_service_url_invalid_scheme(self):
  33. """Test parsing invalid URL scheme"""
  34. with pytest.raises(ValueError, match="Invalid Redis URL scheme"):
  35. parse_redis_service_url("http://invalid")
  36. def test_get_sentinels_from_env(self):
  37. """Test parsing sentinel hosts from environment"""
  38. hosts = "sentinel1,sentinel2,sentinel3"
  39. port = "26379"
  40. result = get_sentinels_from_env(hosts, port)
  41. expected = [("sentinel1", 26379), ("sentinel2", 26379), ("sentinel3", 26379)]
  42. assert result == expected
  43. def test_get_sentinels_from_env_empty(self):
  44. """Test empty sentinel hosts"""
  45. result = get_sentinels_from_env(None, "26379")
  46. assert result == []
  47. @patch("redis.sentinel.Sentinel")
  48. def test_sentinel_redis_proxy_sync_success(self, mock_sentinel_class):
  49. """Test successful sync operation with SentinelRedisProxy"""
  50. mock_sentinel = Mock()
  51. mock_master = Mock()
  52. mock_master.get.return_value = "test_value"
  53. mock_sentinel.master_for.return_value = mock_master
  54. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  55. # Test attribute access
  56. get_method = proxy.__getattr__("get")
  57. result = get_method("test_key")
  58. assert result == "test_value"
  59. mock_sentinel.master_for.assert_called_with("mymaster")
  60. mock_master.get.assert_called_with("test_key")
  61. @patch("redis.sentinel.Sentinel")
  62. @pytest.mark.asyncio
  63. async def test_sentinel_redis_proxy_async_success(self, mock_sentinel_class):
  64. """Test successful async operation with SentinelRedisProxy"""
  65. mock_sentinel = Mock()
  66. mock_master = Mock()
  67. mock_master.get = AsyncMock(return_value="test_value")
  68. mock_sentinel.master_for.return_value = mock_master
  69. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  70. # Test async attribute access
  71. get_method = proxy.__getattr__("get")
  72. result = await get_method("test_key")
  73. assert result == "test_value"
  74. mock_sentinel.master_for.assert_called_with("mymaster")
  75. mock_master.get.assert_called_with("test_key")
  76. @patch("redis.sentinel.Sentinel")
  77. def test_sentinel_redis_proxy_failover_retry(self, mock_sentinel_class):
  78. """Test retry mechanism during failover"""
  79. mock_sentinel = Mock()
  80. mock_master = Mock()
  81. # First call fails, second succeeds
  82. mock_master.get.side_effect = [
  83. redis.exceptions.ConnectionError("Master down"),
  84. "test_value",
  85. ]
  86. mock_sentinel.master_for.return_value = mock_master
  87. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  88. get_method = proxy.__getattr__("get")
  89. result = get_method("test_key")
  90. assert result == "test_value"
  91. assert mock_master.get.call_count == 2
  92. @patch("redis.sentinel.Sentinel")
  93. def test_sentinel_redis_proxy_max_retries_exceeded(self, mock_sentinel_class):
  94. """Test failure after max retries exceeded"""
  95. mock_sentinel = Mock()
  96. mock_master = Mock()
  97. # All calls fail
  98. mock_master.get.side_effect = redis.exceptions.ConnectionError("Master down")
  99. mock_sentinel.master_for.return_value = mock_master
  100. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  101. get_method = proxy.__getattr__("get")
  102. with pytest.raises(redis.exceptions.ConnectionError):
  103. get_method("test_key")
  104. assert mock_master.get.call_count == MAX_RETRY_COUNT
  105. @patch("redis.sentinel.Sentinel")
  106. def test_sentinel_redis_proxy_readonly_error_retry(self, mock_sentinel_class):
  107. """Test retry on ReadOnlyError"""
  108. mock_sentinel = Mock()
  109. mock_master = Mock()
  110. # First call gets ReadOnlyError (old master), second succeeds (new master)
  111. mock_master.get.side_effect = [
  112. redis.exceptions.ReadOnlyError("Read only"),
  113. "test_value",
  114. ]
  115. mock_sentinel.master_for.return_value = mock_master
  116. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  117. get_method = proxy.__getattr__("get")
  118. result = get_method("test_key")
  119. assert result == "test_value"
  120. assert mock_master.get.call_count == 2
  121. @patch("redis.sentinel.Sentinel")
  122. def test_sentinel_redis_proxy_factory_methods(self, mock_sentinel_class):
  123. """Test factory methods are passed through directly"""
  124. mock_sentinel = Mock()
  125. mock_master = Mock()
  126. mock_pipeline = Mock()
  127. mock_master.pipeline.return_value = mock_pipeline
  128. mock_sentinel.master_for.return_value = mock_master
  129. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  130. # Factory methods should be passed through without wrapping
  131. pipeline_method = proxy.__getattr__("pipeline")
  132. result = pipeline_method()
  133. assert result == mock_pipeline
  134. mock_master.pipeline.assert_called_once()
  135. @patch("redis.sentinel.Sentinel")
  136. @patch("redis.from_url")
  137. def test_get_redis_connection_with_sentinel(
  138. self, mock_from_url, mock_sentinel_class
  139. ):
  140. """Test getting Redis connection with Sentinel"""
  141. mock_sentinel = Mock()
  142. mock_sentinel_class.return_value = mock_sentinel
  143. sentinels = [("sentinel1", 26379), ("sentinel2", 26379)]
  144. redis_url = "redis://user:pass@mymaster:6379/0"
  145. result = get_redis_connection(
  146. redis_url=redis_url, redis_sentinels=sentinels, async_mode=False
  147. )
  148. assert isinstance(result, SentinelRedisProxy)
  149. mock_sentinel_class.assert_called_once()
  150. mock_from_url.assert_not_called()
  151. @patch("redis.Redis.from_url")
  152. def test_get_redis_connection_without_sentinel(self, mock_from_url):
  153. """Test getting Redis connection without Sentinel"""
  154. mock_redis = Mock()
  155. mock_from_url.return_value = mock_redis
  156. redis_url = "redis://localhost:6379/0"
  157. result = get_redis_connection(
  158. redis_url=redis_url, redis_sentinels=None, async_mode=False
  159. )
  160. assert result == mock_redis
  161. mock_from_url.assert_called_once_with(redis_url, decode_responses=True)
  162. @patch("redis.asyncio.from_url")
  163. def test_get_redis_connection_without_sentinel_async(self, mock_from_url):
  164. """Test getting async Redis connection without Sentinel"""
  165. mock_redis = Mock()
  166. mock_from_url.return_value = mock_redis
  167. redis_url = "redis://localhost:6379/0"
  168. result = get_redis_connection(
  169. redis_url=redis_url, redis_sentinels=None, async_mode=True
  170. )
  171. assert result == mock_redis
  172. mock_from_url.assert_called_once_with(redis_url, decode_responses=True)
  173. class TestSentinelRedisProxyCommands:
  174. """Test Redis commands through SentinelRedisProxy"""
  175. @patch("redis.sentinel.Sentinel")
  176. def test_hash_commands_sync(self, mock_sentinel_class):
  177. """Test Redis hash commands in sync mode"""
  178. mock_sentinel = Mock()
  179. mock_master = Mock()
  180. # Mock hash command responses
  181. mock_master.hset.return_value = 1
  182. mock_master.hget.return_value = "test_value"
  183. mock_master.hgetall.return_value = {"key1": "value1", "key2": "value2"}
  184. mock_master.hdel.return_value = 1
  185. mock_sentinel.master_for.return_value = mock_master
  186. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  187. # Test hset
  188. hset_method = proxy.__getattr__("hset")
  189. result = hset_method("test_hash", "field1", "value1")
  190. assert result == 1
  191. mock_master.hset.assert_called_with("test_hash", "field1", "value1")
  192. # Test hget
  193. hget_method = proxy.__getattr__("hget")
  194. result = hget_method("test_hash", "field1")
  195. assert result == "test_value"
  196. mock_master.hget.assert_called_with("test_hash", "field1")
  197. # Test hgetall
  198. hgetall_method = proxy.__getattr__("hgetall")
  199. result = hgetall_method("test_hash")
  200. assert result == {"key1": "value1", "key2": "value2"}
  201. mock_master.hgetall.assert_called_with("test_hash")
  202. # Test hdel
  203. hdel_method = proxy.__getattr__("hdel")
  204. result = hdel_method("test_hash", "field1")
  205. assert result == 1
  206. mock_master.hdel.assert_called_with("test_hash", "field1")
  207. @patch("redis.sentinel.Sentinel")
  208. @pytest.mark.asyncio
  209. async def test_hash_commands_async(self, mock_sentinel_class):
  210. """Test Redis hash commands in async mode"""
  211. mock_sentinel = Mock()
  212. mock_master = Mock()
  213. # Mock async hash command responses
  214. mock_master.hset = AsyncMock(return_value=1)
  215. mock_master.hget = AsyncMock(return_value="test_value")
  216. mock_master.hgetall = AsyncMock(
  217. return_value={"key1": "value1", "key2": "value2"}
  218. )
  219. mock_sentinel.master_for.return_value = mock_master
  220. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  221. # Test hset
  222. hset_method = proxy.__getattr__("hset")
  223. result = await hset_method("test_hash", "field1", "value1")
  224. assert result == 1
  225. mock_master.hset.assert_called_with("test_hash", "field1", "value1")
  226. # Test hget
  227. hget_method = proxy.__getattr__("hget")
  228. result = await hget_method("test_hash", "field1")
  229. assert result == "test_value"
  230. mock_master.hget.assert_called_with("test_hash", "field1")
  231. # Test hgetall
  232. hgetall_method = proxy.__getattr__("hgetall")
  233. result = await hgetall_method("test_hash")
  234. assert result == {"key1": "value1", "key2": "value2"}
  235. mock_master.hgetall.assert_called_with("test_hash")
  236. @patch("redis.sentinel.Sentinel")
  237. def test_string_commands_sync(self, mock_sentinel_class):
  238. """Test Redis string commands in sync mode"""
  239. mock_sentinel = Mock()
  240. mock_master = Mock()
  241. # Mock string command responses
  242. mock_master.set.return_value = True
  243. mock_master.get.return_value = "test_value"
  244. mock_master.delete.return_value = 1
  245. mock_master.exists.return_value = True
  246. mock_sentinel.master_for.return_value = mock_master
  247. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  248. # Test set
  249. set_method = proxy.__getattr__("set")
  250. result = set_method("test_key", "test_value")
  251. assert result is True
  252. mock_master.set.assert_called_with("test_key", "test_value")
  253. # Test get
  254. get_method = proxy.__getattr__("get")
  255. result = get_method("test_key")
  256. assert result == "test_value"
  257. mock_master.get.assert_called_with("test_key")
  258. # Test delete
  259. delete_method = proxy.__getattr__("delete")
  260. result = delete_method("test_key")
  261. assert result == 1
  262. mock_master.delete.assert_called_with("test_key")
  263. # Test exists
  264. exists_method = proxy.__getattr__("exists")
  265. result = exists_method("test_key")
  266. assert result is True
  267. mock_master.exists.assert_called_with("test_key")
  268. @patch("redis.sentinel.Sentinel")
  269. def test_list_commands_sync(self, mock_sentinel_class):
  270. """Test Redis list commands in sync mode"""
  271. mock_sentinel = Mock()
  272. mock_master = Mock()
  273. # Mock list command responses
  274. mock_master.lpush.return_value = 1
  275. mock_master.rpop.return_value = "test_value"
  276. mock_master.llen.return_value = 5
  277. mock_master.lrange.return_value = ["item1", "item2", "item3"]
  278. mock_sentinel.master_for.return_value = mock_master
  279. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  280. # Test lpush
  281. lpush_method = proxy.__getattr__("lpush")
  282. result = lpush_method("test_list", "item1")
  283. assert result == 1
  284. mock_master.lpush.assert_called_with("test_list", "item1")
  285. # Test rpop
  286. rpop_method = proxy.__getattr__("rpop")
  287. result = rpop_method("test_list")
  288. assert result == "test_value"
  289. mock_master.rpop.assert_called_with("test_list")
  290. # Test llen
  291. llen_method = proxy.__getattr__("llen")
  292. result = llen_method("test_list")
  293. assert result == 5
  294. mock_master.llen.assert_called_with("test_list")
  295. # Test lrange
  296. lrange_method = proxy.__getattr__("lrange")
  297. result = lrange_method("test_list", 0, -1)
  298. assert result == ["item1", "item2", "item3"]
  299. mock_master.lrange.assert_called_with("test_list", 0, -1)
  300. @patch("redis.sentinel.Sentinel")
  301. def test_pubsub_commands_sync(self, mock_sentinel_class):
  302. """Test Redis pubsub commands in sync mode"""
  303. mock_sentinel = Mock()
  304. mock_master = Mock()
  305. mock_pubsub = Mock()
  306. # Mock pubsub responses
  307. mock_master.pubsub.return_value = mock_pubsub
  308. mock_master.publish.return_value = 1
  309. mock_pubsub.subscribe.return_value = None
  310. mock_pubsub.get_message.return_value = {"type": "message", "data": "test_data"}
  311. mock_sentinel.master_for.return_value = mock_master
  312. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  313. # Test pubsub (factory method - should pass through)
  314. pubsub_method = proxy.__getattr__("pubsub")
  315. result = pubsub_method()
  316. assert result == mock_pubsub
  317. mock_master.pubsub.assert_called_once()
  318. # Test publish
  319. publish_method = proxy.__getattr__("publish")
  320. result = publish_method("test_channel", "test_message")
  321. assert result == 1
  322. mock_master.publish.assert_called_with("test_channel", "test_message")
  323. @patch("redis.sentinel.Sentinel")
  324. def test_pipeline_commands_sync(self, mock_sentinel_class):
  325. """Test Redis pipeline commands in sync mode"""
  326. mock_sentinel = Mock()
  327. mock_master = Mock()
  328. mock_pipeline = Mock()
  329. # Mock pipeline responses
  330. mock_master.pipeline.return_value = mock_pipeline
  331. mock_pipeline.set.return_value = mock_pipeline
  332. mock_pipeline.get.return_value = mock_pipeline
  333. mock_pipeline.execute.return_value = [True, "test_value"]
  334. mock_sentinel.master_for.return_value = mock_master
  335. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  336. # Test pipeline (factory method - should pass through)
  337. pipeline_method = proxy.__getattr__("pipeline")
  338. result = pipeline_method()
  339. assert result == mock_pipeline
  340. mock_master.pipeline.assert_called_once()
  341. @patch("redis.sentinel.Sentinel")
  342. def test_commands_with_failover_retry(self, mock_sentinel_class):
  343. """Test Redis commands with failover retry mechanism"""
  344. mock_sentinel = Mock()
  345. mock_master = Mock()
  346. # First call fails with connection error, second succeeds
  347. mock_master.hget.side_effect = [
  348. redis.exceptions.ConnectionError("Connection failed"),
  349. "recovered_value",
  350. ]
  351. mock_sentinel.master_for.return_value = mock_master
  352. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  353. # Test hget with retry
  354. hget_method = proxy.__getattr__("hget")
  355. result = hget_method("test_hash", "field1")
  356. assert result == "recovered_value"
  357. assert mock_master.hget.call_count == 2
  358. # Verify both calls were made with same parameters
  359. expected_calls = [(("test_hash", "field1"),), (("test_hash", "field1"),)]
  360. actual_calls = [call.args for call in mock_master.hget.call_args_list]
  361. assert actual_calls == expected_calls
  362. @patch("redis.sentinel.Sentinel")
  363. def test_commands_with_readonly_error_retry(self, mock_sentinel_class):
  364. """Test Redis commands with ReadOnlyError retry mechanism"""
  365. mock_sentinel = Mock()
  366. mock_master = Mock()
  367. # First call fails with ReadOnlyError, second succeeds
  368. mock_master.hset.side_effect = [
  369. redis.exceptions.ReadOnlyError(
  370. "READONLY You can't write against a read only replica"
  371. ),
  372. 1,
  373. ]
  374. mock_sentinel.master_for.return_value = mock_master
  375. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
  376. # Test hset with retry
  377. hset_method = proxy.__getattr__("hset")
  378. result = hset_method("test_hash", "field1", "value1")
  379. assert result == 1
  380. assert mock_master.hset.call_count == 2
  381. # Verify both calls were made with same parameters
  382. expected_calls = [
  383. (("test_hash", "field1", "value1"),),
  384. (("test_hash", "field1", "value1"),),
  385. ]
  386. actual_calls = [call.args for call in mock_master.hset.call_args_list]
  387. assert actual_calls == expected_calls
  388. @patch("redis.sentinel.Sentinel")
  389. @pytest.mark.asyncio
  390. async def test_async_commands_with_failover_retry(self, mock_sentinel_class):
  391. """Test async Redis commands with failover retry mechanism"""
  392. mock_sentinel = Mock()
  393. mock_master = Mock()
  394. # First call fails with connection error, second succeeds
  395. mock_master.hget = AsyncMock(
  396. side_effect=[
  397. redis.exceptions.ConnectionError("Connection failed"),
  398. "recovered_value",
  399. ]
  400. )
  401. mock_sentinel.master_for.return_value = mock_master
  402. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  403. # Test async hget with retry
  404. hget_method = proxy.__getattr__("hget")
  405. result = await hget_method("test_hash", "field1")
  406. assert result == "recovered_value"
  407. assert mock_master.hget.call_count == 2
  408. # Verify both calls were made with same parameters
  409. expected_calls = [(("test_hash", "field1"),), (("test_hash", "field1"),)]
  410. actual_calls = [call.args for call in mock_master.hget.call_args_list]
  411. assert actual_calls == expected_calls
  412. class TestSentinelRedisProxyFactoryMethods:
  413. """Test Redis factory methods in async mode - these are special cases that remain sync"""
  414. @patch("redis.sentinel.Sentinel")
  415. @pytest.mark.asyncio
  416. async def test_pubsub_factory_method_async(self, mock_sentinel_class):
  417. """Test pubsub factory method in async mode - should pass through without wrapping"""
  418. mock_sentinel = Mock()
  419. mock_master = Mock()
  420. mock_pubsub = Mock()
  421. # Mock pubsub factory method
  422. mock_master.pubsub.return_value = mock_pubsub
  423. mock_sentinel.master_for.return_value = mock_master
  424. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  425. # Test pubsub factory method - should NOT be wrapped as async
  426. pubsub_method = proxy.__getattr__("pubsub")
  427. result = pubsub_method()
  428. assert result == mock_pubsub
  429. mock_master.pubsub.assert_called_once()
  430. # Verify it's not wrapped as async (no await needed)
  431. assert not inspect.iscoroutine(result)
  432. @patch("redis.sentinel.Sentinel")
  433. @pytest.mark.asyncio
  434. async def test_pipeline_factory_method_async(self, mock_sentinel_class):
  435. """Test pipeline factory method in async mode - should pass through without wrapping"""
  436. mock_sentinel = Mock()
  437. mock_master = Mock()
  438. mock_pipeline = Mock()
  439. # Mock pipeline factory method
  440. mock_master.pipeline.return_value = mock_pipeline
  441. mock_pipeline.set.return_value = mock_pipeline
  442. mock_pipeline.get.return_value = mock_pipeline
  443. mock_pipeline.execute.return_value = [True, "test_value"]
  444. mock_sentinel.master_for.return_value = mock_master
  445. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  446. # Test pipeline factory method - should NOT be wrapped as async
  447. pipeline_method = proxy.__getattr__("pipeline")
  448. result = pipeline_method()
  449. assert result == mock_pipeline
  450. mock_master.pipeline.assert_called_once()
  451. # Verify it's not wrapped as async (no await needed)
  452. assert not inspect.iscoroutine(result)
  453. # Test pipeline usage (these should also be sync)
  454. pipeline_result = result.set("key", "value").get("key").execute()
  455. assert pipeline_result == [True, "test_value"]
  456. @patch("redis.sentinel.Sentinel")
  457. @pytest.mark.asyncio
  458. async def test_factory_methods_vs_regular_commands_async(self, mock_sentinel_class):
  459. """Test that factory methods behave differently from regular commands in async mode"""
  460. mock_sentinel = Mock()
  461. mock_master = Mock()
  462. # Mock both factory method and regular command
  463. mock_pubsub = Mock()
  464. mock_master.pubsub.return_value = mock_pubsub
  465. mock_master.get = AsyncMock(return_value="test_value")
  466. mock_sentinel.master_for.return_value = mock_master
  467. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  468. # Test factory method - should NOT be wrapped
  469. pubsub_method = proxy.__getattr__("pubsub")
  470. pubsub_result = pubsub_method()
  471. # Test regular command - should be wrapped as async
  472. get_method = proxy.__getattr__("get")
  473. get_result = get_method("test_key")
  474. # Factory method returns directly
  475. assert pubsub_result == mock_pubsub
  476. assert not inspect.iscoroutine(pubsub_result)
  477. # Regular command returns coroutine
  478. assert inspect.iscoroutine(get_result)
  479. # Regular command needs await
  480. actual_value = await get_result
  481. assert actual_value == "test_value"
  482. @patch("redis.sentinel.Sentinel")
  483. @pytest.mark.asyncio
  484. async def test_factory_methods_with_failover_async(self, mock_sentinel_class):
  485. """Test factory methods with failover in async mode"""
  486. mock_sentinel = Mock()
  487. mock_master = Mock()
  488. # First call fails, second succeeds
  489. mock_pubsub = Mock()
  490. mock_master.pubsub.side_effect = [
  491. redis.exceptions.ConnectionError("Connection failed"),
  492. mock_pubsub,
  493. ]
  494. mock_sentinel.master_for.return_value = mock_master
  495. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  496. # Test pubsub factory method with failover
  497. pubsub_method = proxy.__getattr__("pubsub")
  498. result = pubsub_method()
  499. assert result == mock_pubsub
  500. assert mock_master.pubsub.call_count == 2 # Retry happened
  501. # Verify it's still not wrapped as async after retry
  502. assert not inspect.iscoroutine(result)
  503. @patch("redis.sentinel.Sentinel")
  504. @pytest.mark.asyncio
  505. async def test_monitor_factory_method_async(self, mock_sentinel_class):
  506. """Test monitor factory method in async mode - should pass through without wrapping"""
  507. mock_sentinel = Mock()
  508. mock_master = Mock()
  509. mock_monitor = Mock()
  510. # Mock monitor factory method
  511. mock_master.monitor.return_value = mock_monitor
  512. mock_sentinel.master_for.return_value = mock_master
  513. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  514. # Test monitor factory method - should NOT be wrapped as async
  515. monitor_method = proxy.__getattr__("monitor")
  516. result = monitor_method()
  517. assert result == mock_monitor
  518. mock_master.monitor.assert_called_once()
  519. # Verify it's not wrapped as async (no await needed)
  520. assert not inspect.iscoroutine(result)
  521. @patch("redis.sentinel.Sentinel")
  522. @pytest.mark.asyncio
  523. async def test_client_factory_method_async(self, mock_sentinel_class):
  524. """Test client factory method in async mode - should pass through without wrapping"""
  525. mock_sentinel = Mock()
  526. mock_master = Mock()
  527. mock_client = Mock()
  528. # Mock client factory method
  529. mock_master.client.return_value = mock_client
  530. mock_sentinel.master_for.return_value = mock_master
  531. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  532. # Test client factory method - should NOT be wrapped as async
  533. client_method = proxy.__getattr__("client")
  534. result = client_method()
  535. assert result == mock_client
  536. mock_master.client.assert_called_once()
  537. # Verify it's not wrapped as async (no await needed)
  538. assert not inspect.iscoroutine(result)
  539. @patch("redis.sentinel.Sentinel")
  540. @pytest.mark.asyncio
  541. async def test_transaction_factory_method_async(self, mock_sentinel_class):
  542. """Test transaction factory method in async mode - should pass through without wrapping"""
  543. mock_sentinel = Mock()
  544. mock_master = Mock()
  545. mock_transaction = Mock()
  546. # Mock transaction factory method
  547. mock_master.transaction.return_value = mock_transaction
  548. mock_sentinel.master_for.return_value = mock_master
  549. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  550. # Test transaction factory method - should NOT be wrapped as async
  551. transaction_method = proxy.__getattr__("transaction")
  552. result = transaction_method()
  553. assert result == mock_transaction
  554. mock_master.transaction.assert_called_once()
  555. # Verify it's not wrapped as async (no await needed)
  556. assert not inspect.iscoroutine(result)
  557. @patch("redis.sentinel.Sentinel")
  558. @pytest.mark.asyncio
  559. async def test_all_factory_methods_async(self, mock_sentinel_class):
  560. """Test all factory methods in async mode - comprehensive test"""
  561. mock_sentinel = Mock()
  562. mock_master = Mock()
  563. # Mock all factory methods
  564. mock_objects = {
  565. "pipeline": Mock(),
  566. "pubsub": Mock(),
  567. "monitor": Mock(),
  568. "client": Mock(),
  569. "transaction": Mock(),
  570. }
  571. for method_name, mock_obj in mock_objects.items():
  572. setattr(mock_master, method_name, Mock(return_value=mock_obj))
  573. mock_sentinel.master_for.return_value = mock_master
  574. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  575. # Test all factory methods
  576. for method_name, expected_obj in mock_objects.items():
  577. method = proxy.__getattr__(method_name)
  578. result = method()
  579. assert result == expected_obj
  580. assert not inspect.iscoroutine(result)
  581. getattr(mock_master, method_name).assert_called_once()
  582. # Reset mock for next iteration
  583. getattr(mock_master, method_name).reset_mock()
  584. @patch("redis.sentinel.Sentinel")
  585. @pytest.mark.asyncio
  586. async def test_mixed_factory_and_regular_commands_async(self, mock_sentinel_class):
  587. """Test using both factory methods and regular commands in async mode"""
  588. mock_sentinel = Mock()
  589. mock_master = Mock()
  590. # Mock pipeline factory and regular commands
  591. mock_pipeline = Mock()
  592. mock_master.pipeline.return_value = mock_pipeline
  593. mock_pipeline.set.return_value = mock_pipeline
  594. mock_pipeline.get.return_value = mock_pipeline
  595. mock_pipeline.execute.return_value = [True, "pipeline_value"]
  596. mock_master.get = AsyncMock(return_value="regular_value")
  597. mock_sentinel.master_for.return_value = mock_master
  598. proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
  599. # Use factory method (sync)
  600. pipeline = proxy.__getattr__("pipeline")()
  601. pipeline_result = pipeline.set("key1", "value1").get("key1").execute()
  602. # Use regular command (async)
  603. get_method = proxy.__getattr__("get")
  604. regular_result = await get_method("key2")
  605. # Verify both work correctly
  606. assert pipeline_result == [True, "pipeline_value"]
  607. assert regular_result == "regular_value"
  608. # Verify calls
  609. mock_master.pipeline.assert_called_once()
  610. mock_master.get.assert_called_with("key2")