1. 移除Telegram格式化测试中未使用的导入项 2. 修复Teams测试用例,添加monkeypatch参数并配置通配符开关 3. 更新钉钉适配器测试,替换弃用的流属性检查 4. 修正Twitch规范化测试,更新ROOMSTATE测试逻辑 5. 重构会话映射测试,完善数据库执行结果模拟 6. 格式化Slack块构建测试的长参数调用 7. 修复LINE适配器测试,更新能力断言和异步锁使用 8. 修正Slack会话解析测试,修复聊天类型判断错误 9. 更新能力测试,补充缺失的字段检查 10. 修复Matrix适配器测试,修正位置参数和配置校验逻辑 11. 为飞书分析模块测试添加跳过标记 12. 新增微信能力、限流、链接格式、会话路由等模块的单元测试 13. 修复Twitch适配器导入路径和测试断言 14. 新增Discord Webhook、Nextcloud Talk、Signal多账户等模块的单元测试 15. 修复Manager阶段测试的导入路径 16. 新增iMessage异常和命令处理的单元测试 17. 新增Nostr健康检查和相关模块的单元测试 18. 新增Signal守护进程和SSE重连相关测试
1211 lines
44 KiB
Python
1211 lines
44 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from yuxi.channels.adapters.zalo_oa.adapter import ZaloOAAdapter
|
|
from yuxi.channels.adapters.zalo_oa.client import ZaloOAClient
|
|
from yuxi.channels.adapters.zalo_oa.formatter import ZaloOAMessageFormatter
|
|
from yuxi.channels.adapters.zalo_oa.normalizer import SkipMessageError, ZaloOAEventNormalizer
|
|
from yuxi.channels.adapters.zalo_oa.session import resolve_thread_key
|
|
from yuxi.channels.adapters.zalo_oa.signature import verify_zalo_oa_signature
|
|
from yuxi.channels.models import (
|
|
ChannelIdentity,
|
|
ChannelMessage,
|
|
ChannelResponse,
|
|
ChannelStatus,
|
|
ChannelType,
|
|
ChatType,
|
|
EventType,
|
|
MessageType,
|
|
)
|
|
|
|
|
|
def make_text_webhook(text="hello", user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "user_send_text",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id, "display_name": "Test User"},
|
|
"recipient": {"id": oa_id},
|
|
"message": {"msg_id": "msg_abc123", "text": text},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_image_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "user_send_image",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {
|
|
"msg_id": "msg_img001",
|
|
"text": "",
|
|
"attachments": [
|
|
{
|
|
"type": "image",
|
|
"payload": {"id": "img_001", "url": "https://example.com/img.jpg"},
|
|
}
|
|
],
|
|
},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_file_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "user_send_file",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {
|
|
"msg_id": "msg_file001",
|
|
"attachments": [
|
|
{
|
|
"type": "file",
|
|
"payload": {"url": "https://example.com/doc.pdf", "name": "doc.pdf", "size": 1024},
|
|
}
|
|
],
|
|
},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_follow_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "follow",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id, "display_name": "New Follower"},
|
|
"recipient": {"id": oa_id},
|
|
"message": {},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_unfollow_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "unfollow",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_sticker_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "user_send_sticker",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {"msg_id": "msg_stk001", "text": "", "sticker_id": "stk_123"},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_link_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "user_send_link",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {
|
|
"msg_id": "msg_link001",
|
|
"text": "check this out",
|
|
"attachments": [
|
|
{
|
|
"type": "link",
|
|
"payload": {
|
|
"url": "https://example.com/article",
|
|
"title": "Example Article",
|
|
"description": "An interesting article about something",
|
|
},
|
|
}
|
|
],
|
|
},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_business_card_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "user_send_business_card",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {
|
|
"msg_id": "msg_card001",
|
|
"attachments": [
|
|
{
|
|
"type": "business_card",
|
|
"payload": {"name": "Nguyen Van A", "phone": "0912345678"},
|
|
}
|
|
],
|
|
},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_reaction_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "reaction",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {
|
|
"msg_id": "msg_react001",
|
|
"reaction": {"type": "like", "message_id": "msg_target_001"},
|
|
},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_read_receipt_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "read_receipt",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {
|
|
"read_message_id": "msg_abc123",
|
|
"read_timestamp": "1715760001",
|
|
},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_location_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "user_send_location",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {
|
|
"msg_id": "msg_loc001",
|
|
"attachments": [
|
|
{
|
|
"type": "location",
|
|
"payload": {"latitude": 10.8231, "longitude": 106.6297},
|
|
}
|
|
],
|
|
},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_video_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "user_send_video",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {
|
|
"msg_id": "msg_vid001",
|
|
"attachments": [
|
|
{
|
|
"type": "video",
|
|
"payload": {"url": "https://example.com/video.mp4", "name": "video.mp4", "size": 2048},
|
|
}
|
|
],
|
|
},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_audio_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "user_send_audio",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {
|
|
"msg_id": "msg_aud001",
|
|
"attachments": [
|
|
{
|
|
"type": "audio",
|
|
"payload": {"url": "https://example.com/audio.mp3", "name": "audio.mp3", "size": 512},
|
|
}
|
|
],
|
|
},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_forward_webhook(user_id="follower_001", oa_id="oa_001"):
|
|
return {
|
|
"event_name": "user_forward_message",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": user_id},
|
|
"recipient": {"id": oa_id},
|
|
"message": {
|
|
"msg_id": "msg_fwd001",
|
|
"text": "forwarded content",
|
|
"forwarded_from": {"id": "original_sender", "display_name": "Original User"},
|
|
},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
def make_oa_echo_webhook():
|
|
return {
|
|
"event_name": "oa_send_text",
|
|
"app_id": "test_app_id",
|
|
"sender": {"id": "oa_001"},
|
|
"recipient": {"id": "follower_001"},
|
|
"message": {"msg_id": "msg_echo", "text": "echo"},
|
|
"timestamp": "1715760000",
|
|
}
|
|
|
|
|
|
class TestWebhookSignature:
|
|
def test_valid_signature(self):
|
|
mac_key = "test-mac-key"
|
|
app_id = "test_app_id"
|
|
timestamp = "1715760000"
|
|
body = '{"event_name":"user_send_text"}'
|
|
import hashlib
|
|
|
|
raw = app_id + body + timestamp + mac_key
|
|
expected = hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
|
|
assert verify_zalo_oa_signature(body, expected, app_id, mac_key, timestamp) is True
|
|
|
|
def test_valid_signature_no_timestamp(self):
|
|
mac_key = "test-mac-key"
|
|
app_id = "test_app_id"
|
|
body = '{"event_name":"user_send_text"}'
|
|
import hashlib
|
|
|
|
raw = app_id + body + "" + mac_key
|
|
expected = hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
|
|
assert verify_zalo_oa_signature(body, expected, app_id, mac_key) is True
|
|
|
|
def test_invalid_signature(self):
|
|
mac_key = "test-mac-key"
|
|
assert verify_zalo_oa_signature("{}", "bad-signature", "app_id", mac_key) is False
|
|
|
|
def test_no_mac_key_skips_verification(self):
|
|
assert verify_zalo_oa_signature("{}", "any-sig", "app_id", "") is False
|
|
|
|
def test_empty_signature(self):
|
|
assert verify_zalo_oa_signature("{}", "", "app_id", "key") is False
|
|
|
|
|
|
class TestZaloOAEventNormalizerExtended:
|
|
def setup_method(self):
|
|
self.normalizer = ZaloOAEventNormalizer()
|
|
|
|
def test_normalize_link_message(self):
|
|
payload = make_link_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.message_type == MessageType.TEXT
|
|
assert result.event_type == EventType.MESSAGE_RECEIVED
|
|
assert "https://example.com/article" in result.content
|
|
assert "Example Article" in result.content
|
|
assert "An interesting article about something" in result.content
|
|
|
|
def test_normalize_link_message_no_title(self):
|
|
payload = make_link_webhook()
|
|
payload["message"]["attachments"][0]["payload"] = {"url": "https://example.com"}
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert "[Link] https://example.com" in result.content
|
|
assert "Title:" not in result.content
|
|
|
|
def test_normalize_business_card_message(self):
|
|
payload = make_business_card_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.message_type == MessageType.CARD
|
|
assert result.event_type == EventType.MESSAGE_RECEIVED
|
|
assert "[Business Card] Nguyen Van A" in result.content
|
|
assert "0912345678" in result.content
|
|
|
|
def test_normalize_business_card_message_no_phone(self):
|
|
payload = make_business_card_webhook()
|
|
payload["message"]["attachments"][0]["payload"] = {"name": "Tran Thi B"}
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert "[Business Card] Tran Thi B" == result.content
|
|
|
|
def test_normalize_business_card_with_contact_name_field(self):
|
|
payload = make_business_card_webhook()
|
|
payload["message"]["attachments"][0]["payload"] = {"contact_name": "Le Van C", "phone": "0987654321"}
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert "[Business Card] Le Van C" in result.content
|
|
assert "0987654321" in result.content
|
|
|
|
def test_normalize_reaction_event(self):
|
|
payload = make_reaction_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.event_type == EventType.REACTION_ADDED
|
|
assert result.identity.channel_user_id == "follower_001"
|
|
|
|
def test_normalize_read_receipt_event(self):
|
|
payload = make_read_receipt_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.event_type == EventType.READ_RECEIPT
|
|
assert result.identity.channel_user_id == "follower_001"
|
|
assert result.metadata["zalo_event_name"] == "read_receipt"
|
|
|
|
def test_normalize_location_message(self):
|
|
payload = make_location_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.message_type == MessageType.LOCATION
|
|
assert "10.8231" in result.content
|
|
assert "106.6297" in result.content
|
|
|
|
def test_normalize_video_message(self):
|
|
payload = make_video_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.message_type == MessageType.VIDEO
|
|
assert len(result.attachments) == 1
|
|
assert result.attachments[0].type == "video"
|
|
assert result.attachments[0].url == "https://example.com/video.mp4"
|
|
assert result.attachments[0].filename == "video.mp4"
|
|
assert result.attachments[0].size_bytes == 2048
|
|
|
|
def test_normalize_audio_message(self):
|
|
payload = make_audio_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.message_type == MessageType.AUDIO
|
|
assert len(result.attachments) == 1
|
|
assert result.attachments[0].type == "audio"
|
|
assert result.attachments[0].url == "https://example.com/audio.mp3"
|
|
assert result.attachments[0].filename == "audio.mp3"
|
|
assert result.attachments[0].size_bytes == 512
|
|
|
|
def test_normalize_forward_message(self):
|
|
payload = make_forward_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.message_type == MessageType.TEXT
|
|
assert result.event_type == EventType.MESSAGE_RECEIVED
|
|
assert "[Forwarded from Original User]" in result.content
|
|
assert "forwarded content" in result.content
|
|
|
|
def test_normalize_sticker_with_id(self):
|
|
payload = make_sticker_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.message_type == MessageType.STICKER
|
|
assert "Sticker: stk_123" in result.content
|
|
|
|
|
|
class TestZaloOAEventNormalizer:
|
|
def setup_method(self):
|
|
self.normalizer = ZaloOAEventNormalizer()
|
|
|
|
def test_normalize_text_message(self):
|
|
payload = make_text_webhook(text="xin chao")
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.identity.channel_id == "zalo_oa"
|
|
assert result.identity.channel_type == ChannelType.ZALO_OA
|
|
assert result.identity.channel_user_id == "follower_001"
|
|
assert result.identity.channel_chat_id == "oa_001"
|
|
assert result.identity.channel_message_id == "msg_abc123"
|
|
assert result.content == "xin chao"
|
|
assert result.message_type == MessageType.TEXT
|
|
assert result.chat_type == ChatType.DIRECT
|
|
assert result.event_type == EventType.MESSAGE_RECEIVED
|
|
assert result.metadata["zalo_event_name"] == "user_send_text"
|
|
assert result.metadata["sender_name"] == "Test User"
|
|
|
|
def test_normalize_image_message(self):
|
|
payload = make_image_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.message_type == MessageType.IMAGE
|
|
assert len(result.attachments) == 1
|
|
assert result.attachments[0].type == "image"
|
|
assert result.attachments[0].url == "https://example.com/img.jpg"
|
|
|
|
def test_normalize_file_message(self):
|
|
payload = make_file_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.message_type == MessageType.FILE
|
|
assert len(result.attachments) == 1
|
|
assert result.attachments[0].type == "file"
|
|
assert result.attachments[0].filename == "doc.pdf"
|
|
assert result.attachments[0].size_bytes == 1024
|
|
|
|
def test_normalize_follow_event(self):
|
|
payload = make_follow_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.event_type == EventType.BOT_ADDED
|
|
assert "Follower" in result.content
|
|
|
|
def test_normalize_unfollow_event(self):
|
|
payload = make_unfollow_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.event_type == EventType.BOT_REMOVED
|
|
assert "unfollowed" in result.content
|
|
|
|
def test_normalize_sticker(self):
|
|
payload = make_sticker_webhook()
|
|
result = self.normalizer.normalize(payload)
|
|
|
|
assert result.message_type == MessageType.STICKER
|
|
|
|
|
|
class TestZaloOAMessageFormatter:
|
|
def setup_method(self):
|
|
self.formatter = ZaloOAMessageFormatter()
|
|
|
|
def _make_response(self, content, message_type=MessageType.TEXT, metadata=None):
|
|
identity = ChannelIdentity(
|
|
channel_id="zalo_oa",
|
|
channel_type=ChannelType.ZALO_OA,
|
|
channel_user_id="follower_001",
|
|
channel_chat_id="oa_001",
|
|
)
|
|
return ChannelResponse(
|
|
identity=identity,
|
|
content=content,
|
|
message_type=message_type,
|
|
metadata=metadata or {},
|
|
)
|
|
|
|
def test_format_text(self):
|
|
response = self._make_response("Hello follower")
|
|
result = self.formatter.format(response)
|
|
|
|
assert result["recipient"]["user_id"] == "follower_001"
|
|
assert result["message"]["text"] == "Hello follower"
|
|
|
|
def test_format_long_text_truncated(self):
|
|
long_text = "A" * 2500
|
|
response = self._make_response(long_text)
|
|
result = self.formatter.format(response)
|
|
|
|
assert len(result["message"]["text"]) <= 2000
|
|
assert result["message"]["text"].endswith("...")
|
|
|
|
def test_format_image(self):
|
|
response = self._make_response(
|
|
"",
|
|
message_type=MessageType.IMAGE,
|
|
metadata={"attachment_id": "att_img_001"},
|
|
)
|
|
result = self.formatter.format(response)
|
|
|
|
assert result["message"]["attachment"]["type"] == "template"
|
|
payload = result["message"]["attachment"]["payload"]
|
|
assert payload["template_type"] == "media"
|
|
assert payload["elements"][0]["media_type"] == "image"
|
|
|
|
def test_format_unsupported_type_fallback(self):
|
|
response = self._make_response("video content", message_type=MessageType.VIDEO)
|
|
result = self.formatter.format(response)
|
|
|
|
assert result["message"]["text"] == "video content"
|
|
|
|
def test_format_empty_content(self):
|
|
identity = ChannelIdentity(
|
|
channel_id="zalo_oa",
|
|
channel_type=ChannelType.ZALO_OA,
|
|
channel_user_id="follower_001",
|
|
channel_chat_id="oa_001",
|
|
)
|
|
response = ChannelResponse(identity=identity, content="")
|
|
result = self.formatter.format(response)
|
|
|
|
assert result["message"]["text"] == ""
|
|
|
|
def test_build_list(self):
|
|
elements = [{"title": "Item 1", "subtitle": "Desc 1", "image_url": "https://example.com/1.jpg"}]
|
|
buttons = [{"title": "Click", "type": "oa.open.url", "payload": {"url": "https://example.com"}}]
|
|
|
|
result = self.formatter.build_list("follower_001", elements, buttons)
|
|
|
|
assert result["recipient"]["user_id"] == "follower_001"
|
|
payload = result["message"]["attachment"]["payload"]
|
|
assert payload["template_type"] == "list"
|
|
assert len(payload["elements"]) == 1
|
|
assert payload["buttons"] is not None
|
|
|
|
def test_build_list_without_buttons(self):
|
|
elements = [{"title": "Item 1", "subtitle": "Desc 1"}]
|
|
result = self.formatter.build_list("follower_001", elements)
|
|
|
|
assert "buttons" not in result["message"]["attachment"]["payload"]
|
|
|
|
def test_build_list_missing_title(self):
|
|
elements = [{"image_url": "https://example.com/1.jpg"}]
|
|
result = self.formatter.build_list("follower_001", elements)
|
|
assert result["message"]["attachment"]["payload"]["template_type"] == "list"
|
|
|
|
def test_format_file_includes_filename(self):
|
|
response = self._make_response(
|
|
"",
|
|
message_type=MessageType.FILE,
|
|
metadata={"attachment_id": "att_file_001", "filename": "report.pdf"},
|
|
)
|
|
result = self.formatter.format(response)
|
|
element = result["message"]["attachment"]["payload"]["elements"][0]
|
|
assert element["media_type"] == "file"
|
|
assert element.get("name") == "report.pdf"
|
|
|
|
|
|
class TestZaloOAAdapter:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {
|
|
"app_id": "test_app_id",
|
|
"secret_key": "test_secret_key",
|
|
"dm_policy": "open",
|
|
}
|
|
return ZaloOAAdapter(config=config)
|
|
|
|
def test_channel_id(self, adapter):
|
|
assert adapter.channel_id == "zalo_oa"
|
|
|
|
def test_channel_type(self, adapter):
|
|
assert adapter.channel_type == ChannelType.ZALO_OA
|
|
|
|
def test_capabilities(self, adapter):
|
|
assert adapter.supports_streaming is False
|
|
assert adapter.supports_markdown is False
|
|
assert adapter.text_chunk_limit == 2000
|
|
assert adapter.max_media_size_mb == 10
|
|
assert "off" in adapter.streaming_modes
|
|
assert adapter.webhook_path == "zalo_oa"
|
|
|
|
def test_initial_status(self, adapter):
|
|
assert adapter.status == ChannelStatus.DISCONNECTED
|
|
|
|
def test_normalize_text_message(self, adapter):
|
|
body = json.dumps(make_text_webhook(text="xin chao")).encode("utf-8")
|
|
result = adapter.normalize_inbound(body)
|
|
|
|
assert isinstance(result, ChannelMessage)
|
|
assert result.content == "xin chao"
|
|
assert result.identity.channel_user_id == "follower_001"
|
|
assert result.message_type == MessageType.TEXT
|
|
assert result.chat_type == ChatType.DIRECT
|
|
|
|
def test_normalize_oa_echo_skipped(self, adapter):
|
|
body = json.dumps(make_oa_echo_webhook()).encode("utf-8")
|
|
|
|
with pytest.raises(SkipMessageError):
|
|
adapter.normalize_inbound(body)
|
|
|
|
def test_format_outbound_text(self, adapter):
|
|
identity = ChannelIdentity(
|
|
channel_id="zalo_oa",
|
|
channel_type=ChannelType.ZALO_OA,
|
|
channel_user_id="follower_001",
|
|
channel_chat_id="oa_001",
|
|
)
|
|
response = ChannelResponse(identity=identity, content="hello")
|
|
result = adapter.format_outbound(response)
|
|
|
|
assert result["recipient"]["user_id"] == "follower_001"
|
|
assert result["message"]["text"] == "hello"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_not_connected(self, adapter):
|
|
identity = ChannelIdentity(
|
|
channel_id="zalo_oa",
|
|
channel_type=ChannelType.ZALO_OA,
|
|
channel_user_id="follower_001",
|
|
channel_chat_id="oa_001",
|
|
)
|
|
response = ChannelResponse(identity=identity, content="test")
|
|
result = await adapter.send(response)
|
|
assert result.success is False
|
|
assert "not connected" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_check_not_connected(self, adapter):
|
|
result = await adapter.health_check()
|
|
assert result.status == "unhealthy"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_signature_no_mac_key(self, adapter):
|
|
result = await adapter.verify_webhook_signature({}, b"{}")
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pre_connect_missing_app_id(self):
|
|
adapter = ZaloOAAdapter(config={})
|
|
result = await adapter.pre_connect()
|
|
assert result["status"] == "error"
|
|
assert "App ID" in result["message"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pre_connect_missing_secret_key(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": ""})
|
|
result = await adapter.pre_connect()
|
|
assert result["status"] == "error"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_when_disconnected(self, adapter):
|
|
await adapter.disconnect()
|
|
assert adapter.status == ChannelStatus.DISCONNECTED
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_user_info_not_connected(self, adapter):
|
|
result = await adapter.get_user_info("follower_001")
|
|
assert result == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_media_not_connected(self, adapter):
|
|
with pytest.raises(Exception):
|
|
await adapter.download_media("https://example.com/test.jpg")
|
|
|
|
|
|
class TestSessionRouting:
|
|
def test_resolve_thread_key(self):
|
|
key = resolve_thread_key("main", "follower_001")
|
|
assert key == "agent:main:zalo_oa:default:direct:follower_001"
|
|
|
|
def test_resolve_thread_key_custom_agent(self):
|
|
key = resolve_thread_key("weather_agent", "follower_999")
|
|
assert key == "agent:weather_agent:zalo_oa:default:direct:follower_999"
|
|
|
|
def test_resolve_thread_key_custom_account(self):
|
|
key = resolve_thread_key("main", "follower_001", account_id="oa_prod")
|
|
assert key == "agent:main:zalo_oa:oa_prod:direct:follower_001"
|
|
|
|
|
|
class TestZaloOAAdapterConnect:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {
|
|
"app_id": "test_app_id",
|
|
"secret_key": "test_secret_key",
|
|
"dm_policy": "open",
|
|
}
|
|
return ZaloOAAdapter(config=config)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_missing_app_id(self):
|
|
adapter = ZaloOAAdapter(config={"secret_key": "test"})
|
|
with pytest.raises(Exception):
|
|
await adapter.connect()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_missing_secret_key(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test"})
|
|
with pytest.raises(Exception):
|
|
await adapter.connect()
|
|
|
|
|
|
class TestTokenExpiredError:
|
|
def test_token_expired_error_importable(self):
|
|
from yuxi.channels.exceptions import TokenExpiredError
|
|
|
|
err = TokenExpiredError()
|
|
assert err.retryable is True
|
|
assert "expired" in str(err).lower()
|
|
|
|
|
|
class TestNormalizeInboundErrorHandling:
|
|
def test_invalid_json_raises_message_format_error(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
with pytest.raises(Exception):
|
|
adapter.normalize_inbound(b"not-valid-json")
|
|
|
|
def test_oa_echo_skipped_after_valid_json(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
body = json.dumps(make_oa_echo_webhook()).encode("utf-8")
|
|
with pytest.raises(SkipMessageError):
|
|
adapter.normalize_inbound(body)
|
|
|
|
|
|
class TestFormatterConstructor:
|
|
def test_formatter_accepts_custom_max_length(self):
|
|
formatter = ZaloOAMessageFormatter(max_text_length=500)
|
|
assert formatter.max_text_length == 500
|
|
|
|
def test_formatter_truncates_with_custom_length(self):
|
|
formatter = ZaloOAMessageFormatter(max_text_length=10)
|
|
identity = ChannelIdentity(
|
|
channel_id="zalo_oa",
|
|
channel_type=ChannelType.ZALO_OA,
|
|
channel_user_id="follower_001",
|
|
channel_chat_id="oa_001",
|
|
)
|
|
response = ChannelResponse(identity=identity, content="Hello World this is long")
|
|
result = formatter.format(response)
|
|
assert len(result["message"]["text"]) <= 10
|
|
assert result["message"]["text"].endswith("...")
|
|
|
|
def test_formatter_default_length_is_2000(self):
|
|
formatter = ZaloOAMessageFormatter()
|
|
assert formatter.max_text_length == 2000
|
|
|
|
|
|
class TestAdapterMaxTextLength:
|
|
def test_adapter_passes_text_chunk_limit_to_formatter(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
assert adapter._formatter.max_text_length == adapter.text_chunk_limit
|
|
|
|
|
|
|
|
|
|
|
|
class TestSendMedia:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {
|
|
"app_id": "test_app_id",
|
|
"secret_key": "test_secret_key",
|
|
"dm_policy": "open",
|
|
}
|
|
return ZaloOAAdapter(config=config)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_not_connected(self, adapter):
|
|
result = await adapter.send_media("user_001", "image", b"\x89PNGtest")
|
|
assert result.success is False
|
|
assert "not connected" in result.error.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_oversized_data(self, adapter):
|
|
from unittest.mock import MagicMock
|
|
adapter._status = ChannelStatus.CONNECTED
|
|
adapter._sender = MagicMock()
|
|
|
|
import random
|
|
big_data = bytes(random.getrandbits(8) for _ in range(11 * 1024 * 1024))
|
|
|
|
result = await adapter.send_media("user_001", "image", big_data)
|
|
assert result.success is False
|
|
assert "exceeds max" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_unsupported_data_type(self, adapter):
|
|
from unittest.mock import MagicMock
|
|
adapter._status = ChannelStatus.CONNECTED
|
|
adapter._sender = MagicMock()
|
|
|
|
result = await adapter.send_media("user_001", "image", 12345)
|
|
assert result.success is False
|
|
assert "unsupported data type" in result.error.lower()
|
|
|
|
|
|
class TestFormatterLocation:
|
|
def test_format_location_message(self):
|
|
formatter = ZaloOAMessageFormatter(max_text_length=2000)
|
|
identity = ChannelIdentity(
|
|
channel_id="zalo_oa",
|
|
channel_type=ChannelType.ZALO_OA,
|
|
channel_user_id="follower_001",
|
|
channel_chat_id="oa_001",
|
|
)
|
|
response = ChannelResponse(
|
|
identity=identity,
|
|
message_type=MessageType.LOCATION,
|
|
content="",
|
|
metadata={"lat": "10.8231", "lon": "106.6297"},
|
|
)
|
|
result = formatter.format(response)
|
|
payload = result["message"]["attachment"]["payload"]
|
|
elements = payload["elements"][0]
|
|
assert elements["media_type"] == "location"
|
|
assert elements["latitude"] == 10.8231
|
|
assert elements["longitude"] == 106.6297
|
|
|
|
|
|
class TestClientUploadSizeValidation:
|
|
def test_max_media_size_stored(self):
|
|
client = ZaloOAClient("app_id", "secret_key", max_media_size_mb=5)
|
|
assert client._max_media_size_mb == 5
|
|
|
|
def test_max_media_size_default(self):
|
|
client = ZaloOAClient("app_id", "secret_key")
|
|
assert client._max_media_size_mb == 10
|
|
|
|
def test_oversized_media_detected_before_upload(self):
|
|
client = ZaloOAClient("app_id", "secret_key", max_media_size_mb=1)
|
|
big_data = bytes(2 * 1024 * 1024)
|
|
|
|
size_mb = len(big_data) / (1024 * 1024)
|
|
assert size_mb > client._max_media_size_mb
|
|
assert size_mb == 2.0
|
|
|
|
|
|
class TestCapabilitiesUpdated:
|
|
def test_reply_is_false(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
assert adapter.capabilities.reply is False
|
|
|
|
def test_unsend_is_true(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
assert adapter.capabilities.unsend is True
|
|
|
|
def test_native_commands_is_true(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
assert adapter.capabilities.native_commands is True
|
|
|
|
def test_chat_types_direct_only(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
assert adapter.capabilities.chat_types == ["direct"]
|
|
|
|
def test_typing_is_true(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
assert adapter.capabilities.typing is True
|
|
|
|
def test_meta_has_selection_label(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
assert "Official Account" in adapter.meta.selection_label
|
|
|
|
def test_meta_has_blurb(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
assert len(adapter.meta.blurb) > 0
|
|
|
|
def test_meta_has_order(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
assert adapter.meta.order == 25
|
|
|
|
def test_meta_has_docs_path(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
assert adapter.meta.docs_path == "docs/channels/zalo-oa"
|
|
|
|
|
|
class TestAdapterNewEvents:
|
|
def test_adapter_normalize_link_message(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test", "dm_policy": "open"})
|
|
body = json.dumps(make_link_webhook()).encode("utf-8")
|
|
result = adapter.normalize_inbound(body)
|
|
|
|
assert isinstance(result, ChannelMessage)
|
|
assert "https://example.com/article" in result.content
|
|
assert "Example Article" in result.content
|
|
assert result.message_type == MessageType.TEXT
|
|
|
|
def test_adapter_normalize_business_card(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test", "dm_policy": "open"})
|
|
body = json.dumps(make_business_card_webhook()).encode("utf-8")
|
|
result = adapter.normalize_inbound(body)
|
|
|
|
assert isinstance(result, ChannelMessage)
|
|
assert "[Business Card] Nguyen Van A" in result.content
|
|
assert result.message_type == MessageType.CARD
|
|
|
|
def test_adapter_normalize_reaction(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test", "dm_policy": "open"})
|
|
body = json.dumps(make_reaction_webhook()).encode("utf-8")
|
|
result = adapter.normalize_inbound(body)
|
|
|
|
assert isinstance(result, ChannelMessage)
|
|
assert result.event_type == EventType.REACTION_ADDED
|
|
|
|
def test_adapter_normalize_read_receipt(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test", "dm_policy": "open"})
|
|
body = json.dumps(make_read_receipt_webhook()).encode("utf-8")
|
|
result = adapter.normalize_inbound(body)
|
|
|
|
assert isinstance(result, ChannelMessage)
|
|
assert result.event_type == EventType.READ_RECEIPT
|
|
|
|
def test_adapter_normalize_location(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test", "dm_policy": "open"})
|
|
body = json.dumps(make_location_webhook()).encode("utf-8")
|
|
result = adapter.normalize_inbound(body)
|
|
|
|
assert isinstance(result, ChannelMessage)
|
|
assert result.message_type == MessageType.LOCATION
|
|
assert "10.8231" in result.content
|
|
|
|
def test_adapter_normalize_video(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test", "dm_policy": "open"})
|
|
body = json.dumps(make_video_webhook()).encode("utf-8")
|
|
result = adapter.normalize_inbound(body)
|
|
|
|
assert isinstance(result, ChannelMessage)
|
|
assert result.message_type == MessageType.VIDEO
|
|
assert len(result.attachments) == 1
|
|
assert result.attachments[0].type == "video"
|
|
|
|
def test_adapter_normalize_audio(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test", "dm_policy": "open"})
|
|
body = json.dumps(make_audio_webhook()).encode("utf-8")
|
|
result = adapter.normalize_inbound(body)
|
|
|
|
assert isinstance(result, ChannelMessage)
|
|
assert result.message_type == MessageType.AUDIO
|
|
assert len(result.attachments) == 1
|
|
assert result.attachments[0].type == "audio"
|
|
|
|
def test_adapter_normalize_forward(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test", "dm_policy": "open"})
|
|
body = json.dumps(make_forward_webhook()).encode("utf-8")
|
|
result = adapter.normalize_inbound(body)
|
|
|
|
assert isinstance(result, ChannelMessage)
|
|
assert "[Forwarded from Original User]" in result.content
|
|
|
|
|
|
class TestSecurityWildcard:
|
|
def test_wildcard_allowlist_allows_all_in_allowlist_policy(self):
|
|
from yuxi.channels.adapters.zalo_oa.security import DMPolicy, check_dm_allowed
|
|
|
|
allowlist = {"*"}
|
|
assert check_dm_allowed("any_user_123", DMPolicy.ALLOWLIST, allowlist) is True
|
|
assert check_dm_allowed("another_user", DMPolicy.ALLOWLIST, allowlist) is True
|
|
|
|
def test_wildcard_allowlist_allows_all_in_pairing_policy(self):
|
|
from yuxi.channels.adapters.zalo_oa.security import DMPolicy, check_dm_allowed
|
|
|
|
allowlist = {"*"}
|
|
assert check_dm_allowed("any_user_123", DMPolicy.PAIRING, allowlist) is True
|
|
|
|
def test_load_allowlist_with_wildcard(self):
|
|
from yuxi.channels.adapters.zalo_oa.security import load_allowlist
|
|
|
|
config = {"allowFrom": ["*"]}
|
|
result = load_allowlist(config)
|
|
assert result == {"*"}
|
|
|
|
def test_load_allowlist_with_wildcard_among_others(self):
|
|
from yuxi.channels.adapters.zalo_oa.security import load_allowlist
|
|
|
|
config = {"allowFrom": ["user1", "*", "user2"]}
|
|
result = load_allowlist(config)
|
|
assert result == {"*"}
|
|
|
|
def test_security_warnings_for_wildcard(self):
|
|
from yuxi.channels.adapters.zalo_oa.security import collect_security_warnings
|
|
|
|
config = {"dm_policy": "allowlist", "allowFrom": ["*"]}
|
|
warnings = collect_security_warnings(config)
|
|
wildcard_warnings = [w for w in warnings if w["type"] == "allowlist_wildcard"]
|
|
assert len(wildcard_warnings) >= 1
|
|
|
|
def test_security_warnings_for_empty_allowlist(self):
|
|
from yuxi.channels.adapters.zalo_oa.security import collect_security_warnings
|
|
|
|
config = {"dm_policy": "allowlist", "allowFrom": []}
|
|
warnings = collect_security_warnings(config)
|
|
empty_warnings = [w for w in warnings if w["type"] == "empty_allowlist"]
|
|
assert len(empty_warnings) >= 1
|
|
|
|
def test_security_warnings_for_open_policy(self):
|
|
from yuxi.channels.adapters.zalo_oa.security import collect_security_warnings
|
|
|
|
config = {"dm_policy": "open"}
|
|
warnings = collect_security_warnings(config)
|
|
open_warnings = [w for w in warnings if w["type"] == "dm_policy_open"]
|
|
assert len(open_warnings) >= 1
|
|
|
|
|
|
class TestSessionRouter:
|
|
def test_session_router_ttl_expiry(self):
|
|
from yuxi.channels.adapters.zalo_oa.session import SessionRouter
|
|
|
|
router = SessionRouter(ttl_sec=0)
|
|
thread_key = router.resolve_thread_key("main", "user_001")
|
|
router.set_session(thread_key, {"key": "value"})
|
|
|
|
assert router.get_session(thread_key) is None
|
|
assert router.session_count == 0
|
|
|
|
def test_session_router_cleanup_expired(self):
|
|
from yuxi.channels.adapters.zalo_oa.session import SessionRouter
|
|
|
|
router = SessionRouter(ttl_sec=0)
|
|
for i in range(5):
|
|
key = router.resolve_thread_key("main", f"user_{i:03d}")
|
|
router.set_session(key, {"index": i})
|
|
|
|
removed = router.cleanup_expired()
|
|
assert removed == 5
|
|
|
|
def test_session_router_clear(self):
|
|
from yuxi.channels.adapters.zalo_oa.session import SessionRouter
|
|
|
|
router = SessionRouter(ttl_sec=999)
|
|
key = router.resolve_thread_key("main", "user_001")
|
|
router.set_session(key, {"data": "test"})
|
|
router.clear_session(key)
|
|
assert router.get_session(key) is None
|
|
|
|
|
|
class TestCircuitBreaker:
|
|
def test_circuit_breaker_threshold_count(self):
|
|
from yuxi.channels.adapters.zalo_oa.client import AUTH_FAILURE_CIRCUIT_BREAKER
|
|
assert AUTH_FAILURE_CIRCUIT_BREAKER == 10
|
|
|
|
def test_client_initial_auth_failure_count_zero(self):
|
|
client = ZaloOAClient("app_id", "secret_key")
|
|
assert client._auth_failure_count == 0
|
|
|
|
def test_client_circuit_breaker_initially_closed(self):
|
|
client = ZaloOAClient("app_id", "secret_key")
|
|
assert client._auth_circuit_open is False
|
|
|
|
|
|
class TestBroadcastState:
|
|
def test_broadcast_state_default_empty(self):
|
|
from yuxi.channels.adapters.zalo_oa.send import BroadcastState
|
|
|
|
state = BroadcastState(state_file="test_broadcast_state_tmp.json")
|
|
assert state.has_state is False
|
|
loaded = state.load()
|
|
assert loaded is None
|
|
|
|
def test_broadcast_state_save_and_load(self):
|
|
from yuxi.channels.adapters.zalo_oa.send import BroadcastState
|
|
|
|
state = BroadcastState(state_file="test_broadcast_state_tmp.json")
|
|
state.save("test_broadcast_id", 50, 100, "Hello broadcast")
|
|
assert state.has_state is True
|
|
|
|
loaded = state.load()
|
|
assert loaded["broadcast_id"] == "test_broadcast_id"
|
|
assert loaded["last_index"] == 50
|
|
assert loaded["total"] == 100
|
|
assert loaded["text"] == "Hello broadcast"
|
|
|
|
def test_broadcast_state_clear(self):
|
|
from yuxi.channels.adapters.zalo_oa.send import BroadcastState
|
|
|
|
state = BroadcastState(state_file="test_broadcast_state_tmp.json")
|
|
state.save("test_id", 10, 20, "test")
|
|
state.clear()
|
|
assert state.has_state is False
|
|
|
|
|
|
class TestUnsendAction:
|
|
def test_unsend_action_registered(self):
|
|
from yuxi.channels.adapters.zalo_oa.message_actions import SUPPORTED_ACTIONS, is_action_supported
|
|
|
|
assert "unsend" in SUPPORTED_ACTIONS
|
|
assert is_action_supported("unsend") is True
|
|
|
|
def test_unsend_action_description(self):
|
|
from yuxi.channels.adapters.zalo_oa.message_actions import describe_actions
|
|
|
|
actions = describe_actions()
|
|
unsend_actions = [a for a in actions if a["action"] == "unsend"]
|
|
assert len(unsend_actions) == 1
|
|
assert "message_id" in unsend_actions[0]["params"]
|
|
assert "user_id" in unsend_actions[0]["params"]
|
|
|
|
def test_unsend_action_handler_requires_client(self):
|
|
from yuxi.channels.adapters.zalo_oa.message_actions import build_action_handler
|
|
|
|
result = build_action_handler("unsend", {"message_id": "msg_001", "user_id": "user_001"}, None, None, client=None)
|
|
assert result is None
|
|
|
|
def test_unsend_action_handler_with_client(self):
|
|
from yuxi.channels.adapters.zalo_oa.message_actions import build_action_handler
|
|
|
|
result = build_action_handler(
|
|
"unsend",
|
|
{"message_id": "msg_001", "user_id": "user_001"},
|
|
None,
|
|
None,
|
|
client="mock_client",
|
|
)
|
|
assert result is not None
|
|
assert result["_unsend_message_id"] == "msg_001"
|
|
assert result["_unsend_user_id"] == "user_001"
|
|
|
|
|
|
class TestStatusIssuesEnhanced:
|
|
def test_status_issues_includes_follower_check(self):
|
|
from yuxi.channels.adapters.zalo_oa.status_issues import collect_status_issues
|
|
|
|
config = {"dm_policy": "open"}
|
|
issues = collect_status_issues(config, {"follower_count": 0})
|
|
follower_issues = [i for i in issues if i["type"] == "no_followers"]
|
|
assert len(follower_issues) >= 1
|
|
|
|
def test_status_issues_includes_security_warnings(self):
|
|
from yuxi.channels.adapters.zalo_oa.status_issues import collect_status_issues
|
|
|
|
config = {"dm_policy": "open"}
|
|
issues = collect_status_issues(config, {})
|
|
security_issues = [i for i in issues if i["type"].startswith("security_")]
|
|
assert len(security_issues) >= 1
|
|
|
|
|
|
class TestVoiceModule:
|
|
def test_voice_disabled_by_default(self):
|
|
from yuxi.channels.adapters.zalo_oa.voice import ZaloOAVoice
|
|
|
|
voice = ZaloOAVoice()
|
|
assert voice.enabled is False
|
|
|
|
def test_voice_enabled_with_api_url(self):
|
|
from yuxi.channels.adapters.zalo_oa.voice import ZaloOAVoice
|
|
|
|
voice = ZaloOAVoice(config={"voice_tts_enabled": True, "voice_tts_api_url": "https://api.example.com/tts"})
|
|
assert voice.enabled is True
|
|
|
|
def test_voice_disabled_without_api_url(self):
|
|
from yuxi.channels.adapters.zalo_oa.voice import ZaloOAVoice
|
|
|
|
voice = ZaloOAVoice(config={"voice_tts_enabled": True})
|
|
assert voice.enabled is False
|
|
|
|
|
|
class TestPollingModule:
|
|
def test_poller_initial_state(self):
|
|
from yuxi.channels.adapters.zalo_oa.polling import ZaloOAPoller
|
|
|
|
poller = ZaloOAPoller(client=None)
|
|
assert poller.is_running is False
|
|
assert poller.poll_count == 0
|
|
|
|
def test_poller_metrics(self):
|
|
from yuxi.channels.adapters.zalo_oa.polling import ZaloOAPoller
|
|
|
|
poller = ZaloOAPoller(client=None, config={"polling_interval_sec": 10, "polling_timeout_ms": 15000})
|
|
metrics = poller.get_poll_metrics()
|
|
assert metrics["interval_sec"] == 10
|
|
assert metrics["timeout_ms"] == 15000
|
|
assert metrics["running"] is False
|
|
|
|
|
|
class TestAdapterRecallMessage:
|
|
def test_recall_message_not_connected(self):
|
|
adapter = ZaloOAAdapter(config={"app_id": "test", "secret_key": "test"})
|
|
import asyncio
|
|
|
|
result = asyncio.run(adapter.recall_message("msg_id", "user_id"))
|
|
assert result is False
|
|
|
|
|
|
class TestConfigSchemaExtended:
|
|
def test_config_has_polling_fields(self):
|
|
from yuxi.channels.adapters.zalo_oa.config_schema import ZaloOAConfig
|
|
|
|
config = ZaloOAConfig()
|
|
assert config.polling_enabled is True
|
|
assert config.polling_timeout_ms == 30000
|
|
assert config.polling_interval_sec == 5
|
|
|
|
def test_config_has_voice_fields(self):
|
|
from yuxi.channels.adapters.zalo_oa.config_schema import ZaloOAConfig
|
|
|
|
config = ZaloOAConfig()
|
|
assert config.voice_tts_enabled is False
|
|
assert config.voice_tts_model == ""
|
|
|
|
def test_config_has_media_vision_extended_fields(self):
|
|
from yuxi.channels.adapters.zalo_oa.config_schema import ZaloOAConfig
|
|
|
|
config = ZaloOAConfig()
|
|
assert config.media_vision_api_url == ""
|
|
assert config.media_vision_api_key == ""
|
|
assert config.media_vision_max_tokens == 100
|
|
assert config.media_vision_timeout_sec == 15
|