新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
669 lines
24 KiB
Python
669 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.models import (
|
|
Attachment,
|
|
ChannelIdentity,
|
|
ChannelMessage,
|
|
ChannelResponse,
|
|
ChannelStatus,
|
|
ChannelType,
|
|
ChatType,
|
|
DeliveryResult,
|
|
EventType,
|
|
HealthStatus,
|
|
MessageType,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def adapter():
|
|
from yuxi.channels.adapters.zalo_user.adapter import ZaloUserAdapter
|
|
|
|
return ZaloUserAdapter({
|
|
"bridge_url": "http://localhost:5556",
|
|
"auth_type": "qr",
|
|
})
|
|
|
|
|
|
class TestRateLimiter:
|
|
def test_check_and_wait_within_limit(self):
|
|
from yuxi.channels.adapters.zalo_user.rate_limiter import RateLimiter
|
|
|
|
limiter = RateLimiter(max_per_minute=3)
|
|
|
|
limiter._send_times.append(100.0)
|
|
assert len(limiter._send_times) == 1
|
|
|
|
def test_cleanup_removes_old_entries(self):
|
|
import time
|
|
from yuxi.channels.adapters.zalo_user.rate_limiter import RateLimiter
|
|
|
|
limiter = RateLimiter(max_per_minute=5)
|
|
now = time.monotonic()
|
|
limiter._send_times.extend([now - 120, now - 90, now - 30])
|
|
limiter._cleanup()
|
|
assert len(limiter._send_times) == 1
|
|
assert limiter._send_times[0] == now - 30
|
|
|
|
|
|
class TestNormalizeInbound:
|
|
def test_direct_message(self):
|
|
from yuxi.channels.adapters.zalo_user.normalize import normalize_inbound
|
|
|
|
result = normalize_inbound("zalo_user", {
|
|
"from_id": "12345",
|
|
"conversation_id": "conv_abc",
|
|
"message_id": "msg_001",
|
|
"text": "Hello Zalo",
|
|
"conversation_type": "direct",
|
|
"from_display_name": "Test User",
|
|
"timestamp": 1700000000000,
|
|
})
|
|
|
|
assert result.identity.channel_id == "zalo_user"
|
|
assert result.identity.channel_type == ChannelType.ZALO_USER
|
|
assert result.identity.channel_user_id == "12345"
|
|
assert result.identity.channel_chat_id == "conv_abc"
|
|
assert result.identity.channel_message_id == "msg_001"
|
|
assert result.chat_type == ChatType.DIRECT
|
|
assert result.content == "Hello Zalo"
|
|
assert result.metadata["zalo_chat_type"] == "direct"
|
|
assert result.metadata["from_display_name"] == "Test User"
|
|
assert result.timestamp is not None
|
|
|
|
def test_group_message(self):
|
|
from yuxi.channels.adapters.zalo_user.normalize import normalize_inbound
|
|
|
|
result = normalize_inbound("zalo_user", {
|
|
"from_id": "67890",
|
|
"conversation_id": "group_xyz",
|
|
"message_id": "msg_002",
|
|
"text": "Hello Group",
|
|
"conversation_type": "group",
|
|
"group_id": "g_001",
|
|
"from_display_name": "Group User",
|
|
"timestamp": 1700000000000,
|
|
})
|
|
|
|
assert result.chat_type == ChatType.GROUP
|
|
assert result.metadata["zalo_chat_type"] == "group"
|
|
assert result.metadata["group_id"] == "g_001"
|
|
|
|
def test_message_with_attachments(self):
|
|
from yuxi.channels.adapters.zalo_user.normalize import normalize_inbound
|
|
|
|
result = normalize_inbound("zalo_user", {
|
|
"from_id": "12345",
|
|
"conversation_id": "conv_abc",
|
|
"message_id": "msg_003",
|
|
"conversation_type": "direct",
|
|
"timestamp": 1700000000000,
|
|
"attachments": [
|
|
{"type": "image", "url": "http://example.com/img.jpg", "filename": "photo.jpg"},
|
|
{"type": "video", "url": "http://example.com/vid.mp4"},
|
|
],
|
|
})
|
|
|
|
assert len(result.attachments) == 2
|
|
assert result.attachments[0].type == "image"
|
|
assert result.attachments[0].url == "http://example.com/img.jpg"
|
|
assert result.attachments[0].filename == "photo.jpg"
|
|
assert result.attachments[1].type == "video"
|
|
|
|
def test_missing_required_fields_raises_error(self):
|
|
from yuxi.channels.adapters.zalo_user.normalize import normalize_inbound
|
|
from yuxi.channels.exceptions import MessageFormatError
|
|
|
|
with pytest.raises(MessageFormatError):
|
|
normalize_inbound("zalo_user", {"text": "no from_id"})
|
|
|
|
with pytest.raises(MessageFormatError):
|
|
normalize_inbound("zalo_user", {"from_id": "123", "text": "no conv_id"})
|
|
|
|
def test_timestamp_defaults_when_missing(self):
|
|
from yuxi.channels.adapters.zalo_user.normalize import normalize_inbound
|
|
|
|
result = normalize_inbound("zalo_user", {
|
|
"from_id": "12345",
|
|
"conversation_id": "conv_abc",
|
|
})
|
|
assert result.timestamp is not None
|
|
|
|
def test_timestamp_zero_uses_default(self):
|
|
from yuxi.channels.adapters.zalo_user.normalize import normalize_inbound
|
|
|
|
result = normalize_inbound("zalo_user", {
|
|
"from_id": "12345",
|
|
"conversation_id": "conv_abc",
|
|
"timestamp": 0,
|
|
})
|
|
assert result.timestamp is not None
|
|
|
|
def test_command_message_type(self):
|
|
from yuxi.channels.adapters.zalo_user.normalize import normalize_inbound
|
|
|
|
result = normalize_inbound("zalo_user", {
|
|
"from_id": "12345",
|
|
"conversation_id": "conv_abc",
|
|
"text": "/start",
|
|
"command": True,
|
|
"timestamp": 1700000000000,
|
|
})
|
|
assert result.message_type == MessageType.COMMAND
|
|
|
|
def test_reply_to_message(self):
|
|
from yuxi.channels.adapters.zalo_user.normalize import normalize_inbound
|
|
|
|
result = normalize_inbound("zalo_user", {
|
|
"from_id": "12345",
|
|
"conversation_id": "conv_abc",
|
|
"quote_message_id": "original_msg",
|
|
"timestamp": 1700000000000,
|
|
})
|
|
assert result.reply_to_message_id == "original_msg"
|
|
|
|
|
|
class TestSessionPolicies:
|
|
def test_check_dm_policy_open(self):
|
|
from yuxi.channels.adapters.zalo_user.session import check_dm_policy
|
|
|
|
assert check_dm_policy("user_1", {"dm_policy": "open"}, set()) is True
|
|
|
|
def test_check_dm_policy_disabled(self):
|
|
from yuxi.channels.adapters.zalo_user.session import check_dm_policy
|
|
|
|
assert check_dm_policy("user_1", {"dm_policy": "disabled"}, set()) is False
|
|
|
|
def test_check_dm_policy_pairing_matched(self):
|
|
from yuxi.channels.adapters.zalo_user.session import check_dm_policy
|
|
|
|
assert check_dm_policy("user_1", {"dm_policy": "pairing"}, {"zalo:user_1"}) is True
|
|
|
|
def test_check_dm_policy_pairing_not_matched(self):
|
|
from yuxi.channels.adapters.zalo_user.session import check_dm_policy
|
|
|
|
assert check_dm_policy("user_1", {"dm_policy": "pairing"}, set()) is False
|
|
|
|
def test_check_dm_policy_allowlist_matched(self):
|
|
from yuxi.channels.adapters.zalo_user.session import check_dm_policy
|
|
|
|
assert check_dm_policy("user_1", {
|
|
"dm_policy": "allowlist",
|
|
"allow_from": ["zalo:user_1"],
|
|
}, set()) is True
|
|
|
|
def test_check_group_policy_open(self):
|
|
from yuxi.channels.adapters.zalo_user.session import check_group_policy
|
|
|
|
assert check_group_policy("chat_1", "user_1", {"group_policy": "open"}) is True
|
|
|
|
def test_check_group_policy_disabled(self):
|
|
from yuxi.channels.adapters.zalo_user.session import check_group_policy
|
|
|
|
assert check_group_policy("chat_1", "user_1", {"group_policy": "disabled"}) is False
|
|
|
|
def test_check_mention_required_no_mention_needed(self):
|
|
from yuxi.channels.adapters.zalo_user.session import check_mention_required
|
|
|
|
assert check_mention_required("chat_1", "hello", {}, "") is True
|
|
|
|
def test_check_mention_required_with_bot_mention(self):
|
|
from yuxi.channels.adapters.zalo_user.session import check_mention_required
|
|
|
|
config = {
|
|
"groups": {
|
|
"chat_1": {"require_mention": True},
|
|
},
|
|
}
|
|
assert check_mention_required("chat_1", "hello @MyBot", config, "MyBot") is True
|
|
|
|
def test_check_mention_required_without_bot_mention(self):
|
|
from yuxi.channels.adapters.zalo_user.session import check_mention_required
|
|
|
|
config = {
|
|
"groups": {
|
|
"chat_1": {"require_mention": True},
|
|
},
|
|
}
|
|
assert check_mention_required("chat_1", "hello", config, "MyBot") is False
|
|
|
|
def test_resolve_agent_route_direct(self):
|
|
from yuxi.channels.adapters.zalo_user.session import resolve_agent_route
|
|
|
|
route = resolve_agent_route("zalo_user", "chat_1", "user_1", "direct", {"default_agent_id": "my_agent"})
|
|
assert route == "agent:my_agent:zalo_user:direct:user_1"
|
|
|
|
def test_resolve_agent_route_group(self):
|
|
from yuxi.channels.adapters.zalo_user.session import resolve_agent_route
|
|
|
|
config = {
|
|
"default_agent_id": "default",
|
|
"groups": {
|
|
"chat_1": {"agent_id": "group_agent"},
|
|
},
|
|
}
|
|
route = resolve_agent_route("zalo_user", "chat_1", "user_1", "group", config)
|
|
assert route == "agent:group_agent:zalo_user:group:chat_1"
|
|
|
|
|
|
class TestBuildSendPayload:
|
|
def test_text_message(self):
|
|
from yuxi.channels.adapters.zalo_user.send import _build_send_payload
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user",
|
|
channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id="bot",
|
|
channel_chat_id="conv_abc",
|
|
),
|
|
message_type=MessageType.TEXT,
|
|
content="Hello World",
|
|
)
|
|
payload = _build_send_payload(response, {})
|
|
assert payload["text"] == "Hello World"
|
|
assert payload["message_type"] == "text"
|
|
assert "attachments" not in payload
|
|
|
|
def test_image_message(self):
|
|
from yuxi.channels.adapters.zalo_user.send import _build_send_payload
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user",
|
|
channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id="bot",
|
|
channel_chat_id="conv_abc",
|
|
),
|
|
message_type=MessageType.IMAGE,
|
|
content="Check this image",
|
|
attachments=[Attachment(type="image", url="http://example.com/img.jpg")],
|
|
)
|
|
payload = _build_send_payload(response, {})
|
|
assert payload["attachments"] == [{"type": "image", "url": "http://example.com/img.jpg"}]
|
|
|
|
def test_file_message_with_filename(self):
|
|
from yuxi.channels.adapters.zalo_user.send import _build_send_payload
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user",
|
|
channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id="bot",
|
|
channel_chat_id="conv_abc",
|
|
),
|
|
message_type=MessageType.FILE,
|
|
content="Here is a file",
|
|
attachments=[Attachment(type="file", url="http://example.com/doc.pdf", filename="doc.pdf")],
|
|
)
|
|
payload = _build_send_payload(response, {})
|
|
assert payload["attachments"][0]["filename"] == "doc.pdf"
|
|
|
|
def test_reply_to_message(self):
|
|
from yuxi.channels.adapters.zalo_user.send import _build_send_payload
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user",
|
|
channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id="bot",
|
|
channel_chat_id="conv_abc",
|
|
),
|
|
message_type=MessageType.TEXT,
|
|
content="Reply",
|
|
reply_to_message_id="original_msg",
|
|
)
|
|
payload = _build_send_payload(response, {})
|
|
assert payload["quote_message_id"] == "original_msg"
|
|
|
|
def test_reply_mode_off_skips_quote(self):
|
|
from yuxi.channels.adapters.zalo_user.send import _build_send_payload
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user",
|
|
channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id="bot",
|
|
channel_chat_id="conv_abc",
|
|
),
|
|
message_type=MessageType.TEXT,
|
|
content="Reply",
|
|
reply_to_message_id="original_msg",
|
|
)
|
|
payload = _build_send_payload(response, {"reply_to_mode": "off"})
|
|
assert "quote_message_id" not in payload
|
|
|
|
def test_group_mention(self):
|
|
from yuxi.channels.adapters.zalo_user.send import _build_send_payload
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user",
|
|
channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id="bot",
|
|
channel_chat_id="conv_abc",
|
|
),
|
|
message_type=MessageType.TEXT,
|
|
content="Hello",
|
|
metadata={"chat_type": "group", "mention_user_ids": ["user_1", "user_2"]},
|
|
)
|
|
payload = _build_send_payload(response, {})
|
|
assert payload["mention_user_ids"] == ["user_1", "user_2"]
|
|
|
|
|
|
class TestBridgeClientWebSocketUrl:
|
|
def test_http_to_ws(self):
|
|
from yuxi.channels.adapters.zalo_user.bridge import _ws_url_from_http
|
|
|
|
assert _ws_url_from_http("http://localhost:5556") == "ws://localhost:5556/messages/stream"
|
|
|
|
def test_https_to_wss(self):
|
|
from yuxi.channels.adapters.zalo_user.bridge import _ws_url_from_http
|
|
|
|
assert _ws_url_from_http("https://bridge.example.com") == "wss://bridge.example.com/messages/stream"
|
|
|
|
def test_http_with_port(self):
|
|
from yuxi.channels.adapters.zalo_user.bridge import _ws_url_from_http
|
|
|
|
assert _ws_url_from_http("http://192.168.1.1:8080") == "ws://192.168.1.1:8080/messages/stream"
|
|
|
|
|
|
class TestAdapterMethods:
|
|
@pytest.mark.asyncio
|
|
async def test_edit_message_not_connected(self, adapter):
|
|
result = await adapter.edit_message("chat_1", "msg_1", "new text")
|
|
assert result.success is False
|
|
assert "Not connected" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_message_not_connected(self, adapter):
|
|
result = await adapter.delete_message("chat_1", "msg_1")
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_reaction_not_connected(self, adapter):
|
|
result = await adapter.send_reaction("chat_1", "msg_1", "\U0001f44d")
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_not_connected(self, adapter):
|
|
result = await adapter.send_media("chat_1", "image", "http://example.com/img.jpg")
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_media_not_connected(self, adapter):
|
|
from yuxi.channels.exceptions import ChannelConnectionError
|
|
|
|
with pytest.raises(ChannelConnectionError):
|
|
await adapter.download_media("file_123")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_user_info_no_bridge(self, adapter):
|
|
result = await adapter.get_user_info("user_123")
|
|
assert result["id"] == "user_123"
|
|
assert result["display_name"] == "user_123"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_check_no_bridge(self, adapter):
|
|
result = await adapter.health_check()
|
|
assert result.status == "unhealthy"
|
|
assert "Bridge not initialized" in result.last_error
|
|
|
|
|
|
class TestChunking:
|
|
def test_short_text_is_unchanged(self):
|
|
from yuxi.channels.adapters.zalo_user.chunking import chunk_text
|
|
|
|
result = chunk_text("Hello World", 2000)
|
|
assert result == ["Hello World"]
|
|
|
|
def test_text_at_limit_is_unchanged(self):
|
|
from yuxi.channels.adapters.zalo_user.chunking import chunk_text
|
|
|
|
text = "a" * 2000
|
|
result = chunk_text(text, 2000)
|
|
assert len(result) == 1
|
|
assert result[0] == text
|
|
|
|
def test_long_text_is_chunked_at_paragraphs(self):
|
|
from yuxi.channels.adapters.zalo_user.chunking import chunk_text
|
|
|
|
para = "a" * 300
|
|
text = "\n\n".join([para] * 10)
|
|
result = chunk_text(text, 2000)
|
|
assert len(result) > 1
|
|
for chunk in result:
|
|
assert len(chunk) <= 2000
|
|
|
|
def test_long_paragraph_is_chunked_at_sentence(self):
|
|
from yuxi.channels.adapters.zalo_user.chunking import chunk_text
|
|
|
|
sentence = "abc. " * 600
|
|
result = chunk_text(sentence, 2000)
|
|
assert len(result) > 1
|
|
for chunk in result:
|
|
assert len(chunk) <= 2000
|
|
|
|
def test_no_sentence_boundary_chunks_at_space(self):
|
|
from yuxi.channels.adapters.zalo_user.chunking import chunk_text
|
|
|
|
text = "x" * 3000
|
|
result = chunk_text(text, 500)
|
|
assert len(result) > 1
|
|
for chunk in result:
|
|
assert len(chunk) <= 500
|
|
|
|
|
|
class TestChunkResponseContent:
|
|
def test_text_within_limit_returns_empty(self):
|
|
from yuxi.channels.adapters.zalo_user.send import _chunk_response_content
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user", channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id="bot", channel_chat_id="conv_abc",
|
|
),
|
|
message_type=MessageType.TEXT,
|
|
content="Short message",
|
|
)
|
|
result = _chunk_response_content(response, 2000)
|
|
assert result == []
|
|
|
|
def test_text_over_limit_returns_chunks(self):
|
|
from yuxi.channels.adapters.zalo_user.send import _chunk_response_content
|
|
|
|
paragraph = "Lorem ipsum dolor sit amet.\n" * 50
|
|
content = paragraph * 30
|
|
assert len(content) > 2000
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user", channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id="bot", channel_chat_id="conv_abc",
|
|
),
|
|
message_type=MessageType.TEXT,
|
|
content=content,
|
|
)
|
|
result = _chunk_response_content(response, 2000)
|
|
assert len(result) >= 2
|
|
for chunk_payload in result:
|
|
assert len(chunk_payload["text"]) <= 2000
|
|
assert chunk_payload["message_type"] == "text"
|
|
assert chunk_payload["conversation_id"] == "conv_abc"
|
|
|
|
def test_image_message_not_chunked(self):
|
|
from yuxi.channels.adapters.zalo_user.send import _chunk_response_content
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user", channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id="bot", channel_chat_id="conv_abc",
|
|
),
|
|
message_type=MessageType.IMAGE,
|
|
content="x" * 3000,
|
|
attachments=[Attachment(type="image", url="http://example.com/img.jpg")],
|
|
)
|
|
result = _chunk_response_content(response, 2000)
|
|
assert result == []
|
|
|
|
def test_first_chunk_has_reply_to(self):
|
|
from yuxi.channels.adapters.zalo_user.send import _chunk_response_content
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user", channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id="bot", channel_chat_id="conv_abc",
|
|
),
|
|
message_type=MessageType.TEXT,
|
|
content="x" * 5000,
|
|
reply_to_message_id="original_msg",
|
|
)
|
|
result = _chunk_response_content(response, 2000)
|
|
assert len(result) >= 3
|
|
assert result[0].get("quote_message_id") == "original_msg"
|
|
assert "quote_message_id" not in result[1]
|
|
|
|
|
|
class TestPairedUsersAutoTracking:
|
|
def test_dm_user_auto_added_to_paired_users(self, adapter):
|
|
user_id = "new_user_123"
|
|
paired_key = f"zalo:{user_id}"
|
|
|
|
assert paired_key not in adapter._paired_users
|
|
|
|
message = ChannelMessage(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user",
|
|
channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id=user_id,
|
|
channel_chat_id="dm_chat",
|
|
),
|
|
event_type=EventType.MESSAGE_RECEIVED,
|
|
message_type=MessageType.TEXT,
|
|
chat_type=ChatType.DIRECT,
|
|
content="Hello bot",
|
|
metadata={"zalo_chat_type": "direct"},
|
|
)
|
|
|
|
pytest.run_sync(adapter._handle_message(message))
|
|
|
|
assert paired_key in adapter._paired_users
|
|
|
|
def test_dm_disabled_policy_blocks_message(self, adapter):
|
|
adapter.config["dm_policy"] = "disabled"
|
|
user_id = "blocked_user"
|
|
paired_key = f"zalo:{user_id}"
|
|
|
|
message = ChannelMessage(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user",
|
|
channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id=user_id,
|
|
channel_chat_id="dm_chat",
|
|
),
|
|
event_type=EventType.MESSAGE_RECEIVED,
|
|
message_type=MessageType.TEXT,
|
|
chat_type=ChatType.DIRECT,
|
|
content="Hello",
|
|
metadata={"zalo_chat_type": "direct"},
|
|
)
|
|
|
|
adapter._message_handler = None
|
|
pytest.run_sync(adapter._handle_message(message))
|
|
|
|
assert paired_key in adapter._paired_users
|
|
|
|
def test_group_message_does_not_add_to_paired(self, adapter):
|
|
user_id = "group_user"
|
|
paired_key = f"zalo:{user_id}"
|
|
|
|
adapter.config["group_policy"] = "open"
|
|
message = ChannelMessage(
|
|
identity=ChannelIdentity(
|
|
channel_id="zalo_user",
|
|
channel_type=ChannelType.ZALO_USER,
|
|
channel_user_id=user_id,
|
|
channel_chat_id="group_chat",
|
|
channel_message_id="msg_001",
|
|
),
|
|
event_type=EventType.MESSAGE_RECEIVED,
|
|
message_type=MessageType.TEXT,
|
|
chat_type=ChatType.GROUP,
|
|
content="Hello group",
|
|
metadata={"zalo_chat_type": "group", "group_id": "g_001"},
|
|
)
|
|
|
|
adapter._message_handler = None
|
|
pytest.run_sync(adapter._handle_message(message))
|
|
|
|
assert paired_key not in adapter._paired_users
|
|
|
|
|
|
class TestDisconnectResilience:
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_when_already_disconnected(self, adapter):
|
|
adapter._status = ChannelStatus.DISCONNECTED
|
|
await adapter.disconnect()
|
|
assert adapter._status == ChannelStatus.DISCONNECTED
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_with_no_monitor_or_bridge(self, adapter):
|
|
adapter._status = ChannelStatus.CONNECTED
|
|
adapter._monitor = None
|
|
adapter._bridge = None
|
|
await adapter.disconnect()
|
|
assert adapter._status == ChannelStatus.DISCONNECTED
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_with_failing_monitor(self, adapter):
|
|
class _FailingMonitor:
|
|
async def stop(self):
|
|
raise RuntimeError("stop failed")
|
|
|
|
adapter._status = ChannelStatus.CONNECTED
|
|
adapter._monitor = _FailingMonitor()
|
|
adapter._bridge = None
|
|
await adapter.disconnect()
|
|
assert adapter._status == ChannelStatus.DISCONNECTED
|
|
assert adapter._monitor is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_with_failing_bridge(self, adapter):
|
|
class _FailingBridge:
|
|
async def stop(self):
|
|
raise RuntimeError("stop failed")
|
|
|
|
adapter._status = ChannelStatus.CONNECTED
|
|
adapter._monitor = None
|
|
adapter._bridge = _FailingBridge()
|
|
await adapter.disconnect()
|
|
assert adapter._status == ChannelStatus.DISCONNECTED
|
|
assert adapter._bridge is None
|
|
|
|
|
|
def _run_sync(coro):
|
|
import asyncio
|
|
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
except RuntimeError:
|
|
return asyncio.run(coro)
|
|
import concurrent.futures
|
|
import threading
|
|
|
|
future = concurrent.futures.Future()
|
|
|
|
def _run():
|
|
try:
|
|
result = asyncio.run(coro)
|
|
future.set_result(result)
|
|
except Exception as e:
|
|
future.set_exception(e)
|
|
|
|
threading.Thread(target=_run, daemon=True).start()
|
|
return future.result()
|
|
|
|
|
|
pytest.run_sync = _run_sync |