mirror of
https://github.com/langgenius/dify.git
synced 2026-06-21 01:41:08 +08:00
161 lines
6.0 KiB
TypeScript
161 lines
6.0 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import { fireEvent, render, screen } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import DevicePage from '../page'
|
|
|
|
const mockPush = vi.fn()
|
|
const mockReplace = vi.fn()
|
|
const mockDeviceLookup = vi.fn()
|
|
let mockSearchParams: Record<string, string | null> = {}
|
|
|
|
vi.mock('@/next/navigation', () => ({
|
|
useSearchParams: () => ({ get: (key: string) => mockSearchParams[key] ?? null }),
|
|
useRouter: () => ({ push: mockPush, replace: mockReplace }),
|
|
usePathname: () => '/device',
|
|
}))
|
|
|
|
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
|
return {
|
|
...actual,
|
|
useQuery: vi.fn(),
|
|
}
|
|
})
|
|
|
|
vi.mock('@/service/device-flow', () => ({
|
|
deviceLookup: (...args: unknown[]) => mockDeviceLookup(...args),
|
|
DeviceFlowError: class extends Error {
|
|
code: string
|
|
status: number
|
|
constructor(code: string, status = 400) {
|
|
super(code)
|
|
this.code = code
|
|
this.status = status
|
|
}
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/features/system-features/client', () => ({
|
|
systemFeaturesQueryOptions: () => ({ queryKey: ['sys'], queryFn: async () => ({}) }),
|
|
}))
|
|
|
|
vi.mock('@/features/account-profile/client', () => ({
|
|
userProfileQueryOptions: () => ({ queryKey: ['profile'], queryFn: async () => null }),
|
|
}))
|
|
|
|
vi.mock('@/service/use-common', () => ({
|
|
commonQueryKeys: { currentWorkspace: ['currentWorkspace'] },
|
|
}))
|
|
|
|
const mockUseQuery = vi.mocked(useQuery)
|
|
|
|
const VALID_CODE = 'ABCD-3456'
|
|
|
|
// Typed reference to the mocked DeviceFlowError — same module reference as classifyLookupError uses
|
|
type MockDeviceFlowErrorCtor = new (code: string, status: number) => Error
|
|
let MockDeviceFlowError: MockDeviceFlowErrorCtor
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks()
|
|
mockSearchParams = {}
|
|
// router.replace(pathname) in the real app drops the query string; mirror
|
|
// that so useSearchParams reflects the cleared URL on the next render.
|
|
mockReplace.mockImplementation(() => {
|
|
mockSearchParams = {}
|
|
})
|
|
mockUseQuery.mockReturnValue({ data: undefined, isError: false } as ReturnType<typeof useQuery>)
|
|
const mod = await import('@/service/device-flow') as { DeviceFlowError: MockDeviceFlowErrorCtor }
|
|
MockDeviceFlowError = mod.DeviceFlowError
|
|
})
|
|
|
|
async function reachTerminal(rejectWith: unknown) {
|
|
mockDeviceLookup.mockRejectedValue(rejectWith)
|
|
render(<DevicePage />)
|
|
const input = screen.getByRole('textbox')
|
|
fireEvent.change(input, { target: { value: VALID_CODE } })
|
|
fireEvent.click(screen.getByRole('button', { name: /Continue/i }))
|
|
}
|
|
|
|
describe('error_expired terminal state', () => {
|
|
it('shows "Code no longer valid" heading', async () => {
|
|
await reachTerminal(new Error('expired'))
|
|
await screen.findByText('Code no longer valid')
|
|
})
|
|
|
|
it('ghost button resets to code_entry', async () => {
|
|
await reachTerminal(new Error('expired'))
|
|
await screen.findByText('Code no longer valid')
|
|
fireEvent.click(screen.getByRole('button', { name: /Try a different code/i }))
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
expect(screen.queryByText('Code no longer valid')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('error_rate_limited terminal state', () => {
|
|
it('shows "Too many attempts" heading', async () => {
|
|
await reachTerminal(new MockDeviceFlowError('rate_limited', 429))
|
|
await screen.findByText('Too many attempts')
|
|
})
|
|
|
|
it('ghost button resets to code_entry', async () => {
|
|
await reachTerminal(new MockDeviceFlowError('rate_limited', 429))
|
|
await screen.findByText('Too many attempts')
|
|
fireEvent.click(screen.getByRole('button', { name: /Try again/i }))
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
expect(screen.queryByText('Too many attempts')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('error_lookup_failed terminal state', () => {
|
|
it('shows "Could not verify the code" heading', async () => {
|
|
await reachTerminal(new MockDeviceFlowError('server_error', 500))
|
|
await screen.findByText('Could not verify the code')
|
|
})
|
|
|
|
it('ghost button resets to code_entry', async () => {
|
|
await reachTerminal(new MockDeviceFlowError('server_error', 500))
|
|
await screen.findByText('Could not verify the code')
|
|
fireEvent.click(screen.getByRole('button', { name: /Try again/i }))
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
expect(screen.queryByText('Could not verify the code')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('sso_error inline banner on the code-entry page', () => {
|
|
const SSO_BANNER_COPY = /identity is linked to a Dify account/i
|
|
|
|
it('shows the error banner with friendly copy when sso_error is present', async () => {
|
|
mockSearchParams = { sso_error: 'email_belongs_to_dify_account' }
|
|
render(<DevicePage />)
|
|
expect(await screen.findByText(SSO_BANNER_COPY)).toBeInTheDocument()
|
|
})
|
|
|
|
it('keeps the code-entry screen visible (error on main page, not a separate view)', async () => {
|
|
mockSearchParams = { sso_error: 'email_belongs_to_dify_account' }
|
|
render(<DevicePage />)
|
|
await screen.findByText(SSO_BANNER_COPY)
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: /Continue/i })).toBeInTheDocument()
|
|
})
|
|
|
|
it('does not surface the raw backend error code', async () => {
|
|
mockSearchParams = { sso_error: 'email_belongs_to_dify_account' }
|
|
render(<DevicePage />)
|
|
await screen.findByText(SSO_BANNER_COPY)
|
|
expect(screen.queryByText('email_belongs_to_dify_account')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('does not scrub the param on mount (regression: error was wiped by router.replace)', async () => {
|
|
mockSearchParams = { sso_error: 'email_belongs_to_dify_account' }
|
|
render(<DevicePage />)
|
|
await screen.findByText(SSO_BANNER_COPY)
|
|
expect(mockReplace).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('shows no banner when sso_error is absent', () => {
|
|
render(<DevicePage />)
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
expect(screen.queryByText(SSO_BANNER_COPY)).not.toBeInTheDocument()
|
|
})
|
|
})
|