ForcePilot/backend/test/integration/api/channels/test_pairing_router.py
Kris 04a2472014 test(channel): add full integration test suite for channels module
1. 新增channels模块集成测试目录与基础fixture
2. 为capability、doctor、reports、dashboard、directory、webhook、wizard、health、analytics、allowlist、config等路由编写完整的鉴权、参数校验与异常场景测试
3. 修复基础集成测试setup,新增external/scheduler/channel数据库schema初始化步骤
2026-07-02 03:25:27 +08:00

347 lines
10 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)
assert "items" 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
# =============================================================================
# === 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 不存在
assert response.status_code == 404, 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