fix(profile): show normalized API error messages

This commit is contained in:
chihao.ou
2026-09-11 19:27:47 +08:00
parent 4726bdd08b
commit 9b6a49b8e7
4 changed files with 54 additions and 9 deletions
@@ -44,6 +44,7 @@ import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { useAppStore } from '@/stores/app'
import { userAPI } from '@/api'
import { extractApiErrorMessage } from '@/utils/apiError'
const props = withDefaults(defineProps<{
initialUsername: string
@@ -76,8 +77,8 @@ const handleUpdateProfile = async () => {
})
authStore.user = updatedUser
appStore.showSuccess(t('profile.updateSuccess'))
} catch (error: any) {
appStore.showError(error.response?.data?.detail || t('profile.updateFailed'))
} catch (error: unknown) {
appStore.showError(extractApiErrorMessage(error, t('profile.updateFailed')))
} finally {
loading.value = false
}
@@ -75,6 +75,7 @@ import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { userAPI } from '@/api'
import { extractApiErrorMessage } from '@/utils/apiError'
const { t } = useI18n()
const appStore = useAppStore()
@@ -107,8 +108,8 @@ const handleChangePassword = async () => {
await userAPI.changePassword(form.value.old_password, form.value.new_password)
form.value = { old_password: '', new_password: '', confirm_password: '' }
appStore.showSuccess(t('profile.passwordChangeSuccess'))
} catch (error: any) {
appStore.showError(error.response?.data?.detail || t('profile.passwordChangeFailed'))
} catch (error: unknown) {
appStore.showError(extractApiErrorMessage(error, t('profile.passwordChangeFailed')))
} finally {
loading.value = false
}
@@ -0,0 +1,41 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import ProfileEditForm from '@/components/user/profile/ProfileEditForm.vue'
const { updateProfileMock, showErrorMock, authState } = vi.hoisted(() => ({
updateProfileMock: vi.fn(),
showErrorMock: vi.fn(),
authState: { user: { username: 'alice' } },
}))
vi.mock('@/api', () => ({
userAPI: { updateProfile: updateProfileMock },
}))
vi.mock('@/stores/auth', () => ({
useAuthStore: () => authState,
}))
vi.mock('@/stores/app', () => ({
useAppStore: () => ({ showError: showErrorMock }),
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({ t: (key: string) => key }),
}))
describe('ProfileEditForm', () => {
it.each([
[{ status: 400, code: 'VALIDATION_ERROR', message: 'username is too long' }, 'username is too long'],
[{ response: { data: { detail: 'backend failure' } } }, 'backend failure'],
[{}, 'profile.updateFailed'],
])('shows API failure %j without changing the saved profile', async (error, expectedMessage) => {
updateProfileMock.mockRejectedValue(error)
const wrapper = mount(ProfileEditForm, { props: { initialUsername: 'alice' } })
await wrapper.get('#username').setValue('new-name')
await wrapper.get('form').trigger('submit.prevent')
expect(updateProfileMock).toHaveBeenCalledWith({ username: 'new-name' })
expect(showErrorMock).toHaveBeenLastCalledWith(expectedMessage)
expect(authState.user.username).toBe('alice')
expect(wrapper.get('button[type="submit"]').attributes('disabled')).toBeUndefined()
})
})
@@ -60,10 +60,12 @@ describe('ProfilePasswordForm', () => {
expect(wrapper.find('.input-error-text').exists()).toBe(false)
})
it('shows API failures as toast messages', async () => {
changePasswordMock.mockRejectedValue({
response: { data: { detail: 'backend failure' } }
})
it.each([
[{ status: 400, code: 'PASSWORD_INCORRECT', message: 'current password is incorrect' }, 'current password is incorrect'],
[{ response: { data: { detail: 'backend failure' } } }, 'backend failure'],
[{}, 'Failed to change password'],
])('shows API failure %j as a toast', async (error, expectedMessage) => {
changePasswordMock.mockRejectedValue(error)
const wrapper = mount(ProfilePasswordForm)
@@ -73,7 +75,7 @@ describe('ProfilePasswordForm', () => {
await wrapper.get('form').trigger('submit.prevent')
expect(changePasswordMock).toHaveBeenCalledWith('old-password', 'new-password')
expect(showErrorMock).toHaveBeenCalledWith('backend failure')
expect(showErrorMock).toHaveBeenLastCalledWith(expectedMessage)
expect(wrapper.find('.input-error-text').exists()).toBe(false)
})
})