"""Unit tests for app.analytics.api.""" from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient pytestmark = pytest.mark.unit def _build_app() -> FastAPI: from app.analytics.api import router app = FastAPI() app.include_router(router) return app def _make_mock_pool() -> MagicMock: mock_conn = AsyncMock() mock_ctx = AsyncMock() mock_ctx.__aenter__ = AsyncMock(return_value=mock_conn) mock_ctx.__aexit__ = AsyncMock(return_value=None) mock_pool = MagicMock() mock_pool.connection.return_value = mock_ctx return mock_pool def _make_analytics_result() -> object: from app.analytics.models import AgentUsage, AnalyticsResult, InterruptStats return AnalyticsResult( range="7d", total_conversations=50, resolution_rate=0.8, escalation_rate=0.1, avg_turns_per_conversation=3.5, avg_cost_per_conversation_usd=0.02, agent_usage=(AgentUsage(agent="order_agent", count=30, percentage=60.0),), interrupt_stats=InterruptStats(total=5, approved=4, rejected=1, expired=0), ) def _get_analytics(app: FastAPI, path: str = "/api/analytics", **patch_kwargs: object) -> object: """Helper: patch get_analytics, make request, return (response, mock).""" analytics_result = _make_analytics_result() with ( patch("app.analytics.api.get_analytics", return_value=analytics_result) as mock_ga, TestClient(app) as client, ): resp = client.get(path) return resp, mock_ga class TestAnalyticsEndpoint: def test_returns_200_with_default_range(self) -> None: app = _build_app() app.state.pool = _make_mock_pool() resp, _ = _get_analytics(app) assert resp.status_code == 200 body = resp.json() assert body["success"] is True assert body["error"] is None assert body["data"]["range"] == "7d" def test_returns_correct_analytics_structure(self) -> None: app = _build_app() app.state.pool = _make_mock_pool() resp, _ = _get_analytics(app) data = resp.json()["data"] assert "total_conversations" in data assert "resolution_rate" in data assert "escalation_rate" in data assert "avg_turns_per_conversation" in data assert "avg_cost_per_conversation_usd" in data assert "agent_usage" in data assert "interrupt_stats" in data def test_custom_range_7d(self) -> None: app = _build_app() app.state.pool = _make_mock_pool() resp, mock_ga = _get_analytics(app, "/api/analytics?range=7d") assert resp.status_code == 200 mock_ga.assert_called_once() call_kwargs = mock_ga.call_args assert call_kwargs[1]["range_days"] == 7 or call_kwargs[0][1] == 7 def test_custom_range_30d(self) -> None: app = _build_app() app.state.pool = _make_mock_pool() resp, mock_ga = _get_analytics(app, "/api/analytics?range=30d") assert resp.status_code == 200 call_kwargs = mock_ga.call_args assert call_kwargs[1].get("range_days") == 30 or ( len(call_kwargs[0]) > 1 and call_kwargs[0][1] == 30 ) def test_invalid_range_format_returns_400(self) -> None: app = _build_app() app.state.pool = _make_mock_pool() with TestClient(app) as client: resp = client.get("/api/analytics?range=invalid") assert resp.status_code == 400 def test_range_without_d_suffix_returns_400(self) -> None: app = _build_app() app.state.pool = _make_mock_pool() with TestClient(app) as client: resp = client.get("/api/analytics?range=7") assert resp.status_code == 400 def test_agent_usage_in_response(self) -> None: app = _build_app() app.state.pool = _make_mock_pool() resp, _ = _get_analytics(app) data = resp.json()["data"] assert len(data["agent_usage"]) == 1 assert data["agent_usage"][0]["agent"] == "order_agent" def test_interrupt_stats_in_response(self) -> None: app = _build_app() app.state.pool = _make_mock_pool() resp, _ = _get_analytics(app) data = resp.json()["data"] assert data["interrupt_stats"]["total"] == 5 assert data["interrupt_stats"]["approved"] == 4 def test_envelope_format(self) -> None: app = _build_app() app.state.pool = _make_mock_pool() resp, _ = _get_analytics(app) body = resp.json() assert "success" in body assert "data" in body assert "error" in body