Download packages/cli/src/ui/auth/AuthDialog.test.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 15.7 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/auth/AuthDialog.test.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/auth/AuthDialog.test.tsx
-
curl -L -o AuthDialog.test.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/auth/AuthDialog.test.tsx
15.7 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { act } from 'react'; | |
| import { renderWithProviders } from '../../test-utils/render.js'; | |
| import { | |
| describe, | |
| it, | |
| expect, | |
| vi, | |
| beforeEach, | |
| afterEach, | |
| type Mock, | |
| } from 'vitest'; | |
| import { AuthDialog } from './AuthDialog.js'; | |
| import { AuthType, type Config, debugLogger } from '@google/gemini-cli-core'; | |
| import type { LoadedSettings } from '../../config/settings.js'; | |
| import { AuthState } from '../types.js'; | |
| import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js'; | |
| import { useKeypress } from '../hooks/useKeypress.js'; | |
| import { validateAuthMethodWithSettings } from './useAuth.js'; | |
| import { runExitCleanup } from '../../utils/cleanup.js'; | |
| import { Text } from 'ink'; | |
| import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js'; | |
| // Mocks | |
| vi.mock('@google/gemini-cli-core', async (importOriginal) => { | |
| const actual = | |
| await importOriginal<typeof import('@google/gemini-cli-core')>(); | |
| return { | |
| ...actual, | |
| clearCachedCredentialFile: vi.fn(), | |
| }; | |
| }); | |
| vi.mock('../../utils/cleanup.js', () => ({ | |
| runExitCleanup: vi.fn(), | |
| })); | |
| vi.mock('./useAuth.js', () => ({ | |
| validateAuthMethodWithSettings: vi.fn(), | |
| })); | |
| vi.mock('../hooks/useKeypress.js', () => ({ | |
| useKeypress: vi.fn(), | |
| })); | |
| vi.mock('../components/shared/RadioButtonSelect.js', () => ({ | |
| RadioButtonSelect: vi.fn(({ items, initialIndex }) => ( | |
| <> | |
| {items.map((item: { value: string; label: string }, index: number) => ( | |
| <Text key={item.value}> | |
| {index === initialIndex ? '(selected)' : '(not selected)'}{' '} | |
| {item.label} | |
| </Text> | |
| ))} | |
| </> | |
| )), | |
| })); | |
| const mockedUseKeypress = useKeypress as Mock; | |
| const mockedRadioButtonSelect = RadioButtonSelect as Mock; | |
| const mockedValidateAuthMethod = validateAuthMethodWithSettings as Mock; | |
| const mockedRunExitCleanup = runExitCleanup as Mock; | |
| describe('AuthDialog', () => { | |
| let props: { | |
| config: Config; | |
| settings: LoadedSettings; | |
| setAuthState: (state: AuthState) => void; | |
| authError: string | null; | |
| onAuthError: (error: string | null) => void; | |
| setAuthContext: (context: { requiresRestart?: boolean }) => void; | |
| }; | |
| beforeEach(() => { | |
| vi.resetAllMocks(); | |
| vi.stubEnv('CLOUD_SHELL', undefined as unknown as string); | |
| vi.stubEnv('GEMINI_CLI_USE_COMPUTE_ADC', undefined as unknown as string); | |
| vi.stubEnv('GEMINI_DEFAULT_AUTH_TYPE', undefined as unknown as string); | |
| vi.stubEnv('GEMINI_API_KEY', undefined as unknown as string); | |
| props = { | |
| config: { | |
| isBrowserLaunchSuppressed: vi.fn().mockReturnValue(false), | |
| } as unknown as Config, | |
| settings: { | |
| merged: { | |
| security: { | |
| auth: {}, | |
| }, | |
| }, | |
| setValue: vi.fn(), | |
| } as unknown as LoadedSettings, | |
| setAuthState: vi.fn(), | |
| authError: null, | |
| onAuthError: vi.fn(), | |
| setAuthContext: vi.fn(), | |
| }; | |
| }); | |
| afterEach(() => { | |
| vi.unstubAllEnvs(); | |
| }); | |
| describe('Environment Variable Effects on Auth Options', () => { | |
| const cloudShellLabel = 'Use Cloud Shell user credentials'; | |
| const metadataServerLabel = | |
| 'Use metadata server application default credentials'; | |
| const computeAdcItem = (label: string) => ({ | |
| label, | |
| value: AuthType.COMPUTE_ADC, | |
| key: AuthType.COMPUTE_ADC, | |
| }); | |
| it.each([ | |
| { | |
| env: { CLOUD_SHELL: 'true' }, | |
| shouldContain: [computeAdcItem(cloudShellLabel)], | |
| shouldNotContain: [computeAdcItem(metadataServerLabel)], | |
| desc: 'in Cloud Shell', | |
| }, | |
| { | |
| env: { GEMINI_CLI_USE_COMPUTE_ADC: 'true' }, | |
| shouldContain: [computeAdcItem(metadataServerLabel)], | |
| shouldNotContain: [computeAdcItem(cloudShellLabel)], | |
| desc: 'with GEMINI_CLI_USE_COMPUTE_ADC', | |
| }, | |
| { | |
| env: {}, | |
| shouldContain: [], | |
| shouldNotContain: [ | |
| computeAdcItem(cloudShellLabel), | |
| computeAdcItem(metadataServerLabel), | |
| ], | |
| desc: 'by default', | |
| }, | |
| ])( | |
| 'correctly shows/hides COMPUTE_ADC options $desc', | |
| async ({ env, shouldContain, shouldNotContain }) => { | |
| for (const [key, value] of Object.entries(env)) { | |
| vi.stubEnv(key, value as string); | |
| } | |
| const { unmount } = await renderWithProviders( | |
| <AuthDialog {...props} />, | |
| ); | |
| const items = mockedRadioButtonSelect.mock.calls[0][0].items; | |
| for (const item of shouldContain) { | |
| expect(items).toContainEqual(item); | |
| } | |
| for (const item of shouldNotContain) { | |
| expect(items).not.toContainEqual(item); | |
| } | |
| unmount(); | |
| }, | |
| ); | |
| }); | |
| it('filters auth types when enforcedType is set', async () => { | |
| props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI; | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const items = mockedRadioButtonSelect.mock.calls[0][0].items; | |
| expect(items).toHaveLength(1); | |
| expect(items[0].value).toBe(AuthType.USE_GEMINI); | |
| unmount(); | |
| }); | |
| it('sets initial index to 0 when enforcedType is set', async () => { | |
| props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI; | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { initialIndex } = mockedRadioButtonSelect.mock.calls[0][0]; | |
| expect(initialIndex).toBe(0); | |
| unmount(); | |
| }); | |
| describe('Initial Auth Type Selection', () => { | |
| it.each([ | |
| { | |
| setup: () => { | |
| props.settings.merged.security.auth.selectedType = | |
| AuthType.USE_VERTEX_AI; | |
| }, | |
| expected: AuthType.USE_VERTEX_AI, | |
| desc: 'from settings', | |
| }, | |
| { | |
| setup: () => { | |
| vi.stubEnv('GEMINI_DEFAULT_AUTH_TYPE', AuthType.USE_GEMINI); | |
| }, | |
| expected: AuthType.USE_GEMINI, | |
| desc: 'from GEMINI_DEFAULT_AUTH_TYPE env var', | |
| }, | |
| { | |
| setup: () => { | |
| vi.stubEnv('GEMINI_API_KEY', 'test-key'); | |
| }, | |
| expected: AuthType.USE_GEMINI, | |
| desc: 'from GEMINI_API_KEY env var', | |
| }, | |
| { | |
| setup: () => {}, | |
| expected: AuthType.LOGIN_WITH_GOOGLE, | |
| desc: 'defaults to Sign in with Google', | |
| }, | |
| ])('selects initial auth type $desc', async ({ setup, expected }) => { | |
| setup(); | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0]; | |
| expect(items[initialIndex].value).toBe(expected); | |
| unmount(); | |
| }); | |
| }); | |
| describe('handleAuthSelect', () => { | |
| it('calls onAuthError if validation fails', async () => { | |
| mockedValidateAuthMethod.mockResolvedValue('Invalid method'); | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { onSelect: handleAuthSelect } = | |
| mockedRadioButtonSelect.mock.calls[0][0]; | |
| await handleAuthSelect(AuthType.USE_GEMINI); | |
| expect(mockedValidateAuthMethod).toHaveBeenCalledWith( | |
| AuthType.USE_GEMINI, | |
| props.settings, | |
| ); | |
| expect(props.onAuthError).toHaveBeenCalledWith('Invalid method'); | |
| expect(props.settings.setValue).not.toHaveBeenCalled(); | |
| unmount(); | |
| }); | |
| it('sets auth context with requiresRestart: true for LOGIN_WITH_GOOGLE', async () => { | |
| mockedValidateAuthMethod.mockResolvedValue(null); | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { onSelect: handleAuthSelect } = | |
| mockedRadioButtonSelect.mock.calls[0][0]; | |
| await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE); | |
| expect(props.setAuthContext).toHaveBeenCalledWith({ | |
| requiresRestart: true, | |
| }); | |
| unmount(); | |
| }); | |
| it('sets auth context with requiresRestart: true for USE_VERTEX_AI in Cloud Shell', async () => { | |
| vi.stubEnv('CLOUD_SHELL', 'true'); | |
| mockedValidateAuthMethod.mockResolvedValue(null); | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { onSelect: handleAuthSelect } = | |
| mockedRadioButtonSelect.mock.calls[0][0]; | |
| await handleAuthSelect(AuthType.USE_VERTEX_AI); | |
| expect(props.setAuthContext).toHaveBeenCalledWith({ | |
| requiresRestart: true, | |
| }); | |
| unmount(); | |
| }); | |
| it('sets auth context with empty object for USE_VERTEX_AI outside Cloud Shell', async () => { | |
| vi.stubEnv('CLOUD_SHELL', ''); | |
| mockedValidateAuthMethod.mockResolvedValue(null); | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { onSelect: handleAuthSelect } = | |
| mockedRadioButtonSelect.mock.calls[0][0]; | |
| await handleAuthSelect(AuthType.USE_VERTEX_AI); | |
| expect(props.setAuthContext).toHaveBeenCalledWith({}); | |
| unmount(); | |
| }); | |
| it('sets auth context with empty object for other auth types', async () => { | |
| mockedValidateAuthMethod.mockResolvedValue(null); | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { onSelect: handleAuthSelect } = | |
| mockedRadioButtonSelect.mock.calls[0][0]; | |
| await handleAuthSelect(AuthType.USE_GEMINI); | |
| expect(props.setAuthContext).toHaveBeenCalledWith({}); | |
| unmount(); | |
| }); | |
| it('always shows API key dialog even when env var is present', async () => { | |
| mockedValidateAuthMethod.mockResolvedValue(null); | |
| vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env'); | |
| // props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { onSelect: handleAuthSelect } = | |
| mockedRadioButtonSelect.mock.calls[0][0]; | |
| await handleAuthSelect(AuthType.USE_GEMINI); | |
| expect(props.setAuthState).toHaveBeenCalledWith( | |
| AuthState.AwaitingApiKeyInput, | |
| ); | |
| unmount(); | |
| }); | |
| it('always shows API key dialog even when env var is empty string', async () => { | |
| mockedValidateAuthMethod.mockResolvedValue(null); | |
| vi.stubEnv('GEMINI_API_KEY', ''); // Empty string | |
| // props.settings.merged.security.auth.selectedType is undefined here | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { onSelect: handleAuthSelect } = | |
| mockedRadioButtonSelect.mock.calls[0][0]; | |
| await handleAuthSelect(AuthType.USE_GEMINI); | |
| expect(props.setAuthState).toHaveBeenCalledWith( | |
| AuthState.AwaitingApiKeyInput, | |
| ); | |
| unmount(); | |
| }); | |
| it('shows API key dialog on initial setup if no env var is present', async () => { | |
| mockedValidateAuthMethod.mockResolvedValue(null); | |
| // process.env['GEMINI_API_KEY'] is not set | |
| // props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { onSelect: handleAuthSelect } = | |
| mockedRadioButtonSelect.mock.calls[0][0]; | |
| await handleAuthSelect(AuthType.USE_GEMINI); | |
| expect(props.setAuthState).toHaveBeenCalledWith( | |
| AuthState.AwaitingApiKeyInput, | |
| ); | |
| unmount(); | |
| }); | |
| it('always shows API key dialog on re-auth even if env var is present', async () => { | |
| 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 = | |
| AuthType.LOGIN_WITH_GOOGLE; | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { onSelect: handleAuthSelect } = | |
| mockedRadioButtonSelect.mock.calls[0][0]; | |
| await handleAuthSelect(AuthType.USE_GEMINI); | |
| expect(props.setAuthState).toHaveBeenCalledWith( | |
| AuthState.AwaitingApiKeyInput, | |
| ); | |
| unmount(); | |
| }); | |
| it('exits process for Sign in with Google when browser is suppressed', async () => { | |
| vi.useFakeTimers(); | |
| const exitSpy = vi | |
| .spyOn(process, 'exit') | |
| .mockImplementation(() => undefined as never); | |
| const logSpy = vi.spyOn(debugLogger, 'log').mockImplementation(() => {}); | |
| vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true); | |
| mockedValidateAuthMethod.mockResolvedValue(null); | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const { onSelect: handleAuthSelect } = | |
| mockedRadioButtonSelect.mock.calls[0][0]; | |
| await act(async () => { | |
| await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE); | |
| await vi.runAllTimersAsync(); | |
| }); | |
| expect(mockedRunExitCleanup).toHaveBeenCalled(); | |
| expect(exitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE); | |
| exitSpy.mockRestore(); | |
| logSpy.mockRestore(); | |
| vi.useRealTimers(); | |
| unmount(); | |
| }); | |
| }); | |
| it('displays authError when provided', async () => { | |
| props.authError = 'Something went wrong'; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <AuthDialog {...props} />, | |
| ); | |
| expect(lastFrame()).toContain('Something went wrong'); | |
| unmount(); | |
| }); | |
| describe('useKeypress', () => { | |
| it.each([ | |
| { | |
| desc: 'does nothing on escape if authError is present', | |
| setup: () => { | |
| props.authError = 'Some error'; | |
| }, | |
| expectations: (p: typeof props) => { | |
| expect(p.onAuthError).not.toHaveBeenCalled(); | |
| expect(p.setAuthState).not.toHaveBeenCalled(); | |
| }, | |
| }, | |
| { | |
| desc: 'calls onAuthError on escape if no auth method is set', | |
| setup: () => { | |
| props.settings.merged.security.auth.selectedType = undefined; | |
| }, | |
| expectations: (p: typeof props) => { | |
| expect(p.onAuthError).toHaveBeenCalledWith( | |
| 'You must select an auth method to proceed. Press Ctrl+C twice to exit.', | |
| ); | |
| }, | |
| }, | |
| { | |
| desc: 'calls setAuthState(Unauthenticated) on escape if auth method is set', | |
| setup: () => { | |
| props.settings.merged.security.auth.selectedType = | |
| AuthType.USE_GEMINI; | |
| }, | |
| expectations: (p: typeof props) => { | |
| expect(p.setAuthState).toHaveBeenCalledWith( | |
| AuthState.Unauthenticated, | |
| ); | |
| expect(p.settings.setValue).not.toHaveBeenCalled(); | |
| }, | |
| }, | |
| ])('$desc', async ({ setup, expectations }) => { | |
| setup(); | |
| const { unmount } = await renderWithProviders(<AuthDialog {...props} />); | |
| const keypressHandler = mockedUseKeypress.mock.calls[0][0]; | |
| keypressHandler({ name: 'escape' }); | |
| expectations(props); | |
| unmount(); | |
| }); | |
| }); | |
| describe('Snapshots', () => { | |
| it('renders correctly with default props', async () => { | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <AuthDialog {...props} />, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| unmount(); | |
| }); | |
| it('renders correctly with auth error', async () => { | |
| props.authError = 'Something went wrong'; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <AuthDialog {...props} />, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| unmount(); | |
| }); | |
| it('renders correctly with enforced auth type', async () => { | |
| props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <AuthDialog {...props} />, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| unmount(); | |
| }); | |
| }); | |
| }); | |