本次提交修复了多个测试文件中的问题: 1. 为各管道测试类添加tracer_port属性初始化 2. 修复QQBot客户端关闭异常的错误类型匹配 3. 修正配对接口查询的状态值小写格式 4. 移除ChannelType枚举值的显式.value调用 5. 新增QQ常量的导出项与Instagram测试桩函数 6. 修复API接口返回值解析,正确访问data字段 7. 调整企业微信富文本消息的断言结构 8. 修正微信iLink的消息类型测试用例 9. 替换废弃的datetime.utc相关导入为UTC常量 10. 修复QQBot白名单适配器的返回值结构 11. 调整outbox工具的channel_msg_id处理逻辑 12. 新增多个适配器与服务的测试用例,覆盖异常降级、缓存处理等场景 13. 重构部分QQBot适配器的辅助函数测试,清理冗余代码 14. 修复微信iLink客户端的上传接口调用参数与缓存清理逻辑 15. 修正配置导出接口的敏感字段过滤与返回值结构
388 lines
13 KiB
Python
388 lines
13 KiB
Python
"""Integration tests for channels pairing_router endpoints."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import httpx
|
||
import pytest
|
||
|
||
from .conftest import (
|
||
BASE_URL,
|
||
DEFAULT_CHANNEL_TYPE,
|
||
ISO_TIME_END,
|
||
ISO_TIME_START,
|
||
NON_EXISTENT_ACCOUNT_ID,
|
||
NON_EXISTENT_PAIRING_ID,
|
||
NON_EXISTENT_PEER_ID,
|
||
)
|
||
|
||
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
||
|
||
PAIRINGS_URL = f"{BASE_URL}/pairings"
|
||
|
||
|
||
def _make_create_pairing_body() -> dict:
|
||
"""Build a minimal CreatePairingRequest body."""
|
||
return {
|
||
"channel_type": DEFAULT_CHANNEL_TYPE,
|
||
"account_id": NON_EXISTENT_ACCOUNT_ID,
|
||
"peer_id": NON_EXISTENT_PEER_ID,
|
||
"peer_name": "pytest_peer",
|
||
"expires_in_seconds": 604800,
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth three-tier for list pairings ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_list_pairings_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.get(PAIRINGS_URL)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_list_pairings_forbids_standard_user(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.get(PAIRINGS_URL, headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_admin_can_list_pairings(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(PAIRINGS_URL, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 200, response.text
|
||
|
||
payload = response.json()
|
||
assert payload["success"] is True
|
||
assert isinstance(payload["data"], dict)
|
||
# handler _pairingList 返回 {"pairings": [...]}(与前端 usePairingApproval 契约一致)
|
||
assert "pairings" in payload["data"]
|
||
|
||
|
||
# =============================================================================
|
||
# === Pagination + filter validation for list pairings ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_list_pairings_rejects_limit_zero(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — limit=0 violates ge=1
|
||
response = await test_client.get(
|
||
PAIRINGS_URL,
|
||
params={"limit": 0},
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_list_pairings_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — limit=1001 violates le=1000
|
||
response = await test_client.get(
|
||
PAIRINGS_URL,
|
||
params={"limit": 1001},
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_list_pairings_rejects_invalid_status(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — status 不在枚举范围内
|
||
response = await test_client.get(
|
||
PAIRINGS_URL,
|
||
params={"status": "INVALID_STATUS"},
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === GET /pairings/count — FR-33 待办角标计数 ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_count_pairings_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.get(f"{PAIRINGS_URL}/count")
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_count_pairings_forbids_standard_user(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.get(f"{PAIRINGS_URL}/count", headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_admin_can_count_pairings(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(f"{PAIRINGS_URL}/count", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 200, response.text
|
||
|
||
payload = response.json()
|
||
assert payload["success"] is True
|
||
assert isinstance(payload["data"], dict)
|
||
|
||
|
||
async def test_count_pairings_with_filters_returns_200(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — 支持按渠道/账户/对端/状态过滤
|
||
response = await test_client.get(
|
||
f"{PAIRINGS_URL}/count",
|
||
params={
|
||
"channel_type": DEFAULT_CHANNEL_TYPE,
|
||
"account_id": NON_EXISTENT_ACCOUNT_ID,
|
||
"peer_id": NON_EXISTENT_PEER_ID,
|
||
"status": "pending",
|
||
},
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 200, response.text
|
||
|
||
|
||
async def test_count_pairings_rejects_invalid_status(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — status 不在 PairingStatus 枚举范围内
|
||
response = await test_client.get(
|
||
f"{PAIRINGS_URL}/count",
|
||
params={"status": "INVALID_STATUS"},
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_count_static_path_not_captured_as_pairing_id(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — /pairings/count 是静态路径,必须先于 /pairings/{pairing_id} 声明,
|
||
# 避免 "count" 被捕获为 pairing_id。若路由顺序错误,此请求会走
|
||
# getPairing("count") 并返回 404,而非 count 端点的 200。
|
||
response = await test_client.get(f"{PAIRINGS_URL}/count", headers=admin_headers)
|
||
# Assert — 命中 count 端点返回 200,证明静态路径未被动态参数捕获
|
||
assert response.status_code == 200, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === create pairing ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_create_pairing_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = _make_create_pairing_body()
|
||
# Act
|
||
response = await test_client.post(PAIRINGS_URL, json=body)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_create_pairing_forbids_standard_user(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = _make_create_pairing_body()
|
||
# Act
|
||
response = await test_client.post(
|
||
PAIRINGS_URL,
|
||
json=body,
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_create_pairing_non_existent_account_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
body = _make_create_pairing_body()
|
||
# Act
|
||
response = await test_client.post(PAIRINGS_URL, json=body, headers=admin_headers)
|
||
# Assert — account_id 不存在:NOT_FOUND (404) 或适配器能力未实现 (501)
|
||
assert response.status_code in (404, 501), response.text
|
||
|
||
|
||
async def test_create_pairing_rejects_expires_zero(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — expires_in_seconds=0 低于下限 1
|
||
body = _make_create_pairing_body()
|
||
body["expires_in_seconds"] = 0
|
||
# Act
|
||
response = await test_client.post(PAIRINGS_URL, json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_create_pairing_rejects_expires_over_max(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — expires_in_seconds > 2592000 超过上限
|
||
body = _make_create_pairing_body()
|
||
body["expires_in_seconds"] = 2592001
|
||
# Act
|
||
response = await test_client.post(PAIRINGS_URL, json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === batch approve / reject ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_batch_approve_pairings_rejects_empty_ids(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — pairing_ids 为空违反 min_items=1
|
||
body = {"pairing_ids": []}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{PAIRINGS_URL}/batch-approve",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_batch_approve_pairings_rejects_too_many_ids(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — pairing_ids 超过 max_items=500
|
||
body = {"pairing_ids": [f"id_{i}" for i in range(501)]}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{PAIRINGS_URL}/batch-approve",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_batch_reject_pairings_rejects_empty_ids(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
body = {"pairing_ids": []}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{PAIRINGS_URL}/batch-reject",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === clean expired ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_clean_expired_pairings_rejects_max_count_zero(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — max_count=0 低于下限 1
|
||
body = {"max_count": 0}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{PAIRINGS_URL}/clean-expired",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_clean_expired_pairings_rejects_max_count_over_max(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — max_count=1001 超过上限 1000
|
||
body = {"max_count": 1001}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{PAIRINGS_URL}/clean-expired",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === pairings stats ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_get_pairings_stats_returns_200(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{PAIRINGS_URL}/stats",
|
||
params={"start_time": ISO_TIME_START, "end_time": ISO_TIME_END},
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 200, response.text
|
||
|
||
payload = response.json()
|
||
assert payload["success"] is True
|
||
assert isinstance(payload["data"], dict)
|
||
|
||
|
||
# =============================================================================
|
||
# === get pairing detail ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_get_pairing_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{PAIRINGS_URL}/{NON_EXISTENT_PAIRING_ID}",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === approve pairing ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_approve_pairing_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
body = {"approver_id": "admin_xxx", "reason": "pytest"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{PAIRINGS_URL}/{NON_EXISTENT_PAIRING_ID}/approve",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === reject pairing ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_reject_pairing_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
body = {"approver_id": "admin_xxx", "reason": "pytest"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{PAIRINGS_URL}/{NON_EXISTENT_PAIRING_ID}/reject",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === revoke pairing ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_revoke_pairing_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
body = {"reason": "pytest"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{PAIRINGS_URL}/{NON_EXISTENT_PAIRING_ID}/revoke",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|