本次提交修复了多个测试文件中的问题: 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. 修正配置导出接口的敏感字段过滤与返回值结构
369 lines
12 KiB
Python
369 lines
12 KiB
Python
"""Integration tests for channels account_router endpoints."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import httpx
|
||
import pytest
|
||
|
||
from .conftest import (
|
||
BASE_URL,
|
||
DEFAULT_CHANNEL_TYPE,
|
||
NON_EXISTENT_ACCOUNT_ID,
|
||
)
|
||
|
||
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
||
|
||
ACCOUNTS_URL = f"{BASE_URL}/accounts"
|
||
|
||
|
||
def _make_create_account_body(display_name: str = "pytest_account") -> dict:
|
||
"""Build a minimal CreateAccountRequest body."""
|
||
return {
|
||
"display_name": display_name,
|
||
"raw_config": {"app_id": "pytest_app", "app_secret": "pytest_secret"},
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth three-tier for list accounts ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_list_accounts_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.get(ACCOUNTS_URL)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_list_accounts_forbids_standard_user(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.get(ACCOUNTS_URL, headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_admin_can_list_accounts(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(ACCOUNTS_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)
|
||
assert "accounts" in payload["data"]
|
||
assert isinstance(payload["data"]["accounts"], list)
|
||
|
||
|
||
# =============================================================================
|
||
# === Pagination validation for list accounts ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_list_accounts_rejects_limit_zero(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — limit=0 violates ge=1
|
||
response = await test_client.get(
|
||
ACCOUNTS_URL,
|
||
params={"limit": 0},
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_list_accounts_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — limit=1001 violates le=1000
|
||
response = await test_client.get(
|
||
ACCOUNTS_URL,
|
||
params={"limit": 1001},
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === batch enable / disable ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_batch_enable_accounts_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {"account_ids": [NON_EXISTENT_ACCOUNT_ID]}
|
||
# Act
|
||
response = await test_client.post(f"{ACCOUNTS_URL}/batch/enable", json=body)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_batch_enable_accounts_forbids_standard_user(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = {"account_ids": [NON_EXISTENT_ACCOUNT_ID]}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{ACCOUNTS_URL}/batch/enable",
|
||
json=body,
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_batch_enable_accounts_rejects_empty_body(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — account_ids 与 filter 同时为空
|
||
body = {"account_ids": None, "filter": None}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{ACCOUNTS_URL}/batch/enable",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code in (400, 422), response.text
|
||
|
||
|
||
async def test_batch_disable_accounts_rejects_empty_body(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
body = {"account_ids": None, "filter": None}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{ACCOUNTS_URL}/batch/disable",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code in (400, 422), response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === create account under channel_type ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_create_account_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = _make_create_account_body()
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts",
|
||
json=body,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_create_account_forbids_standard_user(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = _make_create_account_body()
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts",
|
||
json=body,
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_create_account_invalid_channel_type_returns_422(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — 适配器未注册时返回 422
|
||
body = _make_create_account_body()
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/invalid_channel/accounts",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert — 不应是 401/403;适配器不存在应返回 422
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_create_account_with_invalid_body_returns_not_4xx(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — display_name 过长触发校验失败
|
||
body = {
|
||
"display_name": "x" * 200,
|
||
"raw_config": {"app_id": "pytest_app"},
|
||
}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === get / update / delete / state / runtime / rotate / test / export / clone ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_get_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_update_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
body = {"display_name": "updated_name"}
|
||
# Act
|
||
response = await test_client.put(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_update_account_rejects_empty_body(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — display_name 与 raw_config 同时为空,dispatch 校验返回 400
|
||
body = {"display_name": None, "raw_config": None}
|
||
# Act
|
||
response = await test_client.put(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code in (400, 422, 404), response.text
|
||
|
||
|
||
async def test_delete_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.delete(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_enable_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/enable",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_disable_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/disable",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_recover_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/recover",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_get_runtime_status_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/runtime-status",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_rotate_credentials_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/rotate-credentials",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_test_connection_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/test-connection",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === export account (with include_secrets authorization) ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_export_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/export",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_export_account_with_secrets_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — admin_headers 为 superadmin,include_secrets=True 的 superadmin
|
||
# 校验在 dispatch 阶段通过;账户不存在 → 404。
|
||
# 注:include_secrets=True 拒绝非 superadmin 管理员的场景无法用 admin_headers
|
||
# 验证(admin_headers 本身即 superadmin);standard_user 会被 get_admin_user
|
||
# 守门拦截(403),无法抵达 include_secrets 校验。
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/export",
|
||
params={"include_secrets": True},
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === clone account ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_clone_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
body = {"new_display_name": "cloned_account", "include_credentials": False}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/clone",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_clone_account_rejects_invalid_body(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — new_display_name 过长触发校验失败
|
||
body = {"new_display_name": "x" * 200, "include_credentials": False}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/clone",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|