Files
gemini-cli/packages/cli/src/validateNonInterActiveAuth.test.ts

471 lines
16 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type MockInstance,
} from 'vitest';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import {
AuthType,
OutputFormat,
makeFakeConfig,
debugLogger,
ExitCodes,
coreEvents,
} from '@google/gemini-cli-core';
import type { Config } from '@google/gemini-cli-core';
import * as auth from './config/auth.js';
2025-09-03 15:33:37 -07:00
import { type LoadedSettings } from './config/settings.js';
function createLocalMockConfig(overrides: Partial<Config> = {}): Config {
const config = makeFakeConfig();
Object.assign(config, overrides);
return config;
}
describe('validateNonInterActiveAuth', () => {
let originalEnvGeminiApiKey: string | undefined;
let originalEnvVertexAi: string | undefined;
let originalEnvGcp: string | undefined;
let debugLoggerErrorSpy: ReturnType<typeof vi.spyOn>;
let coreEventsEmitFeedbackSpy: MockInstance;
let processExitSpy: MockInstance;
2025-09-03 15:33:37 -07:00
let mockSettings: LoadedSettings;
beforeEach(() => {
originalEnvGeminiApiKey = process.env['GEMINI_API_KEY'];
originalEnvVertexAi = process.env['GOOGLE_GENAI_USE_VERTEXAI'];
originalEnvGcp = process.env['GOOGLE_GENAI_USE_GCA'];
delete process.env['GEMINI_API_KEY'];
delete process.env['GOOGLE_GENAI_USE_VERTEXAI'];
delete process.env['GOOGLE_GENAI_USE_GCA'];
debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
coreEventsEmitFeedbackSpy = vi
.spyOn(coreEvents, 'emitFeedback')
.mockImplementation(() => {});
processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code?: string | number | null | undefined) => {
throw new Error(`process.exit(${code}) called`);
});
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue(null);
2025-09-03 15:33:37 -07:00
mockSettings = {
system: { path: '', settings: {} },
systemDefaults: { path: '', settings: {} },
user: { path: '', settings: {} },
workspace: { path: '', settings: {} },
errors: [],
setValue: vi.fn(),
merged: {
security: {
auth: {
enforcedType: undefined,
},
},
},
isTrusted: true,
migratedInMemoryScopes: new Set(),
2025-09-03 15:33:37 -07:00
forScope: vi.fn(),
computeMergedSettings: vi.fn(),
} as unknown as LoadedSettings;
});
afterEach(() => {
if (originalEnvGeminiApiKey !== undefined) {
process.env['GEMINI_API_KEY'] = originalEnvGeminiApiKey;
} else {
delete process.env['GEMINI_API_KEY'];
}
if (originalEnvVertexAi !== undefined) {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = originalEnvVertexAi;
} else {
delete process.env['GOOGLE_GENAI_USE_VERTEXAI'];
}
if (originalEnvGcp !== undefined) {
process.env['GOOGLE_GENAI_USE_GCA'] = originalEnvGcp;
} else {
delete process.env['GOOGLE_GENAI_USE_GCA'];
}
vi.restoreAllMocks();
});
it('exits if no auth type is configured or env vars set', async () => {
const nonInteractiveConfig = createLocalMockConfig({
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
});
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
}
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Please set an Auth method'),
);
expect(processExitSpy).toHaveBeenCalledWith(
ExitCodes.FATAL_AUTHENTICATION_ERROR,
);
});
it('uses LOGIN_WITH_GOOGLE if GOOGLE_GENAI_USE_GCA is set', async () => {
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
const nonInteractiveConfig = createLocalMockConfig({});
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect(processExitSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
});
it('uses USE_GEMINI if GEMINI_API_KEY is set', async () => {
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = createLocalMockConfig({});
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect(processExitSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
});
it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true (with GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION)', async () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig = createLocalMockConfig({});
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect(processExitSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
});
it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true and GOOGLE_API_KEY is set', async () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_API_KEY'] = 'vertex-api-key';
const nonInteractiveConfig = createLocalMockConfig({});
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect(processExitSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
});
it('uses LOGIN_WITH_GOOGLE if GOOGLE_GENAI_USE_GCA is set, even with other env vars', async () => {
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
process.env['GEMINI_API_KEY'] = 'fake-key';
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig = createLocalMockConfig({});
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect(processExitSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
});
it('uses USE_VERTEX_AI if both GEMINI_API_KEY and GOOGLE_GENAI_USE_VERTEXAI are set', async () => {
process.env['GEMINI_API_KEY'] = 'fake-key';
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig = createLocalMockConfig({});
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect(processExitSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
});
it('uses USE_GEMINI if GOOGLE_GENAI_USE_VERTEXAI is false, GEMINI_API_KEY is set, and project/location are available', async () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'false';
process.env['GEMINI_API_KEY'] = 'fake-key';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig = createLocalMockConfig({});
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect(processExitSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
});
it('uses configuredAuthType over environment variables', async () => {
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = createLocalMockConfig({});
await validateNonInteractiveAuth(
AuthType.LOGIN_WITH_GOOGLE,
undefined,
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect(processExitSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
});
it('exits if validateAuthMethod returns error', async () => {
// Mock validateAuthMethod to return error
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
const nonInteractiveConfig = createLocalMockConfig({
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
});
try {
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
}
expect(debugLoggerErrorSpy).toHaveBeenCalledWith('Auth error!');
expect(processExitSpy).toHaveBeenCalledWith(
ExitCodes.FATAL_AUTHENTICATION_ERROR,
);
});
it('skips validation if useExternalAuth is true', async () => {
// Mock validateAuthMethod to return error to ensure it's not being called
const validateAuthMethodSpy = vi
.spyOn(auth, 'validateAuthMethod')
.mockReturnValue('Auth error!');
const nonInteractiveConfig = createLocalMockConfig({});
// Even with an invalid auth type, it should not exit
// because validation is skipped.
await validateNonInteractiveAuth(
'invalid-auth-type' as AuthType,
true, // useExternalAuth = true
nonInteractiveConfig,
2025-09-03 15:33:37 -07:00
mockSettings,
);
expect(validateAuthMethodSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
});
2025-09-03 15:33:37 -07:00
it('succeeds if effectiveAuthType matches enforcedAuthType', async () => {
mockSettings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
2025-09-03 15:33:37 -07:00
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = createLocalMockConfig({});
2025-09-03 15:33:37 -07:00
await validateNonInteractiveAuth(
undefined,
2025-09-03 15:33:37 -07:00
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(processExitSpy).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
2025-09-03 15:33:37 -07:00
});
it('exits if configuredAuthType does not match enforcedAuthType', async () => {
mockSettings.merged.security.auth.enforcedType = AuthType.LOGIN_WITH_GOOGLE;
const nonInteractiveConfig = createLocalMockConfig({
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
});
2025-09-03 15:33:37 -07:00
try {
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
2025-09-03 15:33:37 -07:00
}
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
"The enforced authentication type is 'oauth-personal', but the current type is 'gemini-api-key'. Please re-authenticate with the correct type.",
);
expect(processExitSpy).toHaveBeenCalledWith(
ExitCodes.FATAL_AUTHENTICATION_ERROR,
);
});
it('exits if auth from env var does not match enforcedAuthType', async () => {
mockSettings.merged.security.auth.enforcedType = AuthType.LOGIN_WITH_GOOGLE;
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = createLocalMockConfig({
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
});
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
}
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
"The enforced authentication type is 'oauth-personal', but the current type is 'gemini-api-key'. Please re-authenticate with the correct type.",
2025-09-03 15:33:37 -07:00
);
expect(processExitSpy).toHaveBeenCalledWith(
ExitCodes.FATAL_AUTHENTICATION_ERROR,
);
2025-09-03 15:33:37 -07:00
});
describe('JSON output mode', () => {
it(`prints JSON error when no auth is configured and exits with code ${ExitCodes.FATAL_AUTHENTICATION_ERROR}`, async () => {
const nonInteractiveConfig = createLocalMockConfig({
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
});
let thrown: Error | undefined;
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
} catch (e) {
thrown = e as Error;
}
expect(thrown?.message).toBe(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
// Checking coreEventsEmitFeedbackSpy arguments
const errorArg = coreEventsEmitFeedbackSpy.mock.calls[0]?.[1] as string;
const payload = JSON.parse(errorArg);
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
expect(payload.error.message).toContain(
'Please set an Auth method in your',
);
});
it(`prints JSON error when enforced auth mismatches current auth and exits with code ${ExitCodes.FATAL_AUTHENTICATION_ERROR}`, async () => {
mockSettings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const nonInteractiveConfig = createLocalMockConfig({
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
});
let thrown: Error | undefined;
try {
await validateNonInteractiveAuth(
AuthType.LOGIN_WITH_GOOGLE,
undefined,
nonInteractiveConfig,
mockSettings,
);
} catch (e) {
thrown = e as Error;
}
expect(thrown?.message).toBe(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
{
// Checking coreEventsEmitFeedbackSpy arguments
const errorArg = coreEventsEmitFeedbackSpy.mock.calls[0]?.[1] as string;
const payload = JSON.parse(errorArg);
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
expect(payload.error.message).toContain(
"The enforced authentication type is 'gemini-api-key', but the current type is 'oauth-personal'. Please re-authenticate with the correct type.",
);
}
});
it(`prints JSON error when validateAuthMethod fails and exits with code ${ExitCodes.FATAL_AUTHENTICATION_ERROR}`, async () => {
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = createLocalMockConfig({
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
});
let thrown: Error | undefined;
try {
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
} catch (e) {
thrown = e as Error;
}
expect(thrown?.message).toBe(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
{
// Checking coreEventsEmitFeedbackSpy arguments
const errorArg = coreEventsEmitFeedbackSpy.mock.calls[0]?.[1] as string;
const payload = JSON.parse(errorArg);
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
expect(payload.error.message).toBe('Auth error!');
}
});
});
});