From e8af6a6b3b23ad06851cfc49fdd5727a49f2a3e1 Mon Sep 17 00:00:00 2001 From: James <63717587+jamesrayammons@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:55:21 +0200 Subject: [PATCH] test: migrate conversation service mock tests to testcontainers (#35198) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../test_conversation_service_variables.py | 524 ++++++++++++ .../services/test_conversation_service.py | 795 ------------------ 2 files changed, 524 insertions(+), 795 deletions(-) create mode 100644 api/tests/test_containers_integration_tests/services/test_conversation_service_variables.py diff --git a/api/tests/test_containers_integration_tests/services/test_conversation_service_variables.py b/api/tests/test_containers_integration_tests/services/test_conversation_service_variables.py new file mode 100644 index 0000000000..1011db9063 --- /dev/null +++ b/api/tests/test_containers_integration_tests/services/test_conversation_service_variables.py @@ -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] diff --git a/api/tests/unit_tests/services/test_conversation_service.py b/api/tests/unit_tests/services/test_conversation_service.py index 68f4c51afe..2c7f13b79f 100644 --- a/api/tests/unit_tests/services/test_conversation_service.py +++ b/api/tests/unit_tests/services/test_conversation_service.py @@ -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