From 9b6a49b8e70ae65fff231a27aba2307946aaa965 Mon Sep 17 00:00:00 2001 From: "chihao.ou" Date: Fri, 11 Sep 2026 19:27:47 +0800 Subject: [PATCH] fix(profile): show normalized API error messages --- .../user/profile/ProfileEditForm.vue | 5 ++- .../user/profile/ProfilePasswordForm.vue | 5 ++- .../profile/__tests__/ProfileEditForm.spec.ts | 41 +++++++++++++++++++ .../__tests__/ProfilePasswordForm.spec.ts | 12 +++--- 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/user/profile/__tests__/ProfileEditForm.spec.ts diff --git a/frontend/src/components/user/profile/ProfileEditForm.vue b/frontend/src/components/user/profile/ProfileEditForm.vue index e1441921f2..b3605e0ede 100644 --- a/frontend/src/components/user/profile/ProfileEditForm.vue +++ b/frontend/src/components/user/profile/ProfileEditForm.vue @@ -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 } diff --git a/frontend/src/components/user/profile/ProfilePasswordForm.vue b/frontend/src/components/user/profile/ProfilePasswordForm.vue index 46595c1d34..e5b8a85b21 100644 --- a/frontend/src/components/user/profile/ProfilePasswordForm.vue +++ b/frontend/src/components/user/profile/ProfilePasswordForm.vue @@ -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 } diff --git a/frontend/src/components/user/profile/__tests__/ProfileEditForm.spec.ts b/frontend/src/components/user/profile/__tests__/ProfileEditForm.spec.ts new file mode 100644 index 0000000000..159e69465c --- /dev/null +++ b/frontend/src/components/user/profile/__tests__/ProfileEditForm.spec.ts @@ -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() + }) +}) diff --git a/frontend/src/components/user/profile/__tests__/ProfilePasswordForm.spec.ts b/frontend/src/components/user/profile/__tests__/ProfilePasswordForm.spec.ts index b7b2296680..b1fff4ea97 100644 --- a/frontend/src/components/user/profile/__tests__/ProfilePasswordForm.spec.ts +++ b/frontend/src/components/user/profile/__tests__/ProfilePasswordForm.spec.ts @@ -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) }) })