mirror of
https://github.com/langgenius/dify.git
synced 2026-11-12 14:21:29 +08:00
254 lines
9.5 KiB
Python
254 lines
9.5 KiB
Python
"""Transport-boundary tests for account invitation activation."""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
from pytest_mock import MockerFixture
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
from werkzeug.exceptions import UnprocessableEntity
|
|
|
|
from controllers.console.auth.activate import ActivateApi, ActivateCheckApi
|
|
from controllers.console.auth.error import InvitationAccountMismatchError as InvitationAccountMismatchHTTPError
|
|
from controllers.console.error import (
|
|
AccountInFreezeError,
|
|
AlreadyActivateError,
|
|
)
|
|
from controllers.console.error import (
|
|
EmailDomainSuspendedError as EmailDomainSuspendedHTTPError,
|
|
)
|
|
from enums import DeploymentEdition
|
|
from extensions.ext_application_services import build_application_services
|
|
from extensions.ext_redis import RedisClientWrapper
|
|
from libs.login import AccountWithTenant
|
|
from models.account import Account
|
|
from services.account_activation_service import AccountActivationService
|
|
from services.account_errors import AccountEmailDomainSuspendedError as EmailDomainSuspendedRegistrationError
|
|
from services.account_errors import FrozenAccountError, InvalidInvitationError, InvitationAccountMismatchError
|
|
from services.entities.account_activation_entities import (
|
|
ActivationCheckData,
|
|
ActivationCheckResult,
|
|
ActivationCommand,
|
|
InvitationLookup,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def app() -> Flask:
|
|
app = Flask(__name__)
|
|
app.config["TESTING"] = True
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def activation_service(
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
redis_transport: tuple[RedisClientWrapper, MagicMock],
|
|
mocker: MockerFixture,
|
|
) -> AccountActivationService:
|
|
service = build_application_services(
|
|
database_client=sqlite_session_factory,
|
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
|
initialization_password="",
|
|
redis=redis_transport[0],
|
|
).accounts.activation
|
|
mocker.patch.object(service, "check")
|
|
mocker.patch.object(service, "activate")
|
|
return service
|
|
|
|
|
|
def _services(service: AccountActivationService) -> SimpleNamespace:
|
|
return SimpleNamespace(accounts=SimpleNamespace(activation=service))
|
|
|
|
|
|
class TestActivateCheckApi:
|
|
def test_serializes_valid_invitation(
|
|
self,
|
|
app: Flask,
|
|
activation_service: AccountActivationService,
|
|
) -> None:
|
|
activation_service.check.return_value = ActivationCheckResult(
|
|
is_valid=True,
|
|
data=ActivationCheckData(
|
|
workspace_name="Test Workspace",
|
|
workspace_id="workspace-123",
|
|
email="invitee@example.com",
|
|
account_status="pending",
|
|
requires_setup=True,
|
|
),
|
|
)
|
|
with (
|
|
app.test_request_context(
|
|
"/activate/check?workspace_id=workspace-123&email=invitee@example.com&token=valid-token"
|
|
),
|
|
patch(
|
|
"controllers.console.auth.activate.application_services",
|
|
return_value=_services(activation_service),
|
|
),
|
|
):
|
|
response = ActivateCheckApi().get()
|
|
|
|
assert response == {
|
|
"is_valid": True,
|
|
"data": {
|
|
"workspace_name": "Test Workspace",
|
|
"workspace_id": "workspace-123",
|
|
"email": "invitee@example.com",
|
|
"account_status": "pending",
|
|
"requires_setup": True,
|
|
},
|
|
}
|
|
activation_service.check.assert_called_once_with(
|
|
InvitationLookup(
|
|
workspace_id="workspace-123",
|
|
email="invitee@example.com",
|
|
token="valid-token",
|
|
),
|
|
)
|
|
|
|
def test_omits_data_for_invalid_invitation(self, app: Flask, activation_service: AccountActivationService) -> None:
|
|
activation_service.check.return_value = ActivationCheckResult(is_valid=False)
|
|
|
|
with (
|
|
app.test_request_context("/activate/check?token=invalid-token"),
|
|
patch(
|
|
"controllers.console.auth.activate.application_services",
|
|
return_value=_services(activation_service),
|
|
),
|
|
):
|
|
response = ActivateCheckApi().get()
|
|
|
|
assert response == {"is_valid": False}
|
|
|
|
def test_rejects_request_without_token(self, app: Flask, activation_service: AccountActivationService) -> None:
|
|
"""`token` is required, so a tokenless query must not reach the application service."""
|
|
with (
|
|
app.test_request_context("/activate/check?workspace_id=workspace-123"),
|
|
patch(
|
|
"controllers.console.auth.activate.application_services",
|
|
return_value=_services(activation_service),
|
|
),
|
|
pytest.raises(UnprocessableEntity),
|
|
):
|
|
ActivateCheckApi().get()
|
|
|
|
activation_service.check.assert_not_called()
|
|
|
|
|
|
class TestActivateApi:
|
|
def test_passes_parsed_command_to_application_service(
|
|
self,
|
|
app: Flask,
|
|
activation_service: AccountActivationService,
|
|
) -> None:
|
|
payload = {
|
|
"workspace_id": "workspace-123",
|
|
"email": "Invitee@Example.com",
|
|
"token": "valid-token",
|
|
"name": "John Doe",
|
|
"interface_language": "en-US",
|
|
"timezone": "UTC",
|
|
}
|
|
account = Account(name="User", email="user@example.com")
|
|
account.id = "account-123"
|
|
with (
|
|
app.test_request_context("/activate", method="POST", json=payload),
|
|
patch(
|
|
"controllers.console.auth.activate.application_services",
|
|
return_value=_services(activation_service),
|
|
),
|
|
patch("controllers.console.auth.activate.extract_access_token", return_value="access-token"),
|
|
patch(
|
|
"controllers.console.auth.activate.current_account_with_tenant",
|
|
return_value=AccountWithTenant(
|
|
account=account,
|
|
tenant_id="workspace-123",
|
|
),
|
|
),
|
|
):
|
|
response = ActivateApi().post()
|
|
|
|
assert response == {"result": "success"}
|
|
activation_service.activate.assert_called_once_with(
|
|
ActivationCommand(
|
|
invitation=InvitationLookup(
|
|
workspace_id="workspace-123",
|
|
email="Invitee@Example.com",
|
|
token="valid-token",
|
|
),
|
|
name="John Doe",
|
|
interface_language="en-US",
|
|
timezone="UTC",
|
|
),
|
|
authenticated_account_id="account-123",
|
|
)
|
|
|
|
def test_passes_no_authenticated_account_for_token_only_activation(
|
|
self,
|
|
app: Flask,
|
|
activation_service: AccountActivationService,
|
|
) -> None:
|
|
with (
|
|
app.test_request_context("/activate", method="POST", json={"token": "valid-token"}),
|
|
patch(
|
|
"controllers.console.auth.activate.application_services",
|
|
return_value=_services(activation_service),
|
|
),
|
|
patch("controllers.console.auth.activate.extract_access_token", return_value=None),
|
|
patch("controllers.console.auth.activate.current_account_with_tenant") as resolve_account,
|
|
):
|
|
response = ActivateApi().post()
|
|
|
|
assert response == {"result": "success"}
|
|
activation_service.activate.assert_called_once_with(
|
|
ActivationCommand(invitation=InvitationLookup(workspace_id=None, email=None, token="valid-token")),
|
|
authenticated_account_id=None,
|
|
)
|
|
resolve_account.assert_not_called()
|
|
|
|
@pytest.mark.parametrize(
|
|
("service_error", "http_error"),
|
|
[
|
|
(InvalidInvitationError(), AlreadyActivateError),
|
|
(InvitationAccountMismatchError(), InvitationAccountMismatchHTTPError),
|
|
(FrozenAccountError(), AccountInFreezeError),
|
|
(EmailDomainSuspendedRegistrationError(), EmailDomainSuspendedHTTPError),
|
|
],
|
|
)
|
|
def test_translates_application_errors(
|
|
self,
|
|
app: Flask,
|
|
activation_service: AccountActivationService,
|
|
service_error: Exception,
|
|
http_error: type[Exception],
|
|
) -> None:
|
|
activation_service.activate.side_effect = service_error
|
|
|
|
with (
|
|
app.test_request_context("/activate", method="POST", json={"token": "invalid-token"}),
|
|
patch(
|
|
"controllers.console.auth.activate.application_services",
|
|
return_value=_services(activation_service),
|
|
),
|
|
patch("controllers.console.auth.activate.extract_access_token", return_value=None),
|
|
pytest.raises(http_error),
|
|
):
|
|
ActivateApi().post()
|
|
|
|
def test_rejects_request_without_token(self, app: Flask, activation_service: AccountActivationService) -> None:
|
|
"""`token` is required, so a tokenless payload must not resolve the session or reach the service."""
|
|
with (
|
|
app.test_request_context("/activate", method="POST", json={"workspace_id": "workspace-123"}),
|
|
patch(
|
|
"controllers.console.auth.activate.application_services",
|
|
return_value=_services(activation_service),
|
|
),
|
|
patch("controllers.console.auth.activate.extract_access_token") as extract_token,
|
|
pytest.raises(UnprocessableEntity),
|
|
):
|
|
ActivateApi().post()
|
|
|
|
extract_token.assert_not_called()
|
|
activation_service.activate.assert_not_called()
|