218 lines
6.6 KiB
Python
218 lines
6.6 KiB
Python
"""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 == []
|