Merge branch 'main' into 4-27-app-deploy

This commit is contained in:
Stephen Zhou
2026-05-07 12:36:06 +08:00
222 changed files with 6111 additions and 3570 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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()

View File

@ -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")

View File

@ -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