mirror of
https://github.com/langgenius/dify.git
synced 2026-06-08 09:27:39 +08:00
Merge branch 'main' into 4-27-app-deploy
This commit is contained in:
@ -67,7 +67,7 @@ class TestActivateCheckApi:
|
||||
assert response["data"]["email"] == "invitee@example.com"
|
||||
|
||||
@patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback")
|
||||
def test_check_invalid_invitation_token(self, mock_get_invitation, app):
|
||||
def test_check_invalid_invitation_token(self, mock_get_invitation, app: Flask):
|
||||
"""
|
||||
Test checking invalid invitation token.
|
||||
|
||||
@ -227,7 +227,7 @@ class TestActivateApi:
|
||||
mock_db.session.commit.assert_called_once()
|
||||
|
||||
@patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback")
|
||||
def test_activation_with_invalid_token(self, mock_get_invitation, app):
|
||||
def test_activation_with_invalid_token(self, mock_get_invitation, app: Flask):
|
||||
"""
|
||||
Test account activation with invalid token.
|
||||
|
||||
|
||||
@ -140,7 +140,7 @@ class TestEmailCodeLoginSendEmailApi:
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
|
||||
def test_send_email_code_ip_rate_limited(self, mock_is_ip_limit, mock_db, app):
|
||||
def test_send_email_code_ip_rate_limited(self, mock_is_ip_limit, mock_db, app: Flask):
|
||||
"""
|
||||
Test email code sending blocked by IP rate limit.
|
||||
|
||||
@ -160,7 +160,7 @@ class TestEmailCodeLoginSendEmailApi:
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
|
||||
@patch("controllers.console.auth.login.AccountService.get_user_through_email")
|
||||
def test_send_email_code_frozen_account(self, mock_get_user, mock_is_ip_limit, mock_db, app):
|
||||
def test_send_email_code_frozen_account(self, mock_get_user, mock_is_ip_limit, mock_db, app: Flask):
|
||||
"""
|
||||
Test email code sending to frozen account.
|
||||
|
||||
@ -353,7 +353,7 @@ class TestEmailCodeLoginApi:
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
|
||||
def test_email_code_login_invalid_token(self, mock_get_data, mock_db, app):
|
||||
def test_email_code_login_invalid_token(self, mock_get_data, mock_db, app: Flask):
|
||||
"""
|
||||
Test email code login with invalid token.
|
||||
|
||||
@ -375,7 +375,7 @@ class TestEmailCodeLoginApi:
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
|
||||
def test_email_code_login_email_mismatch(self, mock_get_data, mock_db, app):
|
||||
def test_email_code_login_email_mismatch(self, mock_get_data, mock_db, app: Flask):
|
||||
"""
|
||||
Test email code login with mismatched email.
|
||||
|
||||
@ -397,7 +397,7 @@ class TestEmailCodeLoginApi:
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
|
||||
def test_email_code_login_wrong_code(self, mock_get_data, mock_db, app):
|
||||
def test_email_code_login_wrong_code(self, mock_get_data, mock_db, app: Flask):
|
||||
"""
|
||||
Test email code login with incorrect code.
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ This module tests the core authentication endpoints including:
|
||||
"""
|
||||
|
||||
import base64
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
@ -52,12 +52,12 @@ class TestLoginApi:
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def api(self, app):
|
||||
def api(self, app: Flask):
|
||||
"""Create Flask-RESTX API instance."""
|
||||
return Api(app)
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app, api):
|
||||
def client(self, app: Flask, api: Api):
|
||||
"""Create test client."""
|
||||
api.add_resource(LoginApi, "/login")
|
||||
return app.test_client()
|
||||
@ -97,7 +97,7 @@ class TestLoginApi:
|
||||
mock_get_invitation,
|
||||
mock_is_rate_limit,
|
||||
mock_db,
|
||||
app,
|
||||
app: Flask,
|
||||
mock_account,
|
||||
mock_token_pair,
|
||||
):
|
||||
@ -141,14 +141,14 @@ class TestLoginApi:
|
||||
@patch("controllers.console.auth.login.AccountService.reset_login_error_rate_limit")
|
||||
def test_successful_login_with_valid_invitation(
|
||||
self,
|
||||
mock_reset_rate_limit,
|
||||
mock_reset_rate_limit: Mock,
|
||||
mock_login,
|
||||
mock_get_tenants,
|
||||
mock_authenticate,
|
||||
mock_get_invitation,
|
||||
mock_is_rate_limit,
|
||||
mock_db,
|
||||
app,
|
||||
app: Flask,
|
||||
mock_account,
|
||||
mock_token_pair,
|
||||
):
|
||||
@ -188,7 +188,7 @@ class TestLoginApi:
|
||||
@patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
def test_login_fails_when_rate_limited(self, mock_get_invitation, mock_is_rate_limit, mock_db, app):
|
||||
def test_login_fails_when_rate_limited(self, mock_get_invitation, mock_is_rate_limit, mock_db, app: Flask):
|
||||
"""
|
||||
Test login rejection when rate limit is exceeded.
|
||||
|
||||
@ -216,7 +216,7 @@ class TestLoginApi:
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", True)
|
||||
@patch("controllers.console.auth.login.BillingService.is_email_in_freeze")
|
||||
def test_login_fails_when_account_frozen(self, mock_is_frozen, mock_db, app):
|
||||
def test_login_fails_when_account_frozen(self, mock_is_frozen, mock_db, app: Flask):
|
||||
"""
|
||||
Test login rejection for frozen accounts.
|
||||
|
||||
@ -253,7 +253,7 @@ class TestLoginApi:
|
||||
mock_get_invitation,
|
||||
mock_is_rate_limit,
|
||||
mock_db,
|
||||
app,
|
||||
app: Flask,
|
||||
):
|
||||
"""
|
||||
Test login failure with invalid credentials.
|
||||
@ -290,7 +290,7 @@ class TestLoginApi:
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
@patch("controllers.console.auth.login.AccountService.authenticate")
|
||||
def test_login_fails_for_banned_account(
|
||||
self, mock_authenticate, mock_get_invitation, mock_is_rate_limit, mock_db, app
|
||||
self, mock_authenticate, mock_get_invitation, mock_is_rate_limit, mock_db, app: Flask
|
||||
):
|
||||
"""
|
||||
Test login rejection for banned accounts.
|
||||
@ -328,14 +328,14 @@ class TestLoginApi:
|
||||
@patch("controllers.console.auth.login.FeatureService.get_system_features")
|
||||
def test_login_fails_when_no_workspace_and_limit_exceeded(
|
||||
self,
|
||||
mock_get_features,
|
||||
mock_get_tenants,
|
||||
mock_authenticate,
|
||||
mock_get_invitation,
|
||||
mock_is_rate_limit,
|
||||
mock_db,
|
||||
app,
|
||||
mock_account,
|
||||
mock_get_features: MagicMock,
|
||||
mock_get_tenants: MagicMock,
|
||||
mock_authenticate: MagicMock,
|
||||
mock_get_invitation: MagicMock,
|
||||
mock_is_rate_limit: MagicMock,
|
||||
mock_db: MagicMock,
|
||||
app: Flask,
|
||||
mock_account: MagicMock,
|
||||
):
|
||||
"""
|
||||
Test login failure when user has no workspace and workspace limit exceeded.
|
||||
@ -367,7 +367,7 @@ class TestLoginApi:
|
||||
@patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
def test_login_invitation_email_mismatch(self, mock_get_invitation, mock_is_rate_limit, mock_db, app):
|
||||
def test_login_invitation_email_mismatch(self, mock_get_invitation, mock_is_rate_limit, mock_db, app: Flask):
|
||||
"""
|
||||
Test login failure when invitation email doesn't match login email.
|
||||
|
||||
@ -491,7 +491,7 @@ class TestLogoutApi:
|
||||
@patch("controllers.console.auth.login.AccountService.logout")
|
||||
@patch("controllers.console.auth.login.flask_login.logout_user")
|
||||
def test_successful_logout(
|
||||
self, mock_logout_user, mock_service_logout, mock_current_account, mock_db, app, mock_account
|
||||
self, mock_logout_user, mock_service_logout, mock_current_account, mock_db, app: Flask, mock_account
|
||||
):
|
||||
"""
|
||||
Test successful logout flow.
|
||||
@ -518,7 +518,7 @@ class TestLogoutApi:
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.current_account_with_tenant")
|
||||
@patch("controllers.console.auth.login.flask_login")
|
||||
def test_logout_anonymous_user(self, mock_flask_login, mock_current_account, mock_db, app):
|
||||
def test_logout_anonymous_user(self, mock_flask_login, mock_current_account, mock_db, app: Flask):
|
||||
"""
|
||||
Test logout for anonymous (not logged in) user.
|
||||
|
||||
|
||||
@ -28,12 +28,12 @@ class TestRefreshTokenApi:
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def api(self, app):
|
||||
def api(self, app: Flask):
|
||||
"""Create Flask-RESTX API instance."""
|
||||
return Api(app)
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app, api):
|
||||
def client(self, app: Flask, api: Api):
|
||||
"""Create test client."""
|
||||
api.add_resource(RefreshTokenApi, "/refresh-token")
|
||||
return app.test_client()
|
||||
|
||||
@ -49,7 +49,7 @@ class TestPartnerTenants:
|
||||
mock_csrf.return_value = None
|
||||
yield {"db": mock_db, "csrf": mock_csrf}
|
||||
|
||||
def test_put_success(self, app, mock_account, mock_billing_service, mock_decorators):
|
||||
def test_put_success(self, app: Flask, mock_account, mock_billing_service, mock_decorators):
|
||||
"""Test successful partner tenants bindings sync."""
|
||||
# Arrange
|
||||
partner_key_encoded = base64.b64encode(b"partner-key-123").decode("utf-8")
|
||||
@ -79,7 +79,7 @@ class TestPartnerTenants:
|
||||
mock_account.id, "partner-key-123", click_id
|
||||
)
|
||||
|
||||
def test_put_invalid_partner_key_base64(self, app, mock_account, mock_billing_service, mock_decorators):
|
||||
def test_put_invalid_partner_key_base64(self, app: Flask, mock_account, mock_billing_service, mock_decorators):
|
||||
"""Test that invalid base64 partner_key raises BadRequest."""
|
||||
# Arrange
|
||||
invalid_partner_key = "invalid-base64-!@#$"
|
||||
@ -104,7 +104,7 @@ class TestPartnerTenants:
|
||||
resource.put(invalid_partner_key)
|
||||
assert "Invalid partner_key" in str(exc_info.value)
|
||||
|
||||
def test_put_missing_click_id(self, app, mock_account, mock_billing_service, mock_decorators):
|
||||
def test_put_missing_click_id(self, app: Flask, mock_account, mock_billing_service, mock_decorators):
|
||||
"""Test that missing click_id raises BadRequest."""
|
||||
# Arrange
|
||||
partner_key_encoded = base64.b64encode(b"partner-key-123").decode("utf-8")
|
||||
@ -128,7 +128,9 @@ class TestPartnerTenants:
|
||||
with pytest.raises(BadRequest):
|
||||
resource.put(partner_key_encoded)
|
||||
|
||||
def test_put_billing_service_json_decode_error(self, app, mock_account, mock_billing_service, mock_decorators):
|
||||
def test_put_billing_service_json_decode_error(
|
||||
self, app: Flask, mock_account, mock_billing_service, mock_decorators
|
||||
):
|
||||
"""Test handling of billing service JSON decode error.
|
||||
|
||||
When billing service returns non-200 status code with invalid JSON response,
|
||||
@ -174,7 +176,7 @@ class TestPartnerTenants:
|
||||
assert isinstance(exc_info.value, json.JSONDecodeError)
|
||||
assert "Expecting value" in str(exc_info.value)
|
||||
|
||||
def test_put_empty_click_id(self, app, mock_account, mock_billing_service, mock_decorators):
|
||||
def test_put_empty_click_id(self, app: Flask, mock_account, mock_billing_service, mock_decorators):
|
||||
"""Test that empty click_id raises BadRequest."""
|
||||
# Arrange
|
||||
partner_key_encoded = base64.b64encode(b"partner-key-123").decode("utf-8")
|
||||
@ -199,7 +201,7 @@ class TestPartnerTenants:
|
||||
resource.put(partner_key_encoded)
|
||||
assert "Invalid partner information" in str(exc_info.value)
|
||||
|
||||
def test_put_empty_partner_key_after_decode(self, app, mock_account, mock_billing_service, mock_decorators):
|
||||
def test_put_empty_partner_key_after_decode(self, app: Flask, mock_account, mock_billing_service, mock_decorators):
|
||||
"""Test that empty partner_key after decode raises BadRequest."""
|
||||
# Arrange
|
||||
# Base64 encode an empty string
|
||||
@ -225,7 +227,7 @@ class TestPartnerTenants:
|
||||
resource.put(empty_partner_key_encoded)
|
||||
assert "Invalid partner information" in str(exc_info.value)
|
||||
|
||||
def test_put_empty_user_id(self, app, mock_account, mock_billing_service, mock_decorators):
|
||||
def test_put_empty_user_id(self, app: Flask, mock_account, mock_billing_service, mock_decorators):
|
||||
"""Test that empty user id raises BadRequest."""
|
||||
# Arrange
|
||||
partner_key_encoded = base64.b64encode(b"partner-key-123").decode("utf-8")
|
||||
|
||||
@ -8,10 +8,8 @@ from werkzeug.exceptions import Forbidden
|
||||
import controllers.console.tag.tags as module
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.tag.tags import (
|
||||
DeprecatedTagBindingCreateApi,
|
||||
DeprecatedTagBindingRemoveApi,
|
||||
TagBindingCollectionApi,
|
||||
TagBindingItemApi,
|
||||
TagBindingRemoveApi,
|
||||
TagListApi,
|
||||
TagUpdateDeleteApi,
|
||||
)
|
||||
@ -249,39 +247,13 @@ class TestTagBindingCollectionApi:
|
||||
method(api)
|
||||
|
||||
|
||||
class TestDeprecatedTagBindingCreateApi:
|
||||
def test_create_success(self, app, admin_user, payload_patch):
|
||||
api = DeprecatedTagBindingCreateApi()
|
||||
class TestTagBindingRemoveApi:
|
||||
def test_remove_success(self, app, admin_user, payload_patch):
|
||||
api = TagBindingRemoveApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
payload = {
|
||||
"tag_ids": ["tag-1"],
|
||||
"target_id": "target-1",
|
||||
"type": "knowledge",
|
||||
}
|
||||
|
||||
with app.test_request_context("/", json=payload):
|
||||
with (
|
||||
patch(
|
||||
"controllers.console.tag.tags.current_account_with_tenant",
|
||||
return_value=(admin_user, None),
|
||||
),
|
||||
payload_patch(payload),
|
||||
patch("controllers.console.tag.tags.TagService.save_tag_binding") as save_mock,
|
||||
):
|
||||
result, status = method(api)
|
||||
|
||||
save_mock.assert_called_once()
|
||||
assert status == 200
|
||||
assert result["result"] == "success"
|
||||
|
||||
|
||||
class TestTagBindingItemApi:
|
||||
def test_delete_success(self, app, admin_user, payload_patch):
|
||||
api = TagBindingItemApi()
|
||||
method = unwrap(api.delete)
|
||||
|
||||
payload = {
|
||||
"tag_ids": ["tag-1", "tag-2"],
|
||||
"target_id": "target-1",
|
||||
"type": "knowledge",
|
||||
}
|
||||
@ -295,57 +267,16 @@ class TestTagBindingItemApi:
|
||||
payload_patch(payload),
|
||||
patch("controllers.console.tag.tags.TagService.delete_tag_binding") as delete_mock,
|
||||
):
|
||||
result, status = method(api, "tag-1")
|
||||
result, status = method(api)
|
||||
|
||||
delete_mock.assert_called_once()
|
||||
delete_payload = delete_mock.call_args.args[0]
|
||||
assert delete_payload.tag_id == "tag-1"
|
||||
assert delete_payload.target_id == "target-1"
|
||||
assert delete_payload.type == TagType.KNOWLEDGE
|
||||
assert status == 200
|
||||
assert result["result"] == "success"
|
||||
|
||||
def test_delete_forbidden(self, app, readonly_user):
|
||||
api = TagBindingItemApi()
|
||||
method = unwrap(api.delete)
|
||||
|
||||
with app.test_request_context("/"):
|
||||
with patch(
|
||||
"controllers.console.tag.tags.current_account_with_tenant",
|
||||
return_value=(readonly_user, None),
|
||||
):
|
||||
with pytest.raises(Forbidden):
|
||||
method(api, "tag-1")
|
||||
|
||||
|
||||
class TestDeprecatedTagBindingRemoveApi:
|
||||
def test_remove_success(self, app, admin_user, payload_patch):
|
||||
api = DeprecatedTagBindingRemoveApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
payload = {
|
||||
"tag_id": "tag-1",
|
||||
"target_id": "target-1",
|
||||
"type": "knowledge",
|
||||
}
|
||||
|
||||
with app.test_request_context("/", json=payload):
|
||||
with (
|
||||
patch(
|
||||
"controllers.console.tag.tags.current_account_with_tenant",
|
||||
return_value=(admin_user, None),
|
||||
),
|
||||
payload_patch(payload),
|
||||
patch("controllers.console.tag.tags.TagService.delete_tag_binding") as delete_mock,
|
||||
):
|
||||
result, status = method(api)
|
||||
|
||||
delete_mock.assert_called_once()
|
||||
assert delete_payload.tag_ids == ["tag-1", "tag-2"]
|
||||
assert status == 200
|
||||
assert result["result"] == "success"
|
||||
|
||||
def test_remove_forbidden(self, app, readonly_user, payload_patch):
|
||||
api = DeprecatedTagBindingRemoveApi()
|
||||
api = TagBindingRemoveApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
with app.test_request_context("/", json={}):
|
||||
@ -371,32 +302,30 @@ class TestTagResponseModel:
|
||||
|
||||
|
||||
class TestTagBindingRouteMetadata:
|
||||
def test_legacy_write_routes_are_marked_deprecated(self):
|
||||
assert DeprecatedTagBindingCreateApi.post.__apidoc__["deprecated"] is True
|
||||
assert DeprecatedTagBindingRemoveApi.post.__apidoc__["deprecated"] is True
|
||||
def test_write_routes_are_not_deprecated(self):
|
||||
assert TagBindingCollectionApi.post.__apidoc__.get("deprecated") is not True
|
||||
assert TagBindingItemApi.delete.__apidoc__.get("deprecated") is not True
|
||||
assert TagBindingRemoveApi.post.__apidoc__.get("deprecated") is not True
|
||||
|
||||
def test_write_routes_have_stable_operation_ids(self):
|
||||
assert TagBindingCollectionApi.post.__apidoc__["id"] == "create_tag_binding"
|
||||
assert TagBindingItemApi.delete.__apidoc__["id"] == "delete_tag_binding"
|
||||
assert DeprecatedTagBindingCreateApi.post.__apidoc__["id"] == "create_tag_binding_deprecated"
|
||||
assert DeprecatedTagBindingRemoveApi.post.__apidoc__["id"] == "delete_tag_binding_deprecated"
|
||||
assert TagBindingRemoveApi.post.__apidoc__["id"] == "remove_tag_bindings"
|
||||
|
||||
def test_canonical_and_legacy_write_routes_are_registered(self):
|
||||
def test_write_routes_are_registered(self):
|
||||
route_map = {
|
||||
resource.__name__: urls
|
||||
for resource, urls, _route_doc, _kwargs in console_ns.resources
|
||||
if resource.__name__
|
||||
in {
|
||||
"TagBindingCollectionApi",
|
||||
"TagBindingItemApi",
|
||||
"DeprecatedTagBindingCreateApi",
|
||||
"DeprecatedTagBindingRemoveApi",
|
||||
"TagBindingRemoveApi",
|
||||
}
|
||||
}
|
||||
|
||||
assert route_map["TagBindingCollectionApi"] == ("/tag-bindings",)
|
||||
assert route_map["TagBindingItemApi"] == ("/tag-bindings/<uuid:id>",)
|
||||
assert route_map["DeprecatedTagBindingCreateApi"] == ("/tag-bindings/create",)
|
||||
assert route_map["DeprecatedTagBindingRemoveApi"] == ("/tag-bindings/remove",)
|
||||
assert route_map["TagBindingRemoveApi"] == ("/tag-bindings/remove",)
|
||||
|
||||
def test_legacy_write_routes_are_not_registered(self):
|
||||
urls = {url for _resource, resource_urls, _route_doc, _kwargs in console_ns.resources for url in resource_urls}
|
||||
|
||||
assert "/tag-bindings/create" not in urls
|
||||
assert "/tag-bindings/<uuid:id>" not in urls
|
||||
|
||||
Reference in New Issue
Block a user