test(api): fix testcontainer tests for FileService

This commit is contained in:
QuantumGhost 2025-09-18 02:57:38 +08:00
parent 8f2b53275c
commit 7fb1a903ae
1 changed files with 123 additions and 94 deletions

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",