本次提交修复了多个测试文件中的问题: 1. 将 ChannelType 枚举调用改为字符串实例化方式 2. 修正了日志断言、异步mock使用、配置参数等多处测试细节 3. 新增了会话聚合根、跨渠道关联策略等单元测试用例 4. 修复了路由测试中的路径方法错误与断言逻辑 5. 调整了依赖导入与测试夹具的兼容性 6. 统一了重试回退调度的列表/元组使用规范
402 lines
12 KiB
Python
402 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 "items" in payload["data"]
|
||
|
||
|
||
# =============================================================================
|
||
# === 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
|