mirror of
https://github.com/langgenius/dify.git
synced 2026-06-23 04:11:09 +08:00
refactor(api): type end user records with enum (#36945)
Co-authored-by: WH-2099 <wh2099@pm.me>
This commit is contained in:
parent
bd15b8e6ce
commit
8052c93133
@ -10,6 +10,7 @@ from sqlalchemy.orm import sessionmaker
|
||||
from extensions.ext_database import db
|
||||
from libs.login import current_user
|
||||
from models.account import Tenant
|
||||
from models.enums import EndUserType
|
||||
from models.model import DefaultEndUserSessionID, EndUser
|
||||
|
||||
|
||||
@ -75,7 +76,7 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser:
|
||||
if not user_model:
|
||||
user_model = EndUser(
|
||||
tenant_id=tenant_id,
|
||||
type="service_api",
|
||||
type=EndUserType.SERVICE_API,
|
||||
is_anonymous=is_anonymous,
|
||||
session_id=user_id,
|
||||
)
|
||||
|
||||
@ -13,7 +13,7 @@ from core.mcp.server.streamable_http import handle_mcp_request
|
||||
from extensions.ext_database import db
|
||||
from graphon.variables.input_entities import VariableEntity, VariableEntityType
|
||||
from libs import helper
|
||||
from models.enums import AppMCPServerStatus
|
||||
from models.enums import AppMCPServerStatus, EndUserType
|
||||
from models.model import App, AppMCPServer, AppMode, EndUser
|
||||
|
||||
|
||||
@ -201,7 +201,7 @@ class MCPAppApi(Resource):
|
||||
select(EndUser)
|
||||
.where(EndUser.tenant_id == tenant_id)
|
||||
.where(EndUser.session_id == mcp_server_id)
|
||||
.where(EndUser.type == "mcp")
|
||||
.where(EndUser.type == EndUserType.MCP)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
@ -212,7 +212,7 @@ class MCPAppApi(Resource):
|
||||
end_user = EndUser(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
type="mcp",
|
||||
type=EndUserType.MCP,
|
||||
name=client_name,
|
||||
session_id=mcp_server_id,
|
||||
)
|
||||
|
||||
@ -6,9 +6,9 @@ from flask import request
|
||||
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound, Unauthorized
|
||||
|
||||
from controllers.openapi.auth.data import AuthData
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from extensions.ext_database import db
|
||||
from models.account import TenantStatus
|
||||
from models.enums import EndUserType
|
||||
from services.account_service import AccountService, TenantService
|
||||
from services.app_service import AppService
|
||||
from services.end_user_service import EndUserService
|
||||
@ -85,7 +85,7 @@ def resolve_external_user(data: AuthData) -> None:
|
||||
if data.tenant is None or data.app is None or data.external_identity is None:
|
||||
raise Unauthorized("missing context for external user resolution")
|
||||
end_user = EndUserService.get_or_create_end_user_by_type(
|
||||
InvokeFrom.OPENAPI,
|
||||
EndUserType.OPENAPI,
|
||||
tenant_id=str(data.tenant.id),
|
||||
app_id=str(data.app.id),
|
||||
user_id=data.external_identity.email,
|
||||
|
||||
@ -17,6 +17,7 @@ from controllers.web.error import WebAppAuthRequiredError
|
||||
from extensions.ext_database import db
|
||||
from libs.passport import PassportService
|
||||
from libs.token import extract_webapp_access_token
|
||||
from models.enums import EndUserType
|
||||
from models.model import App, EndUser, Site
|
||||
from services.feature_service import FeatureService
|
||||
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
|
||||
@ -82,7 +83,7 @@ class PassportResource(Resource):
|
||||
end_user = EndUser(
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=app_model.id,
|
||||
type="browser",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=True,
|
||||
session_id=user_id,
|
||||
)
|
||||
@ -92,7 +93,7 @@ class PassportResource(Resource):
|
||||
end_user = EndUser(
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=app_model.id,
|
||||
type="browser",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=True,
|
||||
session_id=generate_session_id(),
|
||||
)
|
||||
@ -181,7 +182,7 @@ def exchange_token_for_existing_web_user(
|
||||
end_user = EndUser(
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=app_model.id,
|
||||
type="browser",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=True,
|
||||
session_id=session_id,
|
||||
)
|
||||
@ -225,7 +226,7 @@ def _exchange_for_public_app_token(app_model, site, token_decoded):
|
||||
end_user = EndUser(
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=app_model.id,
|
||||
type="browser",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=True,
|
||||
session_id=generate_session_id(),
|
||||
)
|
||||
|
||||
@ -14,6 +14,7 @@ from extensions.ext_database import db
|
||||
from libs.passport import PassportService
|
||||
from libs.token import extract_access_token, extract_console_cookie_token, extract_webapp_passport
|
||||
from models import Account, Tenant, TenantAccountJoin
|
||||
from models.enums import EndUserType
|
||||
from models.model import AppMCPServer, EndUser
|
||||
from services.account_service import AccountService
|
||||
|
||||
@ -136,7 +137,7 @@ def load_user_from_request(request_from_flask_login: Request) -> LoginUser | Non
|
||||
if not app_mcp_server:
|
||||
raise NotFound("App MCP server not found.")
|
||||
end_user = db.session.scalar(
|
||||
select(EndUser).where(EndUser.session_id == app_mcp_server.id, EndUser.type == "mcp").limit(1)
|
||||
select(EndUser).where(EndUser.session_id == app_mcp_server.id, EndUser.type == EndUserType.MCP).limit(1)
|
||||
)
|
||||
if not end_user:
|
||||
raise NotFound("End user not found.")
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""add resource maintainers
|
||||
|
||||
Revision ID: a7c4e9d2f681
|
||||
Revises: 9f4b7c2d1a80
|
||||
Revises: d2f1a4b8c3e0
|
||||
Create Date: 2026-06-15 12:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
"""normalize legacy end user type
|
||||
|
||||
Revision ID: 4f7b2c8d9a10
|
||||
Revises: a7c4e9d2f681
|
||||
Create Date: 2026-06-15 15:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "4f7b2c8d9a10"
|
||||
down_revision = "a7c4e9d2f681"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(sa.text("UPDATE end_users SET type = 'service-api' WHERE type = 'service_api'"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(sa.text("UPDATE end_users SET type = 'service_api' WHERE type = 'service-api'"))
|
||||
@ -205,6 +205,16 @@ class InvokeFrom(StrEnum):
|
||||
return source_mapping.get(self, "dev")
|
||||
|
||||
|
||||
class EndUserType(StrEnum):
|
||||
"""Persisted type values for the ``end_users.type`` column."""
|
||||
|
||||
BROWSER = "browser"
|
||||
MCP = "mcp"
|
||||
OPENAPI = "openapi"
|
||||
SERVICE_API = "service-api"
|
||||
TRIGGER = "trigger"
|
||||
|
||||
|
||||
class DocumentDocType(StrEnum):
|
||||
"""Document doc_type classification"""
|
||||
|
||||
|
||||
@ -41,6 +41,7 @@ from .enums import (
|
||||
ConversationStatus,
|
||||
CreatorUserRole,
|
||||
CustomizeTokenStrategy,
|
||||
EndUserType,
|
||||
FeedbackFromSource,
|
||||
FeedbackRating,
|
||||
InvokeFrom,
|
||||
@ -2083,7 +2084,7 @@ class EndUser(Base, UserMixin):
|
||||
id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuid4()))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
app_id = mapped_column(StringUUID, nullable=True)
|
||||
type: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
type: Mapped[EndUserType] = mapped_column(EnumText(EndUserType, length=255), nullable=False)
|
||||
external_user_id = mapped_column(String(255), nullable=True)
|
||||
name = mapped_column(String(255))
|
||||
_is_anonymous: Mapped[bool] = mapped_column(
|
||||
|
||||
@ -4,8 +4,8 @@ from collections.abc import Mapping
|
||||
from sqlalchemy import case, select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from extensions.ext_database import db
|
||||
from models.enums import EndUserType
|
||||
from models.model import App, DefaultEndUserSessionID, EndUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -41,11 +41,11 @@ class EndUserService:
|
||||
Get or create an end user for a given app.
|
||||
"""
|
||||
|
||||
return cls.get_or_create_end_user_by_type(InvokeFrom.SERVICE_API, app_model.tenant_id, app_model.id, user_id)
|
||||
return cls.get_or_create_end_user_by_type(EndUserType.SERVICE_API, app_model.tenant_id, app_model.id, user_id)
|
||||
|
||||
@classmethod
|
||||
def get_or_create_end_user_by_type(
|
||||
cls, type: InvokeFrom, tenant_id: str, app_id: str, user_id: str | None = None
|
||||
cls, type: EndUserType, tenant_id: str, app_id: str, user_id: str | None = None
|
||||
) -> EndUser:
|
||||
"""
|
||||
Get or create an end user for a given app and type.
|
||||
@ -98,7 +98,7 @@ class EndUserService:
|
||||
|
||||
@classmethod
|
||||
def create_end_user_batch(
|
||||
cls, type: InvokeFrom, tenant_id: str, app_ids: list[str], user_id: str
|
||||
cls, type: EndUserType, tenant_id: str, app_ids: list[str], user_id: str
|
||||
) -> Mapping[str, EndUser]:
|
||||
"""Create end users in batch.
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.exceptions import RequestEntityTooLarge
|
||||
|
||||
from configs import dify_config
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.app.file_access import DatabaseFileAccessController
|
||||
from core.tools.tool_file_manager import ToolFileManager
|
||||
from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE
|
||||
@ -31,7 +30,7 @@ from factories import file_factory
|
||||
from graphon.entities.graph_config import NodeConfigDict
|
||||
from graphon.file import FileTransferMethod
|
||||
from graphon.variables.types import ArrayValidation, SegmentType
|
||||
from models.enums import AppTriggerStatus, AppTriggerType
|
||||
from models.enums import AppTriggerStatus, AppTriggerType, EndUserType
|
||||
from models.model import App
|
||||
from models.trigger import AppTrigger, WorkflowWebhookTrigger
|
||||
from models.workflow import Workflow
|
||||
@ -810,7 +809,7 @@ class WebhookService:
|
||||
)
|
||||
|
||||
end_user = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.TRIGGER,
|
||||
type=EndUserType.TRIGGER,
|
||||
tenant_id=webhook_trigger.tenant_id,
|
||||
app_id=webhook_trigger.app_id,
|
||||
user_id=None,
|
||||
|
||||
@ -12,6 +12,7 @@ from libs.helper import TokenManager
|
||||
from libs.passport import PassportService
|
||||
from libs.password import compare_password
|
||||
from models import Account, AccountStatus
|
||||
from models.enums import EndUserType
|
||||
from models.model import App, EndUser, Site
|
||||
from services.account_service import AccountService
|
||||
from services.app_service import AppService
|
||||
@ -102,7 +103,7 @@ class WebAppAuthService:
|
||||
end_user = EndUser(
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=app_model.id,
|
||||
type="browser",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=False,
|
||||
session_id=email,
|
||||
name="enterpriseuser",
|
||||
|
||||
@ -15,7 +15,6 @@ from celery import shared_task
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.db.session_factory import session_factory
|
||||
from core.plugin.entities.plugin_daemon import CredentialType
|
||||
from core.plugin.entities.request import TriggerInvokeEventResponse
|
||||
@ -32,6 +31,7 @@ from graphon.enums import WorkflowExecutionStatus
|
||||
from models.enums import (
|
||||
AppTriggerType,
|
||||
CreatorUserRole,
|
||||
EndUserType,
|
||||
WorkflowRunTriggeredFrom,
|
||||
WorkflowTriggerStatus,
|
||||
)
|
||||
@ -265,7 +265,7 @@ def dispatch_triggered_workflow(
|
||||
workflows: Mapping[str, Workflow] = _get_latest_workflows_by_app_ids(session, subscribers)
|
||||
|
||||
end_users: Mapping[str, EndUser] = EndUserService.create_end_user_batch(
|
||||
type=InvokeFrom.TRIGGER,
|
||||
type=EndUserType.TRIGGER,
|
||||
tenant_id=subscription.tenant_id,
|
||||
app_ids=[plugin_trigger.app_id for plugin_trigger in subscribers],
|
||||
user_id=user_id,
|
||||
|
||||
@ -187,6 +187,7 @@ class TestDecodeJwtToken:
|
||||
return flask_app_with_containers
|
||||
|
||||
def _create_app_site_enduser(self, db_session: Session, *, enable_site: bool = True):
|
||||
from models.enums import EndUserType
|
||||
from models.model import App, AppMode, CustomizeTokenStrategy, EndUser, Site
|
||||
|
||||
tenant_id = str(uuid4())
|
||||
@ -215,7 +216,7 @@ class TestDecodeJwtToken:
|
||||
end_user = EndUser(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
type="browser",
|
||||
type=EndUserType.BROWSER,
|
||||
session_id="sess-1",
|
||||
)
|
||||
db_session.add(end_user)
|
||||
|
||||
@ -13,7 +13,7 @@ import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.account import Account
|
||||
from models.enums import CreatorUserRole
|
||||
from models.enums import CreatorUserRole, EndUserType
|
||||
from models.model import App, AppMode, EndUser
|
||||
from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom
|
||||
|
||||
@ -44,7 +44,7 @@ class TestWorkflowNodeExecutionModelCreatedBy:
|
||||
end_user = EndUser(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
type="service_api",
|
||||
type=EndUserType.SERVICE_API,
|
||||
external_user_id=f"ext-{uuid4()}",
|
||||
name="End User",
|
||||
session_id=f"session-{uuid4()}",
|
||||
|
||||
@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from core.plugin.impl.exc import PluginDaemonClientSideError
|
||||
from models import Account, AppMode, CreatorUserRole
|
||||
from models.enums import ConversationFromSource, MessageFileBelongsTo
|
||||
from models.enums import ConversationFromSource, EndUserType, MessageFileBelongsTo
|
||||
from models.model import AppModelConfig, Conversation, EndUser, Message, MessageAgentThought
|
||||
from services.account_service import AccountService, TenantService
|
||||
from services.agent_service import AgentService
|
||||
@ -388,7 +388,7 @@ class TestAgentService:
|
||||
id=fake.uuid4(),
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
type="web_app",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=False,
|
||||
session_id=fake.uuid4(),
|
||||
name=fake.name(),
|
||||
|
||||
@ -9,6 +9,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from models import App
|
||||
from models.enums import EndUserType
|
||||
from models.model import EndUser
|
||||
from models.workflow import Workflow
|
||||
from services.app_generate_service import AppGenerateService
|
||||
@ -446,7 +447,7 @@ class TestAppGenerateService:
|
||||
end_user = EndUser(
|
||||
tenant_id=account.current_tenant.id,
|
||||
app_id=app.id,
|
||||
type="normal",
|
||||
type=EndUserType.BROWSER,
|
||||
external_user_id=fake.uuid4(),
|
||||
name=fake.name(),
|
||||
is_anonymous=False,
|
||||
@ -831,7 +832,7 @@ class TestAppGenerateService:
|
||||
end_user = EndUser(
|
||||
tenant_id=account.current_tenant.id,
|
||||
app_id=app.id,
|
||||
type="normal",
|
||||
type=EndUserType.BROWSER,
|
||||
external_user_id=fake.uuid4(),
|
||||
name=fake.name(),
|
||||
is_anonymous=False,
|
||||
|
||||
@ -12,7 +12,7 @@ from sqlalchemy.orm import Session
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from models import TenantAccountRole
|
||||
from models.account import Account, Tenant, TenantAccountJoin
|
||||
from models.enums import ConversationFromSource
|
||||
from models.enums import ConversationFromSource, EndUserType
|
||||
from models.model import App, Conversation, EndUser, Message, MessageAnnotation
|
||||
from services.annotation_service import AppAnnotationService
|
||||
from services.conversation_service import ConversationService
|
||||
@ -76,7 +76,7 @@ class ConversationServiceIntegrationTestDataFactory:
|
||||
end_user = EndUser(
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
type=EndUserType.SERVICE_API,
|
||||
external_user_id=f"external-{uuid4()}",
|
||||
name="End User",
|
||||
is_anonymous=False,
|
||||
|
||||
@ -12,7 +12,7 @@ from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from extensions.ext_database import db
|
||||
from graphon.variables import FloatVariable, IntegerVariable, StringVariable
|
||||
from models.account import Account, Tenant, TenantAccountJoin
|
||||
from models.enums import ConversationFromSource
|
||||
from models.enums import ConversationFromSource, EndUserType
|
||||
from models.model import App, Conversation, EndUser
|
||||
from models.workflow import ConversationVariable
|
||||
from services.conversation_service import ConversationService
|
||||
@ -78,7 +78,7 @@ class ConversationServiceVariableIntegrationFactory:
|
||||
end_user = EndUser(
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
type=InvokeFrom.SERVICE_API.value,
|
||||
type=EndUserType.SERVICE_API,
|
||||
external_user_id=f"external-{uuid4()}",
|
||||
name=f"End User {uuid4()}",
|
||||
is_anonymous=False,
|
||||
|
||||
@ -6,9 +6,9 @@ from uuid import uuid4
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from models import TenantAccountRole
|
||||
from models.account import Account, Tenant, TenantAccountJoin
|
||||
from models.enums import EndUserType
|
||||
from models.model import App, DefaultEndUserSessionID, EndUser
|
||||
from services.end_user_service import EndUserService
|
||||
|
||||
@ -71,7 +71,7 @@ class TestEndUserServiceFactory:
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
session_id: str,
|
||||
invoke_type: InvokeFrom,
|
||||
invoke_type: EndUserType,
|
||||
is_anonymous: bool = False,
|
||||
):
|
||||
end_user = EndUser(
|
||||
@ -119,7 +119,7 @@ class TestEndUserServiceGetOrCreateEndUser:
|
||||
assert result.tenant_id == app.tenant_id
|
||||
assert result.app_id == app.id
|
||||
assert result.session_id == user_id
|
||||
assert result.type == InvokeFrom.SERVICE_API
|
||||
assert result.type == EndUserType.SERVICE_API
|
||||
assert result.is_anonymous is False
|
||||
|
||||
def test_get_or_create_end_user_without_user_id(
|
||||
@ -147,7 +147,7 @@ class TestEndUserServiceGetOrCreateEndUser:
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
session_id=user_id,
|
||||
invoke_type=InvokeFrom.SERVICE_API,
|
||||
invoke_type=EndUserType.SERVICE_API,
|
||||
)
|
||||
|
||||
# Act
|
||||
@ -162,7 +162,7 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
Unit tests for EndUserService.get_or_create_end_user_by_type method.
|
||||
|
||||
This test suite covers:
|
||||
- Creating end users with different InvokeFrom types
|
||||
- Creating end users with different EndUserType values
|
||||
- Type migration for legacy users
|
||||
- Query ordering and prioritization
|
||||
- Session management
|
||||
@ -185,22 +185,22 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
type=EndUserType.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.type == InvokeFrom.SERVICE_API
|
||||
assert result.type == EndUserType.SERVICE_API
|
||||
assert result.tenant_id == tenant_id
|
||||
assert result.app_id == app_id
|
||||
assert result.session_id == user_id
|
||||
|
||||
def test_create_end_user_web_app_type(
|
||||
def test_create_end_user_browser_type(
|
||||
self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
|
||||
):
|
||||
"""Test creating new end user with WEB_APP type."""
|
||||
"""Test creating new end user with BROWSER type."""
|
||||
# Arrange
|
||||
app = factory.create_app_and_account(db_session_with_containers)
|
||||
tenant_id = app.tenant_id
|
||||
@ -209,14 +209,14 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.WEB_APP,
|
||||
type=EndUserType.BROWSER,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.type == InvokeFrom.WEB_APP
|
||||
assert result.type == EndUserType.BROWSER
|
||||
|
||||
def test_upgrade_legacy_end_user_type(
|
||||
self, caplog: pytest.LogCaptureFixture, db_session_with_containers: Session, factory: TestEndUserServiceFactory
|
||||
@ -234,12 +234,12 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
session_id=user_id,
|
||||
invoke_type=InvokeFrom.SERVICE_API,
|
||||
invoke_type=EndUserType.SERVICE_API,
|
||||
)
|
||||
with caplog.at_level(logging.INFO, logger="services.end_user_service"):
|
||||
# Act - Request with different type
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.WEB_APP,
|
||||
type=EndUserType.BROWSER,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
@ -247,7 +247,7 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
|
||||
# Assert
|
||||
assert result.id == existing_user.id
|
||||
assert result.type == InvokeFrom.WEB_APP # Type should be updated
|
||||
assert result.type == EndUserType.BROWSER # Type should be updated
|
||||
matching_logs = [
|
||||
record
|
||||
for record in caplog.records
|
||||
@ -273,13 +273,13 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
session_id=user_id,
|
||||
invoke_type=InvokeFrom.SERVICE_API,
|
||||
invoke_type=EndUserType.SERVICE_API,
|
||||
)
|
||||
|
||||
# Act - Request with same type
|
||||
with caplog.at_level(logging.INFO, logger="services.end_user_service"):
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
type=EndUserType.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
@ -287,7 +287,7 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
|
||||
# Assert
|
||||
assert result.id == existing_user.id
|
||||
assert result.type == InvokeFrom.SERVICE_API
|
||||
assert result.type == EndUserType.SERVICE_API
|
||||
# No legacy-upgrade log should be emitted when the existing user's type already matches.
|
||||
assert [record for record in caplog.records if record.levelno == logging.INFO] == []
|
||||
|
||||
@ -302,7 +302,7 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
type=EndUserType.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=None,
|
||||
@ -329,19 +329,19 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
session_id=user_id,
|
||||
invoke_type=InvokeFrom.WEB_APP,
|
||||
invoke_type=EndUserType.BROWSER,
|
||||
)
|
||||
matching = factory.create_end_user(
|
||||
db_session_with_containers,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
session_id=user_id,
|
||||
invoke_type=InvokeFrom.SERVICE_API,
|
||||
invoke_type=EndUserType.SERVICE_API,
|
||||
)
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
type=EndUserType.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
@ -363,7 +363,7 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
|
||||
# Act
|
||||
result = EndUserService.get_or_create_end_user_by_type(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
type=EndUserType.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
@ -376,16 +376,16 @@ class TestEndUserServiceGetOrCreateEndUserByType:
|
||||
@pytest.mark.parametrize(
|
||||
"invoke_type",
|
||||
[
|
||||
InvokeFrom.SERVICE_API,
|
||||
InvokeFrom.WEB_APP,
|
||||
InvokeFrom.EXPLORE,
|
||||
InvokeFrom.DEBUGGER,
|
||||
EndUserType.SERVICE_API,
|
||||
EndUserType.BROWSER,
|
||||
EndUserType.OPENAPI,
|
||||
EndUserType.TRIGGER,
|
||||
],
|
||||
)
|
||||
def test_create_end_user_with_different_invoke_types(
|
||||
self, db_session_with_containers: Session, invoke_type: InvokeFrom, factory: TestEndUserServiceFactory
|
||||
self, db_session_with_containers: Session, invoke_type: EndUserType, factory: TestEndUserServiceFactory
|
||||
):
|
||||
"""Test creating end users with different InvokeFrom types."""
|
||||
"""Test creating end users with different EndUserType values."""
|
||||
# Arrange
|
||||
app = factory.create_app_and_account(db_session_with_containers)
|
||||
tenant_id = app.tenant_id
|
||||
@ -421,7 +421,7 @@ class TestEndUserServiceGetEndUserById:
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
session_id=f"session-{uuid4()}",
|
||||
invoke_type=InvokeFrom.SERVICE_API,
|
||||
invoke_type=EndUserType.SERVICE_API,
|
||||
)
|
||||
|
||||
result = EndUserService.get_end_user_by_id(
|
||||
@ -487,7 +487,7 @@ class TestEndUserServiceCreateBatch:
|
||||
|
||||
def test_create_batch_empty_app_ids(self, db_session_with_containers: Session):
|
||||
result = EndUserService.create_end_user_batch(
|
||||
type=InvokeFrom.SERVICE_API, tenant_id=str(uuid4()), app_ids=[], user_id="user-1"
|
||||
type=EndUserType.SERVICE_API, tenant_id=str(uuid4()), app_ids=[], user_id="user-1"
|
||||
)
|
||||
assert result == {}
|
||||
|
||||
@ -499,14 +499,14 @@ class TestEndUserServiceCreateBatch:
|
||||
user_id = f"user-{uuid4()}"
|
||||
|
||||
result = EndUserService.create_end_user_batch(
|
||||
type=InvokeFrom.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
|
||||
type=EndUserType.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
|
||||
)
|
||||
|
||||
assert len(result) == 3
|
||||
for app_id in app_ids:
|
||||
assert app_id in result
|
||||
assert result[app_id].session_id == user_id
|
||||
assert result[app_id].type == InvokeFrom.SERVICE_API
|
||||
assert result[app_id].type == EndUserType.SERVICE_API
|
||||
|
||||
def test_create_batch_default_session_id(
|
||||
self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
|
||||
@ -515,7 +515,7 @@ class TestEndUserServiceCreateBatch:
|
||||
app_ids = [a.id for a in apps]
|
||||
|
||||
result = EndUserService.create_end_user_batch(
|
||||
type=InvokeFrom.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=""
|
||||
type=EndUserType.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=""
|
||||
)
|
||||
|
||||
assert len(result) == 2
|
||||
@ -531,7 +531,7 @@ class TestEndUserServiceCreateBatch:
|
||||
user_id = f"user-{uuid4()}"
|
||||
|
||||
result = EndUserService.create_end_user_batch(
|
||||
type=InvokeFrom.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
|
||||
type=EndUserType.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
|
||||
)
|
||||
|
||||
assert len(result) == 2
|
||||
@ -545,12 +545,12 @@ class TestEndUserServiceCreateBatch:
|
||||
|
||||
# Create batch first time
|
||||
first_result = EndUserService.create_end_user_batch(
|
||||
type=InvokeFrom.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
|
||||
type=EndUserType.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
|
||||
)
|
||||
|
||||
# Create batch second time — should return existing users
|
||||
second_result = EndUserService.create_end_user_batch(
|
||||
type=InvokeFrom.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
|
||||
type=EndUserType.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
|
||||
)
|
||||
|
||||
assert len(second_result) == 2
|
||||
@ -565,7 +565,7 @@ class TestEndUserServiceCreateBatch:
|
||||
|
||||
# Create for first 2 apps
|
||||
first_result = EndUserService.create_end_user_batch(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
type=EndUserType.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_ids=[apps[0].id, apps[1].id],
|
||||
user_id=user_id,
|
||||
@ -573,7 +573,7 @@ class TestEndUserServiceCreateBatch:
|
||||
|
||||
# Create for all 3 apps — should reuse first 2, create 3rd
|
||||
all_result = EndUserService.create_end_user_batch(
|
||||
type=InvokeFrom.SERVICE_API,
|
||||
type=EndUserType.SERVICE_API,
|
||||
tenant_id=tenant_id,
|
||||
app_ids=[a.id for a in apps],
|
||||
user_id=user_id,
|
||||
@ -586,10 +586,10 @@ class TestEndUserServiceCreateBatch:
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invoke_type",
|
||||
[InvokeFrom.SERVICE_API, InvokeFrom.WEB_APP, InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER],
|
||||
[EndUserType.SERVICE_API, EndUserType.BROWSER, EndUserType.OPENAPI, EndUserType.TRIGGER],
|
||||
)
|
||||
def test_create_batch_all_invoke_types(
|
||||
self, db_session_with_containers: Session, invoke_type: InvokeFrom, factory: TestEndUserServiceFactory
|
||||
self, db_session_with_containers: Session, invoke_type: EndUserType, factory: TestEndUserServiceFactory
|
||||
):
|
||||
tenant_id, apps = self._create_multiple_apps(db_session_with_containers, factory, count=1)
|
||||
user_id = f"user-{uuid4()}"
|
||||
|
||||
@ -11,7 +11,7 @@ from werkzeug.exceptions import NotFound
|
||||
from configs import dify_config
|
||||
from extensions.storage.storage_type import StorageType
|
||||
from models import Account, Tenant
|
||||
from models.enums import CreatorUserRole
|
||||
from models.enums import CreatorUserRole, EndUserType
|
||||
from models.model import EndUser, UploadFile
|
||||
from services.errors.file import BlockedFileExtensionError, FileTooLargeError, UnsupportedFileTypeError
|
||||
from services.file_service import FileService
|
||||
@ -112,7 +112,7 @@ class TestFileService:
|
||||
|
||||
end_user = EndUser(
|
||||
tenant_id=str(fake.uuid4()),
|
||||
type="web",
|
||||
type=EndUserType.BROWSER,
|
||||
name=fake.name(),
|
||||
is_anonymous=False,
|
||||
session_id=fake.uuid4(),
|
||||
|
||||
@ -5,7 +5,7 @@ from faker import Faker
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models import App, CreatorUserRole
|
||||
from models.enums import ConversationFromSource
|
||||
from models.enums import ConversationFromSource, EndUserType
|
||||
from models.model import EndUser, Message
|
||||
from models.web import SavedMessage
|
||||
from services.app_service import AppService, CreateAppParams
|
||||
@ -107,7 +107,7 @@ class TestSavedMessageService:
|
||||
app_id=app.id,
|
||||
external_user_id=fake.uuid4(),
|
||||
name=fake.name(),
|
||||
type="normal",
|
||||
type=EndUserType.BROWSER,
|
||||
session_id=fake.uuid4(),
|
||||
is_anonymous=False,
|
||||
)
|
||||
|
||||
@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from models import Account, App
|
||||
from models.enums import ConversationFromSource
|
||||
from models.enums import ConversationFromSource, EndUserType
|
||||
from models.model import Conversation, EndUser
|
||||
from models.web import PinnedConversation
|
||||
from services.account_service import AccountService, TenantService
|
||||
@ -109,7 +109,7 @@ class TestWebConversationService:
|
||||
end_user = EndUser(
|
||||
session_id=fake.uuid4(),
|
||||
app_id=app.id,
|
||||
type="normal",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=False,
|
||||
tenant_id=app.tenant_id,
|
||||
)
|
||||
|
||||
@ -12,7 +12,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from models import EndUser, Workflow, WorkflowAppLog, WorkflowArchiveLog, WorkflowRun
|
||||
from models.enums import AppTriggerType, CreatorUserRole, WorkflowRunTriggeredFrom
|
||||
from models.enums import AppTriggerType, CreatorUserRole, EndUserType, WorkflowRunTriggeredFrom
|
||||
from models.workflow import WorkflowAppLogCreatedFrom
|
||||
from services.account_service import AccountService, TenantService
|
||||
|
||||
@ -821,7 +821,7 @@ class TestWorkflowAppService:
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
type="web",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=False,
|
||||
session_id="test_session_123",
|
||||
created_at=datetime.now(UTC),
|
||||
@ -1567,7 +1567,7 @@ class TestWorkflowAppService:
|
||||
end_user = EndUser(
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
type="browser",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=False,
|
||||
session_id="session-1",
|
||||
)
|
||||
|
||||
@ -7,7 +7,7 @@ import pytest
|
||||
from faker import Faker
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.enums import ConversationFromSource, CreatorUserRole
|
||||
from models.enums import ConversationFromSource, CreatorUserRole, EndUserType
|
||||
from models.model import (
|
||||
Message,
|
||||
)
|
||||
@ -684,7 +684,7 @@ class TestWorkflowRunService:
|
||||
end_user = EndUser(
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
type="web_app",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=False,
|
||||
session_id=str(uuid.uuid4()),
|
||||
external_user_id=str(uuid.uuid4()),
|
||||
|
||||
@ -7,6 +7,7 @@ from pytest_mock import MockerFixture
|
||||
|
||||
from controllers.service_api.end_user.end_user import EndUserApi
|
||||
from controllers.service_api.end_user.error import EndUserNotFoundError
|
||||
from models.enums import EndUserType
|
||||
from models.model import App, EndUser
|
||||
|
||||
|
||||
@ -29,7 +30,7 @@ class TestEndUserApi:
|
||||
end_user.id = str(uuid4())
|
||||
end_user.tenant_id = app_model.tenant_id
|
||||
end_user.app_id = app_model.id
|
||||
end_user.type = "service_api"
|
||||
end_user.type = EndUserType.SERVICE_API
|
||||
end_user.external_user_id = "external-123"
|
||||
end_user.name = "Alice"
|
||||
end_user._is_anonymous = True
|
||||
|
||||
101
api/tests/unit_tests/models/test_end_user_type.py
Normal file
101
api/tests/unit_tests/models/test_end_user_type.py
Normal file
@ -0,0 +1,101 @@
|
||||
import ast
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
|
||||
from models.enums import EndUserType
|
||||
from models.model import EndUser
|
||||
from models.types import EnumText
|
||||
from services.end_user_service import EndUserService
|
||||
|
||||
API_ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def test_end_user_type_covers_persisted_creation_values():
|
||||
assert {member.value for member in EndUserType} == {
|
||||
"browser",
|
||||
"mcp",
|
||||
"openapi",
|
||||
"service-api",
|
||||
"trigger",
|
||||
}
|
||||
|
||||
|
||||
def test_end_user_type_is_plain_persisted_value_enum():
|
||||
assert not hasattr(EndUserType, "from_invoke_from")
|
||||
|
||||
|
||||
def test_end_user_service_creation_methods_accept_end_user_type():
|
||||
assert inspect.signature(EndUserService.get_or_create_end_user_by_type).parameters["type"].annotation is EndUserType
|
||||
assert inspect.signature(EndUserService.create_end_user_batch).parameters["type"].annotation is EndUserType
|
||||
|
||||
|
||||
def test_end_user_service_callers_pass_end_user_type():
|
||||
violations: list[str] = []
|
||||
method_names = {"get_or_create_end_user_by_type", "create_end_user_batch"}
|
||||
|
||||
for source_path in API_ROOT.rglob("*.py"):
|
||||
if "tests" in source_path.parts or ".venv" in source_path.parts:
|
||||
continue
|
||||
|
||||
tree = ast.parse(source_path.read_text(), filename=str(source_path))
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
if not isinstance(node.func, ast.Attribute) or node.func.attr not in method_names:
|
||||
continue
|
||||
if not isinstance(node.func.value, ast.Name) or node.func.value.id != "EndUserService":
|
||||
continue
|
||||
|
||||
type_arg = next((keyword.value for keyword in node.keywords if keyword.arg == "type"), None)
|
||||
if type_arg is None and node.args:
|
||||
type_arg = node.args[0]
|
||||
|
||||
if not (
|
||||
isinstance(type_arg, ast.Attribute)
|
||||
and isinstance(type_arg.value, ast.Name)
|
||||
and type_arg.value.id == "EndUserType"
|
||||
):
|
||||
violations.append(f"{source_path.relative_to(API_ROOT)}:{node.lineno}")
|
||||
|
||||
assert violations == []
|
||||
|
||||
|
||||
def test_end_user_type_column_uses_enum_text():
|
||||
column_type = EndUser.__table__.c.type.type
|
||||
|
||||
assert isinstance(column_type, EnumText)
|
||||
assert column_type._enum_class is EndUserType
|
||||
|
||||
|
||||
def test_production_end_user_constructors_use_end_user_type_enum():
|
||||
violations: list[str] = []
|
||||
|
||||
for source_path in API_ROOT.rglob("*.py"):
|
||||
if "tests" in source_path.parts or ".venv" in source_path.parts:
|
||||
continue
|
||||
|
||||
tree = ast.parse(source_path.read_text(), filename=str(source_path))
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
if not isinstance(node.func, ast.Name) or node.func.id != "EndUser":
|
||||
continue
|
||||
|
||||
for keyword in node.keywords:
|
||||
if keyword.arg != "type":
|
||||
continue
|
||||
value = keyword.value
|
||||
uses_end_user_type_member = (
|
||||
isinstance(value, ast.Attribute)
|
||||
and isinstance(value.value, ast.Name)
|
||||
and value.value.id == "EndUserType"
|
||||
)
|
||||
uses_end_user_service_type_parameter = (
|
||||
source_path.relative_to(API_ROOT) == Path("services/end_user_service.py")
|
||||
and isinstance(value, ast.Name)
|
||||
and value.id == "type"
|
||||
)
|
||||
if not (uses_end_user_type_member or uses_end_user_service_type_parameter):
|
||||
violations.append(f"{source_path.relative_to(API_ROOT)}:{node.lineno}")
|
||||
|
||||
assert violations == []
|
||||
@ -15,7 +15,7 @@ from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormSt
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models.account import Account, Tenant, TenantAccountJoin
|
||||
from models.base import Base
|
||||
from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom
|
||||
from models.enums import CreatorUserRole, EndUserType, WorkflowRunTriggeredFrom
|
||||
from models.human_input import (
|
||||
HumanInputForm,
|
||||
HumanInputFormRecipient,
|
||||
@ -107,7 +107,7 @@ def _create_waiting_form(
|
||||
end_user = EndUser(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
type="web_app",
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=False,
|
||||
session_id="session-1",
|
||||
external_user_id="external-1",
|
||||
@ -210,7 +210,7 @@ def test_issue_upload_token_persists_token_without_technical_end_user(
|
||||
assert token_model.form_id == form_id
|
||||
assert token_model.recipient_id == recipient_id
|
||||
assert token_model.token == token.upload_token
|
||||
assert session.scalar(select(EndUser).where(EndUser.type == "human-input")) is None
|
||||
assert session.scalar(select(EndUser).limit(1)) is None
|
||||
|
||||
|
||||
def test_validate_upload_token_returns_account_owner_and_record_file_link(session_maker) -> None:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user