2025-06-15 22:41:32 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-09-16 10:05:29 -04:00
import type { Credentials } from 'google-auth-library' ;
2025-08-26 00:04:53 +02:00
import type { Mock } from 'vitest' ;
import { describe , it , expect , vi , beforeEach , afterEach } from 'vitest' ;
2025-08-19 17:06:25 -07:00
import {
getOauthClient ,
resetOauthClientForTesting ,
clearCachedCredentialFile ,
clearOauthClientCache ,
} from './oauth2.js' ;
2025-08-20 10:55:47 +09:00
import { UserAccountManager } from '../utils/userAccountManager.js' ;
2025-10-27 16:05:11 -04:00
import { OAuth2Client , Compute , GoogleAuth } from 'google-auth-library' ;
2025-08-25 22:11:27 +02:00
import * as fs from 'node:fs' ;
import * as path from 'node:path' ;
import http from 'node:http' ;
2025-06-15 22:41:32 -07:00
import open from 'open' ;
2025-08-25 22:11:27 +02:00
import crypto from 'node:crypto' ;
import * as os from 'node:os' ;
2025-07-07 15:02:13 -07:00
import { AuthType } from '../core/contentGenerator.js' ;
2025-08-26 00:04:53 +02:00
import type { Config } from '../config/config.js' ;
2025-07-11 14:05:27 -07:00
import readline from 'node:readline' ;
2025-09-16 10:05:29 -04:00
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js' ;
2025-10-14 02:31:39 +09:00
import { GEMINI_DIR } from '../utils/paths.js' ;
2025-06-16 19:31:32 -07:00
vi . mock ( 'os' , async ( importOriginal ) = > {
const os = await importOriginal < typeof import ( 'os' ) > ( ) ;
return {
. . . os ,
homedir : vi.fn ( ) ,
} ;
} ) ;
2025-06-15 22:41:32 -07:00
vi . mock ( 'google-auth-library' ) ;
vi . mock ( 'http' ) ;
vi . mock ( 'open' ) ;
vi . mock ( 'crypto' ) ;
2025-07-11 14:05:27 -07:00
vi . mock ( 'node:readline' ) ;
2025-07-18 17:22:50 -07:00
vi . mock ( '../utils/browser.js' , ( ) = > ( {
shouldAttemptBrowserLaunch : ( ) = > true ,
} ) ) ;
2025-06-15 22:41:32 -07:00
2025-09-16 10:05:29 -04:00
vi . mock ( './oauth-credential-storage.js' , ( ) = > ( {
OAuthCredentialStorage : {
saveCredentials : vi.fn ( ) ,
loadCredentials : vi.fn ( ) ,
clearCredentials : vi.fn ( ) ,
} ,
} ) ) ;
2025-07-10 18:59:02 -07:00
const mockConfig = {
getNoBrowser : ( ) = > false ,
2025-07-18 02:57:37 +08:00
getProxy : ( ) = > 'http://test.proxy.com:8080' ,
2025-07-21 16:23:28 -07:00
isBrowserLaunchSuppressed : ( ) = > false ,
2025-07-10 18:59:02 -07:00
} as unknown as Config ;
2025-06-29 16:35:20 -04:00
// Mock fetch globally
global . fetch = vi . fn ( ) ;
2025-06-15 22:41:32 -07:00
describe ( 'oauth2' , ( ) = > {
2025-09-16 10:05:29 -04:00
describe ( 'with encrypted flag false' , ( ) = > {
let tempHomeDir : string ;
2025-07-07 15:02:13 -07:00
beforeEach ( ( ) = > {
2025-09-16 10:05:29 -04:00
process . env [ FORCE_ENCRYPTED_FILE_ENV_VAR ] = 'false' ;
tempHomeDir = fs . mkdtempSync (
path . join ( os . tmpdir ( ) , 'gemini-cli-test-home-' ) ,
2025-07-07 15:02:13 -07:00
) ;
2025-10-27 16:46:35 -07:00
vi . mocked ( os . homedir ) . mockReturnValue ( tempHomeDir ) ;
2025-07-07 15:02:13 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
afterEach ( ( ) = > {
fs . rmSync ( tempHomeDir , { recursive : true , force : true } ) ;
vi . clearAllMocks ( ) ;
resetOauthClientForTesting ( ) ;
vi . unstubAllEnvs ( ) ;
2025-08-18 14:11:19 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
it ( 'should perform a web login' , async ( ) = > {
const mockAuthUrl = 'https://example.com/auth' ;
const mockCode = 'test-code' ;
const mockState = 'test-state' ;
const mockTokens = {
access_token : 'test-access-token' ,
refresh_token : 'test-refresh-token' ,
2025-08-18 14:11:19 -07:00
} ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
const mockGenerateAuthUrl = vi . fn ( ) . mockReturnValue ( mockAuthUrl ) ;
const mockGetToken = vi . fn ( ) . mockResolvedValue ( { tokens : mockTokens } ) ;
2025-07-25 10:19:38 -07:00
const mockSetCredentials = vi . fn ( ) ;
const mockGetAccessToken = vi
. fn ( )
2025-09-16 10:05:29 -04:00
. mockResolvedValue ( { token : 'mock-access-token' } ) ;
2025-07-25 10:19:38 -07:00
const mockOAuth2Client = {
2025-09-16 10:05:29 -04:00
generateAuthUrl : mockGenerateAuthUrl ,
getToken : mockGetToken ,
2025-07-25 10:19:38 -07:00
setCredentials : mockSetCredentials ,
getAccessToken : mockGetAccessToken ,
2025-09-16 10:05:29 -04:00
credentials : mockTokens ,
2025-07-25 10:19:38 -07:00
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
vi . spyOn ( crypto , 'randomBytes' ) . mockReturnValue ( mockState as never ) ;
2025-10-27 16:46:35 -07:00
vi . mocked ( open ) . mockImplementation (
async ( ) = > ( { on : vi.fn ( ) } ) as never ,
) ;
2025-09-16 10:05:29 -04:00
// Mock the UserInfo API response
2025-10-27 16:46:35 -07:00
vi . mocked ( global . fetch ) . mockResolvedValue ( {
2025-07-25 10:19:38 -07:00
ok : true ,
json : vi
. fn ( )
2025-09-16 10:05:29 -04:00
. mockResolvedValue ( { email : 'test-google-account@gmail.com' } ) ,
2025-07-25 10:19:38 -07:00
} as unknown as Response ) ;
2025-09-16 10:05:29 -04:00
let requestCallback ! : http . RequestListener <
typeof http . IncomingMessage ,
typeof http . ServerResponse
> ;
let serverListeningCallback : ( value : unknown ) = > void ;
const serverListeningPromise = new Promise (
( resolve ) = > ( serverListeningCallback = resolve ) ,
) ;
let capturedPort = 0 ;
const mockHttpServer = {
listen : vi.fn ( ( port : number , _host : string , callback ? : ( ) = > void ) = > {
capturedPort = port ;
if ( callback ) {
callback ( ) ;
}
serverListeningCallback ( undefined ) ;
} ) ,
close : vi.fn ( ( callback ? : ( ) = > void ) = > {
if ( callback ) {
callback ( ) ;
}
} ) ,
on : vi.fn ( ) ,
address : ( ) = > ( { port : capturedPort } ) ,
} ;
( http . createServer as Mock ) . mockImplementation ( ( cb ) = > {
requestCallback = cb as http . RequestListener <
typeof http . IncomingMessage ,
typeof http . ServerResponse
> ;
return mockHttpServer as unknown as http . Server ;
} ) ;
const clientPromise = getOauthClient (
2025-07-25 10:19:38 -07:00
AuthType . LOGIN_WITH_GOOGLE ,
mockConfig ,
) ;
2025-09-16 10:05:29 -04:00
// wait for server to start listening.
await serverListeningPromise ;
const mockReq = {
url : ` /oauth2callback?code= ${ mockCode } &state= ${ mockState } ` ,
} as http . IncomingMessage ;
const mockRes = {
writeHead : vi.fn ( ) ,
end : vi.fn ( ) ,
} as unknown as http . ServerResponse ;
await requestCallback ( mockReq , mockRes ) ;
const client = await clientPromise ;
2025-07-25 10:19:38 -07:00
expect ( client ) . toBe ( mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
expect ( open ) . toHaveBeenCalledWith ( mockAuthUrl ) ;
expect ( mockGetToken ) . toHaveBeenCalledWith ( {
code : mockCode ,
redirect_uri : ` http://localhost: ${ capturedPort } /oauth2callback ` ,
} ) ;
expect ( mockSetCredentials ) . toHaveBeenCalledWith ( mockTokens ) ;
2025-07-25 10:19:38 -07:00
// Verify Google Account was cached
const googleAccountPath = path . join (
tempHomeDir ,
2025-10-14 02:31:39 +09:00
GEMINI_DIR ,
2025-07-25 10:19:38 -07:00
'google_accounts.json' ,
) ;
2025-09-16 10:05:29 -04:00
expect ( fs . existsSync ( googleAccountPath ) ) . toBe ( true ) ;
const cachedGoogleAccount = fs . readFileSync ( googleAccountPath , 'utf-8' ) ;
expect ( JSON . parse ( cachedGoogleAccount ) ) . toEqual ( {
active : 'test-google-account@gmail.com' ,
2025-07-25 14:29:54 -07:00
old : [ ] ,
} ) ;
2025-09-16 10:05:29 -04:00
// Verify the getCachedGoogleAccount function works
const userAccountManager = new UserAccountManager ( ) ;
expect ( userAccountManager . getCachedGoogleAccount ( ) ) . toBe (
'test-google-account@gmail.com' ,
) ;
2025-07-25 10:19:38 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
it ( 'should perform login with user code' , async ( ) = > {
const mockConfigWithNoBrowser = {
getNoBrowser : ( ) = > true ,
getProxy : ( ) = > 'http://test.proxy.com:8080' ,
isBrowserLaunchSuppressed : ( ) = > true ,
} as unknown as Config ;
const mockCodeVerifier = {
codeChallenge : 'test-challenge' ,
codeVerifier : 'test-verifier' ,
} ;
const mockAuthUrl = 'https://example.com/auth-user-code' ;
const mockCode = 'test-user-code' ;
const mockTokens = {
access_token : 'test-access-token-user-code' ,
refresh_token : 'test-refresh-token-user-code' ,
} ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
const mockGenerateAuthUrl = vi . fn ( ) . mockReturnValue ( mockAuthUrl ) ;
const mockGetToken = vi . fn ( ) . mockResolvedValue ( { tokens : mockTokens } ) ;
2025-07-25 10:19:38 -07:00
const mockSetCredentials = vi . fn ( ) ;
2025-09-16 10:05:29 -04:00
const mockGenerateCodeVerifierAsync = vi
2025-07-25 10:19:38 -07:00
. fn ( )
2025-09-16 10:05:29 -04:00
. mockResolvedValue ( mockCodeVerifier ) ;
2025-07-25 10:19:38 -07:00
const mockOAuth2Client = {
2025-09-16 10:05:29 -04:00
generateAuthUrl : mockGenerateAuthUrl ,
getToken : mockGetToken ,
2025-07-25 10:19:38 -07:00
setCredentials : mockSetCredentials ,
2025-09-16 10:05:29 -04:00
generateCodeVerifierAsync : mockGenerateCodeVerifierAsync ,
2025-07-25 10:19:38 -07:00
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
const mockReadline = {
question : vi.fn ( ( _query , callback ) = > callback ( mockCode ) ) ,
close : vi.fn ( ) ,
} ;
( readline . createInterface as Mock ) . mockReturnValue ( mockReadline ) ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
const consoleLogSpy = vi
. spyOn ( console , 'log' )
. mockImplementation ( ( ) = > { } ) ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
const client = await getOauthClient (
AuthType . LOGIN_WITH_GOOGLE ,
mockConfigWithNoBrowser ,
) ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
expect ( client ) . toBe ( mockOAuth2Client ) ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
// Verify the auth flow
expect ( mockGenerateCodeVerifierAsync ) . toHaveBeenCalled ( ) ;
expect ( mockGenerateAuthUrl ) . toHaveBeenCalled ( ) ;
expect ( consoleLogSpy ) . toHaveBeenCalledWith (
expect . stringContaining ( mockAuthUrl ) ,
) ;
expect ( mockReadline . question ) . toHaveBeenCalledWith (
'Enter the authorization code: ' ,
expect . any ( Function ) ,
2025-07-25 10:19:38 -07:00
) ;
2025-09-16 10:05:29 -04:00
expect ( mockGetToken ) . toHaveBeenCalledWith ( {
code : mockCode ,
codeVerifier : mockCodeVerifier.codeVerifier ,
redirect_uri : 'https://codeassist.google.com/authcode' ,
} ) ;
expect ( mockSetCredentials ) . toHaveBeenCalledWith ( mockTokens ) ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
consoleLogSpy . mockRestore ( ) ;
} ) ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
describe ( 'in Cloud Shell' , ( ) = > {
const mockGetAccessToken = vi . fn ( ) ;
let mockComputeClient : Compute ;
2025-07-25 10:19:38 -07:00
2025-09-16 10:05:29 -04:00
beforeEach ( ( ) = > {
mockGetAccessToken . mockResolvedValue ( { token : 'test-access-token' } ) ;
mockComputeClient = {
credentials : { refresh_token : 'test-refresh-token' } ,
getAccessToken : mockGetAccessToken ,
} as unknown as Compute ;
2025-08-19 17:06:25 -07:00
2025-09-16 10:05:29 -04:00
( Compute as unknown as Mock ) . mockImplementation (
( ) = > mockComputeClient ,
) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should attempt to load cached credentials first' , async ( ) = > {
const cachedCreds = { refresh_token : 'cached-token' } ;
2025-10-14 02:31:39 +09:00
const credsPath = path . join (
tempHomeDir ,
GEMINI_DIR ,
'oauth_creds.json' ,
) ;
2025-09-16 10:05:29 -04:00
await fs . promises . mkdir ( path . dirname ( credsPath ) , { recursive : true } ) ;
await fs . promises . writeFile ( credsPath , JSON . stringify ( cachedCreds ) ) ;
const mockClient = {
setCredentials : vi.fn ( ) ,
getAccessToken : vi.fn ( ) . mockResolvedValue ( { token : 'test-token' } ) ,
getTokenInfo : vi.fn ( ) . mockResolvedValue ( { } ) ,
on : vi.fn ( ) ,
} ;
// To mock the new OAuth2Client() inside the function
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation (
2025-09-16 10:05:29 -04:00
( ) = > mockClient as unknown as OAuth2Client ,
) ;
await getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ;
expect ( mockClient . setCredentials ) . toHaveBeenCalledWith ( cachedCreds ) ;
expect ( mockClient . getAccessToken ) . toHaveBeenCalled ( ) ;
expect ( mockClient . getTokenInfo ) . toHaveBeenCalled ( ) ;
expect ( Compute ) . not . toHaveBeenCalled ( ) ; // Should not fetch new client if cache is valid
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should use Compute to get a client if no cached credentials exist' , async ( ) = > {
2025-11-14 11:39:11 -05:00
await getOauthClient ( AuthType . COMPUTE_ADC , mockConfig ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
expect ( Compute ) . toHaveBeenCalledWith ( { } ) ;
expect ( mockGetAccessToken ) . toHaveBeenCalled ( ) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should not cache the credentials after fetching them via ADC' , async ( ) = > {
const newCredentials = { refresh_token : 'new-adc-token' } ;
mockComputeClient . credentials = newCredentials ;
mockGetAccessToken . mockResolvedValue ( { token : 'new-adc-token' } ) ;
2025-09-03 13:51:29 -07:00
2025-11-14 11:39:11 -05:00
await getOauthClient ( AuthType . COMPUTE_ADC , mockConfig ) ;
2025-09-03 13:51:29 -07:00
2025-10-14 02:31:39 +09:00
const credsPath = path . join (
tempHomeDir ,
GEMINI_DIR ,
'oauth_creds.json' ,
) ;
2025-09-16 10:05:29 -04:00
expect ( fs . existsSync ( credsPath ) ) . toBe ( false ) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should return the Compute client on successful ADC authentication' , async ( ) = > {
2025-11-14 11:39:11 -05:00
const client = await getOauthClient ( AuthType . COMPUTE_ADC , mockConfig ) ;
2025-09-16 10:05:29 -04:00
expect ( client ) . toBe ( mockComputeClient ) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should throw an error if ADC fails' , async ( ) = > {
const testError = new Error ( 'ADC Failed' ) ;
mockGetAccessToken . mockRejectedValue ( testError ) ;
await expect (
2025-11-14 11:39:11 -05:00
getOauthClient ( AuthType . COMPUTE_ADC , mockConfig ) ,
2025-09-16 10:05:29 -04:00
) . rejects . toThrow (
2025-11-14 11:39:11 -05:00
'Could not authenticate using metadata server application default credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ADC Failed' ,
2025-09-16 10:05:29 -04:00
) ;
} ) ;
2025-09-03 13:51:29 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
describe ( 'credential loading order' , ( ) = > {
it ( 'should prioritize default cached credentials over GOOGLE_APPLICATION_CREDENTIALS' , async ( ) = > {
// Setup default cached credentials
const defaultCreds = { refresh_token : 'default-cached-token' } ;
const defaultCredsPath = path . join (
tempHomeDir ,
2025-10-14 02:31:39 +09:00
GEMINI_DIR ,
2025-09-16 10:05:29 -04:00
'oauth_creds.json' ,
) ;
await fs . promises . mkdir ( path . dirname ( defaultCredsPath ) , {
recursive : true ,
} ) ;
await fs . promises . writeFile (
defaultCredsPath ,
JSON . stringify ( defaultCreds ) ,
) ;
// Setup credentials via environment variable
const envCreds = { refresh_token : 'env-var-token' } ;
const envCredsPath = path . join ( tempHomeDir , 'env_creds.json' ) ;
await fs . promises . writeFile ( envCredsPath , JSON . stringify ( envCreds ) ) ;
vi . stubEnv ( 'GOOGLE_APPLICATION_CREDENTIALS' , envCredsPath ) ;
const mockClient = {
setCredentials : vi.fn ( ) ,
getAccessToken : vi.fn ( ) . mockResolvedValue ( { token : 'test-token' } ) ,
getTokenInfo : vi.fn ( ) . mockResolvedValue ( { } ) ,
on : vi.fn ( ) ,
} ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation (
2025-09-16 10:05:29 -04:00
( ) = > mockClient as unknown as OAuth2Client ,
) ;
await getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ;
// Assert the correct credentials were used
expect ( mockClient . setCredentials ) . toHaveBeenCalledWith ( defaultCreds ) ;
expect ( mockClient . setCredentials ) . not . toHaveBeenCalledWith ( envCreds ) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should fall back to GOOGLE_APPLICATION_CREDENTIALS if default cache is missing' , async ( ) = > {
// Setup credentials via environment variable
const envCreds = { refresh_token : 'env-var-token' } ;
const envCredsPath = path . join ( tempHomeDir , 'env_creds.json' ) ;
await fs . promises . writeFile ( envCredsPath , JSON . stringify ( envCreds ) ) ;
vi . stubEnv ( 'GOOGLE_APPLICATION_CREDENTIALS' , envCredsPath ) ;
const mockClient = {
setCredentials : vi.fn ( ) ,
getAccessToken : vi.fn ( ) . mockResolvedValue ( { token : 'test-token' } ) ,
getTokenInfo : vi.fn ( ) . mockResolvedValue ( { } ) ,
on : vi.fn ( ) ,
} ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation (
2025-09-16 10:05:29 -04:00
( ) = > mockClient as unknown as OAuth2Client ,
) ;
await getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ;
// Assert the correct credentials were used
expect ( mockClient . setCredentials ) . toHaveBeenCalledWith ( envCreds ) ;
} ) ;
2025-10-27 16:05:11 -04:00
it ( 'should use GoogleAuth for BYOID credentials from GOOGLE_APPLICATION_CREDENTIALS' , async ( ) = > {
// Setup BYOID credentials via environment variable
const byoidCredentials = {
type : 'external_account_authorized_user' ,
client_id : 'mock-client-id' ,
} ;
const envCredsPath = path . join ( tempHomeDir , 'byoid_creds.json' ) ;
await fs . promises . writeFile (
envCredsPath ,
JSON . stringify ( byoidCredentials ) ,
) ;
vi . stubEnv ( 'GOOGLE_APPLICATION_CREDENTIALS' , envCredsPath ) ;
// Mock GoogleAuth and its chain of calls
const mockExternalAccountClient = {
getAccessToken : vi.fn ( ) . mockResolvedValue ( { token : 'byoid-token' } ) ,
} ;
const mockFromJSON = vi
. fn ( )
. mockResolvedValue ( mockExternalAccountClient ) ;
const mockGoogleAuthInstance = {
fromJSON : mockFromJSON ,
} ;
( GoogleAuth as unknown as Mock ) . mockImplementation (
( ) = > mockGoogleAuthInstance ,
) ;
const mockOAuth2Client = {
on : vi.fn ( ) ,
} ;
( OAuth2Client as unknown as Mock ) . mockImplementation (
( ) = > mockOAuth2Client ,
) ;
const client = await getOauthClient (
AuthType . LOGIN_WITH_GOOGLE ,
mockConfig ,
) ;
// Assert that GoogleAuth was used and the correct client was returned
expect ( GoogleAuth ) . toHaveBeenCalledWith ( {
scopes : expect.any ( Array ) ,
} ) ;
expect ( mockFromJSON ) . toHaveBeenCalledWith ( byoidCredentials ) ;
expect ( client ) . toBe ( mockExternalAccountClient ) ;
} ) ;
2025-09-16 10:05:29 -04:00
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
describe ( 'with GCP environment variables' , ( ) = > {
it ( 'should use GOOGLE_CLOUD_ACCESS_TOKEN when GOOGLE_GENAI_USE_GCA is true' , async ( ) = > {
vi . stubEnv ( 'GOOGLE_GENAI_USE_GCA' , 'true' ) ;
vi . stubEnv ( 'GOOGLE_CLOUD_ACCESS_TOKEN' , 'gcp-access-token' ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
const mockSetCredentials = vi . fn ( ) ;
const mockGetAccessToken = vi
. fn ( )
. mockResolvedValue ( { token : 'gcp-access-token' } ) ;
const mockOAuth2Client = {
setCredentials : mockSetCredentials ,
getAccessToken : mockGetAccessToken ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
// Mock the UserInfo API response for fetchAndCacheUserInfo
( global . fetch as Mock ) . mockResolvedValue ( {
ok : true ,
json : vi
. fn ( )
. mockResolvedValue ( { email : 'test-gcp-account@gmail.com' } ) ,
} as unknown as Response ) ;
const client = await getOauthClient (
AuthType . LOGIN_WITH_GOOGLE ,
mockConfig ,
) ;
expect ( client ) . toBe ( mockOAuth2Client ) ;
expect ( mockSetCredentials ) . toHaveBeenCalledWith ( {
access_token : 'gcp-access-token' ,
} ) ;
// Verify fetchAndCacheUserInfo was effectively called
expect ( mockGetAccessToken ) . toHaveBeenCalled ( ) ;
expect ( global . fetch ) . toHaveBeenCalledWith (
'https://www.googleapis.com/oauth2/v2/userinfo' ,
{
headers : {
Authorization : 'Bearer gcp-access-token' ,
} ,
} ,
) ;
// Verify Google Account was cached
const googleAccountPath = path . join (
tempHomeDir ,
2025-10-14 02:31:39 +09:00
GEMINI_DIR ,
2025-09-16 10:05:29 -04:00
'google_accounts.json' ,
) ;
const cachedContent = fs . readFileSync ( googleAccountPath , 'utf-8' ) ;
expect ( JSON . parse ( cachedContent ) ) . toEqual ( {
active : 'test-gcp-account@gmail.com' ,
old : [ ] ,
} ) ;
2025-09-03 13:51:29 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
it ( 'should not use GCP token if GOOGLE_CLOUD_ACCESS_TOKEN is not set' , async ( ) = > {
vi . stubEnv ( 'GOOGLE_GENAI_USE_GCA' , 'true' ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
const mockSetCredentials = vi . fn ( ) ;
const mockGetAccessToken = vi
. fn ( )
. mockResolvedValue ( { token : 'cached-access-token' } ) ;
const mockGetTokenInfo = vi . fn ( ) . mockResolvedValue ( { } ) ;
const mockOAuth2Client = {
setCredentials : mockSetCredentials ,
getAccessToken : mockGetAccessToken ,
getTokenInfo : mockGetTokenInfo ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
// Make it fall through to cached credentials path
const cachedCreds = { refresh_token : 'cached-token' } ;
2025-10-14 02:31:39 +09:00
const credsPath = path . join (
tempHomeDir ,
GEMINI_DIR ,
'oauth_creds.json' ,
) ;
2025-09-16 10:05:29 -04:00
await fs . promises . mkdir ( path . dirname ( credsPath ) , { recursive : true } ) ;
await fs . promises . writeFile ( credsPath , JSON . stringify ( cachedCreds ) ) ;
await getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ;
// It should be called with the cached credentials, not the GCP access token.
expect ( mockSetCredentials ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockSetCredentials ) . toHaveBeenCalledWith ( cachedCreds ) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should not use GCP token if GOOGLE_GENAI_USE_GCA is not set' , async ( ) = > {
vi . stubEnv ( 'GOOGLE_CLOUD_ACCESS_TOKEN' , 'gcp-access-token' ) ;
const mockSetCredentials = vi . fn ( ) ;
const mockGetAccessToken = vi
. fn ( )
. mockResolvedValue ( { token : 'cached-access-token' } ) ;
const mockGetTokenInfo = vi . fn ( ) . mockResolvedValue ( { } ) ;
const mockOAuth2Client = {
setCredentials : mockSetCredentials ,
getAccessToken : mockGetAccessToken ,
getTokenInfo : mockGetTokenInfo ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
// Make it fall through to cached credentials path
const cachedCreds = { refresh_token : 'cached-token' } ;
2025-10-14 02:31:39 +09:00
const credsPath = path . join (
tempHomeDir ,
GEMINI_DIR ,
'oauth_creds.json' ,
) ;
2025-09-16 10:05:29 -04:00
await fs . promises . mkdir ( path . dirname ( credsPath ) , { recursive : true } ) ;
await fs . promises . writeFile ( credsPath , JSON . stringify ( cachedCreds ) ) ;
await getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ;
// It should be called with the cached credentials, not the GCP access token.
expect ( mockSetCredentials ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockSetCredentials ) . toHaveBeenCalledWith ( cachedCreds ) ;
} ) ;
2025-09-03 13:51:29 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
describe ( 'error handling' , ( ) = > {
it ( 'should handle browser launch failure with FatalAuthenticationError' , async ( ) = > {
const mockError = new Error ( 'Browser launch failed' ) ;
( open as Mock ) . mockRejectedValue ( mockError ) ;
const mockOAuth2Client = {
generateAuthUrl : vi.fn ( ) . mockReturnValue ( 'https://example.com/auth' ) ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
await expect (
getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ,
) . rejects . toThrow ( 'Failed to open browser: Browser launch failed' ) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should handle authentication timeout with proper error message' , async ( ) = > {
const mockAuthUrl = 'https://example.com/auth' ;
const mockOAuth2Client = {
generateAuthUrl : vi.fn ( ) . mockReturnValue ( mockAuthUrl ) ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
2025-10-27 16:46:35 -07:00
vi . mocked ( open ) . mockImplementation (
2025-09-16 10:05:29 -04:00
async ( ) = > ( { on : vi.fn ( ) } ) as never ,
) ;
const mockHttpServer = {
listen : vi.fn ( ) ,
close : vi.fn ( ) ,
on : vi.fn ( ) ,
address : ( ) = > ( { port : 3000 } ) ,
} ;
( http . createServer as Mock ) . mockImplementation (
( ) = > mockHttpServer as unknown as http . Server ,
) ;
// Mock setTimeout to trigger timeout immediately
const originalSetTimeout = global . setTimeout ;
global . setTimeout = vi . fn (
( callback ) = > ( callback ( ) , { } as unknown as NodeJS . Timeout ) ,
) as unknown as typeof setTimeout ;
await expect (
getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ,
) . rejects . toThrow (
'Authentication timed out after 5 minutes. The browser tab may have gotten stuck in a loading state. Please try again or use NO_BROWSER=true for manual authentication.' ,
) ;
global . setTimeout = originalSetTimeout ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should handle OAuth callback errors with descriptive messages' , async ( ) = > {
const mockAuthUrl = 'https://example.com/auth' ;
const mockOAuth2Client = {
generateAuthUrl : vi.fn ( ) . mockReturnValue ( mockAuthUrl ) ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
2025-10-27 16:46:35 -07:00
vi . mocked ( open ) . mockImplementation (
2025-09-16 10:05:29 -04:00
async ( ) = > ( { on : vi.fn ( ) } ) as never ,
) ;
let requestCallback ! : http . RequestListener ;
let serverListeningCallback : ( value : unknown ) = > void ;
const serverListeningPromise = new Promise (
( resolve ) = > ( serverListeningCallback = resolve ) ,
) ;
const mockHttpServer = {
listen : vi.fn (
( _port : number , _host : string , callback ? : ( ) = > void ) = > {
if ( callback ) callback ( ) ;
serverListeningCallback ( undefined ) ;
} ,
) ,
close : vi.fn ( ) ,
on : vi.fn ( ) ,
address : ( ) = > ( { port : 3000 } ) ,
} ;
( http . createServer as Mock ) . mockImplementation ( ( cb ) = > {
requestCallback = cb ;
return mockHttpServer as unknown as http . Server ;
} ) ;
const clientPromise = getOauthClient (
AuthType . LOGIN_WITH_GOOGLE ,
mockConfig ,
) ;
await serverListeningPromise ;
// Test OAuth error with description
const mockReq = {
url : '/oauth2callback?error=access_denied&error_description=User+denied+access' ,
} as http . IncomingMessage ;
const mockRes = {
writeHead : vi.fn ( ) ,
end : vi.fn ( ) ,
} as unknown as http . ServerResponse ;
await expect ( async ( ) = > {
await requestCallback ( mockReq , mockRes ) ;
await clientPromise ;
} ) . rejects . toThrow (
'Google OAuth error: access_denied. User denied access' ,
) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should handle OAuth error without description' , async ( ) = > {
const mockAuthUrl = 'https://example.com/auth' ;
const mockOAuth2Client = {
generateAuthUrl : vi.fn ( ) . mockReturnValue ( mockAuthUrl ) ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
2025-10-27 16:46:35 -07:00
vi . mocked ( open ) . mockImplementation (
2025-09-16 10:05:29 -04:00
async ( ) = > ( { on : vi.fn ( ) } ) as never ,
) ;
let requestCallback ! : http . RequestListener ;
let serverListeningCallback : ( value : unknown ) = > void ;
const serverListeningPromise = new Promise (
( resolve ) = > ( serverListeningCallback = resolve ) ,
) ;
const mockHttpServer = {
listen : vi.fn (
( _port : number , _host : string , callback ? : ( ) = > void ) = > {
if ( callback ) callback ( ) ;
serverListeningCallback ( undefined ) ;
} ,
) ,
close : vi.fn ( ) ,
on : vi.fn ( ) ,
address : ( ) = > ( { port : 3000 } ) ,
} ;
( http . createServer as Mock ) . mockImplementation ( ( cb ) = > {
requestCallback = cb ;
return mockHttpServer as unknown as http . Server ;
} ) ;
const clientPromise = getOauthClient (
AuthType . LOGIN_WITH_GOOGLE ,
mockConfig ,
) ;
await serverListeningPromise ;
// Test OAuth error without description
const mockReq = {
url : '/oauth2callback?error=server_error' ,
} as http . IncomingMessage ;
const mockRes = {
writeHead : vi.fn ( ) ,
end : vi.fn ( ) ,
} as unknown as http . ServerResponse ;
await expect ( async ( ) = > {
await requestCallback ( mockReq , mockRes ) ;
await clientPromise ;
} ) . rejects . toThrow (
'Google OAuth error: server_error. No additional details provided' ,
) ;
2025-09-03 13:51:29 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
it ( 'should handle token exchange failure with descriptive error' , async ( ) = > {
const mockAuthUrl = 'https://example.com/auth' ;
const mockCode = 'test-code' ;
const mockState = 'test-state' ;
const mockOAuth2Client = {
generateAuthUrl : vi.fn ( ) . mockReturnValue ( mockAuthUrl ) ,
getToken : vi
. fn ( )
. mockRejectedValue ( new Error ( 'Token exchange failed' ) ) ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
vi . spyOn ( crypto , 'randomBytes' ) . mockReturnValue ( mockState as never ) ;
2025-10-27 16:46:35 -07:00
vi . mocked ( open ) . mockImplementation (
2025-09-16 10:05:29 -04:00
async ( ) = > ( { on : vi.fn ( ) } ) as never ,
) ;
let requestCallback ! : http . RequestListener ;
let serverListeningCallback : ( value : unknown ) = > void ;
const serverListeningPromise = new Promise (
( resolve ) = > ( serverListeningCallback = resolve ) ,
) ;
const mockHttpServer = {
listen : vi.fn (
( _port : number , _host : string , callback ? : ( ) = > void ) = > {
if ( callback ) callback ( ) ;
serverListeningCallback ( undefined ) ;
} ,
) ,
close : vi.fn ( ) ,
on : vi.fn ( ) ,
address : ( ) = > ( { port : 3000 } ) ,
} ;
( http . createServer as Mock ) . mockImplementation ( ( cb ) = > {
requestCallback = cb ;
return mockHttpServer as unknown as http . Server ;
} ) ;
const clientPromise = getOauthClient (
AuthType . LOGIN_WITH_GOOGLE ,
mockConfig ,
) ;
await serverListeningPromise ;
const mockReq = {
url : ` /oauth2callback?code= ${ mockCode } &state= ${ mockState } ` ,
} as http . IncomingMessage ;
const mockRes = {
writeHead : vi.fn ( ) ,
end : vi.fn ( ) ,
} as unknown as http . ServerResponse ;
await expect ( async ( ) = > {
await requestCallback ( mockReq , mockRes ) ;
await clientPromise ;
} ) . rejects . toThrow (
'Failed to exchange authorization code for tokens: Token exchange failed' ,
) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should handle fetchAndCacheUserInfo failure gracefully' , async ( ) = > {
const mockAuthUrl = 'https://example.com/auth' ;
const mockCode = 'test-code' ;
const mockState = 'test-state' ;
const mockTokens = {
access_token : 'test-access-token' ,
refresh_token : 'test-refresh-token' ,
} ;
const mockOAuth2Client = {
generateAuthUrl : vi.fn ( ) . mockReturnValue ( mockAuthUrl ) ,
getToken : vi.fn ( ) . mockResolvedValue ( { tokens : mockTokens } ) ,
setCredentials : vi.fn ( ) ,
getAccessToken : vi
. fn ( )
. mockResolvedValue ( { token : 'test-access-token' } ) ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
vi . spyOn ( crypto , 'randomBytes' ) . mockReturnValue ( mockState as never ) ;
2025-10-27 16:46:35 -07:00
vi . mocked ( open ) . mockImplementation (
2025-09-16 10:05:29 -04:00
async ( ) = > ( { on : vi.fn ( ) } ) as never ,
) ;
// Mock fetch to fail
2025-10-27 16:46:35 -07:00
vi . mocked ( global . fetch ) . mockResolvedValue ( {
2025-09-16 10:05:29 -04:00
ok : false ,
status : 500 ,
statusText : 'Internal Server Error' ,
} as unknown as Response ) ;
2025-10-27 16:46:35 -07:00
const consoleLogSpy = vi
. spyOn ( console , 'log' )
2025-09-16 10:05:29 -04:00
. mockImplementation ( ( ) = > { } ) ;
let requestCallback ! : http . RequestListener ;
let serverListeningCallback : ( value : unknown ) = > void ;
const serverListeningPromise = new Promise (
( resolve ) = > ( serverListeningCallback = resolve ) ,
) ;
const mockHttpServer = {
listen : vi.fn (
( _port : number , _host : string , callback ? : ( ) = > void ) = > {
if ( callback ) callback ( ) ;
serverListeningCallback ( undefined ) ;
} ,
) ,
close : vi.fn ( ) ,
on : vi.fn ( ) ,
address : ( ) = > ( { port : 3000 } ) ,
2025-10-27 16:46:35 -07:00
} as unknown as http . Server ;
2025-09-16 10:05:29 -04:00
( http . createServer as Mock ) . mockImplementation ( ( cb ) = > {
requestCallback = cb ;
2025-10-27 16:46:35 -07:00
return mockHttpServer ;
2025-09-16 10:05:29 -04:00
} ) ;
const clientPromise = getOauthClient (
AuthType . LOGIN_WITH_GOOGLE ,
mockConfig ,
) ;
await serverListeningPromise ;
const mockReq = {
url : ` /oauth2callback?code= ${ mockCode } &state= ${ mockState } ` ,
} as http . IncomingMessage ;
const mockRes = {
writeHead : vi.fn ( ) ,
end : vi.fn ( ) ,
} as unknown as http . ServerResponse ;
2025-09-03 13:51:29 -07:00
await requestCallback ( mockReq , mockRes ) ;
2025-09-16 10:05:29 -04:00
const client = await clientPromise ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
// Authentication should succeed even if fetchAndCacheUserInfo fails
expect ( client ) . toBe ( mockOAuth2Client ) ;
2025-10-27 16:46:35 -07:00
expect ( consoleLogSpy ) . toHaveBeenCalledWith (
2025-09-16 10:05:29 -04:00
'Failed to fetch user info:' ,
500 ,
'Internal Server Error' ,
) ;
2025-09-03 13:51:29 -07:00
2025-10-27 16:46:35 -07:00
consoleLogSpy . mockRestore ( ) ;
2025-09-16 10:05:29 -04:00
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should handle user code authentication failure with descriptive error' , async ( ) = > {
const mockConfigWithNoBrowser = {
getNoBrowser : ( ) = > true ,
getProxy : ( ) = > 'http://test.proxy.com:8080' ,
isBrowserLaunchSuppressed : ( ) = > true ,
} as unknown as Config ;
const mockOAuth2Client = {
generateCodeVerifierAsync : vi.fn ( ) . mockResolvedValue ( {
codeChallenge : 'test-challenge' ,
codeVerifier : 'test-verifier' ,
} ) ,
generateAuthUrl : vi.fn ( ) . mockReturnValue ( 'https://example.com/auth' ) ,
getToken : vi
. fn ( )
. mockRejectedValue ( new Error ( 'Invalid authorization code' ) ) ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
const mockReadline = {
question : vi.fn ( ( _query , callback ) = > callback ( 'invalid-code' ) ) ,
close : vi.fn ( ) ,
} ;
( readline . createInterface as Mock ) . mockReturnValue ( mockReadline ) ;
const consoleLogSpy = vi
. spyOn ( console , 'log' )
. mockImplementation ( ( ) = > { } ) ;
const consoleErrorSpy = vi
. spyOn ( console , 'error' )
. mockImplementation ( ( ) = > { } ) ;
await expect (
getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfigWithNoBrowser ) ,
) . rejects . toThrow ( 'Failed to authenticate with user code.' ) ;
expect ( consoleErrorSpy ) . toHaveBeenCalledWith (
'Failed to authenticate with authorization code:' ,
'Invalid authorization code' ,
) ;
consoleLogSpy . mockRestore ( ) ;
consoleErrorSpy . mockRestore ( ) ;
} ) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
describe ( 'clearCachedCredentialFile' , ( ) = > {
it ( 'should clear cached credentials and Google account' , async ( ) = > {
const cachedCreds = { refresh_token : 'test-token' } ;
2025-10-14 02:31:39 +09:00
const credsPath = path . join (
tempHomeDir ,
GEMINI_DIR ,
'oauth_creds.json' ,
) ;
2025-09-16 10:05:29 -04:00
await fs . promises . mkdir ( path . dirname ( credsPath ) , { recursive : true } ) ;
await fs . promises . writeFile ( credsPath , JSON . stringify ( cachedCreds ) ) ;
const googleAccountPath = path . join (
tempHomeDir ,
2025-10-14 02:31:39 +09:00
GEMINI_DIR ,
2025-09-16 10:05:29 -04:00
'google_accounts.json' ,
) ;
const accountData = { active : 'test@example.com' , old : [ ] } ;
await fs . promises . writeFile (
googleAccountPath ,
JSON . stringify ( accountData ) ,
) ;
const userAccountManager = new UserAccountManager ( ) ;
expect ( fs . existsSync ( credsPath ) ) . toBe ( true ) ;
expect ( fs . existsSync ( googleAccountPath ) ) . toBe ( true ) ;
expect ( userAccountManager . getCachedGoogleAccount ( ) ) . toBe (
'test@example.com' ,
) ;
await clearCachedCredentialFile ( ) ;
expect ( fs . existsSync ( credsPath ) ) . toBe ( false ) ;
expect ( userAccountManager . getCachedGoogleAccount ( ) ) . toBeNull ( ) ;
const updatedAccountData = JSON . parse (
fs . readFileSync ( googleAccountPath , 'utf-8' ) ,
) ;
expect ( updatedAccountData . active ) . toBeNull ( ) ;
expect ( updatedAccountData . old ) . toContain ( 'test@example.com' ) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
it ( 'should clear the in-memory OAuth client cache' , async ( ) = > {
const mockSetCredentials = vi . fn ( ) ;
const mockGetAccessToken = vi
. fn ( )
. mockResolvedValue ( { token : 'test-token' } ) ;
const mockGetTokenInfo = vi . fn ( ) . mockResolvedValue ( { } ) ;
const mockOAuth2Client = {
setCredentials : mockSetCredentials ,
getAccessToken : mockGetAccessToken ,
getTokenInfo : mockGetTokenInfo ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-16 10:05:29 -04:00
// Pre-populate credentials to make getOauthClient resolve quickly
2025-10-14 02:31:39 +09:00
const credsPath = path . join (
tempHomeDir ,
GEMINI_DIR ,
'oauth_creds.json' ,
) ;
2025-09-16 10:05:29 -04:00
await fs . promises . mkdir ( path . dirname ( credsPath ) , { recursive : true } ) ;
await fs . promises . writeFile (
credsPath ,
JSON . stringify ( { refresh_token : 'token' } ) ,
) ;
// First call, should create a client
await getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ;
expect ( OAuth2Client ) . toHaveBeenCalledTimes ( 1 ) ;
// Second call, should use cached client
await getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ;
expect ( OAuth2Client ) . toHaveBeenCalledTimes ( 1 ) ;
clearOauthClientCache ( ) ;
// Third call, after clearing cache, should create a new client
await getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ;
expect ( OAuth2Client ) . toHaveBeenCalledTimes ( 2 ) ;
2025-09-03 13:51:29 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
} ) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
describe ( 'with encrypted flag true' , ( ) = > {
let tempHomeDir : string ;
beforeEach ( ( ) = > {
process . env [ FORCE_ENCRYPTED_FILE_ENV_VAR ] = 'true' ;
tempHomeDir = fs . mkdtempSync (
path . join ( os . tmpdir ( ) , 'gemini-cli-test-home-' ) ,
2025-09-03 13:51:29 -07:00
) ;
2025-09-16 10:05:29 -04:00
( os . homedir as Mock ) . mockReturnValue ( tempHomeDir ) ;
} ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
afterEach ( ( ) = > {
fs . rmSync ( tempHomeDir , { recursive : true , force : true } ) ;
vi . clearAllMocks ( ) ;
resetOauthClientForTesting ( ) ;
vi . unstubAllEnvs ( ) ;
2025-09-03 13:51:29 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
it ( 'should save credentials using OAuthCredentialStorage during web login' , async ( ) = > {
const { OAuthCredentialStorage } = await import (
'./oauth-credential-storage.js'
) ;
2025-09-03 13:51:29 -07:00
const mockAuthUrl = 'https://example.com/auth' ;
const mockCode = 'test-code' ;
const mockState = 'test-state' ;
const mockTokens = {
access_token : 'test-access-token' ,
refresh_token : 'test-refresh-token' ,
} ;
2025-09-16 10:05:29 -04:00
let onTokensCallback : ( tokens : Credentials ) = > void = ( ) = > { } ;
const mockOn = vi . fn ( ( event , callback ) = > {
if ( event === 'tokens' ) {
onTokensCallback = callback ;
}
} ) ;
const mockGetToken = vi . fn ( ) . mockImplementation ( async ( ) = > {
onTokensCallback ( mockTokens ) ;
return { tokens : mockTokens } ;
} ) ;
2025-09-03 13:51:29 -07:00
const mockOAuth2Client = {
generateAuthUrl : vi.fn ( ) . mockReturnValue ( mockAuthUrl ) ,
2025-09-16 10:05:29 -04:00
getToken : mockGetToken ,
2025-09-03 13:51:29 -07:00
setCredentials : vi.fn ( ) ,
getAccessToken : vi
. fn ( )
2025-09-16 10:05:29 -04:00
. mockResolvedValue ( { token : 'mock-access-token' } ) ,
on : mockOn ,
credentials : mockTokens ,
2025-09-03 13:51:29 -07:00
} as unknown as OAuth2Client ;
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation ( ( ) = > mockOAuth2Client ) ;
2025-09-03 13:51:29 -07:00
vi . spyOn ( crypto , 'randomBytes' ) . mockReturnValue ( mockState as never ) ;
2025-10-27 16:46:35 -07:00
vi . mocked ( open ) . mockImplementation (
async ( ) = > ( { on : vi.fn ( ) } ) as never ,
) ;
2025-09-03 13:51:29 -07:00
( global . fetch as Mock ) . mockResolvedValue ( {
2025-09-16 10:05:29 -04:00
ok : true ,
json : vi
. fn ( )
. mockResolvedValue ( { email : 'test-google-account@gmail.com' } ) ,
2025-09-03 13:51:29 -07:00
} as unknown as Response ) ;
let requestCallback ! : http . RequestListener ;
let serverListeningCallback : ( value : unknown ) = > void ;
const serverListeningPromise = new Promise (
( resolve ) = > ( serverListeningCallback = resolve ) ,
) ;
2025-09-16 10:05:29 -04:00
let capturedPort = 0 ;
2025-09-03 13:51:29 -07:00
const mockHttpServer = {
2025-09-16 10:05:29 -04:00
listen : vi.fn ( ( port : number , _host : string , callback ? : ( ) = > void ) = > {
capturedPort = port ;
if ( callback ) {
callback ( ) ;
}
2025-09-03 13:51:29 -07:00
serverListeningCallback ( undefined ) ;
} ) ,
2025-09-16 10:05:29 -04:00
close : vi.fn ( ( callback ? : ( ) = > void ) = > {
if ( callback ) {
callback ( ) ;
}
} ) ,
2025-09-03 13:51:29 -07:00
on : vi.fn ( ) ,
2025-09-16 10:05:29 -04:00
address : ( ) = > ( { port : capturedPort } ) ,
2025-09-03 13:51:29 -07:00
} ;
( http . createServer as Mock ) . mockImplementation ( ( cb ) = > {
2025-09-16 10:05:29 -04:00
requestCallback = cb as http . RequestListener ;
2025-09-03 13:51:29 -07:00
return mockHttpServer as unknown as http . Server ;
} ) ;
const clientPromise = getOauthClient (
AuthType . LOGIN_WITH_GOOGLE ,
mockConfig ,
) ;
2025-09-16 10:05:29 -04:00
2025-09-03 13:51:29 -07:00
await serverListeningPromise ;
const mockReq = {
url : ` /oauth2callback?code= ${ mockCode } &state= ${ mockState } ` ,
} as http . IncomingMessage ;
const mockRes = {
writeHead : vi.fn ( ) ,
end : vi.fn ( ) ,
} as unknown as http . ServerResponse ;
2025-09-16 10:05:29 -04:00
requestCallback ( mockReq , mockRes ) ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
await clientPromise ;
2025-09-03 13:51:29 -07:00
2025-09-16 10:05:29 -04:00
expect (
OAuthCredentialStorage . saveCredentials as Mock ,
) . toHaveBeenCalledWith ( mockTokens ) ;
2025-10-14 02:31:39 +09:00
const credsPath = path . join ( tempHomeDir , GEMINI_DIR , 'oauth_creds.json' ) ;
2025-09-16 10:05:29 -04:00
expect ( fs . existsSync ( credsPath ) ) . toBe ( false ) ;
2025-09-03 13:51:29 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
it ( 'should load credentials using OAuthCredentialStorage and not from file' , async ( ) = > {
const { OAuthCredentialStorage } = await import (
'./oauth-credential-storage.js'
2025-09-03 13:51:29 -07:00
) ;
2025-09-16 10:05:29 -04:00
const cachedCreds = { refresh_token : 'cached-encrypted-token' } ;
( OAuthCredentialStorage . loadCredentials as Mock ) . mockResolvedValue (
cachedCreds ,
2025-09-03 13:51:29 -07:00
) ;
2025-09-16 10:05:29 -04:00
// Create a dummy unencrypted credential file.
// If the logic is correct, this file should be ignored.
const unencryptedCreds = { refresh_token : 'unencrypted-token' } ;
2025-10-14 02:31:39 +09:00
const credsPath = path . join ( tempHomeDir , GEMINI_DIR , 'oauth_creds.json' ) ;
2025-08-19 17:06:25 -07:00
await fs . promises . mkdir ( path . dirname ( credsPath ) , { recursive : true } ) ;
2025-09-16 10:05:29 -04:00
await fs . promises . writeFile ( credsPath , JSON . stringify ( unencryptedCreds ) ) ;
2025-08-19 17:06:25 -07:00
2025-09-16 10:05:29 -04:00
const mockClient = {
setCredentials : vi.fn ( ) ,
getAccessToken : vi.fn ( ) . mockResolvedValue ( { token : 'test-token' } ) ,
getTokenInfo : vi.fn ( ) . mockResolvedValue ( { } ) ,
on : vi.fn ( ) ,
} ;
2025-08-19 17:06:25 -07:00
2025-10-27 16:46:35 -07:00
vi . mocked ( OAuth2Client ) . mockImplementation (
2025-09-16 10:05:29 -04:00
( ) = > mockClient as unknown as OAuth2Client ,
2025-08-20 10:55:47 +09:00
) ;
2025-08-19 17:06:25 -07:00
2025-09-16 10:05:29 -04:00
await getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ;
expect ( OAuthCredentialStorage . loadCredentials as Mock ) . toHaveBeenCalled ( ) ;
expect ( mockClient . setCredentials ) . toHaveBeenCalledWith ( cachedCreds ) ;
expect ( mockClient . setCredentials ) . not . toHaveBeenCalledWith (
unencryptedCreds ,
2025-08-19 17:06:25 -07:00
) ;
} ) ;
2025-09-16 10:05:29 -04:00
it ( 'should clear credentials using OAuthCredentialStorage' , async ( ) = > {
const { OAuthCredentialStorage } = await import (
'./oauth-credential-storage.js'
2025-08-19 17:06:25 -07:00
) ;
2025-09-16 10:05:29 -04:00
// Create a dummy unencrypted credential file. It should not be deleted.
2025-10-14 02:31:39 +09:00
const credsPath = path . join ( tempHomeDir , GEMINI_DIR , 'oauth_creds.json' ) ;
2025-08-19 17:06:25 -07:00
await fs . promises . mkdir ( path . dirname ( credsPath ) , { recursive : true } ) ;
2025-09-16 10:05:29 -04:00
await fs . promises . writeFile ( credsPath , '{}' ) ;
2025-08-19 17:06:25 -07:00
2025-09-16 10:05:29 -04:00
await clearCachedCredentialFile ( ) ;
2025-08-19 17:06:25 -07:00
2025-09-16 10:05:29 -04:00
expect (
OAuthCredentialStorage . clearCredentials as Mock ,
) . toHaveBeenCalled ( ) ;
expect ( fs . existsSync ( credsPath ) ) . toBe ( true ) ; // The unencrypted file should remain
2025-08-19 17:06:25 -07:00
} ) ;
} ) ;
2025-06-15 22:41:32 -07:00
} ) ;