ForcePilot/backend/test/integration/api/channels/test_account_router.py

368 lines
12 KiB
Python
Raw Normal View History

"""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 为 superadmininclude_secrets=True 的 superadmin
# 校验在 dispatch 阶段通过;账户不存在 → 404。
# 注include_secrets=True 拒绝非 superadmin 管理员的场景无法用 admin_headers
# 验证admin_headers 本身即 superadminstandard_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