""" Tests for chat route exception handling. These tests verify that all route endpoints handle exceptions gracefully and return proper 500 error responses with appropriate error messages. """ import json from unittest.mock import patch, MagicMock from sqlalchemy.exc import SQLAlchemyError from local_deep_research.chat.service import ArchiveBlockedError class TestCreateSessionExceptionHandling: """Tests for exception handling in create_session endpoint.""" def test_create_session_db_failure_returns_500(self, authenticated_client): """Test that database failure in create_session returns 500.""" with patch( "local_deep_research.chat.routes.ChatService" ) as mock_service_class: mock_service = MagicMock() mock_service.create_session.side_effect = SQLAlchemyError( "Database connection failed" ) mock_service_class.return_value = mock_service response = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test query"}, content_type="application/json", ) assert response.status_code == 500 data = json.loads(response.data) assert data["success"] is False assert "error" in data def test_create_session_database_failure_returns_500( self, authenticated_client ): """Test that database failure during session creation returns 500.""" with patch( "local_deep_research.chat.service.get_user_db_session" ) as mock_get_session: mock_get_session.side_effect = RuntimeError( "Database connection timeout" ) response = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test query"}, content_type="application/json", ) assert response.status_code == 500 data = json.loads(response.data) assert data["success"] is False class TestGetSessionExceptionHandling: """Tests for exception handling in get_session endpoint.""" def test_get_session_db_timeout_returns_500(self, authenticated_client): """Test that database timeout in get_session returns 500.""" with patch( "local_deep_research.chat.routes.ChatService" ) as mock_service_class: mock_service = MagicMock() mock_service.get_session.side_effect = SQLAlchemyError( "Database query timeout" ) mock_service_class.return_value = mock_service response = authenticated_client.get( "/api/chat/sessions/some-session-id" ) assert response.status_code == 500 data = json.loads(response.data) assert data["success"] is False assert "Failed to get chat session" in data["error"] class TestListSessionsExceptionHandling: """Tests for exception handling in list_sessions endpoint.""" def test_list_sessions_db_connection_error_returns_500( self, authenticated_client ): """Test that database connection error in list_sessions returns 500.""" with patch( "local_deep_research.chat.routes.ChatService" ) as mock_service_class: mock_service = MagicMock() mock_service.list_sessions.side_effect = SQLAlchemyError( "Connection refused" ) mock_service_class.return_value = mock_service response = authenticated_client.get("/api/chat/sessions") assert response.status_code == 500 data = json.loads(response.data) assert data["success"] is False assert "Failed to list chat sessions" in data["error"] class TestUpdateSessionExceptionHandling: """Tests for exception handling in update_session endpoint.""" def test_update_session_db_write_failure_returns_500( self, authenticated_client ): """Test that database write failure in update_session returns 500.""" with patch( "local_deep_research.chat.routes.ChatService" ) as mock_service_class: mock_service = MagicMock() mock_service.update_session_title.side_effect = SQLAlchemyError( "Write failed" ) mock_service_class.return_value = mock_service response = authenticated_client.patch( "/api/chat/sessions/some-session-id", json={"title": "New Title"}, content_type="application/json", ) assert response.status_code == 500 data = json.loads(response.data) assert data["success"] is False assert "Failed to update chat session" in data["error"] class TestArchiveBlockedReturns409: """HTTP-layer regression test for the archive-while-running guard. The service layer raises ``ArchiveBlockedError`` when a chat session has a ResearchHistory row in ``status='in_progress'`` for it. The route MUST turn that into a 409 with the exact hard-coded message (never echo ``str(exc)`` here — CWE-209). Without this test, a regression that drops the ``except ArchiveBlockedError`` branch, changes the status code, or starts interpolating exception text into the response would slip past the suite — the existing service-layer test in test_chat_archive_blocked_in_progress.py only covers the raise, not the route wiring. """ def test_archive_in_progress_returns_409_with_hardcoded_message( self, authenticated_client ): with patch( "local_deep_research.chat.routes.ChatService" ) as mock_service_class: mock_service = MagicMock() # The route calls get_session() first to verify existence, # so it must succeed before archive_session() is reached. mock_service.get_session.return_value = { "id": "sess-1", "title": "t", "status": "active", } # And on success it reads the session back for the response. mock_service.archive_session.side_effect = ArchiveBlockedError( # Deliberately use a message containing something we # would NOT want to leak — confirms the route does not # echo str(exc) into the response body. "research in_progress for session sess-1" ) mock_service_class.return_value = mock_service response = authenticated_client.patch( "/api/chat/sessions/sess-1", json={"status": "archived"}, content_type="application/json", ) assert response.status_code == 409 data = json.loads(response.data) assert data["success"] is False # Exact hard-coded message — must not include the exc text. assert ( data["error"] == "Cannot archive: research in_progress. Stop it first." ) assert "secret-123" not in data["error"] class TestDeleteSessionExceptionHandling: """Tests for exception handling in delete_session endpoint.""" def test_delete_session_db_failure_returns_500(self, authenticated_client): """Test that database failure in delete_session returns 500.""" with patch( "local_deep_research.chat.routes.ChatService" ) as mock_service_class: mock_service = MagicMock() mock_service.delete_session.side_effect = SQLAlchemyError( "Delete operation failed" ) mock_service_class.return_value = mock_service response = authenticated_client.delete( "/api/chat/sessions/some-session-id" ) assert response.status_code == 500 data = json.loads(response.data) assert data["success"] is False assert "Failed to delete chat session" in data["error"] class TestGetMessagesExceptionHandling: """Tests for exception handling in get_messages endpoint.""" def test_get_messages_db_query_failure_returns_500( self, authenticated_client ): """Test that database query failure in get_messages returns 500.""" with patch( "local_deep_research.chat.routes.ChatService" ) as mock_service_class: mock_service = MagicMock() mock_service.get_session_messages.side_effect = SQLAlchemyError( "Query failed" ) mock_service_class.return_value = mock_service response = authenticated_client.get( "/api/chat/sessions/some-session-id/messages" ) assert response.status_code == 500 data = json.loads(response.data) assert data["success"] is False assert "Failed to get chat messages" in data["error"] class TestSendMessageExceptionHandling: """Tests for exception handling in send_message endpoint.""" def test_send_message_service_failure_returns_500( self, authenticated_client ): """Test that service failure in send_message returns 500.""" with patch( "local_deep_research.chat.routes.ChatService" ) as mock_service_class: mock_service = MagicMock() mock_service.get_session.side_effect = RuntimeError( "Service unavailable" ) mock_service_class.return_value = mock_service response = authenticated_client.post( "/api/chat/sessions/some-session-id/messages", json={"content": "Test message", "trigger_research": False}, content_type="application/json", ) assert response.status_code == 500 data = json.loads(response.data) assert data["success"] is False assert "Failed to send message" in data["error"]