Files
DanDingNoneBot/tests/test_onmyoji_gacha_http_api.py

321 lines
11 KiB
Python

"""onmyoji_gacha HTTP DataManager 测试。"""
from __future__ import annotations
import aiohttp
import importlib.util
import inspect
import pytest
import sys
import types
from pathlib import Path
def load_gacha_modules():
"""直接加载 onmyoji_gacha 子模块,避免测试环境执行 nonebot 插件入口。"""
plugin_dir = Path(__file__).resolve().parents[1] / "danding_bot" / "plugins" / "onmyoji_gacha"
package_name = "_onmyoji_gacha_under_test"
package = types.ModuleType(package_name)
package.__path__ = [str(plugin_dir)]
sys.modules[package_name] = package
for module_name in ("config", "data_manager", "gacha"):
full_name = f"{package_name}.{module_name}"
spec = importlib.util.spec_from_file_location(full_name, plugin_dir / f"{module_name}.py")
module = importlib.util.module_from_spec(spec)
sys.modules[full_name] = module
assert spec and spec.loader
spec.loader.exec_module(module)
return (
sys.modules[f"{package_name}.config"],
sys.modules[f"{package_name}.data_manager"],
sys.modules[f"{package_name}.gacha"],
)
config_module, data_manager_module, gacha_module = load_gacha_modules()
Config = config_module.Config
DataManager = data_manager_module.DataManager
GachaSystem = gacha_module.GachaSystem
def load_sign_in_module():
"""直接加载签到编排模块,避免导入 NoneBot 插件入口。"""
plugin_dir = Path(__file__).resolve().parents[1] / "danding_bot" / "plugins" / "onmyoji_gacha"
module_name = "_onmyoji_gacha_sign_in_under_test"
spec = importlib.util.spec_from_file_location(module_name, plugin_dir / "sign_in.py")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
assert spec and spec.loader
spec.loader.exec_module(module)
return module
sign_in_module = load_sign_in_module()
class FakeResponse:
"""模拟 aiohttp 响应上下文。"""
def __init__(self, payload, status=200):
self.payload = payload
self.status = status
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return None
async def json(self):
return self.payload
class FakeSession:
"""记录请求参数的 aiohttp ClientSession 替身。"""
def __init__(self, responses, calls):
self.responses = responses
self.calls = calls
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return None
def get(self, url, params=None, timeout=None):
self.calls.append({"method": "GET", "url": url, "params": params, "timeout": timeout})
return FakeResponse(self.responses.pop(0))
def post(self, url, json=None, timeout=None):
self.calls.append({"method": "POST", "url": url, "json": json, "timeout": timeout})
return FakeResponse(self.responses.pop(0))
@pytest.fixture
def fake_aiohttp(monkeypatch):
calls = []
responses = []
monkeypatch.setattr(data_manager_module.aiohttp, "ClientSession", lambda: FakeSession(responses, calls))
test_config = Config(
GACHA_API_HOST="http://xapi.test/bot/gacha/",
BOT_USER_ID="robot",
BOT_TOKEN="secret",
)
monkeypatch.setattr(data_manager_module, "config", test_config)
monkeypatch.setattr(gacha_module, "config", test_config)
return responses, calls
def success_data(data):
return {"code": 200, "message": "", "data": data}
@pytest.mark.asyncio
async def test_shikigami_cache_and_draw_send_auth_to_xapi(fake_aiohttp):
responses, calls = fake_aiohttp
responses.extend(
[
success_data(
{
"items": [
{"id": 1, "name": "灯笼鬼", "rarity": "R", "image_path": "/r/灯笼鬼.png"},
{"id": 3, "name": "茨木童子", "rarity": "SSR", "image_path": "/ssr/茨木童子.png"},
]
}
),
success_data(
{
"success": True,
"rarity": "SSR",
"name": "茨木童子",
"image_path": "/ssr/茨木童子.png",
"image_url": "/ssr/茨木童子.png",
"draws_left": 2,
"unlocked_achievements": ["no_ssr_60"],
}
),
]
)
manager = DataManager()
grouped = await manager.refresh_shikigami_data()
draw_result = await manager.record_draw_result("10001", "SSR", grouped["SSR"][0])
assert grouped["SSR"][0]["image_url"] == "/ssr/茨木童子.png"
assert draw_result["unlocked_achievements"] == ["no_ssr_60"]
assert calls[0]["method"] == "GET"
assert calls[0]["url"] == "http://xapi.test/bot/gacha/shikigami"
assert calls[0]["params"] == {"user": "robot", "token": "secret"}
assert calls[1]["method"] == "POST"
assert calls[1]["json"] == {
"user": "robot",
"token": "secret",
"user_id": "10001",
"shikigami_id": 3,
"rarity": "SSR",
"name": "茨木童子",
}
@pytest.mark.asyncio
async def test_gacha_system_draw_keeps_probability_local_and_uses_xapi(fake_aiohttp, monkeypatch):
responses, calls = fake_aiohttp
responses.extend(
[
success_data({"draws_left": 3, "daily_limit": 3}),
success_data({"items": [{"id": 3, "name": "茨木童子", "rarity": "SSR", "image_path": "/ssr/茨木童子.png"}]}),
success_data(
{
"success": True,
"rarity": "SSR",
"name": "茨木童子",
"image_path": "/ssr/茨木童子.png",
"image_url": "/ssr/茨木童子.png",
"draws_left": 2,
"unlocked_achievements": [],
}
),
]
)
manager = DataManager()
monkeypatch.setattr(gacha_module, "data_manager", manager)
system = GachaSystem()
monkeypatch.setattr(system, "_draw_rarity", lambda user_id=None: "SSR")
result = await system.draw("10001")
assert result == {
"success": True,
"rarity": "SSR",
"name": "茨木童子",
"image_url": "/ssr/茨木童子.png",
"draws_left": 2,
"unlocked_achievements": [],
}
assert [call["url"].rsplit("/", 1)[-1] for call in calls] == ["draws-left", "shikigami", "draw"]
@pytest.mark.asyncio
async def test_triple_sign_in_claim_and_query_shapes(fake_aiohttp):
responses, calls = fake_aiohttp
responses.extend(
[
success_data({"success": True, "results": [], "draws_left": 0, "unlocked_achievements": ["no_ssr_60"]}),
success_data({"success": True, "signed_already": True}),
success_data({"success": True, "reward_type": "天卡"}),
success_data({"success": True, "total_draws": 1, "R_count": 1, "SR_count": 0, "SSR_count": 0, "SP_count": 0, "recent_draws": []}),
success_data({"success": True, "date": "2026-06-20", "stats": {"total_users": 1}}),
success_data({"items": [{"user_id": "10001", "total_draws": 1, "R_count": 1, "SR_count": 0, "SSR_count": 0, "SP_count": 0, "ssr_sp_total": 0}]}),
success_data({"achievements": {"no_ssr_60": {"unlocked_date": "2026-06-20", "reward_claimed": False}}, "progress": {"no_ssr_streak": 60}}),
success_data({"success": True, "date": "2026-06-20", "records": [], "total_count": 0}),
]
)
manager = DataManager()
draws = [
{"id": 1, "name": "灯笼鬼", "rarity": "R", "image_url": "/r/灯笼鬼.png"},
{"id": 2, "name": "雪女", "rarity": "SR", "image_url": "/sr/雪女.png"},
{"id": 3, "name": "茨木童子", "rarity": "SSR", "image_url": "/ssr/茨木童子.png"},
]
triple = await manager.record_triple_draw_result("10001", draws)
signed = await manager.record_sign_in("10001", 20)
claimed = await manager.claim_achievement_reward("10001", "no_ssr_60")
stats = await manager.get_user_stats("10001")
daily = await manager.get_daily_stats("2026-06-20")
rank = await manager.get_rank()
achievements = await manager.get_user_achievements("10001")
records = await manager.get_daily_records("2026-06-20")
assert triple["unlocked_achievements"] == ["no_ssr_60"]
assert signed is False
assert claimed is True
assert stats["success"] is True
assert daily["date"] == "2026-06-20"
assert rank[0]["user_id"] == "10001"
assert "unlocked" in achievements and "progress" in achievements
assert records["total_count"] == 0
assert calls[0]["json"]["draws"][0] == {"shikigami_id": 1, "rarity": "R", "name": "灯笼鬼"}
assert calls[1]["json"]["points_awarded"] == 20
assert calls[2]["json"]["achievement_id"] == "no_ssr_60"
assert calls[3]["params"]["user_id"] == "10001"
@pytest.mark.asyncio
async def test_network_error_keeps_failure_shapes_and_no_sqlite(monkeypatch):
class FailingSession:
async def __aenter__(self):
raise aiohttp.ClientError("offline")
async def __aexit__(self, exc_type, exc, tb):
return None
monkeypatch.setattr(data_manager_module.aiohttp, "ClientSession", lambda: FailingSession())
manager = DataManager()
assert await manager.get_draws_left("10001") == 0
assert await manager.record_sign_in("10001", 20) is False
assert await manager.get_rank() == []
assert "sqlite3" not in inspect.getsource(data_manager_module)
@pytest.mark.asyncio
async def test_daily_sign_in_awards_points_only_after_sign_recorded():
calls = []
class FakeDataManager:
async def record_sign_in(self, user_id, points_awarded):
calls.append(("sign-in", user_id, points_awarded))
return True
class FakePointsAPI:
async def add_points(self, user_id, amount, source, reason):
calls.append(("add-points", user_id, amount, source, reason))
return True, 66
success, balance, status = await sign_in_module.award_daily_sign_in_points(
data_manager=FakeDataManager(),
points_api=FakePointsAPI(),
user_id="10001",
points=20,
source="gacha_sign",
reason="抽卡签到",
)
assert (success, balance, status) == (True, 66, "awarded")
assert calls == [
("sign-in", "10001", 20),
("add-points", "10001", 20, "gacha_sign", "抽卡签到"),
]
@pytest.mark.asyncio
async def test_daily_sign_in_repeat_does_not_award_points():
calls = []
class FakeDataManager:
async def record_sign_in(self, user_id, points_awarded):
calls.append(("sign-in", user_id, points_awarded))
return False
class FakePointsAPI:
async def add_points(self, user_id, amount, source, reason):
calls.append(("add-points", user_id, amount, source, reason))
return True, 66
success, balance, status = await sign_in_module.award_daily_sign_in_points(
data_manager=FakeDataManager(),
points_api=FakePointsAPI(),
user_id="10001",
points=20,
source="gacha_sign",
reason="抽卡签到",
)
assert (success, balance, status) == (False, 0, "signed_already_or_failed")
assert calls == [("sign-in", "10001", 20)]