""" 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 fastapi import FastAPI from app.models.user import User, UserGroup from app.models.chat_history import ChatHistory from app.models.session_to_agent import SessionToAgent, SessionStatus from app.models.agent import Agent from app.models.role import Role from app.models.customer import Customer from app.models.permission import Permission, role_permissions from uuid import uuid4 from datetime import datetime, timezone from app.api import chat as chat_router from app.core.auth import get_current_user, require_permissions, get_unified_chat_auth from app.database import get_db from app.models.organization import Organization # Create a test FastAPI app app = FastAPI() app.include_router( chat_router.router, prefix="/api/chats", tags=["chat"] ) @pytest.fixture def test_permissions(db) -> list[Permission]: """Create test permissions""" permissions = [] for name in ["view_all_chats", "view_assigned_chats"]: perm = Permission( name=name, description=f"Test permission for {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_id, test_permissions) -> Role: """Create a test role with required permissions""" role = Role( name="Test Role", description="Test Role Description", organization_id=test_organization_id, is_default=True ) 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() db.refresh(role) return role @pytest.fixture def test_user(db, test_organization_id, test_role) -> User: """Create a test user with required permissions""" user = User( id=uuid4(), email="test@example.com", hashed_password="hashed_password", is_active=True, organization_id=test_organization_id, full_name="Test User", role_id=test_role.id ) db.add(user) db.commit() db.refresh(user) return user @pytest.fixture def test_group(db, test_user) -> UserGroup: """Create a test user group""" group = UserGroup( id=uuid4(), name="Test Group", organization_id=test_user.organization_id ) db.add(group) db.commit() db.refresh(group) return group @pytest.fixture def test_agent(db, test_user) -> Agent: """Create a test agent""" agent = Agent( id=uuid4(), name="Test Agent", display_name="Test Display Name", description="Test Description", agent_type="CUSTOMER_SUPPORT", instructions=["Test instruction"], is_active=True, organization_id=test_user.organization_id ) db.add(agent) db.commit() db.refresh(agent) return agent @pytest.fixture def test_customer(db, test_user) -> Customer: """Create a test customer""" customer = Customer( id=uuid4(), email="customer@example.com", full_name="Test Customer", organization_id=test_user.organization_id, is_active=True ) db.add(customer) db.commit() db.refresh(customer) return customer @pytest.fixture def test_chat_session(db, test_user, test_agent, test_customer) -> SessionToAgent: """Create a test chat session""" session = SessionToAgent( session_id=uuid4(), user_id=test_user.id, agent_id=test_agent.id, customer_id=test_customer.id, organization_id=test_user.organization_id, status=SessionStatus.OPEN, assigned_at=datetime.now(timezone.utc) ) db.add(session) db.commit() db.refresh(session) return session @pytest.fixture def test_chat_messages(db, test_chat_session, test_user, test_customer) -> list[ChatHistory]: """Create test chat messages""" messages = [] for i in range(3): message = ChatHistory( organization_id=test_user.organization_id, user_id=test_user.id, customer_id=test_customer.id, agent_id=test_chat_session.agent_id, session_id=test_chat_session.session_id, message=f"Test message {i+1}", message_type="user" if i % 2 == 0 else "bot", created_at=datetime.now(timezone.utc) ) messages.append(message) db.add(message) db.commit() for m in messages: db.refresh(m) return messages @pytest.fixture def client(db, test_user) -> TestClient: """Create test client with mocked dependencies""" async def override_get_current_user(): return test_user async def override_require_permissions(*args, **kwargs): return test_user async def override_get_unified_chat_auth(): # Refresh the user to get the latest role and permissions from the database db.refresh(test_user) db.refresh(test_user.role) # Get user permissions for chat auth user_permissions = {p.name for p in test_user.role.permissions} can_view_all = "view_all_chats" in user_permissions can_view_assigned = "view_assigned_chats" in user_permissions # Check if user has no permissions if not (can_view_all or can_view_assigned): from fastapi import HTTPException raise HTTPException( status_code=403, detail="Not enough permissions" ) return { "auth_type": "jwt", "organization_id": test_user.organization_id, # Keep as UUID "user_id": test_user.id, "current_user": test_user, "can_view_all": can_view_all, "can_view_assigned": can_view_assigned } def override_get_db(): try: yield db finally: pass app.dependency_overrides[get_current_user] = override_get_current_user app.dependency_overrides[require_permissions] = override_require_permissions app.dependency_overrides[get_unified_chat_auth] = override_get_unified_chat_auth app.dependency_overrides[get_db] = override_get_db return TestClient(app) # Test cases def test_get_recent_chats_with_view_all( client, db, test_user, test_chat_session, test_chat_messages ): """Test getting recent chats with view_all permission""" response = client.get("/api/chats/recent") assert response.status_code == 200 chats = response.json() assert len(chats) == 1 assert chats[0]["session_id"] == str(test_chat_session.session_id) assert chats[0]["agent"]["id"] == str(test_chat_session.agent_id) @pytest.mark.parametrize("status", ["null", "open,bogus", "OPEN"]) def test_get_recent_chats_refuses_an_unknown_status(client, test_user, status): """Compared with the status column, an unknown value was a database error (500).""" response = client.get("/api/chats/recent", params={"status": status}) assert response.status_code == 422 assert response.json()["detail"].startswith("Unknown status") def test_get_recent_chats_refuses_an_offset_past_the_database_range(client, test_user): """Past a 64-bit OFFSET the query itself failed (500).""" assert client.get("/api/chats/recent", params={"skip": 15412059487694436352}).status_code == 422 @pytest.mark.parametrize("status", ["open", "open, transferred", "all"]) def test_get_recent_chats_accepts_known_statuses(client, test_user, test_chat_session, status): assert client.get("/api/chats/recent", params={"status": status}).status_code == 200 def test_get_recent_chats_includes_human_handler( client, db, test_user, test_chat_session, test_chat_messages ): """A chat a human holds reports that human, so the inbox can label the row.""" response = client.get("/api/chats/recent") assert response.status_code == 200 chats = response.json() # Three messages must still collapse into one row: the handler columns are # functionally dependent on the session, so grouping cannot multiply rows. assert len(chats) == 1 assert chats[0]["user_id"] == str(test_user.id) assert chats[0]["user_name"] == test_user.full_name def test_get_recent_chats_ai_handled_has_no_handler( client, db, test_user, test_chat_session, test_chat_messages ): """No assignee means the AI still has it — both handler fields stay null.""" test_chat_session.user_id = None db.commit() response = client.get("/api/chats/recent") assert response.status_code == 200 chats = response.json() assert len(chats) == 1 assert chats[0]["user_id"] is None assert chats[0]["user_name"] is None def test_get_recent_chats_reports_a_human_only_agent( client, db, test_user, test_agent, test_chat_session, test_chat_messages ): """The row needs the agent's flag to say "waiting" rather than "AI".""" response = client.get("/api/chats/recent") assert response.status_code == 200 assert response.json()[0]["agent"]["ai_replies_enabled"] is True test_agent.ai_replies_enabled = False db.commit() response = client.get("/api/chats/recent") assert response.status_code == 200 chats = response.json() assert len(chats) == 1 assert chats[0]["agent"]["ai_replies_enabled"] is False def test_get_recent_chats_with_view_assigned( client, db, test_user, test_chat_session, test_chat_messages ): """Test getting recent chats with view_assigned permission""" # Remove view_all permission view_all_perm = db.query(Permission).filter_by(name="view_all_chats").first() db.execute( role_permissions.delete().where( role_permissions.c.permission_id == view_all_perm.id ) ) db.commit() response = client.get("/api/chats/recent") assert response.status_code == 200 chats = response.json() assert len(chats) == 1 # Should only see own chat assert chats[0]["session_id"] == str(test_chat_session.session_id) def test_get_recent_chats_no_permission( client, db, test_user, test_chat_session ): """Test getting recent chats without required permissions""" # Remove all chat permissions db.execute(role_permissions.delete()) db.commit() response = client.get("/api/chats/recent") assert response.status_code == 403 assert response.json()["detail"] == "Not enough permissions" def test_get_chat_detail_success( client, db, test_user, test_chat_session, test_chat_messages ): """Test getting chat detail successfully""" response = client.get(f"/api/chats/{test_chat_session.session_id}") assert response.status_code == 200 chat = response.json() assert chat["session_id"] == str(test_chat_session.session_id) assert len(chat["messages"]) == 3 assert chat["agent"]["id"] == str(test_chat_session.agent_id) def test_get_chat_detail_not_found( client, db, test_user ): """Test getting non-existent chat detail""" response = client.get(f"/api/chats/{uuid4()}") assert response.status_code == 404 assert response.json()["detail"] == "Chat session not found" def test_get_chat_detail_wrong_org( client, db, test_user, test_chat_session ): """Test getting chat detail from wrong organization""" # Create a new organization first new_org = Organization( id=uuid4(), name="Test Org 2", domain="test2.com", timezone="UTC" ) db.add(new_org) db.commit() # Change session's organization test_chat_session.organization_id = new_org.id db.commit() response = client.get(f"/api/chats/{test_chat_session.session_id}") assert response.status_code == 404 assert response.json()["detail"] == "Chat session not found" def test_get_chat_detail_no_permission( client, db, test_user, test_chat_session ): """Test getting chat detail without required permissions""" # Remove all chat permissions db.execute(role_permissions.delete()) db.commit() response = client.get(f"/api/chats/{test_chat_session.session_id}") assert response.status_code == 403 assert response.json()["detail"] == "Not enough permissions" def test_get_chat_history_endpoint(client): """Test the basic chat history endpoint""" response = client.get("/api/chats/") assert response.status_code == 200 assert response.json() == {"message": "Chat history endpoint"} def test_get_recent_chats_with_filters( client, db, test_user, test_agent, test_customer ): """Test getting recent chats with various filters""" # Create multiple sessions with different statuses open_session = SessionToAgent( session_id=uuid4(), user_id=test_user.id, agent_id=test_agent.id, customer_id=test_customer.id, organization_id=test_user.organization_id, status=SessionStatus.OPEN, assigned_at=datetime.now(timezone.utc) ) db.add(open_session) closed_session = SessionToAgent( session_id=uuid4(), user_id=test_user.id, agent_id=test_agent.id, customer_id=test_customer.id, organization_id=test_user.organization_id, status=SessionStatus.CLOSED, assigned_at=datetime.now(timezone.utc) ) db.add(closed_session) db.commit() # Test with status filter response = client.get("/api/chats/recent?status=open") assert response.status_code == 200 chats = response.json() assert all(chat["status"] == "OPEN" for chat in chats) # Test with agent_id filter response = client.get(f"/api/chats/recent?agent_id={test_agent.id}") assert response.status_code == 200 chats = response.json() assert all(chat["agent"]["id"] == str(test_agent.id) for chat in chats) # Test with limit and skip response = client.get("/api/chats/recent?limit=1&skip=0") assert response.status_code == 200 chats = response.json() assert len(chats) <= 1 def test_get_recent_chats_invalid_uuid(client, db, test_user): """Test getting recent chats with invalid UUID format""" response = client.get("/api/chats/recent?user_id=invalid-uuid") assert response.status_code == 400 assert "Invalid UUID format" in response.json()["detail"] def test_get_chat_detail_invalid_uuid(client, db, test_user): """Test getting chat detail with invalid session_id format""" response = client.get("/api/chats/invalid-uuid") assert response.status_code == 400 assert "Invalid UUID format" in response.json()["detail"] def test_get_chat_detail_with_shopify_output( client, db, test_user, test_chat_session, test_customer ): """Test getting chat detail with Shopify output in attributes""" import json # Create message with shopify_output in attributes shopify_data = { "products": [{"id": "123", "title": "Test Product"}], "search_query": "test" } message = ChatHistory( organization_id=test_user.organization_id, user_id=test_user.id, customer_id=test_customer.id, agent_id=test_chat_session.agent_id, session_id=test_chat_session.session_id, message="Here are some products", message_type="bot", attributes={ "shopify_output": shopify_data, "end_chat": False }, created_at=datetime.now(timezone.utc) ) db.add(message) db.commit() response = client.get(f"/api/chats/{test_chat_session.session_id}") assert response.status_code == 200 chat = response.json() # Check that shopify_output was processed messages = chat["messages"] shopify_message = next((m for m in messages if m.get("shopify_output")), None) assert shopify_message is not None assert shopify_message["message_type"] == "product" def test_get_chat_detail_with_end_chat_reason( client, db, test_user, test_chat_session, test_customer ): """Test getting chat detail with end_chat_reason in attributes""" # Create message with valid end_chat_reason message = ChatHistory( organization_id=test_user.organization_id, user_id=test_user.id, customer_id=test_customer.id, agent_id=test_chat_session.agent_id, session_id=test_chat_session.session_id, message="Thank you!", message_type="bot", attributes={ "end_chat": True, "end_chat_reason": "ISSUE_RESOLVED", # Valid enum value "end_chat_description": "Issue resolved" }, created_at=datetime.now(timezone.utc) ) db.add(message) db.commit() response = client.get(f"/api/chats/{test_chat_session.session_id}") assert response.status_code == 200 chat = response.json() messages = chat["messages"] end_chat_message = next((m for m in messages if m.get("end_chat")), None) assert end_chat_message is not None assert end_chat_message["end_chat_reason"] == "ISSUE_RESOLVED" assert end_chat_message["end_chat_description"] == "Issue resolved" def test_get_chat_detail_with_invalid_end_chat_reason( client, db, test_user, test_chat_session, test_customer ): """Test getting chat detail with invalid end_chat_reason (should be set to None)""" # Create message with invalid end_chat_reason message = ChatHistory( organization_id=test_user.organization_id, user_id=test_user.id, customer_id=test_customer.id, agent_id=test_chat_session.agent_id, session_id=test_chat_session.session_id, message="Thank you!", message_type="bot", attributes={ "end_chat": True, "end_chat_reason": "INVALID_REASON", "end_chat_description": "Issue resolved" }, created_at=datetime.now(timezone.utc) ) db.add(message) db.commit() response = client.get(f"/api/chats/{test_chat_session.session_id}") assert response.status_code == 200 chat = response.json() messages = chat["messages"] end_chat_message = next((m for m in messages if m.get("end_chat")), None) assert end_chat_message is not None # Invalid reason should be set to None assert end_chat_message["end_chat_reason"] is None def test_get_chat_detail_assigned_access_denied( client, db, test_user, test_customer, test_agent ): """Test that user with view_assigned permission cannot access unassigned chats""" # Remove view_all permission view_all_perm = db.query(Permission).filter_by(name="view_all_chats").first() db.execute( role_permissions.delete().where( role_permissions.c.permission_id == view_all_perm.id ) ) db.commit() # Create a session not assigned to test_user other_user = User( id=uuid4(), email="other@example.com", hashed_password="hashed", is_active=True, organization_id=test_user.organization_id, full_name="Other User", role_id=test_user.role_id ) db.add(other_user) db.commit() other_session = SessionToAgent( session_id=uuid4(), user_id=other_user.id, agent_id=test_agent.id, customer_id=test_customer.id, organization_id=test_user.organization_id, status=SessionStatus.OPEN, assigned_at=datetime.now(timezone.utc) ) db.add(other_session) db.commit() response = client.get(f"/api/chats/{other_session.session_id}") assert response.status_code == 404 assert response.json()["detail"] == "Chat session not found" def test_get_recent_chats_with_user_name_filter( client, db, test_user, test_chat_session ): """Test getting recent chats with user_name filter""" # Test with user_name filter response = client.get(f"/api/chats/recent?user_name={test_user.full_name}") assert response.status_code == 200 # Filter parameters are passed to repository, endpoint should handle it assert isinstance(response.json(), list) def test_get_recent_chats_shopify_endpoint(client, db, test_user, test_chat_session): """Test the Shopify-specific endpoint""" response = client.get("/api/chats/recent/shopify") assert response.status_code == 200 chats = response.json() assert isinstance(chats, list) def test_get_chat_detail_shopify_endpoint(client, db, test_user, test_chat_session, test_chat_messages): """Test the Shopify-specific chat detail endpoint""" response = client.get(f"/api/chats/{test_chat_session.session_id}/shopify") assert response.status_code == 200 chat = response.json() assert chat["session_id"] == str(test_chat_session.session_id)