diff --git a/api/Dockerfile b/api/Dockerfile
index 311bc51df15..86eef5d329f 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -33,7 +33,6 @@ RUN uv sync --frozen --no-dev --no-editable
FROM base AS production
ENV FLASK_APP=app.py
-ENV EDITION=SELF_HOSTED
ENV DEPLOY_ENV=PRODUCTION
ENV CONSOLE_API_URL=http://127.0.0.1:5001
ENV CONSOLE_WEB_URL=http://127.0.0.1:3000
diff --git a/api/app_factory.py b/api/app_factory.py
index e941389c1b2..2a08aeeed3e 100644
--- a/api/app_factory.py
+++ b/api/app_factory.py
@@ -14,6 +14,7 @@ from contexts.wrapper import RecyclableContextVar
from controllers.console.error import UnauthorizedAndForceLogout
from core.logging.context import init_request_context
from dify_app import DifyApp
+from enums import DeploymentEdition
from extensions.ext_socketio import sio
from services.enterprise.enterprise_service import EnterpriseService
from services.entities.feature_entities import LicenseStatus
@@ -112,7 +113,7 @@ def create_flask_app_with_configs() -> DifyApp:
init_request_context()
RecyclableContextVar.increment_thread_recycles()
- if dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
surface = _match_license_gated_surface(request.path)
if surface is not None:
try:
diff --git a/api/commands/retention.py b/api/commands/retention.py
index d03c9bcc6da..f94dbf25bb6 100644
--- a/api/commands/retention.py
+++ b/api/commands/retention.py
@@ -10,6 +10,8 @@ import click
import sqlalchemy as sa
from sqlalchemy.orm import Session, sessionmaker
+from configs import dify_config
+from enums import CloudPlan, DeploymentEdition
from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs
@@ -126,14 +128,12 @@ def _get_archive_candidate_tenant_ids_by_prefix(
def _filter_paid_workflow_archive_tenant_ids(tenant_ids: list[str]) -> tuple[list[str], list[str]]:
- from configs import dify_config
- from enums.cloud_plan import CloudPlan
from services.billing_service import BillingService
tenant_ids = sorted(set(tenant_ids))
if not tenant_ids:
return [], []
- if not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return tenant_ids, []
plans = BillingService.get_plan_bulk_with_cache(tenant_ids)
@@ -1462,7 +1462,7 @@ def cleanup_orphaned_draft_variables(
"--graceful-period",
default=21,
show_default=True,
- help="Graceful period in days after subscription expiration, will be ignored when billing is disabled.",
+ help="Graceful period in days after subscription expiration; ignored outside the Cloud edition.",
)
@click.option("--dry-run", is_flag=True, default=False, help="Show messages logs would be cleaned without deleting")
def clean_expired_messages(
@@ -1514,8 +1514,8 @@ def clean_expired_messages(
if from_days_ago <= before_days:
raise click.UsageError("--from-days-ago must be greater than --before-days.")
- # Create policy based on billing configuration
- # NOTE: graceful_period will be ignored when billing is disabled.
+ # Create the policy for the configured deployment edition.
+ # NOTE: graceful_period is ignored outside the Cloud edition.
policy = create_message_clean_policy(graceful_period_days=graceful_period)
if from_days_ago is not None and before_days is not None:
diff --git a/api/commands/system.py b/api/commands/system.py
index da0c4f21b6d..2dee41e4d3f 100644
--- a/api/commands/system.py
+++ b/api/commands/system.py
@@ -6,7 +6,7 @@ from sqlalchemy import delete, select, update
from sqlalchemy.orm import sessionmaker
from configs import dify_config
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from events.app_event import app_was_created
from extensions.ext_database import db
from extensions.ext_redis import redis_client
diff --git a/api/configs/app_config.py b/api/configs/app_config.py
index be2f3c7c0e5..29828a3be0d 100644
--- a/api/configs/app_config.py
+++ b/api/configs/app_config.py
@@ -5,7 +5,6 @@ from typing import Any, override
from pydantic.fields import FieldInfo
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource
-from enums.deployment_edition import DeploymentEdition
from libs.file_utils import search_file_upwards
from .deploy import DeploymentConfig
@@ -117,11 +116,3 @@ class DifyConfig(
),
),
)
-
- @property
- def DEPLOYMENT_EDITION(self) -> DeploymentEdition:
- if self.EDITION == "CLOUD":
- return DeploymentEdition.CLOUD
- if self.ENTERPRISE_ENABLED:
- return DeploymentEdition.ENTERPRISE
- return DeploymentEdition.COMMUNITY
diff --git a/api/configs/deploy/__init__.py b/api/configs/deploy/__init__.py
index 145e9fc5638..491157f694e 100644
--- a/api/configs/deploy/__init__.py
+++ b/api/configs/deploy/__init__.py
@@ -1,8 +1,8 @@
-from typing import Literal
-
from pydantic import Field
from pydantic_settings import BaseSettings
+from enums import DeploymentEdition
+
class DeploymentConfig(BaseSettings):
"""
@@ -25,9 +25,9 @@ class DeploymentConfig(BaseSettings):
default=False,
)
- EDITION: Literal["SELF_HOSTED", "CLOUD"] = Field(
- description="Deployment edition of the application (e.g., 'SELF_HOSTED', 'CLOUD')",
- default="SELF_HOSTED",
+ DEPLOYMENT_EDITION: DeploymentEdition = Field(
+ description="Product edition of the application.",
+ default=DeploymentEdition.COMMUNITY,
)
DEPLOY_ENV: str = Field(
diff --git a/api/configs/enterprise/__init__.py b/api/configs/enterprise/__init__.py
index a6afbefce91..d839b840d1c 100644
--- a/api/configs/enterprise/__init__.py
+++ b/api/configs/enterprise/__init__.py
@@ -8,12 +8,6 @@ class EnterpriseFeatureConfig(BaseSettings):
**Before using, please contact business@dify.ai by email to inquire about licensing matters.**
"""
- ENTERPRISE_ENABLED: bool = Field(
- description="Enable or disable enterprise-level features."
- "Before using, please contact business@dify.ai by email to inquire about licensing matters.",
- default=False,
- )
-
WEBAPP_PUBLIC_ACCESS_ENABLED: bool = Field(
description="Whether admins are allowed to set a webapp's access mode to public (anyone with the link, "
"no auth). Disable in security-sensitive on-prem deployments.",
@@ -59,7 +53,7 @@ class EnterpriseTelemetryConfig(BaseSettings):
"""
ENTERPRISE_TELEMETRY_ENABLED: bool = Field(
- description="Enable enterprise telemetry collection (also requires ENTERPRISE_ENABLED=true).",
+ description="Enable enterprise telemetry collection for enterprise deployments.",
default=False,
)
diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py
index 82c46cf8d26..ea2b4cacf80 100644
--- a/api/configs/feature/__init__.py
+++ b/api/configs/feature/__init__.py
@@ -810,17 +810,6 @@ class ModelLoadBalanceConfig(BaseSettings):
)
-class BillingConfig(BaseSettings):
- """
- Configuration for platform billing features
- """
-
- BILLING_ENABLED: bool = Field(
- description="Enable or disable billing functionality",
- default=False,
- )
-
-
class UpdateConfig(BaseSettings):
"""
Configuration for application update checks
@@ -1621,7 +1610,6 @@ class FeatureConfig(
# place the configs in alphabet order
AppExecutionConfig,
AuthConfig, # Changed from OAuthConfig to AuthConfig
- BillingConfig,
CodeExecutionSandboxConfig,
CreatorsPlatformConfig,
TriggerConfig,
diff --git a/api/controllers/common/fields.py b/api/controllers/common/fields.py
index 7faa2cf8542..65d547a349f 100644
--- a/api/controllers/common/fields.py
+++ b/api/controllers/common/fields.py
@@ -126,12 +126,6 @@ class UsageCountResponse(ResponseModel):
count: int
-class IndexInfoResponse(ResponseModel):
- welcome: str
- api_version: str
- server_version: str
-
-
class AvatarUrlResponse(ResponseModel):
avatar_url: str
diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py
index d29e43f7f09..07c9d30d997 100644
--- a/api/controllers/console/auth/activate.py
+++ b/api/controllers/console/auth/activate.py
@@ -10,6 +10,7 @@ from controllers.console import console_ns
from controllers.console.auth.error import InvitationAccountMismatchError
from controllers.console.error import AccountInFreezeError, AlreadyActivateError
from controllers.console.wraps import model_validate
+from enums import DeploymentEdition
from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from libs.helper import EmailStr, timezone
@@ -161,7 +162,9 @@ class ActivateApi(Resource):
if current_account.id != account.id:
raise InvitationAccountMismatchError()
- if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(account.email):
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(
+ account.email
+ ):
raise AccountInFreezeError()
tenant = invitation["tenant"]
diff --git a/api/controllers/console/auth/email_register.py b/api/controllers/console/auth/email_register.py
index 6ee65e87344..b3377bb7019 100644
--- a/api/controllers/console/auth/email_register.py
+++ b/api/controllers/console/auth/email_register.py
@@ -15,6 +15,7 @@ from controllers.console.auth.error import (
InvalidTokenError,
PasswordMismatchError,
)
+from enums import DeploymentEdition
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.helper import EmailStr, extract_remote_ip
@@ -98,7 +99,9 @@ class EmailRegisterSendEmailApi(Resource):
if req_data.language is not None and req_data.language in languages:
language = req_data.language
- if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(normalized_email):
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(
+ normalized_email
+ ):
raise AccountInFreezeError()
account = AccountService.get_account_by_email_with_case_fallback(req_data.email, session=db.session())
diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py
index 9363bc849eb..2b3ae3e6afd 100644
--- a/api/controllers/console/auth/login.py
+++ b/api/controllers/console/auth/login.py
@@ -43,6 +43,7 @@ from controllers.console.wraps import (
setup_required,
with_current_user,
)
+from enums import DeploymentEdition
from extensions.ext_database import db
from libs.helper import EmailStr, extract_remote_ip
from libs.helper import timezone as validate_timezone_string
@@ -121,7 +122,9 @@ class LoginApi(Resource):
request_email = req_data.email
normalized_email = request_email.lower()
- if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(normalized_email):
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(
+ normalized_email
+ ):
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.ACCOUNT_IN_FREEZE)
raise AccountInFreezeError()
@@ -160,7 +163,6 @@ class LoginApi(Resource):
AccountService.add_login_error_rate_limit(normalized_email)
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.INVALID_CREDENTIALS)
raise AuthenticationFailedError() from exc
- # SELF_HOSTED only have one workspace
tenants = TenantService.get_join_tenants(account, session=db.session())
if len(tenants) == 0:
if (
diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py
index c6b80fbb392..0e1b28c58f7 100644
--- a/api/controllers/console/auth/oauth.py
+++ b/api/controllers/console/auth/oauth.py
@@ -12,6 +12,7 @@ from configs import dify_config
from constants.languages import languages
from controllers.common.fields import RedirectResponse
from controllers.common.schema import query_params_from_model, register_response_schema_model, register_schema_models
+from enums import DeploymentEdition
from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from libs.helper import extract_remote_ip
@@ -301,7 +302,9 @@ def _generate_account(
normalized_email = user_info.email.lower()
oauth_new_user = True
if not FeatureService.get_system_features().is_allow_register:
- if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(normalized_email):
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(
+ normalized_email
+ ):
raise AccountRegisterError(
description=(
"This email account has been deleted within the past "
diff --git a/api/controllers/console/billing/billing.py b/api/controllers/console/billing/billing.py
index aa69f48dbe4..bee3cdddbef 100644
--- a/api/controllers/console/billing/billing.py
+++ b/api/controllers/console/billing/billing.py
@@ -15,7 +15,7 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user,
)
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.login import login_required
diff --git a/api/controllers/console/flask_admission.py b/api/controllers/console/flask_admission.py
index debbc19940c..d7ce23b67e1 100644
--- a/api/controllers/console/flask_admission.py
+++ b/api/controllers/console/flask_admission.py
@@ -9,7 +9,7 @@ from flask import Response, abort, request
from configs import dify_config
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
from core.logging.context import get_request_id, get_trace_id
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from libs.login import current_account_with_tenant, login_required
from machinery.context import RequestContext
diff --git a/api/controllers/console/init_validate.py b/api/controllers/console/init_validate.py
index aca22c67f8e..9d9e9a813e8 100644
--- a/api/controllers/console/init_validate.py
+++ b/api/controllers/console/init_validate.py
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
from configs import dify_config
from controllers.fastopenapi import console_router
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from extensions.ext_database import db
from models.model import DifySetup
from services.account_service import TenantService
diff --git a/api/controllers/console/setup.py b/api/controllers/console/setup.py
index 4c1b2e1398d..38e3325d6d4 100644
--- a/api/controllers/console/setup.py
+++ b/api/controllers/console/setup.py
@@ -73,7 +73,7 @@ def setup_system(payload: SetupRequestPayload) -> SetupResponse:
"""Initialize system setup with admin account.
NOTE: This endpoint is unauthenticated by design for first-time bootstrap.
- Access is restricted by deployment mode (`SELF_HOSTED`), one-time setup guards,
+ Access is restricted to self-hosted editions (`COMMUNITY` and `ENTERPRISE`), one-time setup guards,
and init-password validation rather than user session authentication.
"""
try:
diff --git a/api/controllers/console/workflow_run_archive.py b/api/controllers/console/workflow_run_archive.py
index 83f0ece1b6f..170d5268a6f 100644
--- a/api/controllers/console/workflow_run_archive.py
+++ b/api/controllers/console/workflow_run_archive.py
@@ -11,7 +11,6 @@ from controllers.common.schema import register_response_schema_models, register_
from controllers.console import console_ns
from controllers.console.wraps import (
account_initialization_required,
- cloud_edition_billing_enabled,
cloud_edition_billing_paid_plan_required,
model_validate,
only_edition_cloud,
@@ -122,7 +121,6 @@ class WorkflowRunArchivesApi(Resource):
@login_required
@account_initialization_required
@only_edition_cloud
- @cloud_edition_billing_enabled
@cloud_edition_billing_paid_plan_required
def get(self):
tenant_id, _ = _current_owner_or_admin_ids()
@@ -143,7 +141,6 @@ class WorkflowRunArchiveDownloadsApi(Resource):
@login_required
@account_initialization_required
@only_edition_cloud
- @cloud_edition_billing_enabled
@cloud_edition_billing_paid_plan_required
@model_validate(WorkflowRunArchiveDownloadPayload)
def post(self, req_data: WorkflowRunArchiveDownloadPayload):
@@ -170,7 +167,6 @@ class WorkflowRunArchiveDownloadApi(Resource):
@login_required
@account_initialization_required
@only_edition_cloud
- @cloud_edition_billing_enabled
@cloud_edition_billing_paid_plan_required
def get(self, download_id: str):
tenant_id, _ = _current_owner_or_admin_ids()
@@ -195,7 +191,6 @@ class WorkflowRunArchiveDownloadFileApi(Resource):
@login_required
@account_initialization_required
@only_edition_cloud
- @cloud_edition_billing_enabled
@cloud_edition_billing_paid_plan_required
def get(self, download_id: str):
tenant_id, _ = _current_owner_or_admin_ids()
diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py
index a1fcbf976a2..548243e43a9 100644
--- a/api/controllers/console/workspace/account.py
+++ b/api/controllers/console/workspace/account.py
@@ -43,7 +43,6 @@ from controllers.console.workspace.error import (
)
from controllers.console.wraps import (
account_initialization_required,
- cloud_edition_billing_enabled,
enable_change_email,
enterprise_license_required,
model_validate,
@@ -51,7 +50,7 @@ from controllers.console.wraps import (
setup_required,
with_current_user,
)
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from extensions.ext_database import db
from fields.base import ResponseModel
from fields.member_fields import AccountResponse
@@ -541,7 +540,6 @@ class EducationVerifyApi(Resource):
@login_required
@account_initialization_required
@only_edition_cloud
- @cloud_edition_billing_enabled
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationVerifyResponse.__name__])
@with_current_user
def get(self, account: Account):
@@ -559,7 +557,6 @@ class EducationApi(Resource):
@login_required
@account_initialization_required
@only_edition_cloud
- @cloud_edition_billing_enabled
@with_current_user
def post(self, account: Account):
raise EducationDiscountTemporarilyPausedError()
@@ -568,7 +565,6 @@ class EducationApi(Resource):
@login_required
@account_initialization_required
@only_edition_cloud
- @cloud_edition_billing_enabled
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationStatusResponse.__name__])
@with_current_user
def get(self, account: Account):
@@ -586,7 +582,6 @@ class EducationAutoCompleteApi(Resource):
@login_required
@account_initialization_required
@only_edition_cloud
- @cloud_edition_billing_enabled
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationAutocompleteResponse.__name__])
def get(self):
payload = request.args.to_dict(flat=True)
diff --git a/api/controllers/console/workspace/members.py b/api/controllers/console/workspace/members.py
index ae7eb9d2ac1..ebc30c9fa59 100644
--- a/api/controllers/console/workspace/members.py
+++ b/api/controllers/console/workspace/members.py
@@ -32,6 +32,7 @@ from controllers.console.wraps import (
setup_required,
with_current_user,
)
+from enums import DeploymentEdition
from extensions.ext_application_services import application_services
from extensions.ext_database import db
from extensions.ext_redis import redis_client
@@ -179,7 +180,7 @@ def _check_member_invite_limits(tenant_id: str, new_member_count: int, new_accou
features = FeatureService.get_features(tenant_id=tenant_id, exclude_vector_space=True)
- if dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
workspace_members = features.workspace_members
if workspace_members.enabled is True and not workspace_members.is_available(new_member_count):
raise WorkspaceMembersLimitExceeded()
@@ -189,7 +190,7 @@ def _check_member_invite_limits(tenant_id: str, new_member_count: int, new_accou
raise SeatsLimitExceeded()
return
- if dify_config.BILLING_ENABLED and features.billing.enabled is True:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
members = features.members
current_member_count = _count_current_members(tenant_id)
if 0 < members.limit < current_member_count + new_member_count:
@@ -265,7 +266,10 @@ class MemberInviteEmailApi(Resource):
tenant_id = inviter.current_tenant.id
with redis_client.lock(f"workspace_member_invite:{tenant_id}", timeout=60):
- if dify_config.ENTERPRISE_ENABLED is True or dify_config.BILLING_ENABLED is True:
+ if dify_config.DEPLOYMENT_EDITION in {
+ DeploymentEdition.CLOUD,
+ DeploymentEdition.ENTERPRISE,
+ }:
new_member_count, new_account_count = _count_new_member_invites(tenant_id, invitee_emails)
_check_member_invite_limits(tenant_id, new_member_count, new_account_count)
diff --git a/api/controllers/console/workspace/rbac.py b/api/controllers/console/workspace/rbac.py
index f446b56fc58..c60f335762a 100644
--- a/api/controllers/console/workspace/rbac.py
+++ b/api/controllers/console/workspace/rbac.py
@@ -14,6 +14,7 @@ from controllers.console import console_ns
from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate, rbac_permission_required
from core.db.session_factory import session_factory
from core.rbac import RBACResourceWhitelistScope
+from enums import DeploymentEdition
from extensions.ext_database import db
from libs.login import current_account_with_tenant, login_required
from models import Account
@@ -316,14 +317,16 @@ class RBACRolesApi(Resource):
options = req_data.to_inner_options()
if not dify_config.RBAC_ENABLED:
result = _legacy_workspace_roles(
- options, include_owner=req_data.include_owner, billing_enabled=dify_config.BILLING_ENABLED
+ options,
+ include_owner=req_data.include_owner,
+ billing_enabled=dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD,
)
else:
result = svc.RBACService.Roles.list(
tenant_id,
account_id,
include_owner=req_data.include_owner,
- biiling_enabled=dify_config.BILLING_ENABLED,
+ biiling_enabled=dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD,
options=options,
)
@@ -351,7 +354,12 @@ class RBACRoleItemApi(Resource):
def get(self, role_id):
tenant_id, account_id = _current_ids()
return _dump(
- svc.RBACService.Roles.get(tenant_id, account_id, role_id, billing_enabled=dify_config.BILLING_ENABLED)
+ svc.RBACService.Roles.get(
+ tenant_id,
+ account_id,
+ role_id,
+ billing_enabled=dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD,
+ )
)
@login_required
diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py
index e63746f66b4..68daf17d535 100644
--- a/api/controllers/console/workspace/tool_providers.py
+++ b/api/controllers/console/workspace/tool_providers.py
@@ -61,6 +61,7 @@ from core.tools.entities.tool_entities import (
ToolProviderType,
WorkflowToolParameterConfiguration,
)
+from enums import DeploymentEdition
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.helper import alphanumeric, dump_response, uuid_value
@@ -282,10 +283,10 @@ def _resolve_identity_mode(requested: IdentityMode | None, *, current: IdentityM
can never imply forwarding that the runtime won't perform. This gates the
API surface to match the backend gate in
``MCPTool._forwarding_requested`` — both the API and the backend
- invocation must be gated on ``dify_config.ENTERPRISE_ENABLED``.
+ invocation must be gated on the Enterprise deployment edition.
"""
mode = current if requested is None else requested
- if mode != IdentityMode.OFF and not dify_config.ENTERPRISE_ENABLED:
+ if mode != IdentityMode.OFF and dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
return IdentityMode.OFF
return mode
@@ -1417,7 +1418,7 @@ class ToolProviderMCPApi(Resource):
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
# Resolve "leave unchanged" (None) against the stored value, and gate
- # the result on ENTERPRISE_ENABLED — both are API-layer concerns, so
+ # the result on the Enterprise edition — both are API-layer concerns, so
# the service receives a concrete IdentityMode.
existing = service.get_provider(provider_id=req_data.provider_id, tenant_id=current_tenant_id)
identity_mode = _resolve_identity_mode(req_data.identity_mode, current=IdentityMode(existing.identity_mode))
diff --git a/api/controllers/console/wraps.py b/api/controllers/console/wraps.py
index 93fa2ced474..6122670f715 100644
--- a/api/controllers/console/wraps.py
+++ b/api/controllers/console/wraps.py
@@ -19,8 +19,7 @@ from controllers.common.wraps import (
)
from controllers.console.auth.error import AuthenticationFailedError, EmailCodeError
from controllers.console.workspace.error import AccountNotInitializedError
-from enums.cloud_plan import CloudPlan
-from enums.deployment_edition import DeploymentEdition
+from enums import CloudPlan, DeploymentEdition
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from libs.encryption import FieldEncryption
@@ -142,7 +141,7 @@ def only_edition_cloud[**P, R](view: Callable[P, R]) -> Callable[P, R]:
def only_edition_enterprise[**P, R](view: Callable[P, R]) -> Callable[P, R]:
@wraps(view)
def decorated(*args: P.args, **kwargs: P.kwargs):
- if not dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
abort(404)
return view(*args, **kwargs)
@@ -161,16 +160,6 @@ def only_edition_self_hosted[**P, R](view: Callable[P, R]) -> Callable[P, R]:
return decorated
-def cloud_edition_billing_enabled[**P, R](view: Callable[P, R]) -> Callable[P, R]:
- @wraps(view)
- def decorated(*args: P.args, **kwargs: P.kwargs):
- if not dify_config.BILLING_ENABLED:
- abort(403, "Billing feature is not enabled.")
- return view(*args, **kwargs)
-
- return decorated
-
-
def cloud_edition_billing_paid_plan_required[**P, R](view: Callable[P, R]) -> Callable[P, R]:
@wraps(view)
def decorated(*args: P.args, **kwargs: P.kwargs):
@@ -192,7 +181,7 @@ def cloud_edition_billing_resource_check[**P, R](resource: str) -> Callable[[Cal
def decorated(*args: P.args, **kwargs: P.kwargs):
_, current_tenant_id = current_account_with_tenant()
if resource == "vector_space":
- if not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return view(*args, **kwargs)
vector_space = FeatureService.get_vector_space(current_tenant_id)
@@ -301,7 +290,7 @@ def cloud_utm_record[**P, R](view: Callable[P, R]) -> Callable[P, R]:
def decorated(*args: P.args, **kwargs: P.kwargs):
with contextlib.suppress(Exception):
utm_info = request.cookies.get("utm_info")
- if dify_config.BILLING_ENABLED and utm_info:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and utm_info:
_, current_tenant_id = current_account_with_tenant()
utm_info_dict: UtmInfo = json.loads(utm_info)
OperationService.record_utm(current_tenant_id, utm_info_dict)
diff --git a/api/controllers/openapi/__init__.py b/api/controllers/openapi/__init__.py
index 0260422ec1c..681f93e49ba 100644
--- a/api/controllers/openapi/__init__.py
+++ b/api/controllers/openapi/__init__.py
@@ -139,7 +139,6 @@ register_response_schema_models(
register_enum_models(openapi_ns, OpenApiErrorCode)
from . import (
- _meta,
account,
app_dsl,
app_run,
@@ -157,7 +156,6 @@ from . import (
# Request models are imported from _models.py and registered above.
__all__ = [
- "_meta",
"account",
"app_dsl",
"app_run",
diff --git a/api/controllers/openapi/_meta.py b/api/controllers/openapi/_meta.py
deleted file mode 100644
index c49f7526acc..00000000000
--- a/api/controllers/openapi/_meta.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""Meta endpoint: `GET /openapi/v1/_version` — no auth.
-
-Returns the server's project version and edition so the difyctl CLI can probe
-compatibility without needing to be logged in. Mirrors the `_health` endpoint
-in `index.py`.
-"""
-
-from flask_restx import Resource
-
-from configs import dify_config
-from controllers.openapi import openapi_ns
-from controllers.openapi._contract import returns
-from controllers.openapi._models import ServerVersionResponse
-
-
-@openapi_ns.route("/_version")
-class VersionApi(Resource):
- @returns(200, ServerVersionResponse, description="Server version")
- def get(self):
- edition = dify_config.EDITION if dify_config.EDITION in ("SELF_HOSTED", "CLOUD") else "SELF_HOSTED"
- return ServerVersionResponse(
- version=dify_config.project.version,
- edition=edition,
- )
diff --git a/api/controllers/openapi/_models.py b/api/controllers/openapi/_models.py
index 5337612e7b6..c5e8a22d466 100644
--- a/api/controllers/openapi/_models.py
+++ b/api/controllers/openapi/_models.py
@@ -7,6 +7,7 @@ from typing import Any, Final, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
+from enums import DeploymentEdition
from libs.helper import EmailStr, UUIDStr, UUIDStrOrEmpty, uuid_value
from models.model import AppMode
@@ -258,7 +259,7 @@ class ServerVersionResponse(BaseModel):
"""Meta endpoint payload for `GET /openapi/v1/_version` — no auth required."""
version: str
- edition: Literal["SELF_HOSTED", "CLOUD"]
+ edition: DeploymentEdition
class HealthResponse(BaseModel):
diff --git a/api/controllers/openapi/apps_permitted_external.py b/api/controllers/openapi/apps_permitted_external.py
index e00ec5c87f7..a56ab4ae621 100644
--- a/api/controllers/openapi/apps_permitted_external.py
+++ b/api/controllers/openapi/apps_permitted_external.py
@@ -23,7 +23,8 @@ from controllers.openapi._models import (
)
from controllers.openapi.apps import build_app_describe_response
from controllers.openapi.auth.composition import auth_router
-from controllers.openapi.auth.data import AuthData, Edition
+from controllers.openapi.auth.data import AuthData
+from enums import DeploymentEdition
from libs.oauth_bearer import Scope, TokenType
from models import App
from models.enums import AppStatus
@@ -37,7 +38,7 @@ class PermittedExternalAppsListApi(Resource):
@auth_router.guard(
scope=Scope.APPS_READ_PERMITTED_EXTERNAL,
allowed_token_types=frozenset({TokenType.OAUTH_EXTERNAL_SSO}),
- edition=frozenset({Edition.EE}),
+ edition=frozenset({DeploymentEdition.ENTERPRISE}),
)
@returns(200, PermittedExternalAppsListResponse, description="Permitted external apps list")
@accepts(query=PermittedExternalAppsListQuery)
@@ -94,7 +95,7 @@ class PermittedExternalAppDescribeApi(Resource):
@auth_router.guard(
scope=Scope.APPS_READ_PERMITTED_EXTERNAL,
allowed_token_types=frozenset({TokenType.OAUTH_EXTERNAL_SSO}),
- edition=frozenset({Edition.EE}),
+ edition=frozenset({DeploymentEdition.ENTERPRISE}),
)
@returns(200, AppDescribeResponse, description="Permitted external app description")
@accepts(query=AppDescribeQuery)
diff --git a/api/controllers/openapi/auth/composition.py b/api/controllers/openapi/auth/composition.py
index 67f7001c080..9040058b85f 100644
--- a/api/controllers/openapi/auth/composition.py
+++ b/api/controllers/openapi/auth/composition.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from controllers.openapi.auth.conditions import (
- EDITION_EE,
+ EDITION_ENTERPRISE,
HAS_ALLOWED_ROLES,
HAS_RBAC,
LOADED_APP_IS_PRIVATE,
@@ -11,7 +11,6 @@ from controllers.openapi.auth.conditions import (
WORKSPACE_MEMBERSHIP_REQUIRED,
WORKSPACE_SCOPED,
)
-from controllers.openapi.auth.data import Edition
from controllers.openapi.auth.flow import When
from controllers.openapi.auth.pipeline import AuthPipeline, PipelineRoute, PipelineRouter
from controllers.openapi.auth.prepare import (
@@ -33,6 +32,7 @@ from controllers.openapi.auth.verify import (
check_workspace_mismatch,
check_workspace_role,
)
+from enums import DeploymentEdition
from libs.oauth_bearer import TokenType
account_pipeline = AuthPipeline(
@@ -42,7 +42,7 @@ account_pipeline = AuthPipeline(
When(WORKSPACE_MEMBERSHIP_REQUIRED, then=load_tenant_from_request),
load_account,
When(WORKSPACE_SCOPED, then=load_workspace_role),
- When(PATH_HAS_APP_ID & EDITION_EE, then=load_app_access_mode),
+ When(PATH_HAS_APP_ID & EDITION_ENTERPRISE, then=load_app_access_mode),
],
auth=[
When(PATH_HAS_APP_ID, then=check_app_api_enabled),
@@ -51,8 +51,8 @@ account_pipeline = AuthPipeline(
When(PATH_HAS_APP_ID, then=check_workspace_mismatch),
When(HAS_ALLOWED_ROLES, then=check_workspace_role),
When(HAS_RBAC, then=check_rbac_permission),
- When(PATH_HAS_APP_ID & EDITION_EE & WEBAPP_AUTH_ENABLED & WEBAPP_RUN_SCOPED, then=check_acl),
- When(EDITION_EE & LOADED_APP_IS_PRIVATE & WEBAPP_RUN_SCOPED, then=check_private_app_permission),
+ When(PATH_HAS_APP_ID & EDITION_ENTERPRISE & WEBAPP_AUTH_ENABLED & WEBAPP_RUN_SCOPED, then=check_acl),
+ When(EDITION_ENTERPRISE & LOADED_APP_IS_PRIVATE & WEBAPP_RUN_SCOPED, then=check_private_app_permission),
],
)
@@ -74,6 +74,9 @@ external_sso_pipeline = AuthPipeline(
auth_router = PipelineRouter(
{
TokenType.OAUTH_ACCOUNT: PipelineRoute(account_pipeline),
- TokenType.OAUTH_EXTERNAL_SSO: PipelineRoute(external_sso_pipeline, required_edition=frozenset({Edition.EE})),
+ TokenType.OAUTH_EXTERNAL_SSO: PipelineRoute(
+ external_sso_pipeline,
+ required_edition=frozenset({DeploymentEdition.ENTERPRISE}),
+ ),
}
)
diff --git a/api/controllers/openapi/auth/conditions.py b/api/controllers/openapi/auth/conditions.py
index 73a767b8d8e..a25eaf78aa1 100644
--- a/api/controllers/openapi/auth/conditions.py
+++ b/api/controllers/openapi/auth/conditions.py
@@ -2,7 +2,9 @@ from __future__ import annotations
from collections.abc import Callable
-from controllers.openapi.auth.data import AuthData, Edition, RequestContext, current_edition
+from configs import dify_config
+from controllers.openapi.auth.data import AuthData, RequestContext
+from enums import DeploymentEdition
from libs.oauth_bearer import Scope, TokenType
from services.enterprise.enterprise_service import WebAppAccessMode
from services.feature_service import FeatureService
@@ -44,9 +46,9 @@ TOKEN_IS_OAUTH_EXTERNAL_SSO = request_cond(lambda ctx: ctx.token_type == TokenTy
PATH_HAS_APP_ID = request_cond(lambda ctx: "app_id" in ctx.path_params)
-EDITION_CE = config_cond(lambda: current_edition() == Edition.CE)
-EDITION_EE = config_cond(lambda: current_edition() == Edition.EE)
-EDITION_SAAS = config_cond(lambda: current_edition() == Edition.SAAS)
+EDITION_COMMUNITY = config_cond(lambda: dify_config.DEPLOYMENT_EDITION == DeploymentEdition.COMMUNITY)
+EDITION_ENTERPRISE = config_cond(lambda: dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE)
+EDITION_CLOUD = config_cond(lambda: dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD)
WEBAPP_AUTH_ENABLED = config_cond(lambda: FeatureService.get_system_features().webapp_auth.enabled)
diff --git a/api/controllers/openapi/auth/data.py b/api/controllers/openapi/auth/data.py
index 898a9eb8f87..79d139841aa 100644
--- a/api/controllers/openapi/auth/data.py
+++ b/api/controllers/openapi/auth/data.py
@@ -6,34 +6,18 @@ from enum import StrEnum
from pydantic import BaseModel, ConfigDict, Field
from werkzeug.exceptions import InternalServerError
-from configs import dify_config
from core.rbac import RBACPermission, RBACResourceScope
-from enums.deployment_edition import DeploymentEdition
from libs.oauth_bearer import Scope, TokenType
from models.account import Account, Tenant, TenantAccountRole
from models.model import App, EndUser
from services.enterprise.enterprise_service import WebAppAccessMode
-class Edition(StrEnum):
- CE = "ce"
- EE = "ee"
- SAAS = "saas"
-
-
class CallerKind(StrEnum):
ACCOUNT = "account"
END_USER = "end_user"
-def current_edition() -> Edition:
- if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
- return Edition.SAAS
- if dify_config.ENTERPRISE_ENABLED:
- return Edition.EE
- return Edition.CE
-
-
class ExternalIdentity(BaseModel):
model_config = ConfigDict(frozen=True)
diff --git a/api/controllers/openapi/auth/pipeline.py b/api/controllers/openapi/auth/pipeline.py
index 17fdac45cd1..f27064eda96 100644
--- a/api/controllers/openapi/auth/pipeline.py
+++ b/api/controllers/openapi/auth/pipeline.py
@@ -16,16 +16,16 @@ from flask import current_app, request
from flask_login import user_logged_in
from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
+from configs import dify_config
from controllers.openapi._audit import emit_wrong_surface
from controllers.openapi.auth.data import (
AuthData,
- Edition,
ExternalIdentity,
RBACRequirement,
RequestContext,
- current_edition,
)
from controllers.openapi.auth.flow import When
+from enums import DeploymentEdition
from libs.oauth_bearer import (
AuthContext,
Scope,
@@ -112,7 +112,7 @@ class AuthPipeline:
@dataclass(frozen=True)
class PipelineRoute:
pipeline: AuthPipeline
- required_edition: frozenset[Edition] | None = None
+ required_edition: frozenset[DeploymentEdition] | None = None
class PipelineRouter:
@@ -131,7 +131,7 @@ class PipelineRouter:
*,
scope: Scope | None = None,
allowed_token_types: frozenset[TokenType] | None = None,
- edition: frozenset[Edition] | None = None,
+ edition: frozenset[DeploymentEdition] | None = None,
workspace_membership: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None,
@@ -150,7 +150,7 @@ class PipelineRouter:
*,
scope: Scope | None = None,
allowed_token_types: frozenset[TokenType] | None = None,
- edition: frozenset[Edition] | None = None,
+ edition: frozenset[DeploymentEdition] | None = None,
allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None,
) -> Callable:
@@ -168,7 +168,7 @@ class PipelineRouter:
*,
scope: Scope | None,
allowed_token_types: frozenset[TokenType] | None,
- edition: frozenset[Edition] | None,
+ edition: frozenset[DeploymentEdition] | None,
workspace_membership: bool,
allowed_roles: frozenset[TenantAccountRole] | None,
rbac: RBACRequirement | None,
@@ -200,17 +200,17 @@ class PipelineRouter:
*,
scope: Scope | None,
allowed_token_types: frozenset[TokenType] | None,
- edition: frozenset[Edition] | None,
+ edition: frozenset[DeploymentEdition] | None,
workspace_membership: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None,
) -> Any:
# 404 not 403 — this edition doesn't expose the feature at all
- if edition is not None and current_edition() not in edition:
+ if edition is not None and dify_config.DEPLOYMENT_EDITION not in edition:
raise NotFound()
license_checked = False
- if edition is not None and Edition.EE in edition:
+ if edition is not None and DeploymentEdition.ENTERPRISE in edition:
_check_license()
license_checked = True
@@ -234,9 +234,9 @@ class PipelineRouter:
raise Forbidden("unsupported_token_type")
if route.required_edition is not None:
- if current_edition() not in route.required_edition:
+ if dify_config.DEPLOYMENT_EDITION not in route.required_edition:
raise Forbidden("external_sso_requires_ee")
- if not license_checked and Edition.EE in route.required_edition:
+ if not license_checked and DeploymentEdition.ENTERPRISE in route.required_edition:
_check_license()
return route.pipeline._run(
diff --git a/api/controllers/openapi/index.py b/api/controllers/openapi/index.py
index 97e9c6e75d9..6f63232ad58 100644
--- a/api/controllers/openapi/index.py
+++ b/api/controllers/openapi/index.py
@@ -1,8 +1,11 @@
+"""Unauthenticated health and version probes for the OpenAPI surface."""
+
from flask_restx import Resource
+from configs import dify_config
from controllers.openapi import openapi_ns
from controllers.openapi._contract import returns
-from controllers.openapi._models import HealthResponse
+from controllers.openapi._models import HealthResponse, ServerVersionResponse
@openapi_ns.route("/_health")
@@ -10,3 +13,13 @@ class HealthApi(Resource):
@returns(200, HealthResponse, description="Health check")
def get(self):
return HealthResponse(ok=True)
+
+
+@openapi_ns.route("/_version")
+class VersionApi(Resource):
+ @returns(200, ServerVersionResponse, description="Server version")
+ def get(self):
+ return ServerVersionResponse(
+ version=dify_config.project.version,
+ edition=dify_config.DEPLOYMENT_EDITION,
+ )
diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py
index a75e4b391ec..c16118f7799 100644
--- a/api/controllers/service_api/app/completion.py
+++ b/api/controllers/service_api/app/completion.py
@@ -42,7 +42,7 @@ from core.errors.error import (
QuotaExceededError,
)
from core.helper.trace_id_helper import get_external_trace_id, get_trace_session_id, omit_trace_session_id_from_payload
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from graphon.model_runtime.errors.invoke import InvokeError
from libs import helper
from libs.helper import UUIDStrOrEmpty
@@ -376,7 +376,11 @@ class ChatApi(Resource):
payload = ChatRequestPayload.model_validate(omit_trace_session_id_from_payload(service_api_ns.payload) or {})
- if app_mode == AppMode.ADVANCED_CHAT and payload.workflow_id and dify_config.BILLING_ENABLED:
+ if (
+ app_mode == AppMode.ADVANCED_CHAT
+ and payload.workflow_id
+ and dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD
+ ):
billing_info = BillingService.get_info(app_model.tenant_id, exclude_vector_space=True)
if billing_info["enabled"] and billing_info["subscription"]["plan"] == CloudPlan.SANDBOX:
raise WorkflowVersionExecutionNotAllowedError()
diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py
index 1041c038d72..846a500adc3 100644
--- a/api/controllers/service_api/app/workflow.py
+++ b/api/controllers/service_api/app/workflow.py
@@ -45,7 +45,7 @@ from core.errors.error import (
QuotaExceededError,
)
from core.helper.trace_id_helper import get_external_trace_id, get_trace_session_id, omit_trace_session_id_from_payload
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from fields.base import ResponseModel
@@ -451,7 +451,7 @@ class WorkflowRunByIdApi(Resource):
if app_mode != AppMode.WORKFLOW:
raise NotWorkflowAppError()
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
billing_info = BillingService.get_info(app_model.tenant_id, exclude_vector_space=True)
if billing_info["enabled"] and billing_info["subscription"]["plan"] == CloudPlan.SANDBOX:
raise WorkflowVersionExecutionNotAllowedError()
diff --git a/api/controllers/service_api/index.py b/api/controllers/service_api/index.py
index 41f8ef53a5b..365a7fa252f 100644
--- a/api/controllers/service_api/index.py
+++ b/api/controllers/service_api/index.py
@@ -1,9 +1,16 @@
from flask_restx import Resource
from configs import dify_config
-from controllers.common.fields import IndexInfoResponse
from controllers.common.schema import register_response_schema_models
from controllers.service_api import service_api_ns
+from fields.base import ResponseModel
+
+
+class IndexInfoResponse(ResponseModel):
+ welcome: str
+ api_version: str
+ server_version: str
+
register_response_schema_models(service_api_ns, IndexInfoResponse)
@@ -12,8 +19,8 @@ register_response_schema_models(service_api_ns, IndexInfoResponse)
class IndexApi(Resource):
@service_api_ns.response(200, "Success", service_api_ns.models[IndexInfoResponse.__name__])
def get(self) -> dict[str, str]:
- return {
- "welcome": "Dify OpenAPI",
- "api_version": "v1",
- "server_version": dify_config.project.version,
- }
+ return IndexInfoResponse(
+ welcome="Dify OpenAPI",
+ api_version="v1",
+ server_version=dify_config.project.version,
+ ).model_dump(mode="json")
diff --git a/api/controllers/service_api/wraps.py b/api/controllers/service_api/wraps.py
index 8724e65e674..6d74c93ed59 100644
--- a/api/controllers/service_api/wraps.py
+++ b/api/controllers/service_api/wraps.py
@@ -22,7 +22,7 @@ from controllers.service_api.schema import (
USER_QUERY_PARAM,
USER_REQUIRED_ATTR,
)
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from libs.login import current_user
@@ -186,7 +186,7 @@ def cloud_edition_billing_resource_check[**P, R](
def decorated(*args: P.args, **kwargs: P.kwargs):
api_token = validate_and_get_api_token(api_token_type)
if resource == "vector_space":
- if not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return view(*args, **kwargs)
vector_space = FeatureService.get_vector_space(api_token.tenant_id)
diff --git a/api/controllers/trigger/webhook.py b/api/controllers/trigger/webhook.py
index 04b2b50bc4a..7715090b967 100644
--- a/api/controllers/trigger/webhook.py
+++ b/api/controllers/trigger/webhook.py
@@ -7,7 +7,7 @@ from werkzeug.exceptions import NotFound, RequestEntityTooLarge
from controllers.trigger import bp
from core.trigger.debug.event_bus import TriggerDebugEventBus
from core.trigger.debug.events import WebhookDebugEvent, build_webhook_pool_key
-from enums.quota_type import QuotaType
+from enums import QuotaType
from services.errors.app import QuotaExceededError
from services.trigger.webhook_service import RawWebhookDataDict, WebhookService
diff --git a/api/controllers/web/feature.py b/api/controllers/web/feature.py
index e92d9ceca43..fcaaac98e28 100644
--- a/api/controllers/web/feature.py
+++ b/api/controllers/web/feature.py
@@ -2,9 +2,9 @@ from flask_restx import Resource
from controllers.common.schema import register_response_schema_models
from controllers.web import web_ns
+from extensions.ext_application_services import application_services
from libs.helper import dump_response
from services.entities.feature_entities import SystemFeatureModel
-from services.feature_service import FeatureService
register_response_schema_models(web_ns, SystemFeatureModel)
@@ -30,4 +30,7 @@ class SystemFeatureApi(Resource):
Authentication configuration must be available before the authentication flow can be selected.
"""
- return dump_response(SystemFeatureModel, FeatureService.get_system_features())
+ return dump_response(
+ SystemFeatureModel,
+ application_services().feature_queries.get_system_features(),
+ )
diff --git a/api/controllers/web/login.py b/api/controllers/web/login.py
index 0aa42f43687..b841056743d 100644
--- a/api/controllers/web/login.py
+++ b/api/controllers/web/login.py
@@ -30,6 +30,7 @@ from controllers.console.wraps import (
)
from controllers.web import web_ns
from controllers.web.wraps import decode_jwt_token
+from enums import DeploymentEdition
from extensions.ext_database import db
from libs.helper import EmailStr, extract_remote_ip
from libs.passport import PassportService
@@ -146,8 +147,9 @@ class LoginStatusApi(Resource):
if not app_code:
return LoginStatusResponse(logged_in=bool(token), app_logged_in=False).model_dump(mode="json")
app_id = AppService.get_app_id_by_code(app_code, session=db.session())
- is_public = not dify_config.ENTERPRISE_ENABLED or not WebAppAuthService.is_app_require_permission_check(
- app_id=app_id, session=db.session()
+ is_public = (
+ dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE
+ or not WebAppAuthService.is_app_require_permission_check(app_id=app_id, session=db.session())
)
user_logged_in = False
diff --git a/api/controllers/web/site.py b/api/controllers/web/site.py
index 45a470659fe..827fdce72b0 100644
--- a/api/controllers/web/site.py
+++ b/api/controllers/web/site.py
@@ -8,7 +8,7 @@ from configs import dify_config
from controllers.common.schema import register_response_schema_models
from controllers.web import web_ns
from controllers.web.wraps import WebApiResource
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from extensions.ext_database import db
from extensions.storage.storage_type import StorageType
from fields.base import ResponseModel
diff --git a/api/core/hosting_configuration.py b/api/core/hosting_configuration.py
index 09473b8b78f..90c11376ec1 100644
--- a/api/core/hosting_configuration.py
+++ b/api/core/hosting_configuration.py
@@ -6,7 +6,7 @@ from pydantic import BaseModel
from configs import dify_config
from core.entities import DEFAULT_PLUGIN_ID
from core.entities.provider_entities import ProviderQuotaType, QuotaUnit, RestrictModel
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from graphon.model_runtime.entities.model_entities import ModelType
diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py
index 45bba181ba9..7526bc533cc 100644
--- a/api/core/indexing_runner.py
+++ b/api/core/indexing_runner.py
@@ -34,6 +34,7 @@ from core.rag.splitter.fixed_text_splitter import (
)
from core.rag.splitter.text_splitter import TextSplitter
from core.tools.utils.web_reader_tool import get_image_upload_file_ids
+from enums import DeploymentEdition
from extensions.ext_redis import redis_client
from extensions.ext_storage import storage
from graphon.model_runtime.entities.model_entities import ModelType
@@ -330,7 +331,7 @@ class IndexingRunner:
Estimate the indexing for the document.
"""
# check document limit
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
count = len(extract_settings)
batch_upload_limit = dify_config.BATCH_UPLOAD_LIMIT
if count > batch_upload_limit:
diff --git a/api/core/plugin/plugin_service.py b/api/core/plugin/plugin_service.py
index 410287e604f..0fdfd4d27b3 100644
--- a/api/core/plugin/plugin_service.py
+++ b/api/core/plugin/plugin_service.py
@@ -58,6 +58,7 @@ from core.plugin.impl.debugging import PluginDebuggingClient
from core.plugin.impl.endpoint import PluginEndpointClient
from core.plugin.impl.model import PluginModelClient
from core.plugin.impl.plugin import PluginInstaller
+from enums import DeploymentEdition
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from models.provider import Provider, ProviderCredential, TenantPreferredModelProvider
@@ -1256,7 +1257,7 @@ class PluginService:
PluginService.invalidate_plugin_model_providers_cache(tenant_id)
return result
- if dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
PluginManagerService.try_pre_uninstall_plugin(
PreUninstallPluginRequest(
tenant_id=tenant_id,
diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py
index 2a022c93814..4baeaed95bf 100644
--- a/api/core/provider_manager.py
+++ b/api/core/provider_manager.py
@@ -36,7 +36,7 @@ from core.helper.model_provider_cache import ProviderCredentialsCache, ProviderC
from core.helper.position_helper import is_filtered
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.entities.plugin_daemon import PluginModelProviderDeclaration
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from extensions import ext_hosting_provider
from extensions.ext_database import db
from extensions.ext_redis import redis_client
diff --git a/api/core/tools/mcp_tool/tool.py b/api/core/tools/mcp_tool/tool.py
index d5b62a61a9c..899ffe56e6b 100644
--- a/api/core/tools/mcp_tool/tool.py
+++ b/api/core/tools/mcp_tool/tool.py
@@ -25,6 +25,7 @@ from core.tools.__base.tool import Tool
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolProviderType
from core.tools.errors import ToolInvokeError
+from enums import DeploymentEdition
from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata
logger = logging.getLogger(__name__)
@@ -272,7 +273,7 @@ class MCPTool(Tool):
the deployment actually has the enterprise side that can mint tokens.
Non-enterprise installs treat the DB value as a no-op — a stale row
won't trigger a 5xx against a missing inner-API endpoint."""
- return self.identity_mode != IdentityMode.OFF and dify_config.ENTERPRISE_ENABLED
+ return self.identity_mode != IdentityMode.OFF and dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE
def invoke_remote_mcp_tool(
self,
diff --git a/api/docker/entrypoint.sh b/api/docker/entrypoint.sh
index 67832da826a..53c598a5174 100755
--- a/api/docker/entrypoint.sh
+++ b/api/docker/entrypoint.sh
@@ -31,13 +31,13 @@ if [[ "${MODE}" == "worker" ]]; then
CONCURRENCY_OPTION="-c ${CELERY_WORKER_AMOUNT:-1}"
fi
- # Configure queues based on edition if not explicitly set
+ # Configure queues based on product edition if not explicitly set
if [[ -z "${CELERY_QUEUES}" ]]; then
- if [[ "${EDITION}" == "CLOUD" ]]; then
+ if [[ "${DEPLOYMENT_EDITION:-COMMUNITY}" == "CLOUD" ]]; then
# Cloud edition: separate queues for dataset and trigger tasks
DEFAULT_QUEUES="api_token,dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,app_rbac,plugin,workflow_storage,conversation,workflow_professional,workflow_team,workflow_sandbox,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention,workflow_based_app_execution"
else
- # Community edition (SELF_HOSTED): dataset, pipeline and workflow have separate queues
+ # Self-hosted editions: dataset, pipeline and workflow have separate queues
DEFAULT_QUEUES="api_token,dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,app_rbac,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention,workflow_based_app_execution"
fi
else
diff --git a/api/enterprise/telemetry/README.md b/api/enterprise/telemetry/README.md
index e43c0b1ea29..4d0065a5416 100644
--- a/api/enterprise/telemetry/README.md
+++ b/api/enterprise/telemetry/README.md
@@ -32,7 +32,7 @@ The Enterprise OTEL exporter is configured via environment variables.
| Variable | Description | Default |
|----------|-------------|---------|
-| `ENTERPRISE_ENABLED` | Master switch for all enterprise features. | `false` |
+| `DEPLOYMENT_EDITION` | Product edition; enterprise telemetry is only available in `ENTERPRISE`. | `COMMUNITY` |
| `ENTERPRISE_TELEMETRY_ENABLED` | Master switch for enterprise telemetry. | `false` |
| `ENTERPRISE_OTLP_ENDPOINT` | OTLP collector endpoint (e.g., `http://otel-collector:4318`). | - |
| `ENTERPRISE_OTLP_HEADERS` | Custom headers for OTLP requests (e.g., `x-scope-orgid=tenant1`). | - |
diff --git a/api/enterprise/telemetry/exporter.py b/api/enterprise/telemetry/exporter.py
index 80959514f28..a177b4f12a4 100644
--- a/api/enterprise/telemetry/exporter.py
+++ b/api/enterprise/telemetry/exporter.py
@@ -42,12 +42,15 @@ from enterprise.telemetry.id_generator import (
set_correlation_id,
set_span_id_source,
)
+from enums import DeploymentEdition
logger = logging.getLogger(__name__)
def is_enterprise_telemetry_enabled() -> bool:
- return bool(dify_config.ENTERPRISE_ENABLED and dify_config.ENTERPRISE_TELEMETRY_ENABLED)
+ return bool(
+ dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE and dify_config.ENTERPRISE_TELEMETRY_ENABLED
+ )
def _parse_otlp_headers(raw: str) -> dict[str, str]:
diff --git a/api/enums/__init__.py b/api/enums/__init__.py
index e69de29bb2d..dd5d029f56f 100644
--- a/api/enums/__init__.py
+++ b/api/enums/__init__.py
@@ -0,0 +1,59 @@
+from enum import StrEnum, auto
+
+
+class CloudPlan(StrEnum):
+ """
+ Enum representing user plan types in the cloud platform.
+
+ SANDBOX: Free/default plan with limited features
+ PROFESSIONAL: Professional paid plan
+ TEAM: Team collaboration paid plan
+ """
+
+ SANDBOX = auto()
+ PROFESSIONAL = auto()
+ TEAM = auto()
+
+
+class DeploymentEdition(StrEnum):
+ """Enum representing the deployment edition of the platform."""
+
+ COMMUNITY = "COMMUNITY"
+ ENTERPRISE = "ENTERPRISE"
+ CLOUD = "CLOUD"
+
+
+class HostedTrialProvider(StrEnum):
+ """Enum representing hosted model provider names for trial access."""
+
+ OPENAI = "langgenius/openai/openai"
+ ANTHROPIC = "langgenius/anthropic/anthropic"
+ GEMINI = "langgenius/gemini/google"
+ X = "langgenius/x/x"
+ DEEPSEEK = "langgenius/deepseek/deepseek"
+ TONGYI = "langgenius/tongyi/tongyi"
+
+ @property
+ def config_key(self) -> str:
+ """Return the config key used in dify_config (e.g., HOSTED_{config_key}_PAID_ENABLED)."""
+ if self == HostedTrialProvider.X:
+ return "XAI"
+ return self.name
+
+
+class QuotaType(StrEnum):
+ """Supported quota types for tenant feature usage."""
+
+ TRIGGER = auto()
+ WORKFLOW = auto()
+ UNLIMITED = auto()
+
+ @property
+ def billing_key(self) -> str:
+ match self:
+ case QuotaType.TRIGGER:
+ return "trigger_event"
+ case QuotaType.WORKFLOW:
+ return "api_rate_limit"
+ case _:
+ raise ValueError(f"Invalid quota type: {self}")
diff --git a/api/enums/cloud_plan.py b/api/enums/cloud_plan.py
deleted file mode 100644
index 927cff5471a..00000000000
--- a/api/enums/cloud_plan.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from enum import StrEnum, auto
-
-
-class CloudPlan(StrEnum):
- """
- Enum representing user plan types in the cloud platform.
-
- SANDBOX: Free/default plan with limited features
- PROFESSIONAL: Professional paid plan
- TEAM: Team collaboration paid plan
- """
-
- SANDBOX = auto()
- PROFESSIONAL = auto()
- TEAM = auto()
diff --git a/api/enums/deployment_edition.py b/api/enums/deployment_edition.py
deleted file mode 100644
index 5541651b576..00000000000
--- a/api/enums/deployment_edition.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from enum import StrEnum
-
-
-class DeploymentEdition(StrEnum):
- """
- Enum representing the deployment edition of the platform.
- """
-
- COMMUNITY = "COMMUNITY"
- ENTERPRISE = "ENTERPRISE"
- CLOUD = "CLOUD"
diff --git a/api/enums/hosted_provider.py b/api/enums/hosted_provider.py
deleted file mode 100644
index c6d3715dc17..00000000000
--- a/api/enums/hosted_provider.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from enum import StrEnum
-
-
-class HostedTrialProvider(StrEnum):
- """
- Enum representing hosted model provider names for trial access.
- """
-
- OPENAI = "langgenius/openai/openai"
- ANTHROPIC = "langgenius/anthropic/anthropic"
- GEMINI = "langgenius/gemini/google"
- X = "langgenius/x/x"
- DEEPSEEK = "langgenius/deepseek/deepseek"
- TONGYI = "langgenius/tongyi/tongyi"
-
- @property
- def config_key(self) -> str:
- """Return the config key used in dify_config (e.g., HOSTED_{config_key}_PAID_ENABLED)."""
- if self == HostedTrialProvider.X:
- return "XAI"
- return self.name
diff --git a/api/enums/quota_type.py b/api/enums/quota_type.py
deleted file mode 100644
index a10ac21f69e..00000000000
--- a/api/enums/quota_type.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from enum import StrEnum, auto
-
-
-class QuotaType(StrEnum):
- """
- Supported quota types for tenant feature usage.
- """
-
- TRIGGER = auto()
- WORKFLOW = auto()
- UNLIMITED = auto()
-
- @property
- def billing_key(self) -> str:
- match self:
- case QuotaType.TRIGGER:
- return "trigger_event"
- case QuotaType.WORKFLOW:
- return "api_rate_limit"
- case _:
- raise ValueError(f"Invalid quota type: {self}")
diff --git a/api/events/event_handlers/queue_credential_sync_when_tenant_created.py b/api/events/event_handlers/queue_credential_sync_when_tenant_created.py
index 6566c214b05..a9f5e815018 100644
--- a/api/events/event_handlers/queue_credential_sync_when_tenant_created.py
+++ b/api/events/event_handlers/queue_credential_sync_when_tenant_created.py
@@ -1,4 +1,5 @@
from configs import dify_config
+from enums import DeploymentEdition
from events.tenant_event import tenant_was_created
from services.enterprise.workspace_sync import WorkspaceSyncService
@@ -7,7 +8,7 @@ from services.enterprise.workspace_sync import WorkspaceSyncService
def handle(sender, **kwargs):
"""Queue credential sync when a tenant/workspace is created."""
# Only queue sync tasks if plugin manager (enterprise feature) is enabled
- if not dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
return
tenant = sender
diff --git a/api/extensions/ext_application_services.py b/api/extensions/ext_application_services.py
index 8c4316b8146..dd1ead89a5d 100644
--- a/api/extensions/ext_application_services.py
+++ b/api/extensions/ext_application_services.py
@@ -10,7 +10,7 @@ from configs import dify_config
from constants.dsl_version import CURRENT_APP_DSL_VERSION
from core.db.session_factory import get_session_maker
from core.schemas.schema_manager import SchemaManager
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from extensions.ext_redis import RedisClientWrapper, redis_client
from repositories.explore_banner_query_repository import ExploreBannerQueryRepository
from repositories.installation_state_repository import InstallationStateRepository
diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py
index a96d50e1caf..a69971d7848 100644
--- a/api/extensions/ext_celery.py
+++ b/api/extensions/ext_celery.py
@@ -10,6 +10,7 @@ from typing_extensions import TypedDict
from configs import dify_config
from dify_app import DifyApp
+from enums import DeploymentEdition
from extensions.redis_names import normalize_redis_key_prefix
from extensions.workflow_warm_shutdown import setup_workflow_warm_shutdown_handler
@@ -276,8 +277,7 @@ def init_app(app: DifyApp) -> Celery:
}
if (
- dify_config.EDITION == "SELF_HOSTED"
- and not dify_config.ENTERPRISE_ENABLED
+ dify_config.DEPLOYMENT_EDITION == DeploymentEdition.COMMUNITY
and not dify_config.DISABLE_TELEMETRY
and not dify_config.DO_NOT_TRACK
and not dify_config.CI
@@ -288,7 +288,7 @@ def init_app(app: DifyApp) -> Celery:
"schedule": timedelta(minutes=dify_config.TELEMETRY_HEARTBEAT_INTERVAL_MINUTES),
}
- if dify_config.ENTERPRISE_ENABLED and dify_config.ENTERPRISE_TELEMETRY_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE and dify_config.ENTERPRISE_TELEMETRY_ENABLED:
imports.append("tasks.enterprise_telemetry_task")
celery_app.conf.update(beat_schedule=beat_schedule, imports=imports)
diff --git a/api/extensions/ext_enterprise_telemetry.py b/api/extensions/ext_enterprise_telemetry.py
index b3cfa01aee6..2d9bcbad6d1 100644
--- a/api/extensions/ext_enterprise_telemetry.py
+++ b/api/extensions/ext_enterprise_telemetry.py
@@ -4,7 +4,7 @@ Initializes the EnterpriseExporter singleton during ``create_app()``
(single-threaded), registers blinker event handlers, and hooks atexit
for graceful shutdown.
-Skipped entirely when either ``ENTERPRISE_ENABLED`` or ``ENTERPRISE_TELEMETRY_ENABLED``
+Skipped entirely outside the Enterprise edition or when ``ENTERPRISE_TELEMETRY_ENABLED``
is false (``is_enabled()`` gate).
"""
@@ -15,6 +15,7 @@ import logging
from typing import TYPE_CHECKING
from configs import dify_config
+from enums import DeploymentEdition
if TYPE_CHECKING:
from dify_app import DifyApp
@@ -26,7 +27,9 @@ _exporter: EnterpriseExporter | None = None
def is_enabled() -> bool:
- return bool(dify_config.ENTERPRISE_ENABLED and dify_config.ENTERPRISE_TELEMETRY_ENABLED)
+ return bool(
+ dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE and dify_config.ENTERPRISE_TELEMETRY_ENABLED
+ )
def init_app(app: DifyApp) -> None:
diff --git a/api/extensions/ext_otel.py b/api/extensions/ext_otel.py
index 63edbe93e79..ed607847b47 100644
--- a/api/extensions/ext_otel.py
+++ b/api/extensions/ext_otel.py
@@ -59,7 +59,7 @@ def init_app(app: DifyApp):
SERVICE_NAME: dify_config.APPLICATION_NAME,
SERVICE_VERSION: f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}",
PROCESS_PID: os.getpid(),
- DEPLOYMENT_ENVIRONMENT_NAME: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}",
+ DEPLOYMENT_ENVIRONMENT_NAME: f"{dify_config.DEPLOY_ENV}-{dify_config.DEPLOYMENT_EDITION.value}",
HOST_NAME: socket.gethostname(),
HOST_ARCH: platform.machine(),
"custom.deployment.git_commit": dify_config.COMMIT_SHA,
diff --git a/api/extensions/ext_sentry.py b/api/extensions/ext_sentry.py
index 1d4c071d5f5..19a153c6f78 100644
--- a/api/extensions/ext_sentry.py
+++ b/api/extensions/ext_sentry.py
@@ -1,5 +1,6 @@
from configs import dify_config
from dify_app import DifyApp
+from enums import DeploymentEdition
def init_app(app: DifyApp):
@@ -47,7 +48,7 @@ def init_app(app: DifyApp):
before_send=before_send,
)
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
# Cloud only. `opentelemetry.context.detach()` catches its own failures and reports
# them through `logger.exception`, so a double-detach surfaces as an error event
# rather than as a raised exception. Under gevent this can fire about once per
diff --git a/api/extensions/otel/parser/base.py b/api/extensions/otel/parser/base.py
index 621541d1ccb..fc10f183c99 100644
--- a/api/extensions/otel/parser/base.py
+++ b/api/extensions/otel/parser/base.py
@@ -3,7 +3,7 @@ Base parser interface and utilities for OpenTelemetry node parsers.
Content gating: ``should_include_content()`` controls whether content-bearing
span attributes (inputs, outputs, prompts, completions, documents) are written.
-Gate is only active in EE (``ENTERPRISE_ENABLED=True``) when
+Gate is only active in the Enterprise edition when
``ENTERPRISE_INCLUDE_CONTENT=False``; CE behaviour is unchanged.
"""
@@ -15,6 +15,7 @@ from opentelemetry.trace.status import Status, StatusCode
from pydantic import BaseModel
from configs import dify_config
+from enums import DeploymentEdition
from extensions.otel.semconv.gen_ai import ChainAttributes, GenAIAttributes
from graphon.enums import BuiltinNodeTypes
from graphon.file import File
@@ -26,9 +27,9 @@ from graphon.variables import Segment
def should_include_content() -> bool:
"""Return True if content should be written to spans.
- CE (ENTERPRISE_ENABLED=False): always True — no behaviour change.
+ Community and Cloud editions: always True — no behaviour change.
"""
- if not dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
return True
return dify_config.ENTERPRISE_INCLUDE_CONTENT
diff --git a/api/libs/oauth_bearer.py b/api/libs/oauth_bearer.py
index 36de4b85ae0..ed17503bb3b 100644
--- a/api/libs/oauth_bearer.py
+++ b/api/libs/oauth_bearer.py
@@ -25,6 +25,7 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden, ServiceUnavailable, Unauthorized
from configs import dify_config
+from enums import DeploymentEdition
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from libs.rate_limit import enforce_bearer_rate_limit
@@ -531,7 +532,7 @@ def require_workspace_member(ctx: AuthContext, tenant_id: str) -> None:
No-op on EE (gateway RBAC owns tenant isolation) and for SSO subjects
(no `tenant_account_joins` row by definition).
"""
- if dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
return
if ctx.subject_type != SubjectType.ACCOUNT or ctx.account_id is None:
return
diff --git a/api/libs/workspace_permission.py b/api/libs/workspace_permission.py
index 435b07dd6ea..969a81f5f9d 100644
--- a/api/libs/workspace_permission.py
+++ b/api/libs/workspace_permission.py
@@ -12,6 +12,7 @@ import logging
from werkzeug.exceptions import Forbidden
from configs import dify_config
+from enums import DeploymentEdition
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
@@ -32,8 +33,8 @@ def check_workspace_member_invite_permission(workspace_id: str) -> None:
Raises:
Forbidden: If either billing plan or workspace policy prohibits member invitations
"""
- # Check enterprise workspace policy level (only if enterprise enabled)
- if dify_config.ENTERPRISE_ENABLED:
+ # Check the enterprise workspace policy only in the Enterprise edition.
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
try:
permission = EnterpriseService.WorkspacePermissionService.get_permission(workspace_id)
if not permission.allow_member_invite:
@@ -62,8 +63,8 @@ def check_workspace_owner_transfer_permission(workspace_id: str) -> None:
if not features.is_allow_transfer_workspace:
raise Forbidden("Your current plan does not allow workspace ownership transfer")
- # Check enterprise workspace policy level (only if enterprise enabled)
- if dify_config.ENTERPRISE_ENABLED:
+ # Check the enterprise workspace policy only in the Enterprise edition.
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
try:
permission = EnterpriseService.WorkspacePermissionService.get_permission(workspace_id)
if not permission.allow_owner_transfer:
diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md
index c336a5596bb..383dcdc1a6d 100644
--- a/api/openapi/markdown/console-openapi.md
+++ b/api/openapi/markdown/console-openapi.md
@@ -15942,7 +15942,7 @@ Retrieval settings for Amazon Bedrock knowledge base queries.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
-| enabled | boolean | | Yes |
+| enabled | boolean | Deprecated. Use system features deployment_edition to determine the product edition. | Yes |
| subscription | [SubscriptionModel](#subscriptionmodel) | | Yes |
#### BillingResponse
@@ -24709,7 +24709,7 @@ FastOpenAPI proof of concept for Dify API
**Initialize system setup with admin account.
NOTE: This endpoint is unauthenticated by design for first-time bootstrap.
- Access is restricted by deployment mode (`SELF_HOSTED`), one-time setup guards,
+ Access is restricted to self-hosted editions (`COMMUNITY` and `ENTERPRISE`), one-time setup guards,
and init-password validation rather than user session authentication.
**
diff --git a/api/openapi/markdown/openapi-openapi.md b/api/openapi/markdown/openapi-openapi.md
index e67b1f87d10..cb89b267c00 100644
--- a/api/openapi/markdown/openapi-openapi.md
+++ b/api/openapi/markdown/openapi-openapi.md
@@ -648,6 +648,14 @@ mode is a closed enum of listable app types.
| ---- | ---- | ----------- | -------- |
| leaked_dependencies | [ [PluginDependency](#plugindependency) ] | | No |
+#### DeploymentEdition
+
+Enum representing the deployment edition of the platform.
+
+| Name | Type | Description | Required |
+| ---- | ---- | ----------- | -------- |
+| DeploymentEdition | string | Enum representing the deployment edition of the platform. | |
+
#### DeviceCodeRequest
| Name | Type | Description | Required |
@@ -974,7 +982,7 @@ Meta endpoint payload for `GET /openapi/v1/_version` — no auth required.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
-| edition | string,
**Available values:** "CLOUD", "SELF_HOSTED" | *Enum:* `"CLOUD"`, `"SELF_HOSTED"` | Yes |
+| edition | [DeploymentEdition](#deploymentedition) | | Yes |
| version | string | | Yes |
#### SessionListQuery
diff --git a/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/data_exporter/traceclient.py b/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/data_exporter/traceclient.py
index 00aab6bf891..374d20ade9d 100644
--- a/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/data_exporter/traceclient.py
+++ b/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/data_exporter/traceclient.py
@@ -53,7 +53,7 @@ class TraceClient:
attributes={
service_attributes.SERVICE_NAME: service_name,
service_attributes.SERVICE_VERSION: f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}",
- DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}",
+ DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.DEPLOYMENT_EDITION.value}",
HOST_NAME: socket.gethostname(),
ACS_ARMS_SERVICE_FEATURE: "genai_app",
}
diff --git a/api/providers/trace/trace-tencent/src/dify_trace_tencent/client.py b/api/providers/trace/trace-tencent/src/dify_trace_tencent/client.py
index be06ab4a36a..c616de5724b 100644
--- a/api/providers/trace/trace-tencent/src/dify_trace_tencent/client.py
+++ b/api/providers/trace/trace-tencent/src/dify_trace_tencent/client.py
@@ -81,7 +81,7 @@ class TencentTraceClient:
attributes={
service_attributes.SERVICE_NAME: service_name,
service_attributes.SERVICE_VERSION: f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}",
- DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}",
+ DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.DEPLOYMENT_EDITION.value}",
HOST_NAME: socket.gethostname(),
"telemetry.sdk.language": "python",
"telemetry.sdk.name": "opentelemetry",
diff --git a/api/providers/trace/trace-tencent/tests/unit_tests/tencent_trace/test_client.py b/api/providers/trace/trace-tencent/tests/unit_tests/tencent_trace/test_client.py
index 9b199b9b982..f0c2752ffec 100644
--- a/api/providers/trace/trace-tencent/tests/unit_tests/tencent_trace/test_client.py
+++ b/api/providers/trace/trace-tencent/tests/unit_tests/tencent_trace/test_client.py
@@ -16,6 +16,8 @@ from dify_trace_tencent.entities.tencent_trace_entity import SpanData
from opentelemetry.sdk.trace import Event
from opentelemetry.trace import SpanContext, Status, StatusCode, TraceFlags
+from enums import DeploymentEdition
+
metric_reader_instances: list[DummyMetricReader] = []
meter_provider_instances: list[DummyMeterProvider] = []
@@ -158,7 +160,7 @@ def patch_core_components(monkeypatch: pytest.MonkeyPatch) -> PatchedCoreCompone
project=SimpleNamespace(version="test"),
COMMIT_SHA="sha",
DEPLOY_ENV="dev",
- EDITION="cloud",
+ DEPLOYMENT_EDITION=DeploymentEdition.CLOUD,
)
monkeypatch.setattr(client_module, "dify_config", fake_config)
diff --git a/api/schedule/clean_messages.py b/api/schedule/clean_messages.py
index be5f483b959..6cad69f34e9 100644
--- a/api/schedule/clean_messages.py
+++ b/api/schedule/clean_messages.py
@@ -19,15 +19,14 @@ def clean_messages():
Clean expired messages based on clean policy.
This task uses MessagesCleanService to efficiently clean messages in batches.
- The behavior depends on BILLING_ENABLED configuration:
- - BILLING_ENABLED=True: only delete messages from sandbox tenants (with whitelist/grace period)
- - BILLING_ENABLED=False: delete all messages within the time range
+ Cloud only deletes messages from sandbox tenants (with whitelist/grace period).
+ Self-hosted editions delete all messages within the configured time range.
"""
click.echo(click.style("clean_messages: start clean messages.", fg="green"))
start_at = time.perf_counter()
try:
- # Create policy based on billing configuration
+ # Create policy based on deployment edition.
policy = create_message_clean_policy(
graceful_period_days=dify_config.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD,
)
diff --git a/api/schedule/clean_unused_datasets_task.py b/api/schedule/clean_unused_datasets_task.py
index 9bdb074647c..02770c83232 100644
--- a/api/schedule/clean_unused_datasets_task.py
+++ b/api/schedule/clean_unused_datasets_task.py
@@ -10,7 +10,7 @@ import app
from configs import dify_config
from core.db.session_factory import session_factory
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from extensions.ext_redis import redis_client
from libs.pagination import paginate_query
from models.dataset import Dataset, DatasetAutoDisableLog, DatasetQuery, Document
diff --git a/api/schedule/mail_clean_document_notify_task.py b/api/schedule/mail_clean_document_notify_task.py
index 1a76a4aa306..549cf9cef0d 100644
--- a/api/schedule/mail_clean_document_notify_task.py
+++ b/api/schedule/mail_clean_document_notify_task.py
@@ -8,7 +8,7 @@ from sqlalchemy import select
import app
from configs import dify_config
from core.db.session_factory import session_factory
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from extensions.ext_mail import mail
from libs.email_i18n import EmailType, get_email_i18n_service
from models import Account, Tenant, TenantAccountJoin
diff --git a/api/services/account_service.py b/api/services/account_service.py
index 5d41af5dc10..3f460275e20 100644
--- a/api/services/account_service.py
+++ b/api/services/account_service.py
@@ -22,6 +22,7 @@ from werkzeug.exceptions import Unauthorized
from configs import dify_config
from constants.languages import get_valid_language, language_timezone_mapping
+from enums import DeploymentEdition
from events.tenant_event import tenant_was_created
from extensions.ext_database import db
from extensions.ext_redis import redis_client, redis_fallback
@@ -119,7 +120,7 @@ class InvitationDetailDict(TypedDict):
def _try_join_enterprise_default_workspace(account_id: str) -> None:
"""Best-effort join to enterprise default workspace."""
- if not dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
return
from services.enterprise.enterprise_service import try_join_default_workspace
@@ -378,7 +379,7 @@ class AccountService:
payload = {
"user_id": account.id,
"exp": exp,
- "iss": dify_config.EDITION,
+ "iss": dify_config.DEPLOYMENT_EDITION.value,
"sub": "Console API Passport",
}
@@ -463,7 +464,7 @@ class AccountService:
if not FeatureService.get_license().seats.is_available():
raise SeatsLimitExceededError("licensed seats limit exceeded")
- if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email):
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(email):
raise AccountRegisterError(
description=(
"This email account has been deleted within the past "
@@ -1053,7 +1054,7 @@ class AccountService:
@classmethod
def get_user_through_email(cls, email: str, *, session: Session):
- if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email):
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(email):
raise AccountRegisterError(
description=(
"This email account has been deleted within the past "
@@ -1072,7 +1073,7 @@ class AccountService:
@classmethod
def is_account_in_freeze(cls, email: str) -> bool:
- if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email):
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(email):
return True
return False
@@ -1395,7 +1396,7 @@ class TenantService:
session.add(ta)
session.commit()
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
BillingService.clean_billing_info_cache(tenant.id)
return ta
@@ -1828,7 +1829,7 @@ class TenantService:
account_email,
)
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
BillingService.clean_billing_info_cache(tenant.id)
# Queue account deletion sync task for enterprise backend to reassign resources (enterprise only)
diff --git a/api/services/app_generate_service.py b/api/services/app_generate_service.py
index 80e6c574fd1..1fc9d7be2da 100644
--- a/api/services/app_generate_service.py
+++ b/api/services/app_generate_service.py
@@ -21,7 +21,7 @@ from core.app.features.rate_limiting import RateLimit
from core.app.features.rate_limiting.rate_limit import rate_limit_context
from core.app.layers.pause_state_persist_layer import PauseStateLayerConfig
from core.db import session_factory
-from enums.quota_type import QuotaType
+from enums import DeploymentEdition, QuotaType
from extensions.otel import AppGenerateHandler, trace_span
from models.model import Account, App, AppMode, EndUser
from models.workflow import Workflow, WorkflowRun
@@ -134,7 +134,7 @@ class AppGenerateService:
action: Callable[[RateLimit, str], Any],
):
quota_charge = unlimited()
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
try:
quota_charge = QuotaService.reserve(QuotaType.WORKFLOW, app_model.tenant_id)
except QuotaExceededError:
diff --git a/api/services/app_service.py b/api/services/app_service.py
index 1682de7e840..1b49983a219 100644
--- a/api/services/app_service.py
+++ b/api/services/app_service.py
@@ -19,6 +19,7 @@ from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.model_manager import ModelManager
from core.tools.tool_manager import ToolManager
from core.tools.utils.configuration import ToolParameterConfigurationManager
+from enums import DeploymentEdition
from events.app_event import app_was_created, app_was_deleted, app_was_updated
from extensions.ext_database import db # noqa: F401
from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType
@@ -652,7 +653,7 @@ class AppService:
# update web app setting as private
EnterpriseService.WebAppAuth.update_app_access_mode(app.id, "private")
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
BillingService.clean_billing_info_cache(app.tenant_id)
return app
@@ -1056,7 +1057,7 @@ class AppService:
if FeatureService.get_system_features().webapp_auth.enabled:
EnterpriseService.WebAppAuth.cleanup_webapp(app.id)
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
BillingService.clean_billing_info_cache(app.tenant_id)
# Trigger asynchronous deletion of app and related data
diff --git a/api/services/async_workflow_service.py b/api/services/async_workflow_service.py
index 012e979aff2..48a113e8fcc 100644
--- a/api/services/async_workflow_service.py
+++ b/api/services/async_workflow_service.py
@@ -14,7 +14,7 @@ from celery.result import AsyncResult
from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker
-from enums.quota_type import QuotaType
+from enums import QuotaType
from extensions.ext_database import db
from models.account import Account
from models.enums import CreatorUserRole, WorkflowTriggerStatus
diff --git a/api/services/billing_service.py b/api/services/billing_service.py
index 636828b9070..0519f66f2c6 100644
--- a/api/services/billing_service.py
+++ b/api/services/billing_service.py
@@ -12,7 +12,7 @@ from tenacity import retry, retry_if_exception_type, stop_before_delay, wait_fix
from werkzeug.exceptions import InternalServerError
from core.helper.http_client_pooling import get_pooled_http_client
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from extensions.ext_redis import redis_client
from libs.helper import RateLimiter
from models import Account, TenantAccountJoin, TenantAccountRole
diff --git a/api/services/clear_free_plan_tenant_expired_logs.py b/api/services/clear_free_plan_tenant_expired_logs.py
index 963938982d0..96885ebb225 100644
--- a/api/services/clear_free_plan_tenant_expired_logs.py
+++ b/api/services/clear_free_plan_tenant_expired_logs.py
@@ -10,7 +10,7 @@ from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session, sessionmaker
from configs import dify_config
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from extensions.ext_database import db
from extensions.ext_storage import storage
from graphon.model_runtime.utils.encoders import jsonable_encoder
@@ -371,7 +371,7 @@ class ClearFreePlanTenantExpiredLogs:
def process_tenant(flask_app: Flask, tenant_id: str):
try:
if (
- not dify_config.BILLING_ENABLED
+ dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD
or BillingService.get_info(tenant_id)["subscription"]["plan"] == CloudPlan.SANDBOX
):
# only process sandbox tenant
diff --git a/api/services/credit_pool_service.py b/api/services/credit_pool_service.py
index f398ed6e6e4..28df992f0ee 100644
--- a/api/services/credit_pool_service.py
+++ b/api/services/credit_pool_service.py
@@ -15,6 +15,7 @@ from sqlalchemy.orm import Session
from configs import dify_config
from core.errors.error import QuotaExceededError
+from enums import DeploymentEdition
from extensions.ext_redis import redis_client
from models import TenantCreditPool
from models.enums import ProviderQuotaType
@@ -51,7 +52,7 @@ class CreditPoolService:
@staticmethod
def _use_billing_quota() -> bool:
- return bool(dify_config.BILLING_ENABLED)
+ return bool(dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD)
@staticmethod
def _require_session(session: Session | None) -> Session:
diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py
index 884dfce2765..14e75b7016e 100644
--- a/api/services/dataset_service.py
+++ b/api/services/dataset_service.py
@@ -23,7 +23,7 @@ from core.model_manager import ModelManager
from core.rag.index_processor.constant.built_in_field import BuiltInField
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
from core.rag.retrieval.retrieval_methods import RetrievalMethod
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from events.dataset_event import dataset_was_deleted
from events.document_event import document_was_deleted
from extensions.ext_redis import redis_client
diff --git a/api/services/document_indexing_proxy/base.py b/api/services/document_indexing_proxy/base.py
index 02df6752f30..91835c8abb2 100644
--- a/api/services/document_indexing_proxy/base.py
+++ b/api/services/document_indexing_proxy/base.py
@@ -4,7 +4,7 @@ from collections.abc import Callable
from functools import cached_property
from typing import Any, ClassVar
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from services.feature_service import FeatureService
logger = logging.getLogger(__name__)
diff --git a/api/services/enterprise/account_deletion_sync.py b/api/services/enterprise/account_deletion_sync.py
index 89c4b80e670..57a306ed094 100644
--- a/api/services/enterprise/account_deletion_sync.py
+++ b/api/services/enterprise/account_deletion_sync.py
@@ -8,6 +8,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from configs import dify_config
+from enums import DeploymentEdition
from extensions.ext_redis import redis_client
from models.account import TenantAccountJoin
@@ -79,9 +80,9 @@ def sync_workspace_member_removal(workspace_id: str, member_id: str, *, source:
source: Source of the sync request (e.g., "workspace_member_removed")
Returns:
- bool: True if task was queued (or skipped in community), False if queueing failed
+ bool: True if task was queued (or skipped outside the Enterprise edition), False if queueing failed
"""
- if not dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
return True
return _queue_task(workspace_id=workspace_id, member_id=member_id, source=source)
@@ -100,9 +101,9 @@ def sync_account_deletion(account_id: str, *, source: str, session: Session) ->
session: SQLAlchemy session used to fetch workspace memberships
Returns:
- bool: True if all tasks were queued (or skipped in community), False if any queueing failed
+ bool: True if all tasks were queued (or skipped outside the Enterprise edition), False if any queueing failed
"""
- if not dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
return True
# Fetch all workspaces the account belongs to
diff --git a/api/services/enterprise/enterprise_service.py b/api/services/enterprise/enterprise_service.py
index 32e1dbefcd4..e8c768f20be 100644
--- a/api/services/enterprise/enterprise_service.py
+++ b/api/services/enterprise/enterprise_service.py
@@ -9,6 +9,7 @@ from cachetools.func import ttl_cache
from pydantic import BaseModel, ConfigDict, Field, model_validator
from configs import dify_config
+from enums import DeploymentEdition
from extensions.ext_redis import redis_client
from services.enterprise.base import (
EnterpriseRequest,
@@ -95,7 +96,7 @@ def try_join_default_workspace(account_id: str) -> None:
This is a best-effort integration. Failures must not block user registration.
"""
- if not dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
return
try:
@@ -373,9 +374,9 @@ class EnterpriseService:
caching, every request on an expired license would hit the enterprise API.
Returns:
- LicenseStatus enum value, or None if enterprise is disabled / unreachable.
+ LicenseStatus enum value, or None outside the Enterprise edition or when unreachable.
"""
- if not dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
return None
cached = cls._read_cached_license_status()
diff --git a/api/services/entities/feature_entities.py b/api/services/entities/feature_entities.py
index 19786ec6a51..a4ff3c57eba 100644
--- a/api/services/entities/feature_entities.py
+++ b/api/services/entities/feature_entities.py
@@ -4,8 +4,7 @@ from enum import StrEnum
from pydantic import BaseModel, ConfigDict, Field
-from enums.cloud_plan import CloudPlan
-from enums.deployment_edition import DeploymentEdition
+from enums import CloudPlan, DeploymentEdition
class FeatureResponseModel(BaseModel):
@@ -18,7 +17,13 @@ class SubscriptionModel(FeatureResponseModel):
class BillingModel(FeatureResponseModel):
- enabled: bool = False
+ # Deprecated compatibility field. Deployment edition is the only source of truth for product edition.
+ # TODO: Remove after clients migrate to `SystemFeatureModel.deployment_edition`.
+ enabled: bool = Field(
+ default=False,
+ deprecated=True,
+ description="Deprecated. Use system features deployment_edition to determine the product edition.",
+ )
subscription: SubscriptionModel = SubscriptionModel()
diff --git a/api/services/feature_query_service.py b/api/services/feature_query_service.py
index 5e9ea547213..f24e387bb0f 100644
--- a/api/services/feature_query_service.py
+++ b/api/services/feature_query_service.py
@@ -1,4 +1,4 @@
-"""Application service for Console feature queries."""
+"""Application service for feature queries exposed by API adapters."""
from collections.abc import Sequence
from typing import Protocol
diff --git a/api/services/feature_service.py b/api/services/feature_service.py
index 23cc41d83d9..ec94f28630d 100644
--- a/api/services/feature_service.py
+++ b/api/services/feature_service.py
@@ -4,9 +4,7 @@ from collections.abc import Mapping
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from configs import dify_config
-from enums.cloud_plan import CloudPlan
-from enums.deployment_edition import DeploymentEdition
-from enums.hosted_provider import HostedTrialProvider
+from enums import CloudPlan, DeploymentEdition, HostedTrialProvider
from services.billing_service import BillingInfo, BillingService
from services.enterprise.enterprise_service import EnterpriseService
from services.entities import feature_entities
@@ -30,14 +28,14 @@ class FeatureService:
cls._fulfill_params_from_env(features)
- if dify_config.BILLING_ENABLED and tenant_id:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and tenant_id:
cls._fulfill_params_from_billing_api(
features,
tenant_id,
exclude_vector_space=exclude_vector_space,
)
- if dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
features.webapp_copyright_enabled = True
features.knowledge_pipeline.publish_enabled = True
cls._fulfill_params_from_workspace_info(features, tenant_id)
@@ -52,7 +50,7 @@ class FeatureService:
@classmethod
def get_vector_space(cls, tenant_id: str) -> feature_entities.VectorSpaceLimitationModel:
vector_space = feature_entities.VectorSpaceLimitationModel(size=0, limit=5)
- if dify_config.BILLING_ENABLED and tenant_id:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and tenant_id:
billing_vector_space = BillingService.get_vector_space(tenant_id)
# NOTE: billing API returns vector_space.size as float (e.g. 0.0),
# but feature API keeps LimitationModel.size as int for compatibility.
@@ -65,7 +63,7 @@ class FeatureService:
@classmethod
def get_knowledge_rate_limit(cls, tenant_id: str):
knowledge_rate_limit = feature_entities.KnowledgeRateLimitModel()
- if dify_config.BILLING_ENABLED and tenant_id:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and tenant_id:
knowledge_rate_limit.enabled = True
limit_info = BillingService.get_knowledge_rate_limit(tenant_id)
knowledge_rate_limit.limit = limit_info.get("limit", 10)
@@ -75,7 +73,7 @@ class FeatureService:
@classmethod
def get_knowledge_file_size_limit(cls, tenant_id: str | None) -> int:
default_limit = dify_config.UPLOAD_FILE_SIZE_LIMIT
- if not dify_config.BILLING_ENABLED or not tenant_id:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD or not tenant_id:
return default_limit
billing_info = BillingService.get_info(tenant_id, exclude_vector_space=True)
@@ -91,7 +89,7 @@ class FeatureService:
def _resolve_human_input_email_delivery_enabled(
cls, *, features: feature_entities.FeatureModel, tenant_id: str | None
) -> bool:
- if dify_config.ENTERPRISE_ENABLED or not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return True
if not tenant_id:
return False
@@ -107,7 +105,7 @@ class FeatureService:
cls._fulfill_system_params_from_env(system_features)
- if dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
system_features.branding.enabled = True
system_features.webapp_auth.enabled = True
system_features.enable_change_email = False
@@ -125,7 +123,7 @@ class FeatureService:
def is_workspace_creation_allowed(cls) -> bool:
"""Resolve the backend workspace-creation policy, including the Enterprise override."""
is_allowed = dify_config.ALLOW_CREATE_WORKSPACE
- if not dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
return is_allowed
enterprise_info = EnterpriseService.get_info()
@@ -134,12 +132,12 @@ class FeatureService:
@classmethod
def is_plugin_manager_enabled(cls) -> bool:
"""Return whether Enterprise plugin credential policies must be enforced."""
- return dify_config.ENTERPRISE_ENABLED
+ return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE
@classmethod
def get_plugin_installation_permission(cls) -> feature_entities.PluginInstallationPermissionModel:
"""Resolve the validated deployment-wide plugin installation policy."""
- if not dify_config.ENTERPRISE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
return feature_entities.PluginInstallationPermissionModel()
return cls._resolve_plugin_installation_permission(EnterpriseService.get_info())
@@ -151,11 +149,10 @@ class FeatureService:
Non-enterprise deployments have no license, so an unconstrained default
(unlimited seats/workspaces) is returned.
"""
- if dify_config.ENTERPRISE_ENABLED:
- license_model = cls._build_license(EnterpriseService.get_info())
- license_model.license_expiry_notice_enabled = dify_config.ENABLE_LICENSE_EXPIRY_NOTICE
- else:
- license_model = feature_entities.LicenseModel()
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
+ return feature_entities.LicenseModel()
+ license_model = cls._build_license(EnterpriseService.get_info())
+ license_model.license_expiry_notice_enabled = dify_config.ENABLE_LICENSE_EXPIRY_NOTICE
return license_model
@staticmethod
diff --git a/api/services/feature_service_gateway.py b/api/services/feature_service_gateway.py
index 547d4151619..3afa5096442 100644
--- a/api/services/feature_service_gateway.py
+++ b/api/services/feature_service_gateway.py
@@ -1,4 +1,4 @@
-"""Feature-query gateway backed by FeatureService."""
+"""Feature-query gateway backed by the existing FeatureService."""
from typing import override
diff --git a/api/services/model_provider_service.py b/api/services/model_provider_service.py
index e30f25bfea6..11ba3723751 100644
--- a/api/services/model_provider_service.py
+++ b/api/services/model_provider_service.py
@@ -18,6 +18,7 @@ from core.plugin.entities.plugin_daemon import PluginModelProviderBinding
from core.plugin.impl.model_runtime_factory import create_plugin_model_provider_factory, create_plugin_provider_manager
from core.plugin.plugin_service import PluginService
from core.provider_manager import ProviderManager
+from enums import DeploymentEdition
from extensions import ext_hosting_provider
from graphon.model_runtime.entities.model_entities import ModelType, ParameterRule
from models.provider import (
@@ -296,7 +297,7 @@ class ModelProviderService:
) -> ProviderType:
if state.preferred_provider_type is not None:
return state.preferred_provider_type
- if dify_config.EDITION == "CLOUD" and system_enabled:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and system_enabled:
return ProviderType.SYSTEM
if custom_present:
return ProviderType.CUSTOM
diff --git a/api/services/openapi/license_gate.py b/api/services/openapi/license_gate.py
index 044598da148..7ca7779de6f 100644
--- a/api/services/openapi/license_gate.py
+++ b/api/services/openapi/license_gate.py
@@ -1,6 +1,6 @@
"""License gate for the /openapi/v1/permitted-external-apps* surface.
-EE-only. CE deploys (``ENTERPRISE_ENABLED=false``) skip the gate entirely —
+Enterprise-edition only. Community and Cloud deployments skip the gate entirely —
the EE blueprint chain is what gives CE deploys no callers on this surface
in practice, but the explicit short-circuit avoids any test/fixture that
flips the surface on without flipping the license.
@@ -22,6 +22,7 @@ from functools import wraps
from werkzeug.exceptions import Forbidden
from configs import dify_config
+from enums import DeploymentEdition
from services.entities.feature_entities import LicenseStatus
from services.feature_service import FeatureService
@@ -32,12 +33,12 @@ _VALID_LICENSE_STATUSES: frozenset[LicenseStatus] = frozenset({LicenseStatus.ACT
def license_required[**P, R](view: Callable[P, R]) -> Callable[P, R]:
"""Decorator form. Raises ``Forbidden('license_required')`` when the EE
- deployment has no valid license. No-op on CE (``ENTERPRISE_ENABLED=false``).
+ deployment has no valid license. No-op outside the Enterprise edition.
"""
@wraps(view)
def decorated(*args: P.args, **kwargs: P.kwargs) -> R:
- if dify_config.ENTERPRISE_ENABLED and not _is_license_valid():
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE and not _is_license_valid():
raise Forbidden(description="license_required")
return view(*args, **kwargs)
diff --git a/api/services/quota_service.py b/api/services/quota_service.py
index 4c784315c75..9804e26e50e 100644
--- a/api/services/quota_service.py
+++ b/api/services/quota_service.py
@@ -3,12 +3,9 @@ from __future__ import annotations
import logging
import uuid
from dataclasses import dataclass, field
-from typing import TYPE_CHECKING
from configs import dify_config
-
-if TYPE_CHECKING:
- from enums.quota_type import QuotaType
+from enums import DeploymentEdition, QuotaType
logger = logging.getLogger(__name__)
@@ -88,8 +85,6 @@ class QuotaCharge:
def unlimited() -> QuotaCharge:
- from enums.quota_type import QuotaType
-
return QuotaCharge(success=True, charge_id=None, _quota_type=QuotaType.UNLIMITED)
@@ -123,8 +118,8 @@ class QuotaService:
from services.billing_service import BillingService
from services.errors.app import QuotaExceededError
- if not dify_config.BILLING_ENABLED:
- logger.debug("Billing disabled, allowing request for %s", tenant_id)
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
+ logger.debug("Quota billing is unavailable outside the Cloud edition; allowing request for %s", tenant_id)
return QuotaCharge(success=True, charge_id=None, _quota_type=quota_type)
logger.info("Reserving %d %s quota for tenant %s", amount, quota_type.value, tenant_id)
@@ -179,7 +174,7 @@ class QuotaService:
@staticmethod
def check(quota_type: QuotaType, tenant_id: str, amount: int = 1) -> bool:
- if not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return True
if amount <= 0:
@@ -198,7 +193,7 @@ class QuotaService:
try:
from services.billing_service import BillingService
- if not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return
if not reservation_id:
diff --git a/api/services/rag_pipeline/rag_pipeline_task_proxy.py b/api/services/rag_pipeline/rag_pipeline_task_proxy.py
index 52ebbce65a9..4c466e37d0b 100644
--- a/api/services/rag_pipeline/rag_pipeline_task_proxy.py
+++ b/api/services/rag_pipeline/rag_pipeline_task_proxy.py
@@ -5,7 +5,7 @@ from functools import cached_property
from core.app.entities.rag_pipeline_invoke_entities import RagPipelineInvokeEntity
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from extensions.ext_database import db
from services.feature_service import FeatureService
from services.file_service import FileService
diff --git a/api/services/recommended_app_service.py b/api/services/recommended_app_service.py
index 8c9194e731d..7b09e2b4005 100644
--- a/api/services/recommended_app_service.py
+++ b/api/services/recommended_app_service.py
@@ -4,7 +4,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from configs import dify_config
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from models.model import AccountTrialAppRecord, App, TrialApp
from services.recommend_app.recommend_app_factory import RecommendAppRetrievalFactory
diff --git a/api/services/retention/conversation/messages_clean_policy.py b/api/services/retention/conversation/messages_clean_policy.py
index 5196344212b..f3e3cd8fb46 100644
--- a/api/services/retention/conversation/messages_clean_policy.py
+++ b/api/services/retention/conversation/messages_clean_policy.py
@@ -5,7 +5,7 @@ from dataclasses import dataclass
from typing import Protocol, override
from configs import dify_config
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from services.billing_service import BillingService, SubscriptionPlan
logger = logging.getLogger(__name__)
@@ -45,7 +45,7 @@ class MessagesCleanPolicy(Protocol):
class BillingDisabledPolicy(MessagesCleanPolicy):
"""
- Policy for community or enterpriseedition (billing disabled).
+ Policy for self-hosted editions, which do not use Cloud billing plans.
No special filter logic, just return all message ids.
"""
@@ -61,7 +61,7 @@ class BillingDisabledPolicy(MessagesCleanPolicy):
class BillingSandboxPolicy(MessagesCleanPolicy):
"""
- Policy for sandbox plan tenants in cloud edition (billing enabled).
+ Policy for sandbox-plan tenants in the Cloud edition.
Filters messages based on sandbox plan expiration rules:
- Skip tenants in the whitelist
@@ -186,24 +186,22 @@ def create_message_clean_policy(
"""
Factory function to create the appropriate message clean policy.
- Determines which policy to use based on BILLING_ENABLED configuration:
- - If BILLING_ENABLED is True: returns BillingSandboxPolicy
- - If BILLING_ENABLED is False: returns BillingDisabledPolicy
+ Cloud uses BillingSandboxPolicy; self-hosted editions use BillingDisabledPolicy.
Args:
graceful_period_days: Grace period in days after subscription expiration (default: 21)
current_timestamp: Current Unix timestamp for testing (default: None, uses current time)
"""
- if not dify_config.BILLING_ENABLED:
- logger.info("create_message_clean_policy: billing disabled, using BillingDisabledPolicy")
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
+ logger.info("create_message_clean_policy: self-hosted edition, using BillingDisabledPolicy")
return BillingDisabledPolicy()
- # Billing enabled - fetch whitelist from BillingService
+ # Cloud deployment - fetch whitelist from BillingService.
tenant_whitelist = BillingService.get_expired_subscription_cleanup_whitelist()
plan_provider = BillingService.get_plan_bulk_with_cache
logger.info(
- "create_message_clean_policy: billing enabled, using BillingSandboxPolicy "
+ "create_message_clean_policy: Cloud edition, using BillingSandboxPolicy "
"(graceful_period_days=%s, whitelist=%s)",
graceful_period_days,
tenant_whitelist,
diff --git a/api/services/retention/conversation/messages_clean_service.py b/api/services/retention/conversation/messages_clean_service.py
index 1e9f0bf1493..ffe130af681 100644
--- a/api/services/retention/conversation/messages_clean_service.py
+++ b/api/services/retention/conversation/messages_clean_service.py
@@ -169,8 +169,8 @@ class MessagesCleanService:
"""
Service for cleaning expired messages based on retention policies.
- Compatible with non cloud edition (billing disabled): all messages in the time range will be deleted.
- If billing is enabled: only sandbox plan tenant messages are deleted (with whitelist and grace period support).
+ In self-hosted editions, all messages in the time range are deleted.
+ In the Cloud edition, only sandbox-plan tenant messages are deleted, with whitelist and grace-period support.
"""
def __init__(
diff --git a/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py b/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py
index eb69b198202..3890ef58680 100644
--- a/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py
+++ b/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py
@@ -46,7 +46,7 @@ from sqlalchemy import inspect, select
from sqlalchemy.orm import Session, sessionmaker
from configs import dify_config
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from extensions.ext_database import db
from graphon.enums import WorkflowType
from libs.archive_storage import (
@@ -535,8 +535,8 @@ class WorkflowRunArchiver:
if self.paid_tenant_ids is not None:
return tenant_ids & self.paid_tenant_ids
- if not dify_config.BILLING_ENABLED:
- # If billing is not enabled, treat all tenants as paid
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
+ # Self-hosted editions have no Cloud billing plans, so treat all tenants as paid.
return tenant_ids
if not tenant_ids:
diff --git a/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py b/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py
index 3652997f8af..0e1a9ba0c0b 100644
--- a/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py
+++ b/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py
@@ -16,7 +16,7 @@ import click
from sqlalchemy.orm import Session, sessionmaker
from configs import dify_config
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from extensions.ext_database import db
from repositories.api_workflow_run_repository import (
APIWorkflowRunRepository,
@@ -478,7 +478,7 @@ class WorkflowRunCleanup:
def _filter_free_tenants(self, tenant_ids: Iterable[str]) -> set[str]:
tenant_id_list = sorted(set(tenant_ids))
- if not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return set(tenant_id_list)
if not tenant_id_list:
@@ -538,7 +538,7 @@ class WorkflowRunCleanup:
if self._cleanup_whitelist is not None:
return self._cleanup_whitelist
- if not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
self._cleanup_whitelist = set()
return self._cleanup_whitelist
diff --git a/api/services/snippet_service.py b/api/services/snippet_service.py
index ae21f11ab6d..f34f9789fa5 100644
--- a/api/services/snippet_service.py
+++ b/api/services/snippet_service.py
@@ -8,7 +8,9 @@ from typing import Any
from sqlalchemy import delete, event, func, select
from sqlalchemy.orm import Session, sessionmaker
+from configs import dify_config
from core.workflow.node_factory import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING
+from enums import DeploymentEdition
from graphon.enums import BuiltinNodeTypes, NodeType
from libs.infinite_scroll_pagination import InfiniteScrollPagination
from models import Account, TagBinding
@@ -150,10 +152,9 @@ class SnippetService:
@staticmethod
def _delete_archived_workflow_run_files(*, snippet: CustomizedSnippet) -> None:
- from configs import dify_config
from libs.archive_storage import ArchiveStorageNotConfiguredError, get_archive_storage
- if not (dify_config.BILLING_ENABLED and dify_config.ARCHIVE_STORAGE_ENABLED):
+ if not (dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.ARCHIVE_STORAGE_ENABLED):
return
prefix = f"{snippet.tenant_id}/app_id={snippet.id}/"
diff --git a/api/services/telemetry_service.py b/api/services/telemetry_service.py
index e6d141fe55a..adf3565cc0e 100644
--- a/api/services/telemetry_service.py
+++ b/api/services/telemetry_service.py
@@ -9,6 +9,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from configs import dify_config
+from enums import DeploymentEdition
from libs.datetime_utils import naive_utc_now
from models.model import DifySetup
@@ -74,8 +75,7 @@ class CommunityTelemetryService:
@classmethod
def _is_enabled(cls) -> bool:
return (
- dify_config.EDITION == "SELF_HOSTED"
- and not dify_config.ENTERPRISE_ENABLED
+ dify_config.DEPLOYMENT_EDITION == DeploymentEdition.COMMUNITY
and not dify_config.DISABLE_TELEMETRY
and not dify_config.DO_NOT_TRACK
and not dify_config.CI
@@ -88,7 +88,7 @@ class CommunityTelemetryService:
"event": event,
"instance_id": setup.instance_id or "",
"version": setup.version if event == "install" else dify_config.project.version,
- "edition": dify_config.EDITION,
+ "edition": dify_config.DEPLOYMENT_EDITION.value,
"deployment_type": "unknown",
"schema_version": SCHEMA_VERSION,
"os": cls._normalize_os(platform.system()),
diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py
index 45480f71d1a..dc82f91d3c1 100644
--- a/api/services/tools/builtin_tools_manage_service.py
+++ b/api/services/tools/builtin_tools_manage_service.py
@@ -31,6 +31,7 @@ from core.tools.utils.system_encryption import decrypt_system_params
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from models.account import Account
+from models.enums import PermissionEnum
from models.provider_ids import ToolProviderID
from models.tools import BuiltinToolProvider, ToolOAuthSystemClient, ToolOAuthTenantClient
from services.tools.tools_transform_service import ToolTransformService
@@ -282,8 +283,6 @@ class BuiltinToolManageService:
cache=NoOpProviderCredentialCache(),
)
- from models.enums import PermissionEnum
-
visibility_enum = PermissionEnum(visibility) if visibility else PermissionEnum.ALL_TEAM
# Plugin credentials only expose only_me / all_team_members at creation;
# partial-member access is handled by workspace RBAC, not per-credential.
diff --git a/api/services/tools/mcp_tools_manage_service.py b/api/services/tools/mcp_tools_manage_service.py
index ae184ba4561..6a2d2fea819 100644
--- a/api/services/tools/mcp_tools_manage_service.py
+++ b/api/services/tools/mcp_tools_manage_service.py
@@ -259,7 +259,7 @@ class MCPToolManageService:
mcp_provider.encrypted_credentials = self._process_credentials(authentication, mcp_provider, tenant_id)
# Update user-identity forwarding mode. The controller has already
- # resolved "leave unchanged" and applied the ENTERPRISE_ENABLED gate,
+ # resolved "leave unchanged" and applied the Enterprise-edition gate,
# so this is always a concrete, vetted value.
mcp_provider.identity_mode = identity_mode
diff --git a/api/services/trigger/webhook_service.py b/api/services/trigger/webhook_service.py
index a921c91d64a..2e4f86c93c8 100644
--- a/api/services/trigger/webhook_service.py
+++ b/api/services/trigger/webhook_service.py
@@ -23,7 +23,7 @@ from core.workflow.nodes.trigger_webhook.entities import (
WebhookData,
WebhookParameter,
)
-from enums.quota_type import QuotaType
+from enums import QuotaType
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from factories import file_factory
diff --git a/api/services/vector_space_admission_service.py b/api/services/vector_space_admission_service.py
index 7d098072c3d..8d3da92bb1c 100644
--- a/api/services/vector_space_admission_service.py
+++ b/api/services/vector_space_admission_service.py
@@ -15,8 +15,7 @@ from core.rag.datasource.vdb.vector_type import VectorType
from core.rag.embedding.cached_embedding import CacheEmbedding
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
from core.rag.models.document import Document
-from enums.cloud_plan import CloudPlan
-from enums.deployment_edition import DeploymentEdition
+from enums import CloudPlan, DeploymentEdition
from extensions.ext_redis import redis_client
from graphon.model_runtime.entities.model_entities import ModelType
from models.dataset import Dataset
@@ -248,7 +247,6 @@ class VectorSpaceAdmissionService:
) -> None:
if (
dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD
- or not dify_config.BILLING_ENABLED
or dataset.indexing_technique != IndexTechniqueType.HIGH_QUALITY
or workload.total_points == 0
or workload.probe_text is None
diff --git a/api/services/workflow/queue_dispatcher.py b/api/services/workflow/queue_dispatcher.py
index 0944b20357e..1d915a2cd49 100644
--- a/api/services/workflow/queue_dispatcher.py
+++ b/api/services/workflow/queue_dispatcher.py
@@ -10,6 +10,7 @@ with appropriate queue routing and priority assignment.
from enum import StrEnum
from configs import dify_config
+from enums import DeploymentEdition
from services.billing_service import BillingService
@@ -92,7 +93,7 @@ class QueueDispatcherManager:
Returns:
Appropriate queue dispatcher instance
"""
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
try:
billing_info = BillingService.get_info(tenant_id)
plan = billing_info.get("subscription", {}).get("plan", "sandbox")
@@ -100,7 +101,7 @@ class QueueDispatcherManager:
# If billing service fails, default to sandbox
plan = "sandbox"
else:
- # If billing is disabled, use team tier as default
+ # Self-hosted editions use the team queue by default.
plan = "team"
dispatcher_class = cls.PLAN_DISPATCHER_MAP.get(
diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py
index 1c1fab4b4e0..1fb0cf6ff36 100644
--- a/api/services/workflow_service.py
+++ b/api/services/workflow_service.py
@@ -50,7 +50,7 @@ from core.workflow.system_variables import build_bootstrap_variables, build_syst
from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool
from core.workflow.workflow_entry import WorkflowEntry
from enterprise.telemetry.draft_trace import enqueue_draft_node_execution_trace
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from events.app_event import app_draft_workflow_was_synced, app_published_workflow_was_updated
from extensions.ext_database import db
from extensions.ext_storage import storage
@@ -700,7 +700,7 @@ class WorkflowService:
)
# billing check
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
limit_info = BillingService.get_info(app_model.tenant_id)
if limit_info["subscription"]["plan"] == CloudPlan.SANDBOX:
# Check trigger node count limit for SANDBOX plan
diff --git a/api/services/workspace_plan_gateway.py b/api/services/workspace_plan_gateway.py
index ed3e4be30ea..22792b3e38a 100644
--- a/api/services/workspace_plan_gateway.py
+++ b/api/services/workspace_plan_gateway.py
@@ -5,8 +5,7 @@ from collections.abc import Mapping, Sequence
from typing import override
from configs import dify_config
-from enums.cloud_plan import CloudPlan
-from enums.deployment_edition import DeploymentEdition
+from enums import CloudPlan, DeploymentEdition
from services.billing_service import BillingService
from services.feature_service import FeatureService
from services.workspace_query_service import WorkspacePlanGateway
@@ -23,11 +22,11 @@ class DeploymentWorkspacePlanGateway(WorkspacePlanGateway):
if not ids:
return {}
- is_enterprise_only = dify_config.ENTERPRISE_ENABLED and not dify_config.BILLING_ENABLED
+ is_enterprise_only = dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE
if is_enterprise_only:
return dict.fromkeys(ids, str(CloudPlan.SANDBOX))
- is_saas = dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.BILLING_ENABLED
+ is_saas = dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD
bulk_plans = BillingService.get_plan_bulk(ids) if is_saas else {}
if is_saas and not bulk_plans:
logger.warning("get_plan_bulk returned empty result, falling back to FeatureService")
diff --git a/api/services/workspace_query_service.py b/api/services/workspace_query_service.py
index dbcfc8b575e..746454da3c5 100644
--- a/api/services/workspace_query_service.py
+++ b/api/services/workspace_query_service.py
@@ -4,7 +4,7 @@ from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import NamedTuple, Protocol
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from machinery.context import RequestContext
diff --git a/api/services/workspace_service.py b/api/services/workspace_service.py
index dc4743eac84..15c65456ffd 100644
--- a/api/services/workspace_service.py
+++ b/api/services/workspace_service.py
@@ -6,8 +6,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from configs import dify_config
-from enums.cloud_plan import CloudPlan
-from enums.deployment_edition import DeploymentEdition
+from enums import CloudPlan, DeploymentEdition
from models.account import Tenant, TenantAccountJoin, TenantAccountRole
from services.account_service import TenantService
from services.billing_service import BillingService
@@ -53,7 +52,7 @@ def _set_credit_pool_info(
class WorkspaceService:
@classmethod
def get_effective_credit_pool(cls, tenant_id: str, *, session: Session) -> EffectiveCreditPool:
- if not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return EffectiveCreditPool()
billing_info = BillingService.get_info(tenant_id, exclude_vector_space=True)
diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py
index 55a99dde7a1..899df12c7ad 100644
--- a/api/tasks/delete_account_task.py
+++ b/api/tasks/delete_account_task.py
@@ -5,6 +5,7 @@ from sqlalchemy import select
from configs import dify_config
from core.db.session_factory import session_factory
+from enums import DeploymentEdition
from models import Account
from services.billing_service import BillingService
from tasks.mail_account_deletion_task import send_deletion_success_task
@@ -17,7 +18,7 @@ def delete_account_task(account_id):
with session_factory.create_session() as session:
account = session.scalar(select(Account).where(Account.id == account_id).limit(1))
try:
- if dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
BillingService.delete_account(account_id)
except Exception:
logger.exception("Failed to delete account %s from billing service.", account_id)
diff --git a/api/tasks/document_indexing_task.py b/api/tasks/document_indexing_task.py
index e319225079d..2ad17d31577 100644
--- a/api/tasks/document_indexing_task.py
+++ b/api/tasks/document_indexing_task.py
@@ -13,7 +13,7 @@ from core.entities.document_task import DocumentTask
from core.indexing_runner import DocumentIsPausedError, IndexingRunner
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from libs.datetime_utils import naive_utc_now
from models.dataset import Dataset, Document
from models.enums import IndexingStatus
diff --git a/api/tasks/duplicate_document_indexing_task.py b/api/tasks/duplicate_document_indexing_task.py
index 10f37db7494..d062794df33 100644
--- a/api/tasks/duplicate_document_indexing_task.py
+++ b/api/tasks/duplicate_document_indexing_task.py
@@ -12,7 +12,7 @@ from core.entities.document_task import DocumentTask
from core.indexing_runner import DocumentIsPausedError, IndexingRunner
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from libs.datetime_utils import naive_utc_now
from models.dataset import Dataset, Document, DocumentSegment
from models.enums import IndexingStatus
diff --git a/api/tasks/refresh_billing_vector_space_task.py b/api/tasks/refresh_billing_vector_space_task.py
index ff3da012e3e..b6d0a377598 100644
--- a/api/tasks/refresh_billing_vector_space_task.py
+++ b/api/tasks/refresh_billing_vector_space_task.py
@@ -4,6 +4,7 @@ from celery import shared_task
from opentelemetry import metrics
from configs import dify_config
+from enums import DeploymentEdition
from services.billing_service import BillingService
logger = logging.getLogger(__name__)
@@ -19,7 +20,7 @@ _refresh_counter = metrics.get_meter(__name__).create_counter(
@shared_task(queue="dataset", bind=True, max_retries=_MAX_RETRIES, default_retry_delay=_RETRY_DELAY_SECONDS)
def refresh_billing_vector_space_task(self, tenant_id: str) -> None:
"""Refresh billing vector-space usage after vector cleanup has completed."""
- if not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return
try:
@@ -49,7 +50,7 @@ def refresh_billing_vector_space_task(self, tenant_id: str) -> None:
def schedule_billing_vector_space_refresh(tenant_id: str) -> None:
"""Dispatch a best-effort billing refresh without changing cleanup status."""
- if not dify_config.BILLING_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return
try:
diff --git a/api/tasks/remove_app_and_related_data_task.py b/api/tasks/remove_app_and_related_data_task.py
index a4fb6d57207..4562b7d1d90 100644
--- a/api/tasks/remove_app_and_related_data_task.py
+++ b/api/tasks/remove_app_and_related_data_task.py
@@ -13,6 +13,7 @@ from sqlalchemy.orm import sessionmaker
from configs import dify_config
from core.db.session_factory import session_factory
+from enums import DeploymentEdition
from extensions.ext_database import db
from libs.archive_storage import ArchiveStorageNotConfiguredError, get_archive_storage
from models import (
@@ -73,7 +74,7 @@ def remove_app_and_related_data_task(self, tenant_id: str, app_id: str):
_delete_app_workflow_runs(tenant_id, app_id)
_delete_app_workflow_node_executions(tenant_id, app_id)
_delete_app_workflow_app_logs(tenant_id, app_id)
- if dify_config.BILLING_ENABLED and dify_config.ARCHIVE_STORAGE_ENABLED:
+ if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.ARCHIVE_STORAGE_ENABLED:
_delete_app_workflow_archive_logs(tenant_id, app_id)
_delete_archived_workflow_run_files(tenant_id, app_id)
_delete_app_conversations(tenant_id, app_id)
diff --git a/api/tasks/trigger_processing_tasks.py b/api/tasks/trigger_processing_tasks.py
index 93f7f01407b..59e2f4e364c 100644
--- a/api/tasks/trigger_processing_tasks.py
+++ b/api/tasks/trigger_processing_tasks.py
@@ -26,7 +26,7 @@ from core.trigger.entities.entities import TriggerProviderEntity
from core.trigger.provider import PluginTriggerProviderController
from core.trigger.trigger_manager import TriggerManager
from core.workflow.nodes.trigger_plugin.entities import TriggerEventNodeData
-from enums.quota_type import QuotaType
+from enums import QuotaType
from graphon.enums import WorkflowExecutionStatus
from models.enums import (
AppTriggerType,
diff --git a/api/tasks/workflow_cfs_scheduler/entities.py b/api/tasks/workflow_cfs_scheduler/entities.py
index e95d606731c..d308ed0c680 100644
--- a/api/tasks/workflow_cfs_scheduler/entities.py
+++ b/api/tasks/workflow_cfs_scheduler/entities.py
@@ -1,7 +1,7 @@
from enum import StrEnum
from configs import dify_config
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from services.workflow.entities import WorkflowScheduleCFSPlanEntity
# Determine queue names based on edition
diff --git a/api/tasks/workflow_schedule_tasks.py b/api/tasks/workflow_schedule_tasks.py
index 38737f96e78..36ed189c7c1 100644
--- a/api/tasks/workflow_schedule_tasks.py
+++ b/api/tasks/workflow_schedule_tasks.py
@@ -8,7 +8,7 @@ from core.workflow.nodes.trigger_schedule.exc import (
ScheduleNotFoundError,
TenantOwnerNotFoundError,
)
-from enums.quota_type import QuotaType
+from enums import QuotaType
from models.trigger import WorkflowSchedulePlan
from services.async_workflow_service import AsyncWorkflowService
from services.errors.app import QuotaExceededError
diff --git a/api/tests/integration_tests/controllers/openapi/conftest.py b/api/tests/integration_tests/controllers/openapi/conftest.py
index e3a8c2ba9af..d9357e81551 100644
--- a/api/tests/integration_tests/controllers/openapi/conftest.py
+++ b/api/tests/integration_tests/controllers/openapi/conftest.py
@@ -10,6 +10,7 @@ from datetime import UTC, datetime, timedelta
import pytest
from flask import Flask
+from enums import DeploymentEdition
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from models import Account, App, OAuthAccessToken, Tenant, TenantAccountJoin
@@ -26,7 +27,7 @@ def disable_enterprise(monkeypatch: pytest.MonkeyPatch):
EE branch override this with their own monkeypatch in-test."""
from configs import dify_config
- monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@pytest.fixture
diff --git a/api/tests/integration_tests/controllers/openapi/test_auth.py b/api/tests/integration_tests/controllers/openapi/test_auth.py
index 5f0727fbbec..956a0904dab 100644
--- a/api/tests/integration_tests/controllers/openapi/test_auth.py
+++ b/api/tests/integration_tests/controllers/openapi/test_auth.py
@@ -12,6 +12,7 @@ import pytest
from flask import Flask
from flask.testing import FlaskClient
+from enums import DeploymentEdition
from extensions.ext_database import db
from models import App, Tenant
@@ -66,7 +67,7 @@ def test_layer0_denies_account_bearer_without_membership(
assert res.json.get("message") == "workspace_membership_revoked"
-def test_layer0_skipped_when_enterprise_enabled(
+def test_layer0_skipped_for_enterprise_edition(
test_client: FlaskClient,
account_token: str,
other_workspace_app: App,
@@ -81,7 +82,7 @@ def test_layer0_skipped_when_enterprise_enabled(
from configs import dify_config
# Override the conftest autouse default for this test only.
- monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", True)
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
res = test_client.get(
f"/openapi/v1/apps/{other_workspace_app.id}/info",
diff --git a/api/tests/integration_tests/services/retention/test_workflow_run_archiver.py b/api/tests/integration_tests/services/retention/test_workflow_run_archiver.py
index 90639ac60d6..098069776f2 100644
--- a/api/tests/integration_tests/services/retention/test_workflow_run_archiver.py
+++ b/api/tests/integration_tests/services/retention/test_workflow_run_archiver.py
@@ -8,6 +8,7 @@ import pyarrow.parquet as pq
import pytest
from sqlalchemy.exc import OperationalError
+from enums import DeploymentEdition
from models.workflow import WorkflowRunArchiveBundle
from services.retention.workflow_run.archive_paid_plan_workflow_run import (
ArchiveResult,
@@ -281,12 +282,12 @@ class TestGenerateManifest:
class TestFilterPaidTenants:
- def test_all_tenants_paid_when_billing_disabled(self):
+ def test_all_tenants_paid_in_community_edition(self):
archiver = WorkflowRunArchiver(days=90)
tenant_ids = {"t1", "t2", "t3"}
with patch("services.retention.workflow_run.archive_paid_plan_workflow_run.dify_config") as cfg:
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = archiver._filter_paid_tenants(tenant_ids)
assert result == tenant_ids
@@ -295,7 +296,7 @@ class TestFilterPaidTenants:
archiver = WorkflowRunArchiver(days=90)
with patch("services.retention.workflow_run.archive_paid_plan_workflow_run.dify_config") as cfg:
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
result = archiver._filter_paid_tenants(set())
assert result == set()
@@ -313,7 +314,7 @@ class TestFilterPaidTenants:
patch("services.retention.workflow_run.archive_paid_plan_workflow_run.dify_config") as cfg,
patch("services.retention.workflow_run.archive_paid_plan_workflow_run.BillingService") as billing,
):
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
billing.get_plan_bulk_with_cache.return_value = mock_bulk
result = archiver._filter_paid_tenants({"t1", "t2", "t3"})
@@ -328,7 +329,7 @@ class TestFilterPaidTenants:
patch("services.retention.workflow_run.archive_paid_plan_workflow_run.dify_config") as cfg,
patch("services.retention.workflow_run.archive_paid_plan_workflow_run.BillingService") as billing,
):
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
billing.get_plan_bulk_with_cache.side_effect = RuntimeError("API down")
result = archiver._filter_paid_tenants({"t1"})
@@ -341,7 +342,7 @@ class TestFilterPaidTenants:
patch("services.retention.workflow_run.archive_paid_plan_workflow_run.dify_config") as cfg,
patch("services.retention.workflow_run.archive_paid_plan_workflow_run.BillingService") as billing,
):
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
result = archiver._filter_paid_tenants({"t1", "t2", "t3"})
billing.get_plan_bulk_with_cache.assert_not_called()
diff --git a/api/tests/integration_tests/workflow/nodes/test_http.py b/api/tests/integration_tests/workflow/nodes/test_http.py
index 7cd7f50b773..86b0cd0fd71 100644
--- a/api/tests/integration_tests/workflow/nodes/test_http.py
+++ b/api/tests/integration_tests/workflow/nodes/test_http.py
@@ -11,7 +11,7 @@ from core.tools.tool_file_manager import ToolFileManager
from core.workflow.node_factory import DifyNodeFactory
from core.workflow.node_runtime import DifyFileReferenceFactory
from core.workflow.system_variables import build_system_variables
-from graphon.enums import WorkflowNodeExecutionStatus
+from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus
from graphon.file.file_manager import file_manager
from graphon.graph import Graph
from graphon.nodes.http_request import HttpRequestNode, HttpRequestNodeConfig, HttpRequestNodeData
@@ -193,7 +193,6 @@ def test_custom_authorization_header(setup_http_mock):
def test_custom_auth_with_empty_api_key_raises_error(setup_http_mock):
"""Test: In custom authentication mode, when the api_key is empty, AuthorizationConfigError should be raised."""
from core.workflow.system_variables import build_system_variables
- from graphon.enums import BuiltinNodeTypes
from graphon.nodes.http_request.entities import (
HttpRequestNodeAuthorization,
HttpRequestNodeData,
diff --git a/api/tests/test_containers_integration_tests/controllers/web/test_site.py b/api/tests/test_containers_integration_tests/controllers/web/test_site.py
index 349c4ef3d9c..cdba83851fc 100644
--- a/api/tests/test_containers_integration_tests/controllers/web/test_site.py
+++ b/api/tests/test_containers_integration_tests/controllers/web/test_site.py
@@ -11,6 +11,7 @@ from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.web.site import AppSiteApi, WebAppSiteResponse, WebModelConfigResponse
+from enums import DeploymentEdition
from extensions.storage.storage_type import StorageType
from models import Tenant, TenantStatus
from models.account import TenantCustomConfigDict
@@ -120,7 +121,7 @@ class TestAppSiteApi:
mock_get_file_presigned_url.return_value = "https://s3.example.com/icon.png?signature=test"
with (
- patch.object(dify_config, "EDITION", "CLOUD"),
+ patch.object(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch.object(dify_config, "STORAGE_TYPE", StorageType.S3),
app.test_request_context("/site"),
):
diff --git a/api/tests/test_containers_integration_tests/controllers/web/test_wraps.py b/api/tests/test_containers_integration_tests/controllers/web/test_wraps.py
index aa85ac2ca7b..01e241a9b94 100644
--- a/api/tests/test_containers_integration_tests/controllers/web/test_wraps.py
+++ b/api/tests/test_containers_integration_tests/controllers/web/test_wraps.py
@@ -18,6 +18,7 @@ from controllers.web.wraps import (
_validate_webapp_token,
decode_jwt_token,
)
+from models.enums import EndUserType
pytestmark = pytest.mark.usefixtures("db_session_with_containers")
@@ -189,7 +190,6 @@ 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())
diff --git a/api/tests/test_containers_integration_tests/services/test_account_service.py b/api/tests/test_containers_integration_tests/services/test_account_service.py
index 26b20e83a9b..e6d148c0ffe 100644
--- a/api/tests/test_containers_integration_tests/services/test_account_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_account_service.py
@@ -9,6 +9,7 @@ from werkzeug.exceptions import Unauthorized
from configs import dify_config
from controllers.console.error import AccountNotFound, NotAllowedCreateWorkspace
+from enums import DeploymentEdition
from models import AccountStatus, App, Dataset, TenantAccountJoin, TenantStatus
from services.account_service import AccountService, RegisterService, TenantService, TokenPair
from services.errors.account import (
@@ -156,7 +157,7 @@ class TestAccountService:
# Setup mocks
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = True
- dify_config.BILLING_ENABLED = True
+ dify_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with pytest.raises(AccountRegisterError):
AccountService.create_account(
@@ -167,7 +168,7 @@ class TestAccountService:
session=db_session_with_containers,
)
- dify_config.BILLING_ENABLED = False # Reset config for other tests
+ dify_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY # Reset config for other tests
def test_authenticate_account_not_found(
self, db_session_with_containers: Session, mock_external_service_dependencies
@@ -1105,14 +1106,14 @@ class TestAccountService:
fake = Faker()
email_in_freeze = fake.email()
# Setup mocks
- dify_config.BILLING_ENABLED = True
+ dify_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = True
with pytest.raises(AccountRegisterError):
AccountService.get_user_through_email(email_in_freeze, session=db_session_with_containers)
# Reset config
- dify_config.BILLING_ENABLED = False
+ dify_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
def test_delete_account(self, db_session_with_containers: Session, mock_external_service_dependencies):
"""
diff --git a/api/tests/test_containers_integration_tests/services/test_agent_service.py b/api/tests/test_containers_integration_tests/services/test_agent_service.py
index 00b4a1563ff..445f2704641 100644
--- a/api/tests/test_containers_integration_tests/services/test_agent_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_agent_service.py
@@ -843,7 +843,6 @@ class TestAgentService:
conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
from graphon.file import FileTransferMethod, FileType
- from models.enums import CreatorUserRole
# Add files to message
from models.model import MessageFile
diff --git a/api/tests/test_containers_integration_tests/services/test_app_generate_service.py b/api/tests/test_containers_integration_tests/services/test_app_generate_service.py
index 89cc7715d1c..d9f81caf01c 100644
--- a/api/tests/test_containers_integration_tests/services/test_app_generate_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_app_generate_service.py
@@ -8,6 +8,7 @@ from faker import Faker
from sqlalchemy.orm import Session
from core.app.entities.app_invoke_entities import InvokeFrom
+from enums import DeploymentEdition
from models import App
from models.enums import EndUserType
from models.model import EndUser
@@ -106,14 +107,14 @@ class TestAppGenerateService:
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
# Setup dify_config mock returns
- mock_dify_config.BILLING_ENABLED = False
+ mock_dify_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
mock_dify_config.APP_MAX_ACTIVE_REQUESTS = 100
mock_dify_config.APP_DEFAULT_ACTIVE_REQUESTS = 100
mock_dify_config.APP_DAILY_RATE_LIMIT = 1000
- mock_quota_dify_config.BILLING_ENABLED = False
+ mock_quota_dify_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
- mock_global_dify_config.BILLING_ENABLED = False
+ mock_global_dify_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
mock_global_dify_config.APP_MAX_ACTIVE_REQUESTS = 100
mock_global_dify_config.APP_DAILY_RATE_LIMIT = 1000
mock_global_dify_config.HOSTED_POOL_CREDITS = 1000
@@ -514,21 +515,21 @@ class TestAppGenerateService:
# Verify the result
assert result == ["test_response"]
- def test_generate_with_billing_enabled_sandbox_plan(
+ def test_generate_in_cloud_sandbox_plan(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
- Test generation with billing enabled and sandbox plan.
+ Test generation in the Cloud edition with a sandbox plan.
"""
fake = Faker()
app, account = self._create_test_app_and_account(
db_session_with_containers, mock_external_service_dependencies, mode="completion"
)
- # Set BILLING_ENABLED to True for this test
- mock_external_service_dependencies["dify_config"].BILLING_ENABLED = True
- mock_external_service_dependencies["quota_dify_config"].BILLING_ENABLED = True
- mock_external_service_dependencies["global_dify_config"].BILLING_ENABLED = True
+ # Billing services are available in the Cloud deployment edition.
+ mock_external_service_dependencies["dify_config"].DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
+ mock_external_service_dependencies["quota_dify_config"].DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
+ mock_external_service_dependencies["global_dify_config"].DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
# Setup test arguments
args = {"inputs": {"query": fake.text(max_nb_chars=50)}, "response_mode": "streaming"}
diff --git a/api/tests/test_containers_integration_tests/services/test_feature_service.py b/api/tests/test_containers_integration_tests/services/test_feature_service.py
index c0057d600b6..a314be8d8c5 100644
--- a/api/tests/test_containers_integration_tests/services/test_feature_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_feature_service.py
@@ -4,8 +4,7 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
-from enums.cloud_plan import CloudPlan
-from enums.deployment_edition import DeploymentEdition
+from enums import CloudPlan, DeploymentEdition
from services.entities.feature_entities import (
FeatureModel,
KnowledgeRateLimitModel,
@@ -88,20 +87,19 @@ class TestFeatureService:
def test_get_features_success(self, db_session_with_containers: Session, mock_external_service_dependencies):
"""
- Test successful feature retrieval with billing and enterprise enabled.
+ Test successful feature retrieval for the Cloud edition.
This test verifies:
- Proper feature model creation with all required fields
- Correct integration with billing service
- - Proper enterprise workspace information handling
+ - Enterprise workspace information remains isolated from Cloud
- Return value correctness and structure
"""
# Arrange: Setup test data with proper config mocking
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = True
mock_config.DATASET_OPERATOR_ENABLED = True
@@ -146,10 +144,10 @@ class TestFeatureService:
assert result.model_load_balancing_enabled is True
assert result.knowledge_rate_limit == 100
- # Verify enterprise features
- assert result.workspace_members.enabled is True
- assert result.workspace_members.size == 5
- assert result.workspace_members.limit == 10
+ # Enterprise workspace features are not loaded in Cloud.
+ assert result.workspace_members.enabled is False
+ assert result.workspace_members.size == 0
+ assert result.workspace_members.limit == 0
# Verify webapp copyright is enabled for non-sandbox plans
assert result.webapp_copyright_enabled is True
@@ -157,9 +155,7 @@ class TestFeatureService:
# Verify mock interactions
mock_external_service_dependencies["billing_service"].get_info.assert_called_once_with(tenant_id)
- mock_external_service_dependencies["enterprise_service"].get_workspace_info.assert_called_once_with(
- tenant_id
- )
+ mock_external_service_dependencies["enterprise_service"].get_workspace_info.assert_not_called()
def test_get_features_sandbox_plan(self, db_session_with_containers: Session, mock_external_service_dependencies):
"""
@@ -175,8 +171,7 @@ class TestFeatureService:
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = False
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = False
@@ -231,7 +226,7 @@ class TestFeatureService:
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
- Test successful knowledge rate limit retrieval with billing enabled.
+ Test successful knowledge rate limit retrieval in the Cloud edition.
This test verifies:
- Proper knowledge rate limit model creation
@@ -243,7 +238,7 @@ class TestFeatureService:
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
# Act: Execute the method under test
result = FeatureService.get_knowledge_rate_limit(tenant_id)
@@ -277,7 +272,6 @@ class TestFeatureService:
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = True
mock_config.ENABLE_EMAIL_CODE_LOGIN = True
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -350,7 +344,6 @@ class TestFeatureService:
# Arrange: Setup test data with exact same config as success test
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = True
mock_config.ENABLE_EMAIL_CODE_LOGIN = True
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -404,7 +397,7 @@ class TestFeatureService:
"""
# Arrange
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
# Act
result = FeatureService.get_license()
@@ -423,7 +416,7 @@ class TestFeatureService:
):
"""Non-enterprise deployments have no license, so limits are unconstrained."""
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = FeatureService.get_license()
@@ -448,7 +441,6 @@ class TestFeatureService:
# Arrange: Setup basic config mock (no enterprise)
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
- mock_config.ENTERPRISE_ENABLED = False
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = True
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -484,11 +476,11 @@ class TestFeatureService:
# Verify marketplace configuration
assert result.enable_marketplace is False
- def test_get_features_billing_disabled(
+ def test_get_features_community_edition(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
- Test feature retrieval when billing is disabled.
+ Test feature retrieval for the Community edition.
This test verifies:
- Proper feature model creation without billing
@@ -496,10 +488,9 @@ class TestFeatureService:
- Default configuration values
- Return value correctness and structure
"""
- # Arrange: Setup billing disabled mock
+ # Arrange: Use the Community edition.
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = False
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = True
mock_config.DATASET_OPERATOR_ENABLED = True
@@ -541,20 +532,20 @@ class TestFeatureService:
assert result.workspace_members.enabled is False
assert result.webapp_copyright_enabled is False
- def test_get_knowledge_rate_limit_billing_disabled(
+ def test_get_knowledge_rate_limit_community_edition(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
- Test knowledge rate limit retrieval when billing is disabled.
+ Test knowledge rate limit retrieval for the Community edition.
This test verifies:
- Proper knowledge rate limit model creation without billing
- Default rate limit configuration
- Return value correctness and structure
"""
- # Arrange: Setup billing disabled mock
+ # Arrange: Use the Community edition.
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
tenant_id = self._create_test_tenant_id()
@@ -568,7 +559,7 @@ class TestFeatureService:
# Verify default configuration
assert result.enabled is False
assert result.limit == 10
- assert result.subscription_plan == "" # Empty string when billing is disabled
+ assert result.subscription_plan == ""
# Verify no billing service calls
mock_external_service_dependencies["billing_service"].get_knowledge_rate_limit.assert_not_called()
@@ -577,7 +568,7 @@ class TestFeatureService:
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
- Test feature retrieval with enterprise enabled but billing disabled.
+ Test feature retrieval for the Enterprise edition.
This test verifies:
- Proper feature model creation with enterprise only
@@ -587,8 +578,7 @@ class TestFeatureService:
"""
# Arrange: Setup enterprise only mock
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = False
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_config.CAN_REPLACE_LOGO = False
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = False
@@ -603,7 +593,7 @@ class TestFeatureService:
assert result is not None
assert isinstance(result, FeatureModel)
- # Verify billing is disabled
+ # Cloud billing is not loaded in the Enterprise edition.
assert result.billing.enabled is False
# Verify enterprise features
@@ -634,11 +624,11 @@ class TestFeatureService:
)
mock_external_service_dependencies["billing_service"].get_info.assert_not_called()
- def test_get_system_features_enterprise_disabled(
+ def test_get_system_features_community_edition(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
- Test system features retrieval when enterprise is disabled.
+ Test system features retrieval for the Community edition.
This test verifies:
- Proper system feature model creation without enterprise
@@ -646,10 +636,9 @@ class TestFeatureService:
- Default configuration values
- Return value correctness and structure
"""
- # Arrange: Setup enterprise disabled mock
+ # Arrange: Use the Community edition.
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
- mock_config.ENTERPRISE_ENABLED = False
mock_config.MARKETPLACE_ENABLED = True
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -693,18 +682,17 @@ class TestFeatureService:
def test_get_features_no_tenant_id(self, db_session_with_containers: Session, mock_external_service_dependencies):
"""
- Test feature retrieval without tenant ID (billing disabled).
+ Test Cloud feature retrieval without a tenant ID.
This test verifies:
- Proper feature model creation without tenant ID
- - Correct handling when billing is disabled
+ - Billing data is not loaded without a tenant ID
- Default configuration values
- Return value correctness and structure
"""
# Arrange: Setup no tenant ID scenario
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = True
@@ -717,7 +705,7 @@ class TestFeatureService:
assert result is not None
assert isinstance(result, FeatureModel)
- # Verify billing is disabled due to no tenant ID
+ # Billing data is not loaded without a tenant ID.
assert result.billing.enabled is False
# Verify environment-based features
@@ -753,8 +741,7 @@ class TestFeatureService:
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = True
@@ -815,8 +802,7 @@ class TestFeatureService:
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = True
@@ -876,7 +862,6 @@ class TestFeatureService:
# Arrange: Setup edge case webapp auth mock with proper config
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -934,8 +919,7 @@ class TestFeatureService:
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = True
@@ -996,7 +980,6 @@ class TestFeatureService:
# Test case 1: Official only scope
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -1020,7 +1003,6 @@ class TestFeatureService:
# Test case 2: All plugins scope
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -1041,7 +1023,6 @@ class TestFeatureService:
# Test case 3: Specific partners scope
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -1065,7 +1046,6 @@ class TestFeatureService:
# Test case 4: None scope
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -1102,8 +1082,7 @@ class TestFeatureService:
}
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = False
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
# Act: Execute the method under test
result = FeatureService.get_features(tenant_id)
@@ -1139,7 +1118,7 @@ class TestFeatureService:
"""
# Arrange: Setup inactive license mock with proper config
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -1189,7 +1168,6 @@ class TestFeatureService:
# Arrange: Setup partial enterprise info mock with proper config
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -1261,8 +1239,7 @@ class TestFeatureService:
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = True
@@ -1319,7 +1296,6 @@ class TestFeatureService:
# Arrange: Setup edge case protocols mock with proper config
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -1384,6 +1360,7 @@ class TestFeatureService:
}
with patch("services.feature_service.dify_config") as mock_config:
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.EDUCATION_ENABLED = True
# Act: Execute the method under test
@@ -1416,47 +1393,6 @@ class TestFeatureService:
# Verify mock interactions
mock_external_service_dependencies["billing_service"].get_info.assert_called_once_with(tenant_id)
- def test_license_limitation_model_is_available(
- self, db_session_with_containers: Session, mock_external_service_dependencies
- ):
- """
- Test LicenseLimitationModel.is_available method with various scenarios.
-
- This test verifies:
- - Proper quota availability calculation
- - Correct handling of unlimited limits
- - Proper handling of disabled limits
- - Return value correctness for different scenarios
- """
- from services.entities.feature_entities import LicenseLimitationModel
-
- # Test case 1: Limit disabled
- disabled_limit = LicenseLimitationModel(enabled=False, size=5, limit=10)
- assert disabled_limit.is_available(3) is True
- assert disabled_limit.is_available(10) is True
-
- # Test case 2: Unlimited limit
- unlimited_limit = LicenseLimitationModel(enabled=True, size=5, limit=0)
- assert unlimited_limit.is_available(3) is True
- assert unlimited_limit.is_available(100) is True
-
- # Test case 3: Available quota
- available_limit = LicenseLimitationModel(enabled=True, size=5, limit=10)
- assert available_limit.is_available(3) is True
- assert available_limit.is_available(5) is True
- assert available_limit.is_available(1) is True
-
- # Test case 4: Insufficient quota
- insufficient_limit = LicenseLimitationModel(enabled=True, size=8, limit=10)
- assert insufficient_limit.is_available(3) is False
- assert insufficient_limit.is_available(2) is True
- assert insufficient_limit.is_available(1) is True
-
- # Test case 5: Exact quota usage
- exact_limit = LicenseLimitationModel(enabled=True, size=7, limit=10)
- assert exact_limit.is_available(3) is True
- assert exact_limit.is_available(3) is True
-
def test_get_features_workspace_members_disabled(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
@@ -1476,8 +1412,7 @@ class TestFeatureService:
}
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = False
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
# Act: Execute the method under test
result = FeatureService.get_features(tenant_id)
@@ -1511,7 +1446,7 @@ class TestFeatureService:
"""
# Arrange: Setup expired license mock with proper config
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -1562,8 +1497,7 @@ class TestFeatureService:
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = True
mock_config.DATASET_OPERATOR_ENABLED = True
@@ -1619,7 +1553,6 @@ class TestFeatureService:
# Arrange: Setup edge case branding mock with proper config
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -1682,8 +1615,7 @@ class TestFeatureService:
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = True
@@ -1744,8 +1676,7 @@ class TestFeatureService:
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = True
@@ -1808,7 +1739,6 @@ class TestFeatureService:
# Arrange: Setup lost license mock with proper config
with patch("services.feature_service.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- mock_config.ENTERPRISE_ENABLED = True
mock_config.MARKETPLACE_ENABLED = False
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
@@ -1861,8 +1791,7 @@ class TestFeatureService:
tenant_id = self._create_test_tenant_id()
with patch("services.feature_service.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = True
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = True
diff --git a/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py b/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py
index a003c2dbb81..2af90d0ef2a 100644
--- a/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py
@@ -10,7 +10,7 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from extensions.ext_redis import redis_client
from graphon.file import FileType
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
@@ -100,15 +100,21 @@ class TestMessagesCleanServiceIntegration:
yield mock
@pytest.fixture
- def mock_billing_enabled(self):
- """Mock BILLING_ENABLED to be True."""
- with patch("services.retention.conversation.messages_clean_policy.dify_config.BILLING_ENABLED", True):
+ def cloud_edition(self):
+ """Use the Cloud deployment edition."""
+ with patch(
+ "services.retention.conversation.messages_clean_policy.dify_config.DEPLOYMENT_EDITION",
+ DeploymentEdition.CLOUD,
+ ):
yield
@pytest.fixture
- def mock_billing_disabled(self):
- """Mock BILLING_ENABLED to be False."""
- with patch("services.retention.conversation.messages_clean_policy.dify_config.BILLING_ENABLED", False):
+ def non_cloud_edition(self):
+ """Use a non-Cloud deployment edition."""
+ with patch(
+ "services.retention.conversation.messages_clean_policy.dify_config.DEPLOYMENT_EDITION",
+ DeploymentEdition.COMMUNITY,
+ ):
yield
def _create_account_and_tenant(self, db_session_with_containers: Session, plan: str = CloudPlan.SANDBOX):
@@ -311,11 +317,11 @@ class TestMessagesCleanServiceIntegration:
)
db_session_with_containers.add(resource)
- def test_billing_disabled_deletes_all_messages_in_time_range(
- self, db_session_with_containers: Session, mock_billing_disabled
+ def test_non_cloud_edition_deletes_all_messages_in_time_range(
+ self, db_session_with_containers: Session, non_cloud_edition
):
"""Test that BillingDisabledPolicy deletes all messages within time range regardless of tenant plan."""
- # Arrange - Create tenant with messages (plan doesn't matter for billing disabled)
+ # Arrange - Create tenant with messages; plans do not apply outside Cloud.
account, tenant = self._create_account_and_tenant(db_session_with_containers, plan=CloudPlan.SANDBOX)
app = self._create_app(db_session_with_containers, tenant, account)
conv = self._create_conversation(db_session_with_containers, app)
@@ -378,9 +384,7 @@ class TestMessagesCleanServiceIntegration:
== 1
)
- def test_no_messages_returns_empty_stats(
- self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist
- ):
+ def test_no_messages_returns_empty_stats(self, db_session_with_containers: Session, cloud_edition, mock_whitelist):
"""Test cleaning when there are no messages to delete (B1)."""
# Arrange
end_before = datetime.datetime.now() - datetime.timedelta(days=30)
@@ -405,9 +409,7 @@ class TestMessagesCleanServiceIntegration:
assert stats["filtered_messages"] == 0
assert stats["total_deleted"] == 0
- def test_mixed_sandbox_and_paid_tenants(
- self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist
- ):
+ def test_mixed_sandbox_and_paid_tenants(self, db_session_with_containers: Session, cloud_edition, mock_whitelist):
"""Test cleaning with mixed sandbox and paid tenants (B2)."""
# Arrange - Create sandbox tenants with expired messages
sandbox_tenants = []
@@ -501,7 +503,7 @@ class TestMessagesCleanServiceIntegration:
)
def test_cursor_pagination_multiple_batches(
- self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist
+ self, db_session_with_containers: Session, cloud_edition, mock_whitelist
):
"""Test cursor pagination works correctly across multiple batches (B3)."""
# Arrange - Create sandbox tenant with messages that will span multiple batches
@@ -550,7 +552,7 @@ class TestMessagesCleanServiceIntegration:
# All messages should be deleted
assert db_session_with_containers.query(Message).where(Message.id.in_(message_ids)).count() == 0
- def test_dry_run_does_not_delete(self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist):
+ def test_dry_run_does_not_delete(self, db_session_with_containers: Session, cloud_edition, mock_whitelist):
"""Test dry_run mode does not delete messages (B4)."""
# Arrange
account, tenant = self._create_account_and_tenant(db_session_with_containers, plan=CloudPlan.SANDBOX)
@@ -599,9 +601,7 @@ class TestMessagesCleanServiceIntegration:
== 3
)
- def test_partial_plan_data_safe_default(
- self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist
- ):
+ def test_partial_plan_data_safe_default(self, db_session_with_containers: Session, cloud_edition, mock_whitelist):
"""Test when billing returns partial data, unknown tenants are preserved (B5)."""
# Arrange - Create 3 tenants
tenants_data = []
@@ -668,9 +668,7 @@ class TestMessagesCleanServiceIntegration:
db_session_with_containers.query(Message).where(Message.id == tenants_data[2]["message_id"]).count() == 1
) # Unknown tenant's message preserved (safe default)
- def test_empty_plan_data_skips_deletion(
- self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist
- ):
+ def test_empty_plan_data_skips_deletion(self, db_session_with_containers: Session, cloud_edition, mock_whitelist):
"""Test when billing returns empty data, skip deletion entirely (B6)."""
# Arrange
account, tenant = self._create_account_and_tenant(db_session_with_containers, plan=CloudPlan.SANDBOX)
@@ -705,9 +703,7 @@ class TestMessagesCleanServiceIntegration:
# Message should still exist (safe default - don't delete if plan is unknown)
assert db_session_with_containers.query(Message).where(Message.id == msg_id).count() == 1
- def test_time_range_boundary_behavior(
- self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist
- ):
+ def test_time_range_boundary_behavior(self, db_session_with_containers: Session, cloud_edition, mock_whitelist):
"""Test that messages are correctly filtered by [start_from, end_before) time range (B7)."""
# Arrange
account, tenant = self._create_account_and_tenant(db_session_with_containers, plan=CloudPlan.SANDBOX)
@@ -798,7 +794,7 @@ class TestMessagesCleanServiceIntegration:
# After range, kept
assert db_session_with_containers.query(Message).where(Message.id == msg_after_id).count() == 1
- def test_grace_period_scenarios(self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist):
+ def test_grace_period_scenarios(self, db_session_with_containers: Session, cloud_edition, mock_whitelist):
"""Test cleaning with different graceful period scenarios (B8)."""
# Arrange - Create 5 different tenants with different plan and expiration scenarios
now_timestamp = int(datetime.datetime.now(datetime.UTC).timestamp())
@@ -920,7 +916,7 @@ class TestMessagesCleanServiceIntegration:
) # Professional plan, kept
assert db_session_with_containers.query(Message).where(Message.id == msg5_id).count() == 1 # At boundary, kept
- def test_tenant_whitelist(self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist):
+ def test_tenant_whitelist(self, db_session_with_containers: Session, cloud_edition, mock_whitelist):
"""Test that whitelisted tenants' messages are not deleted (B9)."""
# Arrange - Create 3 sandbox tenants with expired messages
tenants_data = []
@@ -989,9 +985,7 @@ class TestMessagesCleanServiceIntegration:
# Verify tenant2's message was deleted (not whitelisted)
assert db_session_with_containers.query(Message).where(Message.id == tenants_data[2]["message_id"]).count() == 0
- def test_from_days_cleans_old_messages(
- self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist
- ):
+ def test_from_days_cleans_old_messages(self, db_session_with_containers: Session, cloud_edition, mock_whitelist):
"""Test from_days correctly cleans messages older than N days (B11)."""
# Arrange
account, tenant = self._create_account_and_tenant(db_session_with_containers, plan=CloudPlan.SANDBOX)
@@ -1054,7 +1048,7 @@ class TestMessagesCleanServiceIntegration:
assert db_session_with_containers.query(Message).where(Message.id.in_(recent_msg_ids)).count() == 2
def test_whitelist_precedence_over_grace_period(
- self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist
+ self, db_session_with_containers: Session, cloud_edition, mock_whitelist
):
"""Test that whitelist takes precedence over grace period logic."""
# Arrange - Create 2 sandbox tenants
@@ -1123,7 +1117,7 @@ class TestMessagesCleanServiceIntegration:
) # Within grace period
def test_empty_whitelist_deletes_eligible_messages(
- self, db_session_with_containers: Session, mock_billing_enabled, mock_whitelist
+ self, db_session_with_containers: Session, cloud_edition, mock_whitelist
):
"""Test that empty whitelist behaves as no whitelist (all eligible messages deleted)."""
# Arrange - Create sandbox tenant with expired messages
diff --git a/api/tests/test_containers_integration_tests/services/test_webhook_service.py b/api/tests/test_containers_integration_tests/services/test_webhook_service.py
index 1b5dc59e30d..a7a06362cad 100644
--- a/api/tests/test_containers_integration_tests/services/test_webhook_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_webhook_service.py
@@ -8,7 +8,7 @@ from faker import Faker
from flask import Flask
from sqlalchemy.orm import Session
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from models.account import Account, Tenant
from models.enums import AppTriggerStatus, AppTriggerType
from models.model import App
diff --git a/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py b/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py
index ddaee0cf051..432a483b8c0 100644
--- a/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py
+++ b/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py
@@ -13,7 +13,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE
-from enums.quota_type import QuotaType
+from enums import QuotaType
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.enums import AppTriggerStatus, AppTriggerType
from models.model import App
diff --git a/api/tests/test_containers_integration_tests/services/test_workflow_service.py b/api/tests/test_containers_integration_tests/services/test_workflow_service.py
index dd188693360..05697997511 100644
--- a/api/tests/test_containers_integration_tests/services/test_workflow_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_workflow_service.py
@@ -12,7 +12,9 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
+from graphon.enums import BuiltinNodeTypes, ErrorStrategy, WorkflowNodeExecutionStatus
from models import Account, AccountStatus, App, TenantStatus, Workflow
+from models.enums import CreatorUserRole
from models.model import AppMode
from models.workflow import WorkflowType
from services.workflow_ref_service import WorkflowRef
@@ -158,7 +160,6 @@ class TestWorkflowService:
workflow = self._create_test_workflow(db_session_with_containers, app, account, fake)
# Create a mock node execution record
- from models.enums import CreatorUserRole
from models.workflow import WorkflowNodeExecutionModel
node_execution = WorkflowNodeExecutionModel()
@@ -1637,7 +1638,6 @@ class TestWorkflowService:
import uuid
from datetime import datetime
- from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus
from graphon.graph_events import NodeRunSucceededEvent
from graphon.node_events import NodeRunResult
from graphon.nodes.base.node import Node
@@ -1682,12 +1682,10 @@ class TestWorkflowService:
# Assert
assert result is not None
assert result.node_id == node_id
- from graphon.enums import BuiltinNodeTypes
assert result.node_type == BuiltinNodeTypes.START # Should match the mock node type
assert result.title == "Test Node"
# Import the enum for comparison
- from graphon.enums import WorkflowNodeExecutionStatus
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.inputs is not None
@@ -1712,7 +1710,6 @@ class TestWorkflowService:
import uuid
from datetime import datetime
- from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus
from graphon.graph_events import NodeRunFailedEvent
from graphon.node_events import NodeRunResult
from graphon.nodes.base.node import Node
@@ -1757,7 +1754,6 @@ class TestWorkflowService:
assert result is not None
assert result.node_id == node_id
# Import the enum for comparison
- from graphon.enums import WorkflowNodeExecutionStatus
assert result.status == WorkflowNodeExecutionStatus.FAILED
assert result.error is not None
@@ -1781,7 +1777,6 @@ class TestWorkflowService:
import uuid
from datetime import datetime
- from graphon.enums import BuiltinNodeTypes, ErrorStrategy, WorkflowNodeExecutionStatus
from graphon.graph_events import NodeRunFailedEvent
from graphon.node_events import NodeRunResult
from graphon.nodes.base.node import Node
@@ -1827,7 +1822,6 @@ class TestWorkflowService:
assert result is not None
assert result.node_id == node_id
# Import the enum for comparison
- from graphon.enums import WorkflowNodeExecutionStatus
assert result.status == WorkflowNodeExecutionStatus.EXCEPTION # Should be EXCEPTION, not FAILED
assert result.outputs is not None
diff --git a/api/tests/test_containers_integration_tests/services/test_workspace_service.py b/api/tests/test_containers_integration_tests/services/test_workspace_service.py
index 1294156273d..d6edcd25f85 100644
--- a/api/tests/test_containers_integration_tests/services/test_workspace_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_workspace_service.py
@@ -1,12 +1,13 @@
from __future__ import annotations
+import json
from unittest.mock import MagicMock, patch
import pytest
from faker import Faker
from sqlalchemy.orm import Session
-from enums.deployment_edition import DeploymentEdition
+from enums import CloudPlan, DeploymentEdition
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from services.credit_pool_service import CreditPoolBalance
from services.workspace_service import WorkspaceService
@@ -333,8 +334,6 @@ class TestWorkspaceService:
for config in test_configs:
# Update tenant custom config
- import json
-
tenant.custom_config = json.dumps(config)
db_session_with_containers.commit()
@@ -505,8 +504,6 @@ class TestWorkspaceService:
for config in test_configs:
# Update tenant custom config
- import json
-
tenant.custom_config = json.dumps(config)
db_session_with_containers.commit()
@@ -564,8 +561,6 @@ class TestWorkspaceService:
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""replace_webapp_logo should be None when custom_config_dict does not have the key."""
- import json
-
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
@@ -586,8 +581,6 @@ class TestWorkspaceService:
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""The logo URL should use dify_config.FILES_URL as the base."""
- import json
-
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
@@ -779,8 +772,6 @@ class TestWorkspaceService:
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""When plan is SANDBOX, skip paid pool and use trial pool."""
- from enums.cloud_plan import CloudPlan
-
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
diff --git a/api/tests/test_containers_integration_tests/tasks/test_batch_clean_document_task.py b/api/tests/test_containers_integration_tests/tasks/test_batch_clean_document_task.py
index 436c8f11b05..4193223f311 100644
--- a/api/tests/test_containers_integration_tests/tasks/test_batch_clean_document_task.py
+++ b/api/tests/test_containers_integration_tests/tasks/test_batch_clean_document_task.py
@@ -19,7 +19,7 @@ from extensions.storage.storage_type import StorageType
from libs.datetime_utils import naive_utc_now
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, Document, DocumentSegment
-from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
+from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
from models.model import UploadFile
from tasks.batch_clean_document_task import batch_clean_document_task
@@ -207,8 +207,6 @@ class TestBatchCleanDocumentTask:
"""
fake = Faker()
- from models.enums import CreatorUserRole
-
upload_file = UploadFile(
tenant_id=account.current_tenant.id,
storage_type=StorageType.LOCAL,
diff --git a/api/tests/test_containers_integration_tests/tasks/test_dataset_indexing_task.py b/api/tests/test_containers_integration_tests/tasks/test_dataset_indexing_task.py
index 7f102f7375f..0c36794cb8c 100644
--- a/api/tests/test_containers_integration_tests/tasks/test_dataset_indexing_task.py
+++ b/api/tests/test_containers_integration_tests/tasks/test_dataset_indexing_task.py
@@ -11,7 +11,7 @@ from sqlalchemy.orm import Session
from core.indexing_runner import DocumentIsPausedError
from core.rag.index_processor.constant.index_type import IndexTechniqueType
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
from models.dataset import Dataset, Document
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
diff --git a/api/tests/test_containers_integration_tests/tasks/test_delete_account_task.py b/api/tests/test_containers_integration_tests/tasks/test_delete_account_task.py
index 9dfc6325d01..bbb95b0d233 100644
--- a/api/tests/test_containers_integration_tests/tasks/test_delete_account_task.py
+++ b/api/tests/test_containers_integration_tests/tasks/test_delete_account_task.py
@@ -14,6 +14,7 @@ from _pytest.logging import LogCaptureFixture
from pytest_mock import MockerFixture
from sqlalchemy.orm import Session
+from enums import DeploymentEdition
from models.account import Account
from tasks.delete_account_task import delete_account_task
@@ -35,14 +36,14 @@ def mock_external_dependencies(mocker: MockerFixture) -> tuple[MagicMock, MagicM
return billing_service, mail_task
-def test_billing_enabled_account_exists_calls_billing_and_sends_email(
+def test_cloud_account_exists_calls_billing_and_sends_email(
db_session_with_containers: Session,
mock_external_dependencies: tuple[MagicMock, MagicMock],
mocker: MockerFixture,
) -> None:
billing_service, mail_task = mock_external_dependencies
account = _create_account(db_session_with_containers, email="a@b.com")
- mocker.patch("tasks.delete_account_task.dify_config.BILLING_ENABLED", True)
+ mocker.patch("tasks.delete_account_task.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
delete_account_task(account.id)
@@ -50,14 +51,14 @@ def test_billing_enabled_account_exists_calls_billing_and_sends_email(
mail_task.delay.assert_called_once_with(account.email)
-def test_billing_disabled_account_exists_sends_email_only(
+def test_community_account_exists_sends_email_only(
db_session_with_containers: Session,
mock_external_dependencies: tuple[MagicMock, MagicMock],
mocker: MockerFixture,
) -> None:
billing_service, mail_task = mock_external_dependencies
account = _create_account(db_session_with_containers, email="x@y.com")
- mocker.patch("tasks.delete_account_task.dify_config.BILLING_ENABLED", False)
+ mocker.patch("tasks.delete_account_task.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
delete_account_task(account.id)
@@ -65,12 +66,12 @@ def test_billing_disabled_account_exists_sends_email_only(
mail_task.delay.assert_called_once_with(account.email)
-def test_billing_enabled_account_not_found_calls_billing_no_email(
+def test_cloud_account_not_found_calls_billing_no_email(
mock_external_dependencies: tuple[MagicMock, MagicMock], mocker: MockerFixture, caplog: LogCaptureFixture
) -> None:
billing_service, mail_task = mock_external_dependencies
account_id = str(uuid4())
- mocker.patch("tasks.delete_account_task.dify_config.BILLING_ENABLED", True)
+ mocker.patch("tasks.delete_account_task.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
delete_account_task(account_id)
@@ -87,7 +88,7 @@ def test_billing_delete_raises_propagates_and_no_email(
billing_service, mail_task = mock_external_dependencies
account = _create_account(db_session_with_containers, email="err@example.com")
billing_service.delete_account.side_effect = RuntimeError("billing down")
- mocker.patch("tasks.delete_account_task.dify_config.BILLING_ENABLED", True)
+ mocker.patch("tasks.delete_account_task.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
with pytest.raises(RuntimeError, match="billing down"):
delete_account_task(account.id)
diff --git a/api/tests/test_containers_integration_tests/tasks/test_document_indexing_task.py b/api/tests/test_containers_integration_tests/tasks/test_document_indexing_task.py
index b58b2f01da4..bb45e2be34a 100644
--- a/api/tests/test_containers_integration_tests/tasks/test_document_indexing_task.py
+++ b/api/tests/test_containers_integration_tests/tasks/test_document_indexing_task.py
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
from core.entities.document_task import DocumentTask
from core.rag.index_processor.constant.index_type import IndexTechniqueType
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
from models.dataset import Dataset, Document
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
diff --git a/api/tests/test_containers_integration_tests/tasks/test_duplicate_document_indexing_task.py b/api/tests/test_containers_integration_tests/tasks/test_duplicate_document_indexing_task.py
index 74199255e96..d5393435bfa 100644
--- a/api/tests/test_containers_integration_tests/tasks/test_duplicate_document_indexing_task.py
+++ b/api/tests/test_containers_integration_tests/tasks/test_duplicate_document_indexing_task.py
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
from core.indexing_runner import DocumentIsPausedError
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, Document, DocumentSegment
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
diff --git a/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py b/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py
index 90dd2bcfc84..c08b0be6a04 100644
--- a/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py
+++ b/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py
@@ -24,6 +24,7 @@ from core.trigger.debug import event_selectors
from core.trigger.debug.event_bus import TriggerDebugEventBus
from core.trigger.debug.event_selectors import PluginTriggerDebugEventPoller, WebhookTriggerDebugEventPoller
from core.trigger.debug.events import PluginTriggerDebugEvent, build_plugin_pool_key
+from enums import DeploymentEdition
from graphon.enums import BuiltinNodeTypes
from libs.datetime_utils import naive_utc_now
from models.account import Account, Tenant
@@ -115,7 +116,9 @@ def test_publish_blocks_start_and_trigger_coexistence(
"is_plugin_manager_enabled",
classmethod(lambda _cls: False),
)
- monkeypatch.setattr("services.workflow_service.dify_config", SimpleNamespace(BILLING_ENABLED=False))
+ monkeypatch.setattr(
+ "services.workflow_service.dify_config", SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
+ )
with pytest.raises(ValueError, match="Start node and trigger nodes cannot coexist"):
workflow_service.publish_workflow(session=db_session_with_containers, app_model=app_model, account=account)
diff --git a/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py b/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py
index 59a12a616b5..5e2f8ffaa09 100644
--- a/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py
+++ b/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py
@@ -16,6 +16,7 @@ from sqlalchemy.orm import Session
import commands
from commands import system as system_commands
from core.tools.entities.tool_entities import ApiProviderSchemaType
+from enums import DeploymentEdition
from graphon.model_runtime.entities.model_entities import ModelType
from models import Tenant
from models.provider import Provider, ProviderModel, ProviderType
@@ -87,7 +88,7 @@ def _bind_command_to_sqlite(monkeypatch: pytest.MonkeyPatch, session: Session) -
def test_reset_aborts_when_not_self_hosted(monkeypatch, capsys):
- monkeypatch.setattr(system_commands.dify_config, "EDITION", "CLOUD")
+ monkeypatch.setattr(system_commands.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
exit_code = _invoke_reset()
captured = capsys.readouterr()
@@ -106,7 +107,7 @@ def test_reset_purges_provider_and_tool_tables_for_each_tenant(
) -> None:
"""The command must purge LLM provider rows AND every tool provider table
that stores ciphertext encrypted under the tenant key (#35396)."""
- monkeypatch.setattr(system_commands.dify_config, "EDITION", "SELF_HOSTED")
+ monkeypatch.setattr(system_commands.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(system_commands, "generate_key_pair", lambda tenant_id: f"new-key-{tenant_id}")
_bind_command_to_sqlite(monkeypatch, sqlite_session)
@@ -146,7 +147,7 @@ def test_reset_purges_provider_and_tool_tables_for_each_tenant(
)
def test_reset_iterates_all_tenants(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None:
"""Multi-tenant deployments must purge every tenant, not just the first."""
- monkeypatch.setattr(system_commands.dify_config, "EDITION", "SELF_HOSTED")
+ monkeypatch.setattr(system_commands.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(system_commands, "generate_key_pair", lambda tenant_id: f"new-key-{tenant_id}")
_bind_command_to_sqlite(monkeypatch, sqlite_session)
diff --git a/api/tests/unit_tests/configs/test_dify_config.py b/api/tests/unit_tests/configs/test_dify_config.py
index ce81057f006..58cfb63cc0a 100644
--- a/api/tests/unit_tests/configs/test_dify_config.py
+++ b/api/tests/unit_tests/configs/test_dify_config.py
@@ -6,6 +6,7 @@ from packaging.version import Version
from yarl import URL
from configs.app_config import DifyConfig
+from enums import DeploymentEdition
def _clear_environment(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -78,7 +79,7 @@ def test_dify_config(monkeypatch: pytest.MonkeyPatch):
assert config.COMMIT_SHA == ""
# default values
- assert config.EDITION == "SELF_HOSTED"
+ assert config.DEPLOYMENT_EDITION is DeploymentEdition.COMMUNITY
assert config.API_COMPRESSION_ENABLED is False
assert config.AGENT_SHELL_ENABLED is True
assert config.SENTRY_TRACES_SAMPLE_RATE == 1.0
@@ -95,6 +96,19 @@ def test_dify_config(monkeypatch: pytest.MonkeyPatch):
assert Version(config.project.version) >= Version("1.0.0")
+@pytest.mark.parametrize("edition", list(DeploymentEdition))
+def test_deployment_edition_is_loaded_from_environment(
+ monkeypatch: pytest.MonkeyPatch,
+ edition: DeploymentEdition,
+) -> None:
+ _set_basic_config_env(monkeypatch)
+ monkeypatch.setenv("DEPLOYMENT_EDITION", edition.value)
+
+ config = DifyConfig(_env_file=None)
+
+ assert config.DEPLOYMENT_EDITION is edition
+
+
def test_new_user_default_plugin_ids_are_parsed_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
_set_basic_config_env(monkeypatch)
monkeypatch.setenv(
@@ -238,7 +252,7 @@ def test_flask_configs(monkeypatch: pytest.MonkeyPatch):
# configs read from pydantic-settings
assert config["LOG_LEVEL"] == "INFO"
assert config["COMMIT_SHA"] == ""
- assert config["EDITION"] == "SELF_HOSTED"
+ assert config["DEPLOYMENT_EDITION"] is DeploymentEdition.COMMUNITY
assert config["API_COMPRESSION_ENABLED"] is False
assert config["SENTRY_TRACES_SAMPLE_RATE"] == 1.0
diff --git a/api/tests/unit_tests/controllers/console/app/test_app_import_api.py b/api/tests/unit_tests/controllers/console/app/test_app_import_api.py
index b92f75f408e..b53fa4676dd 100644
--- a/api/tests/unit_tests/controllers/console/app/test_app_import_api.py
+++ b/api/tests/unit_tests/controllers/console/app/test_app_import_api.py
@@ -13,7 +13,7 @@ from sqlalchemy import Engine, event
from sqlalchemy.orm import Session
from controllers.console.app import app_import as app_import_module
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from models.account import Account
from models.base import TypeBase
from models.engine import db
diff --git a/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py b/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py
index d639f442d93..a1f2147a895 100644
--- a/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py
+++ b/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py
@@ -14,6 +14,7 @@ from controllers.console import console_ns
from controllers.console import wraps as console_wraps
from controllers.console.app import ops_trace as ops_trace_module
from controllers.console.app import wraps as app_wraps
+from enums import DeploymentEdition
from libs import login as login_lib
from models.account import Account, AccountStatus, TenantAccountRole
from models.model import App, AppMode, IconType
@@ -42,7 +43,7 @@ def _patch_console_guards(
) -> None:
monkeypatch.setattr(login_lib.dify_config, "LOGIN_DISABLED", True)
monkeypatch.setattr(login_lib.dify_config, "RBAC_ENABLED", rbac_enabled)
- monkeypatch.setattr(console_wraps.dify_config, "EDITION", "CLOUD")
+ monkeypatch.setattr(console_wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(login_lib, "current_user", account)
monkeypatch.setattr(login_lib, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(console_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_comment_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_comment_api.py
index 8c9c9f9d562..d568744292c 100644
--- a/api/tests/unit_tests/controllers/console/app/test_workflow_comment_api.py
+++ b/api/tests/unit_tests/controllers/console/app/test_workflow_comment_api.py
@@ -14,6 +14,7 @@ from controllers.console import console_ns
from controllers.console import wraps as console_wraps
from controllers.console.app import workflow_comment as workflow_comment_module
from controllers.console.app import wraps as app_wraps
+from enums import DeploymentEdition
from libs import login as login_lib
from models.account import Account, AccountStatus, TenantAccountRole
@@ -47,7 +48,7 @@ def _patch_console_guards(monkeypatch: pytest.MonkeyPatch, account: Account, app
monkeypatch.setattr(login_lib, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(login_lib, "check_csrf_token", lambda *_, **__: None)
monkeypatch.setattr(console_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
- monkeypatch.setattr(console_wraps.dify_config, "EDITION", "CLOUD")
+ monkeypatch.setattr(console_wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(app_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(app_wraps, "_load_app_model_from_scoped_session", lambda _app_id: app_model)
diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_human_input_debug_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_human_input_debug_api.py
index 956706eafb6..2f6a03b107b 100644
--- a/api/tests/unit_tests/controllers/console/app/test_workflow_human_input_debug_api.py
+++ b/api/tests/unit_tests/controllers/console/app/test_workflow_human_input_debug_api.py
@@ -11,6 +11,7 @@ from pydantic import ValidationError
from controllers.console import wraps as console_wraps
from controllers.console.app import workflow as workflow_module
from controllers.console.app import wraps as app_wraps
+from enums import DeploymentEdition
from libs import login as login_lib
from models.account import Account, AccountStatus, TenantAccountRole
from models.model import AppMode
@@ -32,14 +33,14 @@ def _make_app(mode: AppMode) -> SimpleNamespace:
def _patch_console_guards(monkeypatch: pytest.MonkeyPatch, account: Account, app_model: SimpleNamespace) -> None:
# Skip setup and auth guardrails
- monkeypatch.setattr("configs.dify_config.EDITION", "CLOUD")
+ monkeypatch.setattr("configs.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(login_lib.dify_config, "LOGIN_DISABLED", True)
monkeypatch.setattr(login_lib, "current_user", account)
monkeypatch.setattr(login_lib, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(login_lib, "check_csrf_token", lambda *_, **__: None)
monkeypatch.setattr(console_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(app_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
- monkeypatch.setattr(console_wraps.dify_config, "EDITION", "CLOUD")
+ monkeypatch.setattr(console_wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.delenv("INIT_PASSWORD", raising=False)
# Avoid hitting the database when resolving the app model
diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_node_output_inspector.py b/api/tests/unit_tests/controllers/console/app/test_workflow_node_output_inspector.py
index dfe35a89f57..8cca6ab9f41 100644
--- a/api/tests/unit_tests/controllers/console/app/test_workflow_node_output_inspector.py
+++ b/api/tests/unit_tests/controllers/console/app/test_workflow_node_output_inspector.py
@@ -31,6 +31,7 @@ from uuid import UUID
import pytest
from controllers.console.app import workflow_node_output_inspector as ctrl
+from graphon.enums import WorkflowExecutionStatus
from services.workflow.inspector_events import InspectorMessage
from services.workflow.node_output_inspector_service import (
NodeOutputInspectorError,
@@ -61,8 +62,6 @@ def run_id() -> UUID:
def _snapshot_view(*, status: str, node_id: str = "agent-1") -> WorkflowRunSnapshotView:
- from graphon.enums import WorkflowExecutionStatus
-
return WorkflowRunSnapshotView(
workflow_run_id="00000000-0000-0000-0000-0000000000aa",
workflow_run_status=WorkflowExecutionStatus(status),
diff --git a/api/tests/unit_tests/controllers/console/auth/test_account_activation.py b/api/tests/unit_tests/controllers/console/auth/test_account_activation.py
index 55f067ba6c1..e9afc527bcd 100644
--- a/api/tests/unit_tests/controllers/console/auth/test_account_activation.py
+++ b/api/tests/unit_tests/controllers/console/auth/test_account_activation.py
@@ -14,6 +14,7 @@ from controllers.console.auth import activate as activate_module
from controllers.console.auth.activate import ActivateApi, ActivateCheckApi
from controllers.console.auth.error import InvitationAccountMismatchError
from controllers.console.error import AccountInFreezeError, AlreadyActivateError
+from enums import DeploymentEdition
from models.account import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole
@@ -285,7 +286,7 @@ class TestActivateApi:
assert isinstance(account, Account)
account.email = "Invitee@Example.com"
sqlite_session.commit()
- monkeypatch.setattr(activate_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(activate_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
with (
patch.object(
diff --git a/api/tests/unit_tests/controllers/console/auth/test_authentication_security.py b/api/tests/unit_tests/controllers/console/auth/test_authentication_security.py
index 17bee94c520..dc4260e94b1 100644
--- a/api/tests/unit_tests/controllers/console/auth/test_authentication_security.py
+++ b/api/tests/unit_tests/controllers/console/auth/test_authentication_security.py
@@ -10,6 +10,7 @@ from flask_restx import Api
import services.errors.account
from controllers.console.auth.error import AuthenticationFailedError
from controllers.console.auth.login import LoginApi
+from enums import DeploymentEdition
def encode_password(password: str) -> str:
@@ -33,7 +34,7 @@ class TestAuthenticationSecurity:
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.AccountService.authenticate")
@patch("controllers.console.auth.login.AccountService.add_login_error_rate_limit")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
def test_login_invalid_email_with_registration_allowed(
self, mock_get_invitation, mock_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
@@ -65,7 +66,7 @@ class TestAuthenticationSecurity:
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.AccountService.authenticate")
@patch("controllers.console.auth.login.AccountService.add_login_error_rate_limit")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
def test_login_wrong_password_returns_error(
self, mock_get_invitation, mock_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_db
@@ -97,7 +98,7 @@ class TestAuthenticationSecurity:
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.AccountService.authenticate")
@patch("controllers.console.auth.login.AccountService.add_login_error_rate_limit")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
def test_login_invalid_email_with_registration_disabled(
self, mock_get_invitation, mock_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
diff --git a/api/tests/unit_tests/controllers/console/auth/test_email_register.py b/api/tests/unit_tests/controllers/console/auth/test_email_register.py
index 95040cfc63a..6c945dc5f57 100644
--- a/api/tests/unit_tests/controllers/console/auth/test_email_register.py
+++ b/api/tests/unit_tests/controllers/console/auth/test_email_register.py
@@ -11,7 +11,7 @@ from controllers.console.auth.email_register import (
EmailRegisterResetApi,
EmailRegisterSendEmailApi,
)
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from services.entities.feature_entities import SystemFeatureModel
@@ -41,8 +41,8 @@ class TestEmailRegisterSendEmailApi:
is_allow_register=True,
)
with (
- patch("controllers.console.auth.email_register.dify_config.BILLING_ENABLED", True),
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.auth.email_register.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
):
with app.test_request_context(
@@ -86,7 +86,7 @@ class TestEmailRegisterCheckApi:
is_allow_register=True,
)
with (
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
):
with app.test_request_context(
@@ -138,7 +138,7 @@ class TestEmailRegisterResetApi:
is_allow_register=True,
)
with (
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
):
with app.test_request_context(
@@ -190,7 +190,7 @@ class TestEmailRegisterResetApi:
is_allow_register=True,
)
with (
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
):
with app.test_request_context(
@@ -247,7 +247,7 @@ class TestEmailRegisterResetApi:
is_allow_register=True,
)
with (
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
):
with app.test_request_context(
diff --git a/api/tests/unit_tests/controllers/console/auth/test_forgot_password.py b/api/tests/unit_tests/controllers/console/auth/test_forgot_password.py
index 637db6d5d25..438bd35169f 100644
--- a/api/tests/unit_tests/controllers/console/auth/test_forgot_password.py
+++ b/api/tests/unit_tests/controllers/console/auth/test_forgot_password.py
@@ -13,7 +13,7 @@ from controllers.console.auth.forgot_password import (
ForgotPasswordResetApi,
ForgotPasswordSendEmailApi,
)
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from models.account import Account
from models.engine import db
from services.entities.feature_entities import SystemFeatureModel
@@ -61,7 +61,7 @@ class TestForgotPasswordSendEmailApi:
"controllers.console.auth.forgot_password.FeatureService.get_system_features",
return_value=controller_features,
),
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=wraps_features),
):
with app.test_request_context(
@@ -108,7 +108,7 @@ class TestForgotPasswordCheckApi:
enable_email_password_login=True,
)
with (
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=wraps_features),
):
with app.test_request_context(
@@ -154,7 +154,7 @@ class TestForgotPasswordResetApi:
enable_email_password_login=True,
)
with (
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=wraps_features),
):
with database_app.test_request_context(
diff --git a/api/tests/unit_tests/controllers/console/auth/test_login_logout.py b/api/tests/unit_tests/controllers/console/auth/test_login_logout.py
index 51428d3d883..970acd52cfa 100644
--- a/api/tests/unit_tests/controllers/console/auth/test_login_logout.py
+++ b/api/tests/unit_tests/controllers/console/auth/test_login_logout.py
@@ -29,6 +29,7 @@ from controllers.console.error import (
SeatsLimitExceeded,
WorkspacesLimitExceeded,
)
+from enums import DeploymentEdition
from services.entities.auth_entities import LoginFailureReason
from services.errors.account import AccountLoginError, AccountPasswordError, SeatsLimitExceededError
@@ -86,7 +87,7 @@ class TestLoginApi:
return token_pair
@patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
@patch("controllers.console.auth.login.AccountService.authenticate")
@@ -137,7 +138,7 @@ class TestLoginApi:
assert response.json["result"] == "success"
@patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
@patch("controllers.console.auth.login.AccountService.authenticate")
@@ -190,7 +191,7 @@ class TestLoginApi:
assert response.json["result"] == "success"
@patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
def test_login_fails_when_rate_limited(
@@ -223,7 +224,7 @@ class TestLoginApi:
assert warn_records[0].args[1] == LoginFailureReason.LOGIN_RATE_LIMITED
@patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", True)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
@patch("controllers.console.auth.login.BillingService.is_email_in_freeze")
def test_login_fails_when_account_frozen(
self, mock_is_frozen, mock_db, app: Flask, caplog: pytest.LogCaptureFixture
@@ -254,7 +255,7 @@ class TestLoginApi:
assert warn_records[0].args[1] == LoginFailureReason.ACCOUNT_IN_FREEZE
@patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
@patch("controllers.console.auth.login.AccountService.authenticate")
@@ -301,7 +302,7 @@ class TestLoginApi:
assert warn_records[0].args[1] == LoginFailureReason.INVALID_CREDENTIALS
@patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
@patch("controllers.console.auth.login.AccountService.authenticate")
@@ -338,7 +339,7 @@ class TestLoginApi:
assert warn_records[0].args[1] == LoginFailureReason.ACCOUNT_BANNED
@patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
@patch("controllers.console.auth.login.AccountService.authenticate")
@@ -382,7 +383,7 @@ class TestLoginApi:
login_api.post()
@patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
def test_login_invitation_email_mismatch(self, mock_get_invitation, mock_is_rate_limit, mock_db, app: Flask):
@@ -412,7 +413,7 @@ class TestLoginApi:
login_api.post()
@patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
+ @patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
@patch("controllers.console.auth.login.AccountService.authenticate")
diff --git a/api/tests/unit_tests/controllers/console/auth/test_oauth_timezone.py b/api/tests/unit_tests/controllers/console/auth/test_oauth_timezone.py
index 75545cc27e7..eaf339b62a6 100644
--- a/api/tests/unit_tests/controllers/console/auth/test_oauth_timezone.py
+++ b/api/tests/unit_tests/controllers/console/auth/test_oauth_timezone.py
@@ -4,6 +4,7 @@ import pytest
from flask import Flask
from controllers.console.auth.oauth import OAuthLogin, _generate_account
+from enums import DeploymentEdition
from libs.oauth import OAuthUserInfo
from services.errors.account import AccountRegisterError
@@ -116,7 +117,7 @@ def test_generate_account_rejects_new_user_when_registration_disabled(
app: Flask,
):
mock_feature_service.get_system_features.return_value.is_allow_register = False
- mock_config.BILLING_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
user_info = OAuthUserInfo(id="github-123", name="Test User", email="user@example.com")
with app.test_request_context(headers={"Accept-Language": "en-US,en;q=0.9"}):
diff --git a/api/tests/unit_tests/controllers/console/auth/test_password_reset.py b/api/tests/unit_tests/controllers/console/auth/test_password_reset.py
index d14e8f6ed2e..c2375fab888 100644
--- a/api/tests/unit_tests/controllers/console/auth/test_password_reset.py
+++ b/api/tests/unit_tests/controllers/console/auth/test_password_reset.py
@@ -23,7 +23,7 @@ from controllers.console.auth.forgot_password import (
ForgotPasswordSendEmailApi,
)
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from models.account import Account, Tenant, TenantAccountJoin
from services.entities.feature_entities import SystemFeatureModel
@@ -46,7 +46,7 @@ def _bind_database_session(session: Session) -> Generator[scoped_session[Session
def enable_password_login_wrappers(monkeypatch: pytest.MonkeyPatch) -> None:
"""Keep endpoint decorators deterministic without requiring the configured app database."""
- monkeypatch.setattr("controllers.console.wraps.dify_config.EDITION", "CLOUD")
+ monkeypatch.setattr("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(
"controllers.console.wraps.FeatureService.get_system_features",
lambda: SystemFeatureModel(
diff --git a/api/tests/unit_tests/controllers/console/billing/test_billing.py b/api/tests/unit_tests/controllers/console/billing/test_billing.py
index 87f90caa800..e9ac3fd7f81 100644
--- a/api/tests/unit_tests/controllers/console/billing/test_billing.py
+++ b/api/tests/unit_tests/controllers/console/billing/test_billing.py
@@ -9,6 +9,7 @@ from werkzeug.exceptions import BadRequest, UnprocessableEntity
from controllers.console import wraps as console_wraps
from controllers.console.billing.billing import PartnerTenants
+from enums import DeploymentEdition
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.model import DifySetup
@@ -65,7 +66,7 @@ class TestPartnerTenants:
console_wraps._is_setup_completed.reset_success()
monkeypatch.setattr(console_wraps.db, "session", sqlite_session)
with (
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("libs.login.dify_config.LOGIN_DISABLED", False),
patch("libs.login.check_csrf_token") as mock_csrf,
):
diff --git a/api/tests/unit_tests/controllers/console/test_extension.py b/api/tests/unit_tests/controllers/console/test_extension.py
index 8ea327dfdce..2a054f75d41 100644
--- a/api/tests/unit_tests/controllers/console/test_extension.py
+++ b/api/tests/unit_tests/controllers/console/test_extension.py
@@ -20,6 +20,7 @@ from controllers.console.extension import (
APIBasedExtensionDetailAPI,
CodeBasedExtensionAPI,
)
+from enums import DeploymentEdition
if _NEEDS_METHOD_VIEW_CLEANUP:
del builtins.__dict__["MethodView"]
@@ -62,7 +63,7 @@ def _mock_console_guards(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
account.id = "account-123"
account.is_authenticated = True
- monkeypatch.setattr(wraps_module.dify_config, "EDITION", "CLOUD")
+ monkeypatch.setattr(wraps_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr("libs.login.dify_config.LOGIN_DISABLED", True)
monkeypatch.delenv("INIT_PASSWORD", raising=False)
monkeypatch.setattr(wraps_module, "current_account_with_tenant", lambda: (account, "tenant-123"))
diff --git a/api/tests/unit_tests/controllers/console/test_fastopenapi_setup.py b/api/tests/unit_tests/controllers/console/test_fastopenapi_setup.py
index c87b5d2d665..ea72412e794 100644
--- a/api/tests/unit_tests/controllers/console/test_fastopenapi_setup.py
+++ b/api/tests/unit_tests/controllers/console/test_fastopenapi_setup.py
@@ -10,6 +10,7 @@ from controllers.console import setup as setup_controller
from controllers.console import wraps
from controllers.console.error import AlreadySetupError, NotInitValidateError
from dify_app import DifyApp
+from enums import DeploymentEdition
from extensions import ext_fastopenapi
from services.setup_service import (
InitializationValidationRequiredError,
@@ -67,15 +68,18 @@ def test_console_setup_fastopenapi_get_finished_without_setup_time(app: DifyApp,
assert response.get_json() == {"step": "finished", "setup_at": None}
-@pytest.mark.parametrize("enterprise_enabled", [False, True], ids=["community", "enterprise"])
+@pytest.mark.parametrize(
+ "deployment_edition",
+ [DeploymentEdition.COMMUNITY, DeploymentEdition.ENTERPRISE],
+ ids=["community", "enterprise"],
+)
def test_console_setup_fastopenapi_post_success(
app: DifyApp,
setup_service: Mock,
monkeypatch: pytest.MonkeyPatch,
- enterprise_enabled: bool,
+ deployment_edition: DeploymentEdition,
) -> None:
- monkeypatch.setattr(wraps.dify_config, "EDITION", "SELF_HOSTED")
- monkeypatch.setattr(wraps.dify_config, "ENTERPRISE_ENABLED", enterprise_enabled)
+ monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", deployment_edition)
monkeypatch.setattr(setup_controller, "get_init_validate_status", lambda: True)
mark_setup_completed = Mock()
monkeypatch.setattr(setup_controller, "mark_setup_completed", mark_setup_completed)
@@ -112,8 +116,7 @@ def test_console_setup_fastopenapi_post_rejects_cloud_edition(
setup_service: Mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
- monkeypatch.setattr(wraps.dify_config, "EDITION", "CLOUD")
- monkeypatch.setattr(wraps.dify_config, "ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
response = app.test_client().post(
"/console/api/setup",
@@ -167,8 +170,7 @@ def test_console_setup_fastopenapi_post_rejects_invalid_payload_before_service_c
monkeypatch: pytest.MonkeyPatch,
payload: dict[str, str],
) -> None:
- monkeypatch.setattr(wraps.dify_config, "EDITION", "SELF_HOSTED")
- monkeypatch.setattr(wraps.dify_config, "ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
response = app.test_client().post("/console/api/setup", json=payload)
@@ -194,8 +196,7 @@ def test_console_setup_translates_service_errors_to_controller_errors(
service_error: Exception,
expected_controller_error: type[Exception],
) -> None:
- monkeypatch.setattr(wraps.dify_config, "EDITION", "SELF_HOSTED")
- monkeypatch.setattr(wraps.dify_config, "ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(setup_controller, "get_init_validate_status", lambda: False)
mark_setup_completed = Mock()
monkeypatch.setattr(setup_controller, "mark_setup_completed", mark_setup_completed)
@@ -226,8 +227,7 @@ def test_console_setup_fastopenapi_does_not_mark_setup_completed_when_service_fa
setup_service: Mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
- monkeypatch.setattr(wraps.dify_config, "EDITION", "SELF_HOSTED")
- monkeypatch.setattr(wraps.dify_config, "ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(setup_controller, "get_init_validate_status", lambda: True)
mark_setup_completed = Mock()
monkeypatch.setattr(setup_controller, "mark_setup_completed", mark_setup_completed)
diff --git a/api/tests/unit_tests/controllers/console/test_feature.py b/api/tests/unit_tests/controllers/console/test_feature.py
index 8491ad89d66..97e0a1aa8c6 100644
--- a/api/tests/unit_tests/controllers/console/test_feature.py
+++ b/api/tests/unit_tests/controllers/console/test_feature.py
@@ -3,7 +3,7 @@ from unittest.mock import create_autospec
from pytest_mock import MockerFixture
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from extensions.ext_application_services import ApplicationServices
from machinery.context import RequestContext
from services.entities.feature_entities import (
diff --git a/api/tests/unit_tests/controllers/console/test_init_validate.py b/api/tests/unit_tests/controllers/console/test_init_validate.py
index 377135e3f2f..a639886d48b 100644
--- a/api/tests/unit_tests/controllers/console/test_init_validate.py
+++ b/api/tests/unit_tests/controllers/console/test_init_validate.py
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
from controllers.console import init_validate
from controllers.console.error import AlreadySetupError, InitValidateFailedError
+from enums import DeploymentEdition
from models.model import DifySetup
@@ -26,7 +27,7 @@ def test_get_init_status_not_started(monkeypatch: pytest.MonkeyPatch) -> None:
def test_validate_init_password_already_setup(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr(init_validate.dify_config, "EDITION", "SELF_HOSTED")
+ monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(init_validate.TenantService, "get_tenant_count", lambda *, session: 1)
app.secret_key = "test-secret"
@@ -36,7 +37,7 @@ def test_validate_init_password_already_setup(app: Flask, monkeypatch: pytest.Mo
def test_validate_init_password_wrong_password(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr(init_validate.dify_config, "EDITION", "SELF_HOSTED")
+ monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(init_validate.TenantService, "get_tenant_count", lambda *, session: 0)
monkeypatch.setenv("INIT_PASSWORD", "expected")
app.secret_key = "test-secret"
@@ -48,7 +49,7 @@ def test_validate_init_password_wrong_password(app: Flask, monkeypatch: pytest.M
def test_validate_init_password_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr(init_validate.dify_config, "EDITION", "SELF_HOSTED")
+ monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(init_validate.TenantService, "get_tenant_count", lambda *, session: 0)
monkeypatch.setenv("INIT_PASSWORD", "expected")
app.secret_key = "test-secret"
@@ -60,12 +61,12 @@ def test_validate_init_password_success(app: Flask, monkeypatch: pytest.MonkeyPa
def test_get_init_validate_status_not_self_hosted(monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr(init_validate.dify_config, "EDITION", "CLOUD")
+ monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
assert init_validate.get_init_validate_status() is True
def test_get_init_validate_status_validated_session(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr(init_validate.dify_config, "EDITION", "SELF_HOSTED")
+ monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setenv("INIT_PASSWORD", "expected")
app.secret_key = "test-secret"
@@ -78,7 +79,7 @@ def test_get_init_validate_status_validated_session(app: Flask, monkeypatch: pyt
def test_get_init_validate_status_setup_exists(
app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
) -> None:
- monkeypatch.setattr(init_validate.dify_config, "EDITION", "SELF_HOSTED")
+ monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setenv("INIT_PASSWORD", "expected")
monkeypatch.setattr(init_validate, "db", SimpleNamespace(engine=sqlite_session.get_bind()))
sqlite_session.add(DifySetup(version="test-version"))
@@ -94,7 +95,7 @@ def test_get_init_validate_status_setup_exists(
def test_get_init_validate_status_not_validated(
app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
) -> None:
- monkeypatch.setattr(init_validate.dify_config, "EDITION", "SELF_HOSTED")
+ monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setenv("INIT_PASSWORD", "expected")
monkeypatch.setattr(init_validate, "db", SimpleNamespace(engine=sqlite_session.get_bind()))
app.secret_key = "test-secret"
diff --git a/api/tests/unit_tests/controllers/console/test_workflow_run_archive.py b/api/tests/unit_tests/controllers/console/test_workflow_run_archive.py
index 4ecc1e8584e..552a3570900 100644
--- a/api/tests/unit_tests/controllers/console/test_workflow_run_archive.py
+++ b/api/tests/unit_tests/controllers/console/test_workflow_run_archive.py
@@ -91,7 +91,6 @@ def test_workflow_run_archive_endpoints_require_cloud_paid_plan(method) -> None:
assert {
"only_edition_cloud",
- "cloud_edition_billing_enabled",
"cloud_edition_billing_paid_plan_required",
} <= decorator_names
assert "rbac_permission_required" not in decorator_names
diff --git a/api/tests/unit_tests/controllers/console/test_workspace_members.py b/api/tests/unit_tests/controllers/console/test_workspace_members.py
index 9b3a272dfcb..cb7df1644b5 100644
--- a/api/tests/unit_tests/controllers/console/test_workspace_members.py
+++ b/api/tests/unit_tests/controllers/console/test_workspace_members.py
@@ -7,6 +7,7 @@ from flask import Flask, g
from controllers.console.workspace.error import InvalidMemberRoleError
from controllers.console.workspace.members import MemberInviteEmailApi
+from enums import DeploymentEdition
from models.account import Account, TenantAccountRole
@@ -53,8 +54,7 @@ class TestMemberInviteEmailApi:
patch("controllers.console.workspace.members.dify_config.RBAC_ENABLED", False),
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "https://console.example.com"),
patch("controllers.console.workspace.members._count_new_member_invites", return_value=(1, 1)),
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
with app.test_request_context(
"/workspaces/current/members/invite-email",
diff --git a/api/tests/unit_tests/controllers/console/test_wraps.py b/api/tests/unit_tests/controllers/console/test_wraps.py
index d0c6fb54e16..126acdee4eb 100644
--- a/api/tests/unit_tests/controllers/console/test_wraps.py
+++ b/api/tests/unit_tests/controllers/console/test_wraps.py
@@ -19,7 +19,6 @@ from controllers.console.wraps import (
RBACResourceScope,
_is_setup_completed,
account_initialization_required,
- cloud_edition_billing_enabled,
cloud_edition_billing_paid_plan_required,
cloud_edition_billing_rate_limit_check,
cloud_edition_billing_resource_check,
@@ -36,6 +35,7 @@ from controllers.console.wraps import (
with_current_user,
with_current_user_id,
)
+from enums import DeploymentEdition
from libs.login import AccountWithTenant
from machinery.context import RequestContext
from models import Account
@@ -509,7 +509,7 @@ class TestEditionChecks:
return "cloud_success"
# Act
- with patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"):
+ with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD):
result = cloud_view()
# Assert
@@ -526,13 +526,13 @@ class TestEditionChecks:
# Act & Assert
with app.test_request_context():
- with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"):
+ with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
with pytest.raises(HTTPException) as exc_info:
cloud_view()
assert exc_info.value.code == 404
- def test_only_edition_enterprise_allows_when_enabled(self):
- """Test enterprise edition decorator allows when ENTERPRISE_ENABLED is True"""
+ def test_only_edition_enterprise_allows_enterprise_edition(self):
+ """Test enterprise edition decorator allows the ENTERPRISE edition."""
# Arrange
@only_edition_enterprise
@@ -540,14 +540,14 @@ class TestEditionChecks:
return "enterprise_success"
# Act
- with patch("controllers.console.wraps.dify_config.ENTERPRISE_ENABLED", True):
+ with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE):
result = enterprise_view()
# Assert
assert result == "enterprise_success"
def test_only_edition_self_hosted_allows_self_hosted(self):
- """Test self-hosted edition decorator allows SELF_HOSTED edition"""
+ """Test self-hosted edition decorator allows the COMMUNITY edition."""
# Arrange
@only_edition_self_hosted
@@ -555,49 +555,13 @@ class TestEditionChecks:
return "self_hosted_success"
# Act
- with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"):
+ with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
result = self_hosted_view()
# Assert
assert result == "self_hosted_success"
-class TestBillingEnabled:
- """Test billing enabled decorator."""
-
- def test_should_allow_when_billing_config_enabled(self):
- """Test billing decorator uses local config without loading tenant features."""
-
- @cloud_edition_billing_enabled
- def billing_view():
- return "billing_success"
-
- with patch("controllers.console.wraps.dify_config.BILLING_ENABLED", True):
- with patch("controllers.console.wraps.FeatureService.get_features") as get_features:
- result = billing_view()
-
- assert result == "billing_success"
- get_features.assert_not_called()
-
- def test_should_reject_when_billing_config_disabled(self):
- """Test billing decorator rejects when local billing config is disabled."""
- app = create_app_with_login()
-
- @cloud_edition_billing_enabled
- def billing_view():
- return "billing_success"
-
- with app.test_request_context():
- with patch("controllers.console.wraps.dify_config.BILLING_ENABLED", False):
- with patch("controllers.console.wraps.FeatureService.get_features") as get_features:
- with pytest.raises(HTTPException) as exc_info:
- billing_view()
-
- assert exc_info.value.code == 403
- assert "Billing feature is not enabled" in str(exc_info.value.description)
- get_features.assert_not_called()
-
-
class TestBillingPaidPlanRequired:
@pytest.mark.parametrize("plan", ["professional", "team"])
def test_should_allow_paid_plan(self, plan: str):
@@ -689,7 +653,7 @@ class TestBillingResourceLimits:
"controllers.console.wraps.current_account_with_tenant", return_value=(MockUser("test_user"), "tenant123")
):
with (
- patch("controllers.console.wraps.dify_config.BILLING_ENABLED", True),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch(
"controllers.console.wraps.FeatureService.get_vector_space", return_value=mock_vector_space
) as get_vector_space,
@@ -852,8 +816,8 @@ class TestRateLimiting:
class TestCloudUtmRecord:
"""Test cloud UTM recording decorator."""
- def test_should_record_utm_when_billing_config_enabled_and_cookie_exists(self):
- """Test UTM recording uses billing config without loading tenant features."""
+ def test_should_record_utm_for_cloud_edition_and_cookie(self):
+ """Test Cloud UTM recording without loading tenant features."""
app = create_app_with_login()
@cloud_utm_record
@@ -862,7 +826,7 @@ class TestCloudUtmRecord:
with app.test_request_context("/", headers={"Cookie": "utm_info={}"}):
with (
- patch("controllers.console.wraps.dify_config.BILLING_ENABLED", True),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.wraps.current_account_with_tenant", return_value=(MockUser("u1"), "t1")),
patch("controllers.console.wraps.OperationService.record_utm") as record_utm,
patch("controllers.console.wraps.FeatureService.get_features") as get_features,
@@ -873,8 +837,8 @@ class TestCloudUtmRecord:
record_utm.assert_called_once_with("t1", {})
get_features.assert_not_called()
- def test_should_skip_utm_when_billing_config_disabled(self):
- """Test UTM recording skips tenant feature loading when billing config is disabled."""
+ def test_should_skip_utm_outside_cloud_edition(self):
+ """Test UTM recording skips tenant feature loading outside the Cloud edition."""
app = create_app_with_login()
@cloud_utm_record
@@ -883,7 +847,7 @@ class TestCloudUtmRecord:
with app.test_request_context("/", headers={"Cookie": "utm_info={}"}):
with (
- patch("controllers.console.wraps.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("controllers.console.wraps.current_account_with_tenant") as current_account,
patch("controllers.console.wraps.OperationService.record_utm") as record_utm,
patch("controllers.console.wraps.FeatureService.get_features") as get_features,
@@ -909,7 +873,7 @@ class TestSystemSetup:
return "admin_success"
# Act
- with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"):
+ with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
result = admin_view()
# Assert
@@ -924,7 +888,7 @@ class TestSystemSetup:
def admin_view():
return "admin_success"
- with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"):
+ with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
assert admin_view() == "admin_success"
assert admin_view() == "admin_success"
@@ -941,7 +905,7 @@ class TestSystemSetup:
def admin_view():
return "admin_success"
- with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"):
+ with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
with pytest.raises(NotSetupError):
admin_view()
assert admin_view() == "admin_success"
@@ -961,7 +925,7 @@ class TestSystemSetup:
return "admin_success"
# Act & Assert
- with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"):
+ with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
with pytest.raises(NotInitValidateError):
admin_view()
@@ -978,7 +942,7 @@ class TestSystemSetup:
return "admin_success"
# Act & Assert
- with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"):
+ with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
with pytest.raises(NotSetupError):
admin_view()
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_accounts.py b/api/tests/unit_tests/controllers/console/workspace/test_accounts.py
index 6abc1f0379e..d2857182858 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_accounts.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_accounts.py
@@ -36,6 +36,7 @@ from controllers.console.workspace.error import (
CurrentPasswordIncorrectError,
InvalidAccountDeletionCodeError,
)
+from enums import DeploymentEdition
from extensions.storage.storage_type import StorageType
from models import Account, AccountIntegrate, InvitationCode, Tenant, TenantAccountJoin
from models.account import AccountStatus, InvitationCodeStatus, TenantAccountRole
@@ -119,7 +120,7 @@ class TestAccountInitApi:
with (
app.test_request_context("/account/init", json=payload),
- patch("controllers.console.workspace.account.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.workspace.account.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.workspace.account.db.session", sqlite_session),
):
resp = method(api, account)
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_members.py b/api/tests/unit_tests/controllers/console/workspace/test_members.py
index 98222e4836b..e7b60184275 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_members.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_members.py
@@ -31,6 +31,7 @@ from controllers.console.workspace.members import (
SendOwnerTransferEmailApi,
_count_new_member_invites,
)
+from enums import DeploymentEdition
from libs.external_api import ExternalApi
from machinery.context import RequestContext
from services.errors.account import AccountAlreadyInTenantError, SeatsLimitExceededError
@@ -134,7 +135,6 @@ class TestMemberInviteEmailApi:
tenant = MagicMock(id="t1")
user = MagicMock(current_tenant=tenant)
features = MagicMock()
- features.billing.enabled = False
features.workspace_members.enabled = False
features.workspace_members.is_available.return_value = True
@@ -152,8 +152,7 @@ class TestMemberInviteEmailApi:
"controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"
) as mock_invite,
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
result, status = method(api, user)
@@ -171,7 +170,6 @@ class TestMemberInviteEmailApi:
tenant = MagicMock(id="t1")
user = MagicMock(current_tenant=tenant)
features = MagicMock()
- features.billing.enabled = False
features.workspace_members.enabled = True
features.workspace_members.is_available.return_value = False
@@ -184,20 +182,18 @@ class TestMemberInviteEmailApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.members.FeatureService.get_features", return_value=features),
patch("controllers.console.workspace.members._count_new_member_invites", return_value=(1, 1)),
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", True),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
):
with pytest.raises(WorkspaceMembersLimitExceeded):
method(api, user)
- def test_invite_billing_limit_exceeded(self, app: Flask):
+ def test_invite_cloud_member_limit_exceeded(self, app: Flask):
api = MemberInviteEmailApi()
method = unwrap(api.post)
tenant = MagicMock(id="t1")
user = MagicMock(current_tenant=tenant)
features = MagicMock()
- features.billing.enabled = True
features.members.size = 9
features.members.limit = 10
features.workspace_members.enabled = False
@@ -212,8 +208,7 @@ class TestMemberInviteEmailApi:
patch("controllers.console.workspace.members.FeatureService.get_features", return_value=features),
patch("controllers.console.workspace.members._count_new_member_invites", return_value=(2, 2)),
patch("controllers.console.workspace.members._count_current_members", return_value=9),
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", True),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
):
with pytest.raises(WorkspaceMembersLimitExceeded):
method(api, user)
@@ -225,7 +220,6 @@ class TestMemberInviteEmailApi:
tenant = MagicMock(id="t1")
user = MagicMock(current_tenant=tenant)
features = MagicMock()
- features.billing.enabled = False
features.workspace_members.enabled = False
features.workspace_members.is_available.return_value = True
@@ -243,8 +237,7 @@ class TestMemberInviteEmailApi:
side_effect=AccountAlreadyInTenantError(),
),
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
result, status = method(api, user)
@@ -293,7 +286,6 @@ class TestMemberInviteEmailApi:
tenant = MagicMock(id="t1")
user = MagicMock(current_tenant=tenant)
features = MagicMock()
- features.billing.enabled = False
features.workspace_members.enabled = False
features.workspace_members.is_available.return_value = True
@@ -311,8 +303,7 @@ class TestMemberInviteEmailApi:
side_effect=Exception("boom"),
),
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
result, _ = method(api, user)
@@ -325,7 +316,6 @@ class TestMemberInviteEmailApi:
tenant = MagicMock(id="t1")
user = MagicMock(current_tenant=tenant)
features = MagicMock()
- features.billing.enabled = False
features.workspace_members.enabled = False
license_info = MagicMock()
license_info.seats.is_available.return_value = False
@@ -344,8 +334,7 @@ class TestMemberInviteEmailApi:
return_value=license_info,
) as mock_get_license,
patch("controllers.console.workspace.members.RegisterService.invite_new_member") as mock_invite,
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", True),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
):
with pytest.raises(SeatsLimitExceeded):
method(api, user)
@@ -361,7 +350,6 @@ class TestMemberInviteEmailApi:
tenant = MagicMock(id="t1")
user = MagicMock(current_tenant=tenant)
features = MagicMock()
- features.billing.enabled = False
features.workspace_members.enabled = False
license_info = MagicMock()
license_info.seats.is_available.return_value = False
@@ -383,8 +371,7 @@ class TestMemberInviteEmailApi:
"controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"
) as mock_invite,
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", True),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
):
result, status = method(api, user)
@@ -401,7 +388,6 @@ class TestMemberInviteEmailApi:
tenant = MagicMock(id="t1")
user = MagicMock(current_tenant=tenant)
features = MagicMock()
- features.billing.enabled = False
features.workspace_members.enabled = False
license_info = MagicMock()
license_info.seats.is_available.return_value = True
@@ -423,8 +409,7 @@ class TestMemberInviteEmailApi:
"controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"
) as mock_invite,
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", True),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
):
result, status = method(api, user)
@@ -441,7 +426,6 @@ class TestMemberInviteEmailApi:
tenant = MagicMock(id="t1")
user = MagicMock(current_tenant=tenant)
features = MagicMock()
- features.billing.enabled = False
features.workspace_members.enabled = False
license_info = MagicMock()
license_info.seats.is_available.return_value = False
@@ -461,8 +445,7 @@ class TestMemberInviteEmailApi:
) as mock_get_license,
patch("controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"),
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
result, status = method(api, user)
@@ -478,7 +461,6 @@ class TestMemberInviteEmailApi:
tenant = MagicMock(id="t1")
user = MagicMock(current_tenant=tenant)
features = MagicMock()
- features.billing.enabled = False
features.workspace_members.enabled = False
license_info = MagicMock()
license_info.seats.is_available.return_value = True
@@ -501,8 +483,7 @@ class TestMemberInviteEmailApi:
side_effect=SeatsLimitExceededError("licensed seats limit exceeded"),
),
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
- patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", True),
- patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
+ patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
):
result, status = method(api, user)
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py
index b45f6183cbf..44ab34a927d 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py
@@ -27,6 +27,7 @@ from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config
from controllers.console.workspace import rbac as rbac_mod
from controllers.console.workspace.rbac import _RolesListQuery
+from enums import DeploymentEdition
@pytest.fixture
@@ -37,7 +38,8 @@ def app():
def _enabled(enabled: bool):
- return patch("controllers.console.workspace.rbac.dify_config.ENTERPRISE_ENABLED", enabled)
+ deployment_edition = DeploymentEdition.ENTERPRISE if enabled else DeploymentEdition.COMMUNITY
+ return patch("controllers.console.workspace.rbac.dify_config.DEPLOYMENT_EDITION", deployment_edition)
class TestCurrentIds:
@@ -184,14 +186,14 @@ class TestPaginationMapping:
owner_permission_keys = rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["owner"]
valid_owner_permission_keys = []
for permission_key in owner_permission_keys:
- if not dify_config.BILLING_ENABLED and "billing" in permission_key:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD and "billing" in permission_key:
continue
valid_owner_permission_keys.append(permission_key)
admin_permission_keys = rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["admin"]
valid_admin_permission_keys = []
for permission_key in admin_permission_keys:
- if not dify_config.BILLING_ENABLED and "billing" in permission_key:
+ if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD and "billing" in permission_key:
continue
valid_admin_permission_keys.append(permission_key)
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_tool_providers.py b/api/tests/unit_tests/controllers/console/workspace/test_tool_providers.py
index 845d596294e..5d5f47333c0 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_tool_providers.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_tool_providers.py
@@ -18,6 +18,7 @@ from sqlalchemy.orm import Session, scoped_session, sessionmaker
from core.tools.entities.api_entities import ToolProviderApiEntity as CoreToolProviderApiEntity
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolParameter
+from enums import DeploymentEdition
from models import Account, BuiltinToolProvider, Tenant, TenantAccountJoin
from models.account import TenantAccountRole
from models.credential_permission import CredentialPermission
@@ -74,8 +75,8 @@ def controller_module(monkeypatch: pytest.MonkeyPatch):
global _WRAPS_MODULE
wraps_module = importlib.import_module("controllers.console.wraps")
_WRAPS_MODULE = wraps_module
- monkeypatch.setattr(module.dify_config, "EDITION", "CLOUD")
- monkeypatch.setattr(wraps_module.dify_config, "EDITION", "CLOUD")
+ monkeypatch.setattr(module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
+ monkeypatch.setattr(wraps_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
login_module = importlib.import_module("libs.login")
monkeypatch.setattr(login_module, "check_csrf_token", lambda *args, **kwargs: None)
@@ -720,7 +721,7 @@ def test_tool_labels_list(app: Flask, controller_module, monkeypatch: pytest.Mon
def test_resolve_identity_mode_none_keeps_current_when_enterprise(controller_module, monkeypatch: pytest.MonkeyPatch):
"""None means 'leave unchanged' — fall back to the stored mode (update path)."""
identity_mode = importlib.import_module("core.entities.mcp_provider").IdentityMode
- monkeypatch.setattr(controller_module.dify_config, "ENTERPRISE_ENABLED", True)
+ monkeypatch.setattr(controller_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
resolved = controller_module._resolve_identity_mode(None, current=identity_mode.IDP_TOKEN)
@@ -730,7 +731,7 @@ def test_resolve_identity_mode_none_keeps_current_when_enterprise(controller_mod
def test_resolve_identity_mode_explicit_value_overrides_current(controller_module, monkeypatch: pytest.MonkeyPatch):
"""An explicit value wins over the stored mode."""
identity_mode = importlib.import_module("core.entities.mcp_provider").IdentityMode
- monkeypatch.setattr(controller_module.dify_config, "ENTERPRISE_ENABLED", True)
+ monkeypatch.setattr(controller_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
resolved = controller_module._resolve_identity_mode(identity_mode.OFF, current=identity_mode.IDP_TOKEN)
@@ -743,7 +744,7 @@ def test_resolve_identity_mode_coerces_non_off_to_off_when_not_enterprise(
"""Gate: a non-EE deployment must never persist a non-OFF mode — the
runtime won't forward, so the stored row must not imply it does."""
identity_mode = importlib.import_module("core.entities.mcp_provider").IdentityMode
- monkeypatch.setattr(controller_module.dify_config, "ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(controller_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
# Both an explicit idp_token request AND an inherited non-OFF current
# must collapse to OFF.
@@ -759,6 +760,6 @@ def test_resolve_identity_mode_off_is_passthrough_when_not_enterprise(
):
"""OFF is always fine — the gate only neutralizes non-OFF values."""
identity_mode = importlib.import_module("core.entities.mcp_provider").IdentityMode
- monkeypatch.setattr(controller_module.dify_config, "ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(controller_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
assert controller_module._resolve_identity_mode(None, current=identity_mode.OFF) == identity_mode.OFF
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_workspace.py b/api/tests/unit_tests/controllers/console/workspace/test_workspace.py
index cd1afae7f5e..c6028dba20e 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_workspace.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_workspace.py
@@ -38,8 +38,7 @@ from controllers.console.workspace.workspace import (
WorkspacePermissionApi,
WorkspacePermissionResponse,
)
-from enums.cloud_plan import CloudPlan
-from enums.deployment_edition import DeploymentEdition
+from enums import CloudPlan, DeploymentEdition
from libs.datetime_utils import naive_utc_now
from machinery.context import RequestContext
from models.account import Account, Tenant, TenantAccountJoin, TenantCustomConfigDict, TenantStatus
@@ -71,16 +70,12 @@ def workspace_plan_dependencies(monkeypatch: pytest.MonkeyPatch) -> tuple[MagicM
def configure_workspace_plans(
monkeypatch: pytest.MonkeyPatch,
*,
- enterprise_enabled: bool = False,
- billing_enabled: bool = True,
edition: DeploymentEdition = DeploymentEdition.CLOUD,
) -> None:
monkeypatch.setattr(
workspace_plan_gateway,
"dify_config",
SimpleNamespace(
- ENTERPRISE_ENABLED=enterprise_enabled,
- BILLING_ENABLED=billing_enabled,
DEPLOYMENT_EDITION=edition,
),
)
@@ -275,7 +270,6 @@ class TestDeploymentWorkspacePlanGateway:
) -> None:
configure_workspace_plans(
monkeypatch,
- billing_enabled=False,
edition=DeploymentEdition.COMMUNITY,
)
get_plan_bulk, get_features = workspace_plan_dependencies
@@ -294,8 +288,6 @@ class TestDeploymentWorkspacePlanGateway:
) -> None:
configure_workspace_plans(
monkeypatch,
- enterprise_enabled=True,
- billing_enabled=False,
edition=DeploymentEdition.ENTERPRISE,
)
get_plan_bulk, get_features = workspace_plan_dependencies
diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_composition.py b/api/tests/unit_tests/controllers/openapi/auth/test_composition.py
index 11cd1aa1380..41d4416efc5 100644
--- a/api/tests/unit_tests/controllers/openapi/auth/test_composition.py
+++ b/api/tests/unit_tests/controllers/openapi/auth/test_composition.py
@@ -13,6 +13,7 @@ from controllers.openapi.auth.verify import (
check_workspace_role,
)
from core.rbac import RBACPermission, RBACResourceScope
+from enums import DeploymentEdition
from libs.oauth_bearer import Scope, TokenType
from models.account import TenantAccountRole
from services.enterprise.enterprise_service import WebAppAccessMode
@@ -70,12 +71,10 @@ def test_router_routes_contain_both_token_types():
assert TokenType.OAUTH_EXTERNAL_SSO in auth_router._routes
-def test_external_sso_route_has_ee_required_edition():
+def test_external_sso_route_requires_enterprise_edition():
route = auth_router._routes[TokenType.OAUTH_EXTERNAL_SSO]
assert isinstance(route, PipelineRoute)
- from controllers.openapi.auth.data import Edition
-
- assert route.required_edition == frozenset({Edition.EE})
+ assert route.required_edition == frozenset({DeploymentEdition.ENTERPRISE})
def test_account_route_has_no_required_edition():
@@ -147,7 +146,7 @@ def _selected_webapp_steps(*, scope, app_access_mode):
"""
from unittest.mock import MagicMock, patch
- from controllers.openapi.auth.data import AuthData, Edition
+ from controllers.openapi.auth.data import AuthData
ctx = RequestContext(
token_type=TokenType.OAUTH_ACCOUNT,
@@ -164,7 +163,10 @@ def _selected_webapp_steps(*, scope, app_access_mode):
features.webapp_auth.enabled = True
selected = []
with (
- patch("controllers.openapi.auth.conditions.current_edition", return_value=Edition.EE),
+ patch(
+ "controllers.openapi.auth.conditions.dify_config.DEPLOYMENT_EDITION",
+ DeploymentEdition.ENTERPRISE,
+ ),
patch("controllers.openapi.auth.conditions.FeatureService.get_system_features", return_value=features),
):
for step in account_pipeline._auth:
diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py b/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py
index fa882ca96c4..b9cd877f0bf 100644
--- a/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py
+++ b/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py
@@ -1,9 +1,9 @@
from unittest.mock import MagicMock, patch
from controllers.openapi.auth.conditions import (
- EDITION_CE,
- EDITION_EE,
- EDITION_SAAS,
+ EDITION_CLOUD,
+ EDITION_COMMUNITY,
+ EDITION_ENTERPRISE,
HAS_ALLOWED_ROLES,
HAS_RBAC,
LOADED_APP_IS_PRIVATE,
@@ -18,8 +18,9 @@ from controllers.openapi.auth.conditions import (
data_cond,
request_cond,
)
-from controllers.openapi.auth.data import AuthData, Edition, RBACRequirement, RequestContext
+from controllers.openapi.auth.data import AuthData, RBACRequirement, RequestContext
from core.rbac import RBACPermission, RBACResourceScope
+from enums import DeploymentEdition
from libs.oauth_bearer import Scope, TokenType
from models.account import TenantAccountRole
from services.enterprise.enterprise_service import WebAppAccessMode
@@ -115,22 +116,31 @@ def test_path_has_app_id_false():
assert PATH_HAS_APP_ID(_ctx(path_params={})) is False
-def test_edition_ce():
- with patch("controllers.openapi.auth.conditions.current_edition", return_value=Edition.CE):
- assert EDITION_CE(_ctx()) is True
- assert EDITION_EE(_ctx()) is False
- assert EDITION_SAAS(_ctx()) is False
+def test_edition_community():
+ with patch(
+ "controllers.openapi.auth.conditions.dify_config.DEPLOYMENT_EDITION",
+ DeploymentEdition.COMMUNITY,
+ ):
+ assert EDITION_COMMUNITY(_ctx()) is True
+ assert EDITION_ENTERPRISE(_ctx()) is False
+ assert EDITION_CLOUD(_ctx()) is False
-def test_edition_ee():
- with patch("controllers.openapi.auth.conditions.current_edition", return_value=Edition.EE):
- assert EDITION_EE(_ctx()) is True
- assert EDITION_CE(_ctx()) is False
+def test_edition_enterprise():
+ with patch(
+ "controllers.openapi.auth.conditions.dify_config.DEPLOYMENT_EDITION",
+ DeploymentEdition.ENTERPRISE,
+ ):
+ assert EDITION_ENTERPRISE(_ctx()) is True
+ assert EDITION_COMMUNITY(_ctx()) is False
-def test_edition_saas():
- with patch("controllers.openapi.auth.conditions.current_edition", return_value=Edition.SAAS):
- assert EDITION_SAAS(_ctx()) is True
+def test_edition_cloud():
+ with patch(
+ "controllers.openapi.auth.conditions.dify_config.DEPLOYMENT_EDITION",
+ DeploymentEdition.CLOUD,
+ ):
+ assert EDITION_CLOUD(_ctx()) is True
def test_webapp_auth_enabled():
diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_data.py b/api/tests/unit_tests/controllers/openapi/auth/test_data.py
index 7ee1a83ec00..e5171e64303 100644
--- a/api/tests/unit_tests/controllers/openapi/auth/test_data.py
+++ b/api/tests/unit_tests/controllers/openapi/auth/test_data.py
@@ -1,41 +1,16 @@
import uuid
-from unittest.mock import patch
import pytest
from pydantic import ValidationError
from controllers.openapi.auth.data import (
AuthData,
- Edition,
ExternalIdentity,
RequestContext,
- current_edition,
)
-from enums.deployment_edition import DeploymentEdition
from libs.oauth_bearer import Scope, TokenType
-def test_current_edition_saas():
- with patch("controllers.openapi.auth.data.dify_config") as cfg:
- cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
- cfg.ENTERPRISE_ENABLED = True
- assert current_edition() == Edition.SAAS
-
-
-def test_current_edition_ee():
- with patch("controllers.openapi.auth.data.dify_config") as cfg:
- cfg.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
- cfg.ENTERPRISE_ENABLED = True
- assert current_edition() == Edition.EE
-
-
-def test_current_edition_ce():
- with patch("controllers.openapi.auth.data.dify_config") as cfg:
- cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
- cfg.ENTERPRISE_ENABLED = False
- assert current_edition() == Edition.CE
-
-
def test_external_identity_frozen():
ei = ExternalIdentity(email="a@b.com", issuer="idp")
with pytest.raises(ValidationError):
diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_pipeline.py b/api/tests/unit_tests/controllers/openapi/auth/test_pipeline.py
index 37b400b92d0..a9f5df5aae6 100644
--- a/api/tests/unit_tests/controllers/openapi/auth/test_pipeline.py
+++ b/api/tests/unit_tests/controllers/openapi/auth/test_pipeline.py
@@ -5,8 +5,9 @@ import pytest
from flask import Flask
from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
-from controllers.openapi.auth.data import AuthData, Edition
+from controllers.openapi.auth.data import AuthData
from controllers.openapi.auth.pipeline import AuthPipeline, PipelineRoute, PipelineRouter
+from enums import DeploymentEdition
from libs.oauth_bearer import Scope, TokenType
@@ -75,9 +76,12 @@ def test_guard_edition_gate_returns_404(app):
router = _make_router()
with app.test_request_context("/test"):
- with patch("controllers.openapi.auth.pipeline.current_edition", return_value=Edition.CE):
+ with patch(
+ "controllers.openapi.auth.pipeline.dify_config.DEPLOYMENT_EDITION",
+ DeploymentEdition.COMMUNITY,
+ ):
- @router.guard(scope=Scope.FULL, edition=frozenset({Edition.EE}))
+ @router.guard(scope=Scope.FULL, edition=frozenset({DeploymentEdition.ENTERPRISE}))
def view(*, auth_data):
pass
@@ -93,7 +97,10 @@ def test_guard_token_type_gate_returns_403(app):
patch("controllers.openapi.auth.pipeline.extract_bearer", return_value="tok"),
patch("controllers.openapi.auth.pipeline.get_authenticator") as mock_auth,
patch("controllers.openapi.auth.pipeline.emit_wrong_surface"),
- patch("controllers.openapi.auth.pipeline.current_edition", return_value=Edition.CE),
+ patch(
+ "controllers.openapi.auth.pipeline.dify_config.DEPLOYMENT_EDITION",
+ DeploymentEdition.COMMUNITY,
+ ),
):
identity = _fake_identity()
identity.token_type = TokenType.OAUTH_EXTERNAL_SSO
@@ -114,7 +121,10 @@ def test_guard_unregistered_token_type_returns_403(app):
with (
patch("controllers.openapi.auth.pipeline.extract_bearer", return_value="tok"),
patch("controllers.openapi.auth.pipeline.get_authenticator") as mock_auth,
- patch("controllers.openapi.auth.pipeline.current_edition", return_value=Edition.CE),
+ patch(
+ "controllers.openapi.auth.pipeline.dify_config.DEPLOYMENT_EDITION",
+ DeploymentEdition.COMMUNITY,
+ ),
):
identity = _fake_identity()
identity.token_type = TokenType.OAUTH_EXTERNAL_SSO
@@ -196,14 +206,17 @@ def test_guard_resets_auth_ctx_on_exception(app):
def test_router_rejects_token_type_on_wrong_edition(app):
pipeline = AuthPipeline(prepare=[], auth=[])
- route = PipelineRoute(pipeline, required_edition=frozenset({Edition.EE}))
+ route = PipelineRoute(pipeline, required_edition=frozenset({DeploymentEdition.ENTERPRISE}))
router = PipelineRouter({TokenType.OAUTH_EXTERNAL_SSO: route})
with app.test_request_context("/test", headers={"Authorization": "Bearer tok"}):
with (
patch("controllers.openapi.auth.pipeline.extract_bearer", return_value="tok"),
patch("controllers.openapi.auth.pipeline.get_authenticator") as mock_auth,
- patch("controllers.openapi.auth.pipeline.current_edition", return_value=Edition.CE),
+ patch(
+ "controllers.openapi.auth.pipeline.dify_config.DEPLOYMENT_EDITION",
+ DeploymentEdition.COMMUNITY,
+ ),
):
identity = _make_identity(token_type=TokenType.OAUTH_EXTERNAL_SSO)
mock_auth.return_value.authenticate.return_value = identity
diff --git a/api/tests/unit_tests/controllers/openapi/test_meta_version.py b/api/tests/unit_tests/controllers/openapi/test_meta_version.py
index 57de0c517f4..3da3c4fca21 100644
--- a/api/tests/unit_tests/controllers/openapi/test_meta_version.py
+++ b/api/tests/unit_tests/controllers/openapi/test_meta_version.py
@@ -4,6 +4,8 @@ from __future__ import annotations
import pytest
+from enums import DeploymentEdition
+
def test_version_endpoint_returns_200_without_auth(openapi_app):
client = openapi_app.test_client()
@@ -15,7 +17,7 @@ def test_version_endpoint_returns_200_without_auth(openapi_app):
assert "version" in payload
assert "edition" in payload
assert isinstance(payload["version"], str)
- assert payload["edition"] in ("SELF_HOSTED", "CLOUD")
+ assert payload["edition"] in {edition.value for edition in DeploymentEdition}
def test_version_endpoint_ignores_bearer_header(openapi_app):
@@ -35,7 +37,7 @@ def test_version_endpoint_ignores_bearer_header(openapi_app):
def test_version_endpoint_reflects_edition_config(openapi_app, monkeypatch: pytest.MonkeyPatch):
from configs import dify_config
- monkeypatch.setattr(dify_config, "EDITION", "CLOUD")
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
client = openapi_app.test_client()
response = client.get("/openapi/v1/_version")
@@ -44,13 +46,13 @@ def test_version_endpoint_reflects_edition_config(openapi_app, monkeypatch: pyte
assert response.get_json()["edition"] == "CLOUD"
-def test_version_endpoint_falls_back_to_self_hosted_on_unexpected_edition(openapi_app, monkeypatch: pytest.MonkeyPatch):
+def test_version_endpoint_reflects_enterprise_edition(openapi_app, monkeypatch: pytest.MonkeyPatch):
from configs import dify_config
- monkeypatch.setattr(dify_config, "EDITION", "EXPERIMENTAL")
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
client = openapi_app.test_client()
response = client.get("/openapi/v1/_version")
assert response.status_code == 200
- assert response.get_json()["edition"] == "SELF_HOSTED"
+ assert response.get_json()["edition"] == "ENTERPRISE"
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_completion.py b/api/tests/unit_tests/controllers/service_api/app/test_completion.py
index de3ffa82018..39c986e3ca2 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_completion.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_completion.py
@@ -42,7 +42,7 @@ from controllers.service_api.app.error import (
)
from core.app.apps.agent_app.errors import AgentAppNotPublishedError
from core.errors.error import QuotaExceededError
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from graphon.model_runtime.errors.invoke import InvokeError
from models.base import TypeBase
from models.enums import ConversationFromSource, EndUserType
@@ -556,7 +556,7 @@ class TestChatApiController:
self, app: Flask, monkeypatch: pytest.MonkeyPatch, orm_session: Session
) -> None:
completion_module = sys.modules["controllers.service_api.app.completion"]
- monkeypatch.setattr(completion_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(completion_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
billing_get_info = Mock(return_value={"enabled": True, "subscription": {"plan": CloudPlan.SANDBOX}})
generate = Mock()
@@ -582,12 +582,13 @@ class TestChatApiController:
assert exc_info.value.error_code == "workflow_version_execution_not_allowed"
@pytest.mark.parametrize(
- ("billing_config_enabled", "billing_enabled", "plan", "workflow_id"),
+ ("deployment_edition", "billing_enabled", "plan", "workflow_id"),
[
- (False, True, CloudPlan.SANDBOX, str(uuid.uuid4())),
- (True, False, CloudPlan.SANDBOX, str(uuid.uuid4())),
- (True, True, CloudPlan.PROFESSIONAL, str(uuid.uuid4())),
- (True, True, CloudPlan.SANDBOX, None),
+ (DeploymentEdition.COMMUNITY, True, CloudPlan.SANDBOX, str(uuid.uuid4())),
+ (DeploymentEdition.ENTERPRISE, True, CloudPlan.SANDBOX, str(uuid.uuid4())),
+ (DeploymentEdition.CLOUD, False, CloudPlan.SANDBOX, str(uuid.uuid4())),
+ (DeploymentEdition.CLOUD, True, CloudPlan.PROFESSIONAL, str(uuid.uuid4())),
+ (DeploymentEdition.CLOUD, True, CloudPlan.SANDBOX, None),
],
)
def test_allows_default_or_entitled_workflow_version_execution(
@@ -595,13 +596,13 @@ class TestChatApiController:
app: Flask,
monkeypatch: pytest.MonkeyPatch,
orm_session: Session,
- billing_config_enabled: bool,
+ deployment_edition: DeploymentEdition,
billing_enabled: bool,
plan: CloudPlan,
workflow_id: str | None,
) -> None:
completion_module = sys.modules["controllers.service_api.app.completion"]
- monkeypatch.setattr(completion_module.dify_config, "BILLING_ENABLED", billing_config_enabled)
+ monkeypatch.setattr(completion_module.dify_config, "DEPLOYMENT_EDITION", deployment_edition)
billing_get_info = Mock(return_value={"enabled": billing_enabled, "subscription": {"plan": plan}})
generate = Mock(return_value={"result": "ok"})
@@ -621,7 +622,7 @@ class TestChatApiController:
assert response == {"result": "ok"}
generate.assert_called_once()
- if billing_config_enabled and workflow_id:
+ if deployment_edition == DeploymentEdition.CLOUD and workflow_id:
billing_get_info.assert_called_once_with(app_model.tenant_id, exclude_vector_space=True)
else:
billing_get_info.assert_not_called()
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py b/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py
index 6b6581d86f3..dd158731055 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py
@@ -36,11 +36,12 @@ from core.workflow.nodes.human_input.entities import ParagraphInputConfig, UserA
from core.workflow.nodes.human_input.enums import FormInputType, HumanInputFormKind, HumanInputFormStatus
from core.workflow.nodes.human_input.pause_reason import DifyHITLEventType, HumanInputRequired
from core.workflow.system_variables import build_system_variables
+from enums import DeploymentEdition
from graphon.entities import WorkflowStartReason
from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus
from graphon.runtime import GraphRuntimeState, VariablePool
from models.account import Account
-from models.enums import CreatorUserRole
+from models.enums import CreatorUserRole, MessageStatus
from models.human_input import HumanInputForm
from models.model import AppMode
from models.workflow import WorkflowRun
@@ -378,7 +379,7 @@ class TestHitlServiceApi:
monkeypatch: pytest.MonkeyPatch,
sqlite_engine: Engine,
) -> None:
- monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(ags_module, "RateLimit", _DummyRateLimit)
workflow = MagicMock()
@@ -453,7 +454,6 @@ class TestHitlServiceApi:
def test_advanced_chat_blocking_pipeline_pause_payload_contract(self) -> None:
from core.app.app_config.entities import AppAdditionalFeatures
from core.app.apps.advanced_chat.generate_task_pipeline import AdvancedChatAppGenerateTaskPipeline
- from models.enums import MessageStatus
from models.model import EndUser
app_config = WorkflowUIBasedAppConfig(
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py
index 404ac2ac611..c1cf3539477 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py
@@ -42,7 +42,7 @@ from controllers.service_api.app.workflow import (
)
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
from core.app.entities.app_invoke_entities import InvokeFrom
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from graphon.enums import WorkflowExecutionStatus
from models import Account
from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom
@@ -585,7 +585,7 @@ class TestWorkflowRunApi:
self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
) -> None:
workflow_module = sys.modules["controllers.service_api.app.workflow"]
- monkeypatch.setattr(workflow_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(workflow_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
billing_get_info = Mock(return_value={"enabled": True, "subscription": {"plan": CloudPlan.SANDBOX}})
generate = Mock(return_value={"result": "ok"})
@@ -613,7 +613,7 @@ class TestWorkflowRunByIdApi:
self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
) -> None:
workflow_module = sys.modules["controllers.service_api.app.workflow"]
- monkeypatch.setattr(workflow_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(workflow_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
billing_get_info = Mock(return_value={"enabled": True, "subscription": {"plan": CloudPlan.SANDBOX}})
generate = Mock()
@@ -648,24 +648,25 @@ class TestWorkflowRunByIdApi:
}
@pytest.mark.parametrize(
- ("billing_config_enabled", "billing_enabled", "plan"),
+ ("deployment_edition", "billing_enabled", "plan"),
[
- (False, True, CloudPlan.SANDBOX),
- (True, False, CloudPlan.SANDBOX),
- (True, True, CloudPlan.PROFESSIONAL),
+ (DeploymentEdition.COMMUNITY, True, CloudPlan.SANDBOX),
+ (DeploymentEdition.ENTERPRISE, True, CloudPlan.SANDBOX),
+ (DeploymentEdition.CLOUD, False, CloudPlan.SANDBOX),
+ (DeploymentEdition.CLOUD, True, CloudPlan.PROFESSIONAL),
],
)
def test_allows_execution_outside_enabled_sandbox_plan(
self,
app: Flask,
monkeypatch: pytest.MonkeyPatch,
- billing_config_enabled: bool,
+ deployment_edition: DeploymentEdition,
billing_enabled: bool,
plan: CloudPlan,
sqlite_session: Session,
) -> None:
workflow_module = sys.modules["controllers.service_api.app.workflow"]
- monkeypatch.setattr(workflow_module.dify_config, "BILLING_ENABLED", billing_config_enabled)
+ monkeypatch.setattr(workflow_module.dify_config, "DEPLOYMENT_EDITION", deployment_edition)
billing_get_info = Mock(return_value={"enabled": billing_enabled, "subscription": {"plan": plan}})
generate = Mock(return_value={"result": "ok"})
@@ -687,7 +688,7 @@ class TestWorkflowRunByIdApi:
assert response.get_json() == {"result": "ok"}
generate.assert_called_once()
- if billing_config_enabled:
+ if deployment_edition == DeploymentEdition.CLOUD:
billing_get_info.assert_called_once_with(app_model.tenant_id, exclude_vector_space=True)
else:
billing_get_info.assert_not_called()
@@ -695,7 +696,7 @@ class TestWorkflowRunByIdApi:
@pytest.mark.parametrize("sqlite_session", [()], indirect=True)
def test_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None:
workflow_module = sys.modules["controllers.service_api.app.workflow"]
- monkeypatch.setattr(workflow_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(workflow_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(
AppGenerateService,
"generate",
@@ -714,7 +715,7 @@ class TestWorkflowRunByIdApi:
@pytest.mark.parametrize("sqlite_session", [()], indirect=True)
def test_draft_workflow(self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None:
workflow_module = sys.modules["controllers.service_api.app.workflow"]
- monkeypatch.setattr(workflow_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(workflow_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(
AppGenerateService,
"generate",
diff --git a/api/tests/unit_tests/controllers/service_api/test_wraps.py b/api/tests/unit_tests/controllers/service_api/test_wraps.py
index c6809375b4b..a6d502c6613 100644
--- a/api/tests/unit_tests/controllers/service_api/test_wraps.py
+++ b/api/tests/unit_tests/controllers/service_api/test_wraps.py
@@ -23,7 +23,7 @@ from controllers.service_api.wraps import (
validate_app_token,
validate_dataset_token,
)
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from models import Account, Tenant, TenantAccountJoin
from models.account import TenantAccountRole
from models.dataset import Dataset, RateLimitLog
@@ -348,7 +348,7 @@ class TestCloudEditionBillingResourceCheck:
# Act
with (
app.test_request_context("/", method="GET"),
- patch("controllers.service_api.wraps.dify_config.BILLING_ENABLED", True),
+ patch("controllers.service_api.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
):
result = add_segment()
@@ -378,7 +378,7 @@ class TestCloudEditionBillingResourceCheck:
with (
app.test_request_context("/", method="GET"),
- patch("controllers.service_api.wraps.dify_config.BILLING_ENABLED", True),
+ patch("controllers.service_api.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
pytest.raises(ServiceUnavailable) as exc_info,
):
upload_document()
@@ -408,7 +408,7 @@ class TestCloudEditionBillingResourceCheck:
with (
app.test_request_context("/", method="GET"),
- patch("controllers.service_api.wraps.dify_config.BILLING_ENABLED", True),
+ patch("controllers.service_api.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
):
result = upload_document()
diff --git a/api/tests/unit_tests/controllers/web/test_feature.py b/api/tests/unit_tests/controllers/web/test_feature.py
index 6833d78238d..68b8e72be71 100644
--- a/api/tests/unit_tests/controllers/web/test_feature.py
+++ b/api/tests/unit_tests/controllers/web/test_feature.py
@@ -2,34 +2,48 @@
from __future__ import annotations
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock, create_autospec
+import pytest
from flask import Flask
+from pytest_mock import MockerFixture
from controllers.web.feature import SystemFeatureApi
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from services.entities.feature_entities import SystemFeatureModel
+from services.feature_query_service import FeatureQueryService
+
+
+def _install_feature_queries(mocker: MockerFixture) -> MagicMock:
+ feature_queries = create_autospec(FeatureQueryService, instance=True, spec_set=True)
+ application_services = mocker.patch("controllers.web.feature.application_services")
+ application_services.return_value.feature_queries = feature_queries
+ return feature_queries
class TestSystemFeatureApi:
- @patch("controllers.web.feature.FeatureService.get_system_features")
- def test_returns_system_features(self, mock_features: MagicMock, app: Flask) -> None:
- system_features = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY)
- mock_features.return_value = system_features
+ @pytest.mark.parametrize("deployment_edition", list(DeploymentEdition))
+ def test_returns_system_features(
+ self,
+ deployment_edition: DeploymentEdition,
+ app: Flask,
+ mocker: MockerFixture,
+ ) -> None:
+ system_features = SystemFeatureModel(deployment_edition=deployment_edition)
+ feature_queries = _install_feature_queries(mocker)
+ feature_queries.get_system_features.return_value = system_features
with app.test_request_context("/system-features"):
result = SystemFeatureApi().get()
- assert result == system_features.model_dump()
+ assert result == system_features.model_dump(mode="json")
+ assert result["deployment_edition"] == deployment_edition.value
assert result["sso_enforced_for_signin_protocol"] is None
assert result["webapp_auth"]["sso_config"]["protocol"] is None
- mock_features.assert_called_once()
+ feature_queries.get_system_features.assert_called_once_with()
- @patch("controllers.web.feature.FeatureService.get_system_features")
- def test_unauthenticated_access(self, mock_features: MagicMock, app: Flask) -> None:
+ def test_unauthenticated_access(self) -> None:
"""SystemFeatureApi is unauthenticated by design — no WebApiResource decorator."""
- mock_features.return_value = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY)
-
# Verify it's a bare Resource, not WebApiResource
from flask_restx import Resource
diff --git a/api/tests/unit_tests/controllers/web/test_site.py b/api/tests/unit_tests/controllers/web/test_site.py
index ba422ac6977..011d6b6a51e 100644
--- a/api/tests/unit_tests/controllers/web/test_site.py
+++ b/api/tests/unit_tests/controllers/web/test_site.py
@@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch
from configs import dify_config
from controllers.web import site as site_module
+from enums import DeploymentEdition
from extensions.storage.storage_type import StorageType
from models.model import AppMode, IconType, Site
from services.entities.feature_entities import FeatureModel
@@ -48,7 +49,7 @@ def test_build_site_icon_url_uses_s3_presigned_url() -> None:
)
with (
- patch.object(dify_config, "EDITION", "CLOUD"),
+ patch.object(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch.object(dify_config, "STORAGE_TYPE", StorageType.S3),
patch.object(site_module, "db") as mock_db,
patch.object(site_module, "FileService") as mock_file_service,
@@ -76,7 +77,7 @@ def test_build_site_icon_url_keeps_preview_url_for_self_hosted_s3() -> None:
)
with (
- patch.object(dify_config, "EDITION", "SELF_HOSTED"),
+ patch.object(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch.object(dify_config, "STORAGE_TYPE", StorageType.S3),
patch.object(site_module, "FileService") as mock_file_service,
patch.object(site_module, "build_icon_url", return_value="https://api.example.com/files/icon/file-preview"),
@@ -94,7 +95,7 @@ def test_build_site_icon_url_keeps_preview_url_for_non_s3_storage() -> None:
)
with (
- patch.object(dify_config, "EDITION", "CLOUD"),
+ patch.object(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch.object(dify_config, "STORAGE_TYPE", StorageType.LOCAL),
patch.object(site_module, "FileService") as mock_file_service,
patch.object(site_module, "build_icon_url", return_value="https://api.example.com/files/icon/file-preview"),
diff --git a/api/tests/unit_tests/controllers/web/test_web_forgot_password.py b/api/tests/unit_tests/controllers/web/test_web_forgot_password.py
index 315b9c2b281..f2588ec3450 100644
--- a/api/tests/unit_tests/controllers/web/test_web_forgot_password.py
+++ b/api/tests/unit_tests/controllers/web/test_web_forgot_password.py
@@ -14,7 +14,7 @@ from controllers.web.forgot_password import (
ForgotPasswordResetApi,
ForgotPasswordSendEmailApi,
)
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from models.account import Account
from models.engine import db
from services.entities.feature_entities import SystemFeatureModel
@@ -39,8 +39,7 @@ def _patch_wraps():
)
with (
patch("controllers.console.wraps.db") as mock_db,
- patch("controllers.console.wraps.dify_config.ENTERPRISE_ENABLED", True),
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=wraps_features),
):
yield
diff --git a/api/tests/unit_tests/controllers/web/test_web_login.py b/api/tests/unit_tests/controllers/web/test_web_login.py
index d05f135a712..b4dae61bd91 100644
--- a/api/tests/unit_tests/controllers/web/test_web_login.py
+++ b/api/tests/unit_tests/controllers/web/test_web_login.py
@@ -13,7 +13,7 @@ from werkzeug.exceptions import Unauthorized
import services.errors.account
from controllers.console import wraps as console_wraps
from controllers.web.login import EmailCodeLoginApi, EmailCodeLoginSendEmailApi, LoginApi, LoginStatusApi, LogoutApi
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from models.model import DifySetup
from services.entities.auth_entities import LoginFailureReason
@@ -45,8 +45,8 @@ def _patch_wraps(
sqlite_session: Session,
):
wraps_features = SimpleNamespace(enable_email_password_login=True)
- console_dify = SimpleNamespace(ENTERPRISE_ENABLED=True, DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
- web_dify = SimpleNamespace(ENTERPRISE_ENABLED=True)
+ console_dify = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
+ web_dify = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
sqlite_session.add(DifySetup(version="test"))
sqlite_session.commit()
console_wraps._is_setup_completed.reset_success()
diff --git a/api/tests/unit_tests/core/app/apps/test_base_app_generator.py b/api/tests/unit_tests/core/app/apps/test_base_app_generator.py
index 0b2bd95f436..04724d701ef 100644
--- a/api/tests/unit_tests/core/app/apps/test_base_app_generator.py
+++ b/api/tests/unit_tests/core/app/apps/test_base_app_generator.py
@@ -6,6 +6,7 @@ import pytest
from sqlalchemy import inspect
from core.app.apps.base_app_generator import BaseAppGenerator
+from graphon.enums import BuiltinNodeTypes
from graphon.variables.input_entities import VariableEntity, VariableEntityType
from models import Workflow, WorkflowRun
@@ -561,7 +562,6 @@ class TestBaseAppGeneratorExtras:
def test_get_draft_var_saver_factory_debugger(self):
from core.app.entities.app_invoke_entities import InvokeFrom
- from graphon.enums import BuiltinNodeTypes
from models import Account
base_app_generator = BaseAppGenerator()
diff --git a/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py b/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py
index 919c98662cb..ecb2985dc4b 100644
--- a/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py
+++ b/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py
@@ -65,6 +65,7 @@ from core.indexing_runner import (
)
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
from core.rag.models.document import ChildDocument, Document
+from enums import DeploymentEdition
from graphon.model_runtime.entities.model_entities import ModelType
from libs.datetime_utils import naive_utc_now
from models.dataset import Dataset, DatasetProcessRule, DocumentSegment
@@ -1791,7 +1792,7 @@ class TestIndexingRunnerEstimate:
# Create too many extract settings
with patch("core.indexing_runner.dify_config") as mock_config:
- mock_config.BILLING_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.BATCH_UPLOAD_LIMIT = 10
extract_settings = [MagicMock() for _ in range(15)]
@@ -1832,7 +1833,7 @@ class TestIndexingRunnerEstimate:
patch("core.indexing_runner.storage") as mock_storage,
patch("core.indexing_runner.dify_config") as mock_config,
):
- mock_config.BILLING_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = runner.indexing_estimate(
tenant_id=tenant_id,
diff --git a/api/tests/unit_tests/core/test_provider_manager.py b/api/tests/unit_tests/core/test_provider_manager.py
index 9a4b3a39411..bf805e761ab 100644
--- a/api/tests/unit_tests/core/test_provider_manager.py
+++ b/api/tests/unit_tests/core/test_provider_manager.py
@@ -19,6 +19,7 @@ from core.hosting_configuration import HostingProvider, TrialHostingQuota
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.entities.plugin_daemon import PluginModelProviderDeclaration
from core.provider_manager import ProviderConfigurationCacheSource, ProviderManager
+from enums import DeploymentEdition
from graphon.model_runtime.entities.common_entities import I18nObject
from graphon.model_runtime.entities.model_entities import ModelType
from graphon.model_runtime.entities.provider_entities import ConfigurateMethod
@@ -301,7 +302,7 @@ def test_to_system_configuration_uses_owned_session_for_cloud_credit_pools() ->
paid_pool = SimpleNamespace(quota_used=0, quota_limit=0)
with (
- patch.object(provider_manager_module.dify_config, "EDITION", "CLOUD"),
+ patch.object(provider_manager_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch(
"core.provider_manager.ext_hosting_provider.hosting_configuration.provider_map",
{provider_entity.provider: _build_hosting_provider()},
diff --git a/api/tests/unit_tests/core/tools/test_mcp_tool.py b/api/tests/unit_tests/core/tools/test_mcp_tool.py
index be0ce20ad60..0c6ab408a37 100644
--- a/api/tests/unit_tests/core/tools/test_mcp_tool.py
+++ b/api/tests/unit_tests/core/tools/test_mcp_tool.py
@@ -22,6 +22,7 @@ from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolEntity, ToolIdentity, ToolInvokeMessage, ToolProviderType
from core.tools.errors import ToolInvokeError
from core.tools.mcp_tool.tool import MCPTool
+from enums import DeploymentEdition
def _build_mcp_tool(*, with_output_schema: bool = True) -> MCPTool:
@@ -262,12 +263,12 @@ def test_invoke_remote_mcp_tool_fails_closed_when_user_id_missing():
tool = _build_forwarding_tool()
with patch("core.tools.mcp_tool.tool.dify_config") as cfg:
- cfg.ENTERPRISE_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
with pytest.raises(ToolInvokeError, match="no end-user context"):
tool.invoke_remote_mcp_tool({}, user_id=None, app_id=None)
-def test_invoke_skips_forwarding_when_enterprise_disabled():
+def test_invoke_skips_forwarding_outside_enterprise_edition():
"""Non-enterprise deployments treat the DB selector as a no-op: a stale
`identity_mode="idp_token"` row must NOT raise (fail-closed) AND must
NOT call the enterprise inner API. The runtime falls through to the
@@ -275,7 +276,7 @@ def test_invoke_skips_forwarding_when_enterprise_disabled():
tool = _build_forwarding_tool()
with patch("core.tools.mcp_tool.tool.dify_config") as cfg:
- cfg.ENTERPRISE_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
# The fail-closed branch must NOT fire (no enterprise → no forwarding).
# The function will still try the legacy DB-load path; we patch that
# to keep the test unit-scoped.
diff --git a/api/tests/unit_tests/enums/test_quota_type.py b/api/tests/unit_tests/enums/test_quota_type.py
index f256ff3b4e1..7a7d569d59b 100644
--- a/api/tests/unit_tests/enums/test_quota_type.py
+++ b/api/tests/unit_tests/enums/test_quota_type.py
@@ -4,7 +4,7 @@ from unittest.mock import patch
import pytest
-from enums.quota_type import QuotaType
+from enums import DeploymentEdition, QuotaType
from services.quota_service import QuotaCharge, QuotaService, unlimited
@@ -21,19 +21,19 @@ class TestQuotaType:
class TestQuotaService:
- def test_reserve_billing_disabled(self):
+ def test_reserve_outside_cloud_edition(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService"),
):
- mock_cfg.BILLING_ENABLED = False
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
charge = QuotaService.reserve(QuotaType.TRIGGER, "t1")
assert charge.success is True
assert charge.charge_id is None
def test_reserve_zero_amount_raises(self):
with patch("services.quota_service.dify_config") as mock_cfg:
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with pytest.raises(ValueError, match="greater than 0"):
QuotaService.reserve(QuotaType.TRIGGER, "t1", amount=0)
@@ -42,7 +42,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_bs.quota_reserve.return_value = {"reservation_id": "rid-1", "available": 99}
charge = QuotaService.reserve(QuotaType.TRIGGER, "t1", amount=1)
@@ -61,7 +61,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_bs.quota_reserve.return_value = {}
with pytest.raises(QuotaExceededError):
@@ -74,7 +74,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_bs.quota_reserve.side_effect = QuotaExceededError(feature="trigger", tenant_id="t1", required=1)
with pytest.raises(QuotaExceededError):
@@ -85,7 +85,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_bs.quota_reserve.side_effect = RuntimeError("network")
charge = QuotaService.reserve(QuotaType.TRIGGER, "t1")
@@ -97,7 +97,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_bs.quota_reserve.return_value = {"reservation_id": "rid-c"}
mock_bs.quota_commit.return_value = {}
@@ -105,14 +105,14 @@ class TestQuotaService:
assert charge.success is True
mock_bs.quota_commit.assert_called_once()
- def test_check_billing_disabled(self):
+ def test_check_outside_cloud_edition(self):
with patch("services.quota_service.dify_config") as mock_cfg:
- mock_cfg.BILLING_ENABLED = False
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
assert QuotaService.check(QuotaType.TRIGGER, "t1") is True
def test_check_zero_amount_raises(self):
with patch("services.quota_service.dify_config") as mock_cfg:
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with pytest.raises(ValueError, match="greater than 0"):
QuotaService.check(QuotaType.TRIGGER, "t1", amount=0)
@@ -121,7 +121,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch.object(QuotaService, "get_remaining", return_value=100),
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
assert QuotaService.check(QuotaType.TRIGGER, "t1", amount=50) is True
def test_check_insufficient_quota(self):
@@ -129,7 +129,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch.object(QuotaService, "get_remaining", return_value=5),
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
assert QuotaService.check(QuotaType.TRIGGER, "t1", amount=10) is False
def test_check_unlimited_quota(self):
@@ -137,7 +137,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch.object(QuotaService, "get_remaining", return_value=-1),
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
assert QuotaService.check(QuotaType.TRIGGER, "t1", amount=999) is True
def test_check_exception_returns_true(self):
@@ -145,15 +145,15 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch.object(QuotaService, "get_remaining", side_effect=RuntimeError),
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
assert QuotaService.check(QuotaType.TRIGGER, "t1") is True
- def test_release_billing_disabled(self):
+ def test_release_outside_cloud_edition(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
- mock_cfg.BILLING_ENABLED = False
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
QuotaService.release(QuotaType.TRIGGER, "rid-1", "t1", "trigger_event")
mock_bs.quota_release.assert_not_called()
@@ -162,7 +162,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
QuotaService.release(QuotaType.TRIGGER, "", "t1", "trigger_event")
mock_bs.quota_release.assert_not_called()
@@ -171,7 +171,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_bs.quota_release.return_value = {}
QuotaService.release(QuotaType.TRIGGER, "rid-1", "t1", "trigger_event")
mock_bs.quota_release.assert_called_once_with(
@@ -183,7 +183,7 @@ class TestQuotaService:
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
- mock_cfg.BILLING_ENABLED = True
+ mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_bs.quota_release.side_effect = RuntimeError("fail")
QuotaService.release(QuotaType.TRIGGER, "rid-1", "t1", "trigger_event")
diff --git a/api/tests/unit_tests/extensions/test_celery_ssl.py b/api/tests/unit_tests/extensions/test_celery_ssl.py
index 784f6b6417f..ad8192ecea4 100644
--- a/api/tests/unit_tests/extensions/test_celery_ssl.py
+++ b/api/tests/unit_tests/extensions/test_celery_ssl.py
@@ -3,6 +3,8 @@
import ssl
from unittest.mock import MagicMock, patch
+from enums import DeploymentEdition
+
class TestCelerySSLConfiguration:
"""Test suite for Celery SSL configuration."""
@@ -226,7 +228,7 @@ class TestCelerySSLConfiguration:
mock_config.TRIGGER_PROVIDER_REFRESH_INTERVAL = 15
mock_config.ENABLE_API_TOKEN_LAST_USED_UPDATE_TASK = False
mock_config.API_TOKEN_LAST_USED_UPDATE_INTERVAL = 30
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
mock_config.ENTERPRISE_TELEMETRY_ENABLED = False
with patch("extensions.ext_celery.dify_config", mock_config):
diff --git a/api/tests/unit_tests/extensions/test_ext_application_services.py b/api/tests/unit_tests/extensions/test_ext_application_services.py
index eabae51034c..1474ae5fac4 100644
--- a/api/tests/unit_tests/extensions/test_ext_application_services.py
+++ b/api/tests/unit_tests/extensions/test_ext_application_services.py
@@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch
import pytest
from sqlalchemy.orm import Session, sessionmaker
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from extensions.ext_application_services import build_application_services
from extensions.ext_redis import RedisClientWrapper
diff --git a/api/tests/unit_tests/libs/test_workspace_member_helper.py b/api/tests/unit_tests/libs/test_workspace_member_helper.py
index d7f14b878a9..d35a83e6430 100644
--- a/api/tests/unit_tests/libs/test_workspace_member_helper.py
+++ b/api/tests/unit_tests/libs/test_workspace_member_helper.py
@@ -12,6 +12,7 @@ from sqlalchemy import event
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden
+from enums import DeploymentEdition
from libs import oauth_bearer
from libs.oauth_bearer import AuthContext, Scope, SubjectType, TokenType, require_workspace_member
from models.account import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole
@@ -46,7 +47,7 @@ def database(sqlite_session: Session, monkeypatch: pytest.MonkeyPatch) -> Iterat
@pytest.fixture
def community_edition(monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr(oauth_bearer.dify_config, "ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(oauth_bearer.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
def _ctx(
@@ -98,8 +99,8 @@ def _account(account_id: uuid.UUID, *, status: AccountStatus = AccountStatus.ACT
return account
-def test_skips_when_enterprise_enabled(database: Database, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr(oauth_bearer.dify_config, "ENTERPRISE_ENABLED", True)
+def test_skips_for_enterprise_edition(database: Database, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(oauth_bearer.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
before = len(database.statements)
require_workspace_member(_ctx(), "tenant-1")
diff --git a/api/tests/unit_tests/libs/test_workspace_permission.py b/api/tests/unit_tests/libs/test_workspace_permission.py
index e0c425e7c1e..2d3523e1fad 100644
--- a/api/tests/unit_tests/libs/test_workspace_permission.py
+++ b/api/tests/unit_tests/libs/test_workspace_permission.py
@@ -4,6 +4,7 @@ from unittest.mock import Mock, patch
import pytest
from werkzeug.exceptions import Forbidden
+from enums import DeploymentEdition
from libs.workspace_permission import (
check_workspace_member_invite_permission,
check_workspace_owner_transfer_permission,
@@ -17,7 +18,7 @@ class TestWorkspacePermissionHelper:
@patch("libs.workspace_permission.EnterpriseService")
def test_community_edition_allows_invite(self, mock_enterprise_service, mock_config):
"""Community edition should always allow invitations without calling any service."""
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
# Should not raise
check_workspace_member_invite_permission("test-workspace-id")
@@ -29,7 +30,7 @@ class TestWorkspacePermissionHelper:
@patch("libs.workspace_permission.FeatureService")
def test_community_edition_allows_transfer(self, mock_feature_service, mock_config):
"""Community edition should check billing plan but not call enterprise service."""
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
mock_features = Mock()
mock_features.is_allow_transfer_workspace = True
mock_feature_service.get_features.return_value = mock_features
@@ -43,7 +44,7 @@ class TestWorkspacePermissionHelper:
@patch("libs.workspace_permission.dify_config")
def test_enterprise_blocks_invite_when_disabled(self, mock_config, mock_enterprise_service):
"""Enterprise edition should block invitations when workspace policy is False."""
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_permission = Mock()
mock_permission.allow_member_invite = False
@@ -58,7 +59,7 @@ class TestWorkspacePermissionHelper:
@patch("libs.workspace_permission.dify_config")
def test_enterprise_allows_invite_when_enabled(self, mock_config, mock_enterprise_service):
"""Enterprise edition should allow invitations when workspace policy is True."""
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_permission = Mock()
mock_permission.allow_member_invite = True
@@ -74,7 +75,7 @@ class TestWorkspacePermissionHelper:
@patch("libs.workspace_permission.FeatureService")
def test_billing_plan_blocks_transfer(self, mock_feature_service, mock_config, mock_enterprise_service):
"""SANDBOX billing plan should block owner transfer before checking enterprise policy."""
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_features = Mock()
mock_features.is_allow_transfer_workspace = False # SANDBOX plan
mock_feature_service.get_features.return_value = mock_features
@@ -90,7 +91,7 @@ class TestWorkspacePermissionHelper:
@patch("libs.workspace_permission.FeatureService")
def test_enterprise_blocks_transfer_when_disabled(self, mock_feature_service, mock_config, mock_enterprise_service):
"""Enterprise edition should block transfer when workspace policy is False."""
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_features = Mock()
mock_features.is_allow_transfer_workspace = True # Billing plan allows
mock_feature_service.get_features.return_value = mock_features
@@ -111,7 +112,7 @@ class TestWorkspacePermissionHelper:
self, mock_feature_service, mock_config, mock_enterprise_service
):
"""Enterprise edition should allow transfer when both billing and workspace policy allow."""
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_features = Mock()
mock_features.is_allow_transfer_workspace = True # Billing plan allows
mock_feature_service.get_features.return_value = mock_features
@@ -131,7 +132,7 @@ class TestWorkspacePermissionHelper:
self, mock_config, mock_enterprise_service, caplog: pytest.LogCaptureFixture
):
"""On enterprise service error, should fail-open (allow) and log error."""
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
# Simulate enterprise service error
mock_enterprise_service.WorkspacePermissionService.get_permission.side_effect = Exception("Service unavailable")
diff --git a/api/tests/unit_tests/services/controller_api.py b/api/tests/unit_tests/services/controller_api.py
index b106f34964a..4dd0019cfc0 100644
--- a/api/tests/unit_tests/services/controller_api.py
+++ b/api/tests/unit_tests/services/controller_api.py
@@ -97,6 +97,7 @@ from controllers.console.datasets.external import (
ExternalApiTemplateListApi,
)
from controllers.console.datasets.hit_testing import HitTestingApi
+from enums import DeploymentEdition
from models.account import Account, AccountStatus, TenantAccountRole
from models.dataset import Dataset, DatasetPermissionEnum
@@ -829,7 +830,7 @@ class TestExternalDatasetApi:
with (
patch("controllers.console.wraps.current_account_with_tenant") as mock_get_user,
- patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
+ patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("libs.login.check_csrf_token", return_value=None),
):
mock_tenant_id = "tenant-123"
diff --git a/api/tests/unit_tests/services/dataset_service_test_helpers.py b/api/tests/unit_tests/services/dataset_service_test_helpers.py
index d08913f537d..99157c63cab 100644
--- a/api/tests/unit_tests/services/dataset_service_test_helpers.py
+++ b/api/tests/unit_tests/services/dataset_service_test_helpers.py
@@ -19,7 +19,7 @@ from core.rag.entities import PreProcessingRule, Rule, Segmentation
from core.rag.index_processor.constant.built_in_field import BuiltInField
from core.rag.index_processor.constant.index_type import IndexStructureType
from core.rag.retrieval.retrieval_methods import RetrievalMethod
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType
from models import Account, TenantAccountRole
from models.dataset import (
diff --git a/api/tests/unit_tests/services/enterprise/test_account_deletion_sync.py b/api/tests/unit_tests/services/enterprise/test_account_deletion_sync.py
index 0624b5ac778..fd631dc91ec 100644
--- a/api/tests/unit_tests/services/enterprise/test_account_deletion_sync.py
+++ b/api/tests/unit_tests/services/enterprise/test_account_deletion_sync.py
@@ -13,6 +13,7 @@ import pytest
from redis import RedisError
from sqlalchemy.orm import Session
+from enums import DeploymentEdition
from models.account import TenantAccountJoin
from services.enterprise.account_deletion_sync import (
_queue_task,
@@ -48,21 +49,21 @@ class TestSyncWorkspaceMemberRemoval:
mock_queue.return_value = True
yield mock_queue
- def test_sync_workspace_member_removal_enterprise_enabled(self, mock_queue_task):
+ def test_sync_workspace_member_removal_enterprise_edition(self, mock_queue_task):
workspace_id = str(uuid4())
member_id = str(uuid4())
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_workspace_member_removal(workspace_id=workspace_id, member_id=member_id, source="removed")
assert result is True
mock_queue_task.assert_called_once_with(workspace_id=workspace_id, member_id=member_id, source="removed")
- def test_sync_workspace_member_removal_enterprise_disabled(self, mock_queue_task):
+ def test_sync_workspace_member_removal_non_enterprise_edition(self, mock_queue_task):
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = sync_workspace_member_removal(
workspace_id=str(uuid4()), member_id=str(uuid4()), source="test_source"
@@ -75,7 +76,7 @@ class TestSyncWorkspaceMemberRemoval:
mock_queue_task.return_value = False
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_workspace_member_removal(
workspace_id=str(uuid4()), member_id=str(uuid4()), source="test_source"
@@ -92,9 +93,9 @@ class TestSyncAccountDeletion:
mock_queue.return_value = True
yield mock_queue
- def test_sync_account_deletion_enterprise_disabled(self, mock_queue_task, sqlite_session: Session) -> None:
+ def test_sync_account_deletion_non_enterprise_edition(self, mock_queue_task, sqlite_session: Session) -> None:
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = sync_account_deletion(account_id=str(uuid4()), source="account_deleted", session=sqlite_session)
@@ -111,7 +112,7 @@ class TestSyncAccountDeletion:
sqlite_session.commit()
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_account_deletion(account_id=account_id, source="account_deleted", session=sqlite_session)
@@ -123,7 +124,7 @@ class TestSyncAccountDeletion:
def test_sync_account_deletion_no_workspaces(self, sqlite_session: Session, mock_queue_task) -> None:
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_account_deletion(account_id=str(uuid4()), source="account_deleted", session=sqlite_session)
@@ -146,7 +147,7 @@ class TestSyncAccountDeletion:
mock_queue_task.side_effect = queue_side_effect
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_account_deletion(account_id=account_id, source="account_deleted", session=sqlite_session)
@@ -164,7 +165,7 @@ class TestSyncAccountDeletion:
mock_queue_task.return_value = False
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_account_deletion(account_id=account_id, source="account_deleted", session=sqlite_session)
diff --git a/api/tests/unit_tests/services/enterprise/test_enterprise_service.py b/api/tests/unit_tests/services/enterprise/test_enterprise_service.py
index 51556e1cee5..7ec5d1c01e6 100644
--- a/api/tests/unit_tests/services/enterprise/test_enterprise_service.py
+++ b/api/tests/unit_tests/services/enterprise/test_enterprise_service.py
@@ -10,6 +10,8 @@ from unittest.mock import patch
import pytest
+from enums import DeploymentEdition
+from services.enterprise.base import MCPIdentityRefreshError, MCPNoRefreshTokenError, MCPTokenError
from services.enterprise.enterprise_service import (
INVALID_LICENSE_CACHE_TTL,
LICENSE_STATUS_CACHE_KEY,
@@ -20,6 +22,8 @@ from services.enterprise.enterprise_service import (
WorkspacePermission,
try_join_default_workspace,
)
+from services.entities.feature_entities import LicenseStatus
+from services.errors.enterprise import EnterpriseAPIError, EnterpriseAPIForbiddenError, EnterpriseAPIUnauthorizedError
MODULE = "services.enterprise.enterprise_service"
@@ -265,12 +269,12 @@ class TestJoinDefaultWorkspace:
class TestTryJoinDefaultWorkspace:
- def test_try_join_default_workspace_enterprise_disabled_noop(self):
+ def test_try_join_default_workspace_non_enterprise_edition_noop(self):
with (
patch("services.enterprise.enterprise_service.dify_config") as mock_config,
patch("services.enterprise.enterprise_service.EnterpriseService.join_default_workspace") as mock_join,
):
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
try_join_default_workspace("11111111-1111-1111-1111-111111111111")
@@ -283,7 +287,7 @@ class TestTryJoinDefaultWorkspace:
patch("services.enterprise.enterprise_service.dify_config") as mock_config,
patch("services.enterprise.enterprise_service.EnterpriseService.join_default_workspace") as mock_join,
):
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_join.return_value = DefaultWorkspaceJoinResult(
workspace_id="22222222-2222-2222-2222-222222222222",
joined=True,
@@ -302,7 +306,7 @@ class TestTryJoinDefaultWorkspace:
patch("services.enterprise.enterprise_service.dify_config") as mock_config,
patch("services.enterprise.enterprise_service.EnterpriseService.join_default_workspace") as mock_join,
):
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_join.return_value = DefaultWorkspaceJoinResult(
workspace_id="",
joined=False,
@@ -321,7 +325,7 @@ class TestTryJoinDefaultWorkspace:
patch("services.enterprise.enterprise_service.dify_config") as mock_config,
patch("services.enterprise.enterprise_service.EnterpriseService.join_default_workspace") as mock_join,
):
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_join.side_effect = Exception("network failure")
# Should not raise
@@ -331,7 +335,7 @@ class TestTryJoinDefaultWorkspace:
def test_try_join_default_workspace_invalid_account_id_soft_fails(self):
with patch("services.enterprise.enterprise_service.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
# Should not raise even though UUID parsing fails inside join_default_workspace
try_join_default_workspace("not-a-uuid")
@@ -347,21 +351,19 @@ _EE_SVC = "services.enterprise.enterprise_service"
class TestGetCachedLicenseStatus:
"""Tests for EnterpriseService.get_cached_license_status."""
- def test_returns_none_when_enterprise_disabled(self):
+ def test_returns_none_outside_enterprise_edition(self):
with patch(f"{_EE_SVC}.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
assert EnterpriseService.get_cached_license_status() is None
def test_cache_hit_returns_license_status_enum(self):
- from services.entities.feature_entities import LicenseStatus
-
with (
patch(f"{_EE_SVC}.dify_config") as mock_config,
patch(f"{_EE_SVC}.redis_client") as mock_redis,
patch.object(EnterpriseService, "get_info") as mock_get_info,
):
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = b"active"
result = EnterpriseService.get_cached_license_status()
@@ -371,14 +373,12 @@ class TestGetCachedLicenseStatus:
mock_get_info.assert_not_called()
def test_cache_miss_fetches_api_and_caches_valid_status(self):
- from services.entities.feature_entities import LicenseStatus
-
with (
patch(f"{_EE_SVC}.dify_config") as mock_config,
patch(f"{_EE_SVC}.redis_client") as mock_redis,
patch.object(EnterpriseService, "get_info") as mock_get_info,
):
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = None
mock_get_info.return_value = {"License": {"status": "active"}}
@@ -390,14 +390,12 @@ class TestGetCachedLicenseStatus:
)
def test_cache_miss_fetches_api_and_caches_invalid_status_with_short_ttl(self):
- from services.entities.feature_entities import LicenseStatus
-
with (
patch(f"{_EE_SVC}.dify_config") as mock_config,
patch(f"{_EE_SVC}.redis_client") as mock_redis,
patch.object(EnterpriseService, "get_info") as mock_get_info,
):
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = None
mock_get_info.return_value = {"License": {"status": "expired"}}
@@ -409,14 +407,12 @@ class TestGetCachedLicenseStatus:
)
def test_redis_read_failure_falls_through_to_api(self):
- from services.entities.feature_entities import LicenseStatus
-
with (
patch(f"{_EE_SVC}.dify_config") as mock_config,
patch(f"{_EE_SVC}.redis_client") as mock_redis,
patch.object(EnterpriseService, "get_info") as mock_get_info,
):
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.side_effect = ConnectionError("redis down")
mock_get_info.return_value = {"License": {"status": "active"}}
@@ -426,14 +422,12 @@ class TestGetCachedLicenseStatus:
mock_get_info.assert_called_once()
def test_redis_write_failure_still_returns_status(self):
- from services.entities.feature_entities import LicenseStatus
-
with (
patch(f"{_EE_SVC}.dify_config") as mock_config,
patch(f"{_EE_SVC}.redis_client") as mock_redis,
patch.object(EnterpriseService, "get_info") as mock_get_info,
):
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = None
mock_redis.setex.side_effect = ConnectionError("redis down")
mock_get_info.return_value = {"License": {"status": "expiring"}}
@@ -448,7 +442,7 @@ class TestGetCachedLicenseStatus:
patch(f"{_EE_SVC}.redis_client") as mock_redis,
patch.object(EnterpriseService, "get_info") as mock_get_info,
):
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = None
mock_get_info.side_effect = Exception("network failure")
@@ -460,7 +454,7 @@ class TestGetCachedLicenseStatus:
patch(f"{_EE_SVC}.redis_client") as mock_redis,
patch.object(EnterpriseService, "get_info") as mock_get_info,
):
- mock_config.ENTERPRISE_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = None
mock_get_info.return_value = {} # no "License" key
@@ -519,18 +513,12 @@ class TestIssueMCPToken:
assert body["app_id"] == "app-uuid"
def test_401_maps_to_identity_refresh_error(self):
- from services.enterprise.base import MCPIdentityRefreshError
- from services.errors.enterprise import EnterpriseAPIUnauthorizedError
-
with patch(f"{MODULE}.EnterpriseRequest") as req:
req.send_request.side_effect = EnterpriseAPIUnauthorizedError("refresh rejected by IdP")
with pytest.raises(MCPIdentityRefreshError, match="refresh rejected"):
self._call()
def test_428_maps_to_no_refresh_token_error(self):
- from services.enterprise.base import MCPNoRefreshTokenError
- from services.errors.enterprise import EnterpriseAPIError
-
with patch(f"{MODULE}.EnterpriseRequest") as req:
# 428 PreconditionRequired is what EE returns when there's no
# stored SSO refresh token for the user.
@@ -539,34 +527,24 @@ class TestIssueMCPToken:
self._call()
def test_403_maps_to_identity_refresh_error_for_license(self):
- from services.enterprise.base import MCPIdentityRefreshError
- from services.errors.enterprise import EnterpriseAPIForbiddenError
-
with patch(f"{MODULE}.EnterpriseRequest") as req:
req.send_request.side_effect = EnterpriseAPIForbiddenError("not licensed for MCP forwarding")
with pytest.raises(MCPIdentityRefreshError, match="not licensed"):
self._call()
def test_other_status_maps_to_generic_token_error(self):
- from services.enterprise.base import MCPTokenError
- from services.errors.enterprise import EnterpriseAPIError
-
with patch(f"{MODULE}.EnterpriseRequest") as req:
req.send_request.side_effect = EnterpriseAPIError("upstream 502", status_code=502)
with pytest.raises(MCPTokenError, match="status=502"):
self._call()
def test_malformed_response_shape_raises_token_error(self):
- from services.enterprise.base import MCPTokenError
-
with patch(f"{MODULE}.EnterpriseRequest") as req:
req.send_request.return_value = "not-a-dict"
with pytest.raises(MCPTokenError, match="invalid response shape"):
self._call()
def test_missing_token_field_raises_token_error(self):
- from services.enterprise.base import MCPTokenError
-
with patch(f"{MODULE}.EnterpriseRequest") as req:
req.send_request.return_value = {"expires_at": 1700000000} # no token
with pytest.raises(MCPTokenError, match="missing or non-string token"):
@@ -583,8 +561,6 @@ class TestIssueMCPToken:
def test_bool_expires_at_is_rejected(self):
"""bool is a subclass of int — must NOT be accepted as expires_at."""
- from services.enterprise.base import MCPTokenError
-
with patch(f"{MODULE}.EnterpriseRequest") as req:
req.send_request.return_value = {"token": "t", "expires_at": True}
with pytest.raises(MCPTokenError, match="non-numeric expires_at"):
diff --git a/api/tests/unit_tests/services/openapi/test_mint_policy.py b/api/tests/unit_tests/services/openapi/test_mint_policy.py
index 7409a064a97..80425725f39 100644
--- a/api/tests/unit_tests/services/openapi/test_mint_policy.py
+++ b/api/tests/unit_tests/services/openapi/test_mint_policy.py
@@ -10,6 +10,7 @@ from __future__ import annotations
import pytest
+from enums import DeploymentEdition
from libs.oauth_bearer import MINTABLE_PROFILES, Scope, SubjectType
from services.openapi.mint_policy import MintPolicyViolation, validate_mint_policy
@@ -84,7 +85,7 @@ def test_license_required_decorator_skips_on_ce():
return "ok"
with patch("services.openapi.license_gate.dify_config") as cfg:
- cfg.ENTERPRISE_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
assert view() == "ok"
@@ -103,7 +104,7 @@ def test_license_required_decorator_403_on_invalid_ee_license():
patch("services.openapi.license_gate.dify_config") as cfg,
patch("services.openapi.license_gate._is_license_valid", return_value=False),
):
- cfg.ENTERPRISE_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
with pytest.raises(Forbidden) as exc:
view()
assert "license_required" in exc.value.description
@@ -122,5 +123,5 @@ def test_license_required_decorator_passes_on_valid_ee_license():
patch("services.openapi.license_gate.dify_config") as cfg,
patch("services.openapi.license_gate._is_license_valid", return_value=True),
):
- cfg.ENTERPRISE_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
assert view() == "ok"
diff --git a/api/tests/unit_tests/services/plugin/test_plugin_service.py b/api/tests/unit_tests/services/plugin/test_plugin_service.py
index c82787dc640..b0b727c3466 100644
--- a/api/tests/unit_tests/services/plugin/test_plugin_service.py
+++ b/api/tests/unit_tests/services/plugin/test_plugin_service.py
@@ -18,6 +18,7 @@ from core.plugin.entities.plugin_daemon import (
PluginModelProviderEntity,
)
from core.provider_manager import ProviderConfigurationCacheSource, ProviderManager
+from enums import DeploymentEdition
from graphon.model_runtime.entities.common_entities import I18nObject
from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity
from models.provider import Provider, ProviderCredential, ProviderType, TenantPreferredModelProvider
@@ -1426,7 +1427,7 @@ class TestPluginModelProviderCacheInvalidation:
patch(f"{MODULE}.PluginService.invalidate_plugin_model_providers_cache") as invalidate_cache,
patch("core.provider_manager.ProviderManager.invalidate_configurations_cache") as invalidate_configurations,
):
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
installer = installer_cls.return_value
installer.list_plugins.return_value = [plugin]
installer.uninstall.return_value = True
diff --git a/api/tests/unit_tests/services/plugin/test_plugin_service_installation.py b/api/tests/unit_tests/services/plugin/test_plugin_service_installation.py
index 3878d7a788d..a98a249457e 100644
--- a/api/tests/unit_tests/services/plugin/test_plugin_service_installation.py
+++ b/api/tests/unit_tests/services/plugin/test_plugin_service_installation.py
@@ -20,6 +20,7 @@ from sqlalchemy.orm import Session
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.entities.plugin_daemon import PluginVerification
from core.plugin.plugin_service import PluginService
+from enums import DeploymentEdition
from models import ProviderType
from models.engine import db
from models.provider import Provider, ProviderCredential, TenantPreferredModelProvider
@@ -516,7 +517,7 @@ class TestUninstall:
installer.uninstall.return_value = True
with patch("core.plugin.plugin_service.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = PluginService.uninstall(tenant_id, "install-1")
assert result is True
@@ -579,7 +580,7 @@ class TestUninstall:
installer.uninstall.return_value = True
with patch("core.plugin.plugin_service.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = PluginService.uninstall(tenant_id, "install-1", preserve_credentials=True)
assert result is True
@@ -609,7 +610,7 @@ class TestUninstall:
installer.uninstall.return_value = False
with patch("core.plugin.plugin_service.dify_config") as mock_config:
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = PluginService.uninstall(tenant_id, "install-1")
assert result is False
diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py
index 15c0a99989b..895bef70c4d 100644
--- a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py
+++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py
@@ -10,7 +10,12 @@ from sqlalchemy.orm import Session, sessionmaker
from core.app.entities.app_invoke_entities import InvokeFrom
from core.rag.index_processor.constant.index_type import IndexStructureType
-from graphon.enums import WorkflowNodeExecutionStatus
+from graphon.enums import (
+ BuiltinNodeTypes,
+ ErrorStrategy,
+ WorkflowNodeExecutionMetadataKey,
+ WorkflowNodeExecutionStatus,
+)
from graphon.graph_events import NodeRunFailedEvent
from graphon.node_events.base import NodeRunResult
from models import Account, Tenant
@@ -407,7 +412,6 @@ def test_get_default_block_config_returns_config_for_valid_type(
fake_node_class.get_default_config.return_value = {"type": "start", "config": {}}
# Use a simpler approach: test with a known valid node type
- from graphon.enums import BuiltinNodeTypes
mocker.patch(
"services.rag_pipeline.rag_pipeline.get_node_type_classes_mapping",
@@ -769,7 +773,6 @@ def test_run_datasource_node_preview_online_document(
def test_handle_node_run_result_success(
mocker: MockerFixture, rag_pipeline_service: RagPipelineServiceTestContext
) -> None:
- from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus
from graphon.graph_events import NodeRunSucceededEvent
from graphon.node_events.base import NodeRunResult
@@ -1072,7 +1075,6 @@ def test_get_default_block_configs_success(rag_pipeline_service: RagPipelineServ
def test_get_default_block_config_success(rag_pipeline_service: RagPipelineServiceTestContext) -> None:
- from graphon.enums import BuiltinNodeTypes
result = rag_pipeline_service.service.get_default_block_config(BuiltinNodeTypes.LLM)
assert result is not None
@@ -1094,7 +1096,6 @@ def test_publish_workflow_raises_when_draft_workflow_missing(
def test_get_default_block_config_returns_none_when_mapped_type_missing(
mocker: MockerFixture, rag_pipeline_service: RagPipelineServiceTestContext
) -> None:
- from graphon.enums import BuiltinNodeTypes
mocker.patch("services.rag_pipeline.rag_pipeline.get_node_type_classes_mapping", return_value={})
@@ -1104,7 +1105,6 @@ def test_get_default_block_config_returns_none_when_mapped_type_missing(
def test_get_default_block_config_injects_http_request_filter(
mocker: MockerFixture, rag_pipeline_service: RagPipelineServiceTestContext
) -> None:
- from graphon.enums import BuiltinNodeTypes
fake_node_cls = mocker.Mock()
fake_node_cls.get_default_config.return_value = {"type": "http-request"}
@@ -1347,7 +1347,6 @@ def test_handle_node_run_result_default_value_strategy(
) -> None:
from datetime import datetime
- from graphon.enums import BuiltinNodeTypes, ErrorStrategy, WorkflowNodeExecutionStatus
from graphon.graph_events import NodeRunFailedEvent
from graphon.node_events.base import NodeRunResult
@@ -1531,7 +1530,6 @@ def test_set_datasource_variables_raises_when_node_id_missing(
def test_get_default_block_configs_skips_empty_configs(
mocker: MockerFixture, rag_pipeline_service: RagPipelineServiceTestContext
) -> None:
- from graphon.enums import BuiltinNodeTypes
http_node = mocker.Mock()
http_node.get_default_config.return_value = {"type": "http-request"}
@@ -1977,7 +1975,6 @@ def test_publish_workflow_skips_dataset_update_for_non_knowledge_nodes(
def test_get_default_block_config_returns_none_when_default_empty(
mocker: MockerFixture, rag_pipeline_service: RagPipelineServiceTestContext
) -> None:
- from graphon.enums import BuiltinNodeTypes
node_cls = mocker.Mock()
node_cls.get_default_config.return_value = None
diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_task_proxy.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_task_proxy.py
index 281de58abad..e9f5bcab1d2 100644
--- a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_task_proxy.py
+++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_task_proxy.py
@@ -4,6 +4,7 @@ from unittest.mock import Mock
import pytest
from pytest_mock import MockerFixture
+from enums import CloudPlan
from services.rag_pipeline.rag_pipeline_task_proxy import RagPipelineTaskProxy
@@ -52,8 +53,6 @@ def test_dispatch_billing_sandbox_uses_default_tenant_queue(mocker: MockerFixtur
upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1")
send_mock = mocker.patch.object(proxy, "_send_to_default_tenant_queue")
- from enums.cloud_plan import CloudPlan
-
features = SimpleNamespace(
billing=SimpleNamespace(enabled=True, subscription=SimpleNamespace(plan=CloudPlan.SANDBOX))
)
@@ -69,8 +68,6 @@ def test_dispatch_billing_non_sandbox_uses_priority_tenant_queue(mocker: MockerF
upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1")
send_mock = mocker.patch.object(proxy, "_send_to_priority_tenant_queue")
- from enums.cloud_plan import CloudPlan
-
features = SimpleNamespace(
billing=SimpleNamespace(enabled=True, subscription=SimpleNamespace(plan=CloudPlan.PROFESSIONAL))
)
diff --git a/api/tests/unit_tests/services/retention/test_messages_clean_policy.py b/api/tests/unit_tests/services/retention/test_messages_clean_policy.py
index 79c079c683a..6179cead0cc 100644
--- a/api/tests/unit_tests/services/retention/test_messages_clean_policy.py
+++ b/api/tests/unit_tests/services/retention/test_messages_clean_policy.py
@@ -1,6 +1,7 @@
import datetime
from unittest.mock import MagicMock, patch
+from enums import DeploymentEdition
from services.retention.conversation.messages_clean_policy import (
BillingDisabledPolicy,
BillingSandboxPolicy,
@@ -115,19 +116,19 @@ class TestBillingSandboxPolicy:
class TestCreateMessageCleanPolicy:
- def test_billing_disabled_returns_disabled_policy(self):
+ def test_non_cloud_edition_returns_disabled_policy(self):
with patch(f"{MODULE}.dify_config") as cfg:
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
policy = create_message_clean_policy()
assert isinstance(policy, BillingDisabledPolicy)
- def test_billing_enabled_returns_sandbox_policy(self):
+ def test_cloud_edition_returns_sandbox_policy(self):
with (
patch(f"{MODULE}.dify_config") as cfg,
patch(f"{MODULE}.BillingService") as bs,
):
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
bs.get_expired_subscription_cleanup_whitelist.return_value = ["wl1"]
bs.get_plan_bulk_with_cache = MagicMock()
policy = create_message_clean_policy(graceful_period_days=30)
diff --git a/api/tests/unit_tests/services/retention/workflow_run/test_clear_free_plan_expired_workflow_run_logs.py b/api/tests/unit_tests/services/retention/workflow_run/test_clear_free_plan_expired_workflow_run_logs.py
index 524dcd5952f..e8cc8a0c2a7 100644
--- a/api/tests/unit_tests/services/retention/workflow_run/test_clear_free_plan_expired_workflow_run_logs.py
+++ b/api/tests/unit_tests/services/retention/workflow_run/test_clear_free_plan_expired_workflow_run_logs.py
@@ -8,6 +8,7 @@ from unittest.mock import MagicMock, patch
import pytest
from sqlalchemy.orm import Session
+from enums import CloudPlan, DeploymentEdition
from repositories.api_workflow_run_repository import WorkflowRunCleanupRef
from services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs import WorkflowRunCleanup
@@ -29,7 +30,7 @@ def mock_repo():
def cleanup(mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
yield WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
@@ -42,7 +43,7 @@ class TestWorkflowRunCleanupInit:
def test_only_start_from_raises(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError, match="both set or both omitted"):
WorkflowRunCleanup(
days=30,
@@ -54,7 +55,7 @@ class TestWorkflowRunCleanupInit:
def test_only_end_before_raises(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError, match="both set or both omitted"):
WorkflowRunCleanup(
days=30,
@@ -66,7 +67,7 @@ class TestWorkflowRunCleanupInit:
def test_end_before_not_greater_than_start_raises(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError, match="end_before must be greater than start_from"):
WorkflowRunCleanup(
days=30,
@@ -80,7 +81,7 @@ class TestWorkflowRunCleanupInit:
dt = datetime.datetime(2024, 1, 1)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError):
WorkflowRunCleanup(
days=30,
@@ -93,21 +94,21 @@ class TestWorkflowRunCleanupInit:
def test_zero_batch_size_raises(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError, match="batch_size must be greater than 0"):
WorkflowRunCleanup(days=30, batch_size=0, workflow_run_repo=mock_repo)
def test_negative_batch_size_raises(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError):
WorkflowRunCleanup(days=30, batch_size=-1, workflow_run_repo=mock_repo)
def test_valid_window_init(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 7
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
start = datetime.datetime(2024, 1, 1)
end = datetime.datetime(2024, 6, 1)
c = WorkflowRunCleanup(
@@ -123,7 +124,7 @@ class TestWorkflowRunCleanupInit:
def test_default_task_label_is_custom(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
assert c._metrics._base_attributes["task_label"] == "custom"
@@ -216,17 +217,17 @@ class TestIsWithinGracePeriod:
class TestGetCleanupWhitelist:
- def test_billing_disabled_returns_empty(self, cleanup):
+ def test_non_cloud_edition_returns_empty(self, cleanup):
cleanup._cleanup_whitelist = None
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = cleanup._get_cleanup_whitelist()
assert result == set()
- def test_billing_enabled_fetches_whitelist(self, mock_repo):
+ def test_cloud_edition_fetches_whitelist(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
with patch(
"services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService"
@@ -243,7 +244,7 @@ class TestGetCleanupWhitelist:
def test_billing_service_error_returns_empty(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
with patch(
"services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService"
@@ -259,27 +260,25 @@ class TestGetCleanupWhitelist:
class TestFilterFreeTenants:
- def test_billing_disabled_all_tenants_free(self, cleanup):
+ def test_non_cloud_edition_treats_all_tenants_as_free(self, cleanup):
result = cleanup._filter_free_tenants(["t1", "t2"])
assert result == {"t1", "t2"}
def test_empty_tenants_returns_empty(self, cleanup):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
result = cleanup._filter_free_tenants([])
assert result == set()
def test_whitelisted_tenant_excluded(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = {"t1"}
with patch(
"services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService"
) as bs:
- from enums.cloud_plan import CloudPlan
-
bs.get_plan_bulk_with_cache.return_value = {
"t1": {"plan": CloudPlan.SANDBOX, "expiration_date": -1},
"t2": {"plan": CloudPlan.SANDBOX, "expiration_date": -1},
@@ -291,7 +290,7 @@ class TestFilterFreeTenants:
def test_paid_tenant_excluded(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = set()
with patch(
@@ -306,7 +305,7 @@ class TestFilterFreeTenants:
def test_missing_billing_info_treats_as_non_free(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = set()
with patch(
@@ -319,7 +318,7 @@ class TestFilterFreeTenants:
def test_billing_bulk_error_treats_as_non_free(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = True
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = set()
with patch(
@@ -336,17 +335,17 @@ class TestFilterFreeTenants:
class TestRunDeleteMode:
- def _make_cleanup(self, mock_repo, billing_enabled=False):
+ def _make_cleanup(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = billing_enabled
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
return WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
def test_no_rows_stops_immediately(self, mock_repo):
mock_repo.get_cleanup_refs_batch_by_time_range.return_value = []
c = self._make_cleanup(mock_repo)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c.run()
mock_repo.delete_runs_with_related_by_ids.assert_not_called()
@@ -354,10 +353,10 @@ class TestRunDeleteMode:
ref = make_ref("t1")
mock_repo.get_cleanup_refs_batch_by_time_range.side_effect = [[ref], []]
c = self._make_cleanup(mock_repo)
- # billing disabled -> all free; but let's override _filter_free_tenants to return empty
+ # Override the non-Cloud default to exercise the no-deletion path.
c._filter_free_tenants = MagicMock(return_value=set())
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c.run()
mock_repo.delete_runs_with_related_by_ids.assert_not_called()
@@ -375,7 +374,7 @@ class TestRunDeleteMode:
}
c = self._make_cleanup(mock_repo)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.time.sleep"):
c.run()
mock_repo.delete_runs_with_related_by_ids.assert_called_once()
@@ -386,7 +385,7 @@ class TestRunDeleteMode:
mock_repo.delete_runs_with_related_by_ids.side_effect = RuntimeError("db error")
c = self._make_cleanup(mock_repo)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(RuntimeError):
c.run()
@@ -394,7 +393,7 @@ class TestRunDeleteMode:
mock_repo.get_cleanup_refs_batch_by_time_range.return_value = []
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c = WorkflowRunCleanup(
days=30,
batch_size=10,
@@ -414,7 +413,7 @@ class TestRunDryRunMode:
def _make_dry_cleanup(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
return WorkflowRunCleanup(
days=30,
batch_size=10,
@@ -436,7 +435,7 @@ class TestRunDryRunMode:
}
c = self._make_dry_cleanup(mock_repo)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c.run()
mock_repo.delete_runs_with_related_by_ids.assert_not_called()
mock_repo.count_runs_with_related_by_ids.assert_called_once()
@@ -445,7 +444,7 @@ class TestRunDryRunMode:
mock_repo.get_cleanup_refs_batch_by_time_range.return_value = []
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c = WorkflowRunCleanup(
days=30,
batch_size=10,
@@ -462,7 +461,7 @@ class TestRunDryRunMode:
c = self._make_dry_cleanup(mock_repo)
c._filter_free_tenants = MagicMock(return_value=set())
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
- cfg.BILLING_ENABLED = False
+ cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c.run()
mock_repo.count_runs_with_related_by_ids.assert_not_called()
diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py
index 3bd227f1e3e..1e01951a49c 100644
--- a/api/tests/unit_tests/services/test_account_service.py
+++ b/api/tests/unit_tests/services/test_account_service.py
@@ -11,6 +11,7 @@ from sqlalchemy.engine.interfaces import DBAPICursor, ExecutionContext
from sqlalchemy.orm import Session, sessionmaker
from configs import dify_config
+from enums import DeploymentEdition
from models.account import (
Account,
AccountStatus,
@@ -292,7 +293,7 @@ class TestAccountService:
# Setup mocks
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = True
- with patch("services.account_service.dify_config.BILLING_ENABLED", True):
+ with patch("services.account_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD):
with pytest.raises(AccountRegisterError):
AccountService.create_account(
email="frozen@example.com",
@@ -926,7 +927,7 @@ class TestTenantService:
service_session.commit()
with (
- patch("services.account_service.dify_config.BILLING_ENABLED", False),
+ patch("services.account_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("services.enterprise.account_deletion_sync.sync_workspace_member_removal") as mock_sync,
):
mock_sync.return_value = True
@@ -980,7 +981,7 @@ class TestTenantService:
service_session.commit()
with (
- patch("services.account_service.dify_config.BILLING_ENABLED", False),
+ patch("services.account_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("services.enterprise.account_deletion_sync.sync_workspace_member_removal") as mock_sync,
):
mock_sync.return_value = True
@@ -1025,7 +1026,7 @@ class TestTenantService:
service_session.commit()
with (
- patch("services.account_service.dify_config.BILLING_ENABLED", False),
+ patch("services.account_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("services.enterprise.account_deletion_sync.sync_workspace_member_removal") as mock_sync,
):
mock_sync.return_value = True
@@ -1519,14 +1520,14 @@ class TestRegisterService:
# ==================== Registration Tests ====================
- def test_create_account_and_tenant_calls_default_workspace_join_when_enterprise_enabled(
+ def test_create_account_and_tenant_calls_default_workspace_join_for_enterprise_edition(
self,
sqlite_session: Session,
mock_external_service_dependencies: _MockDependencies,
monkeypatch: pytest.MonkeyPatch,
) -> None:
- """Enterprise-only side effect should be invoked when ENTERPRISE_ENABLED is True."""
- monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", True, raising=False)
+ """Enterprise-only side effect should be invoked for the ENTERPRISE edition."""
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE, raising=False)
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
@@ -1554,14 +1555,14 @@ class TestRegisterService:
mock_create_workspace.assert_called_once_with(account=mock_account, session=sqlite_session)
mock_join_default_workspace.assert_called_once_with(mock_account.id)
- def test_create_account_and_tenant_does_not_call_default_workspace_join_when_enterprise_disabled(
+ def test_create_account_and_tenant_skips_default_workspace_join_for_community_edition(
self,
sqlite_session: Session,
mock_external_service_dependencies: _MockDependencies,
monkeypatch: pytest.MonkeyPatch,
) -> None:
- """Enterprise-only side effect should not be invoked when ENTERPRISE_ENABLED is False."""
- monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", False, raising=False)
+ """Enterprise-only side effect should not be invoked for the COMMUNITY edition."""
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY, raising=False)
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
@@ -1597,7 +1598,7 @@ class TestRegisterService:
"""Default workspace join should still be attempted when personal workspace creation fails."""
from services.errors.workspace import WorkSpaceNotAllowedCreateError
- monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", True, raising=False)
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE, raising=False)
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
@@ -1668,14 +1669,14 @@ class TestRegisterService:
)
mock_create_owner_tenant.assert_called_once_with(mock_account, session=sqlite_session)
- def test_register_calls_default_workspace_join_when_enterprise_enabled(
+ def test_register_calls_default_workspace_join_for_enterprise_edition(
self,
sqlite_session: Session,
mock_external_service_dependencies: _MockDependencies,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Enterprise-only side effect should be invoked after successful register commit."""
- monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", True, raising=False)
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE, raising=False)
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
@@ -1702,14 +1703,14 @@ class TestRegisterService:
assert result == mock_account
mock_join_default_workspace.assert_called_once_with(mock_account.id)
- def test_register_does_not_call_default_workspace_join_when_enterprise_disabled(
+ def test_register_skips_default_workspace_join_for_community_edition(
self,
sqlite_session: Session,
mock_external_service_dependencies: _MockDependencies,
monkeypatch: pytest.MonkeyPatch,
) -> None:
- """Enterprise-only side effect should not be invoked when ENTERPRISE_ENABLED is False."""
- monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", False, raising=False)
+ """Enterprise-only side effect should not be invoked for the COMMUNITY edition."""
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY, raising=False)
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
@@ -1744,7 +1745,7 @@ class TestRegisterService:
"""Default workspace join should run even when personal workspace creation raises."""
from services.errors.workspace import WorkSpaceNotAllowedCreateError
- monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", True, raising=False)
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE, raising=False)
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True
mock_external_service_dependencies[
@@ -1784,7 +1785,7 @@ class TestRegisterService:
"""Default workspace join should run before propagating workspace-limit registration failure."""
from services.errors.workspace import WorkspacesLimitExceededError
- monkeypatch.setattr(dify_config, "ENTERPRISE_ENABLED", True, raising=False)
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE, raising=False)
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True
mock_external_service_dependencies[
diff --git a/api/tests/unit_tests/services/test_app_generate_service.py b/api/tests/unit_tests/services/test_app_generate_service.py
index 507977f287f..ff8eceed664 100644
--- a/api/tests/unit_tests/services/test_app_generate_service.py
+++ b/api/tests/unit_tests/services/test_app_generate_service.py
@@ -24,7 +24,7 @@ from pytest_mock import MockerFixture
import services.app_generate_service as ags_module
from core.app.entities.app_invoke_entities import InvokeFrom
-from enums.quota_type import QuotaType
+from enums import DeploymentEdition, QuotaType
from models.model import AppMode
from services.app_generate_service import AppGenerateService
from services.errors.app import WorkflowIdFormatError, WorkflowNotFoundError
@@ -217,7 +217,7 @@ class TestGenerate:
@pytest.fixture(autouse=True)
def _common(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
mocker.patch("services.app_generate_service.RateLimit", _DummyRateLimit)
# Prevent AppExecutionParams.new from touching real models via isinstance
mocker.patch(
@@ -486,8 +486,8 @@ class TestGenerateBilling:
_noop_rate_limit_context,
)
- def test_billing_enabled_consumes_quota(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", True)
+ def test_cloud_edition_consumes_quota(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
+ monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
quota_charge = MagicMock()
reserve_mock = mocker.patch(
"services.app_generate_service.QuotaService.reserve",
@@ -519,7 +519,7 @@ class TestGenerateBilling:
from services.errors.app import QuotaExceededError
from services.errors.llm import InvokeRateLimitError
- monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
mocker.patch(
"services.app_generate_service.QuotaService.reserve",
side_effect=QuotaExceededError(feature="workflow", tenant_id="t", required=1),
@@ -536,7 +536,7 @@ class TestGenerateBilling:
)
def test_exception_refunds_quota_and_exits_rate_limit(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
quota_charge = MagicMock()
mocker.patch(
"services.app_generate_service.QuotaService.reserve",
@@ -566,7 +566,7 @@ class TestGenerateBilling:
self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch
):
"""For non-streaming (blocking) calls, rate_limit.exit should be called in finally."""
- monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
exit_calls: list[str] = []
@@ -596,7 +596,7 @@ class TestGenerateBilling:
assert exit_calls == ["dummy-request-id"]
def test_blocking_failure_exits_rate_limit_once(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
quota_charge = MagicMock()
mocker.patch(
"services.app_generate_service.QuotaService.reserve",
@@ -628,7 +628,7 @@ class TestGenerateBilling:
assert exit_calls == ["dummy-request-id"]
def test_streaming_failure_exits_rate_limit_once(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
quota_charge = MagicMock()
mocker.patch(
"services.app_generate_service.QuotaService.reserve",
diff --git a/api/tests/unit_tests/services/test_app_service.py b/api/tests/unit_tests/services/test_app_service.py
index beb8d63a040..171c9a94a00 100644
--- a/api/tests/unit_tests/services/test_app_service.py
+++ b/api/tests/unit_tests/services/test_app_service.py
@@ -10,6 +10,7 @@ import pytest
from sqlalchemy import event
from sqlalchemy.orm import Session
+from enums import DeploymentEdition
from graphon.model_runtime.entities.model_entities import ModelType
from models import Account, Tenant
from models.account import TenantAccountJoin, TenantAccountRole
@@ -106,7 +107,7 @@ class TestCreateAppTransactionBoundary:
"services.app_service.FeatureService.get_system_features",
return_value=SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
),
- patch("services.app_service.dify_config.BILLING_ENABLED", False),
+ patch("services.app_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
app = AppService().create_app(
account.current_tenant_id,
@@ -139,7 +140,7 @@ class TestCreateAppTransactionBoundary:
"services.app_service.FeatureService.get_system_features",
return_value=SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
),
- patch("services.app_service.dify_config.BILLING_ENABLED", False),
+ patch("services.app_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
app = AppService().create_app(
account.current_tenant_id,
diff --git a/api/tests/unit_tests/services/test_archive_workflow_run_logs.py b/api/tests/unit_tests/services/test_archive_workflow_run_logs.py
index a21f1de769c..5177167ed0e 100644
--- a/api/tests/unit_tests/services/test_archive_workflow_run_logs.py
+++ b/api/tests/unit_tests/services/test_archive_workflow_run_logs.py
@@ -9,6 +9,8 @@ This module contains tests for:
from datetime import datetime
from unittest.mock import MagicMock, patch
+from enums import DeploymentEdition
+
class TestWorkflowRunArchiver:
"""Tests for the WorkflowRunArchiver class."""
@@ -19,7 +21,7 @@ class TestWorkflowRunArchiver:
"""Test archiver can be initialized with various options."""
from services.retention.workflow_run.archive_paid_plan_workflow_run import WorkflowRunArchiver
- mock_config.BILLING_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
archiver = WorkflowRunArchiver(
days=90,
diff --git a/api/tests/unit_tests/services/test_batch_indexing_base.py b/api/tests/unit_tests/services/test_batch_indexing_base.py
index 21aa6fcce95..17420379427 100644
--- a/api/tests/unit_tests/services/test_batch_indexing_base.py
+++ b/api/tests/unit_tests/services/test_batch_indexing_base.py
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
import pytest
from core.entities.document_task import DocumentTask
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from services.document_indexing_proxy.batch_indexing_base import BatchDocumentIndexingProxy
# ---------------------------------------------------------------------------
diff --git a/api/tests/unit_tests/services/test_billing_service.py b/api/tests/unit_tests/services/test_billing_service.py
index d3b01e71a04..b6d3a5c150e 100644
--- a/api/tests/unit_tests/services/test_billing_service.py
+++ b/api/tests/unit_tests/services/test_billing_service.py
@@ -23,7 +23,7 @@ import pytest
from sqlalchemy.orm import Session
from werkzeug.exceptions import InternalServerError
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from services.billing_service import BillingService
diff --git a/api/tests/unit_tests/services/test_clear_free_plan_expired_workflow_run_logs.py b/api/tests/unit_tests/services/test_clear_free_plan_expired_workflow_run_logs.py
index 60488beb248..c50696a5f98 100644
--- a/api/tests/unit_tests/services/test_clear_free_plan_expired_workflow_run_logs.py
+++ b/api/tests/unit_tests/services/test_clear_free_plan_expired_workflow_run_logs.py
@@ -3,6 +3,7 @@ from typing import Any
import pytest
+from enums import DeploymentEdition
from repositories.api_workflow_run_repository import WorkflowRunCleanupRef
from services.billing_service import SubscriptionPlan
from services.retention.workflow_run import clear_free_plan_expired_workflow_run_logs as cleanup_module
@@ -126,10 +127,10 @@ def create_cleanup(
return WorkflowRunCleanup(workflow_run_repo=repo, **kwargs)
-def test_filter_free_tenants_billing_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
+def test_filter_free_tenants_outside_cloud_edition(monkeypatch: pytest.MonkeyPatch) -> None:
cleanup = create_cleanup(monkeypatch, repo=FakeRepo([]), days=30, batch_size=10)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
def fail_bulk(_: list[str]) -> dict[str, SubscriptionPlan]:
raise RuntimeError("should not call")
@@ -145,7 +146,7 @@ def test_filter_free_tenants_billing_disabled(monkeypatch: pytest.MonkeyPatch) -
def test_filter_free_tenants_bulk_mixed(monkeypatch: pytest.MonkeyPatch) -> None:
cleanup = create_cleanup(monkeypatch, repo=FakeRepo([]), days=30, batch_size=10)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(
cleanup_module.BillingService,
"get_plan_bulk_with_cache",
@@ -165,7 +166,7 @@ def test_filter_free_tenants_bulk_mixed(monkeypatch: pytest.MonkeyPatch) -> None
def test_filter_free_tenants_respects_grace_period(monkeypatch: pytest.MonkeyPatch) -> None:
cleanup = create_cleanup(monkeypatch, repo=FakeRepo([]), days=30, batch_size=10, grace_period_days=45)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
now = datetime.datetime.now(datetime.UTC)
within_grace_ts = int((now - datetime.timedelta(days=10)).timestamp())
outside_grace_ts = int((now - datetime.timedelta(days=90)).timestamp())
@@ -192,7 +193,7 @@ def test_filter_free_tenants_skips_cleanup_whitelist(monkeypatch: pytest.MonkeyP
whitelist={"tenant_whitelist"},
)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(
cleanup_module.BillingService,
"get_plan_bulk_with_cache",
@@ -213,7 +214,7 @@ def test_filter_free_tenants_skips_cleanup_whitelist(monkeypatch: pytest.MonkeyP
def test_filter_free_tenants_bulk_failure(monkeypatch: pytest.MonkeyPatch) -> None:
cleanup = create_cleanup(monkeypatch, repo=FakeRepo([]), days=30, batch_size=10)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(
cleanup_module.BillingService,
"get_plan_bulk_with_cache",
@@ -237,7 +238,7 @@ def test_run_deletes_only_free_tenants(monkeypatch: pytest.MonkeyPatch) -> None:
)
cleanup = create_cleanup(monkeypatch, repo=repo, days=30, batch_size=10)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(
cleanup_module.BillingService,
"get_plan_bulk_with_cache",
@@ -267,7 +268,7 @@ def test_run_filters_candidate_tenants_before_target_query(monkeypatch: pytest.M
)
cleanup = create_cleanup(monkeypatch, repo=repo, days=30, batch_size=10)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
billing_calls: list[list[str]] = []
def fake_bulk(tenant_ids: list[str]) -> dict[str, SubscriptionPlan]:
@@ -291,7 +292,7 @@ def test_run_skips_when_no_free_tenants(monkeypatch: pytest.MonkeyPatch) -> None
repo = FakeRepo(batches=[[make_ref("run-paid", "t_paid", cutoff)]])
cleanup = create_cleanup(monkeypatch, repo=repo, days=30, batch_size=10)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(
cleanup_module.BillingService,
"get_plan_bulk_with_cache",
@@ -309,7 +310,7 @@ def test_run_paid_only_records_skipped_metrics(monkeypatch: pytest.MonkeyPatch)
repo = FakeRepo(batches=[[make_ref("run-paid", "t_paid", cutoff)]])
cleanup = create_cleanup(monkeypatch, repo=repo, days=30, batch_size=10)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(
cleanup_module.BillingService,
"get_plan_bulk_with_cache",
@@ -341,7 +342,7 @@ def test_run_target_query_is_bounded_by_candidate_high_water(monkeypatch: pytest
)
cleanup = create_cleanup(monkeypatch, repo=repo, days=30, batch_size=2)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
cleanup.run()
@@ -371,7 +372,7 @@ def test_run_records_metrics_on_success(monkeypatch: pytest.MonkeyPatch) -> None
},
)
cleanup = create_cleanup(monkeypatch, repo=repo, days=30, batch_size=10)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
batch_calls: list[dict[str, object]] = []
completion_calls: list[dict[str, object]] = []
@@ -399,7 +400,7 @@ def test_run_records_failed_metrics(monkeypatch: pytest.MonkeyPatch) -> None:
cutoff = datetime.datetime.now()
repo = FailingRepo(batches=[[make_ref("run-free", "t_free", cutoff)]])
cleanup = create_cleanup(monkeypatch, repo=repo, days=30, batch_size=10)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
completion_calls: list[dict[str, object]] = []
monkeypatch.setattr(cleanup._metrics, "record_completion", lambda **kwargs: completion_calls.append(kwargs))
@@ -427,7 +428,7 @@ def test_run_dry_run_skips_deletions(monkeypatch: pytest.MonkeyPatch, capsys: py
)
cleanup = create_cleanup(monkeypatch, repo=repo, days=30, batch_size=10, dry_run=True)
- monkeypatch.setattr(cleanup_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(cleanup_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
cleanup.run()
diff --git a/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py b/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py
index 863d0b3aef6..d49cfa3bd35 100644
--- a/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py
+++ b/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py
@@ -11,7 +11,7 @@ from sqlalchemy import event
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from graphon.file import FileTransferMethod, FileType
from models.account import Tenant
from models.enums import (
@@ -471,7 +471,7 @@ def test_process_with_tenant_ids_filters_by_plan_and_logs_errors(
sqlite_session.commit()
_configure_process_boundaries(monkeypatch, sqlite_engine)
monkeypatch.setattr(service_module.click, "echo", MagicMock())
- monkeypatch.setattr(service_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
def fake_get_info(tenant_id: str) -> dict[str, dict[str, str]]:
if tenant_id == "tenant-sandbox":
@@ -524,7 +524,7 @@ def test_process_without_tenant_ids_batches_and_scales_interval(
monkeypatch.setattr(service_module.datetime, "datetime", FixedDateTime)
_configure_process_boundaries(monkeypatch, sqlite_engine)
monkeypatch.setattr(service_module.click, "echo", lambda *_args, **_kwargs: None)
- monkeypatch.setattr(service_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
process_tenant = MagicMock()
monkeypatch.setattr(ClearFreePlanTenantExpiredLogs, "process_tenant", process_tenant)
statements: list[str] = []
@@ -564,7 +564,7 @@ def test_process_with_tenant_ids_emits_progress_every_100(
sqlite_session.add_all([_create_tenant(tenant_id) for tenant_id in tenant_ids])
sqlite_session.commit()
_configure_process_boundaries(monkeypatch, sqlite_engine)
- monkeypatch.setattr(service_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
echo = MagicMock()
monkeypatch.setattr(service_module.click, "echo", echo)
monkeypatch.setattr(ClearFreePlanTenantExpiredLogs, "process_tenant", MagicMock())
@@ -598,7 +598,7 @@ def test_process_without_tenant_ids_all_intervals_too_many_uses_min_interval(
monkeypatch.setattr(service_module.datetime, "datetime", FixedDateTime)
_configure_process_boundaries(monkeypatch, sqlite_engine)
monkeypatch.setattr(service_module.click, "echo", lambda *_args, **_kwargs: None)
- monkeypatch.setattr(service_module.dify_config, "BILLING_ENABLED", False)
+ monkeypatch.setattr(service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
process_tenant = MagicMock()
monkeypatch.setattr(ClearFreePlanTenantExpiredLogs, "process_tenant", process_tenant)
statements: list[str] = []
diff --git a/api/tests/unit_tests/services/test_credit_pool_service.py b/api/tests/unit_tests/services/test_credit_pool_service.py
index f3b97e8bbd6..22023520e7f 100644
--- a/api/tests/unit_tests/services/test_credit_pool_service.py
+++ b/api/tests/unit_tests/services/test_credit_pool_service.py
@@ -10,6 +10,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from core.errors.error import QuotaExceededError
+from enums import DeploymentEdition
from models import TenantCreditPool
from models.enums import ProviderQuotaType
from services.credit_pool_service import (
@@ -45,7 +46,7 @@ def _make_redis_lock() -> MagicMock:
@pytest.fixture(autouse=True)
def _disable_billing_quota_by_default() -> Generator[None, None, None]:
- with patch("services.credit_pool_service.dify_config.BILLING_ENABLED", False):
+ with patch("services.credit_pool_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
yield
@@ -247,7 +248,7 @@ def test_deduct_credits_capped_uses_tenant_redis_lock_before_db_deduction(sqlite
def test_get_pool_uses_billing_quota_balance_when_enabled() -> None:
tenant_id = "tenant-1"
with (
- patch("services.credit_pool_service.dify_config.BILLING_ENABLED", True),
+ patch("services.credit_pool_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("services.billing_service.BillingService.quota_get_balance") as quota_get_balance,
):
quota_get_balance.return_value = {
@@ -275,7 +276,7 @@ def test_get_pool_uses_billing_quota_balance_when_enabled() -> None:
def test_check_and_deduct_credits_uses_billing_reserve_and_commit_when_enabled() -> None:
tenant_id = "tenant-1"
with (
- patch("services.credit_pool_service.dify_config.BILLING_ENABLED", True),
+ patch("services.credit_pool_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("services.billing_service.BillingService.quota_reserve") as quota_reserve,
patch("services.billing_service.BillingService.quota_commit") as quota_commit,
patch("services.billing_service.BillingService.quota_release") as quota_release,
@@ -310,7 +311,7 @@ def test_check_and_deduct_credits_uses_billing_reserve_and_commit_when_enabled()
def test_check_and_deduct_credits_raises_when_billing_reserve_is_insufficient() -> None:
with (
- patch("services.credit_pool_service.dify_config.BILLING_ENABLED", True),
+ patch("services.credit_pool_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("services.billing_service.BillingService.quota_reserve") as quota_reserve,
):
quota_reserve.return_value = {"reservation_id": "", "available": 1, "reserved": 0}
@@ -321,7 +322,7 @@ def test_check_and_deduct_credits_raises_when_billing_reserve_is_insufficient()
def test_check_and_deduct_credits_releases_billing_reservation_when_commit_fails() -> None:
with (
- patch("services.credit_pool_service.dify_config.BILLING_ENABLED", True),
+ patch("services.credit_pool_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("services.billing_service.BillingService.quota_reserve") as quota_reserve,
patch("services.billing_service.BillingService.quota_commit", side_effect=RuntimeError("commit failed")),
patch("services.billing_service.BillingService.quota_release") as quota_release,
@@ -343,7 +344,7 @@ def test_check_and_deduct_credits_logs_when_billing_release_fails(
caplog: pytest.LogCaptureFixture,
) -> None:
with (
- patch("services.credit_pool_service.dify_config.BILLING_ENABLED", True),
+ patch("services.credit_pool_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("services.billing_service.BillingService.quota_reserve") as quota_reserve,
patch("services.billing_service.BillingService.quota_commit", side_effect=RuntimeError("commit failed")),
patch(
@@ -369,7 +370,7 @@ def test_check_and_deduct_credits_logs_when_billing_release_fails(
def test_deduct_credits_capped_uses_billing_consume_capped_when_enabled() -> None:
tenant_id = "tenant-1"
with (
- patch("services.credit_pool_service.dify_config.BILLING_ENABLED", True),
+ patch("services.credit_pool_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("services.billing_service.BillingService.quota_consume_capped") as quota_consume_capped,
):
quota_consume_capped.return_value = {
diff --git a/api/tests/unit_tests/services/test_document_indexing_task_proxy.py b/api/tests/unit_tests/services/test_document_indexing_task_proxy.py
index 082bb7aa865..c012b132cd9 100644
--- a/api/tests/unit_tests/services/test_document_indexing_task_proxy.py
+++ b/api/tests/unit_tests/services/test_document_indexing_task_proxy.py
@@ -2,7 +2,7 @@ from unittest.mock import Mock, patch
from core.entities.document_task import DocumentTask
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from services.document_indexing_proxy.document_indexing_task_proxy import DocumentIndexingTaskProxy
diff --git a/api/tests/unit_tests/services/test_duplicate_document_indexing_task_proxy.py b/api/tests/unit_tests/services/test_duplicate_document_indexing_task_proxy.py
index e0370edec9e..2af411b0d76 100644
--- a/api/tests/unit_tests/services/test_duplicate_document_indexing_task_proxy.py
+++ b/api/tests/unit_tests/services/test_duplicate_document_indexing_task_proxy.py
@@ -2,7 +2,7 @@ from unittest.mock import Mock, patch
from core.entities.document_task import DocumentTask
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from services.document_indexing_proxy.duplicate_document_indexing_task_proxy import (
DuplicateDocumentIndexingTaskProxy,
)
diff --git a/api/tests/unit_tests/services/test_feature_entities.py b/api/tests/unit_tests/services/test_feature_entities.py
new file mode 100644
index 00000000000..393e427d751
--- /dev/null
+++ b/api/tests/unit_tests/services/test_feature_entities.py
@@ -0,0 +1,31 @@
+import pytest
+
+from services.entities.feature_entities import LicenseLimitationModel
+
+
+@pytest.mark.parametrize(
+ ("enabled", "size", "limit", "required", "expected"),
+ [
+ (False, 5, 10, 3, True),
+ (False, 5, 10, 10, True),
+ (True, 5, 0, 3, True),
+ (True, 5, 0, 100, True),
+ (True, 5, 10, 3, True),
+ (True, 5, 10, 5, True),
+ (True, 5, 10, 1, True),
+ (True, 8, 10, 3, False),
+ (True, 8, 10, 2, True),
+ (True, 8, 10, 1, True),
+ (True, 7, 10, 3, True),
+ ],
+)
+def test_license_limitation_availability(
+ enabled: bool,
+ size: int,
+ limit: int,
+ required: int,
+ expected: bool,
+) -> None:
+ limitation = LicenseLimitationModel(enabled=enabled, size=size, limit=limit)
+
+ assert limitation.is_available(required) is expected
diff --git a/api/tests/unit_tests/services/test_feature_query_service.py b/api/tests/unit_tests/services/test_feature_query_service.py
index 555b473db54..febbfbc6af3 100644
--- a/api/tests/unit_tests/services/test_feature_query_service.py
+++ b/api/tests/unit_tests/services/test_feature_query_service.py
@@ -2,9 +2,14 @@ from unittest.mock import create_autospec
import pytest
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from machinery.context import RequestContext
-from services.entities.feature_entities import FeatureModel, LicenseModel, LimitationModel, SystemFeatureModel
+from services.entities.feature_entities import (
+ FeatureModel,
+ LicenseModel,
+ SystemFeatureModel,
+ VectorSpaceLimitationModel,
+)
from services.feature_query_service import FeatureQueryGateway, FeatureQueryService
@@ -20,7 +25,7 @@ def _request_context(*, active_workspace_id: str | None = "workspace_123") -> Re
def test_workspace_queries_use_workspace_from_request_context() -> None:
gateway = create_autospec(FeatureQueryGateway, instance=True, spec_set=True)
features = FeatureModel()
- vector_space = LimitationModel(size=1, limit=5)
+ vector_space = VectorSpaceLimitationModel(size=1, limit=5)
gateway.get_workspace_features.return_value = features
gateway.get_vector_space.return_value = vector_space
service = FeatureQueryService(features=gateway, trial_models=(), app_dsl_version="0.7.0")
@@ -52,11 +57,7 @@ def test_deployment_queries_delegate_without_request_context() -> None:
def test_workspace_queries_require_active_workspace() -> None:
gateway = create_autospec(FeatureQueryGateway, instance=True, spec_set=True)
- service = FeatureQueryService(
- features=gateway,
- trial_models=(),
- app_dsl_version="0.7.0",
- )
+ service = FeatureQueryService(features=gateway, trial_models=(), app_dsl_version="0.7.0")
with pytest.raises(RuntimeError, match="did not resolve an active workspace"):
service.get_features(_request_context(active_workspace_id=None))
diff --git a/api/tests/unit_tests/services/test_feature_service_deployment_edition.py b/api/tests/unit_tests/services/test_feature_service_deployment_edition.py
index cba78b23eb5..8336b923ffb 100644
--- a/api/tests/unit_tests/services/test_feature_service_deployment_edition.py
+++ b/api/tests/unit_tests/services/test_feature_service_deployment_edition.py
@@ -1,7 +1,9 @@
+from unittest.mock import MagicMock
+
import pytest
from pydantic import ValidationError
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from services.entities.feature_entities import SystemFeatureModel
from services.feature_service import FeatureService
@@ -12,25 +14,29 @@ def test_system_feature_model_requires_deployment_edition() -> None:
@pytest.mark.parametrize(
- ("edition", "enterprise_enabled", "expected"),
+ "edition",
[
- ("SELF_HOSTED", False, DeploymentEdition.COMMUNITY),
- ("SELF_HOSTED", True, DeploymentEdition.ENTERPRISE),
- ("CLOUD", False, DeploymentEdition.CLOUD),
- ("CLOUD", True, DeploymentEdition.CLOUD),
+ DeploymentEdition.COMMUNITY,
+ DeploymentEdition.ENTERPRISE,
+ DeploymentEdition.CLOUD,
],
)
-def test_get_system_features_resolves_deployment_edition(
+def test_get_system_features_uses_configured_deployment_edition(
monkeypatch: pytest.MonkeyPatch,
- edition: str,
- enterprise_enabled: bool,
- expected: DeploymentEdition,
+ edition: DeploymentEdition,
) -> None:
- monkeypatch.setattr("services.feature_service.dify_config.EDITION", edition)
- monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", enterprise_enabled)
- monkeypatch.setattr("services.feature_service.FeatureService._fulfill_params_from_enterprise", lambda *_: None)
+ fulfill_from_enterprise = MagicMock()
+ monkeypatch.setattr("services.feature_service.dify_config.DEPLOYMENT_EDITION", edition)
+ monkeypatch.setattr(
+ "services.feature_service.FeatureService._fulfill_params_from_enterprise",
+ fulfill_from_enterprise,
+ )
result = FeatureService.get_system_features()
- assert result.deployment_edition is expected
- assert result.model_dump(mode="json")["deployment_edition"] == expected.value
+ assert result.deployment_edition is edition
+ assert result.model_dump(mode="json")["deployment_edition"] == edition.value
+ if edition is DeploymentEdition.ENTERPRISE:
+ fulfill_from_enterprise.assert_called_once_with(result)
+ else:
+ fulfill_from_enterprise.assert_not_called()
diff --git a/api/tests/unit_tests/services/test_feature_service_enable_app_deploy.py b/api/tests/unit_tests/services/test_feature_service_enable_app_deploy.py
index a22f6c53195..c9aa82a443f 100644
--- a/api/tests/unit_tests/services/test_feature_service_enable_app_deploy.py
+++ b/api/tests/unit_tests/services/test_feature_service_enable_app_deploy.py
@@ -1,6 +1,6 @@
import pytest
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from services import feature_service as feature_service_module
from services.entities.feature_entities import SystemFeatureModel
from services.feature_service import FeatureService
diff --git a/api/tests/unit_tests/services/test_feature_service_explore_banner.py b/api/tests/unit_tests/services/test_feature_service_explore_banner.py
index 35f74a96490..fb709d532c2 100644
--- a/api/tests/unit_tests/services/test_feature_service_explore_banner.py
+++ b/api/tests/unit_tests/services/test_feature_service_explore_banner.py
@@ -1,27 +1,26 @@
import pytest
+from enums import DeploymentEdition
from services import feature_service as feature_service_module
from services.feature_service import FeatureService
@pytest.mark.parametrize(
- ("edition", "enterprise_enabled", "configured", "expected"),
+ ("edition", "configured", "expected"),
[
- ("CLOUD", False, True, True),
- ("CLOUD", False, False, False),
- ("SELF_HOSTED", False, True, False),
- ("SELF_HOSTED", True, True, False),
+ (DeploymentEdition.CLOUD, True, True),
+ (DeploymentEdition.CLOUD, False, False),
+ (DeploymentEdition.COMMUNITY, True, False),
+ (DeploymentEdition.ENTERPRISE, True, False),
],
)
def test_get_system_features_enables_explore_banner_only_for_cloud(
monkeypatch: pytest.MonkeyPatch,
- edition: str,
- enterprise_enabled: bool,
+ edition: DeploymentEdition,
configured: bool,
expected: bool,
) -> None:
- monkeypatch.setattr(feature_service_module.dify_config, "EDITION", edition)
- monkeypatch.setattr(feature_service_module.dify_config, "ENTERPRISE_ENABLED", enterprise_enabled)
+ monkeypatch.setattr(feature_service_module.dify_config, "DEPLOYMENT_EDITION", edition)
monkeypatch.setattr(feature_service_module.dify_config, "ENABLE_EXPLORE_BANNER", configured)
monkeypatch.setattr(FeatureService, "_fulfill_params_from_enterprise", lambda *_: None)
diff --git a/api/tests/unit_tests/services/test_feature_service_gateway.py b/api/tests/unit_tests/services/test_feature_service_gateway.py
index 6494596b5a6..253df785743 100644
--- a/api/tests/unit_tests/services/test_feature_service_gateway.py
+++ b/api/tests/unit_tests/services/test_feature_service_gateway.py
@@ -1,10 +1,21 @@
from pytest_mock import MockerFixture
-from services.entities.feature_entities import FeatureModel
+from enums import DeploymentEdition
+from services.entities.feature_entities import FeatureModel, SystemFeatureModel
from services.feature_service import FeatureService
from services.feature_service_gateway import FeatureServiceGateway
+def test_public_system_features_delegate_to_existing_service(mocker: MockerFixture) -> None:
+ system_features = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY)
+ get_system_features = mocker.patch.object(FeatureService, "get_system_features", return_value=system_features)
+
+ result = FeatureServiceGateway().get_public_system_features()
+
+ assert result is system_features
+ get_system_features.assert_called_once_with()
+
+
def test_workspace_features_exclude_independently_queried_vector_space(mocker: MockerFixture) -> None:
features = FeatureModel(vector_space=None)
get_features = mocker.patch.object(FeatureService, "get_features", return_value=features)
diff --git a/api/tests/unit_tests/services/test_feature_service_human_input_email_delivery.py b/api/tests/unit_tests/services/test_feature_service_human_input_email_delivery.py
index d1c494683c2..497e331a448 100644
--- a/api/tests/unit_tests/services/test_feature_service_human_input_email_delivery.py
+++ b/api/tests/unit_tests/services/test_feature_service_human_input_email_delivery.py
@@ -2,7 +2,7 @@ from dataclasses import dataclass
import pytest
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from services import feature_service as feature_service_module
from services.entities.feature_entities import FeatureModel
from services.feature_service import FeatureService
@@ -11,8 +11,7 @@ from services.feature_service import FeatureService
@dataclass(frozen=True)
class HumanInputEmailDeliveryCase:
name: str
- enterprise_enabled: bool
- billing_enabled: bool
+ deployment_edition: DeploymentEdition
tenant_id: str | None
billing_feature_enabled: bool
plan: str
@@ -21,27 +20,24 @@ class HumanInputEmailDeliveryCase:
CASES = [
HumanInputEmailDeliveryCase(
- name="enterprise_enabled",
- enterprise_enabled=True,
- billing_enabled=True,
+ name="enterprise_edition",
+ deployment_edition=DeploymentEdition.ENTERPRISE,
tenant_id=None,
billing_feature_enabled=False,
plan=CloudPlan.SANDBOX,
expected=True,
),
HumanInputEmailDeliveryCase(
- name="billing_disabled",
- enterprise_enabled=False,
- billing_enabled=False,
+ name="community_edition",
+ deployment_edition=DeploymentEdition.COMMUNITY,
tenant_id=None,
billing_feature_enabled=False,
plan=CloudPlan.SANDBOX,
expected=True,
),
HumanInputEmailDeliveryCase(
- name="billing_enabled_requires_tenant",
- enterprise_enabled=False,
- billing_enabled=True,
+ name="cloud_edition_requires_tenant",
+ deployment_edition=DeploymentEdition.CLOUD,
tenant_id=None,
billing_feature_enabled=True,
plan=CloudPlan.PROFESSIONAL,
@@ -49,8 +45,7 @@ CASES = [
),
HumanInputEmailDeliveryCase(
name="billing_feature_off",
- enterprise_enabled=False,
- billing_enabled=True,
+ deployment_edition=DeploymentEdition.CLOUD,
tenant_id="tenant-1",
billing_feature_enabled=False,
plan=CloudPlan.PROFESSIONAL,
@@ -58,8 +53,7 @@ CASES = [
),
HumanInputEmailDeliveryCase(
name="professional_plan",
- enterprise_enabled=False,
- billing_enabled=True,
+ deployment_edition=DeploymentEdition.CLOUD,
tenant_id="tenant-1",
billing_feature_enabled=True,
plan=CloudPlan.PROFESSIONAL,
@@ -67,8 +61,7 @@ CASES = [
),
HumanInputEmailDeliveryCase(
name="team_plan",
- enterprise_enabled=False,
- billing_enabled=True,
+ deployment_edition=DeploymentEdition.CLOUD,
tenant_id="tenant-1",
billing_feature_enabled=True,
plan=CloudPlan.TEAM,
@@ -76,8 +69,7 @@ CASES = [
),
HumanInputEmailDeliveryCase(
name="sandbox_plan",
- enterprise_enabled=False,
- billing_enabled=True,
+ deployment_edition=DeploymentEdition.CLOUD,
tenant_id="tenant-1",
billing_feature_enabled=True,
plan=CloudPlan.SANDBOX,
@@ -91,8 +83,7 @@ def test_resolve_human_input_email_delivery_enabled_matrix(
monkeypatch: pytest.MonkeyPatch,
case: HumanInputEmailDeliveryCase,
):
- monkeypatch.setattr(feature_service_module.dify_config, "ENTERPRISE_ENABLED", case.enterprise_enabled)
- monkeypatch.setattr(feature_service_module.dify_config, "BILLING_ENABLED", case.billing_enabled)
+ monkeypatch.setattr(feature_service_module.dify_config, "DEPLOYMENT_EDITION", case.deployment_edition)
features = FeatureModel()
features.billing.enabled = case.billing_feature_enabled
features.billing.subscription.plan = case.plan
@@ -106,7 +97,7 @@ def test_resolve_human_input_email_delivery_enabled_matrix(
def test_get_vector_space_converts_billing_float_size(monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr(feature_service_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(feature_service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(
feature_service_module.BillingService,
"get_vector_space",
@@ -121,7 +112,7 @@ def test_get_vector_space_converts_billing_float_size(monkeypatch: pytest.Monkey
def test_get_vector_space_preserves_unknown_usage(monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr(feature_service_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(feature_service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(
feature_service_module.BillingService,
"get_vector_space",
diff --git a/api/tests/unit_tests/services/test_feature_service_internal_policies.py b/api/tests/unit_tests/services/test_feature_service_internal_policies.py
index 2ca475001e4..b2a326e1cdb 100644
--- a/api/tests/unit_tests/services/test_feature_service_internal_policies.py
+++ b/api/tests/unit_tests/services/test_feature_service_internal_policies.py
@@ -1,10 +1,11 @@
import pytest
+from enums import DeploymentEdition
from services.feature_service import FeatureService
def test_workspace_creation_uses_environment_policy(monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr("services.feature_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr("services.feature_service.dify_config.ALLOW_CREATE_WORKSPACE", True)
monkeypatch.setattr(
"services.feature_service.EnterpriseService.get_info",
@@ -15,7 +16,7 @@ def test_workspace_creation_uses_environment_policy(monkeypatch: pytest.MonkeyPa
def test_workspace_creation_uses_enterprise_policy(monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True)
+ monkeypatch.setattr("services.feature_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
monkeypatch.setattr(
"services.feature_service.EnterpriseService.get_info",
lambda: {"IsAllowCreateWorkspace": False},
@@ -27,7 +28,7 @@ def test_workspace_creation_uses_enterprise_policy(monkeypatch: pytest.MonkeyPat
def test_workspace_creation_keeps_environment_policy_when_enterprise_value_is_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
- monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True)
+ monkeypatch.setattr("services.feature_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
monkeypatch.setattr("services.feature_service.dify_config.ALLOW_CREATE_WORKSPACE", True)
monkeypatch.setattr("services.feature_service.EnterpriseService.get_info", lambda: {})
@@ -35,8 +36,8 @@ def test_workspace_creation_keeps_environment_policy_when_enterprise_value_is_mi
def test_plugin_manager_is_enabled_only_for_enterprise(monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True)
+ monkeypatch.setattr("services.feature_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
assert FeatureService.is_plugin_manager_enabled() is True
- monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr("services.feature_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
assert FeatureService.is_plugin_manager_enabled() is False
diff --git a/api/tests/unit_tests/services/test_feature_service_knowledge_file_size_limit.py b/api/tests/unit_tests/services/test_feature_service_knowledge_file_size_limit.py
index b8b75cf5020..2bb9a6123c4 100644
--- a/api/tests/unit_tests/services/test_feature_service_knowledge_file_size_limit.py
+++ b/api/tests/unit_tests/services/test_feature_service_knowledge_file_size_limit.py
@@ -2,31 +2,32 @@ from unittest.mock import Mock
import pytest
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from services import feature_service as feature_service_module
from services.feature_service import FeatureService
@pytest.mark.parametrize(
- ("billing_enabled", "tenant_id", "billing_feature_enabled", "plan", "expected"),
+ ("deployment_edition", "tenant_id", "billing_feature_enabled", "plan", "expected"),
[
- (False, "tenant-1", True, CloudPlan.PROFESSIONAL, 15),
- (True, None, True, CloudPlan.PROFESSIONAL, 15),
- (True, "tenant-1", False, CloudPlan.PROFESSIONAL, 15),
- (True, "tenant-1", True, CloudPlan.SANDBOX, 15),
- (True, "tenant-1", True, CloudPlan.PROFESSIONAL, 50),
- (True, "tenant-1", True, CloudPlan.TEAM, 50),
+ (DeploymentEdition.COMMUNITY, "tenant-1", True, CloudPlan.PROFESSIONAL, 15),
+ (DeploymentEdition.ENTERPRISE, "tenant-1", True, CloudPlan.PROFESSIONAL, 15),
+ (DeploymentEdition.CLOUD, None, True, CloudPlan.PROFESSIONAL, 15),
+ (DeploymentEdition.CLOUD, "tenant-1", False, CloudPlan.PROFESSIONAL, 15),
+ (DeploymentEdition.CLOUD, "tenant-1", True, CloudPlan.SANDBOX, 15),
+ (DeploymentEdition.CLOUD, "tenant-1", True, CloudPlan.PROFESSIONAL, 50),
+ (DeploymentEdition.CLOUD, "tenant-1", True, CloudPlan.TEAM, 50),
],
)
def test_get_knowledge_file_size_limit(
monkeypatch: pytest.MonkeyPatch,
- billing_enabled: bool,
+ deployment_edition: DeploymentEdition,
tenant_id: str | None,
billing_feature_enabled: bool,
plan: CloudPlan,
expected: int,
) -> None:
- monkeypatch.setattr(feature_service_module.dify_config, "BILLING_ENABLED", billing_enabled)
+ monkeypatch.setattr(feature_service_module.dify_config, "DEPLOYMENT_EDITION", deployment_edition)
monkeypatch.setattr(feature_service_module.dify_config, "UPLOAD_FILE_SIZE_LIMIT", 15)
monkeypatch.setattr(
feature_service_module.dify_config,
@@ -43,14 +44,14 @@ def test_get_knowledge_file_size_limit(
assert FeatureService.get_knowledge_file_size_limit(tenant_id) == expected
- if billing_enabled and tenant_id:
+ if deployment_edition == DeploymentEdition.CLOUD and tenant_id:
get_info.assert_called_once_with(tenant_id, exclude_vector_space=True)
else:
get_info.assert_not_called()
def test_paid_knowledge_file_size_limit_never_reduces_default(monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setattr(feature_service_module.dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(feature_service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(feature_service_module.dify_config, "UPLOAD_FILE_SIZE_LIMIT", 100)
monkeypatch.setattr(
feature_service_module.dify_config,
diff --git a/api/tests/unit_tests/services/test_feature_service_knowledge_fs.py b/api/tests/unit_tests/services/test_feature_service_knowledge_fs.py
index a269a847920..e9b7d0e0761 100644
--- a/api/tests/unit_tests/services/test_feature_service_knowledge_fs.py
+++ b/api/tests/unit_tests/services/test_feature_service_knowledge_fs.py
@@ -1,6 +1,6 @@
import pytest
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from services.entities.feature_entities import SystemFeatureModel
from services.feature_service import FeatureService
diff --git a/api/tests/unit_tests/services/test_feature_service_learn_app.py b/api/tests/unit_tests/services/test_feature_service_learn_app.py
index d43169e7858..32525b8f3e4 100644
--- a/api/tests/unit_tests/services/test_feature_service_learn_app.py
+++ b/api/tests/unit_tests/services/test_feature_service_learn_app.py
@@ -1,6 +1,6 @@
import pytest
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from services import feature_service as feature_service_module
from services.entities.feature_entities import SystemFeatureModel
from services.feature_service import FeatureService
diff --git a/api/tests/unit_tests/services/test_feature_service_license_expiry_notice.py b/api/tests/unit_tests/services/test_feature_service_license_expiry_notice.py
index 729587a8e61..84c130dda12 100644
--- a/api/tests/unit_tests/services/test_feature_service_license_expiry_notice.py
+++ b/api/tests/unit_tests/services/test_feature_service_license_expiry_notice.py
@@ -1,5 +1,6 @@
import pytest
+from enums import DeploymentEdition
from services import feature_service as feature_service_module
from services.entities.feature_entities import LicenseModel, LicenseStatus
from services.feature_service import FeatureService
@@ -18,7 +19,11 @@ def test_get_license_non_enterprise_ignores_expiry_notice_config(
) -> None:
"""Non-enterprise deployments have no license, so the env toggle never turns the notice on."""
monkeypatch.setattr(feature_service_module.dify_config, "ENABLE_LICENSE_EXPIRY_NOTICE", enabled)
- monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(
+ feature_service_module.dify_config,
+ "DEPLOYMENT_EDITION",
+ DeploymentEdition.COMMUNITY,
+ )
result = FeatureService.get_license()
@@ -31,7 +36,11 @@ def test_get_license_enterprise_reads_license_expiry_notice_enabled(
) -> None:
"""The enterprise-sourced license carries the env-resolved notice flag alongside its real status."""
monkeypatch.setattr(feature_service_module.dify_config, "ENABLE_LICENSE_EXPIRY_NOTICE", enabled)
- monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True)
+ monkeypatch.setattr(
+ feature_service_module.dify_config,
+ "DEPLOYMENT_EDITION",
+ DeploymentEdition.ENTERPRISE,
+ )
monkeypatch.setattr(
feature_service_module.EnterpriseService,
"get_info",
diff --git a/api/tests/unit_tests/services/test_feature_service_licensed_seats.py b/api/tests/unit_tests/services/test_feature_service_licensed_seats.py
index 316c68b8c30..1231c9c56ea 100644
--- a/api/tests/unit_tests/services/test_feature_service_licensed_seats.py
+++ b/api/tests/unit_tests/services/test_feature_service_licensed_seats.py
@@ -1,5 +1,6 @@
import pytest
+from enums import DeploymentEdition
from services import feature_service as feature_service_module
from services.entities.feature_entities import LicenseModel, LicenseStatus
from services.feature_service import FeatureService
@@ -9,7 +10,7 @@ _ENTERPRISE_INFO = {"License": {"licensedSeats": {"enabled": True, "limit": 3, "
def test_get_license_parses_licensed_seats(monkeypatch: pytest.MonkeyPatch):
"""The authenticated license accessor copies the licensed-seat quota out of the enterprise payload."""
- monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True)
+ monkeypatch.setattr("services.feature_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
monkeypatch.setattr(
feature_service_module.EnterpriseService,
"get_info",
@@ -26,7 +27,7 @@ def test_get_license_parses_licensed_seats(monkeypatch: pytest.MonkeyPatch):
def test_get_license_non_enterprise_is_unconstrained(monkeypatch: pytest.MonkeyPatch):
"""Non-enterprise deployments have no license; seat allocation is unconstrained."""
- monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr("services.feature_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
license_model = FeatureService.get_license()
diff --git a/api/tests/unit_tests/services/test_feature_service_plugin_installation_permission.py b/api/tests/unit_tests/services/test_feature_service_plugin_installation_permission.py
index 3b03b4a1312..8ae24b07eb9 100644
--- a/api/tests/unit_tests/services/test_feature_service_plugin_installation_permission.py
+++ b/api/tests/unit_tests/services/test_feature_service_plugin_installation_permission.py
@@ -2,7 +2,7 @@ import logging
import pytest
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from services import feature_service as feature_service_module
from services.entities.feature_entities import PluginInstallationScope, SystemFeatureModel
from services.feature_service import FeatureService
@@ -11,7 +11,7 @@ from services.feature_service import FeatureService
def test_get_plugin_installation_permission_defaults_to_all_for_non_enterprise(
monkeypatch: pytest.MonkeyPatch,
) -> None:
- monkeypatch.setattr(feature_service_module.dify_config, "ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(feature_service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
permission = FeatureService.get_plugin_installation_permission()
@@ -22,7 +22,7 @@ def test_get_plugin_installation_permission_defaults_to_all_for_non_enterprise(
def test_get_plugin_installation_permission_parses_enterprise_policy(
monkeypatch: pytest.MonkeyPatch,
) -> None:
- monkeypatch.setattr(feature_service_module.dify_config, "ENTERPRISE_ENABLED", True)
+ monkeypatch.setattr(feature_service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
monkeypatch.setattr(
feature_service_module.EnterpriseService,
"get_info",
diff --git a/api/tests/unit_tests/services/test_feature_service_sso_protocol.py b/api/tests/unit_tests/services/test_feature_service_sso_protocol.py
index 3578b5b7fc3..0177239077c 100644
--- a/api/tests/unit_tests/services/test_feature_service_sso_protocol.py
+++ b/api/tests/unit_tests/services/test_feature_service_sso_protocol.py
@@ -2,7 +2,7 @@ import logging
import pytest
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from services import feature_service as feature_service_module
from services.entities.feature_entities import SSOProtocol, SystemFeatureModel
from services.feature_service import FeatureService
diff --git a/api/tests/unit_tests/services/test_feature_service_trial_models.py b/api/tests/unit_tests/services/test_feature_service_trial_models.py
index eac3d6235a1..599623aaba0 100644
--- a/api/tests/unit_tests/services/test_feature_service_trial_models.py
+++ b/api/tests/unit_tests/services/test_feature_service_trial_models.py
@@ -1,6 +1,6 @@
import pytest
-from enums.hosted_provider import HostedTrialProvider
+from enums import HostedTrialProvider
from services import feature_service as feature_service_module
from services.feature_service import FeatureService
diff --git a/api/tests/unit_tests/services/test_feature_service_vector_space.py b/api/tests/unit_tests/services/test_feature_service_vector_space.py
index be03b3b4485..9cba1175f87 100644
--- a/api/tests/unit_tests/services/test_feature_service_vector_space.py
+++ b/api/tests/unit_tests/services/test_feature_service_vector_space.py
@@ -1,6 +1,7 @@
from typing import cast
from unittest.mock import patch
+from enums import DeploymentEdition
from services.billing_service import BillingInfo
from services.entities.feature_entities import LimitationModel
from services.feature_service import FeatureService
@@ -27,8 +28,7 @@ def test_get_features_exclude_vector_space_sets_vector_space_to_none():
patch("services.feature_service.BillingService.get_info", return_value=billing_info) as get_info,
patch("services.feature_service.BillingService.get_quota_info", return_value={}),
):
- mock_config.BILLING_ENABLED = True
- mock_config.ENTERPRISE_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_config.CAN_REPLACE_LOGO = False
mock_config.MODEL_LB_ENABLED = False
mock_config.DATASET_OPERATOR_ENABLED = False
diff --git a/api/tests/unit_tests/services/test_feature_service_webapp_public_access.py b/api/tests/unit_tests/services/test_feature_service_webapp_public_access.py
index 2c06dfa311c..00b4bb8ec43 100644
--- a/api/tests/unit_tests/services/test_feature_service_webapp_public_access.py
+++ b/api/tests/unit_tests/services/test_feature_service_webapp_public_access.py
@@ -1,6 +1,6 @@
import pytest
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from services.entities.feature_entities import SystemFeatureModel
from services.feature_service import FeatureService
diff --git a/api/tests/unit_tests/services/test_messages_clean_service.py b/api/tests/unit_tests/services/test_messages_clean_service.py
index 73c096c749c..a7b0a68d2ec 100644
--- a/api/tests/unit_tests/services/test_messages_clean_service.py
+++ b/api/tests/unit_tests/services/test_messages_clean_service.py
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
import pytest
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from services.retention.conversation.messages_clean_policy import (
BillingDisabledPolicy,
BillingSandboxPolicy,
@@ -404,10 +404,10 @@ class TestCreateMessageCleanPolicy:
"""Unit tests for create_message_clean_policy factory function."""
@patch("services.retention.conversation.messages_clean_policy.dify_config")
- def test_billing_disabled_returns_billing_disabled_policy(self, mock_config):
- """Test that BILLING_ENABLED=False returns BillingDisabledPolicy."""
+ def test_non_cloud_edition_returns_billing_disabled_policy(self, mock_config):
+ """Test that the Community edition returns BillingDisabledPolicy."""
# Arrange
- mock_config.BILLING_ENABLED = False
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
# Act
policy = create_message_clean_policy(graceful_period_days=21)
@@ -417,10 +417,10 @@ class TestCreateMessageCleanPolicy:
@patch("services.retention.conversation.messages_clean_policy.BillingService", autospec=True)
@patch("services.retention.conversation.messages_clean_policy.dify_config")
- def test_billing_enabled_policy_has_correct_internals(self, mock_config, mock_billing_service):
+ def test_cloud_edition_policy_has_correct_internals(self, mock_config, mock_billing_service):
"""Test that BillingSandboxPolicy is created with correct internal values."""
# Arrange
- mock_config.BILLING_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
whitelist = ["tenant1", "tenant2"]
mock_billing_service.get_expired_subscription_cleanup_whitelist.return_value = whitelist
mock_plan_provider = MagicMock()
diff --git a/api/tests/unit_tests/services/test_model_provider_service.py b/api/tests/unit_tests/services/test_model_provider_service.py
index 3b57e46a7f9..597d18f49d4 100644
--- a/api/tests/unit_tests/services/test_model_provider_service.py
+++ b/api/tests/unit_tests/services/test_model_provider_service.py
@@ -8,6 +8,7 @@ from core.entities.model_entities import ModelStatus
from core.entities.provider_entities import CredentialConfiguration
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.entities.plugin_daemon import PluginModelProviderBinding
+from enums import DeploymentEdition
from graphon.model_runtime.entities.common_entities import I18nObject
from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType, ParameterRule, ParameterType
from graphon.model_runtime.entities.provider_entities import ConfigurateMethod
@@ -377,7 +378,7 @@ class TestModelProviderServiceConfiguration:
def test_preferred_provider_fallback_uses_custom_presence_not_configuration_status(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
- monkeypatch.setattr(service_module.dify_config, "EDITION", "SELF_HOSTED")
+ monkeypatch.setattr(service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
state = _ProviderSummaryState(has_custom_provider=True)
preferred_provider_type = ModelProviderService._get_preferred_provider_type(
diff --git a/api/tests/unit_tests/services/test_rag_pipeline_task_proxy.py b/api/tests/unit_tests/services/test_rag_pipeline_task_proxy.py
index 305045cb6ee..cb72de5b53e 100644
--- a/api/tests/unit_tests/services/test_rag_pipeline_task_proxy.py
+++ b/api/tests/unit_tests/services/test_rag_pipeline_task_proxy.py
@@ -6,7 +6,7 @@ import pytest
from core.app.entities.rag_pipeline_invoke_entities import RagPipelineInvokeEntity
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from services.rag_pipeline.rag_pipeline_task_proxy import RagPipelineTaskProxy
diff --git a/api/tests/unit_tests/services/test_recommended_app_service.py b/api/tests/unit_tests/services/test_recommended_app_service.py
index 6ebb5b62015..98f4ca467ca 100644
--- a/api/tests/unit_tests/services/test_recommended_app_service.py
+++ b/api/tests/unit_tests/services/test_recommended_app_service.py
@@ -10,7 +10,7 @@ import pytest
from sqlalchemy import select
from sqlalchemy.orm import Session
-from enums.deployment_edition import DeploymentEdition
+from enums import DeploymentEdition
from models.model import AccountTrialAppRecord, App, AppMode, TrialApp
from services import recommended_app_service as service_module
from services.recommended_app_service import RecommendedAppService
@@ -49,24 +49,22 @@ class AppDetailKwargs(TypedDict, total=False):
@pytest.mark.parametrize(
- ("edition", "enterprise_enabled", "feature_enabled", "expected"),
+ ("edition", "feature_enabled", "expected"),
[
- ("CLOUD", False, True, True),
- ("CLOUD", False, False, False),
- ("SELF_HOSTED", False, True, False),
- ("SELF_HOSTED", True, True, False),
+ (DeploymentEdition.CLOUD, True, True),
+ (DeploymentEdition.CLOUD, False, False),
+ (DeploymentEdition.COMMUNITY, True, False),
+ (DeploymentEdition.ENTERPRISE, True, False),
],
)
def test_trial_app_policy_is_cloud_only(
monkeypatch: pytest.MonkeyPatch,
sqlite_session: Session,
- edition: str,
- enterprise_enabled: bool,
+ edition: DeploymentEdition,
feature_enabled: bool,
expected: bool,
) -> None:
- monkeypatch.setattr(service_module.dify_config, "EDITION", edition)
- monkeypatch.setattr(service_module.dify_config, "ENTERPRISE_ENABLED", enterprise_enabled)
+ monkeypatch.setattr(service_module.dify_config, "DEPLOYMENT_EDITION", edition)
monkeypatch.setattr(service_module.dify_config, "ENABLE_TRIAL_APP", feature_enabled)
assert RecommendedAppService.is_trial_app_enabled() is expected
diff --git a/api/tests/unit_tests/services/test_snippet_service.py b/api/tests/unit_tests/services/test_snippet_service.py
index 270bf39acb8..61ab2ff93ba 100644
--- a/api/tests/unit_tests/services/test_snippet_service.py
+++ b/api/tests/unit_tests/services/test_snippet_service.py
@@ -6,6 +6,7 @@ from unittest.mock import Mock
import pytest
+from enums import DeploymentEdition
from models.snippet import SnippetType
from models.workflow import Workflow, WorkflowKind, WorkflowType
from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError, WorkflowNotFoundError
@@ -668,7 +669,7 @@ def test_delete_archived_workflow_run_files_removes_prefixed_objects(monkeypatch
list_objects=Mock(return_value=["tenant-1/app_id=snippet-1/run.json"]),
delete_object=Mock(),
)
- monkeypatch.setattr(dify_config, "BILLING_ENABLED", True)
+ monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(dify_config, "ARCHIVE_STORAGE_ENABLED", True)
monkeypatch.setattr("libs.archive_storage.get_archive_storage", Mock(return_value=archive_storage))
diff --git a/api/tests/unit_tests/services/test_step_by_step_tour_service.py b/api/tests/unit_tests/services/test_step_by_step_tour_service.py
index de08cc596fd..6cfc69dfc74 100644
--- a/api/tests/unit_tests/services/test_step_by_step_tour_service.py
+++ b/api/tests/unit_tests/services/test_step_by_step_tour_service.py
@@ -5,6 +5,7 @@ from datetime import UTC, datetime
import pytest
from sqlalchemy.exc import IntegrityError
+from enums import DeploymentEdition
from models.account import Account, AccountStatus
from models.onboarding import AccountStepByStepTourState
from services import step_by_step_tour_service as service_module
@@ -104,7 +105,7 @@ def test_get_state_creates_state_and_records_first_workspace_for_eligible_accoun
def test_is_eligible_does_not_depend_on_cloud_edition(monkeypatch: pytest.MonkeyPatch) -> None:
_set_tour_config(monkeypatch, enabled=True, rollout_started_at=datetime(2026, 6, 1))
- monkeypatch.setattr(service_module.dify_config, "EDITION", "SELF_HOSTED")
+ monkeypatch.setattr(service_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
result = StepByStepTourService.is_eligible(_account(initialized_at=datetime(2026, 6, 28)))
diff --git a/api/tests/unit_tests/services/test_telemetry_service.py b/api/tests/unit_tests/services/test_telemetry_service.py
index bec9dae012e..d931be80364 100644
--- a/api/tests/unit_tests/services/test_telemetry_service.py
+++ b/api/tests/unit_tests/services/test_telemetry_service.py
@@ -7,6 +7,7 @@ import pytest
from sqlalchemy import select
from sqlalchemy.orm import Session
+from enums import DeploymentEdition
from models.model import DifySetup
from services import telemetry_service
from services.telemetry_service import CommunityTelemetryService
@@ -14,8 +15,7 @@ from services.telemetry_service import CommunityTelemetryService
@pytest.fixture
def telemetry_enabled(monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr(telemetry_service.dify_config, "EDITION", "SELF_HOSTED")
- monkeypatch.setattr(telemetry_service.dify_config, "ENTERPRISE_ENABLED", False)
+ monkeypatch.setattr(telemetry_service.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(telemetry_service.dify_config, "DISABLE_TELEMETRY", False)
monkeypatch.setattr(telemetry_service.dify_config, "DO_NOT_TRACK", False)
monkeypatch.setattr(telemetry_service.dify_config, "CI", False)
@@ -29,8 +29,7 @@ def telemetry_enabled(monkeypatch: pytest.MonkeyPatch):
def test_telemetry_is_disabled_for_enterprise(monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr(telemetry_service.dify_config, "EDITION", "SELF_HOSTED")
- monkeypatch.setattr(telemetry_service.dify_config, "ENTERPRISE_ENABLED", True)
+ monkeypatch.setattr(telemetry_service.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE)
assert CommunityTelemetryService._is_enabled() is False
@@ -38,7 +37,7 @@ def test_telemetry_is_disabled_for_enterprise(monkeypatch: pytest.MonkeyPatch):
@pytest.mark.parametrize(
("setting", "value"),
[
- ("EDITION", "CLOUD"),
+ ("DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
("DISABLE_TELEMETRY", True),
("DO_NOT_TRACK", True),
("CI", True),
diff --git a/api/tests/unit_tests/services/test_vector_space_admission_service.py b/api/tests/unit_tests/services/test_vector_space_admission_service.py
index a3241dfd49e..4050be7ae5b 100644
--- a/api/tests/unit_tests/services/test_vector_space_admission_service.py
+++ b/api/tests/unit_tests/services/test_vector_space_admission_service.py
@@ -3,7 +3,7 @@ import threading
from concurrent.futures import ThreadPoolExecutor
from types import SimpleNamespace, TracebackType
from typing import cast
-from unittest.mock import PropertyMock, call, patch
+from unittest.mock import call, patch
import pytest
from sqlalchemy.orm import Session
@@ -12,8 +12,7 @@ from configs import dify_config
from core.rag.datasource.vdb.vector_type import VectorType
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
from core.rag.models.document import AttachmentDocument, ChildDocument, Document
-from enums.cloud_plan import CloudPlan
-from enums.deployment_edition import DeploymentEdition
+from enums import CloudPlan, DeploymentEdition
from models.dataset import Dataset
from services.vector_space_admission_service import (
VECTOR_SPACE_ADMISSION_ERROR_CODE,
@@ -99,13 +98,7 @@ def _check_estimate(
with (
patch.object(service, "_get_plan", return_value=plan),
patch.object(service, "_get_embedding_dimension", return_value=3072),
- patch.object(
- type(dify_config),
- "DEPLOYMENT_EDITION",
- new_callable=PropertyMock,
- return_value=DeploymentEdition.CLOUD,
- ),
- patch("services.vector_space_admission_service.dify_config.BILLING_ENABLED", True),
+ patch.object(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch(
"services.vector_space_admission_service.dify_config.TIDB_ON_QDRANT_ESTIMATED_STORAGE_LIMITS_MB",
_ESTIMATE_LIMITS,
@@ -244,13 +237,7 @@ def test_pipeline_qa_workload_counts_question_vectors_without_summaries() -> Non
def test_admission_is_cloud_only() -> None:
service = VectorSpaceAdmissionService()
with (
- patch.object(
- type(dify_config),
- "DEPLOYMENT_EDITION",
- new_callable=PropertyMock,
- return_value=DeploymentEdition.COMMUNITY,
- ),
- patch("services.vector_space_admission_service.dify_config.BILLING_ENABLED", True),
+ patch.object(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("services.vector_space_admission_service.Vector.resolve_vector_type") as resolve_vector_type,
patch("services.vector_space_admission_service.BillingService.get_info") as get_info,
):
@@ -268,13 +255,7 @@ def test_admission_is_cloud_only() -> None:
def test_admission_skips_non_tidb_vector_backends() -> None:
service = VectorSpaceAdmissionService()
with (
- patch.object(
- type(dify_config),
- "DEPLOYMENT_EDITION",
- new_callable=PropertyMock,
- return_value=DeploymentEdition.CLOUD,
- ),
- patch("services.vector_space_admission_service.dify_config.BILLING_ENABLED", True),
+ patch.object(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("services.vector_space_admission_service.Vector.resolve_vector_type", return_value=VectorType.QDRANT),
patch("services.vector_space_admission_service.BillingService.get_info") as get_info,
):
@@ -375,13 +356,7 @@ def test_usage_lookup_is_refreshed_for_each_document() -> None:
with (
patch.object(service, "_get_plan", return_value=CloudPlan.SANDBOX),
patch.object(service, "_get_embedding_dimension", return_value=3072),
- patch.object(
- type(dify_config),
- "DEPLOYMENT_EDITION",
- new_callable=PropertyMock,
- return_value=DeploymentEdition.CLOUD,
- ),
- patch("services.vector_space_admission_service.dify_config.BILLING_ENABLED", True),
+ patch.object(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch(
"services.vector_space_admission_service.dify_config.TIDB_ON_QDRANT_ESTIMATED_STORAGE_LIMITS_MB",
_ESTIMATE_LIMITS,
diff --git a/api/tests/unit_tests/services/test_workflow_service.py b/api/tests/unit_tests/services/test_workflow_service.py
index 9fdb2e07bc7..da5a800cd59 100644
--- a/api/tests/unit_tests/services/test_workflow_service.py
+++ b/api/tests/unit_tests/services/test_workflow_service.py
@@ -22,6 +22,7 @@ from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session, sessionmaker
from core.workflow.llm_environment_variable import LLMEnvironmentVariable
+from enums import DeploymentEdition
from graphon.enums import (
BuiltinNodeTypes,
ErrorStrategy,
@@ -1039,7 +1040,7 @@ class TestWorkflowService:
with (
patch("services.workflow_service.app_published_workflow_was_updated"),
- patch("services.workflow_service.dify_config.BILLING_ENABLED", False),
+ patch("services.workflow_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
result, retirement_candidates = workflow_service.publish_workflow(
session=sqlite_session,
@@ -1142,7 +1143,7 @@ class TestWorkflowService:
sqlite_session.commit()
with (
- patch("services.workflow_service.dify_config.BILLING_ENABLED", True),
+ patch("services.workflow_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("services.workflow_service.BillingService") as MockBillingService,
):
MockBillingService.get_info.return_value = {"subscription": {"plan": "sandbox"}}
diff --git a/api/tests/unit_tests/services/test_workspace_credit_pool.py b/api/tests/unit_tests/services/test_workspace_credit_pool.py
index dc3497a1a05..08210e6d391 100644
--- a/api/tests/unit_tests/services/test_workspace_credit_pool.py
+++ b/api/tests/unit_tests/services/test_workspace_credit_pool.py
@@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch
import pytest
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from services.credit_pool_service import CreditPoolBalance
from services.workspace_service import WorkspaceService
@@ -27,7 +27,7 @@ def test_get_effective_credit_pool_prefers_available_paid_pool(
"subscription": {"plan": CloudPlan.TEAM},
"next_credit_reset_date": 1775001600,
}
- config = SimpleNamespace(BILLING_ENABLED=True)
+ config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
with (
patch("services.workspace_service.dify_config", config),
@@ -59,7 +59,7 @@ def test_get_effective_credit_pool_exposes_exhausted_trial_pool() -> None:
"enabled": True,
"subscription": {"plan": CloudPlan.SANDBOX},
}
- config = SimpleNamespace(BILLING_ENABLED=True)
+ config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
with (
patch("services.workspace_service.dify_config", config),
diff --git a/api/tests/unit_tests/services/test_workspace_service.py b/api/tests/unit_tests/services/test_workspace_service.py
index 435c7a5dfa2..9d01cb7b847 100644
--- a/api/tests/unit_tests/services/test_workspace_service.py
+++ b/api/tests/unit_tests/services/test_workspace_service.py
@@ -1,7 +1,7 @@
from types import SimpleNamespace
from unittest.mock import MagicMock, call, patch
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan, DeploymentEdition
from models.account import Tenant
from services.credit_pool_service import CreditPoolBalance
from services.workspace_service import WorkspaceService
@@ -22,7 +22,7 @@ def test_get_current_workspace_summary_sandbox_uses_trial_only() -> None:
"enabled": True,
"subscription": {"plan": CloudPlan.SANDBOX},
}
- config = SimpleNamespace(BILLING_ENABLED=True)
+ config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
with (
patch("services.workspace_service.dify_config", config),
@@ -64,7 +64,7 @@ def test_get_current_workspace_summary_falls_back_from_exhausted_paid_pool() ->
"enabled": True,
"subscription": {"plan": CloudPlan.TEAM},
}
- config = SimpleNamespace(BILLING_ENABLED=True)
+ config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
with (
patch("services.workspace_service.dify_config", config),
@@ -84,11 +84,11 @@ def test_get_current_workspace_summary_falls_back_from_exhausted_paid_pool() ->
]
-def test_get_current_workspace_summary_billing_disabled_skips_billing_and_credits() -> None:
+def test_get_current_workspace_summary_non_cloud_skips_billing_and_credits() -> None:
tenant = Tenant(name="Workspace")
session = MagicMock()
session.scalar.return_value = SimpleNamespace(role="editor")
- config = SimpleNamespace(BILLING_ENABLED=False)
+ config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
with (
patch("services.workspace_service.dify_config", config),
diff --git a/api/tests/unit_tests/services/workflow/test_queue_dispatcher.py b/api/tests/unit_tests/services/workflow/test_queue_dispatcher.py
index 18b46e40f52..78cce598eb6 100644
--- a/api/tests/unit_tests/services/workflow/test_queue_dispatcher.py
+++ b/api/tests/unit_tests/services/workflow/test_queue_dispatcher.py
@@ -1,5 +1,6 @@
from unittest.mock import patch
+from enums import DeploymentEdition
from services.workflow.queue_dispatcher import (
ProfessionalQueueDispatcher,
QueueDispatcherManager,
@@ -36,8 +37,8 @@ class TestDispatchers:
class TestQueueDispatcherManager:
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
- def test_billing_enabled_professional_plan(self, mock_config, mock_billing):
- mock_config.BILLING_ENABLED = True
+ def test_cloud_edition_professional_plan(self, mock_config, mock_billing):
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_billing.get_info.return_value = {"subscription": {"plan": "professional"}}
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
@@ -46,8 +47,8 @@ class TestQueueDispatcherManager:
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
- def test_billing_enabled_team_plan(self, mock_config, mock_billing):
- mock_config.BILLING_ENABLED = True
+ def test_cloud_edition_team_plan(self, mock_config, mock_billing):
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_billing.get_info.return_value = {"subscription": {"plan": "team"}}
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
@@ -56,8 +57,8 @@ class TestQueueDispatcherManager:
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
- def test_billing_enabled_sandbox_plan(self, mock_config, mock_billing):
- mock_config.BILLING_ENABLED = True
+ def test_cloud_edition_sandbox_plan(self, mock_config, mock_billing):
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_billing.get_info.return_value = {"subscription": {"plan": "sandbox"}}
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
@@ -66,8 +67,8 @@ class TestQueueDispatcherManager:
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
- def test_billing_enabled_unknown_plan_defaults_to_sandbox(self, mock_config, mock_billing):
- mock_config.BILLING_ENABLED = True
+ def test_cloud_edition_unknown_plan_defaults_to_sandbox(self, mock_config, mock_billing):
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_billing.get_info.return_value = {"subscription": {"plan": "enterprise"}}
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
@@ -76,8 +77,8 @@ class TestQueueDispatcherManager:
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
- def test_billing_enabled_service_failure_defaults_to_sandbox(self, mock_config, mock_billing):
- mock_config.BILLING_ENABLED = True
+ def test_cloud_edition_billing_failure_defaults_to_sandbox(self, mock_config, mock_billing):
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_billing.get_info.side_effect = Exception("billing unavailable")
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
@@ -85,8 +86,8 @@ class TestQueueDispatcherManager:
assert isinstance(dispatcher, SandboxQueueDispatcher)
@patch("services.workflow.queue_dispatcher.dify_config")
- def test_billing_disabled_defaults_to_team(self, mock_config):
- mock_config.BILLING_ENABLED = False
+ def test_non_cloud_edition_defaults_to_team(self, mock_config):
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
@@ -95,7 +96,7 @@ class TestQueueDispatcherManager:
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
def test_missing_subscription_key_defaults_to_sandbox(self, mock_config, mock_billing):
- mock_config.BILLING_ENABLED = True
+ mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
mock_billing.get_info.return_value = {}
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
diff --git a/api/tests/unit_tests/tasks/test_dataset_indexing_task.py b/api/tests/unit_tests/tasks/test_dataset_indexing_task.py
index 3466543762b..25dd16b2d58 100644
--- a/api/tests/unit_tests/tasks/test_dataset_indexing_task.py
+++ b/api/tests/unit_tests/tasks/test_dataset_indexing_task.py
@@ -15,7 +15,7 @@ from sqlalchemy.orm import Session
from core.indexing_runner import DocumentIsPausedError
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
-from enums.cloud_plan import CloudPlan
+from enums import CloudPlan
from extensions.ext_redis import redis_client
from models.dataset import Dataset, Document
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
diff --git a/api/tests/unit_tests/tasks/test_mail_send_task.py b/api/tests/unit_tests/tasks/test_mail_send_task.py
index 445ef6e3c7c..6862daaeb90 100644
--- a/api/tests/unit_tests/tasks/test_mail_send_task.py
+++ b/api/tests/unit_tests/tasks/test_mail_send_task.py
@@ -13,10 +13,15 @@ import smtplib
from unittest.mock import ANY, MagicMock, patch
import pytest
+from python_http_client.exceptions import ForbiddenError, UnauthorizedError
from configs import dify_config
from configs.feature import TemplateMode
-from libs.email_i18n import EmailType
+from extensions.ext_mail import Mail
+from libs.email_i18n import EmailI18nConfig, EmailI18nService, EmailLanguage, EmailTemplate, EmailType
+from libs.sendgrid import SendGridClient
+from libs.smtp import SMTPClient
+from services.entities.feature_entities import BrandingModel
from tasks.mail_inner_task import _render_template_with_strategy, send_inner_email_task
from tasks.mail_register_task import (
send_email_register_mail_task,
@@ -131,8 +136,6 @@ class TestSMTPIntegration:
def test_smtp_send_with_tls_ssl(self, mock_smtp_ssl):
"""Test SMTP send with TLS using SMTP_SSL."""
# Arrange
- from libs.smtp import SMTPClient
-
mock_server = MagicMock()
mock_smtp_ssl.return_value = mock_server
@@ -161,8 +164,6 @@ class TestSMTPIntegration:
def test_smtp_send_with_opportunistic_tls(self, mock_smtp):
"""Test SMTP send with opportunistic TLS (STARTTLS)."""
# Arrange
- from libs.smtp import SMTPClient
-
mock_server = MagicMock()
mock_smtp.return_value = mock_server
@@ -193,8 +194,6 @@ class TestSMTPIntegration:
def test_smtp_send_without_tls(self, mock_smtp):
"""Test SMTP send without TLS encryption."""
# Arrange
- from libs.smtp import SMTPClient
-
mock_server = MagicMock()
mock_smtp.return_value = mock_server
@@ -223,8 +222,6 @@ class TestSMTPIntegration:
def test_smtp_send_without_authentication(self, mock_smtp):
"""Test SMTP send without authentication (empty credentials)."""
# Arrange
- from libs.smtp import SMTPClient
-
mock_server = MagicMock()
mock_smtp.return_value = mock_server
@@ -252,8 +249,6 @@ class TestSMTPIntegration:
def test_smtp_send_authentication_failure(self, mock_smtp_ssl):
"""Test SMTP send handles authentication failure."""
# Arrange
- from libs.smtp import SMTPClient
-
mock_server = MagicMock()
mock_smtp_ssl.return_value = mock_server
mock_server.login.side_effect = smtplib.SMTPAuthenticationError(535, b"Authentication failed")
@@ -280,8 +275,6 @@ class TestSMTPIntegration:
def test_smtp_send_timeout_error(self, mock_smtp_ssl):
"""Test SMTP send handles timeout errors."""
# Arrange
- from libs.smtp import SMTPClient
-
mock_smtp_ssl.side_effect = TimeoutError("Connection timeout")
client = SMTPClient(
@@ -304,8 +297,6 @@ class TestSMTPIntegration:
def test_smtp_send_connection_refused(self, mock_smtp_ssl):
"""Test SMTP send handles connection refused errors."""
# Arrange
- from libs.smtp import SMTPClient
-
mock_smtp_ssl.side_effect = ConnectionRefusedError("Connection refused")
client = SMTPClient(
@@ -328,8 +319,6 @@ class TestSMTPIntegration:
def test_smtp_send_ensures_cleanup_on_error(self, mock_smtp_ssl):
"""Test SMTP send ensures cleanup even when errors occur."""
# Arrange
- from libs.smtp import SMTPClient
-
mock_server = MagicMock()
mock_smtp_ssl.return_value = mock_server
mock_server.sendmail.side_effect = smtplib.SMTPException("Send failed")
@@ -610,8 +599,6 @@ class TestSendGridIntegration:
def test_sendgrid_send_success(self, mock_sg_client):
"""Test SendGrid client sends email successfully."""
# Arrange
- from libs.sendgrid import SendGridClient
-
mock_client_instance = MagicMock()
mock_sg_client.return_value = mock_client_instance
mock_response = MagicMock()
@@ -633,8 +620,6 @@ class TestSendGridIntegration:
def test_sendgrid_send_missing_recipient(self, mock_sg_client):
"""Test SendGrid client raises error when recipient is missing."""
# Arrange
- from libs.sendgrid import SendGridClient
-
client = SendGridClient(sendgrid_api_key="test_api_key", _from="noreply@example.com")
mail_data = {"to": "", "subject": "Test Subject", "html": "
Test Content
"} @@ -647,10 +632,6 @@ class TestSendGridIntegration: def test_sendgrid_send_unauthorized_error(self, mock_sg_client): """Test SendGrid client handles unauthorized errors.""" # Arrange - from python_http_client.exceptions import UnauthorizedError - - from libs.sendgrid import SendGridClient - mock_client_instance = MagicMock() mock_sg_client.return_value = mock_client_instance mock_client_instance.client.mail.send.post.side_effect = UnauthorizedError( @@ -669,10 +650,6 @@ class TestSendGridIntegration: def test_sendgrid_send_forbidden_error(self, mock_sg_client): """Test SendGrid client handles forbidden errors.""" # Arrange - from python_http_client.exceptions import ForbiddenError - - from libs.sendgrid import SendGridClient - mock_client_instance = MagicMock() mock_sg_client.return_value = mock_client_instance mock_client_instance.client.mail.send.post.side_effect = ForbiddenError(MagicMock(status_code=403), "Forbidden") @@ -689,8 +666,6 @@ class TestSendGridIntegration: def test_sendgrid_send_timeout_error(self, mock_sg_client): """Test SendGrid client handles timeout errors.""" # Arrange - from libs.sendgrid import SendGridClient - mock_client_instance = MagicMock() mock_sg_client.return_value = mock_client_instance mock_client_instance.client.mail.send.post.side_effect = TimeoutError("Request timeout") @@ -711,8 +686,6 @@ class TestMailExtension: def test_mail_init_smtp_configuration(self, mock_config): """Test mail extension initializes SMTP client correctly.""" # Arrange - from extensions.ext_mail import Mail - mock_config.MAIL_TYPE = "smtp" mock_config.SMTP_SERVER = "smtp.example.com" mock_config.SMTP_PORT = 465 @@ -736,8 +709,6 @@ class TestMailExtension: def test_mail_init_without_mail_type(self, mock_config): """Test mail extension skips initialization when MAIL_TYPE is not set.""" # Arrange - from extensions.ext_mail import Mail - mock_config.MAIL_TYPE = None mail = Mail() @@ -753,8 +724,6 @@ class TestMailExtension: def test_mail_send_validates_parameters(self, mock_config): """Test mail send validates required parameters.""" # Arrange - from extensions.ext_mail import Mail - mail = Mail() mail._client = MagicMock() mail._default_send_from = "noreply@example.com" @@ -775,8 +744,6 @@ class TestMailExtension: def test_mail_send_uses_default_from(self, mock_config): """Test mail send uses default from address when not provided.""" # Arrange - from extensions.ext_mail import Mail - mail = Mail() mock_client = MagicMock() mail._client = mock_client @@ -800,9 +767,6 @@ class TestEmailI18nService: def test_email_service_sends_with_branding(self, mock_renderer_class, mock_branding_class, mock_sender_class): """Test email service sends email with branding support.""" # Arrange - from libs.email_i18n import EmailI18nConfig, EmailI18nService, EmailLanguage, EmailTemplate, EmailType - from services.entities.feature_entities import BrandingModel - mock_renderer = MagicMock() mock_renderer.render_template.return_value = "Rendered content" mock_renderer_class.return_value = mock_renderer @@ -848,8 +812,6 @@ class TestEmailI18nService: def test_email_service_send_raw_email_single_recipient(self, mock_sender_class): """Test email service sends raw email to single recipient.""" # Arrange - from libs.email_i18n import EmailI18nConfig, EmailI18nService - mock_sender = MagicMock() mock_sender_class.return_value = mock_sender @@ -872,8 +834,6 @@ class TestEmailI18nService: def test_email_service_send_raw_email_multiple_recipients(self, mock_sender_class): """Test email service sends raw email to multiple recipients.""" # Arrange - from libs.email_i18n import EmailI18nConfig, EmailI18nService - mock_sender = MagicMock() mock_sender_class.return_value = mock_sender @@ -941,8 +901,6 @@ class TestEdgeCasesAndErrorHandling: configuration parameters are not provided. """ # Arrange - from extensions.ext_mail import Mail - mock_config.MAIL_TYPE = "smtp" mock_config.SMTP_SERVER = None # Missing required parameter mock_config.SMTP_PORT = 465 @@ -963,8 +921,6 @@ class TestEdgeCasesAndErrorHandling: This test ensures the configuration is validated properly. """ # Arrange - from extensions.ext_mail import Mail - mock_config.MAIL_TYPE = "smtp" mock_config.SMTP_SERVER = "smtp.example.com" mock_config.SMTP_PORT = 587 @@ -987,8 +943,6 @@ class TestEdgeCasesAndErrorHandling: are accepted and invalid types are rejected. """ # Arrange - from extensions.ext_mail import Mail - mock_config.MAIL_TYPE = "unsupported_provider" mail = Mail() @@ -1007,8 +961,6 @@ class TestEdgeCasesAndErrorHandling: emails with empty subjects without crashing. """ # Arrange - from libs.smtp import SMTPClient - mock_server = MagicMock() mock_smtp_ssl.return_value = mock_server @@ -1040,8 +992,6 @@ class TestEdgeCasesAndErrorHandling: subject lines and email bodies. """ # Arrange - from libs.smtp import SMTPClient - mock_server = MagicMock() mock_smtp_ssl.return_value = mock_server @@ -1141,8 +1091,6 @@ class TestResendIntegration: and the client is initialized. """ # Arrange - from extensions.ext_mail import Mail - mock_config.MAIL_TYPE = "resend" mock_config.RESEND_API_KEY = "re_test_api_key" mock_config.RESEND_API_URL = None @@ -1183,8 +1131,6 @@ class TestResendIntegration: This test ensures custom URLs are properly configured. """ # Arrange - from extensions.ext_mail import Mail - mock_config.MAIL_TYPE = "resend" mock_config.RESEND_API_KEY = "re_test_api_key" mock_config.RESEND_API_URL = "https://custom-resend.example.com" @@ -1224,8 +1170,6 @@ class TestResendIntegration: proper validation of required configuration. """ # Arrange - from extensions.ext_mail import Mail - mock_config.MAIL_TYPE = "resend" mock_config.RESEND_API_KEY = None # Missing API key @@ -1333,8 +1277,6 @@ class TestEmailValidation: this test documents the current behavior. """ # Arrange - from extensions.ext_mail import Mail - mail = Mail() mock_client = MagicMock() mail._client = mock_client @@ -1364,8 +1306,6 @@ class TestSMTPEdgeCases: or extensive formatting. This test ensures they're handled. """ # Arrange - from libs.smtp import SMTPClient - mock_server = MagicMock() mock_smtp_ssl.return_value = mock_server @@ -1401,8 +1341,6 @@ class TestSMTPEdgeCases: recipient per call. This test documents that behavior. """ # Arrange - from libs.smtp import SMTPClient - mock_server = MagicMock() mock_smtp_ssl.return_value = mock_server @@ -1434,8 +1372,6 @@ class TestSMTPEdgeCases: whitespace to avoid authentication with blank credentials. """ # Arrange - from libs.smtp import SMTPClient - mock_server = MagicMock() mock_smtp.return_value = mock_server diff --git a/api/tests/unit_tests/tasks/test_refresh_billing_vector_space_task.py b/api/tests/unit_tests/tasks/test_refresh_billing_vector_space_task.py index 8f4820be660..63bf6fcedfd 100644 --- a/api/tests/unit_tests/tasks/test_refresh_billing_vector_space_task.py +++ b/api/tests/unit_tests/tasks/test_refresh_billing_vector_space_task.py @@ -2,6 +2,7 @@ from unittest.mock import patch import pytest +from enums import DeploymentEdition from tasks.refresh_billing_vector_space_task import ( refresh_billing_vector_space_task, schedule_billing_vector_space_refresh, @@ -10,7 +11,7 @@ from tasks.refresh_billing_vector_space_task import ( def test_refresh_invalidates_vector_space_cache(): with ( - patch("tasks.refresh_billing_vector_space_task.dify_config.BILLING_ENABLED", True), + patch("tasks.refresh_billing_vector_space_task.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD), patch( "tasks.refresh_billing_vector_space_task.BillingService.invalidate_vector_space_cache" ) as invalidate_cache, @@ -24,7 +25,7 @@ def test_refresh_failure_schedules_retry(): error = RuntimeError("billing unavailable") with ( - patch("tasks.refresh_billing_vector_space_task.dify_config.BILLING_ENABLED", True), + patch("tasks.refresh_billing_vector_space_task.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD), patch( "tasks.refresh_billing_vector_space_task.BillingService.invalidate_vector_space_cache", side_effect=error, @@ -39,7 +40,7 @@ def test_refresh_failure_schedules_retry(): def test_dispatch_failure_does_not_propagate(): with ( - patch("tasks.refresh_billing_vector_space_task.dify_config.BILLING_ENABLED", True), + patch("tasks.refresh_billing_vector_space_task.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD), patch.object(refresh_billing_vector_space_task, "delay", side_effect=RuntimeError("broker unavailable")), ): schedule_billing_vector_space_refresh("tenant-1") diff --git a/api/tests/unit_tests/test_app_factory.py b/api/tests/unit_tests/test_app_factory.py index ae373c923b8..acdeecc07c0 100644 --- a/api/tests/unit_tests/test_app_factory.py +++ b/api/tests/unit_tests/test_app_factory.py @@ -7,6 +7,7 @@ from flask import Blueprint, Flask from flask_restx import Resource from app_factory import create_flask_app_with_configs +from enums import DeploymentEdition from libs.external_api import ExternalApi from services.entities.feature_entities import LicenseStatus @@ -18,8 +19,12 @@ def _license(status: LicenseStatus | None): return patch("app_factory.EnterpriseService.get_cached_license_status", return_value=status) -def _enterprise(enabled: bool = True): - return patch("app_factory.dify_config.ENTERPRISE_ENABLED", enabled) +def _enterprise(): + return patch("app_factory.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE) + + +def _community(): + return patch("app_factory.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY) @pytest.fixture @@ -134,7 +139,7 @@ class TestServiceApiLicenseGate: @pytest.mark.parametrize("status", INVALID_STATUSES) def test_does_not_gate_community_edition(self, gated_app: Flask, status: LicenseStatus): - with _enterprise(False), _license(status): + with _community(), _license(status): response = gated_app.test_client().post("/v1/chat-messages") assert response.status_code == 200 @@ -165,7 +170,7 @@ class TestMcpLicenseGate: @pytest.mark.parametrize("status", INVALID_STATUSES) def test_does_not_gate_community_edition(self, gated_app: Flask, status: LicenseStatus): - with _enterprise(False), _license(status): + with _community(), _license(status): response = gated_app.test_client().post("/mcp/server/srv-code/mcp") assert response.status_code == 200 @@ -196,7 +201,7 @@ class TestTriggerLicenseGate: @pytest.mark.parametrize("status", INVALID_STATUSES) def test_does_not_gate_community_edition(self, gated_app: Flask, status: LicenseStatus): - with _enterprise(False), _license(status): + with _community(), _license(status): response = gated_app.test_client().post("/triggers/webhook/hook-id") assert response.status_code == 200 diff --git a/cli/src/api/meta.test.ts b/cli/src/api/meta.test.ts index c3e081a39f9..a73ca6ecf2c 100644 --- a/cli/src/api/meta.test.ts +++ b/cli/src/api/meta.test.ts @@ -38,7 +38,7 @@ describe('MetaClient', () => { const info = await client.serverVersion() expect(info.version).toBe('') - expect(info.edition).toBe('SELF_HOSTED') + expect(info.edition).toBe('COMMUNITY') }) it('throws when the host has no Dify on it', async () => { diff --git a/cli/src/version/enforce.test.ts b/cli/src/version/enforce.test.ts index 92c9d51022d..cca7b33b6e9 100644 --- a/cli/src/version/enforce.test.ts +++ b/cli/src/version/enforce.test.ts @@ -19,7 +19,7 @@ function fakeStore(fresh = false): CompatStore & { readonly marked: string[] } { } } -const server = (version: string): ServerVersionResponse => ({ version, edition: 'SELF_HOSTED' }) +const server = (version: string): ServerVersionResponse => ({ version, edition: 'COMMUNITY' }) describe('enforceDifyVersion', () => { it('throws version_skew (exit 6) when the server is too old, and never caches it', async () => { diff --git a/cli/src/version/nudge.test.ts b/cli/src/version/nudge.test.ts index 53038a28b46..04816689b0f 100644 --- a/cli/src/version/nudge.test.ts +++ b/cli/src/version/nudge.test.ts @@ -15,7 +15,7 @@ const fixedNow = () => NOW type Probe = (host: string) => Promise