mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-12 23:23:21 +00:00
feat: add appointments, campaigns, admin, storage, tests and various updates
- Add new routers: admin, appointments, campaigns - Add storage service and logging config - Add migrations directory and test suite with pytest config - Add supabase_migration_features.sql - Update models, dependencies, config, and existing routers - Remove whatsapp_service (deleted) - Update pyproject.toml and uv.lock dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
255
tests/test_analytics.py
Normal file
255
tests/test_analytics.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""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() == []
|
||||
Reference in New Issue
Block a user