test: migrate conversation service mock tests to testcontainers (#35198)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
James 2026-04-16 09:55:21 +02:00 committed by GitHub
parent 7f4fe4d064
commit e8af6a6b3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 524 additions and 795 deletions

View File

@ -0,0 +1,524 @@
from __future__ import annotations
from datetime import datetime, timedelta
from unittest.mock import patch
from uuid import uuid4
import pytest
from graphon.variables import FloatVariable, IntegerVariable, StringVariable
from sqlalchemy.orm import sessionmaker
from core.app.entities.app_invoke_entities import InvokeFrom
from extensions.ext_database import db
from models.account import Account, Tenant, TenantAccountJoin
from models.enums import ConversationFromSource
from models.model import App, Conversation, EndUser
from models.workflow import ConversationVariable
from services.conversation_service import ConversationService
from services.errors.conversation import (
ConversationVariableNotExistsError,
ConversationVariableTypeMismatchError,
LastConversationNotExistsError,
)
class ConversationServiceVariableIntegrationFactory:
@staticmethod
def create_app_and_account(db_session_with_containers):
tenant = Tenant(name=f"Tenant {uuid4()}")
db_session_with_containers.add(tenant)
db_session_with_containers.flush()
account = Account(
name=f"Account {uuid4()}",
email=f"conversation-variable-{uuid4()}@example.com",
password="hashed-password",
password_salt="salt",
interface_language="en-US",
timezone="UTC",
)
db_session_with_containers.add(account)
db_session_with_containers.flush()
tenant_join = TenantAccountJoin(
tenant_id=tenant.id,
account_id=account.id,
role="owner",
current=True,
)
db_session_with_containers.add(tenant_join)
db_session_with_containers.flush()
app = App(
tenant_id=tenant.id,
name=f"App {uuid4()}",
description="",
mode="chat",
icon_type="emoji",
icon="bot",
icon_background="#FFFFFF",
enable_site=False,
enable_api=True,
api_rpm=100,
api_rph=100,
is_demo=False,
is_public=False,
is_universal=False,
created_by=account.id,
updated_by=account.id,
)
db_session_with_containers.add(app)
db_session_with_containers.commit()
return app, account
@staticmethod
def create_end_user(db_session_with_containers, app: App):
end_user = EndUser(
tenant_id=app.tenant_id,
app_id=app.id,
type=InvokeFrom.SERVICE_API.value,
external_user_id=f"external-{uuid4()}",
name=f"End User {uuid4()}",
is_anonymous=False,
session_id=f"session-{uuid4()}",
)
db_session_with_containers.add(end_user)
db_session_with_containers.commit()
return end_user
@staticmethod
def create_conversation(
db_session_with_containers,
app: App,
user: Account | EndUser,
*,
name: str | None = None,
invoke_from: InvokeFrom = InvokeFrom.WEB_APP,
created_at: datetime | None = None,
updated_at: datetime | None = None,
) -> Conversation:
conversation = Conversation(
app_id=app.id,
app_model_config_id=None,
model_provider=None,
model_id="",
override_model_configs=None,
mode=app.mode,
name=name or f"Conversation {uuid4()}",
summary="",
inputs={},
introduction="",
system_instruction="",
system_instruction_tokens=0,
status="normal",
invoke_from=invoke_from.value,
from_source=ConversationFromSource.API if isinstance(user, EndUser) else ConversationFromSource.CONSOLE,
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,
)
conversation.inputs = {}
if created_at is not None:
conversation.created_at = created_at
if updated_at is not None:
conversation.updated_at = updated_at
db_session_with_containers.add(conversation)
db_session_with_containers.commit()
return conversation
@staticmethod
def create_variable(
db_session_with_containers,
*,
app: App,
conversation: Conversation,
variable: StringVariable | FloatVariable | IntegerVariable,
created_at: datetime | None = None,
) -> ConversationVariable:
row = ConversationVariable.from_variable(app_id=app.id, conversation_id=conversation.id, variable=variable)
if created_at is not None:
row.created_at = created_at
row.updated_at = created_at
db_session_with_containers.add(row)
db_session_with_containers.commit()
return row
@pytest.fixture
def real_conversation_service_session_factory(flask_app_with_containers):
del flask_app_with_containers
real_session_maker = sessionmaker(bind=db.engine, expire_on_commit=False)
with (
patch("services.conversation_service.session_factory.create_session", side_effect=lambda: real_session_maker()),
patch("services.conversation_service.session_factory.get_session_maker", return_value=real_session_maker),
):
yield
class TestConversationServiceVariables:
def test_get_conversational_variable_success(
self, db_session_with_containers, real_conversation_service_session_factory
):
del real_conversation_service_session_factory
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
conversation = factory.create_conversation(db_session_with_containers, app, account)
older_time = datetime(2024, 1, 1, 12, 0, 0)
newer_time = older_time + timedelta(minutes=5)
first_variable = factory.create_variable(
db_session_with_containers,
app=app,
conversation=conversation,
variable=StringVariable(id=str(uuid4()), name="topic", value="billing"),
created_at=older_time,
)
second_variable = factory.create_variable(
db_session_with_containers,
app=app,
conversation=conversation,
variable=StringVariable(id=str(uuid4()), name="priority", value="high"),
created_at=newer_time,
)
result = ConversationService.get_conversational_variable(
app_model=app,
conversation_id=conversation.id,
user=account,
limit=10,
last_id=None,
)
assert [item["id"] for item in result.data] == [first_variable.id, second_variable.id]
assert [item["name"] for item in result.data] == ["topic", "priority"]
assert result.limit == 10
assert result.has_more is False
def test_get_conversational_variable_with_last_id(
self, db_session_with_containers, real_conversation_service_session_factory
):
del real_conversation_service_session_factory
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
conversation = factory.create_conversation(db_session_with_containers, app, account)
base_time = datetime(2024, 1, 1, 9, 0, 0)
first_variable = factory.create_variable(
db_session_with_containers,
app=app,
conversation=conversation,
variable=StringVariable(id=str(uuid4()), name="topic", value="billing"),
created_at=base_time,
)
second_variable = factory.create_variable(
db_session_with_containers,
app=app,
conversation=conversation,
variable=StringVariable(id=str(uuid4()), name="priority", value="high"),
created_at=base_time + timedelta(minutes=1),
)
third_variable = factory.create_variable(
db_session_with_containers,
app=app,
conversation=conversation,
variable=StringVariable(id=str(uuid4()), name="owner", value="alice"),
created_at=base_time + timedelta(minutes=2),
)
result = ConversationService.get_conversational_variable(
app_model=app,
conversation_id=conversation.id,
user=account,
limit=10,
last_id=first_variable.id,
)
assert [item["id"] for item in result.data] == [second_variable.id, third_variable.id]
assert result.has_more is False
def test_get_conversational_variable_last_id_not_found_raises_error(
self, db_session_with_containers, real_conversation_service_session_factory
):
del real_conversation_service_session_factory
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
conversation = factory.create_conversation(db_session_with_containers, app, account)
with pytest.raises(ConversationVariableNotExistsError):
ConversationService.get_conversational_variable(
app_model=app,
conversation_id=conversation.id,
user=account,
limit=10,
last_id=str(uuid4()),
)
def test_get_conversational_variable_sets_has_more(
self, db_session_with_containers, real_conversation_service_session_factory
):
del real_conversation_service_session_factory
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
conversation = factory.create_conversation(db_session_with_containers, app, account)
for index in range(3):
factory.create_variable(
db_session_with_containers,
app=app,
conversation=conversation,
variable=StringVariable(id=str(uuid4()), name=f"var_{index}", value=f"value_{index}"),
created_at=datetime(2024, 1, 1, 10, 0, index),
)
result = ConversationService.get_conversational_variable(
app_model=app,
conversation_id=conversation.id,
user=account,
limit=2,
last_id=None,
)
assert len(result.data) == 2
assert result.has_more is True
def test_update_conversation_variable_success(
self, db_session_with_containers, real_conversation_service_session_factory
):
del real_conversation_service_session_factory
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
conversation = factory.create_conversation(db_session_with_containers, app, account)
existing = factory.create_variable(
db_session_with_containers,
app=app,
conversation=conversation,
variable=StringVariable(id=str(uuid4()), name="topic", value="billing"),
)
updated_at = datetime(2024, 1, 1, 15, 0, 0)
with patch("services.conversation_service.naive_utc_now", return_value=updated_at):
result = ConversationService.update_conversation_variable(
app_model=app,
conversation_id=conversation.id,
variable_id=existing.id,
user=account,
new_value="support",
)
db_session_with_containers.expire_all()
persisted = db_session_with_containers.get(ConversationVariable, (existing.id, conversation.id))
assert persisted is not None
assert persisted.to_variable().value == "support"
assert result["id"] == existing.id
assert result["value"] == "support"
assert result["updated_at"] == updated_at
def test_update_conversation_variable_not_found_raises_error(
self, db_session_with_containers, real_conversation_service_session_factory
):
del real_conversation_service_session_factory
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
conversation = factory.create_conversation(db_session_with_containers, app, account)
with pytest.raises(ConversationVariableNotExistsError):
ConversationService.update_conversation_variable(
app_model=app,
conversation_id=conversation.id,
variable_id=str(uuid4()),
user=account,
new_value="support",
)
def test_update_conversation_variable_type_mismatch_raises_error(
self, db_session_with_containers, real_conversation_service_session_factory
):
del real_conversation_service_session_factory
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
conversation = factory.create_conversation(db_session_with_containers, app, account)
existing = factory.create_variable(
db_session_with_containers,
app=app,
conversation=conversation,
variable=FloatVariable(id=str(uuid4()), name="score", value=1.5),
)
with pytest.raises(ConversationVariableTypeMismatchError, match="expects float"):
ConversationService.update_conversation_variable(
app_model=app,
conversation_id=conversation.id,
variable_id=existing.id,
user=account,
new_value="wrong-type",
)
def test_update_conversation_variable_integer_number_compatibility(
self, db_session_with_containers, real_conversation_service_session_factory
):
del real_conversation_service_session_factory
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
conversation = factory.create_conversation(db_session_with_containers, app, account)
existing = factory.create_variable(
db_session_with_containers,
app=app,
conversation=conversation,
variable=IntegerVariable(id=str(uuid4()), name="attempts", value=1),
)
result = ConversationService.update_conversation_variable(
app_model=app,
conversation_id=conversation.id,
variable_id=existing.id,
user=account,
new_value=42,
)
db_session_with_containers.expire_all()
persisted = db_session_with_containers.get(ConversationVariable, (existing.id, conversation.id))
assert persisted is not None
assert persisted.to_variable().value == 42
assert result["value"] == 42
class TestConversationServicePaginationWithContainers:
def test_pagination_by_last_id_raises_error_when_last_id_missing(self, db_session_with_containers):
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
with pytest.raises(LastConversationNotExistsError):
ConversationService.pagination_by_last_id(
session=db_session_with_containers,
app_model=app,
user=account,
last_id=str(uuid4()),
limit=20,
invoke_from=InvokeFrom.WEB_APP,
)
def test_pagination_by_last_id_with_default_desc_updated_at(self, db_session_with_containers):
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
base_time = datetime(2024, 1, 1, 8, 0, 0)
newest = factory.create_conversation(
db_session_with_containers,
app,
account,
name="Newest",
updated_at=base_time + timedelta(minutes=2),
)
middle = factory.create_conversation(
db_session_with_containers,
app,
account,
name="Middle",
updated_at=base_time + timedelta(minutes=1),
)
oldest = factory.create_conversation(
db_session_with_containers,
app,
account,
name="Oldest",
updated_at=base_time,
)
result = ConversationService.pagination_by_last_id(
session=db_session_with_containers,
app_model=app,
user=account,
last_id=middle.id,
limit=10,
invoke_from=InvokeFrom.WEB_APP,
)
assert newest.id != middle.id
assert [conversation.id for conversation in result.data] == [oldest.id]
def test_pagination_by_last_id_with_name_sort(self, db_session_with_containers):
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
alpha = factory.create_conversation(db_session_with_containers, app, account, name="Alpha")
beta = factory.create_conversation(db_session_with_containers, app, account, name="Beta")
gamma = factory.create_conversation(db_session_with_containers, app, account, name="Gamma")
result = ConversationService.pagination_by_last_id(
session=db_session_with_containers,
app_model=app,
user=account,
last_id=beta.id,
limit=10,
invoke_from=InvokeFrom.WEB_APP,
sort_by="name",
)
assert alpha.id != beta.id
assert [conversation.id for conversation in result.data] == [gamma.id]
def test_pagination_filters_to_end_user_api_source(self, db_session_with_containers):
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
end_user = factory.create_end_user(db_session_with_containers, app)
account_conversation = factory.create_conversation(
db_session_with_containers,
app,
account,
name="Console Conversation",
invoke_from=InvokeFrom.WEB_APP,
)
end_user_conversation = factory.create_conversation(
db_session_with_containers,
app,
end_user,
name="API Conversation",
invoke_from=InvokeFrom.SERVICE_API,
)
result = ConversationService.pagination_by_last_id(
session=db_session_with_containers,
app_model=app,
user=end_user,
last_id=None,
limit=20,
invoke_from=InvokeFrom.SERVICE_API,
)
assert account_conversation.id != end_user_conversation.id
assert [conversation.id for conversation in result.data] == [end_user_conversation.id]
def test_pagination_filters_to_account_console_source(self, db_session_with_containers):
factory = ConversationServiceVariableIntegrationFactory
app, account = factory.create_app_and_account(db_session_with_containers)
end_user = factory.create_end_user(db_session_with_containers, app)
account_conversation = factory.create_conversation(
db_session_with_containers,
app,
account,
name="Console Conversation",
invoke_from=InvokeFrom.WEB_APP,
)
factory.create_conversation(
db_session_with_containers,
app,
end_user,
name="API Conversation",
invoke_from=InvokeFrom.SERVICE_API,
)
result = ConversationService.pagination_by_last_id(
session=db_session_with_containers,
app_model=app,
user=account,
last_id=None,
limit=20,
invoke_from=InvokeFrom.WEB_APP,
)
assert [conversation.id for conversation in result.data] == [account_conversation.id]

View File

@ -6,26 +6,15 @@ Tests are organized by functionality and include edge cases, error handling,
and both positive and negative test scenarios.
"""
from datetime import timedelta
from unittest.mock import MagicMock, Mock, create_autospec, patch
import pytest
from sqlalchemy import asc, desc
from core.app.entities.app_invoke_entities import InvokeFrom
from libs.datetime_utils import naive_utc_now
from libs.infinite_scroll_pagination import InfiniteScrollPagination
from models import Account, ConversationVariable
from models.enums import ConversationFromSource
from models.model import App, Conversation, EndUser, Message
from services.conversation_service import ConversationService
from services.errors.conversation import (
ConversationNotExistsError,
ConversationVariableNotExistsError,
ConversationVariableTypeMismatchError,
LastConversationNotExistsError,
)
from services.errors.message import MessageNotExistsError
class ConversationServiceTestDataFactory:
@ -338,330 +327,9 @@ class TestConversationServiceHelpers:
assert condition is not None
class TestConversationServiceGetConversation:
"""Test conversation retrieval operations."""
@patch("services.conversation_service.db.session")
def test_get_conversation_success_with_account(self, mock_db_session):
"""
Test successful conversation retrieval with account user.
Should return conversation when found with proper filters.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock(
from_account_id=user.id, from_source=ConversationFromSource.CONSOLE
)
mock_db_session.scalar.return_value = conversation
# Act
result = ConversationService.get_conversation(app_model, "conv-123", user)
# Assert
assert result == conversation
@patch("services.conversation_service.db.session")
def test_get_conversation_success_with_end_user(self, mock_db_session):
"""
Test successful conversation retrieval with end user.
Should return conversation when found with proper filters for API user.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_end_user_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock(
from_end_user_id=user.id, from_source=ConversationFromSource.API
)
mock_db_session.scalar.return_value = conversation
# Act
result = ConversationService.get_conversation(app_model, "conv-123", user)
# Assert
assert result == conversation
@patch("services.conversation_service.db.session")
def test_get_conversation_not_found_raises_error(self, mock_db_session):
"""
Test that get_conversation raises error when conversation not found.
Should raise ConversationNotExistsError when no matching conversation found.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
mock_db_session.scalar.return_value = None
# Act & Assert
with pytest.raises(ConversationNotExistsError):
ConversationService.get_conversation(app_model, "conv-123", user)
class TestConversationServiceRename:
"""Test conversation rename operations."""
@patch("services.conversation_service.db.session")
@patch("services.conversation_service.ConversationService.get_conversation")
def test_rename_with_manual_name(self, mock_get_conversation, mock_db_session):
"""
Test renaming conversation with manual name.
Should update conversation name and timestamp when auto_generate is False.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_get_conversation.return_value = conversation
# Act
result = ConversationService.rename(
app_model=app_model,
conversation_id="conv-123",
user=user,
name="New Name",
auto_generate=False,
)
# Assert
assert result == conversation
assert conversation.name == "New Name"
mock_db_session.commit.assert_called_once()
class TestConversationServiceAutoGenerateName:
"""Test conversation auto-name generation operations."""
@patch("services.conversation_service.db.session")
@patch("services.conversation_service.LLMGenerator")
def test_auto_generate_name_success(self, mock_llm_generator, mock_db_session):
"""
Test successful auto-generation of conversation name.
Should generate name using LLMGenerator and update conversation.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
message = ConversationServiceTestDataFactory.create_message_mock(
conversation_id=conversation.id, app_id=app_model.id
)
# Mock database query to return message
mock_db_session.scalar.return_value = message
# Mock LLM generator
mock_llm_generator.generate_conversation_name.return_value = "Generated Name"
# Act
result = ConversationService.auto_generate_name(app_model, conversation)
# Assert
assert result == conversation
assert conversation.name == "Generated Name"
mock_llm_generator.generate_conversation_name.assert_called_once_with(
app_model.tenant_id, message.query, conversation.id, app_model.id
)
mock_db_session.commit.assert_called_once()
@patch("services.conversation_service.db.session")
def test_auto_generate_name_no_message_raises_error(self, mock_db_session):
"""
Test auto-generation fails when no message found.
Should raise MessageNotExistsError when conversation has no messages.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
# Mock database query to return None
mock_db_session.scalar.return_value = None
# Act & Assert
with pytest.raises(MessageNotExistsError):
ConversationService.auto_generate_name(app_model, conversation)
@patch("services.conversation_service.db.session")
@patch("services.conversation_service.LLMGenerator")
def test_auto_generate_name_handles_llm_exception(self, mock_llm_generator, mock_db_session):
"""
Test auto-generation handles LLM generator exceptions gracefully.
Should continue without name when LLMGenerator fails.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
message = ConversationServiceTestDataFactory.create_message_mock(
conversation_id=conversation.id, app_id=app_model.id
)
# Mock database query to return message
mock_db_session.scalar.return_value = message
# Mock LLM generator to raise exception
mock_llm_generator.generate_conversation_name.side_effect = Exception("LLM Error")
# Act
result = ConversationService.auto_generate_name(app_model, conversation)
# Assert
assert result == conversation
# Name should remain unchanged due to exception
mock_db_session.commit.assert_called_once()
class TestConversationServiceDelete:
"""Test conversation deletion operations."""
@patch("services.conversation_service.delete_conversation_related_data")
@patch("services.conversation_service.db.session")
@patch("services.conversation_service.ConversationService.get_conversation")
def test_delete_success(self, mock_get_conversation, mock_db_session, mock_delete_task):
"""
Test successful conversation deletion.
Should delete conversation and schedule cleanup task.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock(name="Test App")
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_get_conversation.return_value = conversation
# Act
ConversationService.delete(app_model, "conv-123", user)
# Assert
mock_db_session.delete.assert_called_once_with(conversation)
mock_db_session.commit.assert_called_once()
mock_delete_task.delay.assert_called_once_with(conversation.id)
class TestConversationServiceConversationalVariable:
"""Test conversational variable operations."""
@patch("services.conversation_service.session_factory")
@patch("services.conversation_service.ConversationService.get_conversation")
def test_get_conversational_variable_success(self, mock_get_conversation, mock_session_factory):
"""
Test successful retrieval of conversational variables.
Should return paginated list of variables for conversation.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_get_conversation.return_value = conversation
# Mock session and variables
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
variable1 = ConversationServiceTestDataFactory.create_conversation_variable_mock()
variable2 = ConversationServiceTestDataFactory.create_conversation_variable_mock(variable_id="var-456")
mock_session.scalars.return_value.all.return_value = [variable1, variable2]
# Act
result = ConversationService.get_conversational_variable(
app_model=app_model,
conversation_id="conv-123",
user=user,
limit=10,
last_id=None,
)
# Assert
assert isinstance(result, InfiniteScrollPagination)
assert len(result.data) == 2
assert result.limit == 10
assert result.has_more is False
@patch("services.conversation_service.session_factory")
@patch("services.conversation_service.ConversationService.get_conversation")
def test_get_conversational_variable_with_last_id(self, mock_get_conversation, mock_session_factory):
"""
Test retrieval of variables with last_id pagination.
Should filter variables created after last_id.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_get_conversation.return_value = conversation
# Mock session and variables
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
last_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(
created_at=naive_utc_now() - timedelta(hours=1)
)
variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(created_at=naive_utc_now())
mock_session.scalar.return_value = last_variable
mock_session.scalars.return_value.all.return_value = [variable]
# Act
result = ConversationService.get_conversational_variable(
app_model=app_model,
conversation_id="conv-123",
user=user,
limit=10,
last_id="var-123",
)
# Assert
assert isinstance(result, InfiniteScrollPagination)
assert len(result.data) == 1
assert result.limit == 10
@patch("services.conversation_service.session_factory")
@patch("services.conversation_service.ConversationService.get_conversation")
def test_get_conversational_variable_last_id_not_found_raises_error(
self, mock_get_conversation, mock_session_factory
):
"""
Test that invalid last_id raises ConversationVariableNotExistsError.
Should raise error when last_id doesn't exist.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_get_conversation.return_value = conversation
# Mock session
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
mock_session.scalar.return_value = None
# Act & Assert
with pytest.raises(ConversationVariableNotExistsError):
ConversationService.get_conversational_variable(
app_model=app_model,
conversation_id="conv-123",
user=user,
limit=10,
last_id="invalid-id",
)
@patch("services.conversation_service.session_factory")
@patch("services.conversation_service.ConversationService.get_conversation")
@patch("services.conversation_service.dify_config")
@ -698,466 +366,3 @@ class TestConversationServiceConversationalVariable:
# Assert - JSON filter should be applied
assert mock_session.scalars.called
@patch("services.conversation_service.session_factory")
@patch("services.conversation_service.ConversationService.get_conversation")
@patch("services.conversation_service.dify_config")
def test_get_conversational_variable_with_name_filter_postgresql(
self, mock_config, mock_get_conversation, mock_session_factory
):
"""
Test variable filtering by name for PostgreSQL databases.
Should apply JSON extraction filter for variable names.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_get_conversation.return_value = conversation
mock_config.DB_TYPE = "postgresql"
# Mock session
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
mock_session.scalars.return_value.all.return_value = []
# Act
ConversationService.get_conversational_variable(
app_model=app_model,
conversation_id="conv-123",
user=user,
limit=10,
last_id=None,
variable_name="test_var",
)
# Assert - JSON filter should be applied
assert mock_session.scalars.called
class TestConversationServiceUpdateVariable:
"""Test conversation variable update operations."""
@patch("services.conversation_service.variable_factory")
@patch("services.conversation_service.ConversationVariableUpdater")
@patch("services.conversation_service.session_factory")
@patch("services.conversation_service.ConversationService.get_conversation")
def test_update_conversation_variable_success(
self, mock_get_conversation, mock_session_factory, mock_updater_class, mock_variable_factory
):
"""
Test successful update of conversation variable.
Should update variable value and return updated data.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_get_conversation.return_value = conversation
# Mock session and existing variable
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
existing_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(value_type="string")
mock_session.scalar.return_value = existing_variable
# Mock variable factory and updater
updated_variable = Mock()
updated_variable.model_dump.return_value = {"id": "var-123", "name": "test_var", "value": "new_value"}
mock_variable_factory.build_conversation_variable_from_mapping.return_value = updated_variable
mock_updater = MagicMock()
mock_updater_class.return_value = mock_updater
# Act
result = ConversationService.update_conversation_variable(
app_model=app_model,
conversation_id="conv-123",
variable_id="var-123",
user=user,
new_value="new_value",
)
# Assert
assert result["id"] == "var-123"
assert result["value"] == "new_value"
mock_updater.update.assert_called_once_with("conv-123", updated_variable)
mock_updater.flush.assert_called_once()
@patch("services.conversation_service.session_factory")
@patch("services.conversation_service.ConversationService.get_conversation")
def test_update_conversation_variable_not_found_raises_error(self, mock_get_conversation, mock_session_factory):
"""
Test update fails when variable doesn't exist.
Should raise ConversationVariableNotExistsError.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_get_conversation.return_value = conversation
# Mock session
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
mock_session.scalar.return_value = None
# Act & Assert
with pytest.raises(ConversationVariableNotExistsError):
ConversationService.update_conversation_variable(
app_model=app_model,
conversation_id="conv-123",
variable_id="invalid-id",
user=user,
new_value="new_value",
)
@patch("services.conversation_service.session_factory")
@patch("services.conversation_service.ConversationService.get_conversation")
def test_update_conversation_variable_type_mismatch_raises_error(self, mock_get_conversation, mock_session_factory):
"""
Test update fails when value type doesn't match expected type.
Should raise ConversationVariableTypeMismatchError.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_get_conversation.return_value = conversation
# Mock session and existing variable
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
existing_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(value_type="number")
mock_session.scalar.return_value = existing_variable
# Act & Assert - Try to set string value for number variable
with pytest.raises(ConversationVariableTypeMismatchError):
ConversationService.update_conversation_variable(
app_model=app_model,
conversation_id="conv-123",
variable_id="var-123",
user=user,
new_value="string_value", # Wrong type
)
@patch("services.conversation_service.session_factory")
@patch("services.conversation_service.ConversationService.get_conversation")
def test_update_conversation_variable_integer_number_compatibility(
self, mock_get_conversation, mock_session_factory
):
"""
Test that integer type accepts number values.
Should allow number values for integer type variables.
"""
# Arrange
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_get_conversation.return_value = conversation
# Mock session and existing variable
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
existing_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(value_type="integer")
mock_session.scalar.return_value = existing_variable
# Mock variable factory and updater
updated_variable = Mock()
updated_variable.model_dump.return_value = {"id": "var-123", "name": "test_var", "value": 42}
with (
patch("services.conversation_service.variable_factory") as mock_variable_factory,
patch("services.conversation_service.ConversationVariableUpdater") as mock_updater_class,
):
mock_variable_factory.build_conversation_variable_from_mapping.return_value = updated_variable
mock_updater = MagicMock()
mock_updater_class.return_value = mock_updater
# Act
result = ConversationService.update_conversation_variable(
app_model=app_model,
conversation_id="conv-123",
variable_id="var-123",
user=user,
new_value=42, # Number value for integer type
)
# Assert
assert result["value"] == 42
mock_updater.update.assert_called_once()
class TestConversationServicePaginationAdvanced:
"""Advanced pagination tests for ConversationService."""
@patch("services.conversation_service.session_factory")
def test_pagination_by_last_id_with_last_id_not_found(self, mock_session_factory):
"""
Test pagination with invalid last_id raises error.
Should raise LastConversationNotExistsError when last_id doesn't exist.
"""
# Arrange
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
mock_session.scalar.return_value = None
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
# Act & Assert
with pytest.raises(LastConversationNotExistsError):
ConversationService.pagination_by_last_id(
session=mock_session,
app_model=app_model,
user=user,
last_id="invalid-id",
limit=20,
invoke_from=InvokeFrom.WEB_APP,
)
@patch("services.conversation_service.session_factory")
def test_pagination_by_last_id_with_exclude_ids(self, mock_session_factory):
"""
Test pagination with exclude_ids filter.
Should exclude specified conversation IDs from results.
"""
# Arrange
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_session.scalars.return_value.all.return_value = [conversation]
mock_session.scalar.return_value = conversation
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
# Act
result = ConversationService.pagination_by_last_id(
session=mock_session,
app_model=app_model,
user=user,
last_id=None,
limit=20,
invoke_from=InvokeFrom.WEB_APP,
exclude_ids=["excluded-123"],
)
# Assert
assert isinstance(result, InfiniteScrollPagination)
assert len(result.data) == 1
@patch("services.conversation_service.session_factory")
def test_pagination_by_last_id_has_more_detection(self, mock_session_factory):
"""
Test pagination has_more detection logic.
Should set has_more=True when there are more results beyond limit.
"""
# Arrange
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
# Return exactly limit items to trigger has_more check
conversations = [
ConversationServiceTestDataFactory.create_conversation_mock(conversation_id=f"conv-{i}") for i in range(20)
]
mock_session.scalars.return_value.all.return_value = conversations
mock_session.scalar.return_value = conversations[-1]
# Mock count query to return > 0
mock_session.scalar.return_value = 5 # Additional items exist
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
# Act
result = ConversationService.pagination_by_last_id(
session=mock_session,
app_model=app_model,
user=user,
last_id=None,
limit=20,
invoke_from=InvokeFrom.WEB_APP,
)
# Assert
assert isinstance(result, InfiniteScrollPagination)
assert result.has_more is True
@patch("services.conversation_service.session_factory")
def test_pagination_by_last_id_with_different_sort_by(self, mock_session_factory):
"""
Test pagination with different sort fields.
Should handle various sort_by parameters correctly.
"""
# Arrange
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_session.scalars.return_value.all.return_value = [conversation]
mock_session.scalar.return_value = conversation
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
# Test different sort fields
sort_fields = ["created_at", "-updated_at", "name", "-status"]
for sort_by in sort_fields:
# Act
result = ConversationService.pagination_by_last_id(
session=mock_session,
app_model=app_model,
user=user,
last_id=None,
limit=20,
invoke_from=InvokeFrom.WEB_APP,
sort_by=sort_by,
)
# Assert
assert isinstance(result, InfiniteScrollPagination)
class TestConversationServiceEdgeCases:
"""Test edge cases and error scenarios."""
@patch("services.conversation_service.session_factory")
def test_pagination_with_end_user_api_source(self, mock_session_factory):
"""
Test pagination correctly handles EndUser with API source.
Should use 'api' as from_source for EndUser instances.
"""
# Arrange
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
conversation = ConversationServiceTestDataFactory.create_conversation_mock(
from_source=ConversationFromSource.API, from_end_user_id="user-123"
)
mock_session.scalars.return_value.all.return_value = [conversation]
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_end_user_mock()
# Act
result = ConversationService.pagination_by_last_id(
session=mock_session,
app_model=app_model,
user=user,
last_id=None,
limit=20,
invoke_from=InvokeFrom.WEB_APP,
)
# Assert
assert isinstance(result, InfiniteScrollPagination)
@patch("services.conversation_service.session_factory")
def test_pagination_with_account_console_source(self, mock_session_factory):
"""
Test pagination correctly handles Account with console source.
Should use 'console' as from_source for Account instances.
"""
# Arrange
mock_session = MagicMock()
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
conversation = ConversationServiceTestDataFactory.create_conversation_mock(
from_source=ConversationFromSource.CONSOLE, from_account_id="account-123"
)
mock_session.scalars.return_value.all.return_value = [conversation]
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
# Act
result = ConversationService.pagination_by_last_id(
session=mock_session,
app_model=app_model,
user=user,
last_id=None,
limit=20,
invoke_from=InvokeFrom.WEB_APP,
)
# Assert
assert isinstance(result, InfiniteScrollPagination)
def test_pagination_with_include_ids_filter(self):
"""
Test pagination with include_ids filter.
Should only return conversations with IDs in include_ids list.
"""
# Arrange
mock_session = MagicMock()
mock_session.scalars.return_value.all.return_value = []
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
# Act
result = ConversationService.pagination_by_last_id(
session=mock_session,
app_model=app_model,
user=user,
last_id=None,
limit=20,
invoke_from=InvokeFrom.WEB_APP,
include_ids=["conv-123", "conv-456"],
)
# Assert
assert isinstance(result, InfiniteScrollPagination)
# Verify that include_ids filter was applied
assert mock_session.scalars.called
def test_pagination_with_empty_exclude_ids(self):
"""
Test pagination with empty exclude_ids list.
Should handle empty exclude_ids gracefully.
"""
# Arrange
mock_session = MagicMock()
mock_session.scalars.return_value.all.return_value = []
app_model = ConversationServiceTestDataFactory.create_app_mock()
user = ConversationServiceTestDataFactory.create_account_mock()
# Act
result = ConversationService.pagination_by_last_id(
session=mock_session,
app_model=app_model,
user=user,
last_id=None,
limit=20,
invoke_from=InvokeFrom.WEB_APP,
exclude_ids=[],
)
# Assert
assert isinstance(result, InfiniteScrollPagination)
assert result.has_more is False