"""danding_points HTTP PointsAPI 测试。""" from __future__ import annotations import aiohttp import importlib.util import pytest import sys import types from pathlib import Path def load_points_modules(): """直接加载 danding_points 子模块,避免测试环境缺 nonebot 时执行插件元数据。""" plugin_dir = Path(__file__).resolve().parents[1] / "danding_bot" / "plugins" / "danding_points" package_name = "_danding_points_under_test" package = types.ModuleType(package_name) package.__path__ = [str(plugin_dir)] sys.modules[package_name] = package for module_name in ("config", "api"): 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}.api"], sys.modules[f"{package_name}.config"] api_module, config_module = load_points_modules() PointsAPI = api_module.PointsAPI Config = config_module.Config 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)) def make_points_api() -> PointsAPI: return PointsAPI( Config( POINTS_API_HOST="http://xapi.test/bot/points/", BOT_USER="robot", BOT_TOKEN="secret", ) ) @pytest.fixture def fake_aiohttp(monkeypatch): calls = [] responses = [] monkeypatch.setattr(api_module.aiohttp, "ClientSession", lambda: FakeSession(responses, calls)) return responses, calls @pytest.mark.asyncio async def test_get_balance_sends_auth_in_query(fake_aiohttp): responses, calls = fake_aiohttp responses.append({"code": 200, "message": "", "data": {"balance": 88}}) points = make_points_api() balance = await points.get_balance("10001") assert balance == 88 assert not hasattr(points, "db") assert calls[0]["method"] == "GET" assert calls[0]["url"] == "http://xapi.test/bot/points/balance" assert calls[0]["params"] == {"user": "robot", "token": "secret", "user_id": "10001"} @pytest.mark.asyncio async def test_add_spend_set_send_auth_in_post_body(fake_aiohttp): responses, calls = fake_aiohttp responses.extend( [ {"code": 200, "message": "", "data": {"success": True, "balance": 10}}, {"code": 200, "message": "", "data": {"success": False, "balance": 7}}, {"code": 200, "message": "", "data": {"success": True, "balance": 99}}, ] ) points = make_points_api() add_result = await points.add_points("10001", 10, "gacha_sign", "签到") spend_result = await points.spend_points("10001", 5, "horse_race", "下注") set_result = await points.set_points("10001", 99, "admin", "调整") assert add_result == (True, 10) assert spend_result == (False, 7) assert set_result == (True, 99) assert [call["method"] for call in calls] == ["POST", "POST", "POST"] assert calls[0]["json"] == { "user": "robot", "token": "secret", "user_id": "10001", "amount": 10, "source": "gacha_sign", "reason": "签到", } assert calls[1]["url"].endswith("/spend") assert calls[2]["url"].endswith("/set") @pytest.mark.asyncio async def test_transactions_and_ranking_return_items_unchanged(fake_aiohttp): responses, calls = fake_aiohttp tx_item = { "id": 1, "user_id": "10001", "amount": 10, "balance_after": 10, "source": "gacha_sign", "reason": "签到", "created_at": "2026-06-20T12:00:00", } ranking_item = { "rank": 1, "user_id": "10001", "points": 10, "total_earned": 10, "total_spent": 0, } responses.extend( [ {"code": 200, "message": "", "data": {"items": [tx_item]}}, {"code": 200, "message": "", "data": {"items": [ranking_item]}}, ] ) points = make_points_api() transactions = await points.get_transactions("10001", limit=5, offset=2) ranking = await points.get_ranking(limit=3, order_by="unknown") assert transactions == [tx_item] assert ranking == [ranking_item] assert calls[0]["params"] == { "user": "robot", "token": "secret", "user_id": "10001", "limit": 5, "offset": 2, } assert calls[1]["params"] == { "user": "robot", "token": "secret", "limit": 3, "order_by": "points", } @pytest.mark.asyncio async def test_network_error_keeps_old_failure_returns(monkeypatch): class FailingSession: async def __aenter__(self): raise aiohttp.ClientError("offline") async def __aexit__(self, exc_type, exc, tb): return None monkeypatch.setattr(api_module.aiohttp, "ClientSession", lambda: FailingSession()) points = make_points_api() assert await points.get_balance("10001") == 0 assert await points.add_points("10001", 1, "gacha_sign") == (False, 0) assert await points.spend_points("10001", 1, "horse_race") == (False, 0) assert await points.set_points("10001", 1, "admin") == (False, 0) assert await points.get_transactions("10001") == [] assert await points.get_ranking() == [] @pytest.mark.asyncio async def test_invalid_change_request_does_not_call_http(fake_aiohttp): _responses, calls = fake_aiohttp points = make_points_api() assert await points.add_points("10001", 0, "gacha_sign") == (False, 0) assert await points.spend_points("", 1, "horse_race") == (False, 0) assert await points.set_points("10001", -1, "admin") == (False, 0) assert calls == []