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

270 lines
8.8 KiB
Python
Raw Normal View History

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