from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest from yuxi.channels.models import DeliveryResult class TestWeChatSetupWizardValidation: @pytest.mark.asyncio async def test_validate_connection_wecom_success(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "wecom" wizard._config_snapshot = { "corp_id": "test_corp_id", "corp_secret": "test_corp_secret", "agent_id": "1000001", } mock_client = AsyncMock() mock_resp = MagicMock() mock_resp.headers = {"content-type": "application/json"} mock_resp.json.return_value = {"access_token": "test_token_123"} mock_client.get.return_value = mock_resp result = await wizard._probe_connection(mock_client) assert result["success"] is True assert result["next_step"] == "webhook" @pytest.mark.asyncio async def test_validate_connection_wecom_failure(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "wecom" wizard._config_snapshot = { "corp_id": "invalid_corp", "corp_secret": "invalid_secret", "agent_id": "1000001", } mock_client = AsyncMock() mock_resp = MagicMock() mock_resp.headers = {"content-type": "application/json"} mock_resp.json.return_value = {"errcode": 40001, "errmsg": "invalid credential"} mock_client.get.return_value = mock_resp result = await wizard._probe_connection(mock_client) assert result["success"] is False assert result["next_step"] == "credentials" @pytest.mark.asyncio async def test_validate_connection_mp_success(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "mp" wizard._config_snapshot = { "app_id": "wx_test_app", "app_secret": "test_app_secret", } mock_client = AsyncMock() mock_resp = MagicMock() mock_resp.headers = {"content-type": "application/json"} mock_resp.json.return_value = {"access_token": "mp_test_token"} mock_client.get.return_value = mock_resp result = await wizard._probe_connection(mock_client) assert result["success"] is True assert result["next_step"] == "webhook" @pytest.mark.asyncio async def test_validate_connection_mp_failure(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "mp" wizard._config_snapshot = { "app_id": "invalid_app", "app_secret": "invalid_secret", } mock_client = AsyncMock() mock_resp = MagicMock() mock_resp.headers = {"content-type": "application/json"} mock_resp.json.return_value = {"errcode": 40001} mock_client.get.return_value = mock_resp result = await wizard._probe_connection(mock_client) assert result["success"] is False @pytest.mark.asyncio async def test_validate_connection_bridge_success(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "personal" wizard._config_snapshot = {"bridge_url": "http://localhost:5555"} mock_client = AsyncMock() mock_health = MagicMock() mock_health.status_code = 200 mock_login = MagicMock() mock_login.json.return_value = {"logged_in": True} mock_client.get.side_effect = [mock_health, mock_login] result = await wizard._probe_connection(mock_client) assert result["success"] is True assert result["next_step"] == "webhook" @pytest.mark.asyncio async def test_validate_connection_bridge_health_failure(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "personal" wizard._config_snapshot = {"bridge_url": "http://localhost:5555"} mock_client = AsyncMock() mock_health = MagicMock() mock_health.status_code = 500 mock_client.get.return_value = mock_health result = await wizard._probe_connection(mock_client) assert result["success"] is False @pytest.mark.asyncio async def test_validate_connection_network_error(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "personal" wizard._config_snapshot = {"bridge_url": "http://localhost:5555"} mock_client = AsyncMock() mock_client.get.side_effect = httpx.ConnectError("Connection refused") result = await wizard._probe_connection(mock_client) assert result["success"] is False @pytest.mark.asyncio async def test_validate_connection_requires_mode_and_creds(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() result = await wizard.validate_connection() assert result["success"] is False assert "No credentials" in result["error"] @pytest.mark.asyncio async def test_validate_connection_empty_credentials(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "wecom" wizard._config_snapshot = {"corp_id": "", "corp_secret": ""} result = await wizard.validate_connection() assert result["success"] is False @pytest.mark.asyncio async def test_validate_connection_with_factory(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "wecom" wizard._config_snapshot = { "corp_id": "test_corp", "corp_secret": "test_secret", "agent_id": "1000001", } mock_client = AsyncMock() mock_resp = MagicMock() mock_resp.headers = {"content-type": "application/json"} mock_resp.json.return_value = {"access_token": "test_token"} mock_client.get.return_value = mock_resp factory = lambda: mock_client result = await wizard.validate_connection(http_client_factory=factory) assert result["success"] is True class TestSetupWizardLifecycle: @pytest.mark.asyncio async def test_select_valid_mode(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() result = await wizard.select_mode("wecom") assert result["success"] is True assert result["mode"] == "wecom" @pytest.mark.asyncio async def test_select_invalid_mode(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() result = await wizard.select_mode("invalid") assert result["success"] is False @pytest.mark.asyncio async def test_set_credentials_wecom_valid(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "wecom" result = await wizard.set_credentials({ "corp_id": "test_corp", "corp_secret": "test_secret", "agent_id": "1000001", }) assert result["success"] is True assert result["next_step"] == "validation" @pytest.mark.asyncio async def test_set_credentials_missing_fields(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "wecom" result = await wizard.set_credentials({ "corp_id": "test_corp", }) assert result["success"] is False @pytest.mark.asyncio async def test_set_credentials_no_mode(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() result = await wizard.set_credentials({"corp_id": "test"}) assert result["success"] is False @pytest.mark.asyncio async def test_configure_webhook(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "wecom" wizard._config_snapshot = {"corp_id": "test"} result = await wizard.configure_webhook("https://example.com/webhook") assert result["success"] is True assert wizard._config_snapshot["webhook_url"] == "https://example.com/webhook" @pytest.mark.asyncio async def test_confirm_and_apply(self): from yuxi.channels.adapters.wechat.setup_wizard import ( WeChatSetupWizard, WizardStep, ) wizard = WeChatSetupWizard() wizard._selected_mode = "wecom" wizard._config_snapshot = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001"} wizard._current_step = WizardStep.CONFIRMATION result = await wizard.confirm_and_apply() assert result["success"] is True assert result["config"]["enabled"] is True @pytest.mark.asyncio async def test_confirm_before_complete_fails(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() result = await wizard.confirm_and_apply() assert result["success"] is False def test_reset(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "wecom" wizard._config_snapshot = {"corp_id": "test"} wizard.reset() assert wizard._selected_mode == "" assert wizard._config_snapshot == {} def test_get_current_step_info(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() wizard._selected_mode = "mp" wizard._config_snapshot = {"app_id": "wx_test"} info = wizard.get_current_step_info() assert info["mode"] == "mp" def test_get_available_modes(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupWizard wizard = WeChatSetupWizard() modes = wizard.get_available_modes() assert len(modes) == 3 mode_ids = {m["id"] for m in modes} assert mode_ids == {"wecom", "mp", "personal"} class TestWeChatSetupAdapter: def test_wizard_summary_wecom(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupAdapter config = { "corp_id": "test_corp", "corp_secret": "test_secret", "agent_id": "1000001", "enabled": True, "webhook_url": "https://example.com/webhook", } summary = WeChatSetupAdapter.get_wizard_summary(config) assert summary["mode"] == "wecom" assert summary["configured"] is True assert summary["webhook_configured"] is True def test_wizard_summary_mp(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupAdapter config = {"app_id": "wx_test", "app_secret": "test_secret", "enabled": True} summary = WeChatSetupAdapter.get_wizard_summary(config) assert summary["mode"] == "mp" assert summary["configured"] is True def test_wizard_summary_personal(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupAdapter config = {"bridge_url": "http://localhost:5555", "enabled": False} summary = WeChatSetupAdapter.get_wizard_summary(config) assert summary["mode"] == "personal" assert summary["configured"] is True assert summary["enabled"] is False def test_wizard_summary_unconfigured(self): from yuxi.channels.adapters.wechat.setup_wizard import WeChatSetupAdapter summary = WeChatSetupAdapter.get_wizard_summary({}) assert summary["mode"] == "unconfigured" assert summary["configured"] is False class TestAdapterDedupEnhancement: def test_dedup_ttl_from_config(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({ "bridge_url": "http://localhost:5555", "dm_policy": "open", "dedup_ttl_seconds": 5.0, "max_dedup_entries": 500, }) assert adapter._dedup_ttl == 5.0 assert adapter._max_dedup_entries == 500 def test_dedup_ttl_default(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) assert adapter._dedup_ttl == 1.0 assert adapter._max_dedup_entries == 10000 def test_export_dedup_state(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._dedup = {("user1", "msg1"): 100.0, ("user2", "msg2"): 200.0} adapter._dedup_ttl = 60.0 state = adapter.export_dedup_state() assert state["dedup_ttl"] == 60.0 assert state["max_entries"] == 10000 assert state["entry_count"] >= 0 def test_import_dedup_state(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._dedup = {} import time now = time.monotonic() state = { "dedup_ttl": 10.0, "entries": { "user1:12345": now, "user2:67890": now, }, } adapter.import_dedup_state(state) assert len(adapter._dedup) >= 2 def test_state_snapshot_includes_dedup(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._dedup = {("u1", "c1"): 100.0, ("u2", "c2"): 200.0} snapshot = adapter.state_snapshot assert snapshot.probe["dedup_entries"] == 2 assert snapshot.probe["dedup_ttl"] == 1.0 class TestAdapterMPMediaEnhancement: @pytest.mark.asyncio async def test_send_mp_media_routes_voice(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter from yuxi.channels.adapters.wechat.mp.client import MPClient adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "mp" adapter._mp_client = MPClient(AsyncMock(), {"app_id": "wx_test", "app_secret": "test"}) with patch( "yuxi.channels.adapters.wechat.mp.send.send_mp_voice", new_callable=AsyncMock, ) as mock_send: mock_send.return_value = DeliveryResult(success=True) adapter._http_client = AsyncMock() result = await adapter._send_mp_media("voice", b"voice_data", "user123") assert result.success is True mock_send.assert_called_once() @pytest.mark.asyncio async def test_send_mp_media_routes_video(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter from yuxi.channels.adapters.wechat.mp.client import MPClient adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "mp" adapter._mp_client = MPClient(AsyncMock(), {"app_id": "wx_test", "app_secret": "test"}) with patch( "yuxi.channels.adapters.wechat.mp.send.send_mp_video", new_callable=AsyncMock, ) as mock_send: mock_send.return_value = DeliveryResult(success=True) adapter._http_client = AsyncMock() result = await adapter._send_mp_media("video", b"video_data", "user123") assert result.success is True mock_send.assert_called_once() @pytest.mark.asyncio async def test_send_mp_media_routes_image(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter from yuxi.channels.adapters.wechat.mp.client import MPClient adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "mp" adapter._mp_client = MPClient(AsyncMock(), {"app_id": "wx_test", "app_secret": "test"}) with patch( "yuxi.channels.adapters.wechat.mp.send.send_mp_image", new_callable=AsyncMock, ) as mock_send: mock_send.return_value = DeliveryResult(success=True) adapter._http_client = AsyncMock() result = await adapter._send_mp_media("image", b"image_data", "user123") assert result.success is True mock_send.assert_called_once() @pytest.mark.asyncio async def test_send_mp_media_rejects_unsupported_type(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter from yuxi.channels.adapters.wechat.mp.client import MPClient adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "mp" adapter._mp_client = MPClient(AsyncMock(), {"app_id": "wx_test", "app_secret": "test"}) adapter._http_client = AsyncMock() result = await adapter._send_mp_media("document", b"data", "user123") assert result.success is False assert "only supports image/voice/video" in result.error @pytest.mark.asyncio async def test_send_mp_media_rejects_non_bytes(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter from yuxi.channels.adapters.wechat.mp.client import MPClient adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "mp" adapter._mp_client = MPClient(AsyncMock(), {"app_id": "wx_test", "app_secret": "test"}) adapter._http_client = AsyncMock() result = await adapter._send_mp_media("image", "not_bytes", "user123") assert result.success is False assert "raw bytes" in result.error class TestAdapterWebhookPath: def test_webhook_path_set(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter assert WeChatAdapter.webhook_path == "wechat"