test: 补充权限、消息与会话服务回归用例
会话读取、接管、重分配与 Socket 订阅统一校验角色、组织与可见性。
This commit is contained in:
1 parent
8e3a4d95da
commit
164f5f9615
97 files changed
+37281
No files matched your search
@@ -0,0 +1,488 @@
|
||||
"""
|
||||
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
|
||||
Reference in new issue
Block a user