mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-06-16 07:51:32 +03:00
🧯 fix: Suppress Google Service Key Noise (#13322)
* fix: suppress unused Google service key noise * test: stabilize Google endpoint config mocks * fix: normalize Google service key endpoint config
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
119
api/server/services/Config/loadAsyncEndpoints.spec.js
Normal file
119
api/server/services/Config/loadAsyncEndpoints.spec.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -8,10 +8,12 @@ const { config } = require('./EndpointService');
|
||||
* @returns {Promise<Object.<string, EndpointWithOrder>>} 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],
|
||||
|
||||
74
api/server/services/Config/loadDefaultEConfig.spec.js
Normal file
74
api/server/services/Config/loadDefaultEConfig.spec.js
Normal file
@@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user