Files
qwen-code/packages/cli/src/validateNonInterActiveAuth.test.ts

508 lines
17 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { AuthType, OutputFormat } from '@qwen-code/qwen-code-core';
import type { Config } from '@qwen-code/qwen-code-core';
import * as auth from './config/auth.js';
import { type LoadedSettings } from './config/settings.js';
import * as JsonOutputAdapterModule from './nonInteractive/io/JsonOutputAdapter.js';
import * as StreamJsonOutputAdapterModule from './nonInteractive/io/StreamJsonOutputAdapter.js';
import * as cleanupModule from './utils/cleanup.js';
describe('validateNonInterActiveAuth', () => {
let originalEnvGeminiApiKey: string | undefined;
let originalEnvVertexAi: string | undefined;
let originalEnvGcp: string | undefined;
let originalEnvOpenAiApiKey: string | undefined;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
let processExitSpy: ReturnType<typeof vi.spyOn<[code?: number], never>>;
let refreshAuthMock: ReturnType<typeof vi.fn>;
let mockSettings: LoadedSettings;
beforeEach(() => {
originalEnvGeminiApiKey = process.env['GEMINI_API_KEY'];
originalEnvVertexAi = process.env['GOOGLE_GENAI_USE_VERTEXAI'];
originalEnvGcp = process.env['GOOGLE_GENAI_USE_GCA'];
originalEnvOpenAiApiKey = process.env['OPENAI_API_KEY'];
delete process.env['GEMINI_API_KEY'];
delete process.env['GOOGLE_GENAI_USE_VERTEXAI'];
delete process.env['GOOGLE_GENAI_USE_GCA'];
delete process.env['OPENAI_API_KEY'];
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
throw new Error(`process.exit(${code}) called`);
}) as ReturnType<typeof vi.spyOn<[code?: number], never>>;
refreshAuthMock = vi.fn().mockResolvedValue('refreshed');
mockSettings = {
system: { path: '', settings: {} },
systemDefaults: { path: '', settings: {} },
user: { path: '', settings: {} },
workspace: { path: '', settings: {} },
errors: [],
setValue: vi.fn(),
merged: {
security: {
auth: {
enforcedType: undefined,
},
},
},
isTrusted: true,
migratedInMemorScopes: new Set(),
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'];
}
if (originalEnvOpenAiApiKey !== undefined) {
process.env['OPENAI_API_KEY'] = originalEnvOpenAiApiKey;
} else {
delete process.env['OPENAI_API_KEY'];
}
vi.restoreAllMocks();
});
it('exits if no auth type is configured or env vars set', async () => {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Please set an Auth method'),
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it('uses USE_OPENAI if OPENAI_API_KEY is set', async () => {
process.env['OPENAI_API_KEY'] = 'fake-openai-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_OPENAI);
});
it('uses configured QWEN_OAUTH if provided', async () => {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
await validateNonInteractiveAuth(
AuthType.QWEN_OAUTH,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.QWEN_OAUTH);
});
it('exits if validateAuthMethod returns error', async () => {
// Mock validateAuthMethod to return error
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith('Auth error!');
expect(processExitSpy).toHaveBeenCalledWith(1);
});
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 = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
// 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,
mockSettings,
);
expect(validateAuthMethodSpy).not.toHaveBeenCalled();
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
// We still expect refreshAuth to be called with the (invalid) type
expect(refreshAuthMock).toHaveBeenCalledWith('invalid-auth-type');
});
it('uses enforcedAuthType if provided', async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_OPENAI;
mockSettings.merged.security!.auth!.selectedType = AuthType.USE_OPENAI;
// Set required env var for USE_OPENAI to ensure enforcedAuthType takes precedence
process.env['OPENAI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
AuthType.USE_OPENAI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_OPENAI);
});
it('exits if currentAuthType does not match enforcedAuthType', async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
process.env['OPENAI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
AuthType.USE_OPENAI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith(
'The configured auth type is qwen-oauth, but the current auth type is openai. Please re-authenticate with the correct type.',
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
describe('JSON output mode', () => {
let emitResultMock: ReturnType<typeof vi.fn>;
let runExitCleanupMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
emitResultMock = vi.fn();
runExitCleanupMock = vi.fn().mockResolvedValue(undefined);
vi.spyOn(JsonOutputAdapterModule, 'JsonOutputAdapter').mockImplementation(
() =>
({
emitResult: emitResultMock,
}) as unknown as JsonOutputAdapterModule.JsonOutputAdapter,
);
vi.spyOn(cleanupModule, 'runExitCleanup').mockImplementation(
runExitCleanupMock,
);
});
it('emits error result and exits when no auth is configured', async () => {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(emitResultMock).toHaveBeenCalledWith({
isError: true,
errorMessage: expect.stringContaining(
'Please set an Auth method in your',
),
durationMs: 0,
apiDurationMs: 0,
numTurns: 0,
usage: undefined,
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('emits error result and exits when enforced auth mismatches current auth', async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
process.env['OPENAI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(emitResultMock).toHaveBeenCalledWith({
isError: true,
errorMessage: expect.stringContaining(
'The configured auth type is qwen-oauth, but the current auth type is openai.',
),
durationMs: 0,
apiDurationMs: 0,
numTurns: 0,
usage: undefined,
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('emits error result and exits when validateAuthMethod fails', async () => {
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
process.env['OPENAI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
AuthType.USE_OPENAI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(emitResultMock).toHaveBeenCalledWith({
isError: true,
errorMessage: 'Auth error!',
durationMs: 0,
apiDurationMs: 0,
numTurns: 0,
usage: undefined,
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
describe('STREAM_JSON output mode', () => {
let emitResultMock: ReturnType<typeof vi.fn>;
let runExitCleanupMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
emitResultMock = vi.fn();
runExitCleanupMock = vi.fn().mockResolvedValue(undefined);
vi.spyOn(
StreamJsonOutputAdapterModule,
'StreamJsonOutputAdapter',
).mockImplementation(
() =>
({
emitResult: emitResultMock,
}) as unknown as StreamJsonOutputAdapterModule.StreamJsonOutputAdapter,
);
vi.spyOn(cleanupModule, 'runExitCleanup').mockImplementation(
runExitCleanupMock,
);
});
it('emits error result and exits when no auth is configured', async () => {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.STREAM_JSON),
getIncludePartialMessages: vi.fn().mockReturnValue(false),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(emitResultMock).toHaveBeenCalledWith({
isError: true,
errorMessage: expect.stringContaining(
'Please set an Auth method in your',
),
durationMs: 0,
apiDurationMs: 0,
numTurns: 0,
usage: undefined,
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('emits error result and exits when enforced auth mismatches current auth', async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
process.env['OPENAI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.STREAM_JSON),
getIncludePartialMessages: vi.fn().mockReturnValue(false),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(emitResultMock).toHaveBeenCalledWith({
isError: true,
errorMessage: expect.stringContaining(
'The configured auth type is qwen-oauth, but the current auth type is openai.',
),
durationMs: 0,
apiDurationMs: 0,
numTurns: 0,
usage: undefined,
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('emits error result and exits when validateAuthMethod fails', async () => {
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
process.env['OPENAI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.STREAM_JSON),
getIncludePartialMessages: vi.fn().mockReturnValue(false),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
AuthType.USE_OPENAI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(emitResultMock).toHaveBeenCalledWith({
isError: true,
errorMessage: 'Auth error!',
durationMs: 0,
apiDurationMs: 0,
numTurns: 0,
usage: undefined,
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
});