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

388 lines
13 KiB
Python
Raw Normal View History

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