refactor(auth): save authType after successfully authenticated (#1036)

This commit is contained in:
Mingholy 2025-11-19 11:21:46 +08:00 committed by GitHub
parent 3ed93d5b5d
commit d0e76c76a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 822 additions and 518 deletions

View file

@ -6,14 +6,13 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import type { DeviceAuthorizationInfo } from './useQwenAuth.js';
import type { DeviceAuthorizationData } from '@qwen-code/qwen-code-core';
import { useQwenAuth } from './useQwenAuth.js';
import {
AuthType,
qwenOAuth2Events,
QwenOAuth2Event,
} from '@qwen-code/qwen-code-core';
import type { LoadedSettings } from '../../config/settings.js';
// Mock the qwenOAuth2Events
vi.mock('@qwen-code/qwen-code-core', async () => {
@ -36,24 +35,14 @@ vi.mock('@qwen-code/qwen-code-core', async () => {
const mockQwenOAuth2Events = vi.mocked(qwenOAuth2Events);
describe('useQwenAuth', () => {
const mockDeviceAuth: DeviceAuthorizationInfo = {
const mockDeviceAuth: DeviceAuthorizationData = {
verification_uri: 'https://oauth.qwen.com/device',
verification_uri_complete: 'https://oauth.qwen.com/device?user_code=ABC123',
user_code: 'ABC123',
expires_in: 1800,
device_code: 'device_code_123',
};
const createMockSettings = (authType: AuthType): LoadedSettings =>
({
merged: {
security: {
auth: {
selectedType: authType,
},
},
},
}) as LoadedSettings;
beforeEach(() => {
vi.clearAllMocks();
});
@ -63,36 +52,33 @@ describe('useQwenAuth', () => {
});
it('should initialize with default state when not Qwen auth', () => {
const settings = createMockSettings(AuthType.USE_GEMINI);
const { result } = renderHook(() => useQwenAuth(settings, false));
const { result } = renderHook(() =>
useQwenAuth(AuthType.USE_GEMINI, false),
);
expect(result.current).toEqual({
isQwenAuthenticating: false,
expect(result.current.qwenAuthState).toEqual({
deviceAuth: null,
authStatus: 'idle',
authMessage: null,
isQwenAuth: false,
cancelQwenAuth: expect.any(Function),
});
expect(result.current.cancelQwenAuth).toBeInstanceOf(Function);
});
it('should initialize with default state when Qwen auth but not authenticating', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
const { result } = renderHook(() => useQwenAuth(settings, false));
const { result } = renderHook(() =>
useQwenAuth(AuthType.QWEN_OAUTH, false),
);
expect(result.current).toEqual({
isQwenAuthenticating: false,
expect(result.current.qwenAuthState).toEqual({
deviceAuth: null,
authStatus: 'idle',
authMessage: null,
isQwenAuth: true,
cancelQwenAuth: expect.any(Function),
});
expect(result.current.cancelQwenAuth).toBeInstanceOf(Function);
});
it('should set up event listeners when Qwen auth and authenticating', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
renderHook(() => useQwenAuth(settings, true));
renderHook(() => useQwenAuth(AuthType.QWEN_OAUTH, true));
expect(mockQwenOAuth2Events.on).toHaveBeenCalledWith(
QwenOAuth2Event.AuthUri,
@ -105,8 +91,7 @@ describe('useQwenAuth', () => {
});
it('should handle device auth event', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
let handleDeviceAuth: (deviceAuth: DeviceAuthorizationInfo) => void;
let handleDeviceAuth: (deviceAuth: DeviceAuthorizationData) => void;
mockQwenOAuth2Events.on.mockImplementation((event, handler) => {
if (event === QwenOAuth2Event.AuthUri) {
@ -115,19 +100,17 @@ describe('useQwenAuth', () => {
return mockQwenOAuth2Events;
});
const { result } = renderHook(() => useQwenAuth(settings, true));
const { result } = renderHook(() => useQwenAuth(AuthType.QWEN_OAUTH, true));
act(() => {
handleDeviceAuth!(mockDeviceAuth);
});
expect(result.current.deviceAuth).toEqual(mockDeviceAuth);
expect(result.current.authStatus).toBe('polling');
expect(result.current.isQwenAuthenticating).toBe(true);
expect(result.current.qwenAuthState.deviceAuth).toEqual(mockDeviceAuth);
expect(result.current.qwenAuthState.authStatus).toBe('polling');
});
it('should handle auth progress event - success', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
let handleAuthProgress: (
status: 'success' | 'error' | 'polling' | 'timeout' | 'rate_limit',
message?: string,
@ -140,18 +123,19 @@ describe('useQwenAuth', () => {
return mockQwenOAuth2Events;
});
const { result } = renderHook(() => useQwenAuth(settings, true));
const { result } = renderHook(() => useQwenAuth(AuthType.QWEN_OAUTH, true));
act(() => {
handleAuthProgress!('success', 'Authentication successful!');
});
expect(result.current.authStatus).toBe('success');
expect(result.current.authMessage).toBe('Authentication successful!');
expect(result.current.qwenAuthState.authStatus).toBe('success');
expect(result.current.qwenAuthState.authMessage).toBe(
'Authentication successful!',
);
});
it('should handle auth progress event - error', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
let handleAuthProgress: (
status: 'success' | 'error' | 'polling' | 'timeout' | 'rate_limit',
message?: string,
@ -164,18 +148,19 @@ describe('useQwenAuth', () => {
return mockQwenOAuth2Events;
});
const { result } = renderHook(() => useQwenAuth(settings, true));
const { result } = renderHook(() => useQwenAuth(AuthType.QWEN_OAUTH, true));
act(() => {
handleAuthProgress!('error', 'Authentication failed');
});
expect(result.current.authStatus).toBe('error');
expect(result.current.authMessage).toBe('Authentication failed');
expect(result.current.qwenAuthState.authStatus).toBe('error');
expect(result.current.qwenAuthState.authMessage).toBe(
'Authentication failed',
);
});
it('should handle auth progress event - polling', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
let handleAuthProgress: (
status: 'success' | 'error' | 'polling' | 'timeout' | 'rate_limit',
message?: string,
@ -188,20 +173,19 @@ describe('useQwenAuth', () => {
return mockQwenOAuth2Events;
});
const { result } = renderHook(() => useQwenAuth(settings, true));
const { result } = renderHook(() => useQwenAuth(AuthType.QWEN_OAUTH, true));
act(() => {
handleAuthProgress!('polling', 'Waiting for user authorization...');
});
expect(result.current.authStatus).toBe('polling');
expect(result.current.authMessage).toBe(
expect(result.current.qwenAuthState.authStatus).toBe('polling');
expect(result.current.qwenAuthState.authMessage).toBe(
'Waiting for user authorization...',
);
});
it('should handle auth progress event - rate_limit', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
let handleAuthProgress: (
status: 'success' | 'error' | 'polling' | 'timeout' | 'rate_limit',
message?: string,
@ -214,7 +198,7 @@ describe('useQwenAuth', () => {
return mockQwenOAuth2Events;
});
const { result } = renderHook(() => useQwenAuth(settings, true));
const { result } = renderHook(() => useQwenAuth(AuthType.QWEN_OAUTH, true));
act(() => {
handleAuthProgress!(
@ -223,14 +207,13 @@ describe('useQwenAuth', () => {
);
});
expect(result.current.authStatus).toBe('rate_limit');
expect(result.current.authMessage).toBe(
expect(result.current.qwenAuthState.authStatus).toBe('rate_limit');
expect(result.current.qwenAuthState.authMessage).toBe(
'Too many requests. The server is rate limiting our requests. Please select a different authentication method or try again later.',
);
});
it('should handle auth progress event without message', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
let handleAuthProgress: (
status: 'success' | 'error' | 'polling' | 'timeout' | 'rate_limit',
message?: string,
@ -243,27 +226,30 @@ describe('useQwenAuth', () => {
return mockQwenOAuth2Events;
});
const { result } = renderHook(() => useQwenAuth(settings, true));
const { result } = renderHook(() => useQwenAuth(AuthType.QWEN_OAUTH, true));
act(() => {
handleAuthProgress!('success');
});
expect(result.current.authStatus).toBe('success');
expect(result.current.authMessage).toBe(null);
expect(result.current.qwenAuthState.authStatus).toBe('success');
expect(result.current.qwenAuthState.authMessage).toBe(null);
});
it('should clean up event listeners when auth type changes', () => {
const qwenSettings = createMockSettings(AuthType.QWEN_OAUTH);
const { rerender } = renderHook(
({ settings, isAuthenticating }) =>
useQwenAuth(settings, isAuthenticating),
{ initialProps: { settings: qwenSettings, isAuthenticating: true } },
({ pendingAuthType, isAuthenticating }) =>
useQwenAuth(pendingAuthType, isAuthenticating),
{
initialProps: {
pendingAuthType: AuthType.QWEN_OAUTH,
isAuthenticating: true,
},
},
);
// Change to non-Qwen auth
const geminiSettings = createMockSettings(AuthType.USE_GEMINI);
rerender({ settings: geminiSettings, isAuthenticating: true });
rerender({ pendingAuthType: AuthType.USE_GEMINI, isAuthenticating: true });
expect(mockQwenOAuth2Events.off).toHaveBeenCalledWith(
QwenOAuth2Event.AuthUri,
@ -276,9 +262,9 @@ describe('useQwenAuth', () => {
});
it('should clean up event listeners when authentication stops', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
const { rerender } = renderHook(
({ isAuthenticating }) => useQwenAuth(settings, isAuthenticating),
({ isAuthenticating }) =>
useQwenAuth(AuthType.QWEN_OAUTH, isAuthenticating),
{ initialProps: { isAuthenticating: true } },
);
@ -296,8 +282,9 @@ describe('useQwenAuth', () => {
});
it('should clean up event listeners on unmount', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
const { unmount } = renderHook(() => useQwenAuth(settings, true));
const { unmount } = renderHook(() =>
useQwenAuth(AuthType.QWEN_OAUTH, true),
);
unmount();
@ -312,8 +299,7 @@ describe('useQwenAuth', () => {
});
it('should reset state when switching from Qwen auth to another auth type', () => {
const qwenSettings = createMockSettings(AuthType.QWEN_OAUTH);
let handleDeviceAuth: (deviceAuth: DeviceAuthorizationInfo) => void;
let handleDeviceAuth: (deviceAuth: DeviceAuthorizationData) => void;
mockQwenOAuth2Events.on.mockImplementation((event, handler) => {
if (event === QwenOAuth2Event.AuthUri) {
@ -323,9 +309,14 @@ describe('useQwenAuth', () => {
});
const { result, rerender } = renderHook(
({ settings, isAuthenticating }) =>
useQwenAuth(settings, isAuthenticating),
{ initialProps: { settings: qwenSettings, isAuthenticating: true } },
({ pendingAuthType, isAuthenticating }) =>
useQwenAuth(pendingAuthType, isAuthenticating),
{
initialProps: {
pendingAuthType: AuthType.QWEN_OAUTH,
isAuthenticating: true,
},
},
);
// Simulate device auth
@ -333,22 +324,19 @@ describe('useQwenAuth', () => {
handleDeviceAuth!(mockDeviceAuth);
});
expect(result.current.deviceAuth).toEqual(mockDeviceAuth);
expect(result.current.authStatus).toBe('polling');
expect(result.current.qwenAuthState.deviceAuth).toEqual(mockDeviceAuth);
expect(result.current.qwenAuthState.authStatus).toBe('polling');
// Switch to different auth type
const geminiSettings = createMockSettings(AuthType.USE_GEMINI);
rerender({ settings: geminiSettings, isAuthenticating: true });
rerender({ pendingAuthType: AuthType.USE_GEMINI, isAuthenticating: true });
expect(result.current.isQwenAuthenticating).toBe(false);
expect(result.current.deviceAuth).toBe(null);
expect(result.current.authStatus).toBe('idle');
expect(result.current.authMessage).toBe(null);
expect(result.current.qwenAuthState.deviceAuth).toBe(null);
expect(result.current.qwenAuthState.authStatus).toBe('idle');
expect(result.current.qwenAuthState.authMessage).toBe(null);
});
it('should reset state when authentication stops', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
let handleDeviceAuth: (deviceAuth: DeviceAuthorizationInfo) => void;
let handleDeviceAuth: (deviceAuth: DeviceAuthorizationData) => void;
mockQwenOAuth2Events.on.mockImplementation((event, handler) => {
if (event === QwenOAuth2Event.AuthUri) {
@ -358,7 +346,8 @@ describe('useQwenAuth', () => {
});
const { result, rerender } = renderHook(
({ isAuthenticating }) => useQwenAuth(settings, isAuthenticating),
({ isAuthenticating }) =>
useQwenAuth(AuthType.QWEN_OAUTH, isAuthenticating),
{ initialProps: { isAuthenticating: true } },
);
@ -367,21 +356,19 @@ describe('useQwenAuth', () => {
handleDeviceAuth!(mockDeviceAuth);
});
expect(result.current.deviceAuth).toEqual(mockDeviceAuth);
expect(result.current.authStatus).toBe('polling');
expect(result.current.qwenAuthState.deviceAuth).toEqual(mockDeviceAuth);
expect(result.current.qwenAuthState.authStatus).toBe('polling');
// Stop authentication
rerender({ isAuthenticating: false });
expect(result.current.isQwenAuthenticating).toBe(false);
expect(result.current.deviceAuth).toBe(null);
expect(result.current.authStatus).toBe('idle');
expect(result.current.authMessage).toBe(null);
expect(result.current.qwenAuthState.deviceAuth).toBe(null);
expect(result.current.qwenAuthState.authStatus).toBe('idle');
expect(result.current.qwenAuthState.authMessage).toBe(null);
});
it('should handle cancelQwenAuth function', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
let handleDeviceAuth: (deviceAuth: DeviceAuthorizationInfo) => void;
let handleDeviceAuth: (deviceAuth: DeviceAuthorizationData) => void;
mockQwenOAuth2Events.on.mockImplementation((event, handler) => {
if (event === QwenOAuth2Event.AuthUri) {
@ -390,53 +377,49 @@ describe('useQwenAuth', () => {
return mockQwenOAuth2Events;
});
const { result } = renderHook(() => useQwenAuth(settings, true));
const { result } = renderHook(() => useQwenAuth(AuthType.QWEN_OAUTH, true));
// Set up some state
act(() => {
handleDeviceAuth!(mockDeviceAuth);
});
expect(result.current.deviceAuth).toEqual(mockDeviceAuth);
expect(result.current.qwenAuthState.deviceAuth).toEqual(mockDeviceAuth);
// Cancel auth
act(() => {
result.current.cancelQwenAuth();
});
expect(result.current.isQwenAuthenticating).toBe(false);
expect(result.current.deviceAuth).toBe(null);
expect(result.current.authStatus).toBe('idle');
expect(result.current.authMessage).toBe(null);
expect(result.current.qwenAuthState.deviceAuth).toBe(null);
expect(result.current.qwenAuthState.authStatus).toBe('idle');
expect(result.current.qwenAuthState.authMessage).toBe(null);
});
it('should maintain isQwenAuth flag correctly', () => {
// Test with Qwen OAuth
const qwenSettings = createMockSettings(AuthType.QWEN_OAUTH);
it('should handle different auth types correctly', () => {
// Test with Qwen OAuth - should set up event listeners when authenticating
const { result: qwenResult } = renderHook(() =>
useQwenAuth(qwenSettings, false),
useQwenAuth(AuthType.QWEN_OAUTH, true),
);
expect(qwenResult.current.isQwenAuth).toBe(true);
expect(qwenResult.current.qwenAuthState.authStatus).toBe('idle');
expect(mockQwenOAuth2Events.on).toHaveBeenCalled();
// Test with other auth types
const geminiSettings = createMockSettings(AuthType.USE_GEMINI);
// Test with other auth types - should not set up event listeners
const { result: geminiResult } = renderHook(() =>
useQwenAuth(geminiSettings, false),
useQwenAuth(AuthType.USE_GEMINI, true),
);
expect(geminiResult.current.isQwenAuth).toBe(false);
expect(geminiResult.current.qwenAuthState.authStatus).toBe('idle');
const oauthSettings = createMockSettings(AuthType.LOGIN_WITH_GOOGLE);
const { result: oauthResult } = renderHook(() =>
useQwenAuth(oauthSettings, false),
useQwenAuth(AuthType.LOGIN_WITH_GOOGLE, true),
);
expect(oauthResult.current.isQwenAuth).toBe(false);
expect(oauthResult.current.qwenAuthState.authStatus).toBe('idle');
});
it('should set isQwenAuthenticating to true when starting authentication with Qwen auth', () => {
const settings = createMockSettings(AuthType.QWEN_OAUTH);
const { result } = renderHook(() => useQwenAuth(settings, true));
it('should initialize with idle status when starting authentication with Qwen auth', () => {
const { result } = renderHook(() => useQwenAuth(AuthType.QWEN_OAUTH, true));
expect(result.current.isQwenAuthenticating).toBe(true);
expect(result.current.authStatus).toBe('idle');
expect(result.current.qwenAuthState.authStatus).toBe('idle');
expect(mockQwenOAuth2Events.on).toHaveBeenCalled();
});
});