mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
test: centralize enterprise config overrides (#40853)
This commit is contained in:
parent
c7f4422333
commit
20fdbcf36f
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 == {
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user