|
|
|
@@ -15,7 +15,7 @@ from controllers.console.auth.error import (
|
|
|
|
|
NotOwnerError,
|
|
|
|
|
OwnerTransferLimitError,
|
|
|
|
|
)
|
|
|
|
|
from controllers.console.error import EmailSendIpLimitError, WorkspaceMembersLimitExceeded
|
|
|
|
|
from controllers.console.error import EmailSendIpLimitError, SeatsLimitExceeded, WorkspaceMembersLimitExceeded
|
|
|
|
|
from controllers.console.workspace.error import InvalidMemberRoleError
|
|
|
|
|
from controllers.console.workspace.members import (
|
|
|
|
|
DatasetOperatorMemberListApi,
|
|
|
|
@@ -26,9 +26,10 @@ from controllers.console.workspace.members import (
|
|
|
|
|
OwnerTransfer,
|
|
|
|
|
OwnerTransferCheckApi,
|
|
|
|
|
SendOwnerTransferEmailApi,
|
|
|
|
|
_count_new_member_invites,
|
|
|
|
|
)
|
|
|
|
|
from libs.external_api import ExternalApi
|
|
|
|
|
from services.errors.account import AccountAlreadyInTenantError
|
|
|
|
|
from services.errors.account import AccountAlreadyInTenantError, SeatsLimitExceededError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMemberListApi:
|
|
|
|
@@ -142,7 +143,7 @@ class TestMemberInviteEmailApi:
|
|
|
|
|
with (
|
|
|
|
|
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) as mock_count,
|
|
|
|
|
patch("controllers.console.workspace.members._count_new_member_invites", return_value=(1, 1)) as mock_count,
|
|
|
|
|
patch(
|
|
|
|
|
"controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"
|
|
|
|
|
) as mock_invite,
|
|
|
|
@@ -178,7 +179,7 @@ class TestMemberInviteEmailApi:
|
|
|
|
|
with (
|
|
|
|
|
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),
|
|
|
|
|
patch("controllers.console.workspace.members._count_new_member_invites", return_value=(1, 1)),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", True),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
|
|
|
|
):
|
|
|
|
@@ -205,7 +206,7 @@ class TestMemberInviteEmailApi:
|
|
|
|
|
with (
|
|
|
|
|
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=2),
|
|
|
|
|
patch("controllers.console.workspace.members._count_new_member_invites", return_value=(2, 2)),
|
|
|
|
|
patch("controllers.console.workspace.members._count_current_members", return_value=9),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", True),
|
|
|
|
@@ -232,7 +233,7 @@ class TestMemberInviteEmailApi:
|
|
|
|
|
with (
|
|
|
|
|
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=0),
|
|
|
|
|
patch("controllers.console.workspace.members._count_new_member_invites", return_value=(0, 0)),
|
|
|
|
|
patch(
|
|
|
|
|
"controllers.console.workspace.members.RegisterService.invite_new_member",
|
|
|
|
|
side_effect=AccountAlreadyInTenantError(),
|
|
|
|
@@ -300,7 +301,7 @@ class TestMemberInviteEmailApi:
|
|
|
|
|
with (
|
|
|
|
|
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),
|
|
|
|
|
patch("controllers.console.workspace.members._count_new_member_invites", return_value=(1, 1)),
|
|
|
|
|
patch(
|
|
|
|
|
"controllers.console.workspace.members.RegisterService.invite_new_member",
|
|
|
|
|
side_effect=Exception("boom"),
|
|
|
|
@@ -313,6 +314,221 @@ class TestMemberInviteEmailApi:
|
|
|
|
|
|
|
|
|
|
assert result["invitation_results"][0]["status"] == "failed"
|
|
|
|
|
|
|
|
|
|
def test_invite_seats_limit_exceeded(self, app: Flask):
|
|
|
|
|
api = MemberInviteEmailApi()
|
|
|
|
|
method = unwrap(api.post)
|
|
|
|
|
|
|
|
|
|
tenant = MagicMock(id="t1")
|
|
|
|
|
user = MagicMock(current_tenant=tenant)
|
|
|
|
|
features = MagicMock()
|
|
|
|
|
features.billing.enabled = False
|
|
|
|
|
features.workspace_members.enabled = False
|
|
|
|
|
system_features = MagicMock()
|
|
|
|
|
system_features.license.seats.is_available.return_value = False
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
"emails": ["a@test.com", "b@test.com"],
|
|
|
|
|
"role": "normal",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
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=(2, 2)),
|
|
|
|
|
patch(
|
|
|
|
|
"controllers.console.workspace.members.FeatureService.get_system_features",
|
|
|
|
|
return_value=system_features,
|
|
|
|
|
) as mock_get_system_features,
|
|
|
|
|
patch("controllers.console.workspace.members.RegisterService.invite_new_member") as mock_invite,
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", True),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
|
|
|
|
):
|
|
|
|
|
with pytest.raises(SeatsLimitExceeded):
|
|
|
|
|
method(api, user)
|
|
|
|
|
|
|
|
|
|
mock_get_system_features.assert_called_once_with(is_authenticated=True)
|
|
|
|
|
system_features.license.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):
|
|
|
|
|
api = MemberInviteEmailApi()
|
|
|
|
|
method = unwrap(api.post)
|
|
|
|
|
|
|
|
|
|
tenant = MagicMock(id="t1")
|
|
|
|
|
user = MagicMock(current_tenant=tenant)
|
|
|
|
|
features = MagicMock()
|
|
|
|
|
features.billing.enabled = False
|
|
|
|
|
features.workspace_members.enabled = False
|
|
|
|
|
system_features = MagicMock()
|
|
|
|
|
system_features.license.seats.is_available.return_value = False
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
"emails": ["a@test.com", "b@test.com"],
|
|
|
|
|
"role": "normal",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
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=(2, 0)),
|
|
|
|
|
patch(
|
|
|
|
|
"controllers.console.workspace.members.FeatureService.get_system_features",
|
|
|
|
|
return_value=system_features,
|
|
|
|
|
) as mock_get_system_features,
|
|
|
|
|
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.ENTERPRISE_ENABLED", True),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
|
|
|
|
):
|
|
|
|
|
result, status = method(api, user)
|
|
|
|
|
|
|
|
|
|
assert status == 201
|
|
|
|
|
assert len(result["invitation_results"]) == 2
|
|
|
|
|
mock_get_system_features.assert_not_called()
|
|
|
|
|
system_features.license.seats.is_available.assert_not_called()
|
|
|
|
|
assert mock_invite.call_count == 2
|
|
|
|
|
|
|
|
|
|
def test_invite_mixed_accounts_with_available_seats(self, app: Flask):
|
|
|
|
|
api = MemberInviteEmailApi()
|
|
|
|
|
method = unwrap(api.post)
|
|
|
|
|
|
|
|
|
|
tenant = MagicMock(id="t1")
|
|
|
|
|
user = MagicMock(current_tenant=tenant)
|
|
|
|
|
features = MagicMock()
|
|
|
|
|
features.billing.enabled = False
|
|
|
|
|
features.workspace_members.enabled = False
|
|
|
|
|
system_features = MagicMock()
|
|
|
|
|
system_features.license.seats.is_available.return_value = True
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
"emails": ["a@test.com", "b@test.com"],
|
|
|
|
|
"role": "normal",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
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=(2, 1)),
|
|
|
|
|
patch(
|
|
|
|
|
"controllers.console.workspace.members.FeatureService.get_system_features",
|
|
|
|
|
return_value=system_features,
|
|
|
|
|
) as mock_get_system_features,
|
|
|
|
|
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.ENTERPRISE_ENABLED", True),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
|
|
|
|
):
|
|
|
|
|
result, status = method(api, user)
|
|
|
|
|
|
|
|
|
|
assert status == 201
|
|
|
|
|
assert len(result["invitation_results"]) == 2
|
|
|
|
|
mock_get_system_features.assert_called_once_with(is_authenticated=True)
|
|
|
|
|
system_features.license.seats.is_available.assert_called_once_with(1)
|
|
|
|
|
assert mock_invite.call_count == 2
|
|
|
|
|
|
|
|
|
|
def test_invite_skips_seats_limit_when_enterprise_disabled(self, app: Flask):
|
|
|
|
|
api = MemberInviteEmailApi()
|
|
|
|
|
method = unwrap(api.post)
|
|
|
|
|
|
|
|
|
|
tenant = MagicMock(id="t1")
|
|
|
|
|
user = MagicMock(current_tenant=tenant)
|
|
|
|
|
features = MagicMock()
|
|
|
|
|
features.billing.enabled = False
|
|
|
|
|
features.workspace_members.enabled = False
|
|
|
|
|
system_features = MagicMock()
|
|
|
|
|
system_features.license.seats.is_available.return_value = False
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
"emails": ["a@test.com"],
|
|
|
|
|
"role": "normal",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
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.FeatureService.get_system_features",
|
|
|
|
|
return_value=system_features,
|
|
|
|
|
) as mock_get_system_features,
|
|
|
|
|
patch("controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
|
|
|
|
):
|
|
|
|
|
result, status = method(api, user)
|
|
|
|
|
|
|
|
|
|
assert status == 201
|
|
|
|
|
assert result["invitation_results"][0]["status"] == "success"
|
|
|
|
|
mock_get_system_features.assert_not_called()
|
|
|
|
|
system_features.license.seats.is_available.assert_not_called()
|
|
|
|
|
|
|
|
|
|
def test_invite_seats_error_is_reported_as_failed_result(self, app: Flask):
|
|
|
|
|
api = MemberInviteEmailApi()
|
|
|
|
|
method = unwrap(api.post)
|
|
|
|
|
|
|
|
|
|
tenant = MagicMock(id="t1")
|
|
|
|
|
user = MagicMock(current_tenant=tenant)
|
|
|
|
|
features = MagicMock()
|
|
|
|
|
features.billing.enabled = False
|
|
|
|
|
features.workspace_members.enabled = False
|
|
|
|
|
system_features = MagicMock()
|
|
|
|
|
system_features.license.seats.is_available.return_value = True
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
"emails": ["a@test.com"],
|
|
|
|
|
"role": "normal",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
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.FeatureService.get_system_features",
|
|
|
|
|
return_value=system_features,
|
|
|
|
|
),
|
|
|
|
|
patch(
|
|
|
|
|
"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.ENTERPRISE_ENABLED", True),
|
|
|
|
|
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
|
|
|
|
):
|
|
|
|
|
result, status = method(api, user)
|
|
|
|
|
|
|
|
|
|
assert status == 201
|
|
|
|
|
assert result["invitation_results"][0]["status"] == "failed"
|
|
|
|
|
assert result["invitation_results"][0]["message"] == "licensed seats limit exceeded"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_count_new_member_invites():
|
|
|
|
|
new_account = None
|
|
|
|
|
existing_account_not_in_tenant = SimpleNamespace(id="account-2")
|
|
|
|
|
existing_account_in_tenant = SimpleNamespace(id="account-3")
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
patch(
|
|
|
|
|
"controllers.console.workspace.members.AccountService.get_account_by_email_with_case_fallback",
|
|
|
|
|
side_effect=[new_account, existing_account_not_in_tenant, existing_account_in_tenant],
|
|
|
|
|
) as mock_get_account,
|
|
|
|
|
patch("controllers.console.workspace.members.db.session") as mock_session,
|
|
|
|
|
):
|
|
|
|
|
mock_session.scalar.side_effect = [None, "join-id"]
|
|
|
|
|
result = _count_new_member_invites(
|
|
|
|
|
"tenant-1",
|
|
|
|
|
["new@test.com", "existing@test.com", "member@test.com"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result == (2, 1)
|
|
|
|
|
assert mock_get_account.call_count == 3
|
|
|
|
|
assert mock_session.scalar.call_count == 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMemberUpdateRoleApi:
|
|
|
|
|
def test_update_invalid_role(self, app: Flask):
|
|
|
|
|