feature: add test containers based tests for web conversation service (#24372)
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from faker import Faker
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from models.account import Account
|
||||
from models.model import Conversation, EndUser
|
||||
from models.web import PinnedConversation
|
||||
from services.account_service import AccountService, TenantService
|
||||
from services.app_service import AppService
|
||||
from services.web_conversation_service import WebConversationService
|
||||
|
||||
|
||||
class TestWebConversationService:
|
||||
"""Integration tests for WebConversationService using testcontainers."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.app_service.FeatureService") as mock_feature_service,
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise_service,
|
||||
patch("services.app_service.ModelManager") as mock_model_manager,
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns for app service
|
||||
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
|
||||
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
|
||||
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
|
||||
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
|
||||
# Mock ModelManager for model configuration
|
||||
mock_model_instance = mock_model_manager.return_value
|
||||
mock_model_instance.get_default_model_instance.return_value = None
|
||||
mock_model_instance.get_default_provider_model_name.return_value = ("openai", "gpt-3.5-turbo")
|
||||
|
||||
yield {
|
||||
"feature_service": mock_feature_service,
|
||||
"enterprise_service": mock_enterprise_service,
|
||||
"model_manager": mock_model_manager,
|
||||
"account_feature_service": mock_account_feature_service,
|
||||
}
|
||||
|
||||
def _create_test_app_and_account(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Helper method to create a test app and account for testing.
|
||||
|
||||
Args:
|
||||
db_session_with_containers: Database session from testcontainers infrastructure
|
||||
mock_external_service_dependencies: Mock dependencies
|
||||
|
||||
Returns:
|
||||
tuple: (app, account) - Created app and account instances
|
||||
"""
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
|
||||
# Create account and tenant
|
||||
account = AccountService.create_account(
|
||||
email=fake.email(),
|
||||
name=fake.name(),
|
||||
interface_language="en-US",
|
||||
password=fake.password(length=12),
|
||||
)
|
||||
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
|
||||
tenant = account.current_tenant
|
||||
|
||||
# Create app with realistic data
|
||||
app_args = {
|
||||
"name": fake.company(),
|
||||
"description": fake.text(max_nb_chars=100),
|
||||
"mode": "chat",
|
||||
"icon_type": "emoji",
|
||||
"icon": "🤖",
|
||||
"icon_background": "#FF6B6B",
|
||||
"api_rph": 100,
|
||||
"api_rpm": 10,
|
||||
}
|
||||
|
||||
app_service = AppService()
|
||||
app = app_service.create_app(tenant.id, app_args, account)
|
||||
|
||||
return app, account
|
||||
|
||||
def _create_test_end_user(self, db_session_with_containers, app):
|
||||
"""
|
||||
Helper method to create a test end user for testing.
|
||||
|
||||
Args:
|
||||
db_session_with_containers: Database session from testcontainers infrastructure
|
||||
app: App instance
|
||||
|
||||
Returns:
|
||||
EndUser: Created end user instance
|
||||
"""
|
||||
fake = Faker()
|
||||
|
||||
end_user = EndUser(
|
||||
session_id=fake.uuid4(),
|
||||
app_id=app.id,
|
||||
type="normal",
|
||||
is_anonymous=False,
|
||||
tenant_id=app.tenant_id,
|
||||
)
|
||||
|
||||
from extensions.ext_database import db
|
||||
|
||||
db.session.add(end_user)
|
||||
db.session.commit()
|
||||
|
||||
return end_user
|
||||
|
||||
def _create_test_conversation(self, db_session_with_containers, app, user, fake):
|
||||
"""
|
||||
Helper method to create a test conversation for testing.
|
||||
|
||||
Args:
|
||||
db_session_with_containers: Database session from testcontainers infrastructure
|
||||
app: App instance
|
||||
user: User instance (Account or EndUser)
|
||||
fake: Faker instance
|
||||
|
||||
Returns:
|
||||
Conversation: Created conversation instance
|
||||
"""
|
||||
conversation = Conversation(
|
||||
app_id=app.id,
|
||||
app_model_config_id=app.app_model_config_id,
|
||||
model_provider="openai",
|
||||
model_id="gpt-3.5-turbo",
|
||||
mode="chat",
|
||||
name=fake.sentence(nb_words=3),
|
||||
summary=fake.text(max_nb_chars=100),
|
||||
inputs={},
|
||||
introduction=fake.text(max_nb_chars=200),
|
||||
system_instruction=fake.text(max_nb_chars=300),
|
||||
system_instruction_tokens=50,
|
||||
status="normal",
|
||||
invoke_from=InvokeFrom.WEB_APP.value,
|
||||
from_source="console" if isinstance(user, Account) else "api",
|
||||
from_end_user_id=user.id if isinstance(user, EndUser) else None,
|
||||
from_account_id=user.id if isinstance(user, Account) else None,
|
||||
dialogue_count=0,
|
||||
is_deleted=False,
|
||||
)
|
||||
|
||||
from extensions.ext_database import db
|
||||
|
||||
db.session.add(conversation)
|
||||
db.session.commit()
|
||||
|
||||
return conversation
|
||||
|
||||
def test_pagination_by_last_id_success(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test successful pagination by last ID with basic parameters.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Create multiple conversations
|
||||
conversations = []
|
||||
for i in range(5):
|
||||
conversation = self._create_test_conversation(db_session_with_containers, app, account, fake)
|
||||
conversations.append(conversation)
|
||||
|
||||
# Test pagination without pinned filter
|
||||
result = WebConversationService.pagination_by_last_id(
|
||||
session=db_session_with_containers,
|
||||
app_model=app,
|
||||
user=account,
|
||||
last_id=None,
|
||||
limit=3,
|
||||
invoke_from=InvokeFrom.WEB_APP,
|
||||
pinned=None,
|
||||
sort_by="-updated_at",
|
||||
)
|
||||
|
||||
# Verify results
|
||||
assert result.limit == 3
|
||||
assert len(result.data) == 3
|
||||
assert result.has_more is True
|
||||
|
||||
# Verify conversations are in descending order by updated_at
|
||||
assert result.data[0].updated_at >= result.data[1].updated_at
|
||||
assert result.data[1].updated_at >= result.data[2].updated_at
|
||||
|
||||
def test_pagination_by_last_id_with_pinned_filter(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test pagination by last ID with pinned conversation filter.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Create conversations
|
||||
conversations = []
|
||||
for i in range(5):
|
||||
conversation = self._create_test_conversation(db_session_with_containers, app, account, fake)
|
||||
conversations.append(conversation)
|
||||
|
||||
# Pin some conversations
|
||||
pinned_conversation1 = PinnedConversation(
|
||||
app_id=app.id,
|
||||
conversation_id=conversations[0].id,
|
||||
created_by_role="account",
|
||||
created_by=account.id,
|
||||
)
|
||||
pinned_conversation2 = PinnedConversation(
|
||||
app_id=app.id,
|
||||
conversation_id=conversations[2].id,
|
||||
created_by_role="account",
|
||||
created_by=account.id,
|
||||
)
|
||||
|
||||
from extensions.ext_database import db
|
||||
|
||||
db.session.add(pinned_conversation1)
|
||||
db.session.add(pinned_conversation2)
|
||||
db.session.commit()
|
||||
|
||||
# Test pagination with pinned filter
|
||||
result = WebConversationService.pagination_by_last_id(
|
||||
session=db_session_with_containers,
|
||||
app_model=app,
|
||||
user=account,
|
||||
last_id=None,
|
||||
limit=10,
|
||||
invoke_from=InvokeFrom.WEB_APP,
|
||||
pinned=True,
|
||||
sort_by="-updated_at",
|
||||
)
|
||||
|
||||
# Verify only pinned conversations are returned
|
||||
assert result.limit == 10
|
||||
assert len(result.data) == 2
|
||||
assert result.has_more is False
|
||||
|
||||
# Verify the returned conversations are the pinned ones
|
||||
returned_ids = [conv.id for conv in result.data]
|
||||
expected_ids = [conversations[0].id, conversations[2].id]
|
||||
assert set(returned_ids) == set(expected_ids)
|
||||
|
||||
def test_pagination_by_last_id_with_unpinned_filter(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test pagination by last ID with unpinned conversation filter.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Create conversations
|
||||
conversations = []
|
||||
for i in range(5):
|
||||
conversation = self._create_test_conversation(db_session_with_containers, app, account, fake)
|
||||
conversations.append(conversation)
|
||||
|
||||
# Pin one conversation
|
||||
pinned_conversation = PinnedConversation(
|
||||
app_id=app.id,
|
||||
conversation_id=conversations[0].id,
|
||||
created_by_role="account",
|
||||
created_by=account.id,
|
||||
)
|
||||
|
||||
from extensions.ext_database import db
|
||||
|
||||
db.session.add(pinned_conversation)
|
||||
db.session.commit()
|
||||
|
||||
# Test pagination with unpinned filter
|
||||
result = WebConversationService.pagination_by_last_id(
|
||||
session=db_session_with_containers,
|
||||
app_model=app,
|
||||
user=account,
|
||||
last_id=None,
|
||||
limit=10,
|
||||
invoke_from=InvokeFrom.WEB_APP,
|
||||
pinned=False,
|
||||
sort_by="-updated_at",
|
||||
)
|
||||
|
||||
# Verify unpinned conversations are returned (should be 4 out of 5)
|
||||
assert result.limit == 10
|
||||
assert len(result.data) == 4
|
||||
assert result.has_more is False
|
||||
|
||||
# Verify the pinned conversation is not in the results
|
||||
returned_ids = [conv.id for conv in result.data]
|
||||
assert conversations[0].id not in returned_ids
|
||||
|
||||
# Verify all other conversations are in the results
|
||||
expected_unpinned_ids = [conv.id for conv in conversations[1:]]
|
||||
assert set(returned_ids) == set(expected_unpinned_ids)
|
||||
|
||||
def test_pin_conversation_success(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test successful pinning of a conversation.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Create a conversation
|
||||
conversation = self._create_test_conversation(db_session_with_containers, app, account, fake)
|
||||
|
||||
# Pin the conversation
|
||||
WebConversationService.pin(app, conversation.id, account)
|
||||
|
||||
# Verify the conversation was pinned
|
||||
from extensions.ext_database import db
|
||||
|
||||
pinned_conversation = (
|
||||
db.session.query(PinnedConversation)
|
||||
.where(
|
||||
PinnedConversation.app_id == app.id,
|
||||
PinnedConversation.conversation_id == conversation.id,
|
||||
PinnedConversation.created_by_role == "account",
|
||||
PinnedConversation.created_by == account.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
assert pinned_conversation is not None
|
||||
assert pinned_conversation.app_id == app.id
|
||||
assert pinned_conversation.conversation_id == conversation.id
|
||||
assert pinned_conversation.created_by_role == "account"
|
||||
assert pinned_conversation.created_by == account.id
|
||||
|
||||
def test_pin_conversation_already_pinned(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test pinning a conversation that is already pinned (should not create duplicate).
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Create a conversation
|
||||
conversation = self._create_test_conversation(db_session_with_containers, app, account, fake)
|
||||
|
||||
# Pin the conversation first time
|
||||
WebConversationService.pin(app, conversation.id, account)
|
||||
|
||||
# Pin the conversation again
|
||||
WebConversationService.pin(app, conversation.id, account)
|
||||
|
||||
# Verify only one pinned conversation record exists
|
||||
from extensions.ext_database import db
|
||||
|
||||
pinned_conversations = (
|
||||
db.session.query(PinnedConversation)
|
||||
.where(
|
||||
PinnedConversation.app_id == app.id,
|
||||
PinnedConversation.conversation_id == conversation.id,
|
||||
PinnedConversation.created_by_role == "account",
|
||||
PinnedConversation.created_by == account.id,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
assert len(pinned_conversations) == 1
|
||||
|
||||
def test_pin_conversation_with_end_user(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test pinning a conversation with an end user.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Create an end user
|
||||
end_user = self._create_test_end_user(db_session_with_containers, app)
|
||||
|
||||
# Create a conversation for the end user
|
||||
conversation = self._create_test_conversation(db_session_with_containers, app, end_user, fake)
|
||||
|
||||
# Pin the conversation
|
||||
WebConversationService.pin(app, conversation.id, end_user)
|
||||
|
||||
# Verify the conversation was pinned
|
||||
from extensions.ext_database import db
|
||||
|
||||
pinned_conversation = (
|
||||
db.session.query(PinnedConversation)
|
||||
.where(
|
||||
PinnedConversation.app_id == app.id,
|
||||
PinnedConversation.conversation_id == conversation.id,
|
||||
PinnedConversation.created_by_role == "end_user",
|
||||
PinnedConversation.created_by == end_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
assert pinned_conversation is not None
|
||||
assert pinned_conversation.app_id == app.id
|
||||
assert pinned_conversation.conversation_id == conversation.id
|
||||
assert pinned_conversation.created_by_role == "end_user"
|
||||
assert pinned_conversation.created_by == end_user.id
|
||||
|
||||
def test_unpin_conversation_success(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test successful unpinning of a conversation.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Create a conversation
|
||||
conversation = self._create_test_conversation(db_session_with_containers, app, account, fake)
|
||||
|
||||
# Pin the conversation first
|
||||
WebConversationService.pin(app, conversation.id, account)
|
||||
|
||||
# Verify it was pinned
|
||||
from extensions.ext_database import db
|
||||
|
||||
pinned_conversation = (
|
||||
db.session.query(PinnedConversation)
|
||||
.where(
|
||||
PinnedConversation.app_id == app.id,
|
||||
PinnedConversation.conversation_id == conversation.id,
|
||||
PinnedConversation.created_by_role == "account",
|
||||
PinnedConversation.created_by == account.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
assert pinned_conversation is not None
|
||||
|
||||
# Unpin the conversation
|
||||
WebConversationService.unpin(app, conversation.id, account)
|
||||
|
||||
# Verify it was unpinned
|
||||
pinned_conversation = (
|
||||
db.session.query(PinnedConversation)
|
||||
.where(
|
||||
PinnedConversation.app_id == app.id,
|
||||
PinnedConversation.conversation_id == conversation.id,
|
||||
PinnedConversation.created_by_role == "account",
|
||||
PinnedConversation.created_by == account.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
assert pinned_conversation is None
|
||||
|
||||
def test_unpin_conversation_not_pinned(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test unpinning a conversation that is not pinned (should not cause error).
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Create a conversation
|
||||
conversation = self._create_test_conversation(db_session_with_containers, app, account, fake)
|
||||
|
||||
# Try to unpin a conversation that was never pinned
|
||||
WebConversationService.unpin(app, conversation.id, account)
|
||||
|
||||
# Verify no pinned conversation record exists
|
||||
from extensions.ext_database import db
|
||||
|
||||
pinned_conversation = (
|
||||
db.session.query(PinnedConversation)
|
||||
.where(
|
||||
PinnedConversation.app_id == app.id,
|
||||
PinnedConversation.conversation_id == conversation.id,
|
||||
PinnedConversation.created_by_role == "account",
|
||||
PinnedConversation.created_by == account.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
assert pinned_conversation is None
|
||||
|
||||
def test_pagination_by_last_id_user_required_error(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test that pagination_by_last_id raises ValueError when user is None.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Test with None user
|
||||
with pytest.raises(ValueError, match="User is required"):
|
||||
WebConversationService.pagination_by_last_id(
|
||||
session=db_session_with_containers,
|
||||
app_model=app,
|
||||
user=None,
|
||||
last_id=None,
|
||||
limit=10,
|
||||
invoke_from=InvokeFrom.WEB_APP,
|
||||
pinned=None,
|
||||
sort_by="-updated_at",
|
||||
)
|
||||
|
||||
def test_pin_conversation_user_none(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test that pin method returns early when user is None.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Create a conversation
|
||||
conversation = self._create_test_conversation(db_session_with_containers, app, account, fake)
|
||||
|
||||
# Try to pin with None user
|
||||
WebConversationService.pin(app, conversation.id, None)
|
||||
|
||||
# Verify no pinned conversation was created
|
||||
from extensions.ext_database import db
|
||||
|
||||
pinned_conversation = (
|
||||
db.session.query(PinnedConversation)
|
||||
.where(
|
||||
PinnedConversation.app_id == app.id,
|
||||
PinnedConversation.conversation_id == conversation.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
assert pinned_conversation is None
|
||||
|
||||
def test_unpin_conversation_user_none(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test that unpin method returns early when user is None.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
# Create a conversation
|
||||
conversation = self._create_test_conversation(db_session_with_containers, app, account, fake)
|
||||
|
||||
# Pin the conversation first
|
||||
WebConversationService.pin(app, conversation.id, account)
|
||||
|
||||
# Verify it was pinned
|
||||
from extensions.ext_database import db
|
||||
|
||||
pinned_conversation = (
|
||||
db.session.query(PinnedConversation)
|
||||
.where(
|
||||
PinnedConversation.app_id == app.id,
|
||||
PinnedConversation.conversation_id == conversation.id,
|
||||
PinnedConversation.created_by_role == "account",
|
||||
PinnedConversation.created_by == account.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
assert pinned_conversation is not None
|
||||
|
||||
# Try to unpin with None user
|
||||
WebConversationService.unpin(app, conversation.id, None)
|
||||
|
||||
# Verify the conversation is still pinned
|
||||
pinned_conversation = (
|
||||
db.session.query(PinnedConversation)
|
||||
.where(
|
||||
PinnedConversation.app_id == app.id,
|
||||
PinnedConversation.conversation_id == conversation.id,
|
||||
PinnedConversation.created_by_role == "account",
|
||||
PinnedConversation.created_by == account.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
assert pinned_conversation is not None
|
Reference in New Issue
Block a user