basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { AuthType } from '@qwen-code/qwen-code-core';8import { vi } from 'vitest';9import { validateAuthMethod } from './auth.js';10import * as settings from './settings.js';11 12vi.mock('./settings.js', () => ({13 loadEnvironment: vi.fn(),14 loadSettings: vi.fn().mockReturnValue({15 merged: {},16 }),17}));18 19describe('validateAuthMethod', () => {20 beforeEach(() => {21 vi.resetModules();22 // Reset mock to default23 vi.mocked(settings.loadSettings).mockReturnValue({24 merged: {},25 } as ReturnType<typeof settings.loadSettings>);26 });27 28 afterEach(() => {29 vi.unstubAllEnvs();30 delete process.env['OPENAI_API_KEY'];31 delete process.env['CUSTOM_API_KEY'];32 delete process.env['GEMINI_API_KEY'];33 delete process.env['GEMINI_API_KEY_ALTERED'];34 delete process.env['ANTHROPIC_API_KEY'];35 delete process.env['ANTHROPIC_BASE_URL'];36 delete process.env['GOOGLE_API_KEY'];37 delete process.env['IDEALAB_KEY'];38 delete process.env['TOKEN_PLAN_KEY'];39 });40 41 it('should return null for USE_OPENAI with default env key', () => {42 process.env['OPENAI_API_KEY'] = 'fake-key';43 expect(validateAuthMethod(AuthType.USE_OPENAI)).toBeNull();44 });45 46 it('should return an error message for USE_OPENAI if no API key is available', () => {47 expect(validateAuthMethod(AuthType.USE_OPENAI)).toBe(48 "Missing API key for OpenAI-compatible auth. Set settings.security.auth.apiKey, or set the 'OPENAI_API_KEY' environment variable.",49 );50 });51 52 it('should return null for USE_OPENAI with custom envKey from modelProviders', () => {53 vi.mocked(settings.loadSettings).mockReturnValue({54 merged: {55 model: { name: 'custom-model' },56 modelProviders: {57 openai: [{ id: 'custom-model', envKey: 'CUSTOM_API_KEY' }],58 },59 },60 } as unknown as ReturnType<typeof settings.loadSettings>);61 process.env['CUSTOM_API_KEY'] = 'custom-key';62 63 expect(validateAuthMethod(AuthType.USE_OPENAI)).toBeNull();64 });65 66 it('should return null for USE_OPENAI with custom envKey stored in settings.env', () => {67 vi.mocked(settings.loadSettings).mockReturnValue({68 merged: {69 env: { CUSTOM_API_KEY: 'settings-env-key' },70 model: { name: 'custom-model' },71 modelProviders: {72 openai: [{ id: 'custom-model', envKey: 'CUSTOM_API_KEY' }],73 },74 },75 } as unknown as ReturnType<typeof settings.loadSettings>);76 77 expect(validateAuthMethod(AuthType.USE_OPENAI)).toBeNull();78 });79 80 it('uses providerProtocol mappings to find custom provider env keys', () => {81 vi.mocked(settings.loadSettings).mockReturnValue({82 merged: {83 model: { name: 'qwen3' },84 modelProviders: {85 idealab: [{ id: 'qwen3', envKey: 'IDEALAB_KEY' }],86 },87 providerProtocol: { idealab: 'openai' },88 },89 } as unknown as ReturnType<typeof settings.loadSettings>);90 process.env['IDEALAB_KEY'] = 'idealab-key';91 92 expect(validateAuthMethod(AuthType.USE_OPENAI)).toBeNull();93 });94 95 it('disambiguates by settings.model.baseUrl when providers share a model id', () => {96 // Two providers with the same id; the persisted baseUrl selects the second.97 // Only the second provider's env key is set, so validation passes only if98 // the lookup honors baseUrl rather than matching the first id entry.99 vi.mocked(settings.loadSettings).mockReturnValue({100 merged: {101 model: {102 name: 'qwen3.7-max',103 baseUrl: 'https://idealab.example.com/v1',104 },105 modelProviders: {106 openai: [107 {108 id: 'qwen3.7-max',109 baseUrl: 'https://token-plan.example.com/v1',110 envKey: 'TOKEN_PLAN_KEY',111 },112 {113 id: 'qwen3.7-max',114 baseUrl: 'https://idealab.example.com/v1',115 envKey: 'IDEALAB_KEY',116 },117 ],118 },119 },120 } as unknown as ReturnType<typeof settings.loadSettings>);121 process.env['IDEALAB_KEY'] = 'idealab-key';122 123 expect(validateAuthMethod(AuthType.USE_OPENAI)).toBeNull();124 });125 126 it('reports the selected provider env key when providers share a model id', () => {127 vi.mocked(settings.loadSettings).mockReturnValue({128 merged: {129 model: {130 name: 'qwen3.7-max',131 baseUrl: 'https://idealab.example.com/v1',132 },133 modelProviders: {134 openai: [135 {136 id: 'qwen3.7-max',137 baseUrl: 'https://token-plan.example.com/v1',138 envKey: 'TOKEN_PLAN_KEY',139 },140 {141 id: 'qwen3.7-max',142 baseUrl: 'https://idealab.example.com/v1',143 envKey: 'IDEALAB_KEY',144 },145 ],146 },147 },148 } as unknown as ReturnType<typeof settings.loadSettings>);149 150 // No env keys set → error must name the selected (IdeaLab) provider's key.151 const result = validateAuthMethod(AuthType.USE_OPENAI);152 expect(result).toContain('IDEALAB_KEY');153 expect(result).not.toContain('TOKEN_PLAN_KEY');154 });155 156 it('should return error with custom envKey hint when modelProviders envKey is set but env var is missing', () => {157 vi.mocked(settings.loadSettings).mockReturnValue({158 merged: {159 model: { name: 'custom-model' },160 modelProviders: {161 openai: [{ id: 'custom-model', envKey: 'CUSTOM_API_KEY' }],162 },163 },164 } as unknown as ReturnType<typeof settings.loadSettings>);165 166 const result = validateAuthMethod(AuthType.USE_OPENAI);167 expect(result).toContain('CUSTOM_API_KEY');168 });169 170 it('should return null for USE_GEMINI with custom envKey', () => {171 vi.mocked(settings.loadSettings).mockReturnValue({172 merged: {173 model: { name: 'gemini-1.5-flash' },174 modelProviders: {175 gemini: [176 { id: 'gemini-1.5-flash', envKey: 'GEMINI_API_KEY_ALTERED' },177 ],178 },179 },180 } as unknown as ReturnType<typeof settings.loadSettings>);181 process.env['GEMINI_API_KEY_ALTERED'] = 'altered-key';182 183 expect(validateAuthMethod(AuthType.USE_GEMINI)).toBeNull();184 });185 186 it('should return error with custom envKey for USE_GEMINI when env var is missing', () => {187 vi.mocked(settings.loadSettings).mockReturnValue({188 merged: {189 model: { name: 'gemini-1.5-flash' },190 modelProviders: {191 gemini: [192 { id: 'gemini-1.5-flash', envKey: 'GEMINI_API_KEY_ALTERED' },193 ],194 },195 },196 } as unknown as ReturnType<typeof settings.loadSettings>);197 198 const result = validateAuthMethod(AuthType.USE_GEMINI);199 expect(result).toContain('GEMINI_API_KEY_ALTERED');200 });201 202 it('should return an error for QWEN_OAUTH (free tier discontinued)', () => {203 const result = validateAuthMethod(AuthType.QWEN_OAUTH);204 expect(result).toContain('discontinued on 2026-04-15');205 });206 207 it('should return an error message for an invalid auth method', () => {208 expect(validateAuthMethod('invalid-method')).toBe(209 'Invalid auth method selected.',210 );211 });212 213 it('should return null for USE_ANTHROPIC with custom envKey and baseUrl', () => {214 vi.mocked(settings.loadSettings).mockReturnValue({215 merged: {216 model: { name: 'claude-3' },217 modelProviders: {218 anthropic: [219 {220 id: 'claude-3',221 envKey: 'CUSTOM_ANTHROPIC_KEY',222 baseUrl: 'https://api.anthropic.com',223 },224 ],225 },226 },227 } as unknown as ReturnType<typeof settings.loadSettings>);228 process.env['CUSTOM_ANTHROPIC_KEY'] = 'custom-anthropic-key';229 230 expect(validateAuthMethod(AuthType.USE_ANTHROPIC)).toBeNull();231 });232 233 it('should return error for USE_ANTHROPIC when baseUrl is missing', () => {234 vi.mocked(settings.loadSettings).mockReturnValue({235 merged: {236 model: { name: 'claude-3' },237 modelProviders: {238 anthropic: [{ id: 'claude-3', envKey: 'CUSTOM_ANTHROPIC_KEY' }],239 },240 },241 } as unknown as ReturnType<typeof settings.loadSettings>);242 process.env['CUSTOM_ANTHROPIC_KEY'] = 'custom-key';243 244 const result = validateAuthMethod(AuthType.USE_ANTHROPIC);245 expect(result).toContain('modelProviders[].baseUrl');246 });247 248 it('should return null for USE_VERTEX_AI with custom envKey', () => {249 vi.mocked(settings.loadSettings).mockReturnValue({250 merged: {251 model: { name: 'vertex-model' },252 modelProviders: {253 'vertex-ai': [254 { id: 'vertex-model', envKey: 'GOOGLE_API_KEY_VERTEX' },255 ],256 },257 },258 } as unknown as ReturnType<typeof settings.loadSettings>);259 process.env['GOOGLE_API_KEY_VERTEX'] = 'vertex-key';260 261 expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBeNull();262 });263 264 it('should use config.getModelsConfig().getModel() when Config is provided', () => {265 // Settings has a different model266 vi.mocked(settings.loadSettings).mockReturnValue({267 merged: {268 model: { name: 'settings-model' },269 modelProviders: {270 openai: [271 { id: 'settings-model', envKey: 'SETTINGS_API_KEY' },272 { id: 'cli-model', envKey: 'CLI_API_KEY' },273 ],274 },275 },276 } as unknown as ReturnType<typeof settings.loadSettings>);277 278 // Mock Config object that returns a different model (e.g., from CLI args)279 const mockConfig = {280 getModelsConfig: vi.fn().mockReturnValue({281 getModel: vi.fn().mockReturnValue('cli-model'),282 getGenerationConfig: vi.fn().mockReturnValue({}),283 }),284 } as unknown as import('@qwen-code/qwen-code-core').Config;285 286 // Set the env key for the CLI model, not the settings model287 process.env['CLI_API_KEY'] = 'cli-key';288 289 // Should use 'cli-model' from config.getModelsConfig().getModel(), not 'settings-model'290 const result = validateAuthMethod(AuthType.USE_OPENAI, mockConfig);291 expect(result).toBeNull();292 expect(mockConfig.getModelsConfig).toHaveBeenCalled();293 });294 295 it('should fail validation when Config provides different model without matching env key', () => {296 // Clean up any existing env keys first297 delete process.env['CLI_API_KEY'];298 delete process.env['SETTINGS_API_KEY'];299 delete process.env['OPENAI_API_KEY'];300 301 vi.mocked(settings.loadSettings).mockReturnValue({302 merged: {303 model: { name: 'settings-model' },304 modelProviders: {305 openai: [306 { id: 'settings-model', envKey: 'SETTINGS_API_KEY' },307 { id: 'cli-model', envKey: 'CLI_API_KEY' },308 ],309 },310 },311 } as unknown as ReturnType<typeof settings.loadSettings>);312 313 const mockConfig = {314 getModelsConfig: vi.fn().mockReturnValue({315 getModel: vi.fn().mockReturnValue('cli-model'),316 getGenerationConfig: vi.fn().mockReturnValue({}),317 }),318 } as unknown as import('@qwen-code/qwen-code-core').Config;319 320 // Don't set CLI_API_KEY - validation should fail321 const result = validateAuthMethod(AuthType.USE_OPENAI, mockConfig);322 expect(result).not.toBeNull();323 expect(result).toContain('CLI_API_KEY');324 });325 326 // Regression test for #3171: validation must accept the API key resolved327 // into generationConfig.apiKey (e.g. from --openai-api-key) instead of328 // requiring an OPENAI_API_KEY env var.329 it('should accept API key resolved into generationConfig from CLI flag', () => {330 delete process.env['OPENAI_API_KEY'];331 vi.mocked(settings.loadSettings).mockReturnValue({332 merged: {},333 } as unknown as ReturnType<typeof settings.loadSettings>);334 335 const mockConfig = {336 getModelsConfig: vi.fn().mockReturnValue({337 getModel: vi.fn().mockReturnValue('gpt-4'),338 getGenerationConfig: vi339 .fn()340 .mockReturnValue({ apiKey: 'cli-provided-key' }),341 }),342 } as unknown as import('@qwen-code/qwen-code-core').Config;343 344 const result = validateAuthMethod(AuthType.USE_OPENAI, mockConfig);345 expect(result).toBeNull();346 });347 348 // Regression test for #3171: when a modelProvider has a custom envKey but349 // the user passes --openai-api-key on the CLI, the resolver picks the CLI350 // value. Validation should match the resolver and accept it instead of351 // demanding the env var.352 it('should accept CLI-resolved key even when modelProvider declares a custom envKey', () => {353 delete process.env['CUSTOM_API_KEY'];354 vi.mocked(settings.loadSettings).mockReturnValue({355 merged: {356 model: { name: 'custom-model' },357 modelProviders: {358 openai: [{ id: 'custom-model', envKey: 'CUSTOM_API_KEY' }],359 },360 },361 } as unknown as ReturnType<typeof settings.loadSettings>);362 363 const mockConfig = {364 getModelsConfig: vi.fn().mockReturnValue({365 getModel: vi.fn().mockReturnValue('custom-model'),366 getGenerationConfig: vi367 .fn()368 .mockReturnValue({ apiKey: 'cli-provided-key' }),369 }),370 } as unknown as import('@qwen-code/qwen-code-core').Config;371 372 const result = validateAuthMethod(AuthType.USE_OPENAI, mockConfig);373 expect(result).toBeNull();374 });375 376 it('should accept runtime-resolved settings key when modelProvider declares a custom envKey', () => {377 delete process.env['CUSTOM_API_KEY'];378 vi.mocked(settings.loadSettings).mockReturnValue({379 merged: {380 security: { auth: { apiKey: 'settings-fallback-key' } },381 model: { name: 'custom-model' },382 modelProviders: {383 openai: [{ id: 'custom-model', envKey: 'CUSTOM_API_KEY' }],384 },385 },386 } as unknown as ReturnType<typeof settings.loadSettings>);387 388 const mockConfig = {389 getModelsConfig: vi.fn().mockReturnValue({390 getModel: vi.fn().mockReturnValue('custom-model'),391 getGenerationConfig: vi392 .fn()393 .mockReturnValue({ apiKey: 'settings-fallback-key' }),394 }),395 } as unknown as import('@qwen-code/qwen-code-core').Config;396 397 const result = validateAuthMethod(AuthType.USE_OPENAI, mockConfig);398 expect(result).toBeNull();399 });400 401 it('should keep no-config validation strict for missing custom envKey', () => {402 delete process.env['CUSTOM_API_KEY'];403 vi.mocked(settings.loadSettings).mockReturnValue({404 merged: {405 security: { auth: { apiKey: 'settings-fallback-key' } },406 model: { name: 'custom-model' },407 modelProviders: {408 openai: [{ id: 'custom-model', envKey: 'CUSTOM_API_KEY' }],409 },410 },411 } as unknown as ReturnType<typeof settings.loadSettings>);412 413 const result = validateAuthMethod(AuthType.USE_OPENAI);414 expect(result).toContain('CUSTOM_API_KEY');415 });416});417 