mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
test: use real plugin upload ownership and storage adapters (#43209)
This commit is contained in:
1 parent
c797b8e99e
commit
bf62165e22
1 file changed
+89
-49
@@ -1,53 +1,62 @@
|
||||
import io
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from core.tools.tool_file_manager import ToolFileManager
|
||||
from core.workflow.file_reference import build_file_reference
|
||||
from extensions.ext_storage import Storage
|
||||
from extensions.storage.opendal_storage import OpenDALStorage
|
||||
from models.account import TenantAccountJoin, TenantAccountRole
|
||||
from models.tools import ToolFile
|
||||
from repositories.plugin_file_upload_repository import SQLAlchemyPluginFileUploadOwnerRepository
|
||||
from services.errors.file import FileTooLargeError
|
||||
from services.plugin_file_upload_gateway import ToolFilePluginUploadGateway
|
||||
from services.plugin_file_upload_service import (
|
||||
PluginFileUploadAccessDeniedError,
|
||||
PluginFileUploadFiles,
|
||||
PluginFileUploadOwnerQuery,
|
||||
PluginFileUploadResult,
|
||||
PluginFileUploadService,
|
||||
PluginUploadUserFrom,
|
||||
)
|
||||
|
||||
|
||||
def _result() -> PluginFileUploadResult:
|
||||
return PluginFileUploadResult(
|
||||
id="file-id",
|
||||
reference="reference",
|
||||
name="report.pdf",
|
||||
size=4,
|
||||
extension=".pdf",
|
||||
mime_type="application/pdf",
|
||||
preview_url="signed-url",
|
||||
source_url=None,
|
||||
original_url=None,
|
||||
user_id="user-id",
|
||||
tenant_id="tenant-id",
|
||||
conversation_id=None,
|
||||
file_key="file-key",
|
||||
)
|
||||
from tests.unit_tests.model_factories import make_account, make_end_user, make_tenant
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def owners() -> Mock:
|
||||
query = Mock(spec=PluginFileUploadOwnerQuery)
|
||||
query.owner_exists.return_value = True
|
||||
return query
|
||||
def owners(sqlite_session_factory: sessionmaker[Session]) -> SQLAlchemyPluginFileUploadOwnerRepository:
|
||||
with sqlite_session_factory.begin() as session:
|
||||
session.add_all(
|
||||
[
|
||||
make_tenant(tenant_id="tenant-id"),
|
||||
make_account(account_id="user-id"),
|
||||
make_end_user(end_user_id="user-id", tenant_id="tenant-id"),
|
||||
TenantAccountJoin(tenant_id="tenant-id", account_id="user-id", role=TenantAccountRole.NORMAL),
|
||||
]
|
||||
)
|
||||
return SQLAlchemyPluginFileUploadOwnerRepository(session_factory=sqlite_session_factory)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def storage(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Storage:
|
||||
storage = Storage()
|
||||
storage.storage_runner = OpenDALStorage(scheme="fs", root=str(tmp_path))
|
||||
monkeypatch.setattr("core.tools.tool_file_manager.storage", storage)
|
||||
return storage
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def files() -> Mock:
|
||||
gateway = Mock(spec=PluginFileUploadFiles)
|
||||
gateway.store.return_value = _result()
|
||||
return gateway
|
||||
def files(mocker: MockerFixture) -> ToolFilePluginUploadGateway:
|
||||
mocker.patch("services.plugin_file_upload_gateway.sign_tool_file", return_value="signed-url")
|
||||
tool_files = ToolFileManager()
|
||||
return ToolFilePluginUploadGateway(tool_files=tool_files)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service(owners: Mock, files: Mock) -> PluginFileUploadService:
|
||||
def service(
|
||||
owners: SQLAlchemyPluginFileUploadOwnerRepository, files: ToolFilePluginUploadGateway
|
||||
) -> PluginFileUploadService:
|
||||
return PluginFileUploadService(owners=owners, files=files)
|
||||
|
||||
|
||||
@@ -77,16 +86,38 @@ def _upload(
|
||||
@pytest.mark.parametrize("user_from", [None, "end-user", "account"])
|
||||
def test_valid_ticket_authorizes_owner_then_stores_file(
|
||||
service: PluginFileUploadService,
|
||||
owners: Mock,
|
||||
files: Mock,
|
||||
owners: SQLAlchemyPluginFileUploadOwnerRepository,
|
||||
files: ToolFilePluginUploadGateway,
|
||||
user_from: PluginUploadUserFrom,
|
||||
storage: Storage,
|
||||
sqlite_session: Session,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
store = mocker.spy(files, "store")
|
||||
owner_exists = mocker.spy(owners, "owner_exists")
|
||||
stream = io.BytesIO(b"data")
|
||||
|
||||
with patch("services.plugin_file_upload_service.verify_plugin_file_signature", return_value=True) as verify:
|
||||
result = _upload(service, stream=stream, user_from=user_from)
|
||||
|
||||
assert result == _result()
|
||||
persisted = sqlite_session.get(ToolFile, result.id)
|
||||
assert persisted is not None
|
||||
assert result == PluginFileUploadResult(
|
||||
id=persisted.id,
|
||||
reference=build_file_reference(record_id=persisted.id),
|
||||
name="report.pdf",
|
||||
size=4,
|
||||
extension=".pdf",
|
||||
mime_type="application/pdf",
|
||||
preview_url="signed-url",
|
||||
source_url=None,
|
||||
original_url=None,
|
||||
user_id="user-id",
|
||||
tenant_id="tenant-id",
|
||||
conversation_id="conversation-id",
|
||||
file_key=persisted.file_key,
|
||||
)
|
||||
assert storage.load_once(persisted.file_key) == b"data"
|
||||
verify.assert_called_once_with(
|
||||
filename="report.pdf",
|
||||
mimetype="application/pdf",
|
||||
@@ -99,12 +130,12 @@ def test_valid_ticket_authorizes_owner_then_stores_file(
|
||||
sign="signature",
|
||||
max_size=None,
|
||||
)
|
||||
owners.owner_exists.assert_called_once_with(
|
||||
owner_exists.assert_called_once_with(
|
||||
tenant_id="tenant-id",
|
||||
user_id="user-id",
|
||||
user_from=user_from,
|
||||
)
|
||||
files.store.assert_called_once_with(
|
||||
store.assert_called_once_with(
|
||||
user_id="user-id",
|
||||
tenant_id="tenant-id",
|
||||
conversation_id="conversation-id",
|
||||
@@ -116,40 +147,45 @@ def test_valid_ticket_authorizes_owner_then_stores_file(
|
||||
|
||||
def test_invalid_signature_has_no_database_or_stream_side_effect(
|
||||
service: PluginFileUploadService,
|
||||
owners: Mock,
|
||||
files: Mock,
|
||||
owners: SQLAlchemyPluginFileUploadOwnerRepository,
|
||||
files: ToolFilePluginUploadGateway,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
store = mocker.spy(files, "store")
|
||||
owner_exists = mocker.spy(owners, "owner_exists")
|
||||
stream = Mock()
|
||||
|
||||
with patch("services.plugin_file_upload_service.verify_plugin_file_signature", return_value=False):
|
||||
with pytest.raises(PluginFileUploadAccessDeniedError):
|
||||
_upload(service, stream=stream)
|
||||
|
||||
owners.owner_exists.assert_not_called()
|
||||
owner_exists.assert_not_called()
|
||||
stream.read.assert_not_called()
|
||||
files.store.assert_not_called()
|
||||
store.assert_not_called()
|
||||
|
||||
|
||||
def test_unknown_owner_is_rejected_before_reading_or_storing(
|
||||
service: PluginFileUploadService,
|
||||
owners: Mock,
|
||||
files: Mock,
|
||||
files: ToolFilePluginUploadGateway,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
store = mocker.spy(files, "store")
|
||||
stream = Mock()
|
||||
owners.owner_exists.return_value = False
|
||||
|
||||
with patch("services.plugin_file_upload_service.verify_plugin_file_signature", return_value=True):
|
||||
with pytest.raises(PluginFileUploadAccessDeniedError):
|
||||
_upload(service, stream=stream, user_from="account")
|
||||
_upload(service, stream=stream, user_id="unknown-user", user_from="account")
|
||||
|
||||
stream.read.assert_not_called()
|
||||
files.store.assert_not_called()
|
||||
store.assert_not_called()
|
||||
|
||||
|
||||
def test_signed_size_reads_only_one_byte_beyond_the_limit(
|
||||
service: PluginFileUploadService,
|
||||
files: Mock,
|
||||
files: ToolFilePluginUploadGateway,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
store = mocker.spy(files, "store")
|
||||
stream = Mock()
|
||||
stream.read.return_value = b"data"
|
||||
|
||||
@@ -157,13 +193,15 @@ def test_signed_size_reads_only_one_byte_beyond_the_limit(
|
||||
_upload(service, stream=stream, max_size=4)
|
||||
|
||||
stream.read.assert_called_once_with(5)
|
||||
assert files.store.call_args.kwargs["content"] == b"data"
|
||||
assert store.call_args.kwargs["content"] == b"data"
|
||||
|
||||
|
||||
def test_zero_signed_size_accepts_an_empty_file(
|
||||
service: PluginFileUploadService,
|
||||
files: Mock,
|
||||
files: ToolFilePluginUploadGateway,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
store = mocker.spy(files, "store")
|
||||
stream = Mock()
|
||||
stream.read.return_value = b""
|
||||
|
||||
@@ -171,7 +209,7 @@ def test_zero_signed_size_accepts_an_empty_file(
|
||||
_upload(service, stream=stream, max_size=0)
|
||||
|
||||
stream.read.assert_called_once_with(1)
|
||||
assert files.store.call_args.kwargs["content"] == b""
|
||||
assert store.call_args.kwargs["content"] == b""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -183,10 +221,12 @@ def test_zero_signed_size_accepts_an_empty_file(
|
||||
)
|
||||
def test_signed_size_rejects_oversized_content_before_storage(
|
||||
service: PluginFileUploadService,
|
||||
files: Mock,
|
||||
files: ToolFilePluginUploadGateway,
|
||||
max_size: int,
|
||||
content: bytes,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
store = mocker.spy(files, "store")
|
||||
stream = Mock()
|
||||
stream.read.return_value = content
|
||||
|
||||
@@ -195,4 +235,4 @@ def test_signed_size_rejects_oversized_content_before_storage(
|
||||
_upload(service, stream=stream, max_size=max_size)
|
||||
|
||||
stream.read.assert_called_once_with(max_size + 1)
|
||||
files.store.assert_not_called()
|
||||
store.assert_not_called()
|
||||
Reference in new issue
Block a user