Initial import of Shirobana bot backend
Include utility modules, shared channels, prompts, read-only skills, wardrobe memory and regression tests. Externalize legacy credentials and exclude private runtime state.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""Adversarial tests for command authorization and Telegram sticker delivery."""
|
||||
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
BASE = Path(__file__).resolve().parents[1]
|
||||
sys.path[:0] = [str(BASE / "bot-utility"), str(BASE)]
|
||||
|
||||
|
||||
def load_dispatch():
|
||||
"""Import dispatch without optional network dependencies of unused modules."""
|
||||
stubs = {}
|
||||
for name in (
|
||||
"setu", "jm", "pixiv", "bilibili", "mail", "live_monitor", "ginkgostu",
|
||||
"identity", "identity_cmd", "for_admin_komari_agent", "for_admin_catch_log",
|
||||
"mode_commands",
|
||||
):
|
||||
stubs[name] = types.ModuleType(name)
|
||||
help_ui = types.ModuleType("help_ui")
|
||||
help_ui.command_index = lambda: "help"
|
||||
stubs["help_ui"] = help_ui
|
||||
deepseek = types.ModuleType("deepseek")
|
||||
deepseek.__path__ = []
|
||||
deepseek.deepseek_balance = types.ModuleType("deepseek.deepseek_balance")
|
||||
stubs["deepseek"] = deepseek
|
||||
stubs["deepseek.deepseek_balance"] = deepseek.deepseek_balance
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"dispatch_security_test", BASE / "bot-utility" / "dispatch.py",
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
with mock.patch.dict(sys.modules, stubs):
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class ChatAdminTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_chat_on_off_requires_admin(self):
|
||||
dispatch = load_dispatch()
|
||||
channel = types.SimpleNamespace(is_admin=mock.Mock(return_value=False))
|
||||
engine = types.SimpleNamespace(set_chat_enabled=mock.Mock())
|
||||
chat = types.ModuleType("chat")
|
||||
chat.engine = engine
|
||||
dispatch.send_message = mock.AsyncMock()
|
||||
with mock.patch.dict(sys.modules, {"chat": chat}):
|
||||
handled = await dispatch._handle_chat_legacy(channel, "group", 20, 10, ".chat on")
|
||||
self.assertTrue(handled)
|
||||
engine.set_chat_enabled.assert_not_called()
|
||||
dispatch.send_message.assert_awaited_once_with(channel, "group", 20, "该指令仅管理员可用")
|
||||
|
||||
async def test_admin_can_toggle_chat(self):
|
||||
dispatch = load_dispatch()
|
||||
channel = types.SimpleNamespace(is_admin=mock.Mock(return_value=True))
|
||||
engine = types.SimpleNamespace(set_chat_enabled=mock.Mock())
|
||||
chat = types.ModuleType("chat")
|
||||
chat.engine = engine
|
||||
dispatch.send_message = mock.AsyncMock()
|
||||
with mock.patch.dict(sys.modules, {"chat": chat}):
|
||||
for arg, enabled in (("on", True), ("off", False)):
|
||||
with self.subTest(arg=arg):
|
||||
handled = await dispatch._handle_chat_legacy(
|
||||
channel, "private", 10, 10, ".chat " + arg,
|
||||
)
|
||||
self.assertTrue(handled)
|
||||
engine.set_chat_enabled.assert_called_with(enabled)
|
||||
self.assertEqual(engine.set_chat_enabled.call_count, 2)
|
||||
|
||||
|
||||
try:
|
||||
from aiogram.types import FSInputFile
|
||||
except ModuleNotFoundError:
|
||||
aiogram = types.ModuleType("aiogram")
|
||||
aiogram.__path__ = []
|
||||
aiogram_types = types.ModuleType("aiogram.types")
|
||||
|
||||
class FSInputFile:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
aiogram_types.FSInputFile = FSInputFile
|
||||
aiogram.types = aiogram_types
|
||||
sys.modules["aiogram"] = aiogram
|
||||
sys.modules["aiogram.types"] = aiogram_types
|
||||
|
||||
from tg_channel import TGChannel
|
||||
|
||||
|
||||
class TelegramStickerTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_agent_sticker_uses_sticker_api_not_photo(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
image = Path(directory) / "sticker.webp"
|
||||
image.write_bytes(b"sticker")
|
||||
bot = types.SimpleNamespace(
|
||||
send_sticker=mock.AsyncMock(return_value=object()),
|
||||
send_photo=mock.AsyncMock(return_value=object()),
|
||||
send_document=mock.AsyncMock(return_value=object()),
|
||||
)
|
||||
channel = TGChannel(bot, admin_ids=[])
|
||||
sent = await channel.send("private", 123, [{
|
||||
"type": "image", "data": {"file": image.as_uri(), "sub_type": 1},
|
||||
}])
|
||||
self.assertTrue(sent)
|
||||
bot.send_sticker.assert_awaited_once()
|
||||
self.assertEqual(bot.send_sticker.await_args.kwargs["chat_id"], 123)
|
||||
bot.send_photo.assert_not_awaited()
|
||||
bot.send_document.assert_not_awaited()
|
||||
|
||||
async def test_regular_image_still_uses_photo_api(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
image = Path(directory) / "image.jpg"
|
||||
image.write_bytes(b"image")
|
||||
bot = types.SimpleNamespace(
|
||||
send_sticker=mock.AsyncMock(return_value=object()),
|
||||
send_photo=mock.AsyncMock(return_value=object()),
|
||||
send_document=mock.AsyncMock(return_value=object()),
|
||||
)
|
||||
channel = TGChannel(bot, admin_ids=[])
|
||||
sent = await channel.send("private", 123, [{
|
||||
"type": "image", "data": {"file": image.as_uri()},
|
||||
}])
|
||||
self.assertTrue(sent)
|
||||
bot.send_photo.assert_awaited_once()
|
||||
bot.send_sticker.assert_not_awaited()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user