Files
nanobot/tests/test_channel_multi_config.py

539 lines
16 KiB
Python

import pytest
from nanobot.bus.events import OutboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel
from nanobot.channels.manager import ChannelManager
from nanobot.config.schema import (
Config,
DingTalkConfig,
DingTalkMultiConfig,
DiscordConfig,
DiscordMultiConfig,
EmailConfig,
EmailMultiConfig,
FeishuConfig,
FeishuMultiConfig,
MatrixConfig,
MatrixMultiConfig,
MochatConfig,
MochatMultiConfig,
QQConfig,
QQMultiConfig,
SlackConfig,
SlackMultiConfig,
TelegramConfig,
TelegramMultiConfig,
WhatsAppConfig,
WhatsAppMultiConfig,
WecomConfig,
WecomMultiConfig,
)
class _DummyChannel(BaseChannel):
name = "dummy"
display_name = "Dummy"
async def start(self) -> None:
self._running = True
async def stop(self) -> None:
self._running = False
async def send(self, msg: OutboundMessage) -> None:
return None
def _patch_registry(monkeypatch: pytest.MonkeyPatch, channel_names: list[str]) -> None:
monkeypatch.setattr(
"nanobot.channels.registry.discover_all",
lambda: {name: _DummyChannel for name in channel_names},
)
@pytest.mark.parametrize(
("field_name", "payload", "expected_cls", "attr_name", "attr_value"),
[
(
"whatsapp",
{"enabled": True, "bridgeUrl": "ws://127.0.0.1:3001", "allowFrom": ["123"]},
WhatsAppConfig,
"bridge_url",
"ws://127.0.0.1:3001",
),
(
"telegram",
{"enabled": True, "token": "tg-1", "allowFrom": ["alice"]},
TelegramConfig,
"token",
"tg-1",
),
(
"discord",
{"enabled": True, "token": "dc-1", "allowFrom": ["42"]},
DiscordConfig,
"token",
"dc-1",
),
(
"feishu",
{"enabled": True, "appId": "fs-1", "appSecret": "secret-1", "allowFrom": ["ou_1"]},
FeishuConfig,
"app_id",
"fs-1",
),
(
"dingtalk",
{
"enabled": True,
"clientId": "dt-1",
"clientSecret": "secret-1",
"allowFrom": ["staff-1"],
},
DingTalkConfig,
"client_id",
"dt-1",
),
(
"matrix",
{
"enabled": True,
"homeserver": "https://matrix.example.com",
"accessToken": "mx-token",
"userId": "@bot:example.com",
"allowFrom": ["@alice:example.com"],
},
MatrixConfig,
"homeserver",
"https://matrix.example.com",
),
(
"email",
{
"enabled": True,
"consentGranted": True,
"imapHost": "imap.example.com",
"allowFrom": ["a@example.com"],
},
EmailConfig,
"imap_host",
"imap.example.com",
),
(
"mochat",
{
"enabled": True,
"clawToken": "claw-token",
"agentUserId": "agent-1",
"allowFrom": ["user-1"],
},
MochatConfig,
"claw_token",
"claw-token",
),
(
"slack",
{"enabled": True, "botToken": "xoxb-1", "appToken": "xapp-1", "allowFrom": ["U1"]},
SlackConfig,
"bot_token",
"xoxb-1",
),
(
"qq",
{
"enabled": True,
"appId": "qq-1",
"secret": "secret-1",
"allowFrom": ["openid-1"],
},
QQConfig,
"app_id",
"qq-1",
),
(
"wecom",
{
"enabled": True,
"botId": "wc-1",
"secret": "secret-1",
"allowFrom": ["user-1"],
},
WecomConfig,
"bot_id",
"wc-1",
),
],
)
def test_config_parses_supported_single_instance_channels(
field_name: str,
payload: dict,
expected_cls: type,
attr_name: str,
attr_value: str,
) -> None:
config = Config.model_validate({"channels": {field_name: payload}})
section = getattr(config.channels, field_name)
assert isinstance(section, expected_cls)
assert getattr(section, attr_name) == attr_value
@pytest.mark.parametrize(
("field_name", "payload", "expected_cls", "expected_names", "attr_name", "attr_value"),
[
(
"whatsapp",
{
"enabled": True,
"instances": [
{"name": "main", "bridgeUrl": "ws://127.0.0.1:3001", "allowFrom": ["123"]},
{"name": "backup", "bridgeUrl": "ws://127.0.0.1:3002", "allowFrom": ["456"]},
],
},
WhatsAppMultiConfig,
["main", "backup"],
"bridge_url",
"ws://127.0.0.1:3002",
),
(
"telegram",
{
"enabled": True,
"instances": [
{"name": "main", "token": "tg-main", "allowFrom": ["alice"]},
{"name": "backup", "token": "tg-backup", "allowFrom": ["bob"]},
],
},
TelegramMultiConfig,
["main", "backup"],
"token",
"tg-backup",
),
(
"discord",
{
"enabled": True,
"instances": [
{"name": "main", "token": "dc-main", "allowFrom": ["42"]},
{"name": "backup", "token": "dc-backup", "allowFrom": ["43"]},
],
},
DiscordMultiConfig,
["main", "backup"],
"token",
"dc-backup",
),
(
"feishu",
{
"enabled": True,
"instances": [
{"name": "main", "appId": "fs-main", "appSecret": "s1", "allowFrom": ["ou_1"]},
{
"name": "backup",
"appId": "fs-backup",
"appSecret": "s2",
"allowFrom": ["ou_2"],
},
],
},
FeishuMultiConfig,
["main", "backup"],
"app_id",
"fs-backup",
),
(
"dingtalk",
{
"enabled": True,
"instances": [
{
"name": "main",
"clientId": "dt-main",
"clientSecret": "s1",
"allowFrom": ["staff-1"],
},
{
"name": "backup",
"clientId": "dt-backup",
"clientSecret": "s2",
"allowFrom": ["staff-2"],
},
],
},
DingTalkMultiConfig,
["main", "backup"],
"client_id",
"dt-backup",
),
(
"matrix",
{
"enabled": True,
"instances": [
{
"name": "main",
"homeserver": "https://matrix-1.example.com",
"accessToken": "mx-token-1",
"userId": "@bot1:example.com",
"allowFrom": ["@alice:example.com"],
},
{
"name": "backup",
"homeserver": "https://matrix-2.example.com",
"accessToken": "mx-token-2",
"userId": "@bot2:example.com",
"allowFrom": ["@bob:example.com"],
},
],
},
MatrixMultiConfig,
["main", "backup"],
"homeserver",
"https://matrix-2.example.com",
),
(
"email",
{
"enabled": True,
"instances": [
{
"name": "work",
"consentGranted": True,
"imapHost": "imap.work",
"allowFrom": ["a@work"],
},
{
"name": "home",
"consentGranted": True,
"imapHost": "imap.home",
"allowFrom": ["a@home"],
},
],
},
EmailMultiConfig,
["work", "home"],
"imap_host",
"imap.home",
),
(
"mochat",
{
"enabled": True,
"instances": [
{
"name": "main",
"clawToken": "claw-main",
"agentUserId": "agent-1",
"allowFrom": ["user-1"],
},
{
"name": "backup",
"clawToken": "claw-backup",
"agentUserId": "agent-2",
"allowFrom": ["user-2"],
},
],
},
MochatMultiConfig,
["main", "backup"],
"claw_token",
"claw-backup",
),
(
"slack",
{
"enabled": True,
"instances": [
{
"name": "main",
"botToken": "xoxb-main",
"appToken": "xapp-main",
"allowFrom": ["U1"],
},
{
"name": "backup",
"botToken": "xoxb-backup",
"appToken": "xapp-backup",
"allowFrom": ["U2"],
},
],
},
SlackMultiConfig,
["main", "backup"],
"bot_token",
"xoxb-backup",
),
(
"qq",
{
"enabled": True,
"instances": [
{"name": "main", "appId": "qq-main", "secret": "s1", "allowFrom": ["openid-1"]},
{
"name": "backup",
"appId": "qq-backup",
"secret": "s2",
"allowFrom": ["openid-2"],
},
],
},
QQMultiConfig,
["main", "backup"],
"app_id",
"qq-backup",
),
(
"wecom",
{
"enabled": True,
"instances": [
{"name": "main", "botId": "wc-main", "secret": "s1", "allowFrom": ["user-1"]},
{
"name": "backup",
"botId": "wc-backup",
"secret": "s2",
"allowFrom": ["user-2"],
},
],
},
WecomMultiConfig,
["main", "backup"],
"bot_id",
"wc-backup",
),
],
)
def test_config_parses_supported_multi_instance_channels(
field_name: str,
payload: dict,
expected_cls: type,
expected_names: list[str],
attr_name: str,
attr_value: str,
) -> None:
config = Config.model_validate({"channels": {field_name: payload}})
section = getattr(config.channels, field_name)
assert isinstance(section, expected_cls)
assert [inst.name for inst in section.instances] == expected_names
assert getattr(section.instances[1], attr_name) == attr_value
def test_channel_manager_registers_mixed_single_and_multi_instance_channels(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_registry(
monkeypatch,
["whatsapp", "telegram", "discord", "qq", "email", "matrix", "mochat"],
)
config = Config.model_validate(
{
"channels": {
"whatsapp": {
"enabled": True,
"instances": [
{
"name": "phone-a",
"bridgeUrl": "ws://127.0.0.1:3001",
"allowFrom": ["123"],
},
],
},
"telegram": {
"enabled": True,
"instances": [
{"name": "main", "token": "tg-main", "allowFrom": ["alice"]},
{"name": "backup", "token": "tg-backup", "allowFrom": ["bob"]},
],
},
"discord": {
"enabled": True,
"token": "dc-main",
"allowFrom": ["42"],
},
"qq": {
"enabled": True,
"instances": [
{
"name": "alpha",
"appId": "qq-alpha",
"secret": "s1",
"allowFrom": ["openid-1"],
},
],
},
"email": {
"enabled": True,
"instances": [
{
"name": "work",
"consentGranted": True,
"imapHost": "imap.work",
"allowFrom": ["a@work"],
},
],
},
"matrix": {
"enabled": True,
"instances": [
{
"name": "ops",
"homeserver": "https://matrix.example.com",
"accessToken": "mx-token",
"userId": "@bot:example.com",
"allowFrom": ["@alice:example.com"],
},
],
},
"mochat": {
"enabled": True,
"instances": [
{
"name": "sales",
"clawToken": "claw-token",
"agentUserId": "agent-1",
"allowFrom": ["user-1"],
},
],
},
}
}
)
manager = ChannelManager(config, MessageBus())
assert manager.enabled_channels == [
"whatsapp/phone-a",
"telegram/main",
"telegram/backup",
"discord",
"qq/alpha",
"email/work",
"matrix/ops",
"mochat/sales",
]
assert manager.get_channel("whatsapp/phone-a").config.bridge_url == "ws://127.0.0.1:3001"
assert manager.get_channel("telegram/backup") is not None
assert manager.get_channel("telegram/backup").config.token == "tg-backup"
assert manager.get_channel("discord") is not None
assert manager.get_channel("qq/alpha").config.app_id == "qq-alpha"
assert manager.get_channel("email/work").config.imap_host == "imap.work"
assert manager.get_channel("matrix/ops").config.user_id == "@bot:example.com"
assert manager.get_channel("mochat/sales").config.claw_token == "claw-token"
def test_channel_manager_skips_empty_multi_instance_channel(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_registry(monkeypatch, ["telegram"])
config = Config.model_validate(
{"channels": {"telegram": {"enabled": True, "instances": []}}}
)
manager = ChannelManager(config, MessageBus())
assert isinstance(config.channels.telegram, TelegramMultiConfig)
assert manager.enabled_channels == []