ForcePilot/backend/test/integration/api/channels/test_analytics_router.py
Kris df116a069c test: 补全全量接口参数校验与权限控制测试用例
新增并完善多模块接口测试用例,覆盖:
1. 权限校验:认证缺失、普通用户越权访问场景
2. 参数合法性校验:空值、格式错误、边界值、枚举约束
3. 响应字段增强:补充业务字段断言与分页回传校验
4. 异常场景:不存在资源、超限参数、无效格式等400/404/422场景
2026-07-11 21:41:45 +08:00

290 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Integration tests for channels analytics_router endpoints."""
from __future__ import annotations
import httpx
import pytest
from .conftest import BASE_URL, DEFAULT_CHANNEL_TYPE, ISO_TIME_END, ISO_TIME_START
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
ANALYTICS_URL = f"{BASE_URL}/analytics"
def _valid_params(**overrides) -> dict:
"""Build valid query params (start_time + end_time) for analytics endpoints."""
params = {"start_time": ISO_TIME_START, "end_time": ISO_TIME_END}
params.update(overrides)
return params
# =============================================================================
# === Auth three-tier for /analytics/messages ===
# =============================================================================
async def test_analyze_messages_requires_auth(test_client: httpx.AsyncClient):
# Act
response = await test_client.get(f"{ANALYTICS_URL}/messages", params=_valid_params())
# Assert
assert response.status_code == 401, response.text
async def test_analyze_messages_requires_admin(test_client: httpx.AsyncClient, standard_user):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/messages",
params=_valid_params(),
headers=standard_user["headers"],
)
# Assert
assert response.status_code == 403, response.text
async def test_admin_can_analyze_messages(test_client: httpx.AsyncClient, admin_headers):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/messages",
params=_valid_params(),
headers=admin_headers,
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
# =============================================================================
# === Parameter validation for /analytics/messages ===
# =============================================================================
async def test_analyze_messages_rejects_missing_start_time(test_client: httpx.AsyncClient, admin_headers):
# Arrange — omit start_time
params = {"end_time": ISO_TIME_END}
# Act
response = await test_client.get(f"{ANALYTICS_URL}/messages", params=params, headers=admin_headers)
# Assert
assert response.status_code == 422, response.text
async def test_analyze_messages_rejects_missing_end_time(test_client: httpx.AsyncClient, admin_headers):
# Arrange — omit end_time
params = {"start_time": ISO_TIME_START}
# Act
response = await test_client.get(f"{ANALYTICS_URL}/messages", params=params, headers=admin_headers)
# Assert
assert response.status_code == 422, response.text
async def test_analyze_messages_rejects_invalid_time_format(test_client: httpx.AsyncClient, admin_headers):
# Arrange — "not-a-date" is not ISO 8601, parse_datetime raises ValidationError (400)
params = _valid_params(start_time="not-a-date")
# Act
response = await test_client.get(f"{ANALYTICS_URL}/messages", params=params, headers=admin_headers)
# Assert
assert response.status_code == 400, response.text
# =============================================================================
# === All analytics endpoints with valid params return 200 ===
# =============================================================================
async def test_admin_can_get_message_distribution(test_client: httpx.AsyncClient, admin_headers):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/messages/distribution",
params=_valid_params(),
headers=admin_headers,
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
async def test_admin_can_analyze_sessions(test_client: httpx.AsyncClient, admin_headers):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/sessions",
params=_valid_params(),
headers=admin_headers,
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
async def test_admin_can_analyze_delivery(test_client: httpx.AsyncClient, admin_headers):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/delivery",
params=_valid_params(),
headers=admin_headers,
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
async def test_admin_can_get_delivery_latency(test_client: httpx.AsyncClient, admin_headers):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/delivery/latency",
params=_valid_params(),
headers=admin_headers,
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
async def test_admin_can_get_delivery_funnel(test_client: httpx.AsyncClient, admin_headers):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/delivery/funnel",
params=_valid_params(),
headers=admin_headers,
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
async def test_admin_can_analyze_accounts(test_client: httpx.AsyncClient, admin_headers):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/accounts",
params=_valid_params(),
headers=admin_headers,
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
data = payload["data"]
# ANL-ACCOUNTS 序列化必须经 DTO.to_dict()trend 项使用 active_accounts
# 字段名(非 TrendDataPoint.value捕获序列化路径回归。
assert set(data.keys()) == {"by_account", "trend"}
assert isinstance(data["by_account"], list)
assert isinstance(data["trend"], list)
for point in data["trend"]:
assert set(point.keys()) == {"timestamp", "active_accounts"}
async def test_admin_can_analyze_peers(test_client: httpx.AsyncClient, admin_headers):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/peers",
params=_valid_params(),
headers=admin_headers,
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
data = payload["data"]
# ANL-PEERS 序列化必须经 DTO.to_dict()top_active 项的 first_seen /
# last_seen 使用 format_utc_datetime带 Z 后缀),捕获序列化路径回归。
assert set(data.keys()) == {"top_active"}
assert isinstance(data["top_active"], list)
for stat in data["top_active"]:
assert set(stat.keys()) == {
"peer_id",
"channel_type",
"message_count",
"session_count",
"first_seen",
"last_seen",
}
async def test_admin_can_analyze_content_review(test_client: httpx.AsyncClient, admin_headers):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/content-review",
params=_valid_params(),
headers=admin_headers,
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
data = payload["data"]
# ANL-CR 序列化必须经 DTO.to_dict()by_category 项含派生字段 rate
# dataclasses.asdict 不会生成),捕获序列化路径回归。
assert set(data.keys()) == {
"total_reviews",
"block_count",
"block_rate",
"by_category",
"trend",
}
assert isinstance(data["by_category"], list)
for item in data["by_category"]:
assert set(item.keys()) == {"category", "count", "rate"}
async def test_admin_can_analyze_messages_with_channel_type(test_client: httpx.AsyncClient, admin_headers):
# Act
response = await test_client.get(
f"{ANALYTICS_URL}/messages",
params=_valid_params(channel_type=DEFAULT_CHANNEL_TYPE),
headers=admin_headers,
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
# =============================================================================
# === /analytics/peers limit validation ===
# =============================================================================
async def test_analyze_peers_rejects_limit_zero(test_client: httpx.AsyncClient, admin_headers):
# Act — limit=0 violates ge=1
response = await test_client.get(
f"{ANALYTICS_URL}/peers",
params=_valid_params(limit=0),
headers=admin_headers,
)
# Assert
assert response.status_code == 422, response.text
async def test_analyze_peers_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers):
# Act — limit=501 violates le=500
response = await test_client.get(
f"{ANALYTICS_URL}/peers",
params=_valid_params(limit=501),
headers=admin_headers,
)
# Assert
assert response.status_code == 422, response.text
async def test_analyze_peers_rejects_empty_account_id(test_client: httpx.AsyncClient, admin_headers):
# Act — account_id="" violates min_length=1
response = await test_client.get(
f"{ANALYTICS_URL}/peers",
params=_valid_params(account_id=""),
headers=admin_headers,
)
# Assert
assert response.status_code == 422, response.text