From 5b0cb2752e6fb038e75bf3e5d963f2f1998a9d84 Mon Sep 17 00:00:00 2001 From: Sarath Francis Date: Tue, 2 Jun 2026 14:20:08 -0400 Subject: [PATCH] test: add unit tests for OAuth2CredentialRefresher Add unit tests for OAuth2CredentialRefresher, which previously had no test coverage while its sibling OAuth2CredentialExchanger was already tested. The tests cover isRefreshNeeded (no oauth2, missing expiresAt, expired and non-expired tokens) and refresh (missing oauth2, missing auth scheme, missing refresh token, refresh not needed, missing token endpoint, missing client credentials, successful refresh, response field fallback, request body construction, and fetch failure). This brings the refresher to coverage parity with the exchanger and mirrors the equivalent test in adk-python. --- .../oauth2_credential_refresher_test.ts | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 core/test/auth/oauth2/oauth2_credential_refresher_test.ts diff --git a/core/test/auth/oauth2/oauth2_credential_refresher_test.ts b/core/test/auth/oauth2/oauth2_credential_refresher_test.ts new file mode 100644 index 00000000..0dead0bd --- /dev/null +++ b/core/test/auth/oauth2/oauth2_credential_refresher_test.ts @@ -0,0 +1,294 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {AuthCredential} from '../../../src/auth/auth_credential.js'; +import {AuthScheme} from '../../../src/auth/auth_schemes.js'; +import {OAuth2CredentialRefresher} from '../../../src/auth/oauth2/oauth2_credential_refresher.js'; +import * as oauth2Utils from '../../../src/auth/oauth2/oauth2_utils.js'; + +vi.mock('../../../src/auth/oauth2/oauth2_utils.js', () => ({ + getTokenEndpoint: vi.fn(), + fetchOAuth2Tokens: vi.fn(), + isTokenExpired: vi.fn(), +})); + +describe('OAuth2CredentialRefresher', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('isRefreshNeeded', () => { + it('returns false when the credential has no oauth2 field', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = {} as AuthCredential; + + const result = await refresher.isRefreshNeeded(authCredential); + + expect(result).toBe(false); + expect(oauth2Utils.isTokenExpired).not.toHaveBeenCalled(); + }); + + it('returns false when oauth2 has no expiresAt', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: {accessToken: 'existing-token'}, + } as AuthCredential; + + const result = await refresher.isRefreshNeeded(authCredential); + + expect(result).toBe(false); + expect(oauth2Utils.isTokenExpired).not.toHaveBeenCalled(); + }); + + it('returns false when the token is not expired', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: {accessToken: 'existing-token', expiresAt: 123}, + } as AuthCredential; + + vi.mocked(oauth2Utils.isTokenExpired).mockReturnValue(false); + + const result = await refresher.isRefreshNeeded(authCredential); + + expect(result).toBe(false); + expect(oauth2Utils.isTokenExpired).toHaveBeenCalledWith( + authCredential.oauth2, + ); + }); + + it('returns true when the token is expired', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: {accessToken: 'existing-token', expiresAt: 123}, + } as AuthCredential; + + vi.mocked(oauth2Utils.isTokenExpired).mockReturnValue(true); + + const result = await refresher.isRefreshNeeded(authCredential); + + expect(result).toBe(true); + expect(oauth2Utils.isTokenExpired).toHaveBeenCalledWith( + authCredential.oauth2, + ); + }); + }); + + describe('refresh', () => { + const authScheme = { + tokenEndpoint: 'https://example.com/token', + } as AuthScheme; + + it('returns the original credential when there is no oauth2 field', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = {} as AuthCredential; + + const result = await refresher.refresh(authCredential, authScheme); + + expect(result).toBe(authCredential); + expect(oauth2Utils.fetchOAuth2Tokens).not.toHaveBeenCalled(); + }); + + it('returns the original credential when no auth scheme is provided', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: {refreshToken: 'refresh-token'}, + } as AuthCredential; + + const result = await refresher.refresh(authCredential); + + expect(result).toBe(authCredential); + expect(oauth2Utils.fetchOAuth2Tokens).not.toHaveBeenCalled(); + }); + + it('returns the original credential when no refresh token is available', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: {accessToken: 'existing-token'}, + } as AuthCredential; + + const result = await refresher.refresh(authCredential, authScheme); + + expect(result).toBe(authCredential); + expect(oauth2Utils.fetchOAuth2Tokens).not.toHaveBeenCalled(); + }); + + it('returns the original credential when refresh is not needed', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: { + clientId: 'id', + clientSecret: 'secret', + refreshToken: 'refresh-token', + expiresAt: 123, + }, + } as AuthCredential; + + vi.mocked(oauth2Utils.isTokenExpired).mockReturnValue(false); + + const result = await refresher.refresh(authCredential, authScheme); + + expect(result).toBe(authCredential); + expect(oauth2Utils.fetchOAuth2Tokens).not.toHaveBeenCalled(); + }); + + it('returns the original credential when the token endpoint is missing', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: { + clientId: 'id', + clientSecret: 'secret', + refreshToken: 'refresh-token', + expiresAt: 123, + }, + } as AuthCredential; + + vi.mocked(oauth2Utils.isTokenExpired).mockReturnValue(true); + vi.mocked(oauth2Utils.getTokenEndpoint).mockReturnValue(undefined); + + const result = await refresher.refresh(authCredential, authScheme); + + expect(result).toBe(authCredential); + expect(oauth2Utils.fetchOAuth2Tokens).not.toHaveBeenCalled(); + }); + + it('returns the original credential when clientId or clientSecret is missing', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: { + refreshToken: 'refresh-token', + expiresAt: 123, + }, + } as AuthCredential; + + vi.mocked(oauth2Utils.isTokenExpired).mockReturnValue(true); + vi.mocked(oauth2Utils.getTokenEndpoint).mockReturnValue( + 'https://example.com/token', + ); + + const result = await refresher.refresh(authCredential, authScheme); + + expect(result).toBe(authCredential); + expect(oauth2Utils.fetchOAuth2Tokens).not.toHaveBeenCalled(); + }); + + it('fetches new tokens and returns the updated credential when refresh is needed', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: { + clientId: 'id', + clientSecret: 'secret', + accessToken: 'old-token', + refreshToken: 'old-refresh-token', + expiresAt: 123, + }, + } as AuthCredential; + + vi.mocked(oauth2Utils.isTokenExpired).mockReturnValue(true); + vi.mocked(oauth2Utils.getTokenEndpoint).mockReturnValue( + 'https://example.com/token', + ); + vi.mocked(oauth2Utils.fetchOAuth2Tokens).mockResolvedValue({ + accessToken: 'new-token', + refreshToken: 'new-refresh-token', + expiresIn: 3600, + expiresAt: 999, + }); + + const result = await refresher.refresh(authCredential, authScheme); + + expect(result).not.toBe(authCredential); + expect(result.oauth2?.accessToken).toBe('new-token'); + expect(result.oauth2?.refreshToken).toBe('new-refresh-token'); + expect(result.oauth2?.expiresIn).toBe(3600); + expect(result.oauth2?.expiresAt).toBe(999); + expect(oauth2Utils.fetchOAuth2Tokens).toHaveBeenCalledTimes(1); + }); + + it('keeps the existing token values when the response omits them', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: { + clientId: 'id', + clientSecret: 'secret', + accessToken: 'old-token', + refreshToken: 'old-refresh-token', + expiresAt: 123, + }, + } as AuthCredential; + + vi.mocked(oauth2Utils.isTokenExpired).mockReturnValue(true); + vi.mocked(oauth2Utils.getTokenEndpoint).mockReturnValue( + 'https://example.com/token', + ); + vi.mocked(oauth2Utils.fetchOAuth2Tokens).mockResolvedValue({ + expiresIn: 3600, + }); + + const result = await refresher.refresh(authCredential, authScheme); + + expect(result.oauth2?.accessToken).toBe('old-token'); + expect(result.oauth2?.refreshToken).toBe('old-refresh-token'); + expect(result.oauth2?.expiresAt).toBe(123); + expect(result.oauth2?.expiresIn).toBe(3600); + }); + + it('builds the request body with the refresh_token grant parameters', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: { + clientId: 'client-id', + clientSecret: 'client-secret', + refreshToken: 'the-refresh-token', + expiresAt: 123, + }, + } as AuthCredential; + + vi.mocked(oauth2Utils.isTokenExpired).mockReturnValue(true); + vi.mocked(oauth2Utils.getTokenEndpoint).mockReturnValue( + 'https://example.com/token', + ); + vi.mocked(oauth2Utils.fetchOAuth2Tokens).mockResolvedValue({ + accessToken: 'new-token', + }); + + await refresher.refresh(authCredential, authScheme); + + expect(oauth2Utils.fetchOAuth2Tokens).toHaveBeenCalledTimes(1); + const [endpoint, body] = vi.mocked(oauth2Utils.fetchOAuth2Tokens).mock + .calls[0]; + expect(endpoint).toBe('https://example.com/token'); + expect(body.get('grant_type')).toBe('refresh_token'); + expect(body.get('refresh_token')).toBe('the-refresh-token'); + expect(body.get('client_id')).toBe('client-id'); + expect(body.get('client_secret')).toBe('client-secret'); + }); + + it('returns the original credential when fetching tokens fails', async () => { + const refresher = new OAuth2CredentialRefresher(); + const authCredential = { + oauth2: { + clientId: 'id', + clientSecret: 'secret', + refreshToken: 'refresh-token', + expiresAt: 123, + }, + } as AuthCredential; + + vi.mocked(oauth2Utils.isTokenExpired).mockReturnValue(true); + vi.mocked(oauth2Utils.getTokenEndpoint).mockReturnValue( + 'https://example.com/token', + ); + vi.mocked(oauth2Utils.fetchOAuth2Tokens).mockRejectedValue( + new Error('Network error'), + ); + + const result = await refresher.refresh(authCredential, authScheme); + + expect(result).toBe(authCredential); + }); + }); +});