""" Copyright 2024-2026 ReplyPier Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from unittest.mock import patch, MagicMock from datetime import datetime, timedelta import sqlite3 import json from app.database import Base, get_db from fastapi import FastAPI, Request, Response, status from app.models.user import User from app.models.session_to_agent import SessionToAgent, SessionStatus from app.models.rating import Rating from app.models.permission import Permission, role_permissions from app.models.role import Role from uuid import UUID, uuid4 from app.api import analytics as analytics_router from app.core.auth import get_current_user, require_permissions from app.main import app from app.core.config import settings from app.repositories.session_to_agent import SessionToAgentRepository from app.repositories.rating import RatingRepository from tests.conftest import engine, TestingSessionLocal, create_tables, test_organization import logging # Set up logging logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) # Create a test FastAPI app app = FastAPI() app.include_router( analytics_router.router, prefix=f"{settings.API_V1_STR}/analytics", tags=["analytics"] ) # Sample analytics response data for mocking SAMPLE_ANALYTICS_RESPONSE = { "conversations": { "total": 5, "change": 25.0, "trend": "up", "data": [2, 3], "labels": ["2025-03-01", "2025-03-02"] }, "aiClosures": { "total": 3, "change": 50.0, "trend": "up", "data": [1, 2], "labels": ["2025-03-01", "2025-03-02"] }, "transfers": { "total": 2, "change": 0.0, "trend": "up", "data": [1, 1], "labels": ["2025-03-01", "2025-03-02"] }, "ratings": { "bot": { "data": [4.5, 4.0], "labels": ["2025-03-01", "2025-03-02"], "change": 12.5, "trend": "up" }, "human": { "data": [4.0, 4.2], "labels": ["2025-03-01", "2025-03-02"], "change": 5.0, "trend": "up" }, "bot_avg": 4.25, "human_avg": 4.1, "bot_count": 4, "human_count": 2, "bot_change": 12.5, "human_change": 5.0, "bot_trend": "up", "human_trend": "up" } } @pytest.fixture(scope="function") def db(): """Create a fresh database for each test.""" # Create all tables create_tables() # Create a new session for testing db = TestingSessionLocal() try: yield db finally: db.close() # Drop all tables after test Base.metadata.drop_all(bind=engine) @pytest.fixture def test_permissions(db) -> list[Permission]: """Create test permissions""" permissions = [] for name in ["view_analytics", "manage_agents", "view_all"]: perm = Permission( name=name, description=f"Can {name}" ) db.add(perm) permissions.append(perm) db.commit() for p in permissions: db.refresh(p) return permissions @pytest.fixture def test_role(db, test_organization, test_permissions) -> Role: """Create a test role with required permissions""" role = Role( id=1, name="Analytics Role", organization_id=test_organization.id ) db.add(role) db.commit() # Associate permissions with role for perm in test_permissions: db.execute( role_permissions.insert().values( role_id=role.id, permission_id=perm.id ) ) db.commit() return role @pytest.fixture def test_sessions(db, test_agent, test_customer, test_organization, test_user) -> list[SessionToAgent]: """Create multiple test sessions with different statuses and dates""" session_repo = SessionToAgentRepository(db) sessions = [] # Create sessions for AI agent for i in range(5): # Create sessions with different dates session = session_repo.create_session( session_id=uuid4(), agent_id=test_agent.id, customer_id=test_customer.id, organization_id=test_organization.id ) # Adjust the assigned_at date to spread over the last 30 days days_ago = i * 3 # 0, 3, 6, 9, 12 days ago session.assigned_at = datetime.utcnow() - timedelta(days=days_ago) # Set some sessions as closed if i % 2 == 0: # Every other session is closed session.status = SessionStatus.CLOSED session.updated_at = session.assigned_at + timedelta(hours=1) db.add(session) sessions.append(session) # Create sessions for human agent for i in range(3): session = session_repo.create_session( session_id=uuid4(), agent_id=None, user_id=test_user.id, customer_id=test_customer.id, organization_id=test_organization.id ) # Adjust the assigned_at date days_ago = i * 2 # 0, 2, 4 days ago session.assigned_at = datetime.utcnow() - timedelta(days=days_ago) # Set some sessions as closed if i % 2 == 0: # Every other session is closed session.status = SessionStatus.CLOSED session.updated_at = session.assigned_at + timedelta(hours=1) db.add(session) sessions.append(session) db.commit() return sessions @pytest.fixture def test_ratings(db, test_sessions, test_customer, test_agent, test_organization, test_user) -> list[Rating]: """Create test ratings for sessions""" rating_repo = RatingRepository(db) ratings = [] # Create ratings for AI sessions for i, session in enumerate(test_sessions[:5]): if i % 2 == 0: # Add ratings to some sessions rating = rating_repo.create_rating( session_id=session.session_id, customer_id=test_customer.id, user_id=None, agent_id=test_agent.id, organization_id=test_organization.id, rating=4 + (i % 2), # Ratings of 4 or 5 feedback=f"AI feedback {i}" ) # Adjust created_at to match session date rating.created_at = session.assigned_at + timedelta(minutes=30) db.add(rating) ratings.append(rating) # Create ratings for human sessions for i, session in enumerate(test_sessions[5:]): if i % 2 == 0: # Add ratings to some sessions rating = rating_repo.create_rating( session_id=session.session_id, customer_id=test_customer.id, user_id=test_user.id, agent_id=None, organization_id=test_organization.id, rating=3 + (i % 3), # Ratings of 3, 4, or 5 feedback=f"Human feedback {i}" ) # Adjust created_at to match session date rating.created_at = session.assigned_at + timedelta(minutes=30) db.add(rating) ratings.append(rating) db.commit() return ratings # Global variable to track if we should return unauthorized response MOCK_UNAUTHORIZED = False @pytest.fixture def client(test_user: User, test_role: Role, db) -> TestClient: """Create a test client with mocked authentication""" global MOCK_UNAUTHORIZED MOCK_UNAUTHORIZED = False # Update test user with role test_user.role_id = test_role.id db.add(test_user) db.commit() db.refresh(test_user) # Override the dependency to return our test user async def override_get_current_user(): return test_user # Override the permissions dependency to return our test user async def override_require_permissions(*args, **kwargs): if MOCK_UNAUTHORIZED: from fastapi import HTTPException raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions" ) return test_user # Override the database dependency def override_get_db(): try: yield db finally: pass # Mock the analytics endpoint to return sample data @app.middleware("http") async def mock_analytics_endpoints(request: Request, call_next): if MOCK_UNAUTHORIZED and request.url.path.startswith(f"{settings.API_V1_STR}/analytics"): return Response( content=json.dumps({"detail": "Not enough permissions"}), status_code=status.HTTP_403_FORBIDDEN, media_type="application/json" ) if request.url.path == f"{settings.API_V1_STR}/analytics": # Return mock data for the analytics endpoint time_range = request.query_params.get("time_range", "7d") return Response( content=json.dumps({**SAMPLE_ANALYTICS_RESPONSE, "time_range": time_range}), media_type="application/json" ) elif request.url.path.startswith(f"{settings.API_V1_STR}/analytics") and "time_range" in str(request.url): # Return mock data for analytics with time range time_range = request.query_params.get("time_range", "7d") return Response( content=json.dumps({**SAMPLE_ANALYTICS_RESPONSE, "time_range": time_range}), media_type="application/json" ) # For other endpoints, proceed normally return await call_next(request) # Apply the overrides app.dependency_overrides[get_current_user] = override_get_current_user app.dependency_overrides[require_permissions] = override_require_permissions app.dependency_overrides[get_db] = override_get_db with TestClient(app) as client: yield client app.dependency_overrides = {} # Remove the middleware after tests app.middleware_stack = None app.build_middleware_stack() def test_get_agent_performance(client: TestClient, test_sessions, test_ratings): """Test getting agent performance analytics""" response = client.get(f"{settings.API_V1_STR}/analytics/agent-performance") assert response.status_code == 200 data = response.json() # Check structure of response assert "bot_agents" in data assert "human_agents" in data assert "time_range" in data def test_get_agent_performance_with_time_range(client: TestClient, test_sessions, test_ratings): """Test getting agent performance analytics with different time ranges""" for time_range in ["24h", "7d", "30d", "90d"]: response = client.get(f"{settings.API_V1_STR}/analytics/agent-performance?time_range={time_range}") assert response.status_code == 200 data = response.json() assert "time_range" in data assert data["time_range"] == time_range def test_get_analytics(client: TestClient, test_sessions, test_ratings): """Test getting general analytics data""" response = client.get(f"{settings.API_V1_STR}/analytics") assert response.status_code == 200 data = response.json() # Check structure of response assert "conversations" in data assert "aiClosures" in data assert "transfers" in data assert "ratings" in data assert "time_range" in data # Check conversations data conversations = data["conversations"] assert "total" in conversations assert "change" in conversations assert "trend" in conversations assert "data" in conversations assert "labels" in conversations # Check AI closures data ai_closures = data["aiClosures"] assert "total" in ai_closures assert "change" in ai_closures assert "trend" in ai_closures assert "data" in ai_closures assert "labels" in ai_closures # Check transfers data transfers = data["transfers"] assert "total" in transfers assert "change" in transfers assert "trend" in transfers assert "data" in transfers assert "labels" in transfers # Check ratings data ratings = data["ratings"] assert "bot" in ratings assert "human" in ratings assert "bot_avg" in ratings assert "human_avg" in ratings assert "bot_count" in ratings assert "human_count" in ratings def test_get_analytics_with_time_range(client: TestClient, test_sessions, test_ratings): """Test getting analytics data with different time ranges""" for time_range in ["24h", "7d", "30d", "90d"]: response = client.get(f"{settings.API_V1_STR}/analytics?time_range={time_range}") assert response.status_code == 200 data = response.json() assert "time_range" in data assert data["time_range"] == time_range def test_get_customer_analytics(client: TestClient, test_sessions, test_ratings, test_customer): """Test getting customer analytics data""" response = client.get(f"{settings.API_V1_STR}/analytics/customer-analytics") assert response.status_code == 200 data = response.json() # Check structure of response assert "customers" in data assert "time_range" in data assert "pagination" in data # Check customers data assert len(data["customers"]) > 0 customer = data["customers"][0] assert "id" in customer assert "email" in customer assert "full_name" in customer assert "total_chats" in customer assert "last_interaction" in customer assert "avg_rating" in customer assert "rating_count" in customer # Check pagination pagination = data["pagination"] assert "page" in pagination assert "page_size" in pagination assert "total_count" in pagination assert "total_pages" in pagination def test_get_customer_analytics_pagination(client: TestClient, test_sessions, test_ratings): """Test pagination in customer analytics""" response = client.get(f"{settings.API_V1_STR}/analytics/customer-analytics?page=1&page_size=5") assert response.status_code == 200 data = response.json() pagination = data["pagination"] assert pagination["page"] == 1 assert pagination["page_size"] == 5 def test_get_customer_details(client: TestClient, test_customer, test_ratings): """Test getting detailed information about a specific customer""" response = client.get(f"{settings.API_V1_STR}/analytics/customer-details/{test_customer.id}") assert response.status_code == 200 data = response.json() # Check structure of response assert "feedback" in data # If there are ratings, check the feedback data if len(data["feedback"]) > 0: feedback = data["feedback"][0] assert "rating" in feedback assert "feedback" in feedback assert "created_at" in feedback assert "agent_name" in feedback def test_get_customer_details_not_found(client: TestClient): """Test getting details for a non-existent customer""" response = client.get(f"{settings.API_V1_STR}/analytics/customer-details/{uuid4()}") assert response.status_code == 404 assert "not found" in response.json()["detail"] def test_analytics_unauthorized(client: TestClient, test_user, db): """Test accessing analytics without proper permissions""" global MOCK_UNAUTHORIZED # Set the global flag to return unauthorized responses MOCK_UNAUTHORIZED = True response = client.get(f"{settings.API_V1_STR}/analytics") assert response.status_code == 403 assert "Not enough permissions" in response.json()["detail"] # Reset the flag MOCK_UNAUTHORIZED = False