ForcePilot/backend/test/integration/api/channels/test_pairing_router.py
Kris 361dbe2149 test: 批量修复单元测试与集成测试用例的各类问题
本次提交修复了多个测试文件中的问题:
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. 修正配置导出接口的敏感字段过滤与返回值结构
2026-07-09 04:23:07 +08:00

388 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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