131 lines
4.6 KiB
Python
131 lines
4.6 KiB
Python
"""Contract tests for the self-hosted BOXIM client."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from services.boxim_client import BoxIMClient, BoxIMError
|
|
|
|
|
|
@pytest.fixture
|
|
def config():
|
|
return {
|
|
"HUIHUI_PLATFORM_BASE_URL": "https://open.example/api",
|
|
"BOXIM_API_BASE_URL": "https://im.example/api",
|
|
"HUIHUI_APP_ID": "test_app",
|
|
"HUIHUI_ACCESS_ID": "test_access",
|
|
"HUIHUI_ACCESS_SECRET": "test_secret",
|
|
}
|
|
|
|
|
|
def _response(payload: dict, status_code: int = 200):
|
|
response = MagicMock()
|
|
response.status_code = status_code
|
|
response.json.return_value = payload
|
|
return response
|
|
|
|
|
|
def _client_patch(*, post_payload=None, request_payload=None, status_code=200):
|
|
client = AsyncMock()
|
|
if post_payload is not None:
|
|
client.post.return_value = _response(post_payload, status_code)
|
|
if request_payload is not None:
|
|
client.request.return_value = _response(request_payload, status_code)
|
|
context = AsyncMock()
|
|
context.__aenter__.return_value = client
|
|
context.__aexit__.return_value = None
|
|
return patch("services.boxim_client.httpx.AsyncClient", return_value=context), client
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_exchange_access_token_uses_huihui_bearer_and_signed_form(config):
|
|
mocked, client = _client_patch(
|
|
post_payload={"code": 0, "data": {"accessToken": "box-token", "accessTokenExpiresIn": 3600}}
|
|
)
|
|
with mocked:
|
|
result = await BoxIMClient(config).exchange_access_token("huihui-token")
|
|
|
|
assert result["accessToken"] == "box-token"
|
|
call = client.post.await_args
|
|
assert call.args[0] == "https://open.example/api/im/box/netease"
|
|
assert call.kwargs["headers"]["Authorization"] == "Bearer huihui-token"
|
|
assert call.kwargs["data"]["appId"] == "test_app"
|
|
assert len(call.kwargs["data"]["signature"]) == 32
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_self_and_incremental_private_messages_use_boxim_header(config):
|
|
client_instance = BoxIMClient(config)
|
|
mocked, client = _client_patch(
|
|
request_payload={"code": 200, "data": {"id": 42, "nickName": "Owner"}}
|
|
)
|
|
with mocked:
|
|
profile = await client_instance.get_self("box-token")
|
|
assert profile["id"] == 42
|
|
assert client.request.await_args.kwargs["headers"] == {"accessToken": "box-token"}
|
|
|
|
mocked, client = _client_patch(
|
|
request_payload={"code": 200, "data": [{"id": 101, "sendId": 7, "recvId": 42}]}
|
|
)
|
|
with mocked:
|
|
messages = await client_instance.fetch_private_messages("box-token", "100")
|
|
assert messages[0]["id"] == 101
|
|
assert client.request.await_args.kwargs["params"] == {"minId": "100"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_private_message_matches_boxim_payload(config):
|
|
mocked, client = _client_patch(
|
|
request_payload={"code": 200, "data": {"id": 88, "localId": 12345}}
|
|
)
|
|
with mocked:
|
|
result = await BoxIMClient(config).send_private_message(
|
|
"box-token", "77", "你好", local_id="12345"
|
|
)
|
|
|
|
assert result["id"] == 88
|
|
call = client.request.await_args
|
|
assert call.args[:2] == ("POST", "https://im.example/api/message/private/send")
|
|
assert call.kwargs["json"] == {
|
|
"localId": 12345,
|
|
"recvId": 77,
|
|
"content": "你好",
|
|
"type": 0,
|
|
"receipt": False,
|
|
"atUserIds": [],
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mark_private_messages_read_uses_latest_message_id(config):
|
|
mocked, client = _client_patch(request_payload={"code": 200, "data": None})
|
|
with mocked:
|
|
await BoxIMClient(config).mark_private_messages_read("box-token", "77", "101")
|
|
|
|
call = client.request.await_args
|
|
assert call.args[:2] == ("PUT", "https://im.example/api/message/private/readed")
|
|
assert call.kwargs["headers"] == {"accessToken": "box-token"}
|
|
assert call.kwargs["params"] == {"friendId": 77, "messageId": 101}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_boxim_auth_error_is_explicit(config):
|
|
mocked, _ = _client_patch(
|
|
request_payload={"code": 400, "message": "未登录"}, status_code=200
|
|
)
|
|
with mocked, pytest.raises(BoxIMError) as exc_info:
|
|
await BoxIMClient(config).get_self("expired")
|
|
assert exc_info.value.auth_error is True
|
|
|
|
|
|
def test_sign_params_include_production_required_fields(config):
|
|
params = BoxIMClient(config)._build_sign_params()
|
|
assert params["appId"] == "test_app"
|
|
assert params["accessId"] == "test_access"
|
|
assert params["signType"] == "MD5"
|
|
assert params["signVersion"] == "1.0"
|
|
assert len(params["nonce"]) == 12
|
|
assert len(params["timestamp"]) == 14
|
|
assert len(params["signature"]) == 32
|
|
assert "accessSecret" not in params
|