mirror of
https://github.com/langgenius/dify.git
synced 2026-01-19 19:55:06 +08:00
Compare commits
11 Commits
fix/httpx_
...
fix/codeow
| Author | SHA1 | Date | |
|---|---|---|---|
| 5874b920b2 | |||
| c51ab6ec37 | |||
| 1fc2255219 | |||
| 037389137d | |||
| 8cd3e84c06 | |||
| b3c6ac1430 | |||
| 68bb97919a | |||
| f268d7c7be | |||
| d695a79ba1 | |||
| cd5a745bd2 | |||
| 51e5f422c4 |
226
.github/CODEOWNERS
vendored
Normal file
226
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1,226 @@
|
||||
# CODEOWNERS
|
||||
# This file defines code ownership for the Dify project.
|
||||
# Each line is a file pattern followed by one or more owners.
|
||||
# Owners can be @username, @org/team-name, or email addresses.
|
||||
# For more information, see: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
* @crazywoola @laipz8200 @Yeuoly
|
||||
|
||||
# Backend (default owner, more specific rules below will override)
|
||||
api/ @QuantumGhost
|
||||
|
||||
# Backend - Workflow - Engine (Core graph execution engine)
|
||||
api/core/workflow/graph_engine/ @laipz8200 @QuantumGhost
|
||||
api/core/workflow/runtime/ @laipz8200 @QuantumGhost
|
||||
api/core/workflow/graph/ @laipz8200 @QuantumGhost
|
||||
api/core/workflow/graph_events/ @laipz8200 @QuantumGhost
|
||||
api/core/workflow/node_events/ @laipz8200 @QuantumGhost
|
||||
api/core/model_runtime/ @laipz8200 @QuantumGhost
|
||||
|
||||
# Backend - Workflow - Nodes (Agent, Iteration, Loop, LLM)
|
||||
api/core/workflow/nodes/agent/ Nov1c444
|
||||
api/core/workflow/nodes/iteration/ Nov1c444
|
||||
api/core/workflow/nodes/loop/ Nov1c444
|
||||
api/core/workflow/nodes/llm/ Nov1c444
|
||||
|
||||
# Backend - RAG (Retrieval Augmented Generation)
|
||||
api/core/rag/ @JohnJyong
|
||||
api/services/rag_pipeline/ @JohnJyong
|
||||
api/services/dataset_service.py @JohnJyong
|
||||
api/services/knowledge_service.py @JohnJyong
|
||||
api/services/external_knowledge_service.py @JohnJyong
|
||||
api/services/hit_testing_service.py @JohnJyong
|
||||
api/services/metadata_service.py @JohnJyong
|
||||
api/services/vector_service.py @JohnJyong
|
||||
api/services/entities/knowledge_entities/ @JohnJyong
|
||||
api/services/entities/external_knowledge_entities/ @JohnJyong
|
||||
api/controllers/console/datasets/ @JohnJyong
|
||||
api/controllers/service_api/dataset/ @JohnJyong
|
||||
api/models/dataset.py @JohnJyong
|
||||
api/tasks/rag_pipeline/ @JohnJyong
|
||||
api/tasks/add_document_to_index_task.py @JohnJyong
|
||||
api/tasks/batch_clean_document_task.py @JohnJyong
|
||||
api/tasks/clean_document_task.py @JohnJyong
|
||||
api/tasks/clean_notion_document_task.py @JohnJyong
|
||||
api/tasks/document_indexing_task.py @JohnJyong
|
||||
api/tasks/document_indexing_sync_task.py @JohnJyong
|
||||
api/tasks/document_indexing_update_task.py @JohnJyong
|
||||
api/tasks/duplicate_document_indexing_task.py @JohnJyong
|
||||
api/tasks/recover_document_indexing_task.py @JohnJyong
|
||||
api/tasks/remove_document_from_index_task.py @JohnJyong
|
||||
api/tasks/retry_document_indexing_task.py @JohnJyong
|
||||
api/tasks/sync_website_document_indexing_task.py @JohnJyong
|
||||
api/tasks/batch_create_segment_to_index_task.py @JohnJyong
|
||||
api/tasks/create_segment_to_index_task.py @JohnJyong
|
||||
api/tasks/delete_segment_from_index_task.py @JohnJyong
|
||||
api/tasks/disable_segment_from_index_task.py @JohnJyong
|
||||
api/tasks/disable_segments_from_index_task.py @JohnJyong
|
||||
api/tasks/enable_segment_to_index_task.py @JohnJyong
|
||||
api/tasks/enable_segments_to_index_task.py @JohnJyong
|
||||
api/tasks/clean_dataset_task.py @JohnJyong
|
||||
api/tasks/deal_dataset_index_update_task.py @JohnJyong
|
||||
api/tasks/deal_dataset_vector_index_task.py @JohnJyong
|
||||
|
||||
# Backend - Plugins
|
||||
api/core/plugin/ @Mairuis @Yeuoly @Stream29
|
||||
api/services/plugin/ @Mairuis @Yeuoly @Stream29
|
||||
api/controllers/console/workspace/plugin.py @Mairuis @Yeuoly @Stream29
|
||||
api/controllers/inner_api/plugin/ @Mairuis @Yeuoly @Stream29
|
||||
api/tasks/process_tenant_plugin_autoupgrade_check_task.py @Mairuis @Yeuoly @Stream29
|
||||
|
||||
# Backend - Trigger/Schedule/Webhook
|
||||
api/controllers/trigger/ @Mairuis @Yeuoly
|
||||
api/controllers/console/app/workflow_trigger.py @Mairuis @Yeuoly
|
||||
api/controllers/console/workspace/trigger_providers.py @Mairuis @Yeuoly
|
||||
api/core/trigger/ @Mairuis @Yeuoly
|
||||
api/core/app/layers/trigger_post_layer.py @Mairuis @Yeuoly
|
||||
api/services/trigger/ @Mairuis @Yeuoly
|
||||
api/models/trigger.py @Mairuis @Yeuoly
|
||||
api/fields/workflow_trigger_fields.py @Mairuis @Yeuoly
|
||||
api/repositories/workflow_trigger_log_repository.py @Mairuis @Yeuoly
|
||||
api/repositories/sqlalchemy_workflow_trigger_log_repository.py @Mairuis @Yeuoly
|
||||
api/libs/schedule_utils.py @Mairuis @Yeuoly
|
||||
api/services/workflow/scheduler.py @Mairuis @Yeuoly
|
||||
api/schedule/trigger_provider_refresh_task.py @Mairuis @Yeuoly
|
||||
api/schedule/workflow_schedule_task.py @Mairuis @Yeuoly
|
||||
api/tasks/trigger_processing_tasks.py @Mairuis @Yeuoly
|
||||
api/tasks/trigger_subscription_refresh_tasks.py @Mairuis @Yeuoly
|
||||
api/tasks/workflow_schedule_tasks.py @Mairuis @Yeuoly
|
||||
api/tasks/workflow_cfs_scheduler/ @Mairuis @Yeuoly
|
||||
api/events/event_handlers/sync_plugin_trigger_when_app_created.py @Mairuis @Yeuoly
|
||||
api/events/event_handlers/update_app_triggers_when_app_published_workflow_updated.py @Mairuis @Yeuoly
|
||||
api/events/event_handlers/sync_workflow_schedule_when_app_published.py @Mairuis @Yeuoly
|
||||
api/events/event_handlers/sync_webhook_when_app_created.py @Mairuis @Yeuoly
|
||||
|
||||
# Backend - Async Workflow
|
||||
api/services/async_workflow_service.py @Mairuis @Yeuoly
|
||||
api/tasks/async_workflow_tasks.py @Mairuis @Yeuoly
|
||||
|
||||
# Backend - Billing
|
||||
api/services/billing_service.py @hj24 @zyssyz123
|
||||
api/controllers/console/billing/ @hj24 @zyssyz123
|
||||
|
||||
# Backend - Enterprise
|
||||
api/configs/enterprise/ @GarfieldDai @GareArc
|
||||
api/services/enterprise/ @GarfieldDai @GareArc
|
||||
api/services/feature_service.py @GarfieldDai @GareArc
|
||||
api/controllers/console/feature.py @GarfieldDai @GareArc
|
||||
api/controllers/web/feature.py @GarfieldDai @GareArc
|
||||
|
||||
# Backend - Database Migrations
|
||||
api/migrations/ @snakevash @laipz8200
|
||||
|
||||
# Frontend
|
||||
web/ @iamjoel
|
||||
|
||||
# Frontend - App - Orchestration
|
||||
web/app/components/workflow/ @iamjoel @zxhlyh
|
||||
web/app/components/workflow-app/ @iamjoel @zxhlyh
|
||||
web/app/components/app/configuration/ @iamjoel @zxhlyh
|
||||
web/app/components/app/app-publisher/ @iamjoel @zxhlyh
|
||||
|
||||
# Frontend - WebApp - Chat
|
||||
web/app/components/base/chat/ @iamjoel @zxhlyh
|
||||
|
||||
# Frontend - WebApp - Completion
|
||||
web/app/components/share/text-generation/ @iamjoel @zxhlyh
|
||||
|
||||
# Frontend - App - List and Creation
|
||||
web/app/components/apps/ @JzoNgKVO @iamjoel
|
||||
web/app/components/app/create-app-dialog/ @JzoNgKVO @iamjoel
|
||||
web/app/components/app/create-app-modal/ @JzoNgKVO @iamjoel
|
||||
web/app/components/app/create-from-dsl-modal/ @JzoNgKVO @iamjoel
|
||||
|
||||
# Frontend - App - API Documentation
|
||||
web/app/components/develop/ @JzoNgKVO @iamjoel
|
||||
|
||||
# Frontend - App - Logs and Annotations
|
||||
web/app/components/app/workflow-log/ @JzoNgKVO @iamjoel
|
||||
web/app/components/app/log/ @JzoNgKVO @iamjoel
|
||||
web/app/components/app/log-annotation/ @JzoNgKVO @iamjoel
|
||||
web/app/components/app/annotation/ @JzoNgKVO @iamjoel
|
||||
|
||||
# Frontend - App - Monitoring
|
||||
web/app/(commonLayout)/app/(appDetailLayout)/\[appId\]/overview/ @JzoNgKVO @iamjoel
|
||||
web/app/components/app/overview/ @JzoNgKVO @iamjoel
|
||||
|
||||
# Frontend - App - Settings
|
||||
web/app/components/app-sidebar/ @JzoNgKVO @iamjoel
|
||||
|
||||
# Frontend - RAG - Hit Testing
|
||||
web/app/components/datasets/hit-testing/ @JzoNgKVO @iamjoel
|
||||
|
||||
# Frontend - RAG - List and Creation
|
||||
web/app/components/datasets/list/ @iamjoel @WTW0313
|
||||
web/app/components/datasets/create/ @iamjoel @WTW0313
|
||||
web/app/components/datasets/create-from-pipeline/ @iamjoel @WTW0313
|
||||
web/app/components/datasets/external-knowledge-base/ @iamjoel @WTW0313
|
||||
|
||||
# Frontend - RAG - Orchestration (general rule first, specific rules below override)
|
||||
web/app/components/rag-pipeline/ @iamjoel @WTW0313
|
||||
web/app/components/rag-pipeline/components/rag-pipeline-main.tsx @iamjoel @zxhlyh
|
||||
web/app/components/rag-pipeline/store/ @iamjoel @zxhlyh
|
||||
|
||||
# Frontend - RAG - Documents List
|
||||
web/app/components/datasets/documents/list.tsx @iamjoel @WTW0313
|
||||
web/app/components/datasets/documents/create-from-pipeline/ @iamjoel @WTW0313
|
||||
|
||||
# Frontend - RAG - Segments List
|
||||
web/app/components/datasets/documents/detail/ @iamjoel @WTW0313
|
||||
|
||||
# Frontend - RAG - Settings
|
||||
web/app/components/datasets/settings/ @iamjoel @WTW0313
|
||||
|
||||
# Frontend - Ecosystem - Plugins
|
||||
web/app/components/plugins/ @iamjoel @zhsama
|
||||
|
||||
# Frontend - Ecosystem - Tools
|
||||
web/app/components/tools/ @iamjoel @Yessenia-d
|
||||
|
||||
# Frontend - Ecosystem - MarketPlace
|
||||
web/app/components/plugins/marketplace/ @iamjoel @Yessenia-d
|
||||
|
||||
# Frontend - Login and Registration
|
||||
web/app/signin/ @douxc @iamjoel
|
||||
web/app/signup/ @douxc @iamjoel
|
||||
web/app/reset-password/ @douxc @iamjoel
|
||||
web/app/install/ @douxc @iamjoel
|
||||
web/app/init/ @douxc @iamjoel
|
||||
web/app/forgot-password/ @douxc @iamjoel
|
||||
web/app/account/ @douxc @iamjoel
|
||||
|
||||
# Frontend - Service Authentication
|
||||
web/service/base.ts @douxc @iamjoel
|
||||
|
||||
# Frontend - WebApp Authentication and Access Control
|
||||
web/app/(shareLayout)/components/ @douxc @iamjoel
|
||||
web/app/(shareLayout)/webapp-signin/ @douxc @iamjoel
|
||||
web/app/(shareLayout)/webapp-reset-password/ @douxc @iamjoel
|
||||
web/app/components/app/app-access-control/ @douxc @iamjoel
|
||||
|
||||
# Frontend - Explore Page
|
||||
web/app/components/explore/ @CodingOnStar @iamjoel
|
||||
|
||||
# Frontend - Personal Settings
|
||||
web/app/components/header/account-setting/ @CodingOnStar @iamjoel
|
||||
web/app/components/header/account-dropdown/ @CodingOnStar @iamjoel
|
||||
|
||||
# Frontend - Analytics
|
||||
web/app/components/base/ga/ @CodingOnStar @iamjoel
|
||||
|
||||
# Frontend - Base Components
|
||||
web/app/components/base/ @iamjoel @zxhlyh
|
||||
|
||||
# Frontend - Utils and Hooks
|
||||
web/utils/classnames.ts @iamjoel @zxhlyh
|
||||
web/utils/time.ts @iamjoel @zxhlyh
|
||||
web/utils/format.ts @iamjoel @zxhlyh
|
||||
web/utils/clipboard.ts @iamjoel @zxhlyh
|
||||
web/hooks/use-document-title.ts @iamjoel @zxhlyh
|
||||
|
||||
# Frontend - Billing and Education
|
||||
web/app/components/billing/ @iamjoel @zxhlyh
|
||||
web/app/education-apply/ @iamjoel @zxhlyh
|
||||
|
||||
# Frontend - Workspace
|
||||
web/app/components/header/account-dropdown/workplace-selector/ @iamjoel @zxhlyh
|
||||
@ -58,7 +58,7 @@ class VersionApi(Resource):
|
||||
response = httpx.get(
|
||||
check_update_url,
|
||||
params={"current_version": args["current_version"]},
|
||||
timeout=httpx.Timeout(10.0, connect=3.0),
|
||||
timeout=httpx.Timeout(connect=3, read=10),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning("Check update version error: %s.", str(error))
|
||||
|
||||
1668
api/tests/unit_tests/core/datasource/test_notion_provider.py
Normal file
1668
api/tests/unit_tests/core/datasource/test_notion_provider.py
Normal file
File diff suppressed because it is too large
Load Diff
1748
api/tests/unit_tests/core/datasource/test_website_crawl.py
Normal file
1748
api/tests/unit_tests/core/datasource/test_website_crawl.py
Normal file
File diff suppressed because it is too large
Load Diff
0
api/tests/unit_tests/core/rag/indexing/__init__.py
Normal file
0
api/tests/unit_tests/core/rag/indexing/__init__.py
Normal file
1532
api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py
Normal file
1532
api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py
Normal file
File diff suppressed because it is too large
Load Diff
1291
api/tests/unit_tests/services/document_indexing_task_proxy.py
Normal file
1291
api/tests/unit_tests/services/document_indexing_task_proxy.py
Normal file
File diff suppressed because it is too large
Load Diff
494
api/tests/unit_tests/services/test_end_user_service.py
Normal file
494
api/tests/unit_tests/services/test_end_user_service.py
Normal file
@ -0,0 +1,494 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from models.model import App, DefaultEndUserSessionID, EndUser
|
||||
from services.end_user_service import EndUserService
|
||||
|
||||
|
||||
class TestEndUserServiceFactory:
|
||||
"""Factory class for creating test data and mock objects for end user service tests."""
|
||||
|
||||
@staticmethod
|
||||
def create_app_mock(
|
||||
app_id: str = "app-123",
|
||||
tenant_id: str = "tenant-456",
|
||||
name: str = "Test App",
|
||||
) -> MagicMock:
|
||||
"""Create a mock App object."""
|
||||
app = MagicMock(spec=App)
|
||||
app.id = app_id
|
||||
app.tenant_id = tenant_id
|
||||
app.name = name
|
||||
return app
|
||||
|
||||
@staticmethod
|
||||
def create_end_user_mock(
|
||||
user_id: str = "user-789",
|
||||
tenant_id: str = "tenant-456",
|
||||
app_id: str = "app-123",
|
||||
session_id: str = "session-001",
|
||||
type: InvokeFrom = InvokeFrom.SERVICE_API,
|
||||
is_anonymous: bool = False,
|
||||
) -> MagicMock:
|
||||
"""Create a mock EndUser object."""
|
||||
end_user = MagicMock(spec=EndUser)
|
||||
end_user.id = user_id
|
||||
end_user.tenant_id = tenant_id
|
||||
end_user.app_id = app_id
|
||||
end_user.session_id = session_id
|
||||
end_user.type = type
|
||||
end_user.is_anonymous = is_anonymous
|
||||
end_user.external_user_id = session_id
|
||||
return end_user
|
||||
|
||||
|
||||
class TestEndUserServiceGetOrCreateEndUser:
|
||||
"""
|
||||
Unit tests for EndUserService.get_or_create_end_user method.
|
||||
|
||||
This test suite covers:
|
||||
- Creating new end users
|
||||
- Retrieving existing end users
|
||||
- Default session ID handling
|
||||
- Anonymous user creation
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def factory(self):
|
||||
"""Provide test data factory."""
|
||||
return TestEndUserServiceFactory()
|
||||
|
||||
# Test 01: Get or create with custom user_id
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_get_or_create_end_user_with_custom_user_id(self, mock_db, mock_session_class, factory):
|
||||
"""Test getting or creating end user with custom user_id."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user_id = "custom-user-123"
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = None # No existing user
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user(app_model=app, user_id=user_id)
|
||||
|
||||
# Assert
|
||||
mock_session.add.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
# Verify the created user has correct attributes
|
||||
added_user = mock_session.add.call_args[0][0]
|
||||
assert added_user.tenant_id == app.tenant_id
|
||||
assert added_user.app_id == app.id
|
||||
assert added_user.session_id == user_id
|
||||
assert added_user.type == InvokeFrom.SERVICE_API
|
||||
assert added_user.is_anonymous is False
|
||||
|
||||
# Test 02: Get or create without user_id (default session)
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_get_or_create_end_user_without_user_id(self, mock_db, mock_session_class, factory):
|
||||
"""Test getting or creating end user without user_id uses default session."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = None # No existing user
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user(app_model=app, user_id=None)
|
||||
|
||||
# Assert
|
||||
mock_session.add.assert_called_once()
|
||||
added_user = mock_session.add.call_args[0][0]
|
||||
assert added_user.session_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID
|
||||
# Verify _is_anonymous is set correctly (property always returns False)
|
||||
assert added_user._is_anonymous is True
|
||||
|
||||
# Test 03: Get existing end user
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_get_existing_end_user(self, mock_db, mock_session_class, factory):
|
||||
"""Test retrieving an existing end user."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user_id = "existing-user-123"
|
||||
existing_user = factory.create_end_user_mock(
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
session_id=user_id,
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
)
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = existing_user
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user(app_model=app, user_id=user_id)
|
||||
|
||||
# Assert
|
||||
assert result == existing_user
|
||||
mock_session.add.assert_not_called() # Should not create new user
|
||||
|
||||
|
||||
class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
"""
|
||||
Unit tests for EndUserService.get_or_create_end_user_by_type method.
|
||||
|
||||
This test suite covers:
|
||||
- Creating end users with different InvokeFrom types
|
||||
- Type migration for legacy users
|
||||
- Query ordering and prioritization
|
||||
- Session management
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def factory(self):
|
||||
"""Provide test data factory."""
|
||||
return TestEndUserServiceFactory()
|
||||
|
||||
# Test 04: Create new end user with SERVICE_API type
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_create_end_user_service_api_type(self, mock_db, mock_session_class, factory):
|
||||
"""Test creating new end user with SERVICE_API type."""
|
||||
# Arrange
|
||||
tenant_id = "tenant-123"
|
||||
app_id = "app-456"
|
||||
user_id = "user-789"
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_session.add.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
added_user = mock_session.add.call_args[0][0]
|
||||
assert added_user.type == InvokeFrom.SERVICE_API
|
||||
assert added_user.tenant_id == tenant_id
|
||||
assert added_user.app_id == app_id
|
||||
assert added_user.session_id == user_id
|
||||
|
||||
# Test 05: Create new end user with WEB_APP type
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_create_end_user_web_app_type(self, mock_db, mock_session_class, factory):
|
||||
"""Test creating new end user with WEB_APP type."""
|
||||
# Arrange
|
||||
tenant_id = "tenant-123"
|
||||
app_id = "app-456"
|
||||
user_id = "user-789"
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.WEB_APP,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_session.add.assert_called_once()
|
||||
added_user = mock_session.add.call_args[0][0]
|
||||
assert added_user.type == InvokeFrom.WEB_APP
|
||||
|
||||
# Test 06: Upgrade legacy end user type
|
||||
@patch("services.end_user_service.logger")
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_upgrade_legacy_end_user_type(self, mock_db, mock_session_class, mock_logger, factory):
|
||||
"""Test upgrading legacy end user with different type."""
|
||||
# Arrange
|
||||
tenant_id = "tenant-123"
|
||||
app_id = "app-456"
|
||||
user_id = "user-789"
|
||||
|
||||
# Existing user with old type
|
||||
existing_user = factory.create_end_user_mock(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
session_id=user_id,
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
)
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = existing_user
|
||||
|
||||
# Act - Request with different type
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.WEB_APP,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == existing_user
|
||||
assert existing_user.type == InvokeFrom.WEB_APP # Type should be updated
|
||||
mock_session.commit.assert_called_once()
|
||||
mock_logger.info.assert_called_once()
|
||||
# Verify log message contains upgrade info
|
||||
log_call = mock_logger.info.call_args[0][0]
|
||||
assert "Upgrading legacy EndUser" in log_call
|
||||
|
||||
# Test 07: Get existing end user with matching type (no upgrade needed)
|
||||
@patch("services.end_user_service.logger")
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_get_existing_end_user_matching_type(self, mock_db, mock_session_class, mock_logger, factory):
|
||||
"""Test retrieving existing end user with matching type."""
|
||||
# Arrange
|
||||
tenant_id = "tenant-123"
|
||||
app_id = "app-456"
|
||||
user_id = "user-789"
|
||||
|
||||
existing_user = factory.create_end_user_mock(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
session_id=user_id,
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
)
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = existing_user
|
||||
|
||||
# Act - Request with same type
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == existing_user
|
||||
assert existing_user.type == InvokeFrom.SERVICE_API
|
||||
# No commit should be called (no type update needed)
|
||||
mock_session.commit.assert_not_called()
|
||||
mock_logger.info.assert_not_called()
|
||||
|
||||
# Test 08: Create anonymous user with default session ID
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_create_anonymous_user_with_default_session(self, mock_db, mock_session_class, factory):
|
||||
"""Test creating anonymous user when user_id is None."""
|
||||
# Arrange
|
||||
tenant_id = "tenant-123"
|
||||
app_id = "app-456"
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=None,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_session.add.assert_called_once()
|
||||
added_user = mock_session.add.call_args[0][0]
|
||||
assert added_user.session_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID
|
||||
# Verify _is_anonymous is set correctly (property always returns False)
|
||||
assert added_user._is_anonymous is True
|
||||
assert added_user.external_user_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID
|
||||
|
||||
# Test 09: Query ordering prioritizes matching type
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_query_ordering_prioritizes_matching_type(self, mock_db, mock_session_class, factory):
|
||||
"""Test that query ordering prioritizes records with matching type."""
|
||||
# Arrange
|
||||
tenant_id = "tenant-123"
|
||||
app_id = "app-456"
|
||||
user_id = "user-789"
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
|
||||
# Act
|
||||
EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Assert
|
||||
# Verify order_by was called (for type prioritization)
|
||||
mock_query.order_by.assert_called_once()
|
||||
|
||||
# Test 10: Session context manager properly closes
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_session_context_manager_closes(self, mock_db, mock_session_class, factory):
|
||||
"""Test that Session context manager is properly used."""
|
||||
# Arrange
|
||||
tenant_id = "tenant-123"
|
||||
app_id = "app-456"
|
||||
user_id = "user-789"
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_context = MagicMock()
|
||||
mock_context.__enter__.return_value = mock_session
|
||||
mock_session_class.return_value = mock_context
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
|
||||
# Act
|
||||
EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Assert
|
||||
# Verify context manager was entered and exited
|
||||
mock_context.__enter__.assert_called_once()
|
||||
mock_context.__exit__.assert_called_once()
|
||||
|
||||
# Test 11: External user ID matches session ID
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_external_user_id_matches_session_id(self, mock_db, mock_session_class, factory):
|
||||
"""Test that external_user_id is set to match session_id."""
|
||||
# Arrange
|
||||
tenant_id = "tenant-123"
|
||||
app_id = "app-456"
|
||||
user_id = "custom-external-id"
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Assert
|
||||
added_user = mock_session.add.call_args[0][0]
|
||||
assert added_user.external_user_id == user_id
|
||||
assert added_user.session_id == user_id
|
||||
|
||||
# Test 12: Different InvokeFrom types
|
||||
@pytest.mark.parametrize(
|
||||
"invoke_type",
|
||||
[
|
||||
InvokeFrom.SERVICE_API,
|
||||
InvokeFrom.WEB_APP,
|
||||
InvokeFrom.EXPLORE,
|
||||
InvokeFrom.DEBUGGER,
|
||||
],
|
||||
)
|
||||
@patch("services.end_user_service.Session")
|
||||
@patch("services.end_user_service.db")
|
||||
def test_create_end_user_with_different_invoke_types(self, mock_db, mock_session_class, invoke_type, factory):
|
||||
"""Test creating end users with different InvokeFrom types."""
|
||||
# Arrange
|
||||
tenant_id = "tenant-123"
|
||||
app_id = "app-456"
|
||||
user_id = "user-789"
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session_class.return_value.__enter__.return_value = mock_session
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=invoke_type,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Assert
|
||||
added_user = mock_session.add.call_args[0][0]
|
||||
assert added_user.type == invoke_type
|
||||
649
api/tests/unit_tests/services/test_message_service.py
Normal file
649
api/tests/unit_tests/services/test_message_service.py
Normal file
@ -0,0 +1,649 @@
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from libs.infinite_scroll_pagination import InfiniteScrollPagination
|
||||
from models.model import App, AppMode, EndUser, Message
|
||||
from services.errors.message import FirstMessageNotExistsError, LastMessageNotExistsError
|
||||
from services.message_service import MessageService
|
||||
|
||||
|
||||
class TestMessageServiceFactory:
|
||||
"""Factory class for creating test data and mock objects for message service tests."""
|
||||
|
||||
@staticmethod
|
||||
def create_app_mock(
|
||||
app_id: str = "app-123",
|
||||
mode: str = AppMode.ADVANCED_CHAT.value,
|
||||
name: str = "Test App",
|
||||
) -> MagicMock:
|
||||
"""Create a mock App object."""
|
||||
app = MagicMock(spec=App)
|
||||
app.id = app_id
|
||||
app.mode = mode
|
||||
app.name = name
|
||||
return app
|
||||
|
||||
@staticmethod
|
||||
def create_end_user_mock(
|
||||
user_id: str = "user-456",
|
||||
session_id: str = "session-789",
|
||||
) -> MagicMock:
|
||||
"""Create a mock EndUser object."""
|
||||
user = MagicMock(spec=EndUser)
|
||||
user.id = user_id
|
||||
user.session_id = session_id
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def create_conversation_mock(
|
||||
conversation_id: str = "conv-001",
|
||||
app_id: str = "app-123",
|
||||
) -> MagicMock:
|
||||
"""Create a mock Conversation object."""
|
||||
conversation = MagicMock()
|
||||
conversation.id = conversation_id
|
||||
conversation.app_id = app_id
|
||||
return conversation
|
||||
|
||||
@staticmethod
|
||||
def create_message_mock(
|
||||
message_id: str = "msg-001",
|
||||
conversation_id: str = "conv-001",
|
||||
query: str = "What is AI?",
|
||||
answer: str = "AI stands for Artificial Intelligence.",
|
||||
created_at: datetime | None = None,
|
||||
) -> MagicMock:
|
||||
"""Create a mock Message object."""
|
||||
message = MagicMock(spec=Message)
|
||||
message.id = message_id
|
||||
message.conversation_id = conversation_id
|
||||
message.query = query
|
||||
message.answer = answer
|
||||
message.created_at = created_at or datetime.now()
|
||||
return message
|
||||
|
||||
|
||||
class TestMessageServicePaginationByFirstId:
|
||||
"""
|
||||
Unit tests for MessageService.pagination_by_first_id method.
|
||||
|
||||
This test suite covers:
|
||||
- Basic pagination with and without first_id
|
||||
- Order handling (asc/desc)
|
||||
- Edge cases (no user, no conversation, invalid first_id)
|
||||
- Has_more flag logic
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def factory(self):
|
||||
"""Provide test data factory."""
|
||||
return TestMessageServiceFactory()
|
||||
|
||||
# Test 01: No user provided
|
||||
def test_pagination_by_first_id_no_user(self, factory):
|
||||
"""Test pagination returns empty result when no user is provided."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_first_id(
|
||||
app_model=app,
|
||||
user=None,
|
||||
conversation_id="conv-001",
|
||||
first_id=None,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, InfiniteScrollPagination)
|
||||
assert result.data == []
|
||||
assert result.limit == 10
|
||||
assert result.has_more is False
|
||||
|
||||
# Test 02: No conversation_id provided
|
||||
def test_pagination_by_first_id_no_conversation(self, factory):
|
||||
"""Test pagination returns empty result when no conversation_id is provided."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_first_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
conversation_id="",
|
||||
first_id=None,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, InfiniteScrollPagination)
|
||||
assert result.data == []
|
||||
assert result.limit == 10
|
||||
assert result.has_more is False
|
||||
|
||||
# Test 03: Basic pagination without first_id (desc order)
|
||||
@patch("services.message_service.db")
|
||||
@patch("services.message_service.ConversationService")
|
||||
def test_pagination_by_first_id_without_first_id_desc(self, mock_conversation_service, mock_db, factory):
|
||||
"""Test basic pagination without first_id in descending order."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
conversation = factory.create_conversation_mock()
|
||||
|
||||
mock_conversation_service.get_conversation.return_value = conversation
|
||||
|
||||
# Create 5 messages
|
||||
messages = [
|
||||
factory.create_message_mock(
|
||||
message_id=f"msg-{i:03d}",
|
||||
created_at=datetime(2024, 1, 1, 12, i),
|
||||
)
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.limit.return_value = mock_query
|
||||
mock_query.all.return_value = messages
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_first_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
conversation_id="conv-001",
|
||||
first_id=None,
|
||||
limit=10,
|
||||
order="desc",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(result.data) == 5
|
||||
assert result.has_more is False
|
||||
assert result.limit == 10
|
||||
# Messages should remain in desc order (not reversed)
|
||||
assert result.data[0].id == "msg-000"
|
||||
|
||||
# Test 04: Basic pagination without first_id (asc order)
|
||||
@patch("services.message_service.db")
|
||||
@patch("services.message_service.ConversationService")
|
||||
def test_pagination_by_first_id_without_first_id_asc(self, mock_conversation_service, mock_db, factory):
|
||||
"""Test basic pagination without first_id in ascending order."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
conversation = factory.create_conversation_mock()
|
||||
|
||||
mock_conversation_service.get_conversation.return_value = conversation
|
||||
|
||||
# Create 5 messages (returned in desc order from DB)
|
||||
messages = [
|
||||
factory.create_message_mock(
|
||||
message_id=f"msg-{i:03d}",
|
||||
created_at=datetime(2024, 1, 1, 12, 4 - i), # Descending timestamps
|
||||
)
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.limit.return_value = mock_query
|
||||
mock_query.all.return_value = messages
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_first_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
conversation_id="conv-001",
|
||||
first_id=None,
|
||||
limit=10,
|
||||
order="asc",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(result.data) == 5
|
||||
assert result.has_more is False
|
||||
# Messages should be reversed to asc order
|
||||
assert result.data[0].id == "msg-004"
|
||||
assert result.data[4].id == "msg-000"
|
||||
|
||||
# Test 05: Pagination with first_id
|
||||
@patch("services.message_service.db")
|
||||
@patch("services.message_service.ConversationService")
|
||||
def test_pagination_by_first_id_with_first_id(self, mock_conversation_service, mock_db, factory):
|
||||
"""Test pagination with first_id to get messages before a specific message."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
conversation = factory.create_conversation_mock()
|
||||
|
||||
mock_conversation_service.get_conversation.return_value = conversation
|
||||
|
||||
first_message = factory.create_message_mock(
|
||||
message_id="msg-005",
|
||||
created_at=datetime(2024, 1, 1, 12, 5),
|
||||
)
|
||||
|
||||
# Messages before first_message
|
||||
history_messages = [
|
||||
factory.create_message_mock(
|
||||
message_id=f"msg-{i:03d}",
|
||||
created_at=datetime(2024, 1, 1, 12, i),
|
||||
)
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
# Setup query mocks
|
||||
mock_query_first = MagicMock()
|
||||
mock_query_history = MagicMock()
|
||||
|
||||
def query_side_effect(*args):
|
||||
if args[0] == Message:
|
||||
# First call returns mock for first_message query
|
||||
if not hasattr(query_side_effect, "call_count"):
|
||||
query_side_effect.call_count = 0
|
||||
query_side_effect.call_count += 1
|
||||
|
||||
if query_side_effect.call_count == 1:
|
||||
return mock_query_first
|
||||
else:
|
||||
return mock_query_history
|
||||
|
||||
mock_db.session.query.side_effect = [mock_query_first, mock_query_history]
|
||||
|
||||
# Setup first message query
|
||||
mock_query_first.where.return_value = mock_query_first
|
||||
mock_query_first.first.return_value = first_message
|
||||
|
||||
# Setup history messages query
|
||||
mock_query_history.where.return_value = mock_query_history
|
||||
mock_query_history.order_by.return_value = mock_query_history
|
||||
mock_query_history.limit.return_value = mock_query_history
|
||||
mock_query_history.all.return_value = history_messages
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_first_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
conversation_id="conv-001",
|
||||
first_id="msg-005",
|
||||
limit=10,
|
||||
order="desc",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(result.data) == 5
|
||||
assert result.has_more is False
|
||||
mock_query_first.where.assert_called_once()
|
||||
mock_query_history.where.assert_called_once()
|
||||
|
||||
# Test 06: First message not found
|
||||
@patch("services.message_service.db")
|
||||
@patch("services.message_service.ConversationService")
|
||||
def test_pagination_by_first_id_first_message_not_exists(self, mock_conversation_service, mock_db, factory):
|
||||
"""Test error handling when first_id doesn't exist."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
conversation = factory.create_conversation_mock()
|
||||
|
||||
mock_conversation_service.get_conversation.return_value = conversation
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.first.return_value = None # Message not found
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(FirstMessageNotExistsError):
|
||||
MessageService.pagination_by_first_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
conversation_id="conv-001",
|
||||
first_id="nonexistent-msg",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
# Test 07: Has_more flag when results exceed limit
|
||||
@patch("services.message_service.db")
|
||||
@patch("services.message_service.ConversationService")
|
||||
def test_pagination_by_first_id_has_more_true(self, mock_conversation_service, mock_db, factory):
|
||||
"""Test has_more flag is True when results exceed limit."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
conversation = factory.create_conversation_mock()
|
||||
|
||||
mock_conversation_service.get_conversation.return_value = conversation
|
||||
|
||||
# Create limit+1 messages (11 messages for limit=10)
|
||||
messages = [
|
||||
factory.create_message_mock(
|
||||
message_id=f"msg-{i:03d}",
|
||||
created_at=datetime(2024, 1, 1, 12, i),
|
||||
)
|
||||
for i in range(11)
|
||||
]
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.limit.return_value = mock_query
|
||||
mock_query.all.return_value = messages
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_first_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
conversation_id="conv-001",
|
||||
first_id=None,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(result.data) == 10 # Last message trimmed
|
||||
assert result.has_more is True
|
||||
assert result.limit == 10
|
||||
|
||||
# Test 08: Empty conversation
|
||||
@patch("services.message_service.db")
|
||||
@patch("services.message_service.ConversationService")
|
||||
def test_pagination_by_first_id_empty_conversation(self, mock_conversation_service, mock_db, factory):
|
||||
"""Test pagination with conversation that has no messages."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
conversation = factory.create_conversation_mock()
|
||||
|
||||
mock_conversation_service.get_conversation.return_value = conversation
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.limit.return_value = mock_query
|
||||
mock_query.all.return_value = []
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_first_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
conversation_id="conv-001",
|
||||
first_id=None,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(result.data) == 0
|
||||
assert result.has_more is False
|
||||
assert result.limit == 10
|
||||
|
||||
|
||||
class TestMessageServicePaginationByLastId:
|
||||
"""
|
||||
Unit tests for MessageService.pagination_by_last_id method.
|
||||
|
||||
This test suite covers:
|
||||
- Basic pagination with and without last_id
|
||||
- Conversation filtering
|
||||
- Include_ids filtering
|
||||
- Edge cases (no user, invalid last_id)
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def factory(self):
|
||||
"""Provide test data factory."""
|
||||
return TestMessageServiceFactory()
|
||||
|
||||
# Test 09: No user provided
|
||||
def test_pagination_by_last_id_no_user(self, factory):
|
||||
"""Test pagination returns empty result when no user is provided."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_last_id(
|
||||
app_model=app,
|
||||
user=None,
|
||||
last_id=None,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, InfiniteScrollPagination)
|
||||
assert result.data == []
|
||||
assert result.limit == 10
|
||||
assert result.has_more is False
|
||||
|
||||
# Test 10: Basic pagination without last_id
|
||||
@patch("services.message_service.db")
|
||||
def test_pagination_by_last_id_without_last_id(self, mock_db, factory):
|
||||
"""Test basic pagination without last_id."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
|
||||
messages = [
|
||||
factory.create_message_mock(
|
||||
message_id=f"msg-{i:03d}",
|
||||
created_at=datetime(2024, 1, 1, 12, i),
|
||||
)
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.limit.return_value = mock_query
|
||||
mock_query.all.return_value = messages
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_last_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
last_id=None,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(result.data) == 5
|
||||
assert result.has_more is False
|
||||
assert result.limit == 10
|
||||
|
||||
# Test 11: Pagination with last_id
|
||||
@patch("services.message_service.db")
|
||||
def test_pagination_by_last_id_with_last_id(self, mock_db, factory):
|
||||
"""Test pagination with last_id to get messages after a specific message."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
|
||||
last_message = factory.create_message_mock(
|
||||
message_id="msg-005",
|
||||
created_at=datetime(2024, 1, 1, 12, 5),
|
||||
)
|
||||
|
||||
# Messages after last_message
|
||||
new_messages = [
|
||||
factory.create_message_mock(
|
||||
message_id=f"msg-{i:03d}",
|
||||
created_at=datetime(2024, 1, 1, 12, i),
|
||||
)
|
||||
for i in range(6, 10)
|
||||
]
|
||||
|
||||
# Setup base query mock that returns itself for chaining
|
||||
mock_base_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_base_query
|
||||
|
||||
# First where() call for last_id lookup
|
||||
mock_query_last = MagicMock()
|
||||
mock_query_last.first.return_value = last_message
|
||||
|
||||
# Second where() call for history messages
|
||||
mock_query_history = MagicMock()
|
||||
mock_query_history.order_by.return_value = mock_query_history
|
||||
mock_query_history.limit.return_value = mock_query_history
|
||||
mock_query_history.all.return_value = new_messages
|
||||
|
||||
# Setup where() to return different mocks on consecutive calls
|
||||
mock_base_query.where.side_effect = [mock_query_last, mock_query_history]
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_last_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
last_id="msg-005",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(result.data) == 4
|
||||
assert result.has_more is False
|
||||
|
||||
# Test 12: Last message not found
|
||||
@patch("services.message_service.db")
|
||||
def test_pagination_by_last_id_last_message_not_exists(self, mock_db, factory):
|
||||
"""Test error handling when last_id doesn't exist."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.first.return_value = None # Message not found
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(LastMessageNotExistsError):
|
||||
MessageService.pagination_by_last_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
last_id="nonexistent-msg",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
# Test 13: Pagination with conversation_id filter
|
||||
@patch("services.message_service.ConversationService")
|
||||
@patch("services.message_service.db")
|
||||
def test_pagination_by_last_id_with_conversation_filter(self, mock_db, mock_conversation_service, factory):
|
||||
"""Test pagination filtered by conversation_id."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
conversation = factory.create_conversation_mock(conversation_id="conv-001")
|
||||
|
||||
mock_conversation_service.get_conversation.return_value = conversation
|
||||
|
||||
messages = [
|
||||
factory.create_message_mock(
|
||||
message_id=f"msg-{i:03d}",
|
||||
conversation_id="conv-001",
|
||||
created_at=datetime(2024, 1, 1, 12, i),
|
||||
)
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.limit.return_value = mock_query
|
||||
mock_query.all.return_value = messages
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_last_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
last_id=None,
|
||||
limit=10,
|
||||
conversation_id="conv-001",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(result.data) == 5
|
||||
assert result.has_more is False
|
||||
# Verify conversation_id was used in query
|
||||
mock_query.where.assert_called()
|
||||
mock_conversation_service.get_conversation.assert_called_once()
|
||||
|
||||
# Test 14: Pagination with include_ids filter
|
||||
@patch("services.message_service.db")
|
||||
def test_pagination_by_last_id_with_include_ids(self, mock_db, factory):
|
||||
"""Test pagination filtered by include_ids."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
|
||||
# Only messages with IDs in include_ids should be returned
|
||||
messages = [
|
||||
factory.create_message_mock(message_id="msg-001"),
|
||||
factory.create_message_mock(message_id="msg-003"),
|
||||
]
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.limit.return_value = mock_query
|
||||
mock_query.all.return_value = messages
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_last_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
last_id=None,
|
||||
limit=10,
|
||||
include_ids=["msg-001", "msg-003"],
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(result.data) == 2
|
||||
assert result.data[0].id == "msg-001"
|
||||
assert result.data[1].id == "msg-003"
|
||||
|
||||
# Test 15: Has_more flag when results exceed limit
|
||||
@patch("services.message_service.db")
|
||||
def test_pagination_by_last_id_has_more_true(self, mock_db, factory):
|
||||
"""Test has_more flag is True when results exceed limit."""
|
||||
# Arrange
|
||||
app = factory.create_app_mock()
|
||||
user = factory.create_end_user_mock()
|
||||
|
||||
# Create limit+1 messages (11 messages for limit=10)
|
||||
messages = [
|
||||
factory.create_message_mock(
|
||||
message_id=f"msg-{i:03d}",
|
||||
created_at=datetime(2024, 1, 1, 12, i),
|
||||
)
|
||||
for i in range(11)
|
||||
]
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.limit.return_value = mock_query
|
||||
mock_query.all.return_value = messages
|
||||
|
||||
# Act
|
||||
result = MessageService.pagination_by_last_id(
|
||||
app_model=app,
|
||||
user=user,
|
||||
last_id=None,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(result.data) == 10 # Last message trimmed
|
||||
assert result.has_more is True
|
||||
assert result.limit == 10
|
||||
1791
api/tests/unit_tests/services/vector_service.py
Normal file
1791
api/tests/unit_tests/services/vector_service.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -123,7 +123,7 @@ services:
|
||||
|
||||
# plugin daemon
|
||||
plugin_daemon:
|
||||
image: langgenius/dify-plugin-daemon:0.4.0-local
|
||||
image: langgenius/dify-plugin-daemon:0.4.1-local
|
||||
restart: always
|
||||
env_file:
|
||||
- ./middleware.env
|
||||
|
||||
@ -187,6 +187,19 @@ const GotoAnything: FC<Props> = ({
|
||||
}, {} as { [key: string]: SearchResult[] }),
|
||||
[searchResults])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCommandsMode)
|
||||
return
|
||||
|
||||
if (!searchResults.length)
|
||||
return
|
||||
|
||||
const currentValueExists = searchResults.some(result => `${result.type}-${result.id}` === cmdVal)
|
||||
|
||||
if (!currentValueExists)
|
||||
setCmdVal(`${searchResults[0].type}-${searchResults[0].id}`)
|
||||
}, [isCommandsMode, searchResults, cmdVal])
|
||||
|
||||
const emptyResult = useMemo(() => {
|
||||
if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
|
||||
return null
|
||||
@ -386,7 +399,7 @@ const GotoAnything: FC<Props> = ({
|
||||
<Command.Item
|
||||
key={`${result.type}-${result.id}`}
|
||||
value={`${result.type}-${result.id}`}
|
||||
className='flex cursor-pointer items-center gap-3 rounded-md p-3 will-change-[background-color] aria-[selected=true]:bg-state-base-hover data-[selected=true]:bg-state-base-hover'
|
||||
className='flex cursor-pointer items-center gap-3 rounded-md p-3 will-change-[background-color] hover:bg-state-base-hover aria-[selected=true]:bg-state-base-hover-alt data-[selected=true]:bg-state-base-hover-alt'
|
||||
onSelect={() => handleNavigate(result)}
|
||||
>
|
||||
{result.icon}
|
||||
|
||||
Reference in New Issue
Block a user