2025-06-15 22:41:32 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-07-07 15:02:13 -07:00
import { describe , it , expect , vi , beforeEach , afterEach , Mock } from 'vitest' ;
2025-07-11 10:57:35 -07:00
import { getOauthClient } from './oauth2.js' ;
import { getCachedGoogleAccount } from '../utils/user_account.js' ;
2025-07-07 15:02:13 -07:00
import { OAuth2Client , Compute } from 'google-auth-library' ;
2025-06-16 19:31:32 -07:00
import * as fs from 'fs' ;
import * as path from 'path' ;
2025-06-15 22:41:32 -07:00
import http from 'http' ;
import open from 'open' ;
import crypto from 'crypto' ;
2025-06-16 19:31:32 -07:00
import * as os from 'os' ;
2025-07-07 15:02:13 -07:00
import { AuthType } from '../core/contentGenerator.js' ;
2025-07-10 18:59:02 -07:00
import { Config } from '../config/config.js' ;
2025-07-11 14:05:27 -07:00
import readline from 'node:readline' ;
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-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-06-16 19:31:32 -07:00
let tempHomeDir : string ;
beforeEach ( ( ) = > {
tempHomeDir = fs . mkdtempSync (
path . join ( os . tmpdir ( ) , 'gemini-cli-test-home-' ) ,
) ;
2025-07-07 15:02:13 -07:00
( os . homedir as Mock ) . mockReturnValue ( tempHomeDir ) ;
2025-06-16 19:31:32 -07:00
} ) ;
afterEach ( ( ) = > {
fs . rmSync ( tempHomeDir , { recursive : true , force : true } ) ;
2025-07-07 15:02:13 -07:00
vi . clearAllMocks ( ) ;
delete process . env . CLOUD_SHELL ;
2025-06-16 19:31:32 -07:00
} ) ;
2025-06-15 22:41:32 -07: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' ,
} ;
const mockGenerateAuthUrl = vi . fn ( ) . mockReturnValue ( mockAuthUrl ) ;
const mockGetToken = vi . fn ( ) . mockResolvedValue ( { tokens : mockTokens } ) ;
const mockSetCredentials = vi . fn ( ) ;
2025-06-29 16:35:20 -04:00
const mockGetAccessToken = vi
. fn ( )
. mockResolvedValue ( { token : 'mock-access-token' } ) ;
2025-06-15 22:41:32 -07:00
const mockOAuth2Client = {
generateAuthUrl : mockGenerateAuthUrl ,
getToken : mockGetToken ,
setCredentials : mockSetCredentials ,
2025-06-29 16:35:20 -04:00
getAccessToken : mockGetAccessToken ,
2025-06-16 19:31:32 -07:00
credentials : mockTokens ,
2025-06-30 08:47:01 -07:00
on : vi.fn ( ) ,
2025-06-15 22:41:32 -07:00
} as unknown as OAuth2Client ;
2025-07-07 15:02:13 -07:00
( OAuth2Client as unknown as Mock ) . mockImplementation (
( ) = > mockOAuth2Client ,
) ;
2025-06-15 22:41:32 -07:00
vi . spyOn ( crypto , 'randomBytes' ) . mockReturnValue ( mockState as never ) ;
2025-07-18 17:22:50 -07:00
( open as Mock ) . mockImplementation ( async ( ) = > ( { on : vi.fn ( ) } ) as never ) ;
2025-06-15 22:41:32 -07:00
2025-06-29 16:35:20 -04:00
// Mock the UserInfo API response
2025-07-07 15:02:13 -07:00
( global . fetch as Mock ) . mockResolvedValue ( {
2025-06-29 16:35:20 -04:00
ok : true ,
2025-07-11 10:57:35 -07:00
json : vi
. fn ( )
. mockResolvedValue ( { email : 'test-google-account@gmail.com' } ) ,
2025-06-29 16:35:20 -04:00
} as unknown as Response ) ;
2025-06-16 19:31:32 -07:00
let requestCallback ! : http . RequestListener <
typeof http . IncomingMessage ,
typeof http . ServerResponse
> ;
2025-06-18 08:34:22 -07:00
let serverListeningCallback : ( value : unknown ) = > void ;
const serverListeningPromise = new Promise (
( resolve ) = > ( serverListeningCallback = resolve ) ,
) ;
2025-06-18 16:34:00 -07:00
let capturedPort = 0 ;
2025-06-15 22:41:32 -07:00
const mockHttpServer = {
2025-07-18 09:55:26 +08:00
listen : vi.fn ( ( port : number , _host : string , callback ? : ( ) = > void ) = > {
2025-06-18 16:34:00 -07:00
capturedPort = port ;
2025-06-15 22:41:32 -07:00
if ( callback ) {
callback ( ) ;
}
2025-06-18 08:34:22 -07:00
serverListeningCallback ( undefined ) ;
2025-06-15 22:41:32 -07:00
} ) ,
close : vi.fn ( ( callback ? : ( ) = > void ) = > {
if ( callback ) {
callback ( ) ;
}
} ) ,
on : vi.fn ( ) ,
2025-06-18 16:34:00 -07:00
address : ( ) = > ( { port : capturedPort } ) ,
2025-06-15 22:41:32 -07:00
} ;
2025-07-07 15:02:13 -07:00
( http . createServer as Mock ) . mockImplementation ( ( cb ) = > {
2025-06-16 19:31:32 -07:00
requestCallback = cb as http . RequestListener <
typeof http . IncomingMessage ,
typeof http . ServerResponse
> ;
2025-06-15 22:41:32 -07:00
return mockHttpServer as unknown as http . Server ;
} ) ;
2025-07-10 18:59:02 -07:00
const clientPromise = getOauthClient (
AuthType . LOGIN_WITH_GOOGLE ,
mockConfig ,
) ;
2025-06-15 22:41:32 -07:00
2025-06-18 08:34:22 -07:00
// wait for server to start listening.
await serverListeningPromise ;
2025-06-15 22:41:32 -07:00
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-06-16 19:31:32 -07:00
await requestCallback ( mockReq , mockRes ) ;
2025-06-15 22:41:32 -07:00
const client = await clientPromise ;
2025-06-16 19:31:32 -07:00
expect ( client ) . toBe ( mockOAuth2Client ) ;
2025-06-15 22:41:32 -07:00
expect ( open ) . toHaveBeenCalledWith ( mockAuthUrl ) ;
2025-06-18 16:34:00 -07:00
expect ( mockGetToken ) . toHaveBeenCalledWith ( {
code : mockCode ,
redirect_uri : ` http://localhost: ${ capturedPort } /oauth2callback ` ,
} ) ;
2025-06-15 22:41:32 -07:00
expect ( mockSetCredentials ) . toHaveBeenCalledWith ( mockTokens ) ;
2025-06-16 19:31:32 -07:00
2025-07-11 10:57:35 -07:00
// Verify Google Account was cached
const googleAccountPath = path . join (
2025-06-29 16:35:20 -04:00
tempHomeDir ,
'.gemini' ,
2025-07-11 10:57:35 -07:00
'google_accounts.json' ,
2025-06-29 16:35:20 -04:00
) ;
2025-07-11 10:57:35 -07: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' ,
old : [ ] ,
} ) ;
2025-06-29 16:35:20 -04:00
2025-07-11 10:57:35 -07:00
// Verify the getCachedGoogleAccount function works
expect ( getCachedGoogleAccount ( ) ) . toBe ( 'test-google-account@gmail.com' ) ;
2025-06-15 22:41:32 -07:00
} ) ;
2025-07-07 15:02:13 -07:00
2025-07-11 14:05:27 -07:00
it ( 'should perform login with user code' , async ( ) = > {
const mockConfigWithNoBrowser = {
getNoBrowser : ( ) = > true ,
2025-07-18 02:57:37 +08:00
getProxy : ( ) = > 'http://test.proxy.com:8080' ,
2025-07-21 16:23:28 -07:00
isBrowserLaunchSuppressed : ( ) = > true ,
2025-07-11 14:05:27 -07:00
} 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' ,
} ;
const mockGenerateAuthUrl = vi . fn ( ) . mockReturnValue ( mockAuthUrl ) ;
const mockGetToken = vi . fn ( ) . mockResolvedValue ( { tokens : mockTokens } ) ;
const mockSetCredentials = vi . fn ( ) ;
const mockGenerateCodeVerifierAsync = vi
. fn ( )
. mockResolvedValue ( mockCodeVerifier ) ;
const mockOAuth2Client = {
generateAuthUrl : mockGenerateAuthUrl ,
getToken : mockGetToken ,
setCredentials : mockSetCredentials ,
generateCodeVerifierAsync : mockGenerateCodeVerifierAsync ,
on : vi.fn ( ) ,
} as unknown as OAuth2Client ;
( OAuth2Client as unknown as Mock ) . mockImplementation (
( ) = > mockOAuth2Client ,
) ;
const mockReadline = {
question : vi.fn ( ( _query , callback ) = > callback ( mockCode ) ) ,
close : vi.fn ( ) ,
} ;
( readline . createInterface as Mock ) . mockReturnValue ( mockReadline ) ;
2025-07-12 15:42:47 -07:00
const consoleLogSpy = vi . spyOn ( console , 'log' ) . mockImplementation ( ( ) = > { } ) ;
2025-07-11 14:05:27 -07:00
const client = await getOauthClient (
AuthType . LOGIN_WITH_GOOGLE ,
mockConfigWithNoBrowser ,
) ;
expect ( client ) . toBe ( mockOAuth2Client ) ;
// Verify the auth flow
expect ( mockGenerateCodeVerifierAsync ) . toHaveBeenCalled ( ) ;
expect ( mockGenerateAuthUrl ) . toHaveBeenCalled ( ) ;
2025-07-12 15:42:47 -07:00
expect ( consoleLogSpy ) . toHaveBeenCalledWith (
2025-07-11 14:05:27 -07:00
expect . stringContaining ( mockAuthUrl ) ,
) ;
expect ( mockReadline . question ) . toHaveBeenCalledWith (
'Enter the authorization code: ' ,
expect . any ( Function ) ,
) ;
expect ( mockGetToken ) . toHaveBeenCalledWith ( {
code : mockCode ,
codeVerifier : mockCodeVerifier.codeVerifier ,
2025-07-18 17:22:50 -07:00
redirect_uri : 'https://codeassist.google.com/authcode' ,
2025-07-11 14:05:27 -07:00
} ) ;
expect ( mockSetCredentials ) . toHaveBeenCalledWith ( mockTokens ) ;
2025-07-12 15:42:47 -07:00
consoleLogSpy . mockRestore ( ) ;
2025-07-11 14:05:27 -07:00
} ) ;
2025-07-07 15:02:13 -07:00
describe ( 'in Cloud Shell' , ( ) = > {
const mockGetAccessToken = vi . fn ( ) ;
let mockComputeClient : Compute ;
beforeEach ( ( ) = > {
vi . spyOn ( os , 'homedir' ) . mockReturnValue ( '/user/home' ) ;
vi . spyOn ( fs . promises , 'mkdir' ) . mockResolvedValue ( undefined ) ;
vi . spyOn ( fs . promises , 'writeFile' ) . mockResolvedValue ( undefined ) ;
vi . spyOn ( fs . promises , 'readFile' ) . mockRejectedValue (
new Error ( 'File not found' ) ,
) ; // Default to no cached creds
mockGetAccessToken . mockResolvedValue ( { token : 'test-access-token' } ) ;
mockComputeClient = {
credentials : { refresh_token : 'test-refresh-token' } ,
getAccessToken : mockGetAccessToken ,
} as unknown as Compute ;
( Compute as unknown as Mock ) . mockImplementation ( ( ) = > mockComputeClient ) ;
} ) ;
it ( 'should attempt to load cached credentials first' , async ( ) = > {
const cachedCreds = { refresh_token : 'cached-token' } ;
vi . spyOn ( fs . promises , 'readFile' ) . mockResolvedValue (
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
( OAuth2Client as unknown as Mock ) . mockImplementation (
( ) = > mockClient as unknown as OAuth2Client ,
) ;
2025-07-10 18:59:02 -07:00
await getOauthClient ( AuthType . LOGIN_WITH_GOOGLE , mockConfig ) ;
2025-07-07 15:02:13 -07:00
expect ( fs . promises . readFile ) . toHaveBeenCalledWith (
'/user/home/.gemini/oauth_creds.json' ,
'utf-8' ,
) ;
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
} ) ;
it ( 'should use Compute to get a client if no cached credentials exist' , async ( ) = > {
2025-07-10 18:59:02 -07:00
await getOauthClient ( AuthType . CLOUD_SHELL , mockConfig ) ;
2025-07-07 15:02:13 -07:00
expect ( Compute ) . toHaveBeenCalledWith ( { } ) ;
expect ( mockGetAccessToken ) . toHaveBeenCalled ( ) ;
} ) ;
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-07-10 18:59:02 -07:00
await getOauthClient ( AuthType . CLOUD_SHELL , mockConfig ) ;
2025-07-07 15:02:13 -07:00
expect ( fs . promises . writeFile ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should return the Compute client on successful ADC authentication' , async ( ) = > {
2025-07-10 18:59:02 -07:00
const client = await getOauthClient ( AuthType . CLOUD_SHELL , mockConfig ) ;
2025-07-07 15:02:13 -07:00
expect ( client ) . toBe ( mockComputeClient ) ;
} ) ;
it ( 'should throw an error if ADC fails' , async ( ) = > {
const testError = new Error ( 'ADC Failed' ) ;
mockGetAccessToken . mockRejectedValue ( testError ) ;
2025-07-10 18:59:02 -07:00
await expect (
getOauthClient ( AuthType . CLOUD_SHELL , mockConfig ) ,
) . rejects . toThrow (
2025-07-07 15:02:13 -07:00
'Could not authenticate using Cloud Shell credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ADC Failed' ,
) ;
} ) ;
} ) ;
2025-06-15 22:41:32 -07:00
} ) ;