Files
shirobana-bot/tests/test_legacy_utility_regressions.py
RRRosmontis 34089d0042 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.
2026-11-11 00:25:30 +08:00

362 lines
16 KiB
Python

# -*- coding: utf-8 -*-
"""旧业务模块回归测试;网络、邮件、签到及生产文件路径全部隔离。"""
import asyncio
import queue
import sys
import tempfile
import types
import unittest
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
BASE = Path(__file__).resolve().parents[1]
sys.path[:0] = [str(BASE / "bot-utility"), str(BASE)]
# The minimal audit container lacks aiohttp. Tests replace every network object,
# so a tiny import stub is sufficient there; production uses the real dependency.
try:
import aiohttp # noqa: F401
except ModuleNotFoundError:
aiohttp_stub = types.ModuleType("aiohttp")
class ClientTimeout:
def __init__(self, *args, **kwargs):
pass
aiohttp_stub.ClientTimeout = ClientTimeout
aiohttp_stub.ClientSession = None
sys.modules["aiohttp"] = aiohttp_stub
import for_admin_komari_agent as komari # noqa: E402
import ginkgostu # noqa: E402
import identity_cmd # noqa: E402
import jm # noqa: E402
import live_monitor # noqa: E402
import mail # noqa: E402
import migrate_chat_data # noqa: E402
import pixiv # noqa: E402
import setu # noqa: E402
class GinkgoRegressionTests(unittest.TestCase):
URL = "https://ginkgostu.dufe.edu.cn/course/detail/1K/batch/2/signin/9/2"
def test_retry_status_confirms_success_without_another_commit(self):
response = MagicMock()
response.json.return_value = {
"data": {"signInModel": {"type": 2, "title": "t"}, "signStatus": 1}
}
session = MagicMock()
with patch.object(ginkgostu, "call_api", return_value=response), \
patch.object(ginkgostu, "commit_sign_in") as commit:
result = ginkgostu.fetch_sign_in_info(
"u", "p", self.URL, sess=session, token="token",
avoid_duplicate=True)
self.assertTrue(result["commit"]["success"])
commit.assert_not_called()
session.close.assert_not_called() # 调用方所有的缓存会话不能关闭
def test_owned_signin_session_closes_on_query_failure(self):
session = MagicMock()
with patch.object(ginkgostu, "login", return_value=(session, "token", "n")), \
patch.object(ginkgostu, "call_api",
side_effect=ginkgostu.UpstreamError("bad response")):
with self.assertRaises(ginkgostu.UpstreamError):
ginkgostu.fetch_sign_in_info("u", "p", self.URL)
session.close.assert_called_once_with()
def test_login_failure_closes_sso_session(self):
session = MagicMock()
with patch.object(ginkgostu, "new_session", return_value=session), \
patch.object(ginkgostu, "get_unify_config",
side_effect=ginkgostu.UpstreamError("failed")):
with self.assertRaises(ginkgostu.UpstreamError):
ginkgostu.login("u", "p")
session.close.assert_called_once_with()
def test_refresh_closes_replaced_cached_session(self):
old = MagicMock()
new = MagicMock()
cached = ginkgostu.GinkgoSession("u", "p")
cached.req = old
with patch.object(ginkgostu, "login", return_value=(new, "new-token", "n")):
cached.refresh()
old.close.assert_called_once_with()
self.assertIs(cached.req, new)
def test_signin_api_forwards_retry_status_check(self):
with patch.object(ginkgostu, "_call_sign_in_local",
return_value=(True, "ok", {}, False)) as local:
ginkgostu.call_sign_in_api("u", "p", self.URL, check_status=True)
self.assertTrue(local.call_args.kwargs["check_status"])
class MailRegressionTests(unittest.TestCase):
def test_transport_result_unknown_is_not_a_plain_send_failure(self):
user = {"addresses": ["sender@rrrosmontis.icu"], "default": "",
"push": {"enabled": False, "last_seen": {}}}
with patch.object(mail, "_load_user", return_value=user), \
patch.object(mail, "_ComSession", side_effect=RuntimeError("lost response")):
result = asyncio.run(mail.send_with_attachments(
42, "sender@rrrosmontis.icu", "to@example.net", "s", "b", []))
self.assertFalse(result["ok"])
self.assertEqual(result["error_code"], "send_unknown")
def test_explicit_bridge_rejection_remains_send_failed(self):
user = {"addresses": ["sender@rrrosmontis.icu"], "default": "",
"push": {"enabled": False, "last_seen": {}}}
app = MagicMock()
app.post.return_value = {"accepted": False}
with patch.object(mail, "_load_user", return_value=user), \
patch.object(mail, "_ComSession") as session:
session.return_value.__enter__.return_value = app
result = asyncio.run(mail.send_with_attachments(
42, "sender@rrrosmontis.icu", "to@example.net", "s", "b", []))
self.assertEqual(result["error_code"], "send_failed")
class LiveMonitorRegressionTests(unittest.TestCase):
def setUp(self):
self.client = live_monitor.app.test_client()
live_monitor._live_sessions.clear()
self.records = {"follows": {"42": [{
"chat_type": "group", "chat_id": 7, "channel": "qq", "site": "1",
}]}}
self.secret = patch.object(live_monitor, "SRS_CALLBACK_SECRET", "test-secret")
self.load = patch.object(live_monitor, "_load_records", return_value=self.records)
self.notifications = patch.object(live_monitor, "notification_queue", queue.Queue())
self.secret.start()
self.load.start()
self.notifications.start()
def tearDown(self):
self.notifications.stop()
self.load.stop()
self.secret.stop()
live_monitor._live_sessions.clear()
def test_missing_srs_secret_fails_closed(self):
with patch.object(live_monitor, "SRS_CALLBACK_SECRET", ""):
response = self.client.post(
"/remote_srs/on_publish", json={"stream": "room_42"})
self.assertEqual(response.status_code, 403)
self.assertTrue(live_monitor.notification_queue.empty())
def test_srs_auth_dedupe_and_unpublish_rearm(self):
payload = {"stream": "room_42"}
denied = self.client.post("/remote_srs/on_publish", json=payload)
self.assertEqual(denied.status_code, 403)
self.assertTrue(live_monitor.notification_queue.empty())
for _ in range(2):
accepted = self.client.post(
"/remote_srs/on_publish?token=test-secret", json=payload)
self.assertEqual(accepted.status_code, 200)
self.assertEqual(live_monitor.notification_queue.qsize(), 1)
denied_end = self.client.post("/remote_srs/on_unpublish", json=payload)
self.assertEqual(denied_end.status_code, 403)
ended = self.client.post(
"/remote_srs/on_unpublish", json=payload,
headers={"X-SRS-Token": "test-secret"})
self.assertEqual(ended.status_code, 200)
self.client.post("/remote_srs/on_publish?token=test-secret", json=payload)
self.assertEqual(live_monitor.notification_queue.qsize(), 2)
class KomariAndIdentityTests(unittest.IsolatedAsyncioTestCase):
async def test_recent_exception_preserves_state_without_offline_notice(self):
komari._last_state = {"node-1": {"name": "one", "offline": False}}
session = MagicMock()
session.__aenter__ = AsyncMock(return_value=session)
session.__aexit__ = AsyncMock(return_value=None)
async def get_json(_session, path, token=None):
if path == "/api/nodes":
return {"status": "success", "data": [{"uuid": "node-1", "name": "one"}]}
raise RuntimeError("temporary recent failure")
with patch.object(komari, "_login", AsyncMock(return_value="key")), \
patch.object(komari.aiohttp, "ClientSession", return_value=session), \
patch.object(komari, "_get_json", side_effect=get_json), \
patch.object(komari, "_notify_admins", AsyncMock()) as notify:
await komari._check_servers_once()
self.assertEqual(komari._last_state["node-1"]["offline"], False)
notify.assert_not_awaited()
async def test_notification_send_false_is_not_reported_as_sent(self):
channel = object()
with patch.object(identity_cmd, "get_channel", return_value=channel), \
patch.object(identity_cmd, "send_message", AsyncMock(return_value=False)):
self.assertFalse(await identity_cmd._notify("qq", 42, "text"))
with patch.object(identity_cmd, "get_channel", return_value=channel), \
patch.object(identity_cmd, "send_message", AsyncMock(return_value=True)):
self.assertTrue(await identity_cmd._notify("qq", 42, "text"))
class QuarantineRegressionTests(unittest.TestCase):
def test_same_name_quarantine_files_never_overwrite(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
source = root / "source"
dest = root / "dest"
first = source / "a" / "same.txt"
second = source / "b" / "same.txt"
first.parent.mkdir(parents=True)
second.parent.mkdir(parents=True)
migrator = migrate_chat_data.ChatMigrator.__new__(
migrate_chat_data.ChatMigrator)
migrator.source = source
migrator.dest = dest
migrator.report = {"quarantine": []}
first.write_bytes(b"first")
second.write_bytes(b"second")
migrator._quarantine(first, "history", "one")
migrator._quarantine(second, "history", "two")
first.write_bytes(b"changed later")
migrator._quarantine(first, "history", "three")
saved = sorted(p.read_bytes() for p in (dest / "quarantine" / "history").iterdir())
self.assertEqual(saved, [b"changed later", b"first", b"second"])
class JmRegressionTests(unittest.TestCase):
def test_local_pack_import_and_stale_workdir_cleanup(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
album = "350234"
stale = root / album / "old-chapter" / "stale.jpg"
stale.parent.mkdir(parents=True)
stale.write_bytes(b"stale")
plan = ("title", [(101,)], 1, 1, "title.zip")
class Downloader:
def __init__(self, _option):
pass
def download_photo(self, photo_id):
target = root / album / str(photo_id)
target.mkdir(parents=True, exist_ok=True)
(target / "image.webp").write_bytes(b"not-an-image-is-copied")
class Options:
@staticmethod
def construct(_config):
return object()
class Config:
option_class = Options
@staticmethod
def downloader_class():
return Downloader
jmcomic_stub = types.ModuleType("jmcomic")
jmcomic_stub.JmModuleConfig = Config
with patch.object(jm, "JM_DIR", str(root)), \
patch.dict(sys.modules, {"jmcomic": jmcomic_stub}):
title, zip_path, count, ok, failed = jm._download_and_pack(
album, 1, 1, plan=plan)
self.assertEqual((title, count, ok, failed), ("title", 1, 1, 0))
self.assertTrue(zip_path.is_file())
self.assertFalse(stale.exists())
class ImageCleanupTests(unittest.IsolatedAsyncioTestCase):
async def test_pixiv_cancellation_cleans_cache_and_releases_lock(self):
with tempfile.TemporaryDirectory() as tmp:
pid = "424242"
local = Path(tmp) / (pid + ".jpg")
local.write_bytes(b"image")
lock = pixiv.get_pid_lock(pid)
with patch.object(pixiv, "SEND_IMAGE", 1), \
patch.object(pixiv, "RETAIN_MODE", "oss"), \
patch.object(pixiv, "IS_NAPCAT_SERVER_OSS_AVAILABLE", False), \
patch.object(pixiv, "OSS_SAVE_DIR", str(Path(tmp) / "oss")), \
patch.object(pixiv, "_load_proxy_ports", return_value=[]), \
patch.object(pixiv, "download_image", return_value=local), \
patch.object(pixiv, "image_base64", return_value="base64://x"), \
patch("channel.send_message", AsyncMock(return_value=True)), \
patch.object(pixiv.asyncio, "sleep",
AsyncMock(side_effect=asyncio.CancelledError)):
with self.assertRaises(asyncio.CancelledError):
await pixiv.handle_pixiv(MagicMock(name="qq"), "private", 1, 2,
".pixiv " + pid)
self.assertFalse(local.exists())
self.assertFalse(lock.locked())
async def test_setu_cancellation_cleans_cache_and_releases_lock(self):
class Response:
status = 200
async def json(self):
return {"data": [{"pid": 77, "urls": {
"original": "https://i.pixiv.re/img/77.jpg"}}]}
class Context:
async def __aenter__(self):
return Response()
async def __aexit__(self, *args):
return None
class Session:
def __init__(self, *args, **kwargs):
pass
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return None
def get(self, *args, **kwargs):
return Context()
with tempfile.TemporaryDirectory() as tmp:
local = Path(tmp) / "77.jpg"
local.write_bytes(b"image")
lock = pixiv.get_pid_lock(77)
with patch.object(setu, "SEND_IMAGE", 1), \
patch.object(setu, "RETAIN_MODE", "oss"), \
patch.object(setu, "IS_NAPCAT_SERVER_OSS_AVAILABLE", False), \
patch.object(setu, "OSS_SAVE_DIR", str(Path(tmp) / "oss")), \
patch.object(setu, "_load_proxy_ports", return_value=[]), \
patch.object(setu, "count_oss_files", return_value=0), \
patch.object(setu.aiohttp, "ClientSession", Session), \
patch.object(pixiv, "download_image", return_value=local), \
patch.object(pixiv, "image_base64", return_value="base64://x"), \
patch("channel.send_message", AsyncMock(return_value=True)), \
patch.object(setu.asyncio, "sleep",
AsyncMock(side_effect=asyncio.CancelledError)):
with self.assertRaises(asyncio.CancelledError):
await setu.handle_setu([], None, None, "private", 1, MagicMock(name="qq"))
self.assertFalse(local.exists())
self.assertFalse(lock.locked())
async def test_interrupted_download_removes_partial_file(self):
class Response:
status_code = 200
headers = {"Content-Type": "image/jpeg"}
def iter_content(self, _size):
yield b"partial"
raise RuntimeError("connection lost")
def close(self):
self.closed = True
response = Response()
with tempfile.TemporaryDirectory() as tmp, \
patch.object(pixiv, "PIXIV_CACHE_DIR", tmp), \
patch("requests.get", return_value=response):
with self.assertRaises(RuntimeError):
pixiv.download_image("88")
self.assertEqual(list(Path(tmp).iterdir()), [])
self.assertTrue(response.closed)
if __name__ == "__main__":
unittest.main()