dify/web/service/__tests__/base-request.spec.ts
Coding On Star 2c4d8ef098
fix(auth): preserve redirect URL across auth flows (#38905)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
2026-07-14 07:47:34 +00:00

160 lines
4.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const createUnauthorizedResponse = () =>
new Response(
JSON.stringify({
code: 'unauthorized',
message: 'Invalid Authorization token.',
status: 401,
}),
{
status: 401,
headers: {
'Content-Type': 'application/json',
},
},
)
async function loadServerRequest() {
vi.resetModules()
const mockBaseFetch = vi.fn(async () => {
throw createUnauthorizedResponse()
})
const mockRefreshAccessTokenOrReLogin = vi.fn()
vi.doMock('@/utils/client', () => ({
isClient: false,
isServer: true,
}))
vi.doMock('../fetch', () => ({
base: mockBaseFetch,
ContentType: {
audio: 'audio/mpeg',
download: 'application/octet-stream',
downloadZip: 'application/zip',
json: 'application/json',
},
getBaseOptions: vi.fn(() => ({})),
}))
vi.doMock('../refresh-token', () => ({
refreshAccessTokenOrReLogin: mockRefreshAccessTokenOrReLogin,
}))
const { request } = await import('../base')
return {
request,
mockRefreshAccessTokenOrReLogin,
}
}
type ClientRequestOptions = {
response: Response
refreshError?: Error
}
async function loadClientRequest({ response, refreshError }: ClientRequestOptions) {
vi.resetModules()
const mockBaseFetch = vi.fn(async () => {
throw response
})
const mockRefreshAccessTokenOrReLogin = refreshError
? vi.fn().mockRejectedValue(refreshError)
: vi.fn()
vi.doMock('@/utils/client', () => ({
isClient: true,
isServer: false,
}))
vi.doMock('@/utils/var', () => ({
basePath: '/app',
}))
vi.doMock('../fetch', () => ({
base: mockBaseFetch,
ContentType: {
audio: 'audio/mpeg',
download: 'application/octet-stream',
downloadZip: 'application/zip',
json: 'application/json',
},
getBaseOptions: vi.fn(() => ({})),
}))
vi.doMock('../refresh-token', () => ({
refreshAccessTokenOrReLogin: mockRefreshAccessTokenOrReLogin,
}))
const { request } = await import('../base')
return {
request,
mockRefreshAccessTokenOrReLogin,
}
}
describe('request 401 handling', () => {
const originalLocation = globalThis.location
beforeEach(() => {
vi.clearAllMocks()
Object.defineProperty(globalThis, 'location', {
value: {
origin: 'https://example.com',
pathname: '/app/apps',
search: '?category=agent',
hash: '#recent',
href: 'https://example.com/app/apps?category=agent#recent',
reload: vi.fn(),
},
writable: true,
configurable: true,
})
})
afterEach(() => {
Object.defineProperty(globalThis, 'location', {
value: originalLocation,
writable: true,
configurable: true,
})
vi.resetModules()
vi.restoreAllMocks()
})
it('should not run browser auth recovery when handling 401 on the server', async () => {
const { request, mockRefreshAccessTokenOrReLogin } = await loadServerRequest()
await expect(request('/account/profile')).rejects.toMatchObject({ status: 401 })
expect(mockRefreshAccessTokenOrReLogin).not.toHaveBeenCalled()
})
it('should preserve the current URL when a 401 response cannot be parsed', async () => {
const response = new Response('not-json', { status: 401 })
const { request, mockRefreshAccessTokenOrReLogin } = await loadClientRequest({ response })
await expect(request('/account/profile')).rejects.toBe(response)
expect(globalThis.location.href).toBe(
`https://example.com/app/signin?redirect_url=${encodeURIComponent('/app/apps?category=agent#recent')}`,
)
expect(mockRefreshAccessTokenOrReLogin).not.toHaveBeenCalled()
})
it('should preserve the current URL when token refresh fails', async () => {
const response = createUnauthorizedResponse()
const { request, mockRefreshAccessTokenOrReLogin } = await loadClientRequest({
response,
refreshError: new Error('refresh failed'),
})
await expect(request('/account/profile')).rejects.toBe(response)
expect(mockRefreshAccessTokenOrReLogin).toHaveBeenCalledOnce()
expect(globalThis.location.href).toBe(
`https://example.com/app/signin?redirect_url=${encodeURIComponent('/app/apps?category=agent#recent')}`,
)
})
})