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