"""Tests for analytics endpoints.""" import pytest from unittest.mock import MagicMock, patch AUTH = {"Authorization": "Bearer test-token"} def _make_sb_with_plan(plan: str): sb = MagicMock() m = MagicMock() m.select.return_value = m m.insert.return_value = m m.update.return_value = m m.delete.return_value = m m.eq.return_value = m m.in_.return_value = m m.limit.return_value = m m.order.return_value = m m.range.return_value = m m.gte.return_value = m m.lt.return_value = m m.execute.return_value = MagicMock(data=[{"plan": plan}], count=0) sb.table.return_value = m sb.auth = MagicMock() return sb def _make_starter_sb(company_data=None, chatbot_data=None): """Starter plan supabase mock with configurable data.""" sb = MagicMock() def table_side_effect(name): m = MagicMock() m.select.return_value = m m.insert.return_value = m m.update.return_value = m m.delete.return_value = m m.eq.return_value = m m.in_.return_value = m m.limit.return_value = m m.order.return_value = m m.range.return_value = m m.gte.return_value = m m.lt.return_value = m if name == "subscriptions": m.execute.return_value = MagicMock(data=[{"plan": "starter"}], count=0) elif name == "companies": m.execute.return_value = MagicMock( data=company_data if company_data is not None else [{"id": "company-1"}], count=0, ) elif name == "chatbots": m.execute.return_value = MagicMock( data=chatbot_data if chatbot_data is not None else [], count=0, ) elif name == "conversations": m.execute.return_value = MagicMock(data=[], count=0) elif name == "messages": m.execute.return_value = MagicMock(data=[], count=0) elif name == "message_feedback": m.execute.return_value = MagicMock(data=[], count=0) else: m.execute.return_value = MagicMock(data=[], count=0) return m sb.table.side_effect = table_side_effect sb.auth = MagicMock() return sb class TestAnalyticsAuth: def test_overview_requires_auth(self, client): resp = client.get("/api/v1/analytics/overview") assert resp.status_code == 401 def test_chatbot_detail_requires_auth(self, client): resp = client.get("/api/v1/analytics/chatbot/some-id") assert resp.status_code == 401 def test_gaps_requires_auth(self, client): resp = client.get("/api/v1/analytics/chatbot/some-id/gaps") assert resp.status_code == 401 class TestAnalyticsPlanGating: def test_free_plan_blocked_on_overview(self, client): with patch("app.routers.analytics.get_supabase") as mock_sb: mock_sb.return_value = _make_sb_with_plan("free") resp = client.get("/api/v1/analytics/overview", headers=AUTH) assert resp.status_code == 402 assert "Starter" in resp.json()["detail"] def test_free_plan_blocked_on_chatbot_detail(self, client): with patch("app.routers.analytics.get_supabase") as mock_sb: mock_sb.return_value = _make_sb_with_plan("free") resp = client.get("/api/v1/analytics/chatbot/some-id", headers=AUTH) assert resp.status_code == 402 def test_free_plan_blocked_on_gaps(self, client): with patch("app.routers.analytics.get_supabase") as mock_sb: mock_sb.return_value = _make_sb_with_plan("free") resp = client.get("/api/v1/analytics/chatbot/some-id/gaps", headers=AUTH) assert resp.status_code == 402 class TestAnalyticsOverview: def test_overview_no_company_returns_404(self, client): with patch("app.routers.analytics.get_supabase") as mock_sb: mock_sb.return_value = _make_starter_sb(company_data=[]) resp = client.get("/api/v1/analytics/overview", headers=AUTH) assert resp.status_code == 404 def test_overview_no_chatbots_returns_empty(self, client): with patch("app.routers.analytics.get_supabase") as mock_sb: mock_sb.return_value = _make_starter_sb(chatbot_data=[]) resp = client.get("/api/v1/analytics/overview", headers=AUTH) assert resp.status_code == 200 body = resp.json() assert body["total_chatbots"] == 0 assert body["total_conversations"] == 0 assert body["chatbots"] == [] assert body["plan"] == "starter" def test_overview_with_chatbots(self, client): chatbot_data = [{"id": "cb-1", "name": "Bot One", "is_published": True, "average_rating": 4.5}] with patch("app.routers.analytics.get_supabase") as mock_sb: mock_sb.return_value = _make_starter_sb(chatbot_data=chatbot_data) resp = client.get("/api/v1/analytics/overview", headers=AUTH) assert resp.status_code == 200 body = resp.json() assert body["total_chatbots"] == 1 assert body["published_chatbots"] == 1 assert len(body["chatbots"]) == 1 assert body["chatbots"][0]["chatbot_name"] == "Bot One" class TestAnalyticsChatbotDetail: def test_chatbot_not_found_returns_404(self, client): def table_side(name): m = MagicMock() m.select.return_value = m m.eq.return_value = m m.in_.return_value = m m.limit.return_value = m m.order.return_value = m m.lt.return_value = m if name == "subscriptions": m.execute.return_value = MagicMock(data=[{"plan": "starter"}]) elif name == "companies": m.execute.return_value = MagicMock(data=[{"id": "comp-1"}]) elif name == "chatbots": m.execute.return_value = MagicMock(data=[]) else: m.execute.return_value = MagicMock(data=[], count=0) return m sb = MagicMock() sb.table.side_effect = table_side sb.auth = MagicMock() with patch("app.routers.analytics.get_supabase", return_value=sb): resp = client.get("/api/v1/analytics/chatbot/nonexistent-id", headers=AUTH) assert resp.status_code == 404 def test_chatbot_detail_success(self, client): def table_side(name): m = MagicMock() m.select.return_value = m m.eq.return_value = m m.in_.return_value = m m.limit.return_value = m m.order.return_value = m m.lt.return_value = m if name == "subscriptions": m.execute.return_value = MagicMock(data=[{"plan": "starter"}]) elif name == "companies": m.execute.return_value = MagicMock(data=[{"id": "comp-1"}]) elif name == "chatbots": m.execute.return_value = MagicMock(data=[{"id": "cb-1", "name": "Bot", "average_rating": 4.0}]) elif name == "conversations": m.execute.return_value = MagicMock(data=[], count=0) elif name == "messages": m.execute.return_value = MagicMock(data=[], count=0) elif name == "message_feedback": m.execute.return_value = MagicMock(data=[], count=0) else: m.execute.return_value = MagicMock(data=[], count=0) return m sb = MagicMock() sb.table.side_effect = table_side sb.auth = MagicMock() with patch("app.routers.analytics.get_supabase", return_value=sb): resp = client.get("/api/v1/analytics/chatbot/cb-1", headers=AUTH) assert resp.status_code == 200 body = resp.json() assert body["chatbot_name"] == "Bot" class TestAnalyticsGaps: def test_gaps_no_company_returns_404(self, client): def table_side(name): m = MagicMock() m.select.return_value = m m.eq.return_value = m m.in_.return_value = m m.limit.return_value = m m.order.return_value = m m.lt.return_value = m if name == "subscriptions": m.execute.return_value = MagicMock(data=[{"plan": "starter"}]) else: m.execute.return_value = MagicMock(data=[]) return m sb = MagicMock() sb.table.side_effect = table_side sb.auth = MagicMock() with patch("app.routers.analytics.get_supabase", return_value=sb): resp = client.get("/api/v1/analytics/chatbot/some-id/gaps", headers=AUTH) assert resp.status_code == 404 def test_gaps_no_conversations_returns_empty(self, client): def table_side(name): m = MagicMock() m.select.return_value = m m.eq.return_value = m m.in_.return_value = m m.limit.return_value = m m.order.return_value = m m.lt.return_value = m if name == "subscriptions": m.execute.return_value = MagicMock(data=[{"plan": "starter"}]) elif name == "companies": m.execute.return_value = MagicMock(data=[{"id": "comp-1"}]) elif name == "chatbots": m.execute.return_value = MagicMock(data=[{"id": "cb-1"}]) else: m.execute.return_value = MagicMock(data=[], count=0) return m sb = MagicMock() sb.table.side_effect = table_side sb.auth = MagicMock() with patch("app.routers.analytics.get_supabase", return_value=sb): resp = client.get("/api/v1/analytics/chatbot/cb-1/gaps", headers=AUTH) assert resp.status_code == 200 assert resp.json() == []