mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
refactor(web): migrate console auth, account and workspace calls in service/common
Sign-in, setup/init, forgot/reset password, email-code login, invitation activation, account deletion, email change, ownership transfer, member removal, workspace rename, default model, model enable/disable and the compliance download now call the console contract directly. Only the web-app (public API) helpers and remote file upload remain in common.ts. The generated response types exposed unreachable branches: console login never returns an access token in the body, and email-code verification and password-reset requests raise errors instead of returning failure codes, so those branches are removed. Default model and parameter-rule responses are normalized where they enter the model provider UI types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7915c5e5ff
commit
bdf872c810
@@ -1814,11 +1814,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/hooks.ts": {
|
||||
"@tanstack/query/prefer-query-options": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 2
|
||||
@@ -4067,7 +4062,7 @@
|
||||
"count": 1
|
||||
},
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 4
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/datasets.ts": {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { emailRegex } from '@/config'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { sendResetPasswordCode } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
|
||||
export default function CheckCode() {
|
||||
const { t } = useTranslation(['login'])
|
||||
@@ -37,17 +37,13 @@ export default function CheckCode() {
|
||||
return
|
||||
}
|
||||
setIsLoading(true)
|
||||
const res = await sendResetPasswordCode(email, locale)
|
||||
const res = await consoleClient.forgotPassword.post({ body: { email, language: locale } })
|
||||
if (res.result === 'success') {
|
||||
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set('token', encodeURIComponent(res.data))
|
||||
params.set('token', encodeURIComponent(res.data ?? ''))
|
||||
params.set('email', encodeURIComponent(email))
|
||||
router.push(`/webapp-reset-password/check-code?${params.toString()}`)
|
||||
} else if (res.code === 'account_not_found') {
|
||||
toast.error(t(($) => $['error.registrationNotAllowed'], { ns: 'login' }))
|
||||
} else {
|
||||
toast.error(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { checkEmailExisted, resetEmail, sendVerifyCode, verifyEmail } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { createHttpError } from '@/service/http-error'
|
||||
import EmailChangeModal from '../email-change-modal'
|
||||
|
||||
@@ -11,11 +11,24 @@ vi.mock('@/app/notifications', () => ({
|
||||
toast: { error: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
checkEmailExisted: vi.fn(),
|
||||
resetEmail: vi.fn(),
|
||||
sendVerifyCode: vi.fn(),
|
||||
verifyEmail: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
account: {
|
||||
changeEmail: {
|
||||
checkEmailUnique: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
reset: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
post: vi.fn(),
|
||||
validity: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
@@ -25,14 +38,18 @@ vi.mock('@/service/use-common', () => ({
|
||||
describe('EmailChangeModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(sendVerifyCode).mockResolvedValue({ result: 'success', data: 'email-token' })
|
||||
vi.mocked(verifyEmail).mockResolvedValue({
|
||||
vi.mocked(consoleClient.account.changeEmail.post).mockResolvedValue({
|
||||
result: 'success',
|
||||
data: 'email-token',
|
||||
})
|
||||
vi.mocked(consoleClient.account.changeEmail.validity.post).mockResolvedValue({
|
||||
email: 'old@example.com',
|
||||
is_valid: true,
|
||||
token: 'verified-token',
|
||||
})
|
||||
vi.mocked(checkEmailExisted).mockResolvedValue({ result: 'success' })
|
||||
vi.mocked(consoleClient.account.changeEmail.checkEmailUnique.post).mockResolvedValue({
|
||||
result: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
it('submits the labeled verification fields with Enter through both email phases', async () => {
|
||||
@@ -46,10 +63,12 @@ describe('EmailChangeModal', () => {
|
||||
name: 'accountSettings.account.changeEmail.codeLabel',
|
||||
})
|
||||
await user.type(originCode, '123456{Enter}')
|
||||
expect(verifyEmail).toHaveBeenCalledWith({
|
||||
email: 'old@example.com',
|
||||
code: '123456',
|
||||
token: 'email-token',
|
||||
expect(consoleClient.account.changeEmail.validity.post).toHaveBeenCalledWith({
|
||||
body: {
|
||||
email: 'old@example.com',
|
||||
code: '123456',
|
||||
token: 'email-token',
|
||||
},
|
||||
})
|
||||
|
||||
const email = await screen.findByRole('textbox', {
|
||||
@@ -62,10 +81,12 @@ describe('EmailChangeModal', () => {
|
||||
).toBeEnabled(),
|
||||
)
|
||||
await user.type(email, '{Enter}')
|
||||
expect(sendVerifyCode).toHaveBeenLastCalledWith({
|
||||
email: 'new@example.com',
|
||||
phase: 'new_email',
|
||||
token: 'verified-token',
|
||||
expect(consoleClient.account.changeEmail.post).toHaveBeenLastCalledWith({
|
||||
body: {
|
||||
email: 'new@example.com',
|
||||
phase: 'new_email',
|
||||
token: 'verified-token',
|
||||
},
|
||||
})
|
||||
|
||||
const newCode = await screen.findByRole('textbox', {
|
||||
@@ -74,16 +95,18 @@ describe('EmailChangeModal', () => {
|
||||
expect(newCode).toHaveValue('')
|
||||
await user.type(newCode, '654321{Enter}')
|
||||
await waitFor(() =>
|
||||
expect(resetEmail).toHaveBeenCalledWith({
|
||||
new_email: 'new@example.com',
|
||||
token: 'verified-token',
|
||||
expect(consoleClient.account.changeEmail.reset.post).toHaveBeenCalledWith({
|
||||
body: {
|
||||
new_email: 'new@example.com',
|
||||
token: 'verified-token',
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(mockLogout).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('associates an unavailable-email error and clears it when the user edits the address', async () => {
|
||||
vi.mocked(checkEmailExisted).mockRejectedValue(
|
||||
vi.mocked(consoleClient.account.changeEmail.checkEmailUnique.post).mockRejectedValue(
|
||||
createHttpError(new Response(null, { status: 400 }), { code: 'email_already_in_use' }),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -112,7 +135,7 @@ describe('EmailChangeModal', () => {
|
||||
})
|
||||
|
||||
it('shows the server message when sending a verification code fails', async () => {
|
||||
vi.mocked(sendVerifyCode).mockRejectedValueOnce(
|
||||
vi.mocked(consoleClient.account.changeEmail.post).mockRejectedValueOnce(
|
||||
createHttpError(new Response(null, { status: 429 }), {
|
||||
code: 'rate_limit_exceeded',
|
||||
message: 'Try again in one minute',
|
||||
@@ -130,12 +153,12 @@ describe('EmailChangeModal', () => {
|
||||
'Error sending verification code: Try again in one minute',
|
||||
),
|
||||
)
|
||||
expect(verifyEmail).not.toHaveBeenCalled()
|
||||
expect(consoleClient.account.changeEmail.validity.post).not.toHaveBeenCalled()
|
||||
expect(mockLogout).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('preserves the verification step after an HTTP error and allows another attempt', async () => {
|
||||
vi.mocked(verifyEmail).mockRejectedValueOnce(
|
||||
vi.mocked(consoleClient.account.changeEmail.validity.post).mockRejectedValueOnce(
|
||||
createHttpError(new Response(null, { status: 400 }), {
|
||||
code: 'invalid_code',
|
||||
message: 'Verification code has expired',
|
||||
@@ -161,7 +184,7 @@ describe('EmailChangeModal', () => {
|
||||
expect(
|
||||
screen.queryByRole('textbox', { name: 'accountSettings.account.changeEmail.emailLabel' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(resetEmail).not.toHaveBeenCalled()
|
||||
expect(consoleClient.account.changeEmail.reset.post).not.toHaveBeenCalled()
|
||||
|
||||
await user.type(originCode, '654321{Enter}')
|
||||
|
||||
@@ -170,10 +193,12 @@ describe('EmailChangeModal', () => {
|
||||
name: 'accountSettings.account.changeEmail.emailLabel',
|
||||
}),
|
||||
).toBeVisible()
|
||||
expect(verifyEmail).toHaveBeenLastCalledWith({
|
||||
email: 'old@example.com',
|
||||
code: '654321',
|
||||
token: 'email-token',
|
||||
expect(consoleClient.account.changeEmail.validity.post).toHaveBeenLastCalledWith({
|
||||
body: {
|
||||
email: 'old@example.com',
|
||||
code: '654321',
|
||||
token: 'email-token',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useDebounceFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { checkEmailExisted, resetEmail, sendVerifyCode, verifyEmail } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { getErrorMessage } from '@/utils/error-message'
|
||||
|
||||
@@ -67,10 +67,8 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
|
||||
|
||||
const sendEmail = async (email: string, isOrigin: boolean, token?: string) => {
|
||||
try {
|
||||
const res = await sendVerifyCode({
|
||||
email,
|
||||
phase: isOrigin ? 'old_email' : 'new_email',
|
||||
token,
|
||||
const res = await consoleClient.account.changeEmail.post({
|
||||
body: { email, phase: isOrigin ? 'old_email' : 'new_email', token },
|
||||
})
|
||||
startCount()
|
||||
if (res.data) setStepToken(res.data)
|
||||
@@ -86,10 +84,8 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
|
||||
callback?: (token: string) => void,
|
||||
) => {
|
||||
try {
|
||||
const res = await verifyEmail({
|
||||
email,
|
||||
code,
|
||||
token,
|
||||
const res = await consoleClient.account.changeEmail.validity.post({
|
||||
body: { email, code, token },
|
||||
})
|
||||
if (res.is_valid) {
|
||||
setStepToken(res.token)
|
||||
@@ -119,9 +115,10 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
|
||||
const checkNewEmailExisted = async (email: string) => {
|
||||
setIsCheckingEmail(true)
|
||||
try {
|
||||
await checkEmailExisted({
|
||||
email,
|
||||
})
|
||||
await consoleClient.account.changeEmail.checkEmailUnique.post(
|
||||
{ body: { email } },
|
||||
{ context: { silent: true } },
|
||||
)
|
||||
if (latestEmailRef.current !== email) return
|
||||
setNewEmailExited(false)
|
||||
setUnAvailableEmail(false)
|
||||
@@ -172,9 +169,8 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
|
||||
const { mutateAsync: logout } = useLogout()
|
||||
const updateEmail = async (lastToken: string) => {
|
||||
try {
|
||||
await resetEmail({
|
||||
new_email: mail.trim(),
|
||||
token: lastToken,
|
||||
await consoleClient.account.changeEmail.reset.post({
|
||||
body: { new_email: mail.trim(), token: lastToken },
|
||||
})
|
||||
await logout()
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { create } from 'zustand'
|
||||
import {
|
||||
sendDeleteAccountCode,
|
||||
submitDeleteAccountFeedback,
|
||||
verifyDeleteAccountCode,
|
||||
} from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
|
||||
type State = {
|
||||
sendEmailToken: string
|
||||
@@ -20,7 +16,7 @@ export function useSendDeleteAccountEmail() {
|
||||
const updateEmailToken = useAccountDeleteStore((state) => state.setSendEmailToken)
|
||||
return useMutation({
|
||||
mutationKey: ['delete-account'],
|
||||
mutationFn: sendDeleteAccountCode,
|
||||
mutationFn: () => consoleClient.account.delete.verify.get(),
|
||||
onSuccess: (ret) => {
|
||||
if (ret.result === 'success') updateEmailToken(ret.data)
|
||||
},
|
||||
@@ -30,13 +26,15 @@ export function useSendDeleteAccountEmail() {
|
||||
export function useConfirmDeleteAccount() {
|
||||
return useMutation({
|
||||
mutationKey: ['confirm-delete-account'],
|
||||
mutationFn: verifyDeleteAccountCode,
|
||||
mutationFn: (body: { code: string; token: string }) =>
|
||||
consoleClient.account.delete.post({ body }),
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteAccountFeedback() {
|
||||
return useMutation({
|
||||
mutationKey: ['delete-account-feedback'],
|
||||
mutationFn: submitDeleteAccountFeedback,
|
||||
mutationFn: (body: { feedback: string; email: string }) =>
|
||||
consoleClient.account.delete.feedback.post({ body }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,14 +7,23 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render as renderWithoutPricing, screen, waitFor } from '@testing-library/react'
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { getDocDownloadUrl } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { seedFeatures } from '@/test/console/query-data'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import Compliance from '../compliance'
|
||||
|
||||
const onPricingUrlUpdate = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/service/common', () => ({ getDocDownloadUrl: vi.fn() }))
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
compliance: {
|
||||
download: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
vi.mock('@/service/base', () => ({
|
||||
request: vi.fn(() => new Promise(() => {})),
|
||||
sseGeneratorPost: vi.fn(),
|
||||
@@ -83,7 +92,9 @@ describe('Compliance', () => {
|
||||
|
||||
it('keeps GDPR downloadable while plan-dependent documents wait for features', async () => {
|
||||
queryClient.clear()
|
||||
vi.mocked(getDocDownloadUrl).mockResolvedValue({ url: 'https://example.com/gdpr.pdf' })
|
||||
vi.mocked(consoleClient.compliance.download.get).mockResolvedValue({
|
||||
url: 'https://example.com/gdpr.pdf',
|
||||
})
|
||||
openMenuAndRender()
|
||||
expect(screen.queryByText('common.compliance.soc2Type1')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('common.compliance.soc2Type2')).not.toBeInTheDocument()
|
||||
@@ -94,7 +105,12 @@ describe('Compliance', () => {
|
||||
)
|
||||
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByText('common.compliance.gdpr'))
|
||||
await waitFor(() => expect(getDocDownloadUrl).toHaveBeenCalledWith('GDPR'))
|
||||
await waitFor(() =>
|
||||
expect(consoleClient.compliance.download.get).toHaveBeenCalledWith(
|
||||
{ query: { doc_name: 'GDPR' } },
|
||||
{ context: { silent: true } },
|
||||
),
|
||||
)
|
||||
expect(onPricingUrlUpdate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -150,7 +166,7 @@ describe('Compliance', () => {
|
||||
it('should trigger download mutation successfully', async () => {
|
||||
// Arrange
|
||||
const mockUrl = 'http://example.com/doc.pdf'
|
||||
vi.mocked(getDocDownloadUrl).mockResolvedValue({ url: mockUrl })
|
||||
vi.mocked(consoleClient.compliance.download.get).mockResolvedValue({ url: mockUrl })
|
||||
seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } })
|
||||
|
||||
// Act
|
||||
@@ -160,7 +176,7 @@ describe('Compliance', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(getDocDownloadUrl).toHaveBeenCalled()
|
||||
expect(consoleClient.compliance.download.get).toHaveBeenCalled()
|
||||
expect(downloadUrl).toHaveBeenCalledWith({ url: mockUrl })
|
||||
expect(toastSuccessSpy).toHaveBeenCalledWith('common.operation.downloadSuccess')
|
||||
})
|
||||
@@ -168,7 +184,9 @@ describe('Compliance', () => {
|
||||
|
||||
it('should handle download mutation error', async () => {
|
||||
// Arrange
|
||||
vi.mocked(getDocDownloadUrl).mockRejectedValue(new Error('Download failed'))
|
||||
vi.mocked(consoleClient.compliance.download.get).mockRejectedValue(
|
||||
new Error('Download failed'),
|
||||
)
|
||||
seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } })
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
@@ -179,7 +197,7 @@ describe('Compliance', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(getDocDownloadUrl).toHaveBeenCalled()
|
||||
expect(consoleClient.compliance.download.get).toHaveBeenCalled()
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith('common.operation.downloadFailed')
|
||||
})
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
@@ -216,7 +234,7 @@ describe('Compliance', () => {
|
||||
it('should show spinner and guard against duplicate download when isPending is true', async () => {
|
||||
// Arrange
|
||||
let resolveDownload: (value: { url: string }) => void
|
||||
vi.mocked(getDocDownloadUrl).mockImplementation(
|
||||
vi.mocked(consoleClient.compliance.download.get).mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveDownload = resolve
|
||||
@@ -248,7 +266,7 @@ describe('Compliance', () => {
|
||||
|
||||
it('should not call downloadCompliance again while pending', async () => {
|
||||
let resolveDownload: (value: { url: string }) => void
|
||||
vi.mocked(getDocDownloadUrl).mockImplementation(
|
||||
vi.mocked(consoleClient.compliance.download.get).mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveDownload = resolve
|
||||
@@ -267,7 +285,7 @@ describe('Compliance', () => {
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(menuItem).toHaveAttribute('aria-disabled', 'true')
|
||||
expect(getDocDownloadUrl).toHaveBeenCalledTimes(1)
|
||||
expect(consoleClient.compliance.download.get).toHaveBeenCalledTimes(1)
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
)
|
||||
@@ -283,7 +301,7 @@ describe('Compliance', () => {
|
||||
{ timeout: 10000 },
|
||||
)
|
||||
// getDocDownloadUrl should still have only been called once
|
||||
expect(getDocDownloadUrl).toHaveBeenCalledTimes(1)
|
||||
expect(consoleClient.compliance.download.get).toHaveBeenCalledTimes(1)
|
||||
}, 20000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,8 +22,7 @@ import {
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { getDocDownloadUrl } from '@/service/common'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import PremiumBadge from '../../base/premium-badge'
|
||||
import { MenuItemContent } from './menu-item-content'
|
||||
@@ -117,7 +116,10 @@ function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProp
|
||||
mutationKey: ['downloadCompliance', docName],
|
||||
mutationFn: async () => {
|
||||
try {
|
||||
const ret = await getDocDownloadUrl(docName)
|
||||
const ret = await consoleClient.compliance.download.get(
|
||||
{ query: { doc_name: docName } },
|
||||
{ context: { silent: true } },
|
||||
)
|
||||
downloadUrl({ url: ret.url })
|
||||
toast.success(t(($) => $['operation.downloadSuccess'], { ns: 'common' }))
|
||||
} catch (error) {
|
||||
|
||||
+21
-8
@@ -3,8 +3,7 @@ import type { Member } from '@/models/common'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { deleteMemberOrCancelInvitation } from '@/service/common'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import MemberMenu from '../member-menu'
|
||||
|
||||
@@ -21,8 +20,19 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
deleteMemberOrCancelInvitation: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
workspaces: {
|
||||
current: {
|
||||
members: {
|
||||
byMemberId: {
|
||||
delete: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
vi.mock('@/app/notifications', () => ({
|
||||
toast: {
|
||||
@@ -81,7 +91,10 @@ describe('MemberMenu', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUpdateRolesOfMember.mockResolvedValue(undefined)
|
||||
vi.mocked(deleteMemberOrCancelInvitation).mockResolvedValue({ result: 'success' })
|
||||
vi.mocked(consoleClient.workspaces.current.members.byMemberId.delete).mockResolvedValue({
|
||||
result: 'success',
|
||||
tenant_id: 'tenant-1',
|
||||
})
|
||||
vi.mocked(mockUpdateRolesOfMemberHook).mockReturnValue({
|
||||
mutateAsync: mockUpdateRolesOfMember,
|
||||
})
|
||||
@@ -181,13 +194,13 @@ describe('MemberMenu', () => {
|
||||
name: /workspaceMembers\.members\.removeFromTeamConfirmTitle:\{"memberName":"Member User"\}/i,
|
||||
})
|
||||
expect(dialog).toHaveTextContent('workspaceMembers.members.removeFromTeamConfirmDescription')
|
||||
expect(deleteMemberOrCancelInvitation).not.toHaveBeenCalled()
|
||||
expect(consoleClient.workspaces.current.members.byMemberId.delete).not.toHaveBeenCalled()
|
||||
|
||||
await user.click(within(dialog).getByRole('button', { name: /common\.operation\.confirm/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteMemberOrCancelInvitation).toHaveBeenCalledWith({
|
||||
url: '/workspaces/current/members/member-1',
|
||||
expect(consoleClient.workspaces.current.members.byMemberId.delete).toHaveBeenCalledWith({
|
||||
params: { member_id: 'member-1' },
|
||||
})
|
||||
})
|
||||
expect(queryClient.getQueryState(membersQueryKey)?.isInvalidated).toBe(true)
|
||||
|
||||
+17
-9
@@ -2,7 +2,7 @@ import type { ConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { vi } from 'vite-plus/test'
|
||||
import { updateWorkspaceInfo } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { render } from '@/test/console/render'
|
||||
import EditWorkspaceModal from '../index'
|
||||
|
||||
@@ -21,7 +21,16 @@ vi.mock('@/context/workspace-state', async () => {
|
||||
return createWorkspaceStateModuleMock(() => mockConsoleState.current)
|
||||
})
|
||||
|
||||
vi.mock('@/service/common')
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
workspaces: {
|
||||
info: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
vi.mock('@/app/notifications', () => ({
|
||||
default: {
|
||||
notify: (args: unknown) => toastMocks.mockNotify(args),
|
||||
@@ -92,7 +101,7 @@ describe('EditWorkspaceModal', () => {
|
||||
assign: mockAssign,
|
||||
origin: 'http://localhost',
|
||||
})
|
||||
vi.mocked(updateWorkspaceInfo).mockResolvedValue({
|
||||
vi.mocked(consoleClient.workspaces.info.post).mockResolvedValue({
|
||||
result: 'success',
|
||||
tenant: { id: 'workspace-id' },
|
||||
})
|
||||
@@ -105,8 +114,7 @@ describe('EditWorkspaceModal', () => {
|
||||
await user.click(getSaveButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateWorkspaceInfo).toHaveBeenCalledWith({
|
||||
url: '/workspaces/info',
|
||||
expect(consoleClient.workspaces.info.post).toHaveBeenCalledWith({
|
||||
body: { name: 'Renamed Workspace' },
|
||||
})
|
||||
expect(mockAssign).toHaveBeenCalledWith('http://localhost')
|
||||
@@ -118,7 +126,7 @@ describe('EditWorkspaceModal', () => {
|
||||
it('should expose the saving label and prevent duplicate form submission', async () => {
|
||||
const user = userEvent.setup()
|
||||
let rejectUpdate!: (reason: Error) => void
|
||||
vi.mocked(updateWorkspaceInfo).mockImplementationOnce(
|
||||
vi.mocked(consoleClient.workspaces.info.post).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((_, reject) => {
|
||||
rejectUpdate = reject
|
||||
@@ -134,7 +142,7 @@ describe('EditWorkspaceModal', () => {
|
||||
const savingButton = screen.getByRole('button', { name: /operation\.saving/i })
|
||||
expect(savingButton).toHaveAttribute('aria-disabled', 'true')
|
||||
fireEvent.submit(savingButton.closest('form')!)
|
||||
expect(updateWorkspaceInfo).toHaveBeenCalledOnce()
|
||||
expect(consoleClient.workspaces.info.post).toHaveBeenCalledOnce()
|
||||
|
||||
rejectUpdate(new Error('update failed'))
|
||||
await waitFor(() => {
|
||||
@@ -145,7 +153,7 @@ describe('EditWorkspaceModal', () => {
|
||||
it('should show error toast when update fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
vi.mocked(updateWorkspaceInfo).mockRejectedValue(new Error('update failed'))
|
||||
vi.mocked(consoleClient.workspaces.info.post).mockRejectedValue(new Error('update failed'))
|
||||
|
||||
renderModal()
|
||||
|
||||
@@ -193,7 +201,7 @@ describe('EditWorkspaceModal', () => {
|
||||
|
||||
fireEvent.submit(form!)
|
||||
|
||||
expect(updateWorkspaceInfo).not.toHaveBeenCalled()
|
||||
expect(consoleClient.workspaces.info.post).not.toHaveBeenCalled()
|
||||
expect(mockNotify).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
+2
-3
@@ -9,7 +9,7 @@ import { useId, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state'
|
||||
import { updateWorkspaceInfo } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
|
||||
type IEditWorkspaceModalProps = {
|
||||
onCancel: () => void
|
||||
@@ -38,8 +38,7 @@ const EditWorkspaceModal = ({ onCancel }: IEditWorkspaceModalProps) => {
|
||||
if (isSubmitting || isSaveUnavailable) return
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await updateWorkspaceInfo({
|
||||
url: '/workspaces/info',
|
||||
await consoleClient.workspaces.info.post({
|
||||
body: {
|
||||
name: normalizedName,
|
||||
},
|
||||
|
||||
@@ -22,8 +22,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { deleteMemberOrCancelInvitation } from '@/service/common'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
import { createSelectedRolePlaceholder } from '../workspace-role-list-query'
|
||||
import AssignRolesModal from './assign-roles-modal'
|
||||
|
||||
@@ -95,7 +94,9 @@ const MemberMenu = ({
|
||||
const handleRemove = useCallback(async () => {
|
||||
setRemoving(true)
|
||||
try {
|
||||
await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` })
|
||||
await consoleClient.workspaces.current.members.byMemberId.delete({
|
||||
params: { member_id: member.id },
|
||||
})
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.workspaces.current.members.get.key(),
|
||||
})
|
||||
|
||||
+84
-22
@@ -2,7 +2,7 @@ import type { ConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { vi } from 'vite-plus/test'
|
||||
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { createAccountProfileQueryWrapper } from '@/test/console/account-profile'
|
||||
import { render } from '@/test/console/render'
|
||||
import TransferOwnershipModal from '../index'
|
||||
@@ -31,7 +31,28 @@ vi.mock('@/context/workspace-state', async () => {
|
||||
return createWorkspaceStateModuleMock(() => mockConsoleState.current)
|
||||
})
|
||||
|
||||
vi.mock('@/service/common')
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
workspaces: {
|
||||
current: {
|
||||
members: {
|
||||
byMemberId: {
|
||||
ownerTransfer: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
ownerTransferCheck: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
sendOwnerTransferConfirmEmail: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
vi.mock('@/app/notifications', () => ({
|
||||
default: {
|
||||
notify: (args: unknown) => toastMocks.mockNotify(args),
|
||||
@@ -101,15 +122,21 @@ describe('TransferOwnershipModal', () => {
|
||||
isValid?: boolean
|
||||
token?: string
|
||||
} = {}) => {
|
||||
vi.mocked(sendOwnerEmail).mockResolvedValue({
|
||||
vi.mocked(
|
||||
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
|
||||
).mockResolvedValue({
|
||||
data: 'step-token',
|
||||
result: 'success',
|
||||
} as unknown as Awaited<ReturnType<typeof sendOwnerEmail>>)
|
||||
vi.mocked(verifyOwnerEmail).mockResolvedValue({
|
||||
} as unknown as Awaited<
|
||||
ReturnType<typeof consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post>
|
||||
>)
|
||||
vi.mocked(consoleClient.workspaces.current.members.ownerTransferCheck.post).mockResolvedValue({
|
||||
is_valid: isValid,
|
||||
token,
|
||||
result: 'success',
|
||||
} as unknown as Awaited<ReturnType<typeof verifyOwnerEmail>>)
|
||||
} as unknown as Awaited<
|
||||
ReturnType<typeof consoleClient.workspaces.current.members.ownerTransferCheck.post>
|
||||
>)
|
||||
}
|
||||
|
||||
const goToTransferStep = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
@@ -129,9 +156,13 @@ describe('TransferOwnershipModal', () => {
|
||||
it('should complete ownership transfer flow through all steps', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockEmailVerification()
|
||||
vi.mocked(ownershipTransfer).mockResolvedValue({
|
||||
vi.mocked(
|
||||
consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post,
|
||||
).mockResolvedValue({
|
||||
result: 'success',
|
||||
} as unknown as Awaited<ReturnType<typeof ownershipTransfer>>)
|
||||
} as unknown as Awaited<
|
||||
ReturnType<typeof consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post>
|
||||
>)
|
||||
|
||||
renderModal()
|
||||
await goToTransferStep(user)
|
||||
@@ -140,7 +171,12 @@ describe('TransferOwnershipModal', () => {
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(ownershipTransfer).toHaveBeenCalledWith('new-owner-id', { token: 'final-token' })
|
||||
expect(
|
||||
consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post,
|
||||
).toHaveBeenCalledWith({
|
||||
params: { member_id: 'new-owner-id' },
|
||||
body: { token: 'final-token' },
|
||||
})
|
||||
expect(window.location.reload).toHaveBeenCalled()
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
@@ -149,10 +185,14 @@ describe('TransferOwnershipModal', () => {
|
||||
|
||||
it('should handle timer countdown and resend', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.mocked(sendOwnerEmail).mockResolvedValue({
|
||||
vi.mocked(
|
||||
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
|
||||
).mockResolvedValue({
|
||||
data: 'token',
|
||||
result: 'success',
|
||||
} as unknown as Awaited<ReturnType<typeof sendOwnerEmail>>)
|
||||
} as unknown as Awaited<
|
||||
ReturnType<typeof consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post>
|
||||
>)
|
||||
|
||||
renderModal()
|
||||
// Trigger the email send (which starts the timer)
|
||||
@@ -181,7 +221,9 @@ describe('TransferOwnershipModal', () => {
|
||||
await act(async () => {
|
||||
fireEvent.click(resendBtn)
|
||||
})
|
||||
expect(sendOwnerEmail).toHaveBeenCalledTimes(2)
|
||||
expect(
|
||||
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
|
||||
).toHaveBeenCalledTimes(2)
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
@@ -205,7 +247,9 @@ describe('TransferOwnershipModal', () => {
|
||||
it('should show error when verifying email throws an error', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockEmailVerification()
|
||||
vi.mocked(verifyOwnerEmail).mockRejectedValue(new Error('verification crash'))
|
||||
vi.mocked(consoleClient.workspaces.current.members.ownerTransferCheck.post).mockRejectedValue(
|
||||
new Error('verification crash'),
|
||||
)
|
||||
|
||||
renderModal()
|
||||
await goToTransferStep(user)
|
||||
@@ -222,7 +266,9 @@ describe('TransferOwnershipModal', () => {
|
||||
|
||||
it('should not show a modal-level toast and should stay on start step when sending verification email fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(sendOwnerEmail).mockRejectedValue(new Error('network error'))
|
||||
vi.mocked(
|
||||
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
|
||||
).mockRejectedValue(new Error('network error'))
|
||||
renderModal()
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /members\.transferModal\.sendVerifyCode/i }),
|
||||
@@ -231,7 +277,9 @@ describe('TransferOwnershipModal', () => {
|
||||
// The base service layer surfaces the real backend error. The modal itself
|
||||
// must NOT show an additional toast (e.g. "Error sending verification code: undefined").
|
||||
await waitFor(() => {
|
||||
expect(sendOwnerEmail).toHaveBeenCalled()
|
||||
expect(
|
||||
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
|
||||
).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockNotify).not.toHaveBeenCalled()
|
||||
// Should remain on the start step instead of advancing to the verify step.
|
||||
@@ -243,7 +291,9 @@ describe('TransferOwnershipModal', () => {
|
||||
it('should show error when ownership transfer fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockEmailVerification()
|
||||
vi.mocked(ownershipTransfer).mockRejectedValue(new Error('transfer failed'))
|
||||
vi.mocked(
|
||||
consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post,
|
||||
).mockRejectedValue(new Error('transfer failed'))
|
||||
renderModal()
|
||||
await goToTransferStep(user)
|
||||
await selectNewOwnerAndSubmit(user)
|
||||
@@ -260,10 +310,14 @@ describe('TransferOwnershipModal', () => {
|
||||
|
||||
it('should handle sendOwnerEmail returning null data', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(sendOwnerEmail).mockResolvedValue({
|
||||
vi.mocked(
|
||||
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
|
||||
).mockResolvedValue({
|
||||
data: null,
|
||||
result: 'success',
|
||||
} as unknown as Awaited<ReturnType<typeof sendOwnerEmail>>)
|
||||
} as unknown as Awaited<
|
||||
ReturnType<typeof consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post>
|
||||
>)
|
||||
|
||||
renderModal()
|
||||
await user.click(
|
||||
@@ -278,7 +332,9 @@ describe('TransferOwnershipModal', () => {
|
||||
|
||||
it('should swallow null rejection from sendOwnerEmail without showing a modal-level toast', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(sendOwnerEmail).mockRejectedValue(null)
|
||||
vi.mocked(
|
||||
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
|
||||
).mockRejectedValue(null)
|
||||
|
||||
renderModal()
|
||||
await user.click(
|
||||
@@ -286,7 +342,9 @@ describe('TransferOwnershipModal', () => {
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendOwnerEmail).toHaveBeenCalled()
|
||||
expect(
|
||||
consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post,
|
||||
).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockNotify).not.toHaveBeenCalled()
|
||||
expect(
|
||||
@@ -297,7 +355,9 @@ describe('TransferOwnershipModal', () => {
|
||||
it('should show fallback error prefix when verifyOwnerEmail throws null', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockEmailVerification()
|
||||
vi.mocked(verifyOwnerEmail).mockRejectedValue(null)
|
||||
vi.mocked(consoleClient.workspaces.current.members.ownerTransferCheck.post).mockRejectedValue(
|
||||
null,
|
||||
)
|
||||
|
||||
renderModal()
|
||||
await goToTransferStep(user)
|
||||
@@ -315,7 +375,9 @@ describe('TransferOwnershipModal', () => {
|
||||
it('should show fallback error prefix when ownershipTransfer throws null', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockEmailVerification()
|
||||
vi.mocked(ownershipTransfer).mockRejectedValue(null)
|
||||
vi.mocked(
|
||||
consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post,
|
||||
).mockRejectedValue(null)
|
||||
|
||||
renderModal()
|
||||
await goToTransferStep(user)
|
||||
|
||||
+9
-7
@@ -9,7 +9,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { currentWorkspaceAtom } from '@/context/workspace-state'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { getErrorMessage } from '@/utils/error-message'
|
||||
import MemberSelector from './member-selector'
|
||||
|
||||
@@ -62,15 +62,16 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||
)
|
||||
}
|
||||
const sendEmail = async () => {
|
||||
const res = await sendOwnerEmail({})
|
||||
const res = await consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post({
|
||||
body: {},
|
||||
})
|
||||
startCount()
|
||||
if (res.data) setStepToken(res.data)
|
||||
}
|
||||
const verifyEmailAddress = async (code: string, token: string, callback?: () => void) => {
|
||||
try {
|
||||
const res = await verifyOwnerEmail({
|
||||
code,
|
||||
token,
|
||||
const res = await consoleClient.workspaces.current.members.ownerTransferCheck.post({
|
||||
body: { code, token },
|
||||
})
|
||||
if (res.is_valid) {
|
||||
setStepToken(res.token)
|
||||
@@ -97,8 +98,9 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||
const handleTransfer = async () => {
|
||||
setIsTransfer(true)
|
||||
try {
|
||||
await ownershipTransfer(newOwner, {
|
||||
token: stepToken,
|
||||
await consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post({
|
||||
params: { member_id: newOwner },
|
||||
body: { token: stepToken },
|
||||
})
|
||||
globalThis.location.reload()
|
||||
} catch (error) {
|
||||
|
||||
+12
-16
@@ -9,7 +9,6 @@ import type {
|
||||
} from '../declarations'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { useLocale } from '#i18n'
|
||||
import { fetchDefaultModal } from '@/service/common'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
@@ -49,10 +48,6 @@ vi.mock('@tanstack/react-query', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
fetchDefaultModal: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
commonQueryKeys: {
|
||||
modelProviders: ['model-providers'],
|
||||
@@ -166,6 +161,9 @@ describe('hooks', () => {
|
||||
provider: {
|
||||
provider: 'openai',
|
||||
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
label: { en_US: 'Provider', zh_Hans: 'Provider' },
|
||||
supported_model_types: [],
|
||||
tenant_id: 'tenant-1',
|
||||
},
|
||||
model,
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
@@ -193,6 +191,9 @@ describe('hooks', () => {
|
||||
provider: {
|
||||
provider: 'anthropic',
|
||||
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
label: { en_US: 'Provider', zh_Hans: 'Provider' },
|
||||
supported_model_types: [],
|
||||
tenant_id: 'tenant-1',
|
||||
},
|
||||
model: 'claude-3',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
@@ -290,15 +291,6 @@ describe('hooks', () => {
|
||||
|
||||
expect(result.current.data).toEqual(mockDefaultModel)
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
|
||||
// Coverage for queryFn
|
||||
const queryCall = (useQuery as Mock).mock.calls.find(
|
||||
(call) => Array.isArray(call[0].queryKey) && call[0].queryKey[0] === 'default-model',
|
||||
)
|
||||
if (queryCall) {
|
||||
await queryCall[0].queryFn()
|
||||
expect(fetchDefaultModal).toHaveBeenCalled()
|
||||
}
|
||||
})
|
||||
|
||||
it('should return undefined when data is not available', () => {
|
||||
@@ -327,7 +319,9 @@ describe('hooks', () => {
|
||||
expect(useQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enabled: false,
|
||||
queryKey: ['default-model', ModelTypeEnum.textEmbedding],
|
||||
queryKey: consoleQuery.workspaces.current.defaultModel.get.queryKey({
|
||||
input: { query: { model_type: ModelTypeEnum.textEmbedding } },
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
@@ -686,7 +680,9 @@ describe('hooks', () => {
|
||||
})
|
||||
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: ['default-model', ModelTypeEnum.textGeneration],
|
||||
queryKey: consoleQuery.workspaces.current.defaultModel.get.queryKey({
|
||||
input: { query: { model_type: ModelTypeEnum.textGeneration } },
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { GetWorkspacesCurrentDefaultModelResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { Field } from '@/app/components/workflow/nodes/llm/types'
|
||||
|
||||
export type FormValue = Record<string, any>
|
||||
@@ -264,14 +265,7 @@ export type ModelProvider = {
|
||||
allow_custom_token?: boolean
|
||||
}
|
||||
|
||||
export type DefaultModelResponse = {
|
||||
model: string
|
||||
model_type: ModelTypeEnum
|
||||
provider: {
|
||||
provider: string
|
||||
icon_small: TypeWithI18N
|
||||
}
|
||||
}
|
||||
export type DefaultModelResponse = NonNullable<GetWorkspacesCurrentDefaultModelResponse['data']>
|
||||
|
||||
export type DefaultModel = {
|
||||
provider: string
|
||||
@@ -292,7 +286,7 @@ export type ModelParameterRule = {
|
||||
max?: number
|
||||
name: string
|
||||
precision?: number
|
||||
required: false
|
||||
required?: boolean
|
||||
type: string
|
||||
use_template?: string
|
||||
options?: string[]
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { getModelLanguage } from '@/i18n/metadata'
|
||||
import { fetchDefaultModal } from '@/service/common'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { commonQueryKeys, modelProviderDetailsQueryOptions } from '@/service/use-common'
|
||||
import { useExpandModelProviderList } from './atoms'
|
||||
@@ -95,14 +94,16 @@ export const useDefaultModel = (
|
||||
type: ModelTypeEnum,
|
||||
{ enabled = true }: ModelQueryOptions = {},
|
||||
) => {
|
||||
const { data, refetch, isPending } = useQuery({
|
||||
queryKey: commonQueryKeys.defaultModel(type),
|
||||
queryFn: () => fetchDefaultModal(`/workspaces/current/default-model?model_type=${type}`),
|
||||
enabled,
|
||||
})
|
||||
const { data, refetch, isPending } = useQuery(
|
||||
consoleQuery.workspaces.current.defaultModel.get.queryOptions({
|
||||
input: { query: { model_type: type } },
|
||||
enabled,
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
data: data?.data,
|
||||
// An unset default model comes back as null.
|
||||
data: data?.data ?? undefined,
|
||||
mutate: refetch,
|
||||
isLoading: enabled && isPending,
|
||||
}
|
||||
@@ -208,7 +209,11 @@ export const useInvalidateDefaultModel = () => {
|
||||
|
||||
return useCallback(
|
||||
(type: ModelTypeEnum) => {
|
||||
queryClient.invalidateQueries({ queryKey: commonQueryKeys.defaultModel(type) })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.workspaces.current.defaultModel.get.queryKey({
|
||||
input: { query: { model_type: type } },
|
||||
}),
|
||||
})
|
||||
},
|
||||
[queryClient],
|
||||
)
|
||||
|
||||
+7
-1
@@ -263,7 +263,13 @@ describe('ModelSelector', () => {
|
||||
defaultModel={{
|
||||
model: 'missing-model',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
provider: { provider, icon_small: { en_US: '', zh_Hans: '' } },
|
||||
provider: {
|
||||
provider,
|
||||
icon_small: { en_US: '', zh_Hans: '' },
|
||||
label: { en_US: provider, zh_Hans: provider },
|
||||
supported_model_types: [],
|
||||
tenant_id: 'tenant-1',
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
+33
-8
@@ -1,7 +1,7 @@
|
||||
import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen'
|
||||
import type { ModelItem, ModelProvider } from '../../declarations'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { disableModel, enableModel } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { render } from '@/test/console/render'
|
||||
import { ModelStatusEnum } from '../../declarations'
|
||||
@@ -28,9 +28,26 @@ vi.mock('@/context/permission-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
enableModel: vi.fn(),
|
||||
disableModel: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
workspaces: {
|
||||
current: {
|
||||
modelProviders: {
|
||||
byProvider: {
|
||||
models: {
|
||||
enable: {
|
||||
patch: vi.fn(),
|
||||
},
|
||||
disable: {
|
||||
patch: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks', () => ({
|
||||
@@ -119,7 +136,9 @@ describe('ModelListItem', () => {
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(disableModel).toHaveBeenCalled()
|
||||
expect(
|
||||
consoleClient.workspaces.current.modelProviders.byProvider.models.disable.patch,
|
||||
).toHaveBeenCalled()
|
||||
expect(onChange).toHaveBeenCalledWith('test-provider')
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
@@ -142,7 +161,9 @@ describe('ModelListItem', () => {
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(enableModel).toHaveBeenCalled()
|
||||
expect(
|
||||
consoleClient.workspaces.current.modelProviders.byProvider.models.enable.patch,
|
||||
).toHaveBeenCalled()
|
||||
expect(onChange).toHaveBeenCalledWith('test-provider')
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
@@ -292,8 +313,12 @@ describe('ModelListItem', () => {
|
||||
expect(statusSwitch).toHaveAttribute('aria-disabled', 'true')
|
||||
fireEvent.click(statusSwitch)
|
||||
expect(statusSwitch).toHaveAttribute('aria-checked', 'false')
|
||||
expect(enableModel).not.toHaveBeenCalled()
|
||||
expect(disableModel).not.toHaveBeenCalled()
|
||||
expect(
|
||||
consoleClient.workspaces.current.modelProviders.byProvider.models.enable.patch,
|
||||
).not.toHaveBeenCalled()
|
||||
expect(
|
||||
consoleClient.workspaces.current.modelProviders.byProvider.models.disable.patch,
|
||||
).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// isConfigurable=true: hover class on row
|
||||
|
||||
+9
-10
@@ -11,8 +11,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||
import { deploymentEditionAtom } from '@/features/system-features/state'
|
||||
import { disableModel, enableModel } from '@/service/common'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { ModelStatusEnum } from '../declarations'
|
||||
import { useUpdateModelList } from '../hooks'
|
||||
@@ -67,15 +66,15 @@ const ModelListItem = ({
|
||||
const toggleModelEnablingStatus = useCallback(
|
||||
async (enabled: boolean) => {
|
||||
if (enabled)
|
||||
await enableModel(
|
||||
`/workspaces/current/model-providers/${provider.provider}/models/enable`,
|
||||
{ model: model.model, model_type: model.model_type },
|
||||
)
|
||||
await consoleClient.workspaces.current.modelProviders.byProvider.models.enable.patch({
|
||||
params: { provider: provider.provider },
|
||||
body: { model: model.model, model_type: model.model_type },
|
||||
})
|
||||
else
|
||||
await disableModel(
|
||||
`/workspaces/current/model-providers/${provider.provider}/models/disable`,
|
||||
{ model: model.model, model_type: model.model_type },
|
||||
)
|
||||
await consoleClient.workspaces.current.modelProviders.byProvider.models.disable.patch({
|
||||
params: { provider: provider.provider },
|
||||
body: { model: model.model, model_type: model.model_type },
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: modelProviderModelListQueryKey,
|
||||
|
||||
+3
@@ -135,6 +135,9 @@ const mockModel: DefaultModelResponse = {
|
||||
provider: {
|
||||
provider: 'openai',
|
||||
icon_small: { en_US: '', zh_Hans: '' },
|
||||
label: { en_US: 'Provider', zh_Hans: 'Provider' },
|
||||
supported_model_types: [],
|
||||
tenant_id: 'tenant-1',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ import type {
|
||||
ModelStatus,
|
||||
ModelType,
|
||||
ModelWithProviderEntityResponse,
|
||||
ParameterRule,
|
||||
} from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { StaticImageData } from 'next/image'
|
||||
import type {
|
||||
CredentialFormSchemaSelect,
|
||||
CredentialFormSchemaTextInput,
|
||||
ModelItem,
|
||||
ModelParameterRule,
|
||||
TypeWithI18N,
|
||||
} from './declarations'
|
||||
import { ModelProviderQuotaGetPaid } from '@/types/model-provider'
|
||||
@@ -197,3 +199,24 @@ const normalizeModelProviderModel = (model: ModelWithProviderEntityResponse): Mo
|
||||
export const normalizeModelProviderModelsResponse = (response: {
|
||||
data: ModelWithProviderEntityResponse[]
|
||||
}): ModelItem[] => response.data.map(normalizeModelProviderModel)
|
||||
|
||||
const isParameterDefault = (value: unknown): value is ModelParameterRule['default'] =>
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'boolean' ||
|
||||
(Array.isArray(value) && value.every((item) => typeof item === 'string'))
|
||||
|
||||
export const normalizeModelParameterRule = (rule: ParameterRule): ModelParameterRule => ({
|
||||
name: rule.name,
|
||||
type: rule.type,
|
||||
label: normalizeModelLabel(rule.label),
|
||||
help: rule.help ? normalizeModelLabel(rule.help) : undefined,
|
||||
// `default` is untyped JSON in the console API response.
|
||||
default: isParameterDefault(rule.default) ? rule.default : undefined,
|
||||
min: rule.min ?? undefined,
|
||||
max: rule.max ?? undefined,
|
||||
precision: rule.precision ?? undefined,
|
||||
required: rule.required,
|
||||
use_template: rule.use_template ?? undefined,
|
||||
options: rule.options,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { changePasswordWithToken } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { useVerifyForgotPasswordToken } from '@/service/use-common'
|
||||
import ChangePasswordForm from './ChangePasswordForm'
|
||||
|
||||
@@ -15,8 +15,15 @@ vi.mock('@/service/use-common', () => ({
|
||||
useVerifyForgotPasswordToken: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
changePasswordWithToken: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
forgotPassword: {
|
||||
resets: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-document-title', () => ({
|
||||
@@ -28,7 +35,7 @@ vi.mock('@/utils/var', () => ({ basePath: '' }))
|
||||
|
||||
type UseVerifyResult = ReturnType<typeof useVerifyForgotPasswordToken>
|
||||
const mockUseVerify = vi.mocked(useVerifyForgotPasswordToken)
|
||||
const mockChangePassword = vi.mocked(changePasswordWithToken)
|
||||
const mockChangePassword = vi.mocked(consoleClient.forgotPassword.resets.post)
|
||||
const mockUseDocumentTitle = vi.mocked(useDocumentTitle)
|
||||
|
||||
const VALID_PASSWORD = 'ValidPass123!'
|
||||
@@ -79,7 +86,6 @@ describe('ChangePasswordForm', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockChangePassword).toHaveBeenCalledWith({
|
||||
url: '/forgot-password/resets',
|
||||
body: {
|
||||
token: T2,
|
||||
new_password: VALID_PASSWORD,
|
||||
@@ -165,7 +171,7 @@ describe('ChangePasswordForm', () => {
|
||||
it('prevents duplicate submissions while the password is changing', async () => {
|
||||
const user = userEvent.setup()
|
||||
let resolveRequest:
|
||||
| ((value: Awaited<ReturnType<typeof changePasswordWithToken>>) => void)
|
||||
| ((value: Awaited<ReturnType<typeof consoleClient.forgotPassword.resets.post>>) => void)
|
||||
| undefined
|
||||
mockChangePassword.mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { validPassword } from '@/config'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useSearchParams } from '@/next/navigation'
|
||||
import { changePasswordWithToken } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { useVerifyForgotPasswordToken } from '@/service/use-common'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
@@ -56,8 +56,7 @@ const ChangePasswordForm = () => {
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await changePasswordWithToken({
|
||||
url: '/forgot-password/resets',
|
||||
await consoleClient.forgotPassword.resets.post({
|
||||
body: {
|
||||
token: resetToken,
|
||||
new_password: formValues.password,
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
|
||||
import type { InitValidateStatusResponse } from '@/models/common'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import {
|
||||
fetchInitValidateStatus,
|
||||
fetchSetupStatus,
|
||||
sendForgotPasswordEmail,
|
||||
} from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import ForgotPasswordForm from './ForgotPasswordForm'
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
fetchSetupStatus: vi.fn(),
|
||||
fetchInitValidateStatus: vi.fn(),
|
||||
sendForgotPasswordEmail: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
setup: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
init: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
forgotPassword: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-document-title', () => ({
|
||||
@@ -20,13 +25,13 @@ vi.mock('@/hooks/use-document-title', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockFetchSetupStatus = vi.mocked(fetchSetupStatus)
|
||||
const mockFetchInitValidateStatus = vi.mocked(fetchInitValidateStatus)
|
||||
const mockSendForgotPasswordEmail = vi.mocked(sendForgotPasswordEmail)
|
||||
const mockFetchSetupStatus = vi.mocked(consoleClient.setup.get)
|
||||
const mockFetchInitValidateStatus = vi.mocked(consoleClient.init.get)
|
||||
const mockSendForgotPasswordEmail = vi.mocked(consoleClient.forgotPassword.post)
|
||||
const mockUseDocumentTitle = vi.mocked(useDocumentTitle)
|
||||
|
||||
const prepareLoadedState = () => {
|
||||
mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' } as SetupStatusResponse)
|
||||
mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' })
|
||||
mockFetchInitValidateStatus.mockResolvedValue({
|
||||
status: 'finished',
|
||||
} as InitValidateStatusResponse)
|
||||
@@ -90,7 +95,6 @@ describe('ForgotPasswordForm', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSendForgotPasswordEmail).toHaveBeenCalledWith({
|
||||
url: '/forgot-password',
|
||||
body: { email: 'test@example.com' },
|
||||
})
|
||||
})
|
||||
@@ -115,7 +119,6 @@ describe('ForgotPasswordForm', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSendForgotPasswordEmail).toHaveBeenCalledWith({
|
||||
url: '/forgot-password',
|
||||
body: { email: 'test@example.com' },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import type { InitValidateStatusResponse } from '@/models/common'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
@@ -11,11 +10,7 @@ import * as z from 'zod'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import {
|
||||
fetchInitValidateStatus,
|
||||
fetchSetupStatus,
|
||||
sendForgotPasswordEmail,
|
||||
} from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
type ForgotPasswordFormValues = {
|
||||
@@ -43,10 +38,7 @@ const ForgotPasswordForm = () => {
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
const res = await sendForgotPasswordEmail({
|
||||
url: '/forgot-password',
|
||||
body: { email },
|
||||
})
|
||||
const res = await consoleClient.forgotPassword.post({ body: { email } })
|
||||
if (res.result === 'success') setIsEmailSent(true)
|
||||
else console.error('Email verification failed')
|
||||
} catch (error) {
|
||||
@@ -57,8 +49,8 @@ const ForgotPasswordForm = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchSetupStatus().then(() => {
|
||||
fetchInitValidateStatus().then((res: InitValidateStatusResponse) => {
|
||||
consoleClient.setup.get().then(() => {
|
||||
consoleClient.init.get().then((res) => {
|
||||
if (res.status === 'not_started') window.location.href = `${basePath}/init`
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import type { InitValidateStatusResponse } from '@/models/common'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -7,7 +6,7 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { toast } from '@/app/notifications'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { fetchInitValidateStatus, initValidate } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
const InitPasswordPopup = () => {
|
||||
@@ -23,7 +22,7 @@ const InitPasswordPopup = () => {
|
||||
const handleValidation = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await initValidate({ body: { password } })
|
||||
const response = await consoleClient.init.post({ body: { password } })
|
||||
if (response.result === 'success') {
|
||||
setValidated(true)
|
||||
router.push('/install') // or render setup form
|
||||
@@ -37,7 +36,7 @@ const InitPasswordPopup = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchInitValidateStatus().then((res: InitValidateStatusResponse) => {
|
||||
consoleClient.init.get().then((res) => {
|
||||
if (res.status === 'finished') window.location.href = `${basePath}/install`
|
||||
else setLoading(false)
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
|
||||
import type { InitValidateStatusResponse } from '@/models/common'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { expectLoadingButton } from '@/test/button'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { encryptPassword } from '@/utils/encryption'
|
||||
@@ -21,20 +21,29 @@ vi.mock('@/hooks/use-document-title', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
fetchSetupStatus: vi.fn(),
|
||||
fetchInitValidateStatus: vi.fn(),
|
||||
setup: vi.fn(),
|
||||
login: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
setup: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
init: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
login: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const mockFetchSetupStatus = vi.mocked(fetchSetupStatus)
|
||||
const mockFetchInitValidateStatus = vi.mocked(fetchInitValidateStatus)
|
||||
const mockSetup = vi.mocked(setup)
|
||||
const mockLogin = vi.mocked(login)
|
||||
const mockFetchSetupStatus = vi.mocked(consoleClient.setup.get)
|
||||
const mockFetchInitValidateStatus = vi.mocked(consoleClient.init.get)
|
||||
const mockSetup = vi.mocked(consoleClient.setup.post)
|
||||
const mockLogin = vi.mocked(consoleClient.login.post)
|
||||
|
||||
const prepareLoadedState = () => {
|
||||
mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' } as SetupStatusResponse)
|
||||
mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' })
|
||||
mockFetchInitValidateStatus.mockResolvedValue({
|
||||
status: 'finished',
|
||||
} as InitValidateStatusResponse)
|
||||
@@ -161,7 +170,6 @@ describe('InstallForm', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockLogin).toHaveBeenCalledWith({
|
||||
url: '/login',
|
||||
body: {
|
||||
email: 'admin@example.com',
|
||||
password: encryptPassword('Password123'),
|
||||
@@ -232,7 +240,7 @@ describe('InstallForm', () => {
|
||||
})
|
||||
|
||||
it('should redirect to sign in when setup is finished', async () => {
|
||||
mockFetchSetupStatus.mockResolvedValue({ step: 'finished' } as SetupStatusResponse)
|
||||
mockFetchSetupStatus.mockResolvedValue({ step: 'finished' })
|
||||
|
||||
render(<InstallForm />)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
|
||||
import { zPostSetupBody } from '@dify/contracts/api/console/setup/zod.gen'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import {
|
||||
@@ -24,8 +23,7 @@ import { LICENSE_LINK } from '@/constants/link'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
import { encryptPassword as encodePassword } from '@/utils/encryption'
|
||||
|
||||
const accountFormSchema = zPostSetupBody.pick({ email: true, name: true, password: true }).extend({
|
||||
@@ -52,7 +50,7 @@ const InstallForm = () => {
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
// First, setup the admin account
|
||||
await setup({
|
||||
await consoleClient.setup.post({
|
||||
body: {
|
||||
...value,
|
||||
language: i18n.language,
|
||||
@@ -60,8 +58,7 @@ const InstallForm = () => {
|
||||
})
|
||||
|
||||
// Then, automatically login with the same credentials
|
||||
const loginRes = await login({
|
||||
url: '/login',
|
||||
const loginRes = await consoleClient.login.post({
|
||||
body: {
|
||||
email: value.email,
|
||||
password: encodePassword(value.password),
|
||||
@@ -82,11 +79,11 @@ const InstallForm = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchSetupStatus().then((res: SetupStatusResponse) => {
|
||||
consoleClient.setup.get().then((res) => {
|
||||
if (res.step === 'finished') {
|
||||
push('/signin')
|
||||
} else {
|
||||
fetchInitValidateStatus().then((res: InitValidateStatusResponse) => {
|
||||
consoleClient.init.get().then((res) => {
|
||||
if (res.status === 'not_started') push('/init')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useLocale } from '#i18n'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { sendResetPasswordCode } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import ResetPasswordPage from '../page'
|
||||
|
||||
const mockPush = vi.fn()
|
||||
@@ -27,8 +27,13 @@ vi.mock('@/next/navigation', () => ({
|
||||
useSearchParams: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
sendResetPasswordCode: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
forgotPassword: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
describe('ResetPasswordPage', () => {
|
||||
@@ -41,7 +46,10 @@ describe('ResetPasswordPage', () => {
|
||||
vi.mocked(useSearchParams).mockReturnValue(
|
||||
new URLSearchParams() as unknown as ReturnType<typeof useSearchParams>,
|
||||
)
|
||||
vi.mocked(sendResetPasswordCode).mockResolvedValue({ result: 'success', data: 'reset-token' })
|
||||
vi.mocked(consoleClient.forgotPassword.post).mockResolvedValue({
|
||||
result: 'success',
|
||||
data: 'reset-token',
|
||||
})
|
||||
})
|
||||
|
||||
it('labels the email field and submits it with Enter', async () => {
|
||||
@@ -54,7 +62,9 @@ describe('ResetPasswordPage', () => {
|
||||
await user.type(emailInput, 'user@example.com{Enter}')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendResetPasswordCode).toHaveBeenCalledWith('user@example.com', 'en-US')
|
||||
expect(consoleClient.forgotPassword.post).toHaveBeenCalledWith({
|
||||
body: { email: 'user@example.com', language: 'en-US' },
|
||||
})
|
||||
})
|
||||
expect(mockPush).toHaveBeenCalledWith(
|
||||
'/reset-password/check-code?token=reset-token&email=user%2540example.com',
|
||||
@@ -73,7 +83,7 @@ describe('ResetPasswordPage', () => {
|
||||
expect(emailInput).toHaveAttribute('aria-invalid', 'true')
|
||||
expect(emailInput).toHaveAccessibleDescription('login.error.emailInValid')
|
||||
expect(emailInput).toHaveFocus()
|
||||
expect(sendResetPasswordCode).not.toHaveBeenCalled()
|
||||
expect(consoleClient.forgotPassword.post).not.toHaveBeenCalled()
|
||||
|
||||
await user.clear(emailInput)
|
||||
await user.click(screen.getByRole('button', { name: 'login.sendVerificationCode' }))
|
||||
@@ -84,6 +94,6 @@ describe('ResetPasswordPage', () => {
|
||||
expect(emailInput).toHaveAttribute('aria-invalid', 'true')
|
||||
expect(emailInput).toHaveAccessibleDescription('login.error.emailEmpty')
|
||||
expect(emailInput).toHaveFocus()
|
||||
expect(sendResetPasswordCode).not.toHaveBeenCalled()
|
||||
expect(consoleClient.forgotPassword.post).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useLocale } from '#i18n'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { verifyResetPasswordCode } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import CheckCodePage from '../page'
|
||||
|
||||
const mockPush = vi.fn()
|
||||
@@ -25,9 +25,16 @@ vi.mock('@/next/navigation', () => ({
|
||||
useSearchParams: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
sendResetPasswordCode: vi.fn(),
|
||||
verifyResetPasswordCode: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
forgotPassword: {
|
||||
post: vi.fn(),
|
||||
validity: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
describe('ResetPasswordCheckCodePage', () => {
|
||||
@@ -43,8 +50,8 @@ describe('ResetPasswordCheckCodePage', () => {
|
||||
token: 'email-token',
|
||||
}) as unknown as ReturnType<typeof useSearchParams>,
|
||||
)
|
||||
vi.mocked(verifyResetPasswordCode).mockResolvedValue({
|
||||
result: 'success',
|
||||
vi.mocked(consoleClient.forgotPassword.validity.post).mockResolvedValue({
|
||||
email: 'user@example.com',
|
||||
is_valid: true,
|
||||
token: 'verified-token',
|
||||
})
|
||||
@@ -63,10 +70,12 @@ describe('ResetPasswordCheckCodePage', () => {
|
||||
await user.type(codeInput, '123456{Enter}')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(verifyResetPasswordCode).toHaveBeenCalledWith({
|
||||
code: '123456',
|
||||
email: 'user@example.com',
|
||||
token: 'email-token',
|
||||
expect(consoleClient.forgotPassword.validity.post).toHaveBeenCalledWith({
|
||||
body: {
|
||||
code: '123456',
|
||||
email: 'user@example.com',
|
||||
token: 'email-token',
|
||||
},
|
||||
})
|
||||
})
|
||||
expect(mockPush).toHaveBeenCalledWith(
|
||||
@@ -87,6 +96,6 @@ describe('ResetPasswordCheckCodePage', () => {
|
||||
expect(codeInput).toHaveAttribute('aria-invalid', 'true')
|
||||
expect(codeInput).toHaveAccessibleDescription(error.textContent ?? '')
|
||||
expect(codeInput).toHaveFocus()
|
||||
expect(verifyResetPasswordCode).not.toHaveBeenCalled()
|
||||
expect(consoleClient.forgotPassword.validity.post).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import Countdown from '@/app/components/signin/countdown'
|
||||
import { toast } from '@/app/notifications'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { sendResetPasswordCode, verifyResetPasswordCode } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
|
||||
type CheckCodeFormValues = {
|
||||
code: string
|
||||
@@ -31,7 +31,7 @@ export default function CheckCode() {
|
||||
if (loading) return
|
||||
try {
|
||||
setLoading(true)
|
||||
const ret = await verifyResetPasswordCode({ email, code, token })
|
||||
const ret = await consoleClient.forgotPassword.validity.post({ body: { email, code, token } })
|
||||
if (ret.is_valid) {
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set('token', encodeURIComponent(ret.token))
|
||||
@@ -46,10 +46,10 @@ export default function CheckCode() {
|
||||
|
||||
const resendCode = async () => {
|
||||
try {
|
||||
const res = await sendResetPasswordCode(email, locale)
|
||||
const res = await consoleClient.forgotPassword.post({ body: { email, language: locale } })
|
||||
if (res.result === 'success') {
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set('token', encodeURIComponent(res.data))
|
||||
params.set('token', encodeURIComponent(res.data ?? ''))
|
||||
router.replace(`/reset-password/check-code?${params.toString()}`)
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { emailRegex } from '@/config'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { sendResetPasswordCode } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '../components/signin/storage'
|
||||
|
||||
export default function CheckCode() {
|
||||
@@ -28,11 +28,11 @@ export default function CheckCode() {
|
||||
if (loading) return
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await sendResetPasswordCode(email, locale)
|
||||
const res = await consoleClient.forgotPassword.post({ body: { email, language: locale } })
|
||||
if (res.result === 'success') {
|
||||
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set('token', encodeURIComponent(res.data))
|
||||
params.set('token', encodeURIComponent(res.data ?? ''))
|
||||
params.set('email', encodeURIComponent(email))
|
||||
router.push(`/reset-password/check-code?${params.toString()}`)
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { changePasswordWithToken } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import ChangePasswordForm from '../page'
|
||||
|
||||
const countdownState = vi.hoisted(() => ({
|
||||
@@ -29,8 +29,15 @@ vi.mock('@/next/link', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
changePasswordWithToken: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
forgotPassword: {
|
||||
resets: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-document-title', () => ({
|
||||
@@ -40,7 +47,7 @@ vi.mock('@/hooks/use-document-title', () => ({
|
||||
const mockReplace = vi.fn()
|
||||
const mockUseRouter = vi.mocked(useRouter)
|
||||
const mockUseSearchParams = vi.mocked(useSearchParams)
|
||||
const mockChangePasswordWithToken = vi.mocked(changePasswordWithToken)
|
||||
const mockChangePasswordWithToken = vi.mocked(consoleClient.forgotPassword.resets.post)
|
||||
const mockUseDocumentTitle = vi.mocked(useDocumentTitle)
|
||||
|
||||
const redirectUrl = '/apps?template-id=template-1&utm_source=dify_blog'
|
||||
@@ -108,7 +115,6 @@ describe('Reset Password Set Password Page', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockChangePasswordWithToken).toHaveBeenCalledWith({
|
||||
url: '/forgot-password/resets',
|
||||
body: {
|
||||
token: 'reset-token',
|
||||
new_password: 'ValidPass123!',
|
||||
|
||||
@@ -19,7 +19,7 @@ import { validPassword } from '@/config'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { changePasswordWithToken } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
|
||||
type PasswordFormValues = {
|
||||
password: string
|
||||
@@ -75,8 +75,7 @@ const ChangePasswordForm = () => {
|
||||
if (isSubmitting) return
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await changePasswordWithToken({
|
||||
url: '/forgot-password/resets',
|
||||
await consoleClient.forgotPassword.resets.post({
|
||||
body: {
|
||||
token,
|
||||
new_password: formValues.password,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { noop, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { createHttpError } from '@/service/http-error'
|
||||
import { seedSystemFeatures } from '@/test/console/query-data'
|
||||
import CheckCode from '../page'
|
||||
@@ -76,9 +76,16 @@ vi.mock('@/next/navigation', () => ({
|
||||
|
||||
vi.mock('@/service/base', () => serviceBaseMocks)
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
emailLoginWithCode: vi.fn(),
|
||||
sendEMailLoginCode: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
emailCodeLogin: {
|
||||
validity: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/encryption', () => ({
|
||||
@@ -139,8 +146,10 @@ const accountProfile: GetAccountProfileResponse = {
|
||||
describe('CheckCode', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(emailLoginWithCode).mockReset().mockResolvedValue({ result: 'success' })
|
||||
vi.mocked(sendEMailLoginCode).mockReset()
|
||||
vi.mocked(consoleClient.emailCodeLogin.validity.post)
|
||||
.mockReset()
|
||||
.mockResolvedValue({ result: 'success' })
|
||||
vi.mocked(consoleClient.emailCodeLogin.post).mockReset()
|
||||
turnstileWidgets.clear()
|
||||
navigationMocks.searchParams = new URLSearchParams({
|
||||
email: 'user@example.com',
|
||||
@@ -225,18 +234,13 @@ describe('CheckCode', () => {
|
||||
expect(codeInput).toHaveAttribute('aria-invalid', 'true')
|
||||
expect(codeInput).toHaveAccessibleDescription('login.checkCode.invalidCode')
|
||||
expect(codeInput).toHaveFocus()
|
||||
expect(emailLoginWithCode).not.toHaveBeenCalled()
|
||||
expect(consoleClient.emailCodeLogin.validity.post).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps Community verification independent of Turnstile', async () => {
|
||||
const user = userEvent.setup()
|
||||
const queryClient = createQueryClient()
|
||||
vi.mocked(emailLoginWithCode).mockResolvedValue({
|
||||
code: 'invalid_code',
|
||||
data: '',
|
||||
message: 'Invalid code',
|
||||
result: 'fail',
|
||||
})
|
||||
vi.mocked(consoleClient.emailCodeLogin.validity.post).mockResolvedValue({ result: 'fail' })
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CheckCode />
|
||||
@@ -246,12 +250,14 @@ describe('CheckCode', () => {
|
||||
await user.type(screen.getByLabelText('login.checkCode.verificationCode'), '123456')
|
||||
await user.click(screen.getByRole('button', { name: 'login.checkCode.verify' }))
|
||||
|
||||
expect(emailLoginWithCode).toHaveBeenCalledWith({
|
||||
code: '123456',
|
||||
email: 'user@example.com',
|
||||
language: expect.any(String),
|
||||
timezone: 'Asia/Singapore',
|
||||
token: 'email-login-token',
|
||||
expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenCalledWith({
|
||||
body: {
|
||||
code: '123456',
|
||||
email: 'user@example.com',
|
||||
language: expect.any(String),
|
||||
timezone: 'Asia/Singapore',
|
||||
token: 'email-login-token',
|
||||
},
|
||||
})
|
||||
expect(turnstileMocks.render).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -259,8 +265,11 @@ describe('CheckCode', () => {
|
||||
it('does not resend while verification is in progress', async () => {
|
||||
const user = userEvent.setup()
|
||||
const queryClient = createQueryClient()
|
||||
const verificationRequest = createDeferred<Awaited<ReturnType<typeof emailLoginWithCode>>>()
|
||||
vi.mocked(emailLoginWithCode).mockReturnValue(verificationRequest.promise)
|
||||
const verificationRequest =
|
||||
createDeferred<Awaited<ReturnType<typeof consoleClient.emailCodeLogin.validity.post>>>()
|
||||
vi.mocked(consoleClient.emailCodeLogin.validity.post).mockReturnValue(
|
||||
verificationRequest.promise,
|
||||
)
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CheckCode />
|
||||
@@ -270,22 +279,17 @@ describe('CheckCode', () => {
|
||||
await user.type(screen.getByLabelText('login.checkCode.verificationCode'), '123456')
|
||||
await user.click(screen.getByRole('button', { name: 'login.checkCode.verify' }))
|
||||
await waitFor(() => {
|
||||
expect(emailLoginWithCode).toHaveBeenCalledOnce()
|
||||
expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
const resendButton = screen.getByRole('button', { name: 'resend-code' })
|
||||
expect(resendButton).toBeDisabled()
|
||||
resendButton.removeAttribute('disabled')
|
||||
fireEvent.click(resendButton)
|
||||
expect(sendEMailLoginCode).not.toHaveBeenCalled()
|
||||
expect(consoleClient.emailCodeLogin.post).not.toHaveBeenCalled()
|
||||
|
||||
act(() => {
|
||||
verificationRequest.resolve({
|
||||
code: 'invalid_code',
|
||||
data: '',
|
||||
message: 'Invalid code',
|
||||
result: 'fail',
|
||||
})
|
||||
verificationRequest.resolve({ result: 'fail' })
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'login.checkCode.verify' })).toBeEnabled()
|
||||
@@ -295,8 +299,9 @@ describe('CheckCode', () => {
|
||||
it('does not verify while resend is in progress', async () => {
|
||||
const user = userEvent.setup()
|
||||
const queryClient = createQueryClient()
|
||||
const resendRequest = createDeferred<Awaited<ReturnType<typeof sendEMailLoginCode>>>()
|
||||
vi.mocked(sendEMailLoginCode).mockReturnValue(resendRequest.promise)
|
||||
const resendRequest =
|
||||
createDeferred<Awaited<ReturnType<typeof consoleClient.emailCodeLogin.post>>>()
|
||||
vi.mocked(consoleClient.emailCodeLogin.post).mockReturnValue(resendRequest.promise)
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CheckCode />
|
||||
@@ -306,7 +311,7 @@ describe('CheckCode', () => {
|
||||
await user.type(screen.getByLabelText('login.checkCode.verificationCode'), '123456')
|
||||
await user.click(screen.getByRole('button', { name: 'resend-code' }))
|
||||
await waitFor(() => {
|
||||
expect(sendEMailLoginCode).toHaveBeenCalledOnce()
|
||||
expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: 'login.checkCode.verify' })
|
||||
@@ -314,7 +319,7 @@ describe('CheckCode', () => {
|
||||
const form = verifyButton.closest('form')
|
||||
if (!form) throw new Error('Verification form is missing')
|
||||
fireEvent.submit(form)
|
||||
expect(emailLoginWithCode).not.toHaveBeenCalled()
|
||||
expect(consoleClient.emailCodeLogin.validity.post).not.toHaveBeenCalled()
|
||||
|
||||
act(() => {
|
||||
resendRequest.resolve({ data: '', result: 'fail' })
|
||||
@@ -330,14 +335,9 @@ describe('CheckCode', () => {
|
||||
turnstileMocks.siteKey = 'cloud-site-key'
|
||||
const queryClient = createQueryClient()
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
vi.mocked(emailLoginWithCode)
|
||||
vi.mocked(consoleClient.emailCodeLogin.validity.post)
|
||||
.mockRejectedValueOnce(new Error('invalid verification code'))
|
||||
.mockResolvedValueOnce({
|
||||
code: 'invalid_code',
|
||||
data: '',
|
||||
message: 'Invalid code',
|
||||
result: 'fail',
|
||||
})
|
||||
.mockResolvedValueOnce({ result: 'fail' })
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -365,11 +365,11 @@ describe('CheckCode', () => {
|
||||
await user.click(verifyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(emailLoginWithCode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenCalledWith({
|
||||
body: expect.objectContaining({
|
||||
turnstile_token: 'signin_code_verify-token-1',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
expect(codeInput).toHaveValue('123456')
|
||||
await waitFor(() => {
|
||||
@@ -387,11 +387,11 @@ describe('CheckCode', () => {
|
||||
await user.click(verifyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(emailLoginWithCode).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenLastCalledWith({
|
||||
body: expect.objectContaining({
|
||||
turnstile_token: 'signin_code_verify-token-2',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
expect(codeInput).toHaveValue('123456')
|
||||
})
|
||||
@@ -401,7 +401,10 @@ describe('CheckCode', () => {
|
||||
turnstileMocks.deploymentEdition = 'CLOUD'
|
||||
turnstileMocks.siteKey = 'cloud-site-key'
|
||||
const queryClient = createQueryClient()
|
||||
vi.mocked(sendEMailLoginCode).mockResolvedValue({ result: 'success', data: 'new-login-token' })
|
||||
vi.mocked(consoleClient.emailCodeLogin.post).mockResolvedValue({
|
||||
result: 'success',
|
||||
data: 'new-login-token',
|
||||
})
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -434,11 +437,13 @@ describe('CheckCode', () => {
|
||||
await user.click(await screen.findByRole('button', { name: 'verify-turnstile-signin_code' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendEMailLoginCode).toHaveBeenCalledWith(
|
||||
'user@example.com',
|
||||
expect.any(String),
|
||||
'signin_code-token-2',
|
||||
)
|
||||
expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledWith({
|
||||
body: {
|
||||
email: 'user@example.com',
|
||||
language: expect.any(String),
|
||||
turnstile_token: 'signin_code-token-2',
|
||||
},
|
||||
})
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@@ -456,7 +461,10 @@ describe('CheckCode', () => {
|
||||
turnstileMocks.deploymentEdition = 'CLOUD'
|
||||
turnstileMocks.siteKey = 'cloud-site-key'
|
||||
const queryClient = createQueryClient()
|
||||
vi.mocked(sendEMailLoginCode).mockResolvedValue({ result: 'success', data: 'new-login-token' })
|
||||
vi.mocked(consoleClient.emailCodeLogin.post).mockResolvedValue({
|
||||
result: 'success',
|
||||
data: 'new-login-token',
|
||||
})
|
||||
Object.defineProperty(window, 'turnstile', {
|
||||
configurable: true,
|
||||
value: undefined,
|
||||
@@ -491,11 +499,13 @@ describe('CheckCode', () => {
|
||||
await user.click(await screen.findByRole('button', { name: 'verify-turnstile-signin_code' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendEMailLoginCode).toHaveBeenCalledWith(
|
||||
'user@example.com',
|
||||
expect.any(String),
|
||||
'signin_code-token-1',
|
||||
)
|
||||
expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledWith({
|
||||
body: {
|
||||
email: 'user@example.com',
|
||||
language: expect.any(String),
|
||||
turnstile_token: 'signin_code-token-1',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -503,7 +513,10 @@ describe('CheckCode', () => {
|
||||
const user = userEvent.setup()
|
||||
const queryClient = createQueryClient()
|
||||
turnstileMocks.siteKey = 'site-key-not-used-outside-cloud'
|
||||
vi.mocked(sendEMailLoginCode).mockResolvedValue({ result: 'success', data: 'new-login-token' })
|
||||
vi.mocked(consoleClient.emailCodeLogin.post).mockResolvedValue({
|
||||
result: 'success',
|
||||
data: 'new-login-token',
|
||||
})
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -516,11 +529,9 @@ describe('CheckCode', () => {
|
||||
expect(resendButton).toBeEnabled()
|
||||
await user.click(resendButton)
|
||||
|
||||
expect(sendEMailLoginCode).toHaveBeenCalledWith(
|
||||
'user@example.com',
|
||||
expect.any(String),
|
||||
undefined,
|
||||
)
|
||||
expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledWith({
|
||||
body: { email: 'user@example.com', language: expect.any(String) },
|
||||
})
|
||||
})
|
||||
|
||||
describe('Post-login profile bootstrap', () => {
|
||||
|
||||
@@ -10,13 +10,12 @@ import { useLocale } from '#i18n'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { TURNSTILE_SITE_KEY } from '@/config'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { encryptVerificationCode } from '@/utils/encryption'
|
||||
import { replaceLoginRedirect } from '@/utils/login-redirect.client'
|
||||
import { getBrowserTimezone } from '@/utils/timezone'
|
||||
@@ -62,13 +61,15 @@ export default function CheckCode() {
|
||||
|
||||
setLoading(true)
|
||||
shouldResetTurnstile = isTurnstileRequired
|
||||
const ret = await emailLoginWithCode({
|
||||
email,
|
||||
code: encryptVerificationCode(code),
|
||||
token,
|
||||
language,
|
||||
timezone: getBrowserTimezone(),
|
||||
...(isTurnstileRequired ? { turnstile_token: verifyTurnstileToken } : {}),
|
||||
const ret = await consoleClient.emailCodeLogin.validity.post({
|
||||
body: {
|
||||
email,
|
||||
code: encryptVerificationCode(code),
|
||||
token,
|
||||
language,
|
||||
timezone: getBrowserTimezone(),
|
||||
...(isTurnstileRequired ? { turnstile_token: verifyTurnstileToken } : {}),
|
||||
},
|
||||
})
|
||||
if (ret.result === 'success') {
|
||||
// Track login success event
|
||||
@@ -85,8 +86,6 @@ export default function CheckCode() {
|
||||
await queryClient.query(profileQueryOptions)
|
||||
replaceLoginRedirect(resolvePostLoginRedirect(searchParams), router.replace, basePath)
|
||||
}
|
||||
} else {
|
||||
toast.error(ret.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -106,11 +105,13 @@ export default function CheckCode() {
|
||||
const resendCode = async (turnstileToken?: string) => {
|
||||
setIsResending(true)
|
||||
try {
|
||||
const ret = await sendEMailLoginCode(
|
||||
email,
|
||||
locale,
|
||||
isTurnstileRequired ? turnstileToken : undefined,
|
||||
)
|
||||
const ret = await consoleClient.emailCodeLogin.post({
|
||||
body: {
|
||||
email,
|
||||
language: locale,
|
||||
...(isTurnstileRequired ? { turnstile_token: turnstileToken } : {}),
|
||||
},
|
||||
})
|
||||
if (ret.result === 'success') {
|
||||
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
|
||||
setCountdownGeneration((value) => value + 1)
|
||||
|
||||
@@ -69,8 +69,13 @@ vi.mock('@/config', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
sendEMailLoginCode: (...args: unknown[]) => mocks.sendEMailLoginCode(...args),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
emailCodeLogin: {
|
||||
post: (...args: unknown[]) => mocks.sendEMailLoginCode(...args),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
describe('MailAndCodeAuth', () => {
|
||||
@@ -149,11 +154,9 @@ describe('MailAndCodeAuth', () => {
|
||||
await user.click(screen.getByRole('button', { name: 'login.signup.verifyMail' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith(
|
||||
'user@example.com',
|
||||
'en-US',
|
||||
'turnstile-token',
|
||||
)
|
||||
expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith({
|
||||
body: { email: 'user@example.com', language: 'en-US', turnstile_token: 'turnstile-token' },
|
||||
})
|
||||
})
|
||||
expect(mocks.push).toHaveBeenCalledWith(expect.stringContaining('/signin/check-code?'))
|
||||
})
|
||||
@@ -177,11 +180,13 @@ describe('MailAndCodeAuth', () => {
|
||||
await user.click(continueButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith(
|
||||
'user@example.com',
|
||||
'en-US',
|
||||
'consumed-turnstile-token',
|
||||
)
|
||||
expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith({
|
||||
body: {
|
||||
email: 'user@example.com',
|
||||
language: 'en-US',
|
||||
turnstile_token: 'consumed-turnstile-token',
|
||||
},
|
||||
})
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(continueButton).toBeDisabled()
|
||||
@@ -195,11 +200,13 @@ describe('MailAndCodeAuth', () => {
|
||||
await user.click(continueButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.sendEMailLoginCode).toHaveBeenLastCalledWith(
|
||||
'user@example.com',
|
||||
'en-US',
|
||||
'fresh-turnstile-token',
|
||||
)
|
||||
expect(mocks.sendEMailLoginCode).toHaveBeenLastCalledWith({
|
||||
body: {
|
||||
email: 'user@example.com',
|
||||
language: 'en-US',
|
||||
turnstile_token: 'fresh-turnstile-token',
|
||||
},
|
||||
})
|
||||
})
|
||||
expect(mocks.push).toHaveBeenCalledWith(expect.stringContaining('/signin/check-code?'))
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/si
|
||||
import { emailRegex, TURNSTILE_SITE_KEY } from '@/config'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { sendEMailLoginCode } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import Turnstile from './turnstile'
|
||||
|
||||
type MailAndCodeAuthProps = {
|
||||
@@ -39,11 +39,13 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
|
||||
try {
|
||||
setLoading(true)
|
||||
shouldResetTurnstile = isTurnstileRequired
|
||||
const ret = await sendEMailLoginCode(
|
||||
email,
|
||||
locale,
|
||||
isTurnstileRequired ? turnstileToken : undefined,
|
||||
)
|
||||
const ret = await consoleClient.emailCodeLogin.post({
|
||||
body: {
|
||||
email,
|
||||
language: locale,
|
||||
...(isTurnstileRequired ? { turnstile_token: turnstileToken } : {}),
|
||||
},
|
||||
})
|
||||
if (ret.result === 'success') {
|
||||
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
|
||||
const params = new URLSearchParams(searchParams)
|
||||
|
||||
@@ -13,9 +13,7 @@ import { toast } from '@/app/notifications'
|
||||
import { emailRegex } from '@/config'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { login } from '@/service/common'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { setWebAppAccessToken } from '@/service/webapp-auth'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
import { encryptPassword } from '@/utils/encryption'
|
||||
import { replaceLoginRedirect } from '@/utils/login-redirect.client'
|
||||
import { basePath } from '@/utils/var'
|
||||
@@ -52,15 +50,8 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup }: MailAndP
|
||||
}
|
||||
if (isInvite)
|
||||
loginData.invite_token = decodeURIComponent(searchParams.get('invite_token') as string)
|
||||
const res = await login({
|
||||
url: '/login',
|
||||
body: loginData,
|
||||
})
|
||||
const res = await consoleClient.login.post({ body: loginData })
|
||||
if (res.result === 'success') {
|
||||
if (res?.data?.access_token) {
|
||||
// Track login success event
|
||||
setWebAppAccessToken(res.data.access_token)
|
||||
}
|
||||
trackEvent('user_login_success', {
|
||||
method: 'email_password',
|
||||
is_invite: isInvite,
|
||||
|
||||
@@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { useLocale } from '#i18n'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { activateMember } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { useInvitationCheck } from '@/service/use-common'
|
||||
import { getBrowserTimezone } from '@/utils/timezone'
|
||||
import InviteSettingsPage from '../page'
|
||||
@@ -62,8 +62,13 @@ vi.mock('@/next/navigation', () => ({
|
||||
useSearchParams: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
activateMember: vi.fn(),
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
activate: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
@@ -84,7 +89,7 @@ const mockRefetch = vi.fn()
|
||||
const mockUseLocale = useLocale as unknown as MockedFunction<typeof useLocale>
|
||||
const mockUseRouter = useRouter as unknown as MockedFunction<typeof useRouter>
|
||||
const mockUseSearchParams = useSearchParams as unknown as MockedFunction<typeof useSearchParams>
|
||||
const mockActivateMember = activateMember as unknown as MockedFunction<typeof activateMember>
|
||||
const mockActivateMember = vi.mocked(consoleClient.activate.post)
|
||||
const mockUseQuery = vi.mocked(useQuery)
|
||||
const mockUseInvitationCheck = useInvitationCheck as unknown as MockedFunction<
|
||||
typeof useInvitationCheck
|
||||
@@ -182,7 +187,6 @@ describe('InviteSettingsPage', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockActivateMember).toHaveBeenCalledWith({
|
||||
url: '/activate',
|
||||
body: {
|
||||
token: 'invite-token',
|
||||
name: 'Invitee',
|
||||
@@ -204,7 +208,6 @@ describe('InviteSettingsPage', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockActivateMember).toHaveBeenCalledWith({
|
||||
url: '/activate',
|
||||
body: {
|
||||
token: 'invite-token',
|
||||
name: 'Invitee',
|
||||
@@ -238,7 +241,6 @@ describe('InviteSettingsPage', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockActivateMember).toHaveBeenCalledWith({
|
||||
url: '/activate',
|
||||
body: {
|
||||
token: 'invite-token',
|
||||
},
|
||||
@@ -268,7 +270,6 @@ describe('InviteSettingsPage', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockActivateMember).toHaveBeenCalledWith({
|
||||
url: '/activate',
|
||||
body: {
|
||||
token: 'invite-token',
|
||||
},
|
||||
@@ -299,7 +300,6 @@ describe('InviteSettingsPage', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockActivateMember).toHaveBeenCalledWith({
|
||||
url: '/activate',
|
||||
body: {
|
||||
token: 'invite-token',
|
||||
name: 'Invitee',
|
||||
|
||||
@@ -28,8 +28,7 @@ import { setLocaleOnClient } from '@/i18n/client'
|
||||
import { languages } from '@/i18n/language'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { activateMember } from '@/service/common'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
import { isUnauthorizedError } from '@/service/http-error'
|
||||
import { useInvitationCheck } from '@/service/use-common'
|
||||
import { replaceLoginRedirect } from '@/utils/login-redirect.client'
|
||||
@@ -156,10 +155,7 @@ export default function InviteSettingsPage() {
|
||||
: {
|
||||
token,
|
||||
}
|
||||
const res = await activateMember({
|
||||
url: '/activate',
|
||||
body,
|
||||
})
|
||||
const res = await consoleClient.activate.post({ body })
|
||||
if (res.result === 'success') {
|
||||
// Tokens are now stored in cookies by the backend
|
||||
if (requiresAccountSetup) await setLocaleOnClient(language!, false)
|
||||
|
||||
@@ -10,7 +10,7 @@ import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { invitationCheck } from '@/service/common'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { isUnauthorizedError } from '@/service/http-error'
|
||||
import { replaceLoginRedirect } from '@/utils/login-redirect.client'
|
||||
import { basePath } from '@/utils/var'
|
||||
@@ -58,13 +58,7 @@ function NormalForm() {
|
||||
isError: isInviteCheckError,
|
||||
} = useQuery({
|
||||
queryKey: ['signin', 'invite-check', inviteToken],
|
||||
queryFn: () =>
|
||||
invitationCheck({
|
||||
url: '/activate/check',
|
||||
params: {
|
||||
token: inviteToken,
|
||||
},
|
||||
}),
|
||||
queryFn: () => consoleClient.activate.check.get({ query: { token: inviteToken } }),
|
||||
enabled: isInviteLink,
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
REGISTRATION_SUCCESS_STORAGE_KEY,
|
||||
} from '@/app/components/base/amplitude/registration-session-state'
|
||||
import { clearPageLeaveGuards, registerPageLeaveGuard } from '@/utils/page-leave-guard'
|
||||
import { emailLoginWithCode, sendEMailLoginCode } from './common'
|
||||
import { useLogout } from './use-common'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -34,63 +33,6 @@ vi.mock('@/utils/var', async (importOriginal) => ({
|
||||
},
|
||||
}))
|
||||
|
||||
describe('sendEMailLoginCode', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('includes the Turnstile token when provided', async () => {
|
||||
await sendEMailLoginCode('user@example.com', 'en-US', 'turnstile-token')
|
||||
|
||||
expect(mocks.post).toHaveBeenCalledWith('/email-code-login', {
|
||||
body: {
|
||||
email: 'user@example.com',
|
||||
language: 'en-US',
|
||||
turnstile_token: 'turnstile-token',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('omits the Turnstile token when it is not provided', async () => {
|
||||
await sendEMailLoginCode('user@example.com', 'en-US')
|
||||
|
||||
expect(mocks.post).toHaveBeenCalledWith('/email-code-login', {
|
||||
body: {
|
||||
email: 'user@example.com',
|
||||
language: 'en-US',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('emailLoginWithCode', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('includes the verification-specific Turnstile token when provided', async () => {
|
||||
await emailLoginWithCode({
|
||||
code: 'encrypted-code',
|
||||
email: 'user@example.com',
|
||||
language: 'en-US',
|
||||
timezone: 'Asia/Singapore',
|
||||
token: 'email-login-token',
|
||||
turnstile_token: 'verify-turnstile-token',
|
||||
})
|
||||
|
||||
expect(mocks.post).toHaveBeenCalledWith('/email-code-login/validity', {
|
||||
body: {
|
||||
code: 'encrypted-code',
|
||||
email: 'user@example.com',
|
||||
language: 'en-US',
|
||||
timezone: 'Asia/Singapore',
|
||||
token: 'email-login-token',
|
||||
turnstile_token: 'verify-turnstile-token',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('useLogout', () => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: new QueryClient() }, children)
|
||||
|
||||
+2
-223
@@ -1,19 +1,5 @@
|
||||
import type { EmailCodeLoginPayload } from '@dify/contracts/api/console/email-code-login/types.gen'
|
||||
import type {
|
||||
PostWorkspacesInfoData,
|
||||
PostWorkspacesInfoResponse,
|
||||
} from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type {
|
||||
DefaultModelResponse,
|
||||
ModelParameterRule,
|
||||
ModelTypeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type {
|
||||
CommonResponse,
|
||||
InitValidateStatusResponse,
|
||||
SetupStatusResponse,
|
||||
} from '@/models/common'
|
||||
import { del, get, patch, post } from './base'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import { post } from './base'
|
||||
|
||||
type LoginSuccess = {
|
||||
result: 'success'
|
||||
@@ -26,15 +12,6 @@ type LoginFail = {
|
||||
message: string
|
||||
}
|
||||
type LoginResponse = LoginSuccess | LoginFail
|
||||
export const login = ({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
body: Record<string, any>
|
||||
}): Promise<LoginResponse> => {
|
||||
return post<LoginResponse>(url, { body })
|
||||
}
|
||||
export const webAppLogin = ({
|
||||
url,
|
||||
body,
|
||||
@@ -45,133 +22,6 @@ export const webAppLogin = ({
|
||||
return post<LoginResponse>(url, { body }, { isPublicAPI: true })
|
||||
}
|
||||
|
||||
export const setup = ({ body }: { body: Record<string, any> }): Promise<CommonResponse> => {
|
||||
return post<CommonResponse>('/setup', { body })
|
||||
}
|
||||
|
||||
export const initValidate = ({ body }: { body: Record<string, any> }): Promise<CommonResponse> => {
|
||||
return post<CommonResponse>('/init', { body })
|
||||
}
|
||||
|
||||
export const fetchInitValidateStatus = (): Promise<InitValidateStatusResponse> => {
|
||||
return get<InitValidateStatusResponse>('/init')
|
||||
}
|
||||
|
||||
export const fetchSetupStatus = (): Promise<SetupStatusResponse> => {
|
||||
return get<SetupStatusResponse>('/setup')
|
||||
}
|
||||
export const deleteMemberOrCancelInvitation = ({
|
||||
url,
|
||||
}: {
|
||||
url: string
|
||||
}): Promise<CommonResponse> => {
|
||||
return del<CommonResponse>(url)
|
||||
}
|
||||
|
||||
export const sendOwnerEmail = (body: {
|
||||
language?: string
|
||||
}): Promise<CommonResponse & { data: string }> =>
|
||||
post<CommonResponse & { data: string }>(
|
||||
'/workspaces/current/members/send-owner-transfer-confirm-email',
|
||||
{ body },
|
||||
)
|
||||
|
||||
export const verifyOwnerEmail = (body: {
|
||||
code: string
|
||||
token: string
|
||||
}): Promise<CommonResponse & { is_valid: boolean; email: string; token: string }> =>
|
||||
post<CommonResponse & { is_valid: boolean; email: string; token: string }>(
|
||||
'/workspaces/current/members/owner-transfer-check',
|
||||
{ body },
|
||||
)
|
||||
|
||||
export const ownershipTransfer = (
|
||||
memberID: string,
|
||||
body: { token: string },
|
||||
): Promise<CommonResponse & { is_valid: boolean; email: string; token: string }> =>
|
||||
post<CommonResponse & { is_valid: boolean; email: string; token: string }>(
|
||||
`/workspaces/current/members/${memberID}/owner-transfer`,
|
||||
{ body },
|
||||
)
|
||||
|
||||
export const updateWorkspaceInfo = ({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: PostWorkspacesInfoData['url']
|
||||
body: PostWorkspacesInfoData['body']
|
||||
}): Promise<PostWorkspacesInfoResponse> => {
|
||||
return post<PostWorkspacesInfoResponse>(url, { body })
|
||||
}
|
||||
|
||||
type InvitationCheckData = {
|
||||
workspace_name: string
|
||||
email: string
|
||||
workspace_id: string
|
||||
account_status?: string
|
||||
requires_setup?: boolean
|
||||
}
|
||||
|
||||
type ActivateMemberBody = {
|
||||
token: string
|
||||
name?: string
|
||||
interface_language?: string
|
||||
timezone?: string
|
||||
}
|
||||
|
||||
export const invitationCheck = ({
|
||||
url,
|
||||
params,
|
||||
}: {
|
||||
url: string
|
||||
params: { workspace_id?: string; email?: string; token: string }
|
||||
}): Promise<CommonResponse & { is_valid: boolean; data: InvitationCheckData }> => {
|
||||
return get<CommonResponse & { is_valid: boolean; data: InvitationCheckData }>(url, { params })
|
||||
}
|
||||
|
||||
export const activateMember = ({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
body: ActivateMemberBody
|
||||
}): Promise<LoginResponse> => {
|
||||
return post<LoginResponse>(url, { body })
|
||||
}
|
||||
|
||||
export const fetchDefaultModal = (url: string): Promise<{ data: DefaultModelResponse }> => {
|
||||
return get<{ data: DefaultModelResponse }>(url)
|
||||
}
|
||||
|
||||
export const fetchModelParameterRules = (url: string): Promise<{ data: ModelParameterRule[] }> => {
|
||||
return get<{ data: ModelParameterRule[] }>(url)
|
||||
}
|
||||
|
||||
export const enableModel = (
|
||||
url: string,
|
||||
body: { model: string; model_type: ModelTypeEnum },
|
||||
): Promise<CommonResponse> => patch<CommonResponse>(url, { body })
|
||||
|
||||
export const disableModel = (
|
||||
url: string,
|
||||
body: { model: string; model_type: ModelTypeEnum },
|
||||
): Promise<CommonResponse> => patch<CommonResponse>(url, { body })
|
||||
|
||||
export const sendForgotPasswordEmail = ({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
body: { email: string }
|
||||
}): Promise<CommonResponse & { data: string }> =>
|
||||
post<CommonResponse & { data: string }>(url, { body })
|
||||
export const changePasswordWithToken = ({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
body: { token: string; new_password: string; password_confirm: string }
|
||||
}): Promise<CommonResponse> => post<CommonResponse>(url, { body })
|
||||
export const changeWebAppPasswordWithToken = ({
|
||||
url,
|
||||
body,
|
||||
@@ -193,37 +43,6 @@ export const uploadRemoteFileInfo = (
|
||||
)
|
||||
}
|
||||
|
||||
export const sendEMailLoginCode = (
|
||||
email: string,
|
||||
language = 'en-US',
|
||||
turnstileToken?: string,
|
||||
): Promise<CommonResponse & { data: string }> =>
|
||||
post<CommonResponse & { data: string }>('/email-code-login', {
|
||||
body: {
|
||||
email,
|
||||
language,
|
||||
...(turnstileToken === undefined ? {} : { turnstile_token: turnstileToken }),
|
||||
},
|
||||
})
|
||||
|
||||
export const emailLoginWithCode = (data: EmailCodeLoginPayload): Promise<LoginResponse> =>
|
||||
post<LoginResponse>('/email-code-login/validity', { body: data })
|
||||
|
||||
export const sendResetPasswordCode = (
|
||||
email: string,
|
||||
language = 'en-US',
|
||||
): Promise<CommonResponse & { data: string; message?: string; code?: string }> =>
|
||||
post<CommonResponse & { data: string; message?: string; code?: string }>('/forgot-password', {
|
||||
body: { email, language },
|
||||
})
|
||||
|
||||
export const verifyResetPasswordCode = (body: {
|
||||
email: string
|
||||
code: string
|
||||
token: string
|
||||
}): Promise<CommonResponse & { is_valid: boolean; token: string }> =>
|
||||
post<CommonResponse & { is_valid: boolean; token: string }>('/forgot-password/validity', { body })
|
||||
|
||||
export const sendWebAppEMailLoginCode = (
|
||||
email: string,
|
||||
language = 'en-US',
|
||||
@@ -262,46 +81,6 @@ export const verifyWebAppResetPasswordCode = (body: {
|
||||
{ isPublicAPI: true },
|
||||
)
|
||||
|
||||
export const sendDeleteAccountCode = (): Promise<CommonResponse & { data: string }> =>
|
||||
get<CommonResponse & { data: string }>('/account/delete/verify')
|
||||
|
||||
export const verifyDeleteAccountCode = (body: {
|
||||
code: string
|
||||
token: string
|
||||
}): Promise<CommonResponse & { is_valid: boolean }> =>
|
||||
post<CommonResponse & { is_valid: boolean }>('/account/delete', { body })
|
||||
|
||||
export const submitDeleteAccountFeedback = (body: {
|
||||
feedback: string
|
||||
email: string
|
||||
}): Promise<CommonResponse> => post<CommonResponse>('/account/delete/feedback', { body })
|
||||
|
||||
export const getDocDownloadUrl = (doc_name: string): Promise<{ url: string }> =>
|
||||
get<{ url: string }>('/compliance/download', { params: { doc_name } }, { silent: true })
|
||||
|
||||
export const sendVerifyCode = (body: {
|
||||
email: string
|
||||
phase: string
|
||||
token?: string
|
||||
}): Promise<CommonResponse & { data: string }> =>
|
||||
post<CommonResponse & { data: string }>('/account/change-email', { body })
|
||||
|
||||
export const verifyEmail = (body: {
|
||||
email: string
|
||||
code: string
|
||||
token: string
|
||||
}): Promise<CommonResponse & { is_valid: boolean; email: string; token: string }> =>
|
||||
post<CommonResponse & { is_valid: boolean; email: string; token: string }>(
|
||||
'/account/change-email/validity',
|
||||
{ body },
|
||||
)
|
||||
|
||||
export const resetEmail = (body: { new_email: string; token: string }): Promise<CommonResponse> =>
|
||||
post<CommonResponse>('/account/change-email/reset', { body })
|
||||
|
||||
export const checkEmailExisted = (body: { email: string }): Promise<CommonResponse> =>
|
||||
post<CommonResponse>('/account/change-email/check-email-unique', { body }, { silent: true })
|
||||
|
||||
export const getAvatar = async ({
|
||||
avatar,
|
||||
}: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
FormValue,
|
||||
ModelParameterRule,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { normalizeModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/utils'
|
||||
|
||||
export const mergeValidCompletionParams = (
|
||||
oldParams: FormValue | undefined,
|
||||
@@ -82,10 +83,13 @@ export const fetchModelParameterRulesForModel = async (
|
||||
provider: string,
|
||||
modelId: string,
|
||||
): Promise<ModelParameterRule[]> => {
|
||||
const { fetchModelParameterRules } = await import('@/service/common')
|
||||
const url = `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}`
|
||||
const { data: parameterRules } = await fetchModelParameterRules(url)
|
||||
return parameterRules ?? []
|
||||
const { consoleClient } = await import('@/service/console')
|
||||
const { data: parameterRules } =
|
||||
await consoleClient.workspaces.current.modelProviders.byProvider.models.parameterRules.get({
|
||||
params: { provider },
|
||||
query: { model: modelId },
|
||||
})
|
||||
return (parameterRules ?? []).map(normalizeModelParameterRule)
|
||||
}
|
||||
|
||||
export const fetchAndMergeValidCompletionParams = async (
|
||||
|
||||
Reference in New Issue
Block a user