from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest from yuxi.channels.models import DeliveryResult class TestSendMediaAdapter: @pytest.mark.asyncio async def test_send_media_not_initialized_mode(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "personal" adapter._http_client = AsyncMock() result = await adapter.send_media("chat1", "image", b"\x89PNG") assert result.success is False assert "not supported" in result.error @pytest.mark.asyncio async def test_send_media_wecom_image_bytes_required(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "wecom" adapter._wecom_client = MagicMock() result = await adapter._send_wecom_media("image", "not_bytes", "user1") assert result.success is False assert "raw bytes" in result.error @pytest.mark.asyncio async def test_send_media_wecom_image_success(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "wecom" adapter._http_client = AsyncMock() adapter.config = {"agent_id": "1000001", "dm_policy": "open"} mock_client = MagicMock() mock_client.upload_media = AsyncMock(return_value="media_123") adapter._wecom_client = mock_client with patch( "yuxi.channels.adapters.wechat.wecom.send.send_wecom_message", new=AsyncMock(return_value=DeliveryResult(success=True)), ) as mock_send: result = await adapter._send_wecom_media("image", b"\x89PNG\x00", "user1") assert result.success is True mock_client.upload_media.assert_called_once() mock_send.assert_called_once() payload = mock_send.call_args[0][2] assert payload["msgtype"] == "image" assert payload["image"]["media_id"] == "media_123" @pytest.mark.asyncio async def test_send_media_wecom_file_success(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "wecom" adapter._http_client = AsyncMock() adapter.config = {"agent_id": "1000001", "dm_policy": "open"} mock_client = MagicMock() mock_client.upload_media = AsyncMock(return_value="file_456") adapter._wecom_client = mock_client with patch( "yuxi.channels.adapters.wechat.wecom.send.send_wecom_message", new=AsyncMock(return_value=DeliveryResult(success=True)), ) as mock_send: result = await adapter._send_wecom_media("file", b"PDF_CONTENT", "user1") assert result.success is True payload = mock_send.call_args[0][2] assert payload["msgtype"] == "file" assert payload["file"]["media_id"] == "file_456" @pytest.mark.asyncio async def test_send_media_wecom_unsupported_type(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "wecom" adapter.config = {"agent_id": "1000001", "dm_policy": "open"} mock_client = MagicMock() mock_client.upload_media = AsyncMock(return_value="voice_789") adapter._wecom_client = mock_client adapter._http_client = AsyncMock() result = await adapter._send_wecom_media("voice", b"audio_data", "user1") assert result.success is False assert "Unsupported media_type" in result.error @pytest.mark.asyncio async def test_send_media_wecom_upload_failure(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "wecom" adapter.config = {"agent_id": "1000001", "dm_policy": "open"} mock_client = MagicMock() mock_client.upload_media = AsyncMock(side_effect=RuntimeError("Network error")) adapter._wecom_client = mock_client result = await adapter._send_wecom_media("image", b"img", "user1") assert result.success is False assert "upload failed" in result.error @pytest.mark.asyncio async def test_send_media_mp_not_bytes(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "mp" adapter._mp_client = MagicMock() result = await adapter._send_mp_media("image", "not_bytes", "user1") assert result.success is False assert "raw bytes" in result.error @pytest.mark.asyncio async def test_send_media_mp_unsupported_type(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "mp" adapter._mp_client = MagicMock() result = await adapter._send_mp_media("file", b"data", "user1") assert result.success is False assert "only supports image" in result.error @pytest.mark.asyncio async def test_send_media_mp_image_success(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "mp" adapter._http_client = AsyncMock() with patch( "yuxi.channels.adapters.wechat.mp.send.send_mp_image", new=AsyncMock(return_value=DeliveryResult(success=True)), ) as mock_send: result = await adapter._send_mp_media("image", b"\x89PNG", "openid123") assert result.success is True mock_send.assert_called_once() class TestWeComMediaPayload: def test_build_image_payload(self): from yuxi.channels.adapters.wechat.wecom.send import build_wecom_image_payload payload = build_wecom_image_payload("agent1", "user123", "media_abc") assert payload["touser"] == "user123" assert payload["msgtype"] == "image" assert payload["agentid"] == "agent1" assert payload["image"]["media_id"] == "media_abc" def test_build_file_payload(self): from yuxi.channels.adapters.wechat.wecom.send import build_wecom_file_payload payload = build_wecom_file_payload("agent1", "user123", "media_xyz") assert payload["msgtype"] == "file" assert payload["file"]["media_id"] == "media_xyz" class TestRetryHelper: @pytest.mark.asyncio async def test_retry_success_first_attempt(self): from yuxi.channels.adapters.wechat.retry import retry_with_backoff call_count = 0 async def succeed(): nonlocal call_count call_count += 1 return "ok" result = await retry_with_backoff(succeed, max_retries=3, base_delay=0.01) assert result == "ok" assert call_count == 1 @pytest.mark.asyncio async def test_retry_timeout_retries(self): from yuxi.channels.adapters.wechat.retry import retry_with_backoff fail_count = 0 async def flaky(): nonlocal fail_count fail_count += 1 if fail_count < 2: raise httpx.TimeoutException("timeout") return "recovered" result = await retry_with_backoff(flaky, max_retries=3, base_delay=0.01) assert result == "recovered" assert fail_count == 2 @pytest.mark.asyncio async def test_retry_timeout_exhausted(self): from yuxi.channels.adapters.wechat.retry import retry_with_backoff async def always_fail(): raise httpx.TimeoutException("always timeout") with pytest.raises(httpx.TimeoutException): await retry_with_backoff(always_fail, max_retries=2, base_delay=0.01) @pytest.mark.asyncio async def test_retry_no_retry_on_non_retryable(self): from yuxi.channels.adapters.wechat.retry import retry_with_backoff async def auth_fail(): raise ValueError("auth error") with pytest.raises(ValueError): await retry_with_backoff(auth_fail, max_retries=3, base_delay=0.01) @pytest.mark.asyncio async def test_retry_backoff_increasing_delay(self): from yuxi.channels.adapters.wechat.retry import retry_with_backoff delays = [] async def fail_then_ok(): delays.append(len(delays)) if len(delays) >= 3: return "ok" raise httpx.ConnectError("refused") result = await retry_with_backoff(fail_then_ok, max_retries=4, base_delay=0.01) assert result == "ok" assert delays == [0, 1, 2] class TestMediaExtensionHelper: def test_image_extension(self): from yuxi.channels.adapters.wechat.adapter import _media_extension assert _media_extension("image") == "png" def test_voice_extension(self): from yuxi.channels.adapters.wechat.adapter import _media_extension assert _media_extension("voice") == "mp3" def test_video_extension(self): from yuxi.channels.adapters.wechat.adapter import _media_extension assert _media_extension("video") == "mp4" def test_unknown_extension(self): from yuxi.channels.adapters.wechat.adapter import _media_extension assert _media_extension("unknown") == "bin" def test_file_extension(self): from yuxi.channels.adapters.wechat.adapter import _media_extension assert _media_extension("file") == "bin"