From 5753552b958084a7695fec7dfd923e30304ee108 Mon Sep 17 00:00:00 2001 From: "chihao.ou" Date: Fri, 9 Oct 2026 20:07:28 +0800 Subject: [PATCH] fix(gemini): preserve authorization URL error messages --- .../__tests__/useGeminiOAuth.errors.spec.ts | 39 +++++++++++++++++++ frontend/src/composables/useGeminiOAuth.ts | 5 ++- 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 frontend/src/composables/__tests__/useGeminiOAuth.errors.spec.ts diff --git a/frontend/src/composables/__tests__/useGeminiOAuth.errors.spec.ts b/frontend/src/composables/__tests__/useGeminiOAuth.errors.spec.ts new file mode 100644 index 0000000000..e2f2555582 --- /dev/null +++ b/frontend/src/composables/__tests__/useGeminiOAuth.errors.spec.ts @@ -0,0 +1,39 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useGeminiOAuth } from '../useGeminiOAuth' + +const mocks = vi.hoisted(() => ({ generateAuthUrl: vi.fn(), showError: vi.fn() })) +vi.mock('@/api/admin', () => ({ adminAPI: { gemini: { generateAuthUrl: mocks.generateAuthUrl } } })) +vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError: mocks.showError }) })) +vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) })) +beforeEach(() => vi.resetAllMocks()) + +describe('Gemini authorization URL errors', () => { + it.each([ + [{ status: 400, message: 'Selected proxy is unavailable' }, 'Selected proxy is unavailable'], + [{ response: { data: { detail: 'OAuth is not configured' } } }, 'OAuth is not configured'], + [{}, 'admin.accounts.oauth.gemini.failedToGenerateUrl'], + [null, 'admin.accounts.oauth.gemini.failedToGenerateUrl'] + ])('preserves useful API feedback for %j', async (error, expected) => { + mocks.generateAuthUrl.mockRejectedValue(error) + const oauth = useGeminiOAuth() + expect(await oauth.generateAuthUrl(7)).toBe(false) + expect(oauth.error.value).toBe(expected) + expect(mocks.showError).toHaveBeenCalledWith(expected) + expect(oauth.loading.value).toBe(false) + expect(oauth.authUrl.value).toBe('') + }) + + it('can retry successfully after an error', async () => { + mocks.generateAuthUrl.mockRejectedValueOnce({ message: 'Temporary failure' }).mockResolvedValueOnce({ + auth_url: 'https://example.com/auth', session_id: 'session', state: 'state' + }) + const oauth = useGeminiOAuth() + await oauth.generateAuthUrl(7) + expect(await oauth.generateAuthUrl(7, ' project ')).toBe(true) + expect(oauth.error.value).toBe('') + expect(oauth.authUrl.value).toBe('https://example.com/auth') + expect(oauth.sessionId.value).toBe('session') + expect(oauth.state.value).toBe('state') + expect(mocks.generateAuthUrl).toHaveBeenLastCalledWith({ proxy_id: 7, project_id: 'project' }) + }) +}) diff --git a/frontend/src/composables/useGeminiOAuth.ts b/frontend/src/composables/useGeminiOAuth.ts index 51abdbbfe9..9550704025 100644 --- a/frontend/src/composables/useGeminiOAuth.ts +++ b/frontend/src/composables/useGeminiOAuth.ts @@ -2,6 +2,7 @@ import { ref } from 'vue' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' import { adminAPI } from '@/api/admin' +import { extractApiErrorMessage } from '@/utils/apiError' import type { GeminiOAuthCapabilities } from '@/api/admin/gemini' export interface GeminiTokenInfo { @@ -61,8 +62,8 @@ export function useGeminiOAuth() { sessionId.value = response.session_id state.value = response.state return true - } catch (err: any) { - error.value = err.response?.data?.detail || t('admin.accounts.oauth.gemini.failedToGenerateUrl') + } catch (err: unknown) { + error.value = extractApiErrorMessage(err, t('admin.accounts.oauth.gemini.failedToGenerateUrl')) appStore.showError(error.value) return false } finally {