feat(logout): implement logout mutation and reset login cache on success

test(logout): add unit tests for useLogout hook to verify cache reset
refactor(signin): enhance signin URL handling for OAuth and generic redirects
This commit is contained in:
yyh 2026-02-10 17:55:22 +08:00
parent acbcca0322
commit 3a8bbd10cd
No known key found for this signature in database
4 changed files with 78 additions and 5 deletions

View File

@ -23,6 +23,23 @@ describe('buildSigninUrlWithRedirect', () => {
})
})
describe('Non-OAuth redirect handling', () => {
it('should return plain signin URL without oauth_redirect_url for generic pages', () => {
// Arrange
const currentLocation = {
origin: 'https://example.com',
pathname: '/apps',
search: '?tab=all',
} as const
// Act
const signinUrl = buildSigninUrlWithRedirect(currentLocation, '')
// Assert
expect(signinUrl).toBe('https://example.com/signin')
})
})
describe('Signin self-redirect guard', () => {
it('should return plain signin URL when current path is already signin', () => {
// Arrange
@ -41,12 +58,12 @@ describe('buildSigninUrlWithRedirect', () => {
})
describe('basePath support', () => {
it('should respect basePath when building signin URL', () => {
it('should respect basePath for OAuth authorize path', () => {
// Arrange
const currentLocation = {
origin: 'https://example.com',
pathname: '/console/apps',
search: '?tab=all',
pathname: '/console/account/oauth/authorize',
search: '?client_id=abc',
} as const
// Act
@ -55,7 +72,7 @@ describe('buildSigninUrlWithRedirect', () => {
// Assert
expect(signinUrl.startsWith('https://example.com/console/signin?')).toBe(true)
const encodedRedirect = new URL(signinUrl).searchParams.get('oauth_redirect_url')
expect(decodeURIComponent(encodedRedirect || '')).toBe('https://example.com/console/apps?tab=all')
expect(decodeURIComponent(encodedRedirect || '')).toBe('https://example.com/console/account/oauth/authorize?client_id=abc')
})
})
})

View File

@ -38,6 +38,7 @@ import { getWebAppPassport } from './webapp-auth'
const TIME_OUT = 100000
const SIGNIN_REDIRECT_URL_KEY = 'oauth_redirect_url'
const OAUTH_AUTHORIZE_PATH = '/account/oauth/authorize'
export type IconObject = {
background: string
@ -160,11 +161,17 @@ function jumpTo(url: string) {
export function buildSigninUrlWithRedirect(currentLocation: Pick<Location, 'origin' | 'pathname' | 'search'>, currentBasePath: string) {
const signinPath = `${currentBasePath}/signin`
const signinUrl = `${currentLocation.origin}${signinPath}`
const oauthAuthorizePath = `${currentBasePath}${OAUTH_AUTHORIZE_PATH}`
// Keep signin as-is to avoid redirect loops.
if (currentLocation.pathname === signinPath)
return signinUrl
// Only OAuth authorization flow requires preserving the full original URL.
// For generic 401 redirects (e.g. manual logout), keep signin URL clean.
if (currentLocation.pathname !== oauthAuthorizePath)
return signinUrl
const currentUrl = `${currentLocation.origin}${currentLocation.pathname}${currentLocation.search}`
const params = new URLSearchParams()
params.set(SIGNIN_REDIRECT_URL_KEY, encodeURIComponent(currentUrl))

View File

@ -0,0 +1,43 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { post } from './base'
import { commonQueryKeys, useLogout } from './use-common'
vi.mock('./base', () => ({
get: vi.fn(),
post: vi.fn(),
}))
const createWrapper = (queryClient: QueryClient) => {
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
describe('useLogout', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should reset login cache when logout succeeds', async () => {
// Arrange
const queryClient = new QueryClient()
const wrapper = createWrapper(queryClient)
queryClient.setQueryData(commonQueryKeys.isLogin, { logged_in: true })
vi.mocked(post).mockResolvedValue({ result: 'success' } as never)
const { result } = renderHook(() => useLogout(), { wrapper })
// Act
await act(async () => {
await result.current.mutateAsync()
})
// Assert
expect(post).toHaveBeenCalledWith('/logout')
expect(queryClient.getQueryData(commonQueryKeys.isLogin)).toEqual({ logged_in: false })
})
})

View File

@ -22,7 +22,7 @@ import type {
UserProfileResponse,
} from '@/models/common'
import type { RETRIEVE_METHOD } from '@/types/app'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { IS_DEV } from '@/config'
import { get, post } from './base'
import { useInvalid } from './use-base'
@ -232,9 +232,15 @@ export const useIsLogin = () => {
}
export const useLogout = () => {
const queryClient = useQueryClient()
return useMutation({
mutationKey: [NAME_SPACE, 'logout'],
mutationFn: () => post('/logout'),
onSuccess: () => {
queryClient.setQueryData(commonQueryKeys.isLogin, { logged_in: false })
queryClient.removeQueries({ queryKey: commonQueryKeys.userProfile })
},
})
}