From cb23d6b9932e4ccdb2eb73f3b00cb7f342f8827b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 26 May 2026 15:33:38 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AF=20fix:=20Suppress=20Google=20Servi?= =?UTF-8?q?ce=20Key=20Noise=20(#13322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: suppress unused Google service key noise * test: stabilize Google endpoint config mocks * fix: normalize Google service key endpoint config --- .../services/Config/loadAsyncEndpoints.js | 42 +++++-- .../Config/loadAsyncEndpoints.spec.js | 119 ++++++++++++++++++ .../services/Config/loadDefaultEConfig.js | 4 +- .../Config/loadDefaultEConfig.spec.js | 74 +++++++++++ 4 files changed, 229 insertions(+), 10 deletions(-) create mode 100644 api/server/services/Config/loadAsyncEndpoints.spec.js create mode 100644 api/server/services/Config/loadDefaultEConfig.spec.js diff --git a/api/server/services/Config/loadAsyncEndpoints.js b/api/server/services/Config/loadAsyncEndpoints.js index 0d6a05aff7..5eaf6afb65 100644 --- a/api/server/services/Config/loadAsyncEndpoints.js +++ b/api/server/services/Config/loadAsyncEndpoints.js @@ -1,10 +1,34 @@ const path = require('path'); +const fs = require('fs/promises'); const { logger } = require('@librechat/data-schemas'); const { loadServiceKey, isUserProvided } = require('@librechat/api'); const { config } = require('./EndpointService'); +const defaultServiceKeyPath = path.join(__dirname, '../../..', 'data', 'auth.json'); + +async function getServiceKeyPath() { + const serviceKeyPath = process.env.GOOGLE_SERVICE_KEY_FILE?.trim(); + if (serviceKeyPath) { + return serviceKeyPath; + } + + try { + await fs.access(defaultServiceKeyPath); + return defaultServiceKeyPath; + } catch (error) { + if (error?.code !== 'ENOENT') { + logger.warn( + `Unable to access default Google service key file: ${defaultServiceKeyPath}`, + error, + ); + } + return null; + } +} + async function loadAsyncEndpoints() { - let serviceKey, googleUserProvides; + let serviceKey; + let googleUserProvides = false; const { googleKey } = config; /** Check if GOOGLE_KEY is provided at all(including 'user_provided') */ @@ -14,15 +38,15 @@ async function loadAsyncEndpoints() { /** If GOOGLE_KEY is provided, check if it's user_provided */ googleUserProvides = isUserProvided(googleKey); } else { - /** Only attempt to load service key if GOOGLE_KEY is not provided */ - const serviceKeyPath = - process.env.GOOGLE_SERVICE_KEY_FILE || path.join(__dirname, '../../..', 'data', 'auth.json'); + const serviceKeyPath = await getServiceKeyPath(); - try { - serviceKey = await loadServiceKey(serviceKeyPath); - } catch (error) { - logger.error('Error loading service key', error); - serviceKey = null; + if (serviceKeyPath) { + try { + serviceKey = await loadServiceKey(serviceKeyPath); + } catch (error) { + logger.warn('Error loading Google service key', error); + serviceKey = null; + } } } diff --git a/api/server/services/Config/loadAsyncEndpoints.spec.js b/api/server/services/Config/loadAsyncEndpoints.spec.js new file mode 100644 index 0000000000..1c63bf4228 --- /dev/null +++ b/api/server/services/Config/loadAsyncEndpoints.spec.js @@ -0,0 +1,119 @@ +const mockAccess = jest.fn(); +const mockLoadServiceKey = jest.fn(); +const mockIsUserProvided = jest.fn((value) => value === 'user_provided'); +const mockLogger = { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), +}; + +function mockOptionalModule(moduleName, factory) { + try { + require.resolve(moduleName); + jest.doMock(moduleName, factory); + } catch { + jest.doMock(moduleName, factory, { virtual: true }); + } +} + +function mockDependencies() { + jest.doMock('fs/promises', () => ({ + access: mockAccess, + })); + + mockOptionalModule('@librechat/api', () => ({ + isEnabled: (value) => value === true || value === 'true' || value === '1', + isUserProvided: mockIsUserProvided, + loadServiceKey: mockLoadServiceKey, + })); + + mockOptionalModule('@librechat/data-schemas', () => ({ + logger: mockLogger, + })); + + mockOptionalModule('librechat-data-provider', () => ({ + EModelEndpoint: { + agents: 'agents', + anthropic: 'anthropic', + assistants: 'assistants', + azureAssistants: 'azureAssistants', + azureOpenAI: 'azureOpenAI', + bedrock: 'bedrock', + google: 'google', + openAI: 'openAI', + }, + })); + + jest.doMock('~/server/utils/handleText', () => ({ + generateConfig: (key) => (key ? { userProvide: key === 'user_provided' } : false), + })); +} + +describe('loadAsyncEndpoints', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + mockDependencies(); + process.env = { ...originalEnv }; + delete process.env.GOOGLE_KEY; + delete process.env.GOOGLE_SERVICE_KEY_FILE; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + function loadModule(env = {}) { + process.env = { ...process.env, ...env }; + return require('./loadAsyncEndpoints'); + } + + it('does not load the default Google service key when the default file is missing', async () => { + mockAccess.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })); + const loadAsyncEndpoints = loadModule(); + + const result = await loadAsyncEndpoints(); + + expect(result).toEqual({ google: false }); + expect(mockLoadServiceKey).not.toHaveBeenCalled(); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + it('loads the default Google service key when the default file exists', async () => { + const serviceKey = { project_id: 'test-project' }; + mockAccess.mockResolvedValue(); + mockLoadServiceKey.mockResolvedValue(serviceKey); + const loadAsyncEndpoints = loadModule(); + + const result = await loadAsyncEndpoints(); + + expect(result).toEqual({ google: { userProvide: false } }); + expect(mockLoadServiceKey).toHaveBeenCalledWith(expect.stringContaining('api/data/auth.json')); + }); + + it('loads an explicitly configured Google service key path without probing the default file', async () => { + const serviceKey = { project_id: 'test-project' }; + mockLoadServiceKey.mockResolvedValue(serviceKey); + const loadAsyncEndpoints = loadModule({ + GOOGLE_SERVICE_KEY_FILE: '/secrets/google-service-account.json', + }); + + const result = await loadAsyncEndpoints(); + + expect(result).toEqual({ google: { userProvide: false } }); + expect(mockAccess).not.toHaveBeenCalled(); + expect(mockLoadServiceKey).toHaveBeenCalledWith('/secrets/google-service-account.json'); + }); + + it('uses GOOGLE_KEY without probing for a service key', async () => { + const loadAsyncEndpoints = loadModule({ GOOGLE_KEY: 'user_provided' }); + + const result = await loadAsyncEndpoints(); + + expect(result).toEqual({ google: { userProvide: true } }); + expect(mockAccess).not.toHaveBeenCalled(); + expect(mockLoadServiceKey).not.toHaveBeenCalled(); + }); +}); diff --git a/api/server/services/Config/loadDefaultEConfig.js b/api/server/services/Config/loadDefaultEConfig.js index 557b93ce8e..d8e51e6455 100644 --- a/api/server/services/Config/loadDefaultEConfig.js +++ b/api/server/services/Config/loadDefaultEConfig.js @@ -8,10 +8,12 @@ const { config } = require('./EndpointService'); * @returns {Promise>} An object whose keys are endpoint names and values are objects that contain the endpoint configuration and an order. */ async function loadDefaultEndpointsConfig(appConfig) { - const { google } = await loadAsyncEndpoints(appConfig); const { assistants, azureAssistants, azureOpenAI } = config; const enabledEndpoints = getEnabledEndpoints(); + const { google } = enabledEndpoints.includes(EModelEndpoint.google) + ? await loadAsyncEndpoints(appConfig) + : { google: false }; const endpointConfig = { [EModelEndpoint.openAI]: config[EModelEndpoint.openAI], diff --git a/api/server/services/Config/loadDefaultEConfig.spec.js b/api/server/services/Config/loadDefaultEConfig.spec.js new file mode 100644 index 0000000000..6845a549a7 --- /dev/null +++ b/api/server/services/Config/loadDefaultEConfig.spec.js @@ -0,0 +1,74 @@ +const mockGetEnabledEndpoints = jest.fn(); +const mockLoadAsyncEndpoints = jest.fn(); + +function mockOptionalModule(moduleName, factory) { + try { + require.resolve(moduleName); + jest.doMock(moduleName, factory); + } catch { + jest.doMock(moduleName, factory, { virtual: true }); + } +} + +function mockDependencies() { + mockOptionalModule('librechat-data-provider', () => ({ + EModelEndpoint: { + agents: 'agents', + anthropic: 'anthropic', + assistants: 'assistants', + azureAssistants: 'azureAssistants', + azureOpenAI: 'azureOpenAI', + bedrock: 'bedrock', + google: 'google', + openAI: 'openAI', + }, + getEnabledEndpoints: mockGetEnabledEndpoints, + })); + + jest.doMock('./loadAsyncEndpoints', () => mockLoadAsyncEndpoints); + + jest.doMock('./EndpointService', () => ({ + config: { + agents: { userProvide: false }, + anthropic: false, + assistants: false, + azureAssistants: false, + azureOpenAI: false, + bedrock: false, + openAI: { userProvide: false }, + }, + })); +} + +describe('loadDefaultEndpointsConfig', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + mockDependencies(); + }); + + it('does not probe async Google credentials when Google is excluded from enabled endpoints', async () => { + mockGetEnabledEndpoints.mockReturnValue(['openAI']); + const loadDefaultEndpointsConfig = require('./loadDefaultEConfig'); + + const result = await loadDefaultEndpointsConfig(); + + expect(mockLoadAsyncEndpoints).not.toHaveBeenCalled(); + expect(result).toEqual({ + openAI: { userProvide: false, order: 0 }, + }); + }); + + it('loads async Google credentials when Google is enabled', async () => { + mockGetEnabledEndpoints.mockReturnValue(['google']); + mockLoadAsyncEndpoints.mockResolvedValue({ google: { userProvide: false } }); + const loadDefaultEndpointsConfig = require('./loadDefaultEConfig'); + + const result = await loadDefaultEndpointsConfig(); + + expect(mockLoadAsyncEndpoints).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + google: { userProvide: false, order: 0 }, + }); + }); +});