ForcePilot/backend/test/unit/channels/test_twitch_eventsub.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

249 lines
8.3 KiB
Python

from __future__ import annotations
import asyncio
from unittest.mock import AsyncMock
import pytest
from yuxi.channels.adapters.twitch.eventsub_client import EventSubListener
from yuxi.channels.adapters.twitch.helix import HelixClient
from yuxi.channels.models import (
ChatType,
EventType,
MessageType,
)
def _make_listener():
helix = HelixClient("dummy_id", "dummy_token")
return EventSubListener(
helix=helix,
app_access_token="app_token",
broadcaster_ids=["12345"],
)
class TestEventSubNormalize:
def setup_method(self):
self.listener = _make_listener()
def test_normalize_follow(self):
event = {
"id": "follow-001",
"broadcaster_user_id": "12345",
"user_id": "678",
"user_name": "Follower1",
}
msg = self.listener._normalize_follow(event, "12345")
assert msg is not None
assert msg.event_type == EventType.SYSTEM_EVENT
assert "followed" in msg.content
assert msg.metadata["event"] == "follow"
assert msg.identity.channel_user_id == "678"
def test_normalize_subscription(self):
event = {
"id": "sub-001",
"broadcaster_user_id": "12345",
"user_id": "678",
"user_name": "Subscriber1",
"tier": "1000",
}
msg = EventSubListener._normalize_subscription(event, "12345")
assert msg is not None
assert "subscribed" in msg.content
assert msg.metadata["event"] == "subscription"
assert msg.metadata["tier"] == "1000"
def test_normalize_subscription_message(self):
event = {
"id": "resub-001",
"broadcaster_user_id": "12345",
"user_id": "678",
"user_name": "Resubber1",
"tier": "2000",
"cumulative_months": 12,
"message": {"text": "I love this channel!"},
}
msg = EventSubListener._normalize_subscription_message(event, "12345")
assert msg is not None
assert "resubscribed" in msg.content
assert msg.metadata["cumulative_months"] == 12
assert msg.metadata["tier"] == "2000"
def test_normalize_subscription_gift(self):
event = {
"id": "gift-001",
"broadcaster_user_id": "12345",
"user_id": "678",
"user_name": "Gifter1",
"total": 5,
"tier": "1000",
}
msg = EventSubListener._normalize_subscription_gift(event, "12345")
assert msg is not None
assert "gifted" in msg.content
assert msg.metadata["total"] == 5
def test_normalize_cheer(self):
event = {
"id": "cheer-001",
"broadcaster_user_id": "12345",
"user_id": "678",
"user_name": "Cheerer1",
"bits": 1000,
"message": "Go team!",
}
msg = EventSubListener._normalize_cheer(event, "12345")
assert msg is not None
assert "cheered" in msg.content
assert msg.metadata["bits"] == 1000
def test_normalize_raid(self):
event = {
"id": "raid-001",
"broadcaster_user_id": "12345",
"from_broadcaster_user_id": "999",
"from_broadcaster_user_name": "RaiderChannel",
"viewers": 50,
}
msg = EventSubListener._normalize_raid(event, "12345")
assert msg is not None
assert "raided" in msg.content
assert msg.metadata["viewers"] == 50
assert msg.metadata["from_broadcaster_name"] == "RaiderChannel"
def test_normalize_channel_points(self):
event = {
"id": "cp-001",
"broadcaster_user_id": "12345",
"user_id": "678",
"user_name": "Viewer1",
"user_input": "Hello!",
"reward": {"title": "Shoutout", "cost": 500, "id": "reward-1"},
"status": "unfulfilled",
}
msg = EventSubListener._normalize_channel_points(event, "12345")
assert msg is not None
assert msg.content == "Hello!"
assert msg.metadata["reward_title"] == "Shoutout"
def test_normalize_hype_train(self):
event = {
"id": "ht-001",
"broadcaster_user_id": "12345",
"level": 3,
"progress": 200,
"goal": 500,
"total": 150,
"top_contributions": [],
}
msg = EventSubListener._normalize_hype_train(event, "12345")
assert msg is not None
assert "Hype Train" in msg.content
assert msg.metadata["level"] == 3
def test_normalize_event_routing_follow(self):
event_data = {
"subscription": {"type": "channel.follow"},
"event": {
"id": "f-1",
"broadcaster_user_id": "12345",
"user_id": "678",
"user_name": "Follower1",
},
}
msg = self.listener._normalize_event(event_data)
assert msg is not None
assert "followed" in msg.content
def test_normalize_event_routing_raid(self):
event_data = {
"subscription": {"type": "channel.raid"},
"event": {
"id": "r-1",
"broadcaster_user_id": "12345",
"from_broadcaster_user_id": "999",
"from_broadcaster_user_name": "Raider",
"viewers": 10,
},
}
msg = self.listener._normalize_event(event_data)
assert msg is not None
assert "raided" in msg.content
def test_normalize_event_unknown_type(self):
event_data = {
"subscription": {"type": "unknown.event"},
"event": {"id": "x-1", "broadcaster_user_id": "12345"},
}
msg = self.listener._normalize_event(event_data)
assert msg is None
def test_normalize_event_with_subscription_message(self):
event_data = {
"subscription": {"type": "channel.subscription.message"},
"event": {
"id": "rm-1",
"broadcaster_user_id": "12345",
"user_id": "678",
"user_name": "Resubber",
"tier": "3000",
"cumulative_months": 24,
"message": {"text": "Best channel ever!"},
},
}
msg = self.listener._normalize_event(event_data)
assert msg is not None
assert msg.metadata["event"] == "resubscription"
class TestEventSubConstants:
def test_has_follow_subscription(self):
types = EventSubListener.SUBSCRIPTION_TYPES
type_names = [t["type"] for t in types]
assert "channel.follow" in type_names
def test_has_raid_subscription(self):
types = EventSubListener.SUBSCRIPTION_TYPES
type_names = [t["type"] for t in types]
assert "channel.raid" in type_names
def test_has_subscription_message(self):
types = EventSubListener.SUBSCRIPTION_TYPES
type_names = [t["type"] for t in types]
assert "channel.subscription.message" in type_names
def test_keepalive_timeout_reasonable(self):
assert 10 <= EventSubListener.KEEPALIVE_TIMEOUT <= 30
def test_max_reconnect_delay_reasonable(self):
assert EventSubListener.MAX_RECONNECT_DELAY >= 60
class TestEventSubMessageLoopExceptions:
@pytest.mark.asyncio
async def test_cancelled_error_propagates(self):
helix = HelixClient(client_id="test_id", access_token="test_token")
listener = EventSubListener(helix, "test_app_token", ["12345"])
listener._ws = AsyncMock()
listener._ws.receive_json = AsyncMock(
side_effect=asyncio.CancelledError()
)
listener._ws.close = AsyncMock()
listener._running = True
with pytest.raises(asyncio.CancelledError):
await listener._message_loop()
@pytest.mark.asyncio
async def test_general_exception_breaks_loop(self):
helix = HelixClient(client_id="test_id", access_token="test_token")
listener = EventSubListener(helix, "test_app_token", ["12345"])
listener._ws = AsyncMock()
listener._ws.receive_json = AsyncMock(side_effect=RuntimeError("test error"))
listener._ws.close = AsyncMock()
listener._running = True
await listener._message_loop()
assert listener._running is True