mirror of
https://github.com/langgenius/dify.git
synced 2026-07-07 05:47:38 +08:00
Compare commits
3 Commits
main
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
| 868eff9427 | |||
| 4767d90e1a | |||
| e32b778606 |
@ -56,7 +56,7 @@ from models.account import Account
|
||||
from services.account_service import AccountService, InvitationDetailDict, RegisterService, TenantService
|
||||
from services.billing_service import BillingService
|
||||
from services.entities.auth_entities import LoginFailureReason, LoginPayloadBase
|
||||
from services.errors.account import AccountRegisterError, RefreshTokenAccountNotFoundError, RefreshTokenNotFoundError
|
||||
from services.errors.account import AccountRegisterError
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
@ -359,22 +359,18 @@ class RefreshTokenApi(Resource):
|
||||
|
||||
try:
|
||||
new_token_pair = AccountService.refresh_token(refresh_token, session=db.session)
|
||||
except Unauthorized as exc:
|
||||
return SimpleResultMessageResponse(result="fail", message=exc.description or "Unauthorized.").model_dump(
|
||||
mode="json"
|
||||
), 401
|
||||
except (RefreshTokenNotFoundError, RefreshTokenAccountNotFoundError) as exc:
|
||||
return SimpleResultMessageResponse(result="fail", message=str(exc)).model_dump(mode="json"), 401
|
||||
|
||||
# Create response with new cookies
|
||||
# response-contract:ignore cookie-bearing Flask response
|
||||
response = make_response(SimpleResultResponse(result="success").model_dump(mode="json"))
|
||||
# Create response with new cookies
|
||||
# response-contract:ignore cookie-bearing Flask response
|
||||
response = make_response(SimpleResultResponse(result="success").model_dump(mode="json"))
|
||||
|
||||
# Update cookies with new tokens
|
||||
set_csrf_token_to_cookie(request, response, new_token_pair.csrf_token)
|
||||
set_access_token_to_cookie(request, response, new_token_pair.access_token)
|
||||
set_refresh_token_to_cookie(request, response, new_token_pair.refresh_token)
|
||||
return response
|
||||
# Update cookies with new tokens
|
||||
set_csrf_token_to_cookie(request, response, new_token_pair.csrf_token)
|
||||
set_access_token_to_cookie(request, response, new_token_pair.access_token)
|
||||
set_refresh_token_to_cookie(request, response, new_token_pair.refresh_token)
|
||||
return response
|
||||
except Exception as e:
|
||||
return SimpleResultMessageResponse(result="fail", message=str(e)).model_dump(mode="json"), 401
|
||||
|
||||
|
||||
def _get_account_with_case_fallback(email: str):
|
||||
|
||||
@ -65,8 +65,6 @@ from services.errors.account import (
|
||||
LinkAccountIntegrateError,
|
||||
MemberNotInTenantError,
|
||||
NoPermissionError,
|
||||
RefreshTokenAccountNotFoundError,
|
||||
RefreshTokenNotFoundError,
|
||||
RoleAlreadyAssignedError,
|
||||
TenantNotFoundError,
|
||||
)
|
||||
@ -656,11 +654,11 @@ class AccountService:
|
||||
# Verify the refresh token
|
||||
account_id = redis_client.get(AccountService._get_refresh_token_key(refresh_token))
|
||||
if not account_id:
|
||||
raise RefreshTokenNotFoundError("Invalid refresh token")
|
||||
raise ValueError("Invalid refresh token")
|
||||
|
||||
account = AccountService.load_user(account_id.decode("utf-8"), session)
|
||||
if not account:
|
||||
raise RefreshTokenAccountNotFoundError("Invalid account")
|
||||
raise ValueError("Invalid account")
|
||||
|
||||
# Generate new access token and refresh token
|
||||
new_access_token = AccountService.get_account_jwt_token(account)
|
||||
|
||||
@ -17,14 +17,6 @@ class AccountPasswordError(BaseServiceError):
|
||||
pass
|
||||
|
||||
|
||||
class RefreshTokenNotFoundError(BaseServiceError):
|
||||
pass
|
||||
|
||||
|
||||
class RefreshTokenAccountNotFoundError(BaseServiceError):
|
||||
pass
|
||||
|
||||
|
||||
class AccountNotLinkTenantError(BaseServiceError):
|
||||
pass
|
||||
|
||||
|
||||
@ -13,10 +13,8 @@ from unittest.mock import ANY, MagicMock, patch
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from flask_restx import Api
|
||||
from werkzeug.exceptions import Unauthorized
|
||||
|
||||
from controllers.console.auth.login import RefreshTokenApi
|
||||
from services.errors.account import RefreshTokenAccountNotFoundError, RefreshTokenNotFoundError
|
||||
|
||||
|
||||
class TestRefreshTokenApi:
|
||||
@ -100,19 +98,18 @@ class TestRefreshTokenApi:
|
||||
|
||||
@patch("controllers.console.auth.login.extract_refresh_token", autospec=True)
|
||||
@patch("controllers.console.auth.login.AccountService.refresh_token", autospec=True)
|
||||
def test_refresh_returns_unauthorized_for_invalid_refresh_token(
|
||||
self, mock_refresh_token, mock_extract_token, app: Flask
|
||||
):
|
||||
def test_refresh_fails_with_invalid_token(self, mock_refresh_token, mock_extract_token, app: Flask):
|
||||
"""
|
||||
Test token refresh maps invalid refresh tokens to unauthorized responses.
|
||||
Test token refresh failure with invalid refresh token.
|
||||
|
||||
Verifies that:
|
||||
- Invalid refresh token validation failures return 401
|
||||
- The failure response preserves the validation message
|
||||
- Exception is caught when token is invalid
|
||||
- 401 status code is returned
|
||||
- Error message is included in response
|
||||
"""
|
||||
# Arrange
|
||||
mock_extract_token.return_value = "invalid_refresh_token"
|
||||
mock_refresh_token.side_effect = RefreshTokenNotFoundError("Invalid refresh token")
|
||||
mock_refresh_token.side_effect = Exception("Invalid refresh token")
|
||||
|
||||
# Act
|
||||
with app.test_request_context("/refresh-token", method="POST"):
|
||||
@ -122,21 +119,22 @@ class TestRefreshTokenApi:
|
||||
# Assert
|
||||
assert status_code == 401
|
||||
assert response["result"] == "fail"
|
||||
assert response["message"] == "Invalid refresh token"
|
||||
assert "Invalid refresh token" in response["message"]
|
||||
|
||||
@patch("controllers.console.auth.login.extract_refresh_token", autospec=True)
|
||||
@patch("controllers.console.auth.login.AccountService.refresh_token", autospec=True)
|
||||
def test_refresh_returns_unauthorized_for_invalid_account(self, mock_refresh_token, mock_extract_token, app: Flask):
|
||||
def test_refresh_fails_with_expired_token(self, mock_refresh_token, mock_extract_token, app: Flask):
|
||||
"""
|
||||
Test token refresh maps missing accounts to unauthorized responses.
|
||||
Test token refresh failure with expired refresh token.
|
||||
|
||||
Verifies that:
|
||||
- Invalid account validation failures return 401
|
||||
- The failure response preserves the validation message
|
||||
- Expired tokens are rejected
|
||||
- 401 status code is returned
|
||||
- Appropriate error handling
|
||||
"""
|
||||
# Arrange
|
||||
mock_extract_token.return_value = "refresh_token_for_missing_account"
|
||||
mock_refresh_token.side_effect = RefreshTokenAccountNotFoundError("Invalid account")
|
||||
mock_extract_token.return_value = "expired_refresh_token"
|
||||
mock_refresh_token.side_effect = Exception("Refresh token expired")
|
||||
|
||||
# Act
|
||||
with app.test_request_context("/refresh-token", method="POST"):
|
||||
@ -146,71 +144,7 @@ class TestRefreshTokenApi:
|
||||
# Assert
|
||||
assert status_code == 401
|
||||
assert response["result"] == "fail"
|
||||
assert response["message"] == "Invalid account"
|
||||
|
||||
@patch("controllers.console.auth.login.extract_refresh_token", autospec=True)
|
||||
@patch("controllers.console.auth.login.AccountService.refresh_token", autospec=True)
|
||||
def test_refresh_returns_unauthorized_for_banned_account(self, mock_refresh_token, mock_extract_token, app: Flask):
|
||||
"""
|
||||
Test token refresh maps banned accounts to unauthorized responses.
|
||||
|
||||
Verifies that:
|
||||
- Authorization failures raised during account loading return 401
|
||||
- The failure response preserves the authorization message
|
||||
"""
|
||||
# Arrange
|
||||
mock_extract_token.return_value = "refresh_token_for_banned_account"
|
||||
mock_refresh_token.side_effect = Unauthorized("Account is banned.")
|
||||
|
||||
# Act
|
||||
with app.test_request_context("/refresh-token", method="POST"):
|
||||
refresh_api = RefreshTokenApi()
|
||||
response, status_code = refresh_api.post()
|
||||
|
||||
# Assert
|
||||
assert status_code == 401
|
||||
assert response["result"] == "fail"
|
||||
assert response["message"] == "Account is banned."
|
||||
|
||||
@patch("controllers.console.auth.login.extract_refresh_token", autospec=True)
|
||||
@patch("controllers.console.auth.login.AccountService.refresh_token", autospec=True)
|
||||
def test_refresh_propagates_non_whitelisted_value_error(self, mock_refresh_token, mock_extract_token, app: Flask):
|
||||
"""
|
||||
Test token refresh preserves non-whitelisted ValueError failures.
|
||||
|
||||
Verifies that:
|
||||
- Only known refresh-token validation errors are mapped to 401
|
||||
- Unexpected ValueError instances continue to propagate
|
||||
"""
|
||||
# Arrange
|
||||
mock_extract_token.return_value = "valid_refresh_token"
|
||||
mock_refresh_token.side_effect = ValueError("unexpected parse failure")
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context("/refresh-token", method="POST"):
|
||||
refresh_api = RefreshTokenApi()
|
||||
with pytest.raises(ValueError, match="unexpected parse failure"):
|
||||
refresh_api.post()
|
||||
|
||||
@patch("controllers.console.auth.login.extract_refresh_token", autospec=True)
|
||||
@patch("controllers.console.auth.login.AccountService.refresh_token", autospec=True)
|
||||
def test_refresh_propagates_unexpected_service_errors(self, mock_refresh_token, mock_extract_token, app: Flask):
|
||||
"""
|
||||
Test token refresh preserves unexpected service failures.
|
||||
|
||||
Verifies that:
|
||||
- Operational errors are not misreported as authentication failures
|
||||
- The original exception is preserved for higher-level error handling
|
||||
"""
|
||||
# Arrange
|
||||
mock_extract_token.return_value = "valid_refresh_token"
|
||||
mock_refresh_token.side_effect = RuntimeError("redis unavailable")
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context("/refresh-token", method="POST"):
|
||||
refresh_api = RefreshTokenApi()
|
||||
with pytest.raises(RuntimeError, match="redis unavailable"):
|
||||
refresh_api.post()
|
||||
assert "expired" in response["message"].lower()
|
||||
|
||||
@patch("controllers.console.auth.login.extract_refresh_token", autospec=True)
|
||||
@patch("controllers.console.auth.login.AccountService.refresh_token", autospec=True)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
|
||||
@ -120,6 +120,41 @@ describe('AgentEnvEditor', () => {
|
||||
expect(screen.getByDisplayValue('SECOND_KEY')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should preserve a newly added variable key when key and value change in the same batch', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<AgentComposerProvider
|
||||
initialDraft={{
|
||||
...defaultAgentSoulConfigFormState,
|
||||
envVariables: [{
|
||||
id: 'env-1',
|
||||
key: 'API_KEY',
|
||||
value: 'secret-value',
|
||||
scope: 'plain',
|
||||
}],
|
||||
}}
|
||||
>
|
||||
<AgentEnvEditor />
|
||||
</AgentComposerProvider>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'agentV2.agentDetail.configure.advancedSettings.envEditor.add' }))
|
||||
|
||||
const keyInputs = screen.getAllByPlaceholderText('agentV2.agentDetail.configure.advancedSettings.envEditor.keyPlaceholder')
|
||||
const valueInputs = screen.getAllByPlaceholderText('agentV2.agentDetail.configure.advancedSettings.envEditor.valuePlaceholder')
|
||||
const newKeyInput = keyInputs[1]!
|
||||
const newValueInput = valueInputs[1]!
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(newKeyInput, { target: { value: 'SECOND_KEY' } })
|
||||
fireEvent.change(newValueInput, { target: { value: 'plain' } })
|
||||
})
|
||||
|
||||
expect(newKeyInput).toHaveValue('SECOND_KEY')
|
||||
expect(newValueInput).toHaveValue('plain')
|
||||
})
|
||||
|
||||
it('should import dotenv variables into the env table when a file is selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { container } = renderAgentEnvEditor()
|
||||
|
||||
@ -436,6 +436,23 @@ export function AgentEnvEditor() {
|
||||
const envEditorTableId = 'agent-configure-env-editor-table'
|
||||
const visibleEnvVariables = envVariables.length > 0 ? envVariables : [starterVariable]
|
||||
|
||||
const updateVariable = (id: string, updater: (variable: EnvVariable) => EnvVariable) => {
|
||||
setEnvVariables((currentEnvVariables) => {
|
||||
const existingVariable = currentEnvVariables.find(variable => variable.id === id)
|
||||
|
||||
if (existingVariable) {
|
||||
return currentEnvVariables.map(variable => (
|
||||
variable.id === id ? updater(variable) : variable
|
||||
))
|
||||
}
|
||||
|
||||
if (id === starterVariable.id)
|
||||
return [updater(starterVariable)]
|
||||
|
||||
return currentEnvVariables
|
||||
})
|
||||
}
|
||||
|
||||
const addVariable = ({
|
||||
focusField = 'key',
|
||||
scope,
|
||||
@ -448,8 +465,8 @@ export function AgentEnvEditor() {
|
||||
...(scope ? { scope } : {}),
|
||||
}
|
||||
|
||||
addEnvVariable({
|
||||
starterVariable,
|
||||
setEnvVariables(currentEnvVariables => [
|
||||
...(currentEnvVariables.length > 0 ? currentEnvVariables : [starterVariable]),
|
||||
variable,
|
||||
})
|
||||
setFocusedVariable({ id: variable.id, field: focusField })
|
||||
@ -470,7 +487,7 @@ export function AgentEnvEditor() {
|
||||
if (importedVariables.length === 0)
|
||||
return
|
||||
|
||||
importEnvVariables(importedVariables)
|
||||
setEnvVariables(currentEnvVariables => [...currentEnvVariables, ...importedVariables])
|
||||
}
|
||||
const updateVariableKey = (id: string, key: string) => {
|
||||
setEnvVariableKey({ id, key, starterVariable })
|
||||
@ -482,7 +499,7 @@ export function AgentEnvEditor() {
|
||||
setEnvVariableValue({ id, starterVariable, value })
|
||||
}
|
||||
const deleteVariable = (id: string) => {
|
||||
removeEnvVariable(id)
|
||||
setEnvVariables(currentEnvVariables => currentEnvVariables.filter(variable => variable.id !== id))
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user