ForcePilot/backend/test/unit/channels/test_wechat_enhancements.py

507 lines
19 KiB
Python
Raw Normal View History

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"