from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest from yuxi.channel.constants import CHANNEL_PROCESSED_TTL_SECONDS, channel_processed_key from yuxi.channel.message.dedupe import MessageDeduper @pytest.fixture def redis_mock(): return MagicMock( set=AsyncMock(), exists=AsyncMock(), setex=AsyncMock(), ) @pytest.fixture def deduper(): return MessageDeduper() async def test_is_processed_new_message(redis_mock, deduper): redis_mock.set.return_value = True with patch("yuxi.channel.message.dedupe.get_redis_client", AsyncMock(return_value=redis_mock)): result = await deduper.is_processed("msg-1") assert result is False redis_mock.set.assert_awaited_once_with( channel_processed_key("msg-1"), "1", nx=True, ex=CHANNEL_PROCESSED_TTL_SECONDS ) async def test_is_processed_duplicate_message(redis_mock, deduper): redis_mock.set.return_value = None with patch("yuxi.channel.message.dedupe.get_redis_client", AsyncMock(return_value=redis_mock)): result = await deduper.is_processed("msg-1") assert result is True async def test_is_processed_empty_message_id_returns_true(redis_mock, deduper): with patch("yuxi.channel.message.dedupe.get_redis_client", AsyncMock(return_value=redis_mock)): assert await deduper.is_processed("") is True assert await deduper.is_processed(None) is True redis_mock.set.assert_not_called() async def test_is_already_processed_true(redis_mock, deduper): redis_mock.exists.return_value = 1 with patch("yuxi.channel.message.dedupe.get_redis_client", AsyncMock(return_value=redis_mock)): result = await deduper.is_already_processed("msg-1") assert result is True redis_mock.exists.assert_awaited_once_with(channel_processed_key("msg-1")) async def test_is_already_processed_false(redis_mock, deduper): redis_mock.exists.return_value = 0 with patch("yuxi.channel.message.dedupe.get_redis_client", AsyncMock(return_value=redis_mock)): result = await deduper.is_already_processed("msg-1") assert result is False async def test_is_already_processed_empty_message_id_returns_true(redis_mock, deduper): with patch("yuxi.channel.message.dedupe.get_redis_client", AsyncMock(return_value=redis_mock)): assert await deduper.is_already_processed("") is True redis_mock.exists.assert_not_called() async def test_mark_processed_sets_key_with_ttl(redis_mock, deduper): with patch("yuxi.channel.message.dedupe.get_redis_client", AsyncMock(return_value=redis_mock)): await deduper.mark_processed("msg-1") redis_mock.setex.assert_awaited_once_with(channel_processed_key("msg-1"), CHANNEL_PROCESSED_TTL_SECONDS, "1") async def test_mark_processed_empty_message_id_is_noop(redis_mock, deduper): with patch("yuxi.channel.message.dedupe.get_redis_client", AsyncMock(return_value=redis_mock)): await deduper.mark_processed("") await deduper.mark_processed(None) redis_mock.setex.assert_not_called() async def test_custom_ttl(redis_mock): deduper = MessageDeduper(ttl_seconds=60) redis_mock.set.return_value = True with patch("yuxi.channel.message.dedupe.get_redis_client", AsyncMock(return_value=redis_mock)): await deduper.is_processed("msg-1") redis_mock.set.assert_awaited_once_with(channel_processed_key("msg-1"), "1", nx=True, ex=60)