mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
JSON errors in non-interactive auth validation (#8373)
This commit is contained in:
@@ -34,4 +34,38 @@ describe('JSON output', () => {
|
|||||||
expect(parsed).toHaveProperty('stats');
|
expect(parsed).toHaveProperty('stats');
|
||||||
expect(typeof parsed.stats).toBe('object');
|
expect(typeof parsed.stats).toBe('object');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return a JSON error for enforced auth mismatch before running', async () => {
|
||||||
|
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
|
||||||
|
await rig.setup('json-output-auth-mismatch', {
|
||||||
|
settings: {
|
||||||
|
security: { auth: { enforcedType: 'gemini-api-key' } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let thrown: Error | undefined;
|
||||||
|
try {
|
||||||
|
await rig.run('Hello', '--output-format', 'json');
|
||||||
|
expect.fail('Expected process to exit with error');
|
||||||
|
} catch (e) {
|
||||||
|
thrown = e as Error;
|
||||||
|
} finally {
|
||||||
|
delete process.env['GOOGLE_GENAI_USE_GCA'];
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(thrown).toBeDefined();
|
||||||
|
const message = (thrown as Error).message;
|
||||||
|
const jsonStart = message.indexOf('{');
|
||||||
|
expect(jsonStart).toBeGreaterThan(-1);
|
||||||
|
const payload = JSON.parse(message.slice(jsonStart));
|
||||||
|
expect(payload.error).toBeDefined();
|
||||||
|
expect(payload.error.type).toBe('Error');
|
||||||
|
expect(payload.error.code).toBe(1);
|
||||||
|
expect(payload.error.message).toContain(
|
||||||
|
'configured auth type is gemini-api-key',
|
||||||
|
);
|
||||||
|
expect(payload.error.message).toContain(
|
||||||
|
'current auth type is oauth-personal',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
|
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
|
||||||
import { AuthType } from '@google/gemini-cli-core';
|
import { AuthType, OutputFormat } from '@google/gemini-cli-core';
|
||||||
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
import * as auth from './config/auth.js';
|
import * as auth from './config/auth.js';
|
||||||
import { type LoadedSettings } from './config/settings.js';
|
import { type LoadedSettings } from './config/settings.js';
|
||||||
|
|
||||||
@@ -74,6 +75,10 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
it('exits if no auth type is configured or env vars set', async () => {
|
it('exits if no auth type is configured or env vars set', async () => {
|
||||||
const nonInteractiveConfig = {
|
const nonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
|
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
|
||||||
|
getContentGeneratorConfig: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ authType: undefined }),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await validateNonInteractiveAuth(
|
await validateNonInteractiveAuth(
|
||||||
@@ -223,6 +228,10 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
|
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
|
||||||
const nonInteractiveConfig = {
|
const nonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
|
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
|
||||||
|
getContentGeneratorConfig: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ authType: undefined }),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await validateNonInteractiveAuth(
|
await validateNonInteractiveAuth(
|
||||||
@@ -286,6 +295,10 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
|
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
|
||||||
const nonInteractiveConfig = {
|
const nonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
|
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
|
||||||
|
getContentGeneratorConfig: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ authType: undefined }),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await validateNonInteractiveAuth(
|
await validateNonInteractiveAuth(
|
||||||
@@ -303,4 +316,107 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
);
|
);
|
||||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('JSON output mode', () => {
|
||||||
|
it('prints JSON error when no auth is configured and exits with code 1', async () => {
|
||||||
|
const nonInteractiveConfig = {
|
||||||
|
refreshAuth: refreshAuthMock,
|
||||||
|
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
|
||||||
|
getContentGeneratorConfig: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ authType: undefined }),
|
||||||
|
};
|
||||||
|
|
||||||
|
let thrown: Error | undefined;
|
||||||
|
try {
|
||||||
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig as unknown as Config,
|
||||||
|
mockSettings,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
thrown = e as Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(thrown?.message).toBe('process.exit(1) called');
|
||||||
|
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
|
||||||
|
const payload = JSON.parse(errorArg);
|
||||||
|
expect(payload.error.type).toBe('Error');
|
||||||
|
expect(payload.error.code).toBe(1);
|
||||||
|
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 1', async () => {
|
||||||
|
mockSettings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||||
|
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
|
||||||
|
|
||||||
|
const nonInteractiveConfig = {
|
||||||
|
refreshAuth: refreshAuthMock,
|
||||||
|
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
|
||||||
|
getContentGeneratorConfig: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ authType: undefined }),
|
||||||
|
};
|
||||||
|
|
||||||
|
let thrown: Error | undefined;
|
||||||
|
try {
|
||||||
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig as unknown as Config,
|
||||||
|
mockSettings,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
thrown = e as Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(thrown?.message).toBe('process.exit(1) called');
|
||||||
|
{
|
||||||
|
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
|
||||||
|
const payload = JSON.parse(errorArg);
|
||||||
|
expect(payload.error.type).toBe('Error');
|
||||||
|
expect(payload.error.code).toBe(1);
|
||||||
|
expect(payload.error.message).toContain(
|
||||||
|
'The configured auth type is gemini-api-key, but the current auth type is oauth-personal.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prints JSON error when validateAuthMethod fails and exits with code 1', async () => {
|
||||||
|
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
|
||||||
|
process.env['GEMINI_API_KEY'] = 'fake-key';
|
||||||
|
|
||||||
|
const nonInteractiveConfig = {
|
||||||
|
refreshAuth: refreshAuthMock,
|
||||||
|
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
|
||||||
|
getContentGeneratorConfig: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ authType: undefined }),
|
||||||
|
};
|
||||||
|
|
||||||
|
let thrown: Error | undefined;
|
||||||
|
try {
|
||||||
|
await validateNonInteractiveAuth(
|
||||||
|
AuthType.USE_GEMINI,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig as unknown as Config,
|
||||||
|
mockSettings,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
thrown = e as Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(thrown?.message).toBe('process.exit(1) called');
|
||||||
|
{
|
||||||
|
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
|
||||||
|
const payload = JSON.parse(errorArg);
|
||||||
|
expect(payload.error.type).toBe('Error');
|
||||||
|
expect(payload.error.code).toBe(1);
|
||||||
|
expect(payload.error.message).toBe('Auth error!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
import { AuthType } from '@google/gemini-cli-core';
|
import { AuthType, OutputFormat } from '@google/gemini-cli-core';
|
||||||
import { USER_SETTINGS_PATH } from './config/settings.js';
|
import { USER_SETTINGS_PATH } from './config/settings.js';
|
||||||
import { validateAuthMethod } from './config/auth.js';
|
import { validateAuthMethod } from './config/auth.js';
|
||||||
import { type LoadedSettings } from './config/settings.js';
|
import { type LoadedSettings } from './config/settings.js';
|
||||||
|
import { handleError } from './utils/errors.js';
|
||||||
|
|
||||||
function getAuthTypeFromEnv(): AuthType | undefined {
|
function getAuthTypeFromEnv(): AuthType | undefined {
|
||||||
if (process.env['GOOGLE_GENAI_USE_GCA'] === 'true') {
|
if (process.env['GOOGLE_GENAI_USE_GCA'] === 'true') {
|
||||||
@@ -29,14 +30,13 @@ export async function validateNonInteractiveAuth(
|
|||||||
nonInteractiveConfig: Config,
|
nonInteractiveConfig: Config,
|
||||||
settings: LoadedSettings,
|
settings: LoadedSettings,
|
||||||
) {
|
) {
|
||||||
|
try {
|
||||||
const enforcedType = settings.merged.security?.auth?.enforcedType;
|
const enforcedType = settings.merged.security?.auth?.enforcedType;
|
||||||
if (enforcedType) {
|
if (enforcedType) {
|
||||||
const currentAuthType = getAuthTypeFromEnv();
|
const currentAuthType = getAuthTypeFromEnv();
|
||||||
if (currentAuthType !== enforcedType) {
|
if (currentAuthType !== enforcedType) {
|
||||||
console.error(
|
const message = `The configured auth type is ${enforcedType}, but the current auth type is ${currentAuthType}. Please re-authenticate with the correct type.`;
|
||||||
`The configured auth type is ${enforcedType}, but the current auth type is ${currentAuthType}. Please re-authenticate with the correct type.`,
|
throw new Error(message);
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,20 +44,31 @@ export async function validateNonInteractiveAuth(
|
|||||||
enforcedType || getAuthTypeFromEnv() || configuredAuthType;
|
enforcedType || getAuthTypeFromEnv() || configuredAuthType;
|
||||||
|
|
||||||
if (!effectiveAuthType) {
|
if (!effectiveAuthType) {
|
||||||
console.error(
|
const message = `Please set an Auth method in your ${USER_SETTINGS_PATH} or specify one of the following environment variables before running: GEMINI_API_KEY, GOOGLE_GENAI_USE_VERTEXAI, GOOGLE_GENAI_USE_GCA`;
|
||||||
`Please set an Auth method in your ${USER_SETTINGS_PATH} or specify one of the following environment variables before running: GEMINI_API_KEY, GOOGLE_GENAI_USE_VERTEXAI, GOOGLE_GENAI_USE_GCA`,
|
throw new Error(message);
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authType: AuthType = effectiveAuthType as AuthType;
|
||||||
|
|
||||||
if (!useExternalAuth) {
|
if (!useExternalAuth) {
|
||||||
const err = validateAuthMethod(effectiveAuthType);
|
const err = validateAuthMethod(String(authType));
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
console.error(err);
|
throw new Error(err);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await nonInteractiveConfig.refreshAuth(effectiveAuthType);
|
await nonInteractiveConfig.refreshAuth(authType);
|
||||||
return nonInteractiveConfig;
|
return nonInteractiveConfig;
|
||||||
|
} catch (error) {
|
||||||
|
if (nonInteractiveConfig.getOutputFormat() === OutputFormat.JSON) {
|
||||||
|
handleError(
|
||||||
|
error instanceof Error ? error : new Error(String(error)),
|
||||||
|
nonInteractiveConfig,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user