Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/ARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default class MyCommand extends DifyCommand {
process.stdout.write(
await runMyThing(
{
/* args */
// args
},
{ bundle: ctx.bundle, http: ctx.http, io: ctx.io },
),
Expand Down
3 changes: 0 additions & 3 deletions oxlint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6061,9 +6061,6 @@
},
"jsx_a11y/no-static-element-interactions": {
"count": 2
},
"no-useless-return": {
"count": 1
}
},
"web/app/components/workflow/panel/version-history-panel/version-history-item.tsx": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"check": "vp fmt --check && vp lint --quiet && pnpm lint:eslint",
"check": "vp check && pnpm lint:eslint",
"check:fix": "pnpm lint:eslint:fix && vp check --fix",
"dev": "concurrently -k -n vinext,proxy \"vp run dify-web#dev:vinext\" \"vp run dify-web#dev:proxy\"",
"prepare": "vp config",
Expand Down
2,043 changes: 1,079 additions & 964 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ overrides:
solid-js: 1.9.13
string-width: ~8.2.1
tar@<=7.5.15: ^7.5.16
vite: npm:@voidzero-dev/vite-plus-core@0.2.4
vite: npm:@voidzero-dev/vite-plus-core@0.2.5
ws@>=8.0.0 <8.20.1: ^8.21.0
yaml@>=2.0.0 <2.8.3: 2.9.0
yauzl@<3.2.1: 3.2.1
Expand Down Expand Up @@ -251,9 +251,9 @@ catalog:
use-context-selector: 2.0.0
uuid: 14.0.1
vinext: 1.0.0-beta.1
vite: npm:@voidzero-dev/vite-plus-core@0.2.4
vite: npm:@voidzero-dev/vite-plus-core@0.2.5
vite-plugin-inspect: 12.0.0-beta.3
vite-plus: 0.2.4
vite-plus: 0.2.5
vitest: 4.1.10
vitest-browser-react: 2.2.0
vitest-canvas-mock: 1.1.4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
*/

let mockPostMarketplaceShouldFail = false
const mockPostMarketplace = vi.hoisted(() => vi.fn())
const mockPostMarketplaceResponse = {
data: {
plugins: [{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] }],
total: 1,
},
}

vi.mock('@/service/base', () => ({
postMarketplace: vi.fn(async () => {
if (mockPostMarketplaceShouldFail) throw new Error('Mock API error')
return mockPostMarketplaceResponse
}),
}))
mockPostMarketplace.mockImplementation(async () => {
if (mockPostMarketplaceShouldFail) throw new Error('Mock API error')
return mockPostMarketplaceResponse
})

vi.mock('@/service/base', () => ({ postMarketplace: mockPostMarketplace }))

vi.mock('@/config', () => ({
API_PREFIX: '/api',
Expand Down Expand Up @@ -169,8 +170,7 @@ describe('useMarketplacePlugins (integration)', () => {

it('should show isLoading during initial fetch', async () => {
// Delay the response so we can observe the loading state
const { postMarketplace } = await import('@/service/base')
vi.mocked(postMarketplace).mockImplementationOnce(
mockPostMarketplace.mockImplementationOnce(
() =>
new Promise((resolve) => {
setTimeout(
Expand Down Expand Up @@ -234,8 +234,7 @@ describe('useMarketplacePlugins (integration)', () => {
total: 1,
},
}
const { postMarketplace } = await import('@/service/base')
vi.mocked(postMarketplace).mockResolvedValueOnce(bundleResponse)
mockPostMarketplace.mockResolvedValueOnce(bundleResponse)

const { useMarketplacePlugins } = await import('../hooks')
const { Wrapper } = createWrapper()
Expand Down Expand Up @@ -323,8 +322,7 @@ describe('useMarketplacePlugins (integration)', () => {
})

it('should handle response with bundles field (bundles || plugins fallback)', async () => {
const { postMarketplace } = await import('@/service/base')
vi.mocked(postMarketplace).mockResolvedValueOnce({
mockPostMarketplace.mockResolvedValueOnce({
data: {
bundles: [
{ type: 'bundle', org: 'test', name: 'b1', tags: [], description: 'desc', labels: {} },
Expand Down Expand Up @@ -352,8 +350,7 @@ describe('useMarketplacePlugins (integration)', () => {
})

it('should handle response with no bundles and no plugins (empty fallback)', async () => {
const { postMarketplace } = await import('@/service/base')
vi.mocked(postMarketplace).mockResolvedValueOnce({
mockPostMarketplace.mockResolvedValueOnce({
data: {
total: 0,
},
Expand Down
19 changes: 9 additions & 10 deletions web/app/components/plugins/marketplace/__tests__/hooks.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function createWrapper() {
}

let mockPostMarketplaceShouldFail = false
const mockPostMarketplace = vi.hoisted(() => vi.fn())
const mockPostMarketplaceResponse = {
data: {
plugins: [
Expand All @@ -45,12 +46,12 @@ const mockPostMarketplaceResponse = {
},
}

vi.mock('@/service/base', () => ({
postMarketplace: vi.fn(() => {
if (mockPostMarketplaceShouldFail) return Promise.reject(new Error('Mock API error'))
return Promise.resolve(mockPostMarketplaceResponse)
}),
}))
mockPostMarketplace.mockImplementation(() => {
if (mockPostMarketplaceShouldFail) return Promise.reject(new Error('Mock API error'))
return Promise.resolve(mockPostMarketplaceResponse)
})

vi.mock('@/service/base', () => ({ postMarketplace: mockPostMarketplace }))

vi.mock('@/config', () => ({
API_PREFIX: '/api',
Expand Down Expand Up @@ -308,8 +309,7 @@ describe('Hooks queryFn Coverage', () => {
})

it('should expose page and total from infinite query data', async () => {
const { postMarketplace } = await import('@/service/base')
vi.mocked(postMarketplace)
mockPostMarketplace
.mockResolvedValueOnce({
data: {
plugins: [
Expand Down Expand Up @@ -420,8 +420,7 @@ describe('Hooks queryFn Coverage', () => {
})

it('should test getNextPageParam via fetchNextPage behavior', async () => {
const { postMarketplace } = await import('@/service/base')
vi.mocked(postMarketplace)
mockPostMarketplace
.mockResolvedValueOnce({
data: { plugins: [], total: 100 },
})
Expand Down
132 changes: 51 additions & 81 deletions web/service/__tests__/base-request.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,40 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
// oxlint-disable-next-line no-restricted-imports -- This spec directly tests the legacy request owner.
import { request } from '../base'

const mocks = vi.hoisted(() => ({
isClient: true,
baseFetch: vi.fn(),
refreshAccessTokenOrReLogin: vi.fn(),
}))

vi.mock('@/utils/client', () => ({
get isClient() {
return mocks.isClient
},
get isServer() {
return !mocks.isClient
},
}))

vi.mock('@/utils/var', () => ({
basePath: '/app',
}))

vi.mock('../fetch', () => ({
base: mocks.baseFetch,
ContentType: {
audio: 'audio/mpeg',
download: 'application/octet-stream',
downloadZip: 'application/zip',
json: 'application/json',
},
getBaseOptions: vi.fn(() => ({})),
}))

vi.mock('../refresh-token', () => ({
refreshAccessTokenOrReLogin: mocks.refreshAccessTokenOrReLogin,
}))

const createUnauthorizedResponse = () =>
new Response(
Expand All @@ -15,89 +51,23 @@ const createUnauthorizedResponse = () =>
},
)

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,
}
function arrangeClientRequest({ response, refreshError }: ClientRequestOptions) {
mocks.baseFetch.mockRejectedValue(response)
if (refreshError) mocks.refreshAccessTokenOrReLogin.mockRejectedValue(refreshError)
}

describe('request 401 handling', () => {
const originalLocation = globalThis.location

beforeEach(() => {
vi.clearAllMocks()
mocks.isClient = true
mocks.baseFetch.mockReset()
mocks.refreshAccessTokenOrReLogin.mockReset()
Object.defineProperty(globalThis, 'location', {
value: {
origin: 'https://example.com',
Expand All @@ -118,40 +88,40 @@ describe('request 401 handling', () => {
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()
const response = createUnauthorizedResponse()
mocks.isClient = false
mocks.baseFetch.mockRejectedValue(response)

await expect(request('/account/profile')).rejects.toMatchObject({ status: 401 })
await expect(request('/account/profile')).rejects.toBe(response)

expect(mockRefreshAccessTokenOrReLogin).not.toHaveBeenCalled()
expect(mocks.refreshAccessTokenOrReLogin).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 })
arrangeClientRequest({ 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()
expect(mocks.refreshAccessTokenOrReLogin).not.toHaveBeenCalled()
})

it('should preserve the current URL when token refresh fails', async () => {
const response = createUnauthorizedResponse()
const { request, mockRefreshAccessTokenOrReLogin } = await loadClientRequest({
arrangeClientRequest({
response,
refreshError: new Error('refresh failed'),
})

await expect(request('/account/profile')).rejects.toBe(response)

expect(mockRefreshAccessTokenOrReLogin).toHaveBeenCalledOnce()
expect(mocks.refreshAccessTokenOrReLogin).toHaveBeenCalledOnce()
expect(globalThis.location.href).toBe(
`https://example.com/app/signin?redirect_url=${encodeURIComponent('/app/apps?category=agent#recent')}`,
)
Expand Down
Loading