Files
replypier/backend/tests/api/test_analytics.py
T
WangShen007 2554eb7bc7 test: 补充权限、消息与会话服务回归用例
会话读取、接管、重分配与 Socket 订阅统一校验角色、组织与可见性。
2026-10-08 22:35:00 +08:00

488 lines
16 KiB
Python

"""
Copyright 2024-2026 ChatterMate
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