mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 11:34:48 +00:00
feat!: api 拆包、心跳迁入传输层、限流重试与后台 handler
This commit is contained in:
1 parent
750ed81d96
commit
3237558128
30 files changed
+1145
-714
No files matched your search
+61
-25
@@ -1,4 +1,6 @@
|
||||
"""AccessToken:成功换取与失败时抛 TokenError(带 QQ 的 code/message)。"""
|
||||
"""AccessToken:缓存复用、失败抛 TokenError、并发只换取一次、invalidate 强制刷新。"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from aiohttp import ClientTimeout
|
||||
@@ -7,27 +9,8 @@ from qqbotsdk.config import Config
|
||||
from qqbotsdk.token import AccessToken, TokenError
|
||||
|
||||
|
||||
def make_token(payload) -> AccessToken:
|
||||
class FakeResponse:
|
||||
status = 200
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._payload = payload
|
||||
|
||||
async def json(self, **kwargs):
|
||||
return self._payload
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
class FakeSession:
|
||||
def post(self, *args, **kwargs):
|
||||
return FakeResponse()
|
||||
|
||||
config = Config(
|
||||
def make_config() -> Config:
|
||||
return Config(
|
||||
app_id="id",
|
||||
app_secret="s",
|
||||
base_url="https://x",
|
||||
@@ -38,14 +21,46 @@ def make_token(payload) -> AccessToken:
|
||||
webhook_port=8080,
|
||||
webhook_path="/qqbot/webhook",
|
||||
)
|
||||
return AccessToken(config, FakeSession()) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, payload, status: int = 200) -> None:
|
||||
self.status = status
|
||||
self._payload = payload
|
||||
|
||||
async def json(self, **kwargs):
|
||||
return self._payload
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
|
||||
class FakeSession:
|
||||
"""按序弹出响应体,记录 post 次数。"""
|
||||
|
||||
def __init__(self, *payloads) -> None:
|
||||
self._payloads = list(payloads)
|
||||
self.posts = 0
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
self.posts += 1
|
||||
return FakeResponse(self._payloads.pop(0))
|
||||
|
||||
|
||||
def make_token(*payloads) -> AccessToken:
|
||||
return AccessToken(make_config(), FakeSession(*payloads)) # type: ignore[arg-type]
|
||||
|
||||
|
||||
async def test_success_caches_token():
|
||||
token = make_token({"access_token": "t1", "expires_in": 7200})
|
||||
session = FakeSession({"access_token": "t1", "expires_in": 7200})
|
||||
token = AccessToken(make_config(), session) # type: ignore[arg-type]
|
||||
assert await token.get_access_token() == "t1"
|
||||
# 缓存期内直接复用,不再发请求(FakeSession 也没有第二次响应的意义)
|
||||
# 缓存期内直接复用,不再发请求
|
||||
assert await token.get_access_token() == "t1"
|
||||
assert session.posts == 1
|
||||
|
||||
|
||||
async def test_error_payload_raises_token_error():
|
||||
@@ -62,3 +77,24 @@ async def test_non_dict_payload_raises_token_error():
|
||||
with pytest.raises(TokenError) as exc_info:
|
||||
await token.get_access_token()
|
||||
assert exc_info.value.message == "gateway busy"
|
||||
|
||||
|
||||
async def test_concurrent_first_fetch_only_requests_once():
|
||||
session = FakeSession({"access_token": "t1", "expires_in": 7200})
|
||||
token = AccessToken(make_config(), session) # type: ignore[arg-type]
|
||||
|
||||
results = await asyncio.gather(*(token.get_access_token() for _ in range(5)))
|
||||
assert results == ["t1"] * 5
|
||||
assert session.posts == 1 # 双重检查锁:并发首调只换取一次
|
||||
|
||||
|
||||
async def test_invalidate_forces_refetch():
|
||||
session = FakeSession(
|
||||
{"access_token": "t1", "expires_in": 7200},
|
||||
{"access_token": "t2", "expires_in": 7200},
|
||||
)
|
||||
token = AccessToken(make_config(), session) # type: ignore[arg-type]
|
||||
assert await token.get_access_token() == "t1"
|
||||
token.invalidate() # 模拟服务端提前作废
|
||||
assert await token.get_access_token() == "t2"
|
||||
assert session.posts == 2
|
||||
Reference in new issue
Block a user