test: centralize enterprise config overrides (#40853)

This commit is contained in:
Asuka Minato 2026-08-18 12:01:01 +00:00 committed by GitHub
parent c7f4422333
commit 20fdbcf36f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 221 additions and 296 deletions

View File

@ -34,6 +34,14 @@ def _build_feature_flags():
class TestMemberInviteEmailApi:
@pytest.fixture(autouse=True)
def _member_config(self, config_overrides) -> None:
config_overrides(
RBAC_ENABLED=False,
CONSOLE_WEB_URL="https://console.example.com",
DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY,
)
@pytest.fixture(autouse=True)
def _mock_member_invite_lock(self):
with patch("controllers.console.workspace.members.redis_client.lock", return_value=nullcontext()):
@ -51,10 +59,7 @@ class TestMemberInviteEmailApi:
inviter = SimpleNamespace(email="Owner@Example.com", current_tenant=tenant, status="active")
with (
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.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
with app.test_request_context(
"/workspaces/current/members/invite-email",
@ -90,26 +95,25 @@ class TestMemberInviteEmailApi:
mock_invite_member,
mock_get_features,
app,
config_overrides,
):
"""When RBAC is enabled, any non-empty role string should be accepted."""
config_overrides(RBAC_ENABLED=True)
mock_get_features.return_value = _build_feature_flags()
mock_invite_member.return_value = "rbac-token"
tenant = SimpleNamespace(id="tenant-1", name="Test Tenant")
with patch("controllers.console.workspace.members.dify_config") as mock_config:
mock_config.RBAC_ENABLED = True
mock_config.CONSOLE_WEB_URL = "https://console.example.com"
with app.test_request_context(
"/workspaces/current/members/invite-email",
method="POST",
json={"emails": ["user@example.com"], "role": "rbac-role-id-abc", "language": "en-US"},
):
account = Account(name="tester", email="tester@example.com")
account._current_tenant = tenant
g._login_user = account
g._current_tenant = tenant
response, status_code = MemberInviteEmailApi().post()
with app.test_request_context(
"/workspaces/current/members/invite-email",
method="POST",
json={"emails": ["user@example.com"], "role": "rbac-role-id-abc", "language": "en-US"},
):
account = Account(name="tester", email="tester@example.com")
account._current_tenant = tenant
g._login_user = account
g._current_tenant = tenant
response, status_code = MemberInviteEmailApi().post()
assert status_code == 201
mock_invite_member.assert_called_once()
@ -131,20 +135,17 @@ class TestMemberInviteEmailApi:
tenant = SimpleNamespace(id="tenant-1", name="Test Tenant")
with patch("controllers.console.workspace.members.dify_config") as mock_config:
mock_config.RBAC_ENABLED = False
mock_config.CONSOLE_WEB_URL = "https://console.example.com"
with app.test_request_context(
"/workspaces/current/members/invite-email",
method="POST",
json={"emails": ["user@example.com"], "role": "invalid-role", "language": "en-US"},
):
account = Account(name="tester", email="tester@example.com")
account._current_tenant = tenant
g._login_user = account
g._current_tenant = tenant
with pytest.raises(InvalidMemberRoleError) as exc_info:
MemberInviteEmailApi().post()
with app.test_request_context(
"/workspaces/current/members/invite-email",
method="POST",
json={"emails": ["user@example.com"], "role": "invalid-role", "language": "en-US"},
):
account = Account(name="tester", email="tester@example.com")
account._current_tenant = tenant
g._login_user = account
g._current_tenant = tenant
with pytest.raises(InvalidMemberRoleError) as exc_info:
MemberInviteEmailApi().post()
assert exc_info.value.error_code == "invalid_role"
assert exc_info.value.data == {"code": "invalid_role", "message": "Invalid role.", "status": 400}
@ -164,19 +165,16 @@ class TestMemberInviteEmailApi:
tenant = SimpleNamespace(id="tenant-1", name="Test Tenant")
with patch("controllers.console.workspace.members.dify_config") as mock_config:
mock_config.RBAC_ENABLED = False
mock_config.CONSOLE_WEB_URL = "https://console.example.com"
with app.test_request_context(
"/workspaces/current/members/invite-email",
method="POST",
json={"emails": ["user@example.com"], "role": "owner", "language": "en-US"},
):
account = Account(name="tester", email="tester@example.com")
account._current_tenant = tenant
g._login_user = account
g._current_tenant = tenant
with pytest.raises(InvalidMemberRoleError) as exc_info:
MemberInviteEmailApi().post()
with app.test_request_context(
"/workspaces/current/members/invite-email",
method="POST",
json={"emails": ["user@example.com"], "role": "owner", "language": "en-US"},
):
account = Account(name="tester", email="tester@example.com")
account._current_tenant = tenant
g._login_user = account
g._current_tenant = tenant
with pytest.raises(InvalidMemberRoleError) as exc_info:
MemberInviteEmailApi().post()
assert exc_info.value.error_code == "invalid_role"

View File

@ -123,6 +123,13 @@ class TestMemberListApi:
class TestMemberInviteEmailApi:
@pytest.fixture(autouse=True)
def _invite_config(self, config_overrides) -> None:
config_overrides(
CONSOLE_WEB_URL="http://x",
DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY,
)
@pytest.fixture(autouse=True)
def _mock_member_invite_lock(self):
with patch("controllers.console.workspace.members.redis_client.lock", return_value=nullcontext()):
@ -151,8 +158,6 @@ class TestMemberInviteEmailApi:
patch(
"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.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
result, status = method(api, user)
@ -163,7 +168,8 @@ class TestMemberInviteEmailApi:
mock_invite.assert_called_once()
assert mock_invite.call_args.kwargs["email"] == "a@test.com"
def test_invite_limit_exceeded(self, app: Flask):
def test_invite_limit_exceeded(self, app: Flask, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
api = MemberInviteEmailApi()
method = unwrap(api.post)
@ -182,12 +188,12 @@ 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.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
):
with pytest.raises(WorkspaceMembersLimitExceeded):
method(api, user)
def test_invite_cloud_member_limit_exceeded(self, app: Flask):
def test_invite_cloud_member_limit_exceeded(self, app: Flask, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
api = MemberInviteEmailApi()
method = unwrap(api.post)
@ -208,7 +214,6 @@ 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.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
):
with pytest.raises(WorkspaceMembersLimitExceeded):
method(api, user)
@ -236,8 +241,6 @@ class TestMemberInviteEmailApi:
"controllers.console.workspace.members.RegisterService.invite_new_member",
side_effect=AccountAlreadyInTenantError(),
),
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
result, status = method(api, user)
@ -302,14 +305,13 @@ class TestMemberInviteEmailApi:
"controllers.console.workspace.members.RegisterService.invite_new_member",
side_effect=Exception("boom"),
),
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
patch("controllers.console.workspace.members.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
result, _ = method(api, user)
assert result["invitation_results"][0]["status"] == "failed"
def test_invite_seats_limit_exceeded(self, app: Flask):
def test_invite_seats_limit_exceeded(self, app: Flask, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
api = MemberInviteEmailApi()
method = unwrap(api.post)
@ -334,7 +336,6 @@ 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.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
):
with pytest.raises(SeatsLimitExceeded):
method(api, user)
@ -343,7 +344,8 @@ class TestMemberInviteEmailApi:
license_info.seats.is_available.assert_called_once_with(2)
mock_invite.assert_not_called()
def test_invite_existing_accounts_do_not_consume_seats(self, app: Flask):
def test_invite_existing_accounts_do_not_consume_seats(self, app: Flask, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
api = MemberInviteEmailApi()
method = unwrap(api.post)
@ -370,8 +372,6 @@ class TestMemberInviteEmailApi:
patch(
"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.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
):
result, status = method(api, user)
@ -381,7 +381,8 @@ class TestMemberInviteEmailApi:
license_info.seats.is_available.assert_not_called()
assert mock_invite.call_count == 2
def test_invite_mixed_accounts_with_available_seats(self, app: Flask):
def test_invite_mixed_accounts_with_available_seats(self, app: Flask, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
api = MemberInviteEmailApi()
method = unwrap(api.post)
@ -408,8 +409,6 @@ class TestMemberInviteEmailApi:
patch(
"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.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
):
result, status = method(api, user)
@ -444,8 +443,6 @@ class TestMemberInviteEmailApi:
return_value=license_info,
) 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.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
):
result, status = method(api, user)
@ -454,7 +451,8 @@ class TestMemberInviteEmailApi:
mock_get_license.assert_not_called()
license_info.seats.is_available.assert_not_called()
def test_invite_seats_error_is_reported_as_failed_result(self, app: Flask):
def test_invite_seats_error_is_reported_as_failed_result(self, app: Flask, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
api = MemberInviteEmailApi()
method = unwrap(api.post)
@ -482,8 +480,6 @@ class TestMemberInviteEmailApi:
"controllers.console.workspace.members.RegisterService.invite_new_member",
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.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE),
):
result, status = method(api, user)

View File

@ -37,9 +37,13 @@ def app():
return flask_app
def _enabled(enabled: bool):
deployment_edition = DeploymentEdition.ENTERPRISE if enabled else DeploymentEdition.COMMUNITY
return patch("controllers.console.workspace.rbac.dify_config.DEPLOYMENT_EDITION", deployment_edition)
@pytest.fixture(autouse=True)
def _rbac_config(config_overrides) -> None:
config_overrides(
DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE,
RBAC_ENABLED=True,
LOGIN_DISABLED=True,
)
def _account() -> Account:
@ -198,10 +202,10 @@ class TestPydanticModels:
class TestPaginationMapping:
def test_roles_get_returns_legacy_compatible_roles_when_rbac_disabled(self, app):
def test_roles_get_returns_legacy_compatible_roles_when_rbac_disabled(self, app, config_overrides):
config_overrides(RBAC_ENABLED=False)
with (
app.test_request_context("/workspaces/current/rbac/roles?page=1&limit=2&include_owner=1"),
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list,
):
@ -256,10 +260,10 @@ class TestPaginationMapping:
}
mock_list.assert_not_called()
def test_roles_get_filters_out_owner_when_include_owner_is_zero(self, app):
def test_roles_get_filters_out_owner_when_include_owner_is_zero(self, app, config_overrides):
config_overrides(RBAC_ENABLED=False)
with (
app.test_request_context("/workspaces/current/rbac/roles?include_owner=0"),
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
):
@ -268,10 +272,10 @@ class TestPaginationMapping:
names = [r["name"] for r in response["data"]]
assert "owner" not in names
def test_roles_get_keeps_owner_when_include_owner_is_one(self, app):
def test_roles_get_keeps_owner_when_include_owner_is_one(self, app, config_overrides):
config_overrides(RBAC_ENABLED=False)
with (
app.test_request_context("/workspaces/current/rbac/roles?include_owner=1"),
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
):
@ -283,10 +287,10 @@ class TestPaginationMapping:
names = [r["name"] for r in response["data"]]
assert "owner" in names
def test_roles_get_filters_out_owner_by_default(self, app):
def test_roles_get_filters_out_owner_by_default(self, app, config_overrides):
config_overrides(RBAC_ENABLED=False)
with (
app.test_request_context("/workspaces/current/rbac/roles"),
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
):
@ -298,7 +302,6 @@ class TestPaginationMapping:
def test_roles_get_forwards_outer_pagination_params(self, app):
with (
app.test_request_context("/workspaces/current/rbac/roles?page=2&limit=50&reverse=true&include_owner=1"),
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", True),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list,
patch("controllers.console.workspace.rbac._dump", return_value={}),
@ -324,7 +327,6 @@ class TestResourceAccessScopeBindings:
json={"scope": "all"},
),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")),
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", True),
patch(
"controllers.console.workspace.rbac.svc.RBACService.AppAccess.replace_whitelist",
return_value=rbac_mod.svc.ResourceWhitelist(),
@ -345,7 +347,6 @@ class TestResourceAccessScopeBindings:
json={"scope": "all"},
),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")),
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", True),
patch(
"controllers.console.workspace.rbac.svc.RBACService.DatasetAccess.replace_whitelist",
return_value=rbac_mod.svc.ResourceWhitelist(),
@ -364,7 +365,6 @@ class TestResourceAccessScopeBindings:
json={"scope": "specific"},
),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")),
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", True),
patch(
"controllers.console.workspace.rbac.svc.RBACService.DatasetAccess.replace_whitelist",
return_value=rbac_mod.svc.ResourceWhitelist(),
@ -454,7 +454,6 @@ class TestPaginationForwarding:
app.test_request_context(
"/workspaces/current/rbac/access-policies?resource_type=app&page=3&limit=25&reverse=false"
),
_enabled(True),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.AccessPolicies.list") as mock_list,
patch("controllers.console.workspace.rbac._dump", return_value={}),
@ -471,7 +470,6 @@ class TestPaginationForwarding:
def test_workspace_app_matrix_forwards_outer_pagination_params(self, app):
with (
app.test_request_context("/workspaces/current/rbac/workspace/apps/access-policy?page=4&limit=10"),
_enabled(True),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.WorkspaceAccess.app_matrix") as mock_list,
patch("controllers.console.workspace.rbac._dump", return_value={}),
@ -489,7 +487,6 @@ class TestPaginationForwarding:
app.test_request_context(
"/workspaces/current/rbac/workspace/datasets/access-policy?page=5&limit=15&reverse=true"
),
_enabled(True),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.WorkspaceAccess.dataset_matrix") as mock_list,
patch("controllers.console.workspace.rbac._dump", return_value={}),
@ -507,7 +504,6 @@ class TestAccessPolicyBindingLockUnlock:
def test_lock_forwards_binding_id(self, app):
with (
app.test_request_context("/workspaces/current/rbac/access-policy-bindings/binding-1/lock", method="PUT"),
_enabled(True),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.AccessPolicyBindings.lock") as mock_lock,
patch("controllers.console.workspace.rbac._dump", return_value={}),
@ -521,7 +517,6 @@ class TestAccessPolicyBindingLockUnlock:
def test_unlock_forwards_binding_id(self, app):
with (
app.test_request_context("/workspaces/current/rbac/access-policy-bindings/binding-1/unlock", method="PUT"),
_enabled(True),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.AccessPolicyBindings.unlock") as mock_unlock,
patch("controllers.console.workspace.rbac._dump", return_value={}),
@ -537,7 +532,6 @@ class TestRoleCopy:
def test_role_copy_forwards_path_id(self, app):
with (
app.test_request_context("/workspaces/current/rbac/roles/role-1/copy", method="POST", json={}),
_enabled(True),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.copy") as mock_copy,
patch("controllers.console.workspace.rbac._dump", return_value={}),
@ -555,8 +549,6 @@ class TestWorkspaceRbacGuards:
method="POST",
json={"name": "test_role", "permission_keys": []},
),
patch("libs.login.dify_config.LOGIN_DISABLED", True),
patch("controllers.console.wraps.dify_config.RBAC_ENABLED", True),
patch(
"controllers.common.wraps.current_account_with_tenant",
return_value=(_account(), "tenant-1"),
@ -576,8 +568,6 @@ class TestWorkspaceRbacGuards:
method="POST",
json={"name": "full_access", "resource_type": "app", "permission_keys": []},
),
patch("libs.login.dify_config.LOGIN_DISABLED", True),
patch("controllers.console.wraps.dify_config.RBAC_ENABLED", True),
patch(
"controllers.common.wraps.current_account_with_tenant",
return_value=(_account(), "tenant-1"),

View File

@ -37,6 +37,13 @@ def sqlite_session_factory(sqlite_engine: Engine) -> sessionmaker[Session]:
class TestRepositoryFactory:
"""Test cases for RepositoryFactory."""
@pytest.fixture(autouse=True)
def _repository_config(self, config_overrides) -> None:
config_overrides(
CORE_WORKFLOW_EXECUTION_REPOSITORY="unittest.mock.MagicMock",
CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY="unittest.mock.MagicMock",
)
def test_import_string_success(self):
"""Test successful class import."""
# Test importing a real class
@ -62,12 +69,8 @@ class TestRepositoryFactory:
import_string("invalidpath")
assert "doesn't look like a module path" in str(exc_info.value)
@patch("core.repositories.factory.dify_config")
def test_create_workflow_execution_repository_success(self, mock_config, sqlite_session_factory):
def test_create_workflow_execution_repository_success(self, sqlite_session_factory):
"""Test successful WorkflowExecutionRepository creation."""
# Setup mock configuration
mock_config.CORE_WORKFLOW_EXECUTION_REPOSITORY = "unittest.mock.MagicMock"
# Create non-database dependencies
mock_user = Account(name="Test Account", email="test@example.com")
app_id = "test-app-id"
@ -98,11 +101,9 @@ class TestRepositoryFactory:
)
assert result is mock_repository_instance
@patch("core.repositories.factory.dify_config")
def test_create_workflow_execution_repository_import_error(self, mock_config, sqlite_session_factory):
def test_create_workflow_execution_repository_import_error(self, sqlite_session_factory, config_overrides):
"""Test WorkflowExecutionRepository creation with import error."""
# Setup mock configuration with invalid class path
mock_config.CORE_WORKFLOW_EXECUTION_REPOSITORY = "invalid.module.InvalidClass"
config_overrides(CORE_WORKFLOW_EXECUTION_REPOSITORY="invalid.module.InvalidClass")
mock_user = Account(name="Test Account", email="test@example.com")
@ -116,12 +117,8 @@ class TestRepositoryFactory:
)
assert "Failed to create WorkflowExecutionRepository" in str(exc_info.value)
@patch("core.repositories.factory.dify_config")
def test_create_workflow_execution_repository_instantiation_error(self, mock_config, sqlite_session_factory):
def test_create_workflow_execution_repository_instantiation_error(self, sqlite_session_factory):
"""Test WorkflowExecutionRepository creation with instantiation error."""
# Setup mock configuration
mock_config.CORE_WORKFLOW_EXECUTION_REPOSITORY = "unittest.mock.MagicMock"
mock_user = Account(name="Test Account", email="test@example.com")
# Create a mock repository class that raises exception on instantiation
@ -140,12 +137,8 @@ class TestRepositoryFactory:
)
assert "Failed to create WorkflowExecutionRepository" in str(exc_info.value)
@patch("core.repositories.factory.dify_config")
def test_create_workflow_node_execution_repository_success(self, mock_config, sqlite_session_factory):
def test_create_workflow_node_execution_repository_success(self, sqlite_session_factory):
"""Test successful WorkflowNodeExecutionRepository creation."""
# Setup mock configuration
mock_config.CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY = "unittest.mock.MagicMock"
# Create non-database dependencies
mock_user = EndUser()
app_id = "test-app-id"
@ -176,11 +169,9 @@ class TestRepositoryFactory:
)
assert result is mock_repository_instance
@patch("core.repositories.factory.dify_config")
def test_create_workflow_node_execution_repository_import_error(self, mock_config, sqlite_session_factory):
def test_create_workflow_node_execution_repository_import_error(self, sqlite_session_factory, config_overrides):
"""Test WorkflowNodeExecutionRepository creation with import error."""
# Setup mock configuration with invalid class path
mock_config.CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY = "invalid.module.InvalidClass"
config_overrides(CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY="invalid.module.InvalidClass")
mock_user = EndUser()
@ -194,12 +185,8 @@ class TestRepositoryFactory:
)
assert "Failed to create WorkflowNodeExecutionRepository" in str(exc_info.value)
@patch("core.repositories.factory.dify_config")
def test_create_workflow_node_execution_repository_instantiation_error(self, mock_config, sqlite_session_factory):
def test_create_workflow_node_execution_repository_instantiation_error(self, sqlite_session_factory):
"""Test WorkflowNodeExecutionRepository creation with instantiation error."""
# Setup mock configuration
mock_config.CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY = "unittest.mock.MagicMock"
mock_user = EndUser()
# Create a mock repository class that raises exception on instantiation
@ -224,12 +211,8 @@ class TestRepositoryFactory:
error = RepositoryImportError(error_message)
assert str(error) == error_message
@patch("core.repositories.factory.dify_config")
def test_create_with_engine_instead_of_sessionmaker(self, mock_config, sqlite_engine: Engine):
def test_create_with_engine_instead_of_sessionmaker(self, sqlite_engine: Engine):
"""Test repository creation with Engine instead of sessionmaker."""
# Setup mock configuration
mock_config.CORE_WORKFLOW_EXECUTION_REPOSITORY = "unittest.mock.MagicMock"
# Pass the real Engine directly instead of wrapping it in sessionmaker
mock_user = Account(name="Test Account", email="test@example.com")
app_id = "test-app-id"

View File

@ -22,6 +22,11 @@ from services.enterprise.account_deletion_sync import (
)
@pytest.fixture(autouse=True)
def _enterprise_edition(config_overrides) -> None:
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
class TestQueueTask:
def test_queue_task_redis_error(self, caplog: pytest.LogCaptureFixture):
with patch("services.enterprise.account_deletion_sync.redis_client") as mock_redis:
@ -53,36 +58,24 @@ class TestSyncWorkspaceMemberRemoval:
workspace_id = str(uuid4())
member_id = str(uuid4())
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_workspace_member_removal(workspace_id=workspace_id, member_id=member_id, source="removed")
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")
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_non_enterprise_edition(self, mock_queue_task, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
result = sync_workspace_member_removal(workspace_id=str(uuid4()), member_id=str(uuid4()), source="test_source")
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.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = sync_workspace_member_removal(
workspace_id=str(uuid4()), member_id=str(uuid4()), source="test_source"
)
assert result is True
mock_queue_task.assert_not_called()
assert result is True
mock_queue_task.assert_not_called()
def test_sync_workspace_member_removal_queue_failure(self, mock_queue_task):
mock_queue_task.return_value = False
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_workspace_member_removal(workspace_id=str(uuid4()), member_id=str(uuid4()), source="test_source")
result = sync_workspace_member_removal(
workspace_id=str(uuid4()), member_id=str(uuid4()), source="test_source"
)
assert result is False
assert result is False
@pytest.mark.parametrize("sqlite_session", [(TenantAccountJoin,)], indirect=True)
@ -93,14 +86,15 @@ class TestSyncAccountDeletion:
mock_queue.return_value = True
yield mock_queue
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.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
def test_sync_account_deletion_non_enterprise_edition(
self, mock_queue_task, sqlite_session: Session, config_overrides
) -> None:
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
result = sync_account_deletion(account_id=str(uuid4()), source="account_deleted", session=sqlite_session)
result = sync_account_deletion(account_id=str(uuid4()), source="account_deleted", session=sqlite_session)
assert result is True
mock_queue_task.assert_not_called()
assert result is True
mock_queue_task.assert_not_called()
def test_sync_account_deletion_multiple_workspaces(self, sqlite_session: Session, mock_queue_task) -> None:
account_id = str(uuid4())
@ -111,25 +105,19 @@ class TestSyncAccountDeletion:
sqlite_session.add(join)
sqlite_session.commit()
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_account_deletion(account_id=account_id, source="account_deleted", session=sqlite_session)
result = sync_account_deletion(account_id=account_id, source="account_deleted", session=sqlite_session)
assert result is True
assert mock_queue_task.call_count == 3
assert result is True
assert mock_queue_task.call_count == 3
queued_workspace_ids = {call.kwargs["workspace_id"] for call in mock_queue_task.call_args_list}
assert queued_workspace_ids == set(tenant_ids)
queued_workspace_ids = {call.kwargs["workspace_id"] for call in mock_queue_task.call_args_list}
assert queued_workspace_ids == set(tenant_ids)
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_account_deletion(account_id=str(uuid4()), source="account_deleted", session=sqlite_session)
result = sync_account_deletion(account_id=str(uuid4()), source="account_deleted", session=sqlite_session)
assert result is True
mock_queue_task.assert_not_called()
assert result is True
mock_queue_task.assert_not_called()
def test_sync_account_deletion_partial_failure(self, sqlite_session: Session, mock_queue_task) -> None:
account_id = str(uuid4())
@ -146,13 +134,10 @@ class TestSyncAccountDeletion:
mock_queue_task.side_effect = queue_side_effect
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_account_deletion(account_id=account_id, source="account_deleted", session=sqlite_session)
result = sync_account_deletion(account_id=account_id, source="account_deleted", session=sqlite_session)
assert result is False
assert mock_queue_task.call_count == 3
assert result is False
assert mock_queue_task.call_count == 3
def test_sync_account_deletion_all_failures(self, sqlite_session: Session, mock_queue_task) -> None:
account_id = str(uuid4())
@ -164,10 +149,7 @@ class TestSyncAccountDeletion:
mock_queue_task.return_value = False
with patch("services.enterprise.account_deletion_sync.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
result = sync_account_deletion(account_id=account_id, source="account_deleted", session=sqlite_session)
result = sync_account_deletion(account_id=account_id, source="account_deleted", session=sqlite_session)
assert result is False
mock_queue_task.assert_called_once()
assert result is False
mock_queue_task.assert_called_once()

View File

@ -269,13 +269,13 @@ class TestJoinDefaultWorkspace:
class TestTryJoinDefaultWorkspace:
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.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
@pytest.fixture(autouse=True)
def _enterprise_edition(self, config_overrides) -> None:
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
def test_try_join_default_workspace_non_enterprise_edition_noop(self, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
with patch("services.enterprise.enterprise_service.EnterpriseService.join_default_workspace") as mock_join:
try_join_default_workspace("11111111-1111-1111-1111-111111111111")
mock_join.assert_not_called()
@ -283,11 +283,7 @@ class TestTryJoinDefaultWorkspace:
def test_try_join_default_workspace_successful_join_does_not_raise(self):
account_id = "11111111-1111-1111-1111-111111111111"
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
with patch("services.enterprise.enterprise_service.EnterpriseService.join_default_workspace") as mock_join:
mock_join.return_value = DefaultWorkspaceJoinResult(
workspace_id="22222222-2222-2222-2222-222222222222",
joined=True,
@ -302,11 +298,7 @@ class TestTryJoinDefaultWorkspace:
def test_try_join_default_workspace_skipped_join_does_not_raise(self):
account_id = "11111111-1111-1111-1111-111111111111"
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
with patch("services.enterprise.enterprise_service.EnterpriseService.join_default_workspace") as mock_join:
mock_join.return_value = DefaultWorkspaceJoinResult(
workspace_id="",
joined=False,
@ -321,11 +313,7 @@ class TestTryJoinDefaultWorkspace:
def test_try_join_default_workspace_api_failure_soft_fails(self):
account_id = "11111111-1111-1111-1111-111111111111"
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
with patch("services.enterprise.enterprise_service.EnterpriseService.join_default_workspace") as mock_join:
mock_join.side_effect = Exception("network failure")
# Should not raise
@ -334,11 +322,8 @@ class TestTryJoinDefaultWorkspace:
mock_join.assert_called_once_with(account_id=account_id)
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
# Should not raise even though UUID parsing fails inside join_default_workspace
try_join_default_workspace("not-a-uuid")
# Should not raise even though UUID parsing fails inside join_default_workspace
try_join_default_workspace("not-a-uuid")
# ---------------------------------------------------------------------------
@ -351,19 +336,19 @@ _EE_SVC = "services.enterprise.enterprise_service"
class TestGetCachedLicenseStatus:
"""Tests for EnterpriseService.get_cached_license_status."""
def test_returns_none_outside_enterprise_edition(self):
with patch(f"{_EE_SVC}.dify_config") as mock_config:
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
@pytest.fixture(autouse=True)
def _enterprise_edition(self, config_overrides) -> None:
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
assert EnterpriseService.get_cached_license_status() is None
def test_returns_none_outside_enterprise_edition(self, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
assert EnterpriseService.get_cached_license_status() is None
def test_cache_hit_returns_license_status_enum(self):
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = b"active"
result = EnterpriseService.get_cached_license_status()
@ -374,11 +359,9 @@ class TestGetCachedLicenseStatus:
def test_cache_miss_fetches_api_and_caches_valid_status(self):
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = None
mock_get_info.return_value = {"License": {"status": "active"}}
@ -391,11 +374,9 @@ class TestGetCachedLicenseStatus:
def test_cache_miss_fetches_api_and_caches_invalid_status_with_short_ttl(self):
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = None
mock_get_info.return_value = {"License": {"status": "expired"}}
@ -408,11 +389,9 @@ class TestGetCachedLicenseStatus:
def test_redis_read_failure_falls_through_to_api(self):
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.side_effect = ConnectionError("redis down")
mock_get_info.return_value = {"License": {"status": "active"}}
@ -423,11 +402,9 @@ class TestGetCachedLicenseStatus:
def test_redis_write_failure_still_returns_status(self):
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.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"}}
@ -438,11 +415,9 @@ class TestGetCachedLicenseStatus:
def test_api_failure_returns_none(self):
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = None
mock_get_info.side_effect = Exception("network failure")
@ -450,11 +425,9 @@ class TestGetCachedLicenseStatus:
def test_api_returns_no_license_info(self):
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.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
mock_redis.get.return_value = None
mock_get_info.return_value = {} # no "License" key

View File

@ -536,6 +536,10 @@ class TestWorkspaceAccess:
@pytest.mark.parametrize("sqlite_session", [(TenantAccountJoin,)], indirect=True)
class TestMyPermissions:
@pytest.fixture(autouse=True)
def _rbac_enabled(self, config_overrides) -> None:
config_overrides(RBAC_ENABLED=True)
def test_resource_snapshot_maps_defaults_and_overrides(self, sqlite_session: Session):
snapshot = svc.ResourcePermissionSnapshot(
default_permission_keys=["app.acl.view_layout"],
@ -559,8 +563,7 @@ class TestMyPermissions:
"dataset": {"default_permission_keys": [], "overrides": []},
}
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", True):
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", session=sqlite_session)
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", session=sqlite_session)
call = _call_args(mock_send)
assert call.method == "GET"
@ -612,13 +615,14 @@ class TestMyPermissions:
app_keys: list[str],
dataset_keys: list[str],
sqlite_session: Session,
config_overrides,
):
config_overrides(RBAC_ENABLED=False)
sqlite_session.add(
TenantAccountJoin(tenant_id="tenant-1", account_id="acct-1", role=svc.TenantAccountRole(role))
)
sqlite_session.commit()
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", False):
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", session=sqlite_session)
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", session=sqlite_session)
mock_send.assert_not_called()
assert out.workspace.permission_keys == workspace_keys
@ -654,13 +658,14 @@ class TestMyPermissions:
role: str,
expected_snippet_keys: set[str],
sqlite_session: Session,
config_overrides,
):
config_overrides(RBAC_ENABLED=False)
sqlite_session.add(
TenantAccountJoin(tenant_id="tenant-1", account_id="acct-1", role=svc.TenantAccountRole(role))
)
sqlite_session.commit()
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", False):
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", session=sqlite_session)
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", session=sqlite_session)
actual_snippet_keys = {
permission_key for permission_key in out.workspace.permission_keys if permission_key.startswith("snippets.")
@ -669,9 +674,11 @@ class TestMyPermissions:
mock_send.assert_not_called()
assert actual_snippet_keys == expected_snippet_keys
def test_get_returns_empty_when_role_missing_and_rbac_disabled(self, mock_send: MagicMock, sqlite_session: Session):
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", False):
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", session=sqlite_session)
def test_get_returns_empty_when_role_missing_and_rbac_disabled(
self, mock_send: MagicMock, sqlite_session: Session, config_overrides
):
config_overrides(RBAC_ENABLED=False)
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", session=sqlite_session)
mock_send.assert_not_called()
assert out.workspace.permission_keys == []
@ -688,8 +695,7 @@ class TestMyPermissions:
"dataset": {"default_permission_keys": [], "overrides": []},
}
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", True):
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", app_id="app-1", session=sqlite_session)
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", app_id="app-1", session=sqlite_session)
call = _call_args(mock_send)
assert call.method == "GET"
@ -700,6 +706,10 @@ class TestMyPermissions:
@pytest.mark.parametrize("sqlite_session", [(TenantAccountJoin,)], indirect=True)
class TestMemberRoles:
@pytest.fixture(autouse=True)
def _rbac_enabled(self, config_overrides) -> None:
config_overrides(RBAC_ENABLED=True)
def test_get(self, mock_send: MagicMock, sqlite_session: Session):
mock_send.return_value = {
"account_id": "acct-2",
@ -711,8 +721,7 @@ class TestMemberRoles:
}
],
}
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", True):
out = svc.RBACService.MemberRoles.get("tenant-1", "acct-1", "acct-2", session=sqlite_session)
out = svc.RBACService.MemberRoles.get("tenant-1", "acct-1", "acct-2", session=sqlite_session)
call = _call_args(mock_send)
assert call.method == "GET"
assert call.endpoint == "/rbac/members/rbac-roles"
@ -720,14 +729,16 @@ class TestMemberRoles:
assert out.account_id == "acct-2"
assert out.roles[0].name == "Member"
def test_get_legacy_role_includes_permission_keys(self, mock_send: MagicMock, sqlite_session: Session):
def test_get_legacy_role_includes_permission_keys(
self, mock_send: MagicMock, sqlite_session: Session, config_overrides
):
config_overrides(RBAC_ENABLED=False)
sqlite_session.add(
TenantAccountJoin(tenant_id="tenant-1", account_id="acct-2", role=svc.TenantAccountRole.EDITOR)
)
sqlite_session.commit()
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", False):
out = svc.RBACService.MemberRoles.get("tenant-1", "acct-1", "acct-2", session=sqlite_session)
out = svc.RBACService.MemberRoles.get("tenant-1", "acct-1", "acct-2", session=sqlite_session)
mock_send.assert_not_called()
assert out.account_id == "acct-2"
@ -748,31 +759,32 @@ class TestMemberRoles:
def test_replace(self, mock_send: MagicMock, sqlite_session: Session):
mock_send.return_value = {"account_id": "acct-2", "roles": []}
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", True):
svc.RBACService.MemberRoles.replace(
"tenant-1",
"acct-1",
"acct-2",
role_ids=["workspace.owner", "workspace.editor"],
session=sqlite_session,
)
svc.RBACService.MemberRoles.replace(
"tenant-1",
"acct-1",
"acct-2",
role_ids=["workspace.owner", "workspace.editor"],
session=sqlite_session,
)
call = _call_args(mock_send)
assert call.method == "PUT"
assert call.endpoint == "/rbac/members/rbac-roles"
assert call.params == {"account_id": "acct-2"}
assert call.json == {"role_ids": ["workspace.owner", "workspace.editor"]}
def test_replace_commits_legacy_join_role_when_rbac_disabled(self, mock_send: MagicMock, sqlite_session: Session):
def test_replace_commits_legacy_join_role_when_rbac_disabled(
self, mock_send: MagicMock, sqlite_session: Session, config_overrides
):
config_overrides(RBAC_ENABLED=False)
target_join = TenantAccountJoin(tenant_id="tenant-1", account_id="acct-2", role=svc.TenantAccountRole.NORMAL)
sqlite_session.add(target_join)
sqlite_session.commit()
target_join_id = target_join.id
engine = sqlite_session.get_bind()
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", False):
out = svc.RBACService.MemberRoles.replace(
"tenant-1", "acct-1", "acct-2", role_ids=["editor"], session=sqlite_session
)
out = svc.RBACService.MemberRoles.replace(
"tenant-1", "acct-1", "acct-2", role_ids=["editor"], session=sqlite_session
)
mock_send.assert_not_called()
# Closing the writer rolls back any uncommitted update and prevents its identity map
@ -789,17 +801,17 @@ class TestMemberRoles:
assert "app.acl.preview" in out.roles[0].permission_keys
def test_replace_legacy_owner_demotes_current_owner_when_rbac_disabled(
self, mock_send: MagicMock, sqlite_session: Session
self, mock_send: MagicMock, sqlite_session: Session, config_overrides
):
config_overrides(RBAC_ENABLED=False)
target_join = TenantAccountJoin(tenant_id="tenant-1", account_id="acct-2", role=svc.TenantAccountRole.NORMAL)
owner_join = TenantAccountJoin(tenant_id="tenant-1", account_id="acct-owner", role=svc.TenantAccountRole.OWNER)
sqlite_session.add_all([target_join, owner_join])
sqlite_session.commit()
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", False):
out = svc.RBACService.MemberRoles.replace(
"tenant-1", "acct-1", "acct-2", role_ids=["owner"], session=sqlite_session
)
out = svc.RBACService.MemberRoles.replace(
"tenant-1", "acct-1", "acct-2", role_ids=["owner"], session=sqlite_session
)
mock_send.assert_not_called()
persisted_joins = {
@ -837,6 +849,10 @@ class TestMemberRoles:
@pytest.mark.parametrize("sqlite_session", [(TenantAccountJoin,)], indirect=True)
class TestResourcePermissions:
@pytest.fixture(autouse=True)
def _rbac_enabled(self, config_overrides) -> None:
config_overrides(RBAC_ENABLED=True)
def test_app_permissions_batch_get(self, mock_send: MagicMock, sqlite_session: Session):
mock_send.return_value = {
"data": [
@ -848,10 +864,7 @@ class TestResourcePermissions:
]
}
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", True):
out = svc.RBACService.AppPermissions.batch_get(
"tenant-1", "acct-1", ["app-1", "app-2"], session=sqlite_session
)
out = svc.RBACService.AppPermissions.batch_get("tenant-1", "acct-1", ["app-1", "app-2"], session=sqlite_session)
call = _call_args(mock_send)
assert call.method == "POST"
@ -863,16 +876,14 @@ class TestResourcePermissions:
}
def test_app_permissions_batch_get_uses_legacy_role_permissions_when_rbac_disabled(
self, mock_send: MagicMock, sqlite_session: Session
self, mock_send: MagicMock, sqlite_session: Session, config_overrides
):
config_overrides(RBAC_ENABLED=False)
sqlite_session.add(
TenantAccountJoin(tenant_id="tenant-1", account_id="acct-1", role=svc.TenantAccountRole.EDITOR)
)
sqlite_session.commit()
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", False):
out = svc.RBACService.AppPermissions.batch_get(
"tenant-1", "acct-1", ["app-1", "app-2"], session=sqlite_session
)
out = svc.RBACService.AppPermissions.batch_get("tenant-1", "acct-1", ["app-1", "app-2"], session=sqlite_session)
mock_send.assert_not_called()
assert out == {
@ -889,10 +900,9 @@ class TestResourcePermissions:
]
}
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", True):
out = svc.RBACService.DatasetPermissions.batch_get(
"tenant-1", "acct-1", ["ds-1", "ds-2"], session=sqlite_session
)
out = svc.RBACService.DatasetPermissions.batch_get(
"tenant-1", "acct-1", ["ds-1", "ds-2"], session=sqlite_session
)
call = _call_args(mock_send)
assert call.method == "POST"
@ -904,8 +914,9 @@ class TestResourcePermissions:
}
def test_dataset_permissions_batch_get_uses_legacy_role_permissions_when_rbac_disabled(
self, mock_send: MagicMock, sqlite_session: Session
self, mock_send: MagicMock, sqlite_session: Session, config_overrides
):
config_overrides(RBAC_ENABLED=False)
sqlite_session.add(
TenantAccountJoin(
tenant_id="tenant-1",
@ -914,10 +925,9 @@ class TestResourcePermissions:
)
)
sqlite_session.commit()
with patch(f"{MODULE}.dify_config.RBAC_ENABLED", False):
out = svc.RBACService.DatasetPermissions.batch_get(
"tenant-1", "acct-1", ["ds-1", "ds-2"], session=sqlite_session
)
out = svc.RBACService.DatasetPermissions.batch_get(
"tenant-1", "acct-1", ["ds-1", "ds-2"], session=sqlite_session
)
mock_send.assert_not_called()
assert out == {

View File

@ -1,5 +1,7 @@
from unittest.mock import patch
import pytest
from enums import DeploymentEdition
from services.workflow.queue_dispatcher import (
ProfessionalQueueDispatcher,
@ -35,10 +37,12 @@ class TestDispatchers:
class TestQueueDispatcherManager:
@pytest.fixture(autouse=True)
def _cloud_edition(self, config_overrides) -> None:
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
def test_cloud_edition_professional_plan(self, mock_config, mock_billing):
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
def test_cloud_edition_professional_plan(self, mock_billing):
mock_billing.get_info.return_value = {"subscription": {"plan": "professional"}}
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
@ -46,9 +50,7 @@ class TestQueueDispatcherManager:
assert isinstance(dispatcher, ProfessionalQueueDispatcher)
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
def test_cloud_edition_team_plan(self, mock_config, mock_billing):
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
def test_cloud_edition_team_plan(self, mock_billing):
mock_billing.get_info.return_value = {"subscription": {"plan": "team"}}
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
@ -56,9 +58,7 @@ class TestQueueDispatcherManager:
assert isinstance(dispatcher, TeamQueueDispatcher)
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
def test_cloud_edition_sandbox_plan(self, mock_config, mock_billing):
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
def test_cloud_edition_sandbox_plan(self, mock_billing):
mock_billing.get_info.return_value = {"subscription": {"plan": "sandbox"}}
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
@ -66,9 +66,7 @@ class TestQueueDispatcherManager:
assert isinstance(dispatcher, SandboxQueueDispatcher)
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
def test_cloud_edition_unknown_plan_defaults_to_sandbox(self, mock_config, mock_billing):
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
def test_cloud_edition_unknown_plan_defaults_to_sandbox(self, mock_billing):
mock_billing.get_info.return_value = {"subscription": {"plan": "enterprise"}}
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
@ -76,27 +74,22 @@ class TestQueueDispatcherManager:
assert isinstance(dispatcher, SandboxQueueDispatcher)
@patch("services.workflow.queue_dispatcher.BillingService")
@patch("services.workflow.queue_dispatcher.dify_config")
def test_cloud_edition_billing_failure_defaults_to_sandbox(self, mock_config, mock_billing):
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
def test_cloud_edition_billing_failure_defaults_to_sandbox(self, mock_billing):
mock_billing.get_info.side_effect = Exception("billing unavailable")
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
assert isinstance(dispatcher, SandboxQueueDispatcher)
@patch("services.workflow.queue_dispatcher.dify_config")
def test_non_cloud_edition_defaults_to_team(self, mock_config):
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
def test_non_cloud_edition_defaults_to_team(self, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")
assert isinstance(dispatcher, TeamQueueDispatcher)
@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.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
def test_missing_subscription_key_defaults_to_sandbox(self, mock_billing):
mock_billing.get_info.return_value = {}
dispatcher = QueueDispatcherManager.get_dispatcher("tenant-1")