Compare commits

..

21 Commits

Author SHA1 Message Date
5e4524de8b refactor: simplify spread usage in various components (#25886) 2025-09-18 12:15:26 +08:00
a984549f01 refactor: simplify array spread usage in various components 2025-09-18 12:10:17 +08:00
afe61c486b del website test (#25883) 2025-09-18 11:36:14 +08:00
b588985074 del website test 2025-09-18 11:33:28 +08:00
2539803337 del website test (#25882) 2025-09-18 11:26:11 +08:00
a570925130 del website test 2025-09-18 11:24:56 +08:00
aa3c8f0657 test(api): fix broken testcontainer tests (#25869) 2025-09-18 11:21:43 +08:00
f1e2ef3762 fix version missing (#25879) 2025-09-18 11:14:58 +08:00
195c52be9b fix version missing 2025-09-18 11:14:04 +08:00
c0a3fc1412 fix version missing 2025-09-18 11:09:02 +08:00
092ced7c66 fix: Fix dependency version display (#25856) 2025-09-18 11:07:02 +08:00
18027b530a Merge branch 'feat/rag-2' into fix/dependency-version 2025-09-18 11:06:26 +08:00
0d9becd060 fix version missing 2025-09-18 10:51:47 +08:00
5956375cec fix: ensure output_schema properties are checked before accessing them in strategy detail, use config, and tool default components 2025-09-18 10:11:15 +08:00
a678dd1a32 WIP: test(api): fix broken tests for WebsiteService 2025-09-18 03:00:57 +08:00
fda15ef018 test(api): Fix testscontainer tests for WorkflowDraftVariableService 2025-09-18 02:58:07 +08:00
7fb1a903ae test(api): fix testcontainer tests for FileService 2025-09-18 02:57:38 +08:00
8f2b53275c fix(api): Remove postgresql_nulls_not_distinct=False in unique indexes
This option would generate upgrading / table creating sql with `NULLS
DISTINCT` part and causing syntax error while running testcontainer
tests.

The `NULLS DISTINCT` syntax is only supported by PG 15+.
2025-09-18 02:55:19 +08:00
87fe8c8a2f fix(api): fix line too long (#25868) 2025-09-18 00:01:04 +08:00
370127b87a fix(api): fix line too long 2025-09-17 23:59:14 +08:00
55f96a4266 refactor(fetch): convert baseOptions to a function for dynamic request options 2025-09-17 22:37:04 +08:00
18 changed files with 166 additions and 1564 deletions

View File

@ -178,6 +178,7 @@ class PluginDependency(BaseModel):
class Marketplace(BaseModel):
marketplace_plugin_unique_identifier: str
version: str | None = None
@property
def plugin_unique_identifier(self) -> str:
@ -185,6 +186,7 @@ class PluginDependency(BaseModel):
class Package(BaseModel):
plugin_unique_identifier: str
version: str | None = None
type: Type
value: Github | Marketplace | Package

View File

@ -156,7 +156,7 @@ def upgrade():
sa.Column('type', sa.String(20), nullable=False),
sa.Column('file_id', models.types.StringUUID(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('workflow_node_execution_offload_pkey')),
sa.UniqueConstraint('node_execution_id', 'type', name=op.f('workflow_node_execution_offload_node_execution_id_key'), postgresql_nulls_not_distinct=False)
sa.UniqueConstraint('node_execution_id', 'type', name=op.f('workflow_node_execution_offload_node_execution_id_key'))
)
with op.batch_alter_table('datasets', schema=None) as batch_op:
batch_op.add_column(sa.Column('keyword_number', sa.Integer(), server_default=sa.text('10'), nullable=True))

View File

@ -890,12 +890,18 @@ class WorkflowNodeExecutionModel(Base): # This model is expected to have `offlo
class WorkflowNodeExecutionOffload(Base):
__tablename__ = "workflow_node_execution_offload"
__table_args__ = (
# PostgreSQL 14 treats NULL values as distinct in unique constraints by default,
# allowing multiple records with NULL values for the same column combination.
#
# This behavior allows us to have multiple records with NULL node_execution_id,
# simplifying garbage collection process.
UniqueConstraint(
"node_execution_id",
"type",
# Treat `NULL` as distinct for this unique index, so
# we can have mutitple records with `NULL` node_exeution_id, simplify garbage collection process.
postgresql_nulls_not_distinct=False,
# Note: PostgreSQL 15+ supports explicit `nulls distinct` behavior through
# `postgresql_nulls_not_distinct=False`, which would make our intention clearer.
# We rely on PostgreSQL's default behavior of treating NULLs as distinct values.
# postgresql_nulls_not_distinct=False,
),
)
_HASH_COL_SIZE = 64

View File

@ -1,9 +1,14 @@
import re
from configs import dify_config
from core.helper import marketplace
from core.plugin.entities.plugin import PluginDependency, PluginInstallationSource
from core.plugin.impl.plugin import PluginInstaller
from models.provider_ids import ModelProviderID, ToolProviderID
# Compile regex pattern for version extraction at module level for better performance
_VERSION_REGEX = re.compile(r":(?P<version>[0-9]+(?:\.[0-9]+){2}(?:[+-][0-9A-Za-z.-]+)?)(?:@|$)")
class DependenciesAnalysisService:
@classmethod
@ -49,6 +54,13 @@ class DependenciesAnalysisService:
for dependency in dependencies:
unique_identifier = dependency.value.plugin_unique_identifier
if unique_identifier in missing_plugin_unique_identifiers:
# Extract version for Marketplace dependencies
if dependency.type == PluginDependency.Type.Marketplace:
version_match = _VERSION_REGEX.search(unique_identifier)
if version_match:
dependency.value.version = version_match.group("version")
# Create and append the dependency (same for all types)
leaked_dependencies.append(
PluginDependency(
type=dependency.type,

View File

@ -4,6 +4,7 @@ from unittest.mock import create_autospec, patch
import pytest
from faker import Faker
from sqlalchemy import Engine
from werkzeug.exceptions import NotFound
from configs import dify_config
@ -17,6 +18,12 @@ from services.file_service import FileService
class TestFileService:
"""Integration tests for FileService using testcontainers."""
@pytest.fixture
def engine(self, db_session_with_containers):
bind = db_session_with_containers.get_bind()
assert isinstance(bind, Engine)
return bind
@pytest.fixture
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
@ -156,7 +163,7 @@ class TestFileService:
return upload_file
# Test upload_file method
def test_upload_file_success(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_success(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test successful file upload with valid parameters.
"""
@ -167,7 +174,7 @@ class TestFileService:
content = b"test file content"
mimetype = "application/pdf"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -187,13 +194,9 @@ class TestFileService:
# Verify storage was called
mock_external_service_dependencies["storage"].save.assert_called_once()
# Verify database state
from extensions.ext_database import db
db.session.refresh(upload_file)
assert upload_file.id is not None
def test_upload_file_with_end_user(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_with_end_user(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test file upload with end user instead of account.
"""
@ -204,7 +207,7 @@ class TestFileService:
content = b"test image content"
mimetype = "image/jpeg"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -215,7 +218,9 @@ class TestFileService:
assert upload_file.created_by == end_user.id
assert upload_file.created_by_role == CreatorUserRole.END_USER.value
def test_upload_file_with_datasets_source(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_with_datasets_source(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload with datasets source parameter.
"""
@ -226,7 +231,7 @@ class TestFileService:
content = b"test file content"
mimetype = "application/pdf"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -239,7 +244,7 @@ class TestFileService:
assert upload_file.source_url == "https://example.com/source"
def test_upload_file_invalid_filename_characters(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload with invalid filename characters.
@ -252,14 +257,16 @@ class TestFileService:
mimetype = "text/plain"
with pytest.raises(ValueError, match="Filename contains invalid characters"):
FileService.upload_file(
FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
user=account,
)
def test_upload_file_filename_too_long(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_filename_too_long(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload with filename that exceeds length limit.
"""
@ -272,7 +279,7 @@ class TestFileService:
content = b"test content"
mimetype = "text/plain"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -288,7 +295,7 @@ class TestFileService:
assert len(base_name) <= 200
def test_upload_file_datasets_unsupported_type(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload for datasets with unsupported file type.
@ -301,7 +308,7 @@ class TestFileService:
mimetype = "image/jpeg"
with pytest.raises(UnsupportedFileTypeError):
FileService.upload_file(
FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -309,7 +316,7 @@ class TestFileService:
source="datasets",
)
def test_upload_file_too_large(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_too_large(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test file upload with file size exceeding limit.
"""
@ -322,7 +329,7 @@ class TestFileService:
mimetype = "image/jpeg"
with pytest.raises(FileTooLargeError):
FileService.upload_file(
FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -331,7 +338,7 @@ class TestFileService:
# Test is_file_size_within_limit method
def test_is_file_size_within_limit_image_success(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for image files within limit.
@ -339,12 +346,12 @@ class TestFileService:
extension = "jpg"
file_size = dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT * 1024 * 1024 # Exactly at limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
def test_is_file_size_within_limit_video_success(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for video files within limit.
@ -352,12 +359,12 @@ class TestFileService:
extension = "mp4"
file_size = dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT * 1024 * 1024 # Exactly at limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
def test_is_file_size_within_limit_audio_success(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for audio files within limit.
@ -365,12 +372,12 @@ class TestFileService:
extension = "mp3"
file_size = dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT * 1024 * 1024 # Exactly at limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
def test_is_file_size_within_limit_document_success(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for document files within limit.
@ -378,12 +385,12 @@ class TestFileService:
extension = "pdf"
file_size = dify_config.UPLOAD_FILE_SIZE_LIMIT * 1024 * 1024 # Exactly at limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
def test_is_file_size_within_limit_image_exceeded(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for image files exceeding limit.
@ -391,12 +398,12 @@ class TestFileService:
extension = "jpg"
file_size = dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT * 1024 * 1024 + 1 # Exceeds limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is False
def test_is_file_size_within_limit_unknown_extension(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for unknown file extension.
@ -404,12 +411,12 @@ class TestFileService:
extension = "xyz"
file_size = dify_config.UPLOAD_FILE_SIZE_LIMIT * 1024 * 1024 # Uses default limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
# Test upload_text method
def test_upload_text_success(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_text_success(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test successful text upload.
"""
@ -422,21 +429,25 @@ class TestFileService:
mock_current_user.current_tenant_id = str(fake.uuid4())
mock_current_user.id = str(fake.uuid4())
with patch("services.file_service.current_user", mock_current_user):
upload_file = FileService.upload_text(text=text, text_name=text_name)
upload_file = FileService(engine).upload_text(
text=text,
text_name=text_name,
user_id=mock_current_user.id,
tenant_id=mock_current_user.current_tenant_id,
)
assert upload_file is not None
assert upload_file.name == text_name
assert upload_file.size == len(text)
assert upload_file.extension == "txt"
assert upload_file.mime_type == "text/plain"
assert upload_file.used is True
assert upload_file.used_by == mock_current_user.id
assert upload_file is not None
assert upload_file.name == text_name
assert upload_file.size == len(text)
assert upload_file.extension == "txt"
assert upload_file.mime_type == "text/plain"
assert upload_file.used is True
assert upload_file.used_by == mock_current_user.id
# Verify storage was called
mock_external_service_dependencies["storage"].save.assert_called_once()
# Verify storage was called
mock_external_service_dependencies["storage"].save.assert_called_once()
def test_upload_text_name_too_long(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_text_name_too_long(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test text upload with name that exceeds length limit.
"""
@ -449,15 +460,19 @@ class TestFileService:
mock_current_user.current_tenant_id = str(fake.uuid4())
mock_current_user.id = str(fake.uuid4())
with patch("services.file_service.current_user", mock_current_user):
upload_file = FileService.upload_text(text=text, text_name=long_name)
upload_file = FileService(engine).upload_text(
text=text,
text_name=long_name,
user_id=mock_current_user.id,
tenant_id=mock_current_user.current_tenant_id,
)
# Verify name was truncated
assert len(upload_file.name) <= 200
assert upload_file.name == "a" * 200
# Verify name was truncated
assert len(upload_file.name) <= 200
assert upload_file.name == "a" * 200
# Test get_file_preview method
def test_get_file_preview_success(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_file_preview_success(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test successful file preview generation.
"""
@ -473,12 +488,14 @@ class TestFileService:
db.session.commit()
result = FileService.get_file_preview(file_id=upload_file.id)
result = FileService(engine).get_file_preview(file_id=upload_file.id)
assert result == "extracted text content"
mock_external_service_dependencies["extract_processor"].load_from_upload_file.assert_called_once()
def test_get_file_preview_file_not_found(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_file_preview_file_not_found(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file preview with non-existent file.
"""
@ -486,10 +503,10 @@ class TestFileService:
non_existent_id = str(fake.uuid4())
with pytest.raises(NotFound, match="File not found"):
FileService.get_file_preview(file_id=non_existent_id)
FileService(engine).get_file_preview(file_id=non_existent_id)
def test_get_file_preview_unsupported_file_type(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file preview with unsupported file type.
@ -507,9 +524,11 @@ class TestFileService:
db.session.commit()
with pytest.raises(UnsupportedFileTypeError):
FileService.get_file_preview(file_id=upload_file.id)
FileService(engine).get_file_preview(file_id=upload_file.id)
def test_get_file_preview_text_truncation(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_file_preview_text_truncation(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file preview with text that exceeds preview limit.
"""
@ -529,13 +548,13 @@ class TestFileService:
long_text = "x" * 5000 # Longer than PREVIEW_WORDS_LIMIT
mock_external_service_dependencies["extract_processor"].load_from_upload_file.return_value = long_text
result = FileService.get_file_preview(file_id=upload_file.id)
result = FileService(engine).get_file_preview(file_id=upload_file.id)
assert len(result) == 3000 # PREVIEW_WORDS_LIMIT
assert result == "x" * 3000
# Test get_image_preview method
def test_get_image_preview_success(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_image_preview_success(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test successful image preview generation.
"""
@ -555,7 +574,7 @@ class TestFileService:
nonce = "test_nonce"
sign = "test_signature"
generator, mime_type = FileService.get_image_preview(
generator, mime_type = FileService(engine).get_image_preview(
file_id=upload_file.id,
timestamp=timestamp,
nonce=nonce,
@ -566,7 +585,9 @@ class TestFileService:
assert mime_type == upload_file.mime_type
mock_external_service_dependencies["file_helpers"].verify_image_signature.assert_called_once()
def test_get_image_preview_invalid_signature(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_image_preview_invalid_signature(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test image preview with invalid signature.
"""
@ -584,14 +605,16 @@ class TestFileService:
sign = "invalid_signature"
with pytest.raises(NotFound, match="File not found or signature is invalid"):
FileService.get_image_preview(
FileService(engine).get_image_preview(
file_id=upload_file.id,
timestamp=timestamp,
nonce=nonce,
sign=sign,
)
def test_get_image_preview_file_not_found(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_image_preview_file_not_found(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test image preview with non-existent file.
"""
@ -603,7 +626,7 @@ class TestFileService:
sign = "test_signature"
with pytest.raises(NotFound, match="File not found or signature is invalid"):
FileService.get_image_preview(
FileService(engine).get_image_preview(
file_id=non_existent_id,
timestamp=timestamp,
nonce=nonce,
@ -611,7 +634,7 @@ class TestFileService:
)
def test_get_image_preview_unsupported_file_type(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test image preview with non-image file type.
@ -633,7 +656,7 @@ class TestFileService:
sign = "test_signature"
with pytest.raises(UnsupportedFileTypeError):
FileService.get_image_preview(
FileService(engine).get_image_preview(
file_id=upload_file.id,
timestamp=timestamp,
nonce=nonce,
@ -642,7 +665,7 @@ class TestFileService:
# Test get_file_generator_by_file_id method
def test_get_file_generator_by_file_id_success(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test successful file generator retrieval.
@ -657,7 +680,7 @@ class TestFileService:
nonce = "test_nonce"
sign = "test_signature"
generator, file_obj = FileService.get_file_generator_by_file_id(
generator, file_obj = FileService(engine).get_file_generator_by_file_id(
file_id=upload_file.id,
timestamp=timestamp,
nonce=nonce,
@ -665,11 +688,11 @@ class TestFileService:
)
assert generator is not None
assert file_obj == upload_file
assert file_obj.id == upload_file.id
mock_external_service_dependencies["file_helpers"].verify_file_signature.assert_called_once()
def test_get_file_generator_by_file_id_invalid_signature(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file generator retrieval with invalid signature.
@ -688,7 +711,7 @@ class TestFileService:
sign = "invalid_signature"
with pytest.raises(NotFound, match="File not found or signature is invalid"):
FileService.get_file_generator_by_file_id(
FileService(engine).get_file_generator_by_file_id(
file_id=upload_file.id,
timestamp=timestamp,
nonce=nonce,
@ -696,7 +719,7 @@ class TestFileService:
)
def test_get_file_generator_by_file_id_file_not_found(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file generator retrieval with non-existent file.
@ -709,7 +732,7 @@ class TestFileService:
sign = "test_signature"
with pytest.raises(NotFound, match="File not found or signature is invalid"):
FileService.get_file_generator_by_file_id(
FileService(engine).get_file_generator_by_file_id(
file_id=non_existent_id,
timestamp=timestamp,
nonce=nonce,
@ -717,7 +740,9 @@ class TestFileService:
)
# Test get_public_image_preview method
def test_get_public_image_preview_success(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_public_image_preview_success(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test successful public image preview generation.
"""
@ -733,14 +758,14 @@ class TestFileService:
db.session.commit()
generator, mime_type = FileService.get_public_image_preview(file_id=upload_file.id)
generator, mime_type = FileService(engine).get_public_image_preview(file_id=upload_file.id)
assert generator is not None
assert mime_type == upload_file.mime_type
mock_external_service_dependencies["storage"].load.assert_called_once()
def test_get_public_image_preview_file_not_found(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test public image preview with non-existent file.
@ -749,10 +774,10 @@ class TestFileService:
non_existent_id = str(fake.uuid4())
with pytest.raises(NotFound, match="File not found or signature is invalid"):
FileService.get_public_image_preview(file_id=non_existent_id)
FileService(engine).get_public_image_preview(file_id=non_existent_id)
def test_get_public_image_preview_unsupported_file_type(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test public image preview with non-image file type.
@ -770,10 +795,10 @@ class TestFileService:
db.session.commit()
with pytest.raises(UnsupportedFileTypeError):
FileService.get_public_image_preview(file_id=upload_file.id)
FileService(engine).get_public_image_preview(file_id=upload_file.id)
# Test edge cases and boundary conditions
def test_upload_file_empty_content(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_empty_content(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test file upload with empty content.
"""
@ -784,7 +809,7 @@ class TestFileService:
content = b""
mimetype = "text/plain"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -795,7 +820,7 @@ class TestFileService:
assert upload_file.size == 0
def test_upload_file_special_characters_in_name(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload with special characters in filename (but valid ones).
@ -807,7 +832,7 @@ class TestFileService:
content = b"test content"
mimetype = "text/plain"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -818,7 +843,7 @@ class TestFileService:
assert upload_file.name == filename
def test_upload_file_different_case_extensions(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload with different case extensions.
@ -830,7 +855,7 @@ class TestFileService:
content = b"test content"
mimetype = "application/pdf"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -840,7 +865,7 @@ class TestFileService:
assert upload_file is not None
assert upload_file.extension == "pdf" # Should be converted to lowercase
def test_upload_text_empty_text(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_text_empty_text(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test text upload with empty text.
"""
@ -853,13 +878,17 @@ class TestFileService:
mock_current_user.current_tenant_id = str(fake.uuid4())
mock_current_user.id = str(fake.uuid4())
with patch("services.file_service.current_user", mock_current_user):
upload_file = FileService.upload_text(text=text, text_name=text_name)
upload_file = FileService(engine).upload_text(
text=text,
text_name=text_name,
user_id=mock_current_user.id,
tenant_id=mock_current_user.current_tenant_id,
)
assert upload_file is not None
assert upload_file.size == 0
assert upload_file is not None
assert upload_file.size == 0
def test_file_size_limits_edge_cases(self, db_session_with_containers, mock_external_service_dependencies):
def test_file_size_limits_edge_cases(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test file size limits with edge case values.
"""
@ -871,15 +900,15 @@ class TestFileService:
("pdf", dify_config.UPLOAD_FILE_SIZE_LIMIT),
]:
file_size = limit_config * 1024 * 1024
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
# Test one byte over limit
file_size = limit_config * 1024 * 1024 + 1
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is False
def test_upload_file_with_source_url(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_with_source_url(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test file upload with source URL that gets overridden by signed URL.
"""
@ -891,7 +920,7 @@ class TestFileService:
mimetype = "application/pdf"
source_url = "https://original-source.com/file.pdf"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -904,7 +933,7 @@ class TestFileService:
# The signed URL should only be set when source_url is empty
# Let's test that scenario
upload_file2 = FileService.upload_file(
upload_file2 = FileService(engine).upload_file(
filename="test2.pdf",
content=b"test content 2",
mimetype="application/pdf",

View File

@ -108,6 +108,7 @@ class TestWorkflowDraftVariableService:
created_by=app.created_by,
environment_variables=[],
conversation_variables=[],
rag_pipeline_variables=[],
)
from extensions.ext_database import db

View File

@ -149,7 +149,7 @@ export const PortalToFollowElemTrigger = (
context.getReferenceProps({
ref,
...props,
...(children.props || {}),
...children.props,
'data-state': context.open ? 'open' : 'closed',
} as React.HTMLProps<HTMLElement>),
)

View File

@ -91,7 +91,7 @@ const PageSelector = ({
if (current.expand) {
current.expand = false
newDataList = [...dataList.filter(item => !descendantsIds.includes(item.page_id))]
newDataList = dataList.filter(item => !descendantsIds.includes(item.page_id))
}
else {
current.expand = true
@ -110,7 +110,7 @@ const PageSelector = ({
}, [dataList, listMapWithChildrenAndDescendants, pagesMap])
const handleCheck = useCallback((index: number) => {
const copyValue = new Set([...checkedIds])
const copyValue = new Set(checkedIds)
const current = currentDataList[index]
const pageId = current.page_id
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
@ -138,7 +138,7 @@ const PageSelector = ({
}
}
onSelect(new Set([...copyValue]))
onSelect(new Set(copyValue))
}, [currentDataList, isMultipleChoice, listMapWithChildrenAndDescendants, onSelect, searchValue, checkedIds])
const handlePreview = useCallback((index: number) => {

View File

@ -59,7 +59,7 @@ export const useMarketplaceAllPlugins = (providers: any[], searchText: string) =
}, [queryPlugins, queryPluginsWithDebounced, searchText, exclude])
const allPlugins = useMemo(() => {
const allPlugins = [...collectionPlugins.filter(plugin => !exclude.includes(plugin.plugin_id))]
const allPlugins = collectionPlugins.filter(plugin => !exclude.includes(plugin.plugin_id))
if (plugins?.length) {
for (let i = 0; i < plugins.length; i++) {

View File

@ -43,7 +43,7 @@ const StrategyDetail: FC<Props> = ({
const outputSchema = useMemo(() => {
const res: any[] = []
if (!detail.output_schema)
if (!detail.output_schema || !detail.output_schema.properties)
return []
Object.keys(detail.output_schema.properties).forEach((outputKey) => {
const output = detail.output_schema.properties[outputKey]

View File

@ -765,7 +765,7 @@ export const useNodesInteractions = () => {
nodesWithSameType.length > 0
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
: defaultValue.title,
...(toolDefaultValue || {}),
...toolDefaultValue,
selected: true,
_showAddVariablePopup:
(nodeType === BlockEnum.VariableAssigner
@ -866,7 +866,7 @@ export const useNodesInteractions = () => {
const nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
[...(newEdge ? [{ type: 'add', edge: newEdge }] : [])],
(newEdge ? [{ type: 'add', edge: newEdge }] : []),
nodes,
)
const newNodes = produce(nodes, (draft: Node[]) => {
@ -1331,7 +1331,7 @@ export const useNodesInteractions = () => {
nodesWithSameType.length > 0
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
: defaultValue.title,
...(toolDefaultValue || {}),
...toolDefaultValue,
_connectedSourceHandleIds: [],
_connectedTargetHandleIds: [],
selected: currentNode.data.selected,

View File

@ -181,7 +181,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
const outputSchema = useMemo(() => {
const res: any[] = []
if (!inputs.output_schema)
if (!inputs.output_schema || !inputs.output_schema.properties)
return []
Object.keys(inputs.output_schema.properties).forEach((outputKey) => {
const output = inputs.output_schema.properties[outputKey]

View File

@ -28,7 +28,7 @@ export const useReplaceDataSourceNode = (id: string) => {
const { newNode } = generateNewNode({
data: {
...(defaultValue as any),
...(toolDefaultValue || {}),
...toolDefaultValue,
},
position: {
x: emptyNode.position.x,

View File

@ -89,7 +89,7 @@ const nodeDefault: NodeDefault<ToolNodeType> = {
const currTool = currCollection?.tools.find(tool => tool.name === payload.tool_name)
const output_schema = currTool?.output_schema
let res: any[] = []
if (!output_schema) {
if (!output_schema || !output_schema.properties) {
res = TOOL_OUTPUT_STRUCT
}
else {

View File

@ -65,7 +65,7 @@ const AddBlock = ({
data: {
...(defaultValue as any),
title: nodesWithSameType.length > 0 ? `${defaultValue.title} ${nodesWithSameType.length + 1}` : defaultValue.title,
...(toolDefaultValue || {}),
...toolDefaultValue,
_isCandidate: true,
},
position: {

View File

@ -23,7 +23,7 @@ import type {
} from '@/types/workflow'
import { removeAccessToken } from '@/app/components/share/utils'
import type { FetchOptionType, ResponseError } from './fetch'
import { ContentType, base, baseOptions, getAccessToken } from './fetch'
import { ContentType, base, getAccessToken, getBaseOptions } from './fetch'
import { asyncRunSafe } from '@/utils'
import type {
DataSourceNodeCompletedResponse,
@ -400,6 +400,7 @@ export const ssePost = async (
const token = localStorage.getItem('console_token')
const baseOptions = getBaseOptions()
const options = Object.assign({}, baseOptions, {
method: 'POST',
signal: abortController.signal,

View File

@ -111,7 +111,7 @@ const baseClient = ky.create({
timeout: TIME_OUT,
})
export const baseOptions: RequestInit = {
export const getBaseOptions = (): RequestInit => ({
method: 'GET',
mode: 'cors',
credentials: 'include', // always send cookies、HTTP Basic authentication.
@ -119,9 +119,10 @@ export const baseOptions: RequestInit = {
'Content-Type': ContentType.json,
}),
redirect: 'follow',
}
})
async function base<T>(url: string, options: FetchOptionType = {}, otherOptions: IOtherOptions = {}): Promise<T> {
const baseOptions = getBaseOptions()
const { params, body, headers, ...init } = Object.assign({}, baseOptions, options)
const {
isPublicAPI = false,