mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
test: persist account initialization and avatar ownership fixtures (#43199)
This commit is contained in:
1 parent
8055521b49
commit
14cd89ded5
2 files changed
+96
-40
No files matched your search
@@ -1,11 +1,12 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from machinery.context import RequestContext
|
||||
from services.account_avatar_file_gateway import SQLAlchemyAccountAvatarFileGateway
|
||||
from services.account_avatar_service import AccountAvatarService
|
||||
from services.account_errors import AvatarFileNotFoundError
|
||||
from services.account_ports import AccountAvatarFileGateway
|
||||
from tests.unit_tests.model_factories import make_upload_file
|
||||
|
||||
|
||||
def _context() -> RequestContext:
|
||||
@@ -17,30 +18,54 @@ def _context() -> RequestContext:
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_passes_through_external_avatar_without_calling_gateway() -> None:
|
||||
files = Mock(spec=AccountAvatarFileGateway)
|
||||
@pytest.fixture
|
||||
def files(sqlite_session_factory: sessionmaker[Session]) -> SQLAlchemyAccountAvatarFileGateway:
|
||||
return SQLAlchemyAccountAvatarFileGateway(session_factory=sqlite_session_factory)
|
||||
|
||||
|
||||
def test_resolve_passes_through_external_avatar_without_calling_gateway(
|
||||
files: SQLAlchemyAccountAvatarFileGateway,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
get_owned_url = mocker.spy(files, "get_owned_signed_url")
|
||||
service = AccountAvatarService(files=files)
|
||||
|
||||
result = service.resolve(_context(), "https://cdn.example/avatar.png")
|
||||
|
||||
assert result == "https://cdn.example/avatar.png"
|
||||
files.get_owned_signed_url.assert_not_called()
|
||||
get_owned_url.assert_not_called()
|
||||
|
||||
|
||||
def test_resolve_returns_owned_signed_url() -> None:
|
||||
files = Mock(spec=AccountAvatarFileGateway)
|
||||
files.get_owned_signed_url.return_value = "https://signed.example/avatar"
|
||||
def test_resolve_returns_owned_signed_url(
|
||||
files: SQLAlchemyAccountAvatarFileGateway,
|
||||
sqlite_session_factory: sessionmaker[Session],
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
with sqlite_session_factory.begin() as session:
|
||||
session.add(make_upload_file(file_id="file-1"))
|
||||
get_owned_url = mocker.spy(files, "get_owned_signed_url")
|
||||
sign_url = mocker.patch(
|
||||
"services.account_avatar_file_gateway.file_helpers.get_signed_file_url",
|
||||
return_value="https://signed.example/avatar",
|
||||
)
|
||||
service = AccountAvatarService(files=files)
|
||||
|
||||
result = service.resolve(_context(), "file-1")
|
||||
|
||||
assert result == "https://signed.example/avatar"
|
||||
files.get_owned_signed_url.assert_called_once_with(account_id="account-1", upload_file_id="file-1")
|
||||
sign_url.assert_called_once_with(upload_file_id="file-1")
|
||||
get_owned_url.assert_called_once_with(account_id="account-1", upload_file_id="file-1")
|
||||
|
||||
|
||||
def test_resolve_rejects_missing_or_unowned_file() -> None:
|
||||
files = Mock(spec=AccountAvatarFileGateway)
|
||||
files.get_owned_signed_url.return_value = None
|
||||
@pytest.mark.parametrize("exists", [False, True])
|
||||
def test_resolve_rejects_missing_or_unowned_file(
|
||||
files: SQLAlchemyAccountAvatarFileGateway,
|
||||
sqlite_session_factory: sessionmaker[Session],
|
||||
exists: bool,
|
||||
) -> None:
|
||||
if exists:
|
||||
with sqlite_session_factory.begin() as session:
|
||||
session.add(make_upload_file(file_id="file-1", created_by="another-account"))
|
||||
service = AccountAvatarService(files=files)
|
||||
|
||||
with pytest.raises(AvatarFileNotFoundError):
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from machinery.context import RequestContext
|
||||
from models.account import Account, AccountStatus, InvitationCode, InvitationCodeStatus
|
||||
from repositories.account.repository import SQLAlchemyAccountRepository
|
||||
from services.account_errors import (
|
||||
AccountAlreadyInitializedError,
|
||||
InvalidInvitationCodeError,
|
||||
MissingInvitationCodeError,
|
||||
)
|
||||
from services.account_initialization_service import AccountInitializationService
|
||||
from services.account_ports import AccountRepository
|
||||
from services.entities.account_entities import (
|
||||
AccountInitialization,
|
||||
AccountInitializationResult,
|
||||
AccountInitializationStatus,
|
||||
AccountSnapshot,
|
||||
)
|
||||
from tests.unit_tests.model_factories import make_account_snapshot
|
||||
from tests.unit_tests.model_factories import make_account
|
||||
|
||||
|
||||
def _context() -> RequestContext:
|
||||
@@ -31,17 +31,21 @@ def _context() -> RequestContext:
|
||||
)
|
||||
|
||||
|
||||
def _account(*, status: str = "uninitialized") -> AccountSnapshot:
|
||||
return make_account_snapshot(status=status)
|
||||
@pytest.fixture
|
||||
def accounts(sqlite_session_factory: sessionmaker[Session]) -> SQLAlchemyAccountRepository:
|
||||
with sqlite_session_factory.begin() as session:
|
||||
session.add(make_account(status=AccountStatus.UNINITIALIZED))
|
||||
session.add(InvitationCode(batch="test", code="invite-1"))
|
||||
return SQLAlchemyAccountRepository(sqlite_session_factory)
|
||||
|
||||
|
||||
def test_cloud_initialization_consumes_invitation_and_updates_account_atomically() -> None:
|
||||
def test_cloud_initialization_consumes_invitation_and_updates_account_atomically(
|
||||
accounts: SQLAlchemyAccountRepository,
|
||||
sqlite_session_factory: sessionmaker[Session],
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
initialized_at = datetime(2026, 8, 10, 12, 0)
|
||||
accounts = Mock(spec=AccountRepository)
|
||||
accounts.initialize.return_value = AccountInitializationResult(
|
||||
status=AccountInitializationStatus.INITIALIZED,
|
||||
account=_account(status="active"),
|
||||
)
|
||||
initialize = mocker.spy(accounts, "initialize")
|
||||
service = AccountInitializationService(
|
||||
accounts=accounts,
|
||||
invitation_required=True,
|
||||
@@ -56,7 +60,7 @@ def test_cloud_initialization_consumes_invitation_and_updates_account_atomically
|
||||
)
|
||||
|
||||
assert result.status == "active"
|
||||
accounts.initialize.assert_called_once_with(
|
||||
initialize.assert_called_once_with(
|
||||
"account-1",
|
||||
AccountInitialization(
|
||||
interface_language="zh-Hans",
|
||||
@@ -68,9 +72,26 @@ def test_cloud_initialization_consumes_invitation_and_updates_account_atomically
|
||||
workspace_id="workspace-1",
|
||||
)
|
||||
|
||||
with sqlite_session_factory() as session:
|
||||
invitation = session.scalar(select(InvitationCode).where(InvitationCode.code == "invite-1"))
|
||||
account = session.get(Account, "account-1")
|
||||
assert invitation is not None
|
||||
assert account is not None
|
||||
assert invitation.status == InvitationCodeStatus.USED
|
||||
assert invitation.used_at == account.initialized_at == initialized_at
|
||||
assert invitation.used_by_account_id == account.id
|
||||
assert invitation.used_by_tenant_id == "workspace-1"
|
||||
assert account.status == AccountStatus.ACTIVE
|
||||
assert account.interface_language == "zh-Hans"
|
||||
assert account.interface_theme == "light"
|
||||
assert account.timezone == "Asia/Shanghai"
|
||||
|
||||
def test_cloud_initialization_rejects_missing_or_invalid_invitation() -> None:
|
||||
accounts = Mock(spec=AccountRepository)
|
||||
|
||||
def test_cloud_initialization_rejects_missing_or_invalid_invitation(
|
||||
accounts: SQLAlchemyAccountRepository,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
initialize = mocker.spy(accounts, "initialize")
|
||||
service = AccountInitializationService(
|
||||
accounts=accounts,
|
||||
invitation_required=True,
|
||||
@@ -80,20 +101,24 @@ def test_cloud_initialization_rejects_missing_or_invalid_invitation() -> None:
|
||||
with pytest.raises(MissingInvitationCodeError):
|
||||
service.initialize(_context(), interface_language="en-US", timezone="UTC", invitation_code=None)
|
||||
|
||||
accounts.initialize.return_value = AccountInitializationResult(
|
||||
status=AccountInitializationStatus.INVALID_INVITATION
|
||||
)
|
||||
initialize.assert_not_called()
|
||||
with pytest.raises(InvalidInvitationCodeError):
|
||||
service.initialize(_context(), interface_language="en-US", timezone="UTC", invitation_code="used")
|
||||
|
||||
accounts.initialize.assert_called_once()
|
||||
initialize.assert_called_once()
|
||||
account = accounts.get("account-1")
|
||||
assert account is not None
|
||||
assert account.status == "uninitialized"
|
||||
|
||||
|
||||
def test_initialization_rejects_an_active_account_before_consuming_invitation() -> None:
|
||||
accounts = Mock(spec=AccountRepository)
|
||||
accounts.initialize.return_value = AccountInitializationResult(
|
||||
status=AccountInitializationStatus.ALREADY_INITIALIZED
|
||||
)
|
||||
def test_initialization_rejects_an_active_account_before_consuming_invitation(
|
||||
accounts: SQLAlchemyAccountRepository,
|
||||
sqlite_session_factory: sessionmaker[Session],
|
||||
) -> None:
|
||||
with sqlite_session_factory.begin() as session:
|
||||
account = session.get(Account, "account-1")
|
||||
assert account is not None
|
||||
account.status = AccountStatus.ACTIVE
|
||||
service = AccountInitializationService(
|
||||
accounts=accounts,
|
||||
invitation_required=True,
|
||||
@@ -101,4 +126,10 @@ def test_initialization_rejects_an_active_account_before_consuming_invitation()
|
||||
)
|
||||
|
||||
with pytest.raises(AccountAlreadyInitializedError):
|
||||
service.initialize(_context(), interface_language="en-US", timezone="UTC", invitation_code="invite")
|
||||
service.initialize(_context(), interface_language="en-US", timezone="UTC", invitation_code="invite-1")
|
||||
|
||||
with sqlite_session_factory() as session:
|
||||
invitation = session.scalar(select(InvitationCode).where(InvitationCode.code == "invite-1"))
|
||||
assert invitation is not None
|
||||
assert invitation.status == InvitationCodeStatus.UNUSED
|
||||
assert invitation.used_at is None
|
||||
Reference in new issue
Block a user