mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-09 16:36:40 -07:00
fix(cli): allow keychain auth for --list-sessions and non-interactive mode (#26921)
This commit is contained in:
@@ -150,6 +150,9 @@ vi.mock('./hooks/useQuotaAndFallback.js');
|
||||
vi.mock('./hooks/useHistoryManager.js');
|
||||
vi.mock('./hooks/useThemeCommand.js');
|
||||
vi.mock('./auth/useAuth.js');
|
||||
vi.mock('../config/auth.js', () => ({
|
||||
validateAuthMethod: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
vi.mock('./hooks/useEditorSettings.js');
|
||||
vi.mock('./hooks/useSettingsCommand.js');
|
||||
vi.mock('./hooks/useModelCommand.js');
|
||||
@@ -217,6 +220,7 @@ vi.mock('../utils/cleanup.js');
|
||||
import { useHistory } from './hooks/useHistoryManager.js';
|
||||
import { useThemeCommand } from './hooks/useThemeCommand.js';
|
||||
import { useAuthCommand } from './auth/useAuth.js';
|
||||
import { validateAuthMethod } from '../config/auth.js';
|
||||
import { useEditorSettings } from './hooks/useEditorSettings.js';
|
||||
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
|
||||
import { useModelCommand } from './hooks/useModelCommand.js';
|
||||
@@ -576,6 +580,36 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
describe('State Initialization', () => {
|
||||
it('calls validateAuthMethod and onAuthError if validation fails', async () => {
|
||||
const mockOnAuthError = vi.fn();
|
||||
mockedUseAuthCommand.mockReturnValue({
|
||||
authState: 'authenticated',
|
||||
setAuthState: vi.fn(),
|
||||
authError: null,
|
||||
onAuthError: mockOnAuthError,
|
||||
});
|
||||
vi.mocked(validateAuthMethod).mockResolvedValueOnce('Validation Failed');
|
||||
|
||||
const { unmount } = await act(async () =>
|
||||
renderAppContainer({
|
||||
settings: createMockSettings({
|
||||
merged: {
|
||||
security: {
|
||||
auth: { selectedType: 'oauth-personal', useExternal: false },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(validateAuthMethod).toHaveBeenCalledWith('oauth-personal');
|
||||
expect(mockOnAuthError).toHaveBeenCalledWith('Validation Failed');
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('sends a macOS notification when confirmation is pending and terminal is unfocused', async () => {
|
||||
mockedUseFocusState.mockReturnValue({
|
||||
isFocused: false,
|
||||
|
||||
@@ -912,12 +912,22 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
return;
|
||||
}
|
||||
|
||||
const error = validateAuthMethod(
|
||||
settings.merged.security.auth.selectedType,
|
||||
);
|
||||
if (error) {
|
||||
onAuthError(error);
|
||||
}
|
||||
const authMethod = settings.merged.security.auth.selectedType;
|
||||
void (async () => {
|
||||
try {
|
||||
const error = await validateAuthMethod(authMethod);
|
||||
if (
|
||||
error &&
|
||||
authMethod === settings.merged.security.auth.selectedType
|
||||
) {
|
||||
onAuthError(error);
|
||||
}
|
||||
} catch (e) {
|
||||
if (authMethod === settings.merged.security.auth.selectedType) {
|
||||
onAuthError(getErrorMessage(e));
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [
|
||||
settings.merged.security.auth.selectedType,
|
||||
|
||||
@@ -215,11 +215,11 @@ describe('AuthDialog', () => {
|
||||
|
||||
describe('handleAuthSelect', () => {
|
||||
it('calls onAuthError if validation fails', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue('Invalid method');
|
||||
mockedValidateAuthMethod.mockResolvedValue('Invalid method');
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
handleAuthSelect(AuthType.USE_GEMINI);
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
|
||||
expect(mockedValidateAuthMethod).toHaveBeenCalledWith(
|
||||
AuthType.USE_GEMINI,
|
||||
@@ -231,7 +231,7 @@ describe('AuthDialog', () => {
|
||||
});
|
||||
|
||||
it('sets auth context with requiresRestart: true for LOGIN_WITH_GOOGLE', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
mockedValidateAuthMethod.mockResolvedValue(null);
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
@@ -245,7 +245,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('sets auth context with requiresRestart: true for USE_VERTEX_AI in Cloud Shell', async () => {
|
||||
vi.stubEnv('CLOUD_SHELL', 'true');
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
mockedValidateAuthMethod.mockResolvedValue(null);
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
@@ -259,7 +259,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('sets auth context with empty object for USE_VERTEX_AI outside Cloud Shell', async () => {
|
||||
vi.stubEnv('CLOUD_SHELL', '');
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
mockedValidateAuthMethod.mockResolvedValue(null);
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
@@ -270,7 +270,7 @@ describe('AuthDialog', () => {
|
||||
});
|
||||
|
||||
it('sets auth context with empty object for other auth types', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
mockedValidateAuthMethod.mockResolvedValue(null);
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
@@ -281,7 +281,7 @@ describe('AuthDialog', () => {
|
||||
});
|
||||
|
||||
it('always shows API key dialog even when env var is present', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
mockedValidateAuthMethod.mockResolvedValue(null);
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
|
||||
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
|
||||
|
||||
@@ -297,7 +297,7 @@ describe('AuthDialog', () => {
|
||||
});
|
||||
|
||||
it('always shows API key dialog even when env var is empty string', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
mockedValidateAuthMethod.mockResolvedValue(null);
|
||||
vi.stubEnv('GEMINI_API_KEY', ''); // Empty string
|
||||
// props.settings.merged.security.auth.selectedType is undefined here
|
||||
|
||||
@@ -313,7 +313,7 @@ describe('AuthDialog', () => {
|
||||
});
|
||||
|
||||
it('shows API key dialog on initial setup if no env var is present', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
mockedValidateAuthMethod.mockResolvedValue(null);
|
||||
// process.env['GEMINI_API_KEY'] is not set
|
||||
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
|
||||
|
||||
@@ -329,7 +329,7 @@ describe('AuthDialog', () => {
|
||||
});
|
||||
|
||||
it('always shows API key dialog on re-auth even if env var is present', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
mockedValidateAuthMethod.mockResolvedValue(null);
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
|
||||
// Simulate switching from a different auth method (e.g., Google Login → API key)
|
||||
props.settings.merged.security.auth.selectedType =
|
||||
@@ -353,7 +353,7 @@ describe('AuthDialog', () => {
|
||||
.mockImplementation(() => undefined as never);
|
||||
const logSpy = vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
|
||||
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
mockedValidateAuthMethod.mockResolvedValue(null);
|
||||
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
|
||||
@@ -154,8 +154,11 @@ export function AuthDialog({
|
||||
[settings, config, setAuthState, exiting, setAuthContext],
|
||||
);
|
||||
|
||||
const handleAuthSelect = (authMethod: AuthType) => {
|
||||
const error = validateAuthMethodWithSettings(authMethod, settings);
|
||||
const handleAuthSelect = async (authMethod: AuthType) => {
|
||||
const error = await validateAuthMethodWithSettings(
|
||||
authMethod,
|
||||
settings,
|
||||
).catch((e) => (e instanceof Error ? e.message : String(e)));
|
||||
if (error) {
|
||||
onAuthError(error);
|
||||
} else {
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('useAuth', () => {
|
||||
});
|
||||
|
||||
describe('validateAuthMethodWithSettings', () => {
|
||||
it('should return error if auth type is enforced and does not match', () => {
|
||||
it('should return error if auth type is enforced and does not match', async () => {
|
||||
const settings = {
|
||||
merged: {
|
||||
security: {
|
||||
@@ -56,14 +56,14 @@ describe('useAuth', () => {
|
||||
},
|
||||
} as LoadedSettings;
|
||||
|
||||
const error = validateAuthMethodWithSettings(
|
||||
const error = await validateAuthMethodWithSettings(
|
||||
AuthType.USE_GEMINI,
|
||||
settings,
|
||||
);
|
||||
expect(error).toContain('Authentication is enforced to be oauth');
|
||||
});
|
||||
|
||||
it('should return null if useExternal is true', () => {
|
||||
it('should return null if useExternal is true', async () => {
|
||||
const settings = {
|
||||
merged: {
|
||||
security: {
|
||||
@@ -74,14 +74,14 @@ describe('useAuth', () => {
|
||||
},
|
||||
} as LoadedSettings;
|
||||
|
||||
const error = validateAuthMethodWithSettings(
|
||||
const error = await validateAuthMethodWithSettings(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
settings,
|
||||
);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null if authType is USE_GEMINI', () => {
|
||||
it('should return null if authType is USE_GEMINI', async () => {
|
||||
const settings = {
|
||||
merged: {
|
||||
security: {
|
||||
@@ -90,14 +90,14 @@ describe('useAuth', () => {
|
||||
},
|
||||
} as LoadedSettings;
|
||||
|
||||
const error = validateAuthMethodWithSettings(
|
||||
const error = await validateAuthMethodWithSettings(
|
||||
AuthType.USE_GEMINI,
|
||||
settings,
|
||||
);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
it('should call validateAuthMethod for other auth types', () => {
|
||||
it('should call validateAuthMethod for other auth types', async () => {
|
||||
const settings = {
|
||||
merged: {
|
||||
security: {
|
||||
@@ -106,8 +106,8 @@ describe('useAuth', () => {
|
||||
},
|
||||
} as LoadedSettings;
|
||||
|
||||
mockValidateAuthMethod.mockReturnValue('Validation Error');
|
||||
const error = validateAuthMethodWithSettings(
|
||||
mockValidateAuthMethod.mockResolvedValue('Validation Error');
|
||||
const error = await validateAuthMethodWithSettings(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
settings,
|
||||
);
|
||||
@@ -265,7 +265,7 @@ describe('useAuth', () => {
|
||||
});
|
||||
|
||||
it('should set error if validation fails', async () => {
|
||||
mockValidateAuthMethod.mockReturnValue('Validation Failed');
|
||||
mockValidateAuthMethod.mockResolvedValue('Validation Failed');
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
@@ -18,10 +18,10 @@ import { getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { AuthState } from '../types.js';
|
||||
import { validateAuthMethod } from '../../config/auth.js';
|
||||
|
||||
export function validateAuthMethodWithSettings(
|
||||
export async function validateAuthMethodWithSettings(
|
||||
authType: AuthType,
|
||||
settings: LoadedSettings,
|
||||
): string | null {
|
||||
): Promise<string | null> {
|
||||
const enforcedType = settings.merged.security.auth.enforcedType;
|
||||
if (enforcedType && enforcedType !== authType) {
|
||||
return `Authentication is enforced to be ${enforcedType}, but you are currently using ${authType}.`;
|
||||
@@ -111,7 +111,11 @@ export const useAuthCommand = (
|
||||
}
|
||||
}
|
||||
|
||||
const error = validateAuthMethodWithSettings(authType, settings);
|
||||
const error = await validateAuthMethodWithSettings(
|
||||
authType,
|
||||
settings,
|
||||
).catch((e: unknown) => getErrorMessage(e));
|
||||
|
||||
if (error) {
|
||||
onAuthError(error);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user