mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix(totp): display normalized API error messages
This commit is contained in:
@@ -87,6 +87,7 @@ import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { totpAPI } from '@/api'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
@@ -120,7 +121,7 @@ const loadVerificationMethod = async () => {
|
||||
const method = await totpAPI.getVerificationMethod()
|
||||
verificationMethod.value = method.method
|
||||
} catch (err: any) {
|
||||
appStore.showError(err.response?.data?.message || t('common.error'))
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
emit('close')
|
||||
} finally {
|
||||
methodLoading.value = false
|
||||
@@ -148,7 +149,7 @@ const handleSendCode = async () => {
|
||||
}
|
||||
}, 1000)
|
||||
} catch (err: any) {
|
||||
appStore.showError(err.response?.data?.message || t('profile.totp.sendCodeFailed'))
|
||||
appStore.showError(extractApiErrorMessage(err, t('profile.totp.sendCodeFailed')))
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
}
|
||||
@@ -168,7 +169,7 @@ const handleDisable = async () => {
|
||||
appStore.showSuccess(t('profile.totp.disableSuccess'))
|
||||
emit('success')
|
||||
} catch (err: any) {
|
||||
appStore.showError(err.response?.data?.message || t('profile.totp.disableFailed'))
|
||||
appStore.showError(extractApiErrorMessage(err, t('profile.totp.disableFailed')))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -171,6 +171,7 @@ import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { totpAPI } from '@/api'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import type { TotpSetupResponse } from '@/types'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
@@ -315,7 +316,7 @@ const loadVerificationMethod = async () => {
|
||||
const method = await totpAPI.getVerificationMethod()
|
||||
verificationMethod.value = method.method
|
||||
} catch (err: any) {
|
||||
appStore.showError(err.response?.data?.message || t('common.error'))
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
emit('close')
|
||||
} finally {
|
||||
methodLoading.value = false
|
||||
@@ -343,7 +344,7 @@ const handleSendCode = async () => {
|
||||
}
|
||||
}, 1000)
|
||||
} catch (err: any) {
|
||||
appStore.showError(err.response?.data?.message || t('profile.totp.sendCodeFailed'))
|
||||
appStore.showError(extractApiErrorMessage(err, t('profile.totp.sendCodeFailed')))
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
}
|
||||
@@ -360,7 +361,7 @@ const handleVerifyAndSetup = async () => {
|
||||
setupData.value = await totpAPI.initiateSetup(request)
|
||||
step.value = 1
|
||||
} catch (err: any) {
|
||||
appStore.showError(err.response?.data?.message || t('profile.totp.setupFailed'))
|
||||
appStore.showError(extractApiErrorMessage(err, t('profile.totp.setupFailed')))
|
||||
} finally {
|
||||
setupLoading.value = false
|
||||
}
|
||||
@@ -380,7 +381,7 @@ const handleVerify = async () => {
|
||||
appStore.showSuccess(t('profile.totp.enableSuccess'))
|
||||
emit('success')
|
||||
} catch (err: any) {
|
||||
appStore.showError(err.response?.data?.message || t('profile.totp.verifyFailed'))
|
||||
appStore.showError(extractApiErrorMessage(err, t('profile.totp.verifyFailed')))
|
||||
code.value = ['', '', '', '', '', '']
|
||||
nextTick(() => {
|
||||
inputRefs.value[0]?.focus()
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import TotpSetupModal from '../TotpSetupModal.vue'
|
||||
import TotpDisableDialog from '../TotpDisableDialog.vue'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
showError: vi.fn(), showSuccess: vi.fn(),
|
||||
totpAPI: {
|
||||
getVerificationMethod: vi.fn(), sendVerifyCode: vi.fn(),
|
||||
initiateSetup: vi.fn(), enable: vi.fn(), disable: vi.fn()
|
||||
}
|
||||
}))
|
||||
vi.mock('@/api', () => ({ totpAPI: mocks.totpAPI }))
|
||||
vi.mock('@/stores/app', () => ({ useAppStore: () => mocks }))
|
||||
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) }))
|
||||
vi.mock('qrcode', () => ({ default: { toDataURL: vi.fn().mockResolvedValue('data:image/png;base64,qr') } }))
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
mocks.totpAPI.getVerificationMethod.mockResolvedValue({ method: 'password' })
|
||||
})
|
||||
|
||||
describe.each([
|
||||
{ name: 'setup', component: TotpSetupModal, action: mocks.totpAPI.initiateSetup, fallback: 'profile.totp.setupFailed' },
|
||||
{ name: 'disable', component: TotpDisableDialog, action: mocks.totpAPI.disable, fallback: 'profile.totp.disableFailed' }
|
||||
])('TOTP $name errors', ({ name, component, action, fallback }) => {
|
||||
it('shows the verification-method API error before closing', async () => {
|
||||
mocks.totpAPI.getVerificationMethod.mockRejectedValue({ status: 503, message: 'Verification is temporarily unavailable' })
|
||||
const wrapper = mount(component)
|
||||
await flushPromises()
|
||||
expect(mocks.showError).toHaveBeenCalledWith('Verification is temporarily unavailable')
|
||||
expect(wrapper.emitted('close')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('shows why the email verification code cannot be sent', async () => {
|
||||
mocks.totpAPI.getVerificationMethod.mockResolvedValue({ method: 'email' })
|
||||
mocks.totpAPI.sendVerifyCode.mockRejectedValue({ status: 429, message: 'Please wait before requesting another code' })
|
||||
const wrapper = mount(component)
|
||||
await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text() === 'profile.totp.sendCode')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(mocks.showError).toHaveBeenCalledWith('Please wait before requesting another code')
|
||||
})
|
||||
|
||||
it.each([
|
||||
[{ status: 400, message: 'Incorrect password' }, 'Incorrect password'],
|
||||
[{ response: { data: { message: 'Legacy message' } } }, 'Legacy message'],
|
||||
[{}, fallback]
|
||||
])('preserves the action error or localized fallback: %j', async (error, expected) => {
|
||||
action.mockRejectedValue(error)
|
||||
const wrapper = mount(component)
|
||||
await flushPromises()
|
||||
await wrapper.get('input[type="password"]').setValue('incorrect-password')
|
||||
if (name === 'setup') await wrapper.get('.btn-primary').trigger('click')
|
||||
else await wrapper.get('form').trigger('submit')
|
||||
await flushPromises()
|
||||
expect(mocks.showError).toHaveBeenCalledWith(expected)
|
||||
expect(wrapper.emitted('success')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the reason a TOTP code was rejected', async () => {
|
||||
mocks.totpAPI.initiateSetup.mockResolvedValue({ secret: 'EXAMPLE', qr_code_url: '', setup_token: 'setup-token' })
|
||||
mocks.totpAPI.enable.mockRejectedValue({ status: 400, message: 'Invalid TOTP code' })
|
||||
const wrapper = mount(TotpSetupModal)
|
||||
await flushPromises()
|
||||
await wrapper.get('input[type="password"]').setValue('password')
|
||||
await wrapper.get('.btn-primary').trigger('click')
|
||||
await flushPromises()
|
||||
await wrapper.get('.btn-primary').trigger('click')
|
||||
for (const input of wrapper.findAll('input[maxlength="1"]')) await input.setValue('1')
|
||||
await wrapper.get('form').trigger('submit')
|
||||
await flushPromises()
|
||||
expect(mocks.totpAPI.enable).toHaveBeenCalledWith({ totp_code: '111111', setup_token: 'setup-token' })
|
||||
expect(mocks.showError).toHaveBeenCalledWith('Invalid TOTP code')
|
||||
expect(wrapper.emitted('success')).toBeUndefined()
|
||||
})
|
||||
Reference in New Issue
Block a user