Files
dify/api/tests/unit_tests/services/test_conversation_service.py

274 lines
9.3 KiB
Python

"""
Comprehensive unit tests for ConversationService.
This file keeps non-SQL guard/unit tests.
SQL-related tests were migrated to testcontainers integration tests.
"""
from datetime import datetime
from unittest.mock import MagicMock, Mock, create_autospec, patch
from core.app.entities.app_invoke_entities import InvokeFrom
from models import Account
from models.model import App, Conversation, EndUser
from services.conversation_service import ConversationService
from services.message_service import MessageService
class ConversationServiceTestDataFactory:
"""
Factory for creating test data and mock objects.
Provides reusable methods to create consistent mock objects for testing
conversation-related operations.
"""
@staticmethod
def create_account_mock(account_id: str = "account-123", **kwargs) -> Mock:
"""
Create a mock Account object.
Args:
account_id: Unique identifier for the account
**kwargs: Additional attributes to set on the mock
Returns:
Mock Account object with specified attributes
"""
account = create_autospec(Account, instance=True)
account.id = account_id
for key, value in kwargs.items():
setattr(account, key, value)
return account
@staticmethod
def create_end_user_mock(user_id: str = "user-123", **kwargs) -> Mock:
"""
Create a mock EndUser object.
Args:
user_id: Unique identifier for the end user
**kwargs: Additional attributes to set on the mock
Returns:
Mock EndUser object with specified attributes
"""
user = create_autospec(EndUser, instance=True)
user.id = user_id
for key, value in kwargs.items():
setattr(user, key, value)
return user
@staticmethod
def create_app_mock(app_id: str = "app-123", tenant_id: str = "tenant-123", **kwargs) -> Mock:
"""
Create a mock App object.
Args:
app_id: Unique identifier for the app
tenant_id: Tenant/workspace identifier
**kwargs: Additional attributes to set on the mock
Returns:
Mock App object with specified attributes
"""
app = create_autospec(App, instance=True)
app.id = app_id
app.tenant_id = tenant_id
app.name = kwargs.get("name", "Test App")
app.mode = kwargs.get("mode", "chat")
app.status = kwargs.get("status", "normal")
for key, value in kwargs.items():
setattr(app, key, value)
return app
@staticmethod
def create_conversation_mock(
conversation_id: str = "conv-123",
app_id: str = "app-123",
from_source: str = "console",
**kwargs,
) -> Mock:
"""
Create a mock Conversation object.
Args:
conversation_id: Unique identifier for the conversation
app_id: Associated app identifier
from_source: Source of conversation ('console' or 'api')
**kwargs: Additional attributes to set on the mock
Returns:
Mock Conversation object with specified attributes
"""
conversation = create_autospec(Conversation, instance=True)
conversation.id = conversation_id
conversation.app_id = app_id
conversation.from_source = from_source
conversation.from_end_user_id = kwargs.get("from_end_user_id")
conversation.from_account_id = kwargs.get("from_account_id")
conversation.is_deleted = kwargs.get("is_deleted", False)
conversation.name = kwargs.get("name", "Test Conversation")
conversation.status = kwargs.get("status", "normal")
conversation.created_at = kwargs.get("created_at", datetime.utcnow())
conversation.updated_at = kwargs.get("updated_at", datetime.utcnow())
for key, value in kwargs.items():
setattr(conversation, key, value)
return conversation
class TestConversationServicePagination:
"""Test conversation pagination operations."""
def test_pagination_with_empty_include_ids(self):
"""
Test that empty include_ids returns empty result.
When include_ids is an empty list, the service should short-circuit
and return empty results without querying the database.
"""
# Arrange - Set up test data
mock_session = MagicMock() # Mock database session
mock_app_model = ConversationServiceTestDataFactory.create_app_mock()
mock_user = ConversationServiceTestDataFactory.create_account_mock()
# Act - Call the service method with empty include_ids
result = ConversationService.pagination_by_last_id(
session=mock_session,
app_model=mock_app_model,
user=mock_user,
last_id=None,
limit=20,
invoke_from=InvokeFrom.WEB_APP,
include_ids=[], # Empty list should trigger early return
exclude_ids=None,
)
# Assert - Verify empty result without database query
assert result.data == [] # No conversations returned
assert result.has_more is False # No more pages available
assert result.limit == 20 # Limit preserved in response
def test_pagination_returns_empty_when_user_is_none(self):
"""
Test that pagination returns empty result when user is None.
This ensures proper handling of unauthenticated requests.
"""
# Arrange
mock_session = MagicMock()
mock_app_model = ConversationServiceTestDataFactory.create_app_mock()
# Act
result = ConversationService.pagination_by_last_id(
session=mock_session,
app_model=mock_app_model,
user=None, # No user provided
last_id=None,
limit=20,
invoke_from=InvokeFrom.WEB_APP,
)
# Assert - should return empty result without querying database
assert result.data == []
assert result.has_more is False
assert result.limit == 20
class TestConversationServiceMessageCreation:
"""
Test message creation and pagination.
Tests MessageService operations for creating and retrieving messages
within conversations.
"""
def test_pagination_returns_empty_when_no_user(self):
"""
Test that pagination returns empty result when user is None.
This ensures proper handling of unauthenticated requests.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
# Act
result = MessageService.pagination_by_first_id(
app_model=app_model,
user=None,
conversation_id="conv-123",
first_id=None,
limit=10,
)
# Assert
assert result.data == []
assert result.has_more is False
def test_pagination_returns_empty_when_no_conversation_id(self):
"""
Test that pagination returns empty result when conversation_id is None.
This ensures proper handling of invalid requests.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
# Act
result = MessageService.pagination_by_first_id(
app_model=app_model,
user=user,
conversation_id="",
first_id=None,
limit=10,
)
# Assert
assert result.data == []
assert result.has_more is False
class TestConversationServiceSummarization:
"""
Test conversation summarization (auto-generated names).
Tests the auto_generate_name functionality that creates conversation
titles based on the first message.
"""
@patch("services.conversation_service.db.session", autospec=True)
@patch("services.conversation_service.ConversationService.get_conversation", autospec=True)
@patch("services.conversation_service.ConversationService.auto_generate_name", autospec=True)
def test_rename_with_auto_generate(self, mock_auto_generate, mock_get_conversation, mock_db_session):
"""
Test renaming conversation with auto-generation enabled.
When auto_generate is True, the service should call the auto_generate_name
method to generate a new name for the conversation.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
conversation.name = "Auto-generated Name"
# Mock the conversation lookup to return our test conversation
mock_get_conversation.return_value = conversation
# Mock the auto_generate_name method to return the conversation
mock_auto_generate.return_value = conversation
# Act
result = ConversationService.rename(
app_model=app_model,
conversation_id=conversation.id,
user=user,
name="",
auto_generate=True,
)
# Assert
mock_auto_generate.assert_called_once_with(app_model, conversation)
assert result == conversation