test: centralize app and account config overrides (#40860)

This commit is contained in:
Asuka Minato authored and GitHub committed 2026-08-25 06:19:35 +00:00
1 parent bc845ea748
commit f3bb73b671
11 files changed
+273 -256

No files matched your search

@@ -3,6 +3,7 @@ from __future__ import annotations
import builtins
import json
import sys
from collections.abc import Callable
from datetime import datetime
from importlib import util
from pathlib import Path
@@ -17,7 +18,6 @@ from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session
from werkzeug.datastructures import MultiDict
from configs import dify_config
from models.model import App, AppMode, IconType
from models.workflow import Workflow, WorkflowType
@@ -594,7 +594,10 @@ def test_app_list_uses_injected_session_for_draft_workflows(
assert response["data"][0]["permission_keys"] == ["app.acl.edit"]
def test_app_create_api_attaches_permission_keys(app, app_module, unbound_session: Session):
def test_app_create_api_attaches_permission_keys(
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
):
config_overrides(RBAC_ENABLED=True)
method = app_module.AppListApi.post
while hasattr(method, "__wrapped__"):
method = method.__wrapped__
@@ -611,7 +614,6 @@ def test_app_create_api_attaches_permission_keys(app, app_module, unbound_sessio
with app.test_request_context("/apps", method="POST", json={}):
with pytest.MonkeyPatch.context() as monkeypatch:
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
app_module.console_ns.payload = {
"name": "Created App",
"description": "Summary",
@@ -658,7 +660,10 @@ def test_app_create_api_attaches_permission_keys(app, app_module, unbound_sessio
initialize_rbac_task.delay.assert_called_once_with("tenant-1", "acct-1", app_id="app-new")
def test_app_list_api_attaches_permission_keys(app, app_module, sqlite_session: Session):
def test_app_list_api_attaches_permission_keys(
app, app_module, sqlite_session: Session, config_overrides: Callable[..., None]
):
config_overrides(RBAC_ENABLED=True)
method = app_module.AppListApi.get
while hasattr(method, "__wrapped__"):
method = method.__wrapped__
@@ -678,7 +683,6 @@ def test_app_list_api_attaches_permission_keys(app, app_module, sqlite_session:
with app.test_request_context("/apps"):
with pytest.MonkeyPatch.context() as monkeypatch:
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
monkeypatch.setattr(
app_module.AppService,
"get_paginate_apps",
@@ -719,7 +723,13 @@ def test_app_list_api_attaches_permission_keys(app, app_module, sqlite_session:
assert resp["data"][0]["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
def test_recent_app_list_api_returns_only_home_card_fields(app, app_module, unbound_session: Session):
def test_recent_app_list_api_returns_only_home_card_fields(
app,
app_module,
unbound_session: Session,
config_overrides: Callable[..., None],
):
config_overrides(RBAC_ENABLED=False)
method = app_module.RecentAppListApi.get
while hasattr(method, "__wrapped__"):
method = method.__wrapped__
@@ -739,7 +749,6 @@ def test_recent_app_list_api_returns_only_home_card_fields(app, app_module, unbo
with app.test_request_context("/apps/recent?limit=8"):
with pytest.MonkeyPatch.context() as monkeypatch:
monkeypatch.setattr(dify_config, "RBAC_ENABLED", False)
monkeypatch.setattr(app_module.AppService, "get_recent_apps", get_recent_apps)
monkeypatch.setattr(
app_module.enterprise_rbac_service.RBACService.MyPermissions,
@@ -797,7 +806,10 @@ def test_recent_app_response_rejects_non_home_app_modes(app_module, mode: str) -
)
def test_recent_app_list_api_applies_rbac_visibility_filter(app, app_module, unbound_session: Session):
def test_recent_app_list_api_applies_rbac_visibility_filter(
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
):
config_overrides(RBAC_ENABLED=True)
method = app_module.RecentAppListApi.get
while hasattr(method, "__wrapped__"):
method = method.__wrapped__
@@ -805,7 +817,6 @@ def test_recent_app_list_api_applies_rbac_visibility_filter(app, app_module, unb
get_recent_apps = MagicMock(return_value=[])
with app.test_request_context("/apps/recent"):
with pytest.MonkeyPatch.context() as monkeypatch:
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
monkeypatch.setattr(app_module.AppService, "get_recent_apps", get_recent_apps)
monkeypatch.setattr(
app_module.enterprise_rbac_service.RBACService.MyPermissions,
@@ -835,8 +846,9 @@ def test_recent_app_list_api_applies_rbac_visibility_filter(app, app_module, unb
def test_app_list_api_limits_to_apps_created_by_current_user_without_view_permission(
app, app_module, unbound_session: Session
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
):
config_overrides(RBAC_ENABLED=True)
method = app_module.AppListApi.get
while hasattr(method, "__wrapped__"):
method = method.__wrapped__
@@ -847,7 +859,6 @@ def test_app_list_api_limits_to_apps_created_by_current_user_without_view_permis
with app.test_request_context("/apps"):
with pytest.MonkeyPatch.context() as monkeypatch:
monkeypatch.setattr(app_module.AppService, "get_paginate_apps", get_paginate_apps)
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
monkeypatch.setattr(
app_module.enterprise_rbac_service.RBACService.MyPermissions,
"get",
@@ -879,8 +890,9 @@ def test_app_list_api_limits_to_apps_created_by_current_user_without_view_permis
def test_app_list_api_limits_to_preview_overrides_without_manage_own_permission(
app, app_module, unbound_session: Session
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
):
config_overrides(RBAC_ENABLED=True)
method = app_module.AppListApi.get
while hasattr(method, "__wrapped__"):
method = method.__wrapped__
@@ -891,7 +903,6 @@ def test_app_list_api_limits_to_preview_overrides_without_manage_own_permission(
with app.test_request_context("/apps"):
with pytest.MonkeyPatch.context() as monkeypatch:
monkeypatch.setattr(app_module.AppService, "get_paginate_apps", get_paginate_apps)
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
monkeypatch.setattr(
app_module.enterprise_rbac_service.RBACService.MyPermissions,
"get",
@@ -936,8 +947,9 @@ def test_app_list_api_limits_to_preview_overrides_without_manage_own_permission(
def test_app_list_api_returns_no_apps_without_workspace_or_resource_view_permission(
app, app_module, unbound_session: Session
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
):
config_overrides(RBAC_ENABLED=True)
method = app_module.AppListApi.get
while hasattr(method, "__wrapped__"):
method = method.__wrapped__
@@ -948,7 +960,6 @@ def test_app_list_api_returns_no_apps_without_workspace_or_resource_view_permiss
with app.test_request_context("/apps"):
with pytest.MonkeyPatch.context() as monkeypatch:
monkeypatch.setattr(app_module.AppService, "get_paginate_apps", get_paginate_apps)
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
monkeypatch.setattr(
app_module.enterprise_rbac_service.RBACService.MyPermissions,
"get",
@@ -973,7 +984,10 @@ def test_app_list_api_returns_no_apps_without_workspace_or_resource_view_permiss
assert params.is_created_by_me is None
def test_app_detail_api_attaches_current_user_permission_keys(app, app_module, unbound_session: Session):
def test_app_detail_api_attaches_current_user_permission_keys(
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
):
config_overrides(RBAC_ENABLED=True)
method = app_module.AppApi.get
while hasattr(method, "__wrapped__"):
method = method.__wrapped__
@@ -990,7 +1004,6 @@ def test_app_detail_api_attaches_current_user_permission_keys(app, app_module, u
with app.test_request_context("/apps/app-1"):
with pytest.MonkeyPatch.context() as monkeypatch:
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
get_app = MagicMock(return_value=app_obj)
monkeypatch.setattr(app_module, "AppService", lambda: SimpleNamespace(get_app=get_app))
monkeypatch.setattr(
@@ -1039,7 +1052,14 @@ def test_app_detail_api_attaches_current_user_permission_keys(app, app_module, u
]
def test_app_copy_api_attaches_permission_keys(app, app_module, sqlite_session: Session, sqlite_engine: Engine):
def test_app_copy_api_attaches_permission_keys(
app,
app_module,
sqlite_session: Session,
sqlite_engine: Engine,
config_overrides: Callable[..., None],
):
config_overrides(RBAC_ENABLED=True)
method = app_module.AppCopyApi.post
while hasattr(method, "__wrapped__"):
method = method.__wrapped__
@@ -1063,7 +1083,6 @@ def test_app_copy_api_attaches_permission_keys(app, app_module, sqlite_session:
with app.test_request_context("/apps/app-original/copy", method="POST", json={}):
with pytest.MonkeyPatch.context() as monkeypatch:
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
monkeypatch.setattr(
app_module,
"AppDslService",
@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Callable
from unittest.mock import MagicMock, patch
import pytest
@@ -24,6 +25,11 @@ from services.errors.account import (
)
@pytest.fixture(autouse=True)
def _cloud_edition(config_overrides: Callable[..., None]) -> None:
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
class TestEmailRegisterSendEmailApi:
@patch("controllers.console.auth.email_register.AccountService.get_account_by_email_with_case_fallback")
@patch("controllers.console.auth.email_register.AccountService.send_email_register_email")
@@ -50,8 +56,6 @@ class TestEmailRegisterSendEmailApi:
is_allow_register=True,
)
with (
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(
@@ -138,7 +142,6 @@ class TestEmailRegisterCheckApi:
is_allow_register=True,
)
with (
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(
@@ -212,7 +215,6 @@ class TestEmailRegisterResetApi:
is_allow_register=True,
)
with (
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(
@@ -265,7 +267,6 @@ class TestEmailRegisterResetApi:
is_allow_register=True,
)
with (
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(
@@ -323,7 +324,6 @@ class TestEmailRegisterResetApi:
is_allow_register=True,
)
with (
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(
@@ -9,6 +9,7 @@ This module tests the email code login mechanism including:
"""
import base64
from collections.abc import Callable
from unittest.mock import ANY, MagicMock, patch
import pytest
@@ -56,6 +57,11 @@ from services.turnstile_service import TurnstileChallengeRejectedError, Turnstil
TEST_TOKEN = "00000000-0000-4000-8000-000000000001"
@pytest.fixture(autouse=True)
def _default_edition(config_overrides: Callable[..., None]) -> None:
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
def encode_code(code: str) -> str:
"""Helper to encode verification code as Base64 for testing."""
return base64.b64encode(code.encode("utf-8")).decode()
@@ -197,7 +203,14 @@ class TestEmailCodeLoginSendEmailApi:
@patch("controllers.console.wraps.db")
@patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
@patch("controllers.console.auth.login.TurnstileService.verify")
def test_send_email_code_ip_rate_limited(self, mock_verify, mock_is_ip_limit, mock_db, app: Flask):
def test_send_email_code_ip_rate_limited(
self,
mock_verify,
mock_is_ip_limit,
mock_db,
app: Flask,
config_overrides: Callable[..., None],
):
"""
Test email code sending blocked by IP rate limit.
@@ -205,12 +218,11 @@ class TestEmailCodeLoginSendEmailApi:
- EmailSendIpLimitError is raised when IP limit exceeded
- Prevents spam and abuse
"""
# Arrange
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
mock_is_ip_limit.return_value = True
# Act & Assert
with (
patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
app.test_request_context("/email-code-login", method="POST", json={"email": "test@example.com"}),
):
with pytest.raises(EmailSendIpLimitError):
@@ -232,11 +244,12 @@ class TestEmailCodeLoginSendEmailApi:
mock_db,
app: Flask,
mock_account,
config_overrides: Callable[..., None],
):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
mock_get_user.return_value = mock_account
with (
patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
app.test_request_context(
"/email-code-login",
method="POST",
@@ -268,9 +281,10 @@ class TestEmailCodeLoginSendEmailApi:
app: Flask,
service_error: Exception,
http_error: type[Exception],
config_overrides: Callable[..., None],
):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
with (
patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
patch("controllers.console.auth.login.TurnstileService.verify", side_effect=service_error),
app.test_request_context(
"/email-code-login",
@@ -297,11 +311,12 @@ class TestEmailCodeLoginSendEmailApi:
mock_db,
app: Flask,
mock_account,
config_overrides: Callable[..., None],
):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
mock_get_user.return_value = mock_account
with (
patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
app.test_request_context("/email-code-login", method="POST", json={"email": "test@example.com"}),
):
response = EmailCodeLoginSendEmailApi().post()
@@ -10,6 +10,7 @@ This module tests the core authentication endpoints including:
import base64
import logging
from collections.abc import Callable
from unittest.mock import ANY, MagicMock, Mock, patch
import pytest
@@ -47,6 +48,11 @@ from services.errors.account import (
TEST_TOKEN = "00000000-0000-4000-8000-000000000001"
@pytest.fixture(autouse=True)
def _login_config(config_overrides: Callable[..., None]) -> None:
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
def encode_password(password: str) -> str:
"""Helper to encode password as Base64 for testing."""
return base64.b64encode(password.encode("utf-8")).decode()
@@ -98,7 +104,6 @@ class TestLoginApi:
return token_pair
@patch("controllers.console.wraps.db")
@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")
@@ -149,7 +154,6 @@ class TestLoginApi:
assert response.json["result"] == "success"
@patch("controllers.console.wraps.db")
@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")
@@ -202,7 +206,6 @@ class TestLoginApi:
assert response.json["result"] == "success"
@patch("controllers.console.wraps.db")
@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(
@@ -235,10 +238,14 @@ class TestLoginApi:
assert warn_records[0].args[1] == LoginFailureReason.LOGIN_RATE_LIMITED
@patch("controllers.console.wraps.db")
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
@patch("controllers.console.auth.login.BillingService.get_email_freeze_type")
def test_login_fails_when_account_frozen(
self, mock_is_frozen, mock_db, app: Flask, caplog: pytest.LogCaptureFixture
self,
mock_get_freeze_type,
mock_db,
app: Flask,
caplog: pytest.LogCaptureFixture,
config_overrides: Callable[..., None],
):
"""
Test login rejection for frozen accounts.
@@ -248,7 +255,8 @@ class TestLoginApi:
- AccountInFreezeError is raised for frozen accounts
"""
# Arrange
mock_is_frozen.return_value = "freeze"
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
mock_get_freeze_type.return_value = "freeze"
# Act & Assert
with app.test_request_context(
@@ -266,9 +274,15 @@ class TestLoginApi:
assert warn_records[0].args[1] == LoginFailureReason.ACCOUNT_IN_FREEZE
@patch("controllers.console.wraps.db")
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
@patch("controllers.console.auth.login.BillingService.get_email_freeze_type")
def test_login_fails_when_email_domain_is_suspended(self, mock_get_freeze_type, mock_db, app: Flask):
def test_login_fails_when_email_domain_is_suspended(
self,
mock_get_freeze_type,
mock_db,
app: Flask,
config_overrides: Callable[..., None],
):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
mock_get_freeze_type.return_value = "email_domain_suspended"
with app.test_request_context(
@@ -376,7 +390,6 @@ class TestLoginApi:
ResetPasswordSendEmailApi().post()
@patch("controllers.console.wraps.db")
@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")
@@ -423,7 +436,6 @@ class TestLoginApi:
assert warn_records[0].args[1] == LoginFailureReason.INVALID_CREDENTIALS
@patch("controllers.console.wraps.db")
@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")
@@ -460,7 +472,6 @@ class TestLoginApi:
assert warn_records[0].args[1] == LoginFailureReason.ACCOUNT_BANNED
@patch("controllers.console.wraps.db")
@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")
@@ -504,7 +515,6 @@ class TestLoginApi:
login_api.post()
@patch("controllers.console.wraps.db")
@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):
@@ -534,7 +544,6 @@ class TestLoginApi:
login_api.post()
@patch("controllers.console.wraps.db")
@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")
@@ -1,3 +1,4 @@
from collections.abc import Callable
from types import SimpleNamespace
from typing import override
from unittest.mock import MagicMock, patch
@@ -52,6 +53,15 @@ def reset_setup_required_cache():
_is_setup_completed.reset_success()
@pytest.fixture(autouse=True)
def _wraps_config(config_overrides: Callable[..., None]) -> None:
config_overrides(
RBAC_ENABLED=True,
DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY,
INIT_PASSWORD="",
)
class MockUser(UserMixin):
"""Simple User class for testing."""
@@ -399,7 +409,6 @@ class TestRbacPermissionRequired:
return "ok"
with (
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-1")),
patch("controllers.common.wraps._extract_resource_id", return_value="app-123") as mock_extract,
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=False) as mock_owned,
@@ -427,7 +436,6 @@ class TestRbacPermissionRequired:
return "ok"
with (
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-2")),
patch("controllers.common.wraps._extract_resource_id") as mock_extract,
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=False) as mock_owned,
@@ -455,7 +463,6 @@ class TestRbacPermissionRequired:
return "ok"
with (
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-3")),
patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check,
):
@@ -477,7 +484,6 @@ class TestRbacPermissionRequired:
return "ok"
with (
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-4")),
patch("controllers.common.wraps._extract_resource_id", return_value="app-123"),
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=True) as mock_owned,
@@ -496,7 +502,6 @@ class TestRbacPermissionRequired:
return "ok"
with (
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-5")),
patch("controllers.common.wraps._extract_resource_id", return_value="dataset-123"),
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=True) as mock_owned,
@@ -610,8 +615,7 @@ class TestRbacPermissionRequired:
def protected_view():
return "ok"
with patch("controllers.console.wraps.dify_config.RBAC_ENABLED", True):
assert protected_view() == "ok"
assert protected_view() == "ok"
class TestModelValidationInjection:
@@ -668,7 +672,7 @@ class TestModelValidationInjection:
class TestEditionChecks:
"""Test edition-specific decorators"""
def test_only_edition_cloud_allows_cloud_edition(self):
def test_only_edition_cloud_allows_cloud_edition(self, config_overrides: Callable[..., None]):
"""Test cloud edition decorator allows CLOUD edition"""
# Arrange
@@ -676,9 +680,8 @@ class TestEditionChecks:
def cloud_view():
return "cloud_success"
# Act
with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD):
result = cloud_view()
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
result = cloud_view()
# Assert
assert result == "cloud_success"
@@ -694,12 +697,11 @@ class TestEditionChecks:
# Act & Assert
with app.test_request_context():
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
with pytest.raises(HTTPException) as exc_info:
cloud_view()
assert exc_info.value.code == 404
def test_only_edition_enterprise_allows_enterprise_edition(self):
def test_only_edition_enterprise_allows_enterprise_edition(self, config_overrides: Callable[..., None]):
"""Test enterprise edition decorator allows the ENTERPRISE edition."""
# Arrange
@@ -707,9 +709,8 @@ class TestEditionChecks:
def enterprise_view():
return "enterprise_success"
# Act
with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE):
result = enterprise_view()
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
result = enterprise_view()
# Assert
assert result == "enterprise_success"
@@ -722,9 +723,7 @@ class TestEditionChecks:
def self_hosted_view():
return "self_hosted_success"
# Act
with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
result = self_hosted_view()
result = self_hosted_view()
# Assert
assert result == "self_hosted_success"
@@ -805,8 +804,9 @@ class TestBillingResourceLimits:
assert result == "member_added"
get_features.assert_called_once_with("tenant123", exclude_vector_space=True)
def test_should_load_vector_space_from_dedicated_quota_api(self):
def test_should_load_vector_space_from_dedicated_quota_api(self, config_overrides: Callable[..., None]):
"""Test vector-space limit checks avoid loading the full feature payload."""
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
# Arrange
mock_vector_space = MagicMock()
mock_vector_space.limit = 10
@@ -821,7 +821,6 @@ class TestBillingResourceLimits:
"controllers.console.wraps.current_account_with_tenant", return_value=(MockUser("test_user"), "tenant123")
):
with (
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,
@@ -984,8 +983,9 @@ class TestRateLimiting:
class TestCloudUtmRecord:
"""Test cloud UTM recording decorator."""
def test_should_record_utm_for_cloud_edition_and_cookie(self):
def test_should_record_utm_for_cloud_edition_and_cookie(self, config_overrides: Callable[..., None]):
"""Test Cloud UTM recording without loading tenant features."""
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
app = create_app_with_login()
@cloud_utm_record
@@ -994,7 +994,6 @@ class TestCloudUtmRecord:
with app.test_request_context("/", headers={"Cookie": "utm_info={}"}):
with (
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,
@@ -1015,7 +1014,6 @@ class TestCloudUtmRecord:
with app.test_request_context("/", headers={"Cookie": "utm_info={}"}):
with (
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,
@@ -1047,10 +1045,7 @@ class TestSystemSetup:
return "admin_success"
# Act
with (
patch("controllers.console.wraps.db.session", sqlite_session),
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
with patch("controllers.console.wraps.db.session", sqlite_session):
result = admin_view()
# Assert
@@ -1064,10 +1059,7 @@ class TestSystemSetup:
def admin_view():
return "admin_success"
with (
patch("controllers.console.wraps.db.session", sqlite_session),
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
with patch("controllers.console.wraps.db.session", sqlite_session):
assert admin_view() == "admin_success"
sqlite_session.delete(setup)
sqlite_session.commit()
@@ -1084,27 +1076,23 @@ class TestSystemSetup:
with (
patch("controllers.console.wraps.db.session", sqlite_session),
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("controllers.console.wraps.dify_config.INIT_PASSWORD", ""),
):
with pytest.raises(NotSetupError):
admin_view()
self._complete_setup(sqlite_session)
assert admin_view() == "admin_success"
def test_should_raise_not_init_validate_error_with_init_password(self, sqlite_session: Session):
def test_should_raise_not_init_validate_error_with_init_password(
self, sqlite_session: Session, config_overrides: Callable[..., None]
):
"""Test NotInitValidateError when INIT_PASSWORD is set but setup not complete"""
@setup_required
def admin_view():
return "admin_success"
# Act & Assert
with (
patch("controllers.console.wraps.db.session", sqlite_session),
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("controllers.console.wraps.dify_config.INIT_PASSWORD", "some_password"),
):
config_overrides(INIT_PASSWORD="some_password")
with patch("controllers.console.wraps.db.session", sqlite_session):
with pytest.raises(NotInitValidateError):
admin_view()
@@ -1116,11 +1104,7 @@ class TestSystemSetup:
return "admin_success"
# Act & Assert
with (
patch("controllers.console.wraps.db.session", sqlite_session),
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("controllers.console.wraps.dify_config.INIT_PASSWORD", ""),
):
with patch("controllers.console.wraps.db.session", sqlite_session):
with pytest.raises(NotSetupError):
admin_view()
@@ -1,4 +1,5 @@
import uuid
from collections.abc import Callable
from unittest.mock import patch
import pytest
@@ -24,6 +25,11 @@ from models.model import App
from services.enterprise.enterprise_service import WebAppAccessMode
@pytest.fixture(autouse=True)
def _rbac_config(config_overrides: Callable[..., None]) -> None:
config_overrides(RBAC_ENABLED=True)
def _data(**kwargs) -> AuthData:
defaults: dict = {"token_type": TokenType.OAUTH_ACCOUNT, "token_hash": "hash", "scopes": frozenset({Scope.FULL})}
defaults.update(kwargs)
@@ -88,18 +94,15 @@ def test_check_rbac_noop_when_no_requirement():
mock_enforce.assert_not_called()
def test_check_rbac_noop_when_rbac_disabled():
with (
patch("controllers.openapi.auth.verify.dify_config.RBAC_ENABLED", False),
patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce,
):
def test_check_rbac_noop_when_rbac_disabled(config_overrides: Callable[..., None]):
config_overrides(RBAC_ENABLED=False)
with patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce:
check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="account"))
mock_enforce.assert_not_called()
def test_check_rbac_skips_end_user_caller():
with (
patch("controllers.openapi.auth.verify.dify_config.RBAC_ENABLED", True),
patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce,
):
check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="end_user"))
@@ -107,9 +110,8 @@ def test_check_rbac_skips_end_user_caller():
def test_check_rbac_raises_when_context_missing():
with patch("controllers.openapi.auth.verify.dify_config.RBAC_ENABLED", True):
with pytest.raises(Forbidden, match="rbac context missing"):
check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="account", account_id=None, tenant=None))
with pytest.raises(Forbidden, match="rbac context missing"):
check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="account", account_id=None, tenant=None))
def test_check_rbac_enforces_for_account_caller():
@@ -124,7 +126,6 @@ def test_check_rbac_enforces_for_account_caller():
path_params={"app_id": "app-1"},
)
with (
patch("controllers.openapi.auth.verify.dify_config.RBAC_ENABLED", True),
patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce,
):
check_rbac_permission(data)