test: use real plugin upload ownership and storage adapters (#43209)

This commit is contained in:
Asuka Minato authored and GitHub committed 2026-10-02 12:21:08 +00:00
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()