* feat: add Gemini ADC and access token auth * feat: add Gemini token and ADC provider setup * feat: add Gemini token and ADC provider setup * fix: honor Gemini auth mode on restart
187 lines
5.5 KiB
TypeScript
187 lines
5.5 KiB
TypeScript
import { afterEach, describe, expect, test } from 'bun:test'
|
|
|
|
import {
|
|
getGeminiProjectIdHint,
|
|
mayHaveGeminiAdcCredentials,
|
|
resolveGeminiCredential,
|
|
} from './geminiAuth.ts'
|
|
|
|
const existingFilePath = import.meta.path
|
|
|
|
const originalEnv = {
|
|
GEMINI_API_KEY: process.env.GEMINI_API_KEY,
|
|
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
|
|
GEMINI_ACCESS_TOKEN: process.env.GEMINI_ACCESS_TOKEN,
|
|
GEMINI_AUTH_MODE: process.env.GEMINI_AUTH_MODE,
|
|
GOOGLE_APPLICATION_CREDENTIALS: process.env.GOOGLE_APPLICATION_CREDENTIALS,
|
|
GOOGLE_CLOUD_PROJECT: process.env.GOOGLE_CLOUD_PROJECT,
|
|
GCLOUD_PROJECT: process.env.GCLOUD_PROJECT,
|
|
GOOGLE_PROJECT_ID: process.env.GOOGLE_PROJECT_ID,
|
|
APPDATA: process.env.APPDATA,
|
|
}
|
|
|
|
function restoreEnv(key: string, value: string | undefined): void {
|
|
if (value === undefined) {
|
|
delete process.env[key]
|
|
} else {
|
|
process.env[key] = value
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY)
|
|
restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY)
|
|
restoreEnv('GEMINI_ACCESS_TOKEN', originalEnv.GEMINI_ACCESS_TOKEN)
|
|
restoreEnv('GEMINI_AUTH_MODE', originalEnv.GEMINI_AUTH_MODE)
|
|
restoreEnv(
|
|
'GOOGLE_APPLICATION_CREDENTIALS',
|
|
originalEnv.GOOGLE_APPLICATION_CREDENTIALS,
|
|
)
|
|
restoreEnv('GOOGLE_CLOUD_PROJECT', originalEnv.GOOGLE_CLOUD_PROJECT)
|
|
restoreEnv('GCLOUD_PROJECT', originalEnv.GCLOUD_PROJECT)
|
|
restoreEnv('GOOGLE_PROJECT_ID', originalEnv.GOOGLE_PROJECT_ID)
|
|
restoreEnv('APPDATA', originalEnv.APPDATA)
|
|
})
|
|
|
|
describe('resolveGeminiCredential', () => {
|
|
test('prefers GEMINI_API_KEY over other Gemini auth inputs', async () => {
|
|
process.env.GEMINI_API_KEY = 'gem-key'
|
|
process.env.GOOGLE_API_KEY = 'google-key'
|
|
process.env.GEMINI_ACCESS_TOKEN = 'token-123'
|
|
|
|
await expect(resolveGeminiCredential(process.env)).resolves.toEqual({
|
|
kind: 'api-key',
|
|
credential: 'gem-key',
|
|
})
|
|
})
|
|
|
|
test('uses GEMINI_ACCESS_TOKEN when no API key is configured', async () => {
|
|
delete process.env.GEMINI_API_KEY
|
|
delete process.env.GOOGLE_API_KEY
|
|
process.env.GEMINI_AUTH_MODE = 'access-token'
|
|
process.env.GEMINI_ACCESS_TOKEN = 'token-123'
|
|
process.env.GOOGLE_CLOUD_PROJECT = 'test-project'
|
|
|
|
await expect(resolveGeminiCredential(process.env)).resolves.toEqual({
|
|
kind: 'access-token',
|
|
credential: 'token-123',
|
|
projectId: 'test-project',
|
|
})
|
|
})
|
|
|
|
test('falls back to ADC when available', async () => {
|
|
delete process.env.GEMINI_API_KEY
|
|
delete process.env.GOOGLE_API_KEY
|
|
delete process.env.GEMINI_ACCESS_TOKEN
|
|
process.env.GEMINI_AUTH_MODE = 'adc'
|
|
process.env.GOOGLE_APPLICATION_CREDENTIALS = existingFilePath
|
|
|
|
const fakeAuth = {
|
|
async getClient() {
|
|
return {
|
|
async getAccessToken() {
|
|
return { token: 'adc-token' }
|
|
},
|
|
}
|
|
},
|
|
async getProjectId() {
|
|
return 'adc-project'
|
|
},
|
|
}
|
|
|
|
await expect(
|
|
resolveGeminiCredential(process.env, {
|
|
createGoogleAuth: async () => fakeAuth,
|
|
}),
|
|
).resolves.toEqual({
|
|
kind: 'adc',
|
|
credential: 'adc-token',
|
|
projectId: 'adc-project',
|
|
})
|
|
})
|
|
|
|
test('returns none when no Gemini auth source is configured', async () => {
|
|
delete process.env.GEMINI_API_KEY
|
|
delete process.env.GOOGLE_API_KEY
|
|
delete process.env.GEMINI_ACCESS_TOKEN
|
|
delete process.env.GOOGLE_APPLICATION_CREDENTIALS
|
|
|
|
await expect(resolveGeminiCredential(process.env)).resolves.toEqual({
|
|
kind: 'none',
|
|
})
|
|
})
|
|
|
|
test('access-token mode does not silently fall back to ADC', async () => {
|
|
delete process.env.GEMINI_API_KEY
|
|
delete process.env.GOOGLE_API_KEY
|
|
delete process.env.GEMINI_ACCESS_TOKEN
|
|
process.env.GEMINI_AUTH_MODE = 'access-token'
|
|
process.env.GOOGLE_APPLICATION_CREDENTIALS = existingFilePath
|
|
|
|
const fakeAuth = {
|
|
async getClient() {
|
|
return {
|
|
async getAccessToken() {
|
|
return { token: 'adc-token' }
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
await expect(
|
|
resolveGeminiCredential(process.env, {
|
|
createGoogleAuth: async () => fakeAuth,
|
|
}),
|
|
).resolves.toEqual({
|
|
kind: 'none',
|
|
})
|
|
})
|
|
|
|
test('adc mode ignores GEMINI_ACCESS_TOKEN and uses ADC credentials', async () => {
|
|
delete process.env.GEMINI_API_KEY
|
|
delete process.env.GOOGLE_API_KEY
|
|
process.env.GEMINI_AUTH_MODE = 'adc'
|
|
process.env.GEMINI_ACCESS_TOKEN = 'token-123'
|
|
process.env.GOOGLE_APPLICATION_CREDENTIALS = existingFilePath
|
|
|
|
const fakeAuth = {
|
|
async getClient() {
|
|
return {
|
|
async getAccessToken() {
|
|
return { token: 'adc-token' }
|
|
},
|
|
}
|
|
},
|
|
async getProjectId() {
|
|
return 'adc-project'
|
|
},
|
|
}
|
|
|
|
await expect(
|
|
resolveGeminiCredential(process.env, {
|
|
createGoogleAuth: async () => fakeAuth,
|
|
}),
|
|
).resolves.toEqual({
|
|
kind: 'adc',
|
|
credential: 'adc-token',
|
|
projectId: 'adc-project',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Gemini auth helpers', () => {
|
|
test('detects explicit project id hints', () => {
|
|
process.env.GOOGLE_PROJECT_ID = 'project-a'
|
|
expect(getGeminiProjectIdHint(process.env)).toBe('project-a')
|
|
})
|
|
|
|
test('only treats existing ADC paths as valid hints', () => {
|
|
process.env.GOOGLE_APPLICATION_CREDENTIALS = existingFilePath
|
|
expect(mayHaveGeminiAdcCredentials(process.env)).toBe(true)
|
|
|
|
process.env.GOOGLE_APPLICATION_CREDENTIALS = `${existingFilePath}.missing`
|
|
process.env.APPDATA = undefined
|
|
expect(mayHaveGeminiAdcCredentials(process.env)).toBe(false)
|
|
})
|
|
})
|