| | jest.mock('librechat-data-provider', () => ({ |
| | ...jest.requireActual('librechat-data-provider'), |
| | extractVariableName: jest.fn(), |
| | })); |
| |
|
| | jest.mock('@librechat/data-schemas', () => ({ |
| | ...jest.requireActual('@librechat/data-schemas'), |
| | logger: { |
| | debug: jest.fn(), |
| | warn: jest.fn(), |
| | }, |
| | })); |
| |
|
| | import { handleRateLimits } from './limits'; |
| | import { checkWebSearchConfig } from './checks'; |
| | import { logger } from '@librechat/data-schemas'; |
| | import { extractVariableName as extract } from 'librechat-data-provider'; |
| |
|
| | const extractVariableName = extract as jest.MockedFunction<typeof extract>; |
| |
|
| | describe('checkWebSearchConfig', () => { |
| | let originalEnv: NodeJS.ProcessEnv; |
| |
|
| | beforeEach(() => { |
| | |
| | jest.clearAllMocks(); |
| |
|
| | |
| | originalEnv = process.env; |
| |
|
| | |
| | process.env = { ...originalEnv }; |
| | }); |
| |
|
| | afterEach(() => { |
| | |
| | process.env = originalEnv; |
| | }); |
| |
|
| | describe('when webSearchConfig is undefined or null', () => { |
| | it('should return early without logging when config is undefined', () => { |
| | checkWebSearchConfig(undefined); |
| |
|
| | expect(logger.debug).not.toHaveBeenCalled(); |
| | expect(logger.warn).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should return early without logging when config is null', () => { |
| | checkWebSearchConfig(null); |
| |
|
| | expect(logger.debug).not.toHaveBeenCalled(); |
| | expect(logger.warn).not.toHaveBeenCalled(); |
| | }); |
| | }); |
| |
|
| | describe('when config values are proper environment variable references', () => { |
| | it('should log debug message for each valid environment variable with value set', () => { |
| | const config = { |
| | serperApiKey: '${SERPER_API_KEY}', |
| | jinaApiKey: '${JINA_API_KEY}', |
| | }; |
| |
|
| | extractVariableName.mockReturnValueOnce('SERPER_API_KEY').mockReturnValueOnce('JINA_API_KEY'); |
| |
|
| | process.env.SERPER_API_KEY = 'test-serper-key'; |
| | process.env.JINA_API_KEY = 'test-jina-key'; |
| |
|
| | checkWebSearchConfig(config); |
| |
|
| | expect(extractVariableName).toHaveBeenCalledWith('${SERPER_API_KEY}'); |
| | expect(extractVariableName).toHaveBeenCalledWith('${JINA_API_KEY}'); |
| | expect(logger.debug).toHaveBeenCalledWith( |
| | 'Web search serperApiKey: Using environment variable SERPER_API_KEY with value set', |
| | ); |
| | expect(logger.debug).toHaveBeenCalledWith( |
| | 'Web search jinaApiKey: Using environment variable JINA_API_KEY with value set', |
| | ); |
| | expect(logger.warn).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should log debug message for environment variables not set in environment', () => { |
| | const config = { |
| | cohereApiKey: '${COHERE_API_KEY}', |
| | }; |
| |
|
| | extractVariableName.mockReturnValue('COHERE_API_KEY'); |
| |
|
| | delete process.env.COHERE_API_KEY; |
| |
|
| | checkWebSearchConfig(config); |
| |
|
| | expect(logger.debug).toHaveBeenCalledWith( |
| | 'Web search cohereApiKey: Using environment variable COHERE_API_KEY (not set in environment, user provided value)', |
| | ); |
| | expect(logger.warn).not.toHaveBeenCalled(); |
| | }); |
| | }); |
| |
|
| | describe('when config values are actual values instead of environment variable references', () => { |
| | it('should warn when serperApiKey contains actual API key', () => { |
| | const config = { |
| | serperApiKey: 'sk-1234567890abcdef', |
| | }; |
| |
|
| | extractVariableName.mockReturnValue(null); |
| |
|
| | checkWebSearchConfig(config); |
| |
|
| | expect(logger.warn).toHaveBeenCalledWith( |
| | expect.stringContaining( |
| | '❗ Web search configuration error: serperApiKey contains an actual value', |
| | ), |
| | ); |
| | expect(logger.warn).toHaveBeenCalledWith( |
| | expect.stringContaining('Current value: "sk-1234567..."'), |
| | ); |
| | expect(logger.debug).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should warn when firecrawlApiUrl contains actual URL', () => { |
| | const config = { |
| | firecrawlApiUrl: 'https://api.firecrawl.dev', |
| | }; |
| |
|
| | extractVariableName.mockReturnValue(null); |
| |
|
| | checkWebSearchConfig(config); |
| |
|
| | expect(logger.warn).toHaveBeenCalledWith( |
| | expect.stringContaining( |
| | '❗ Web search configuration error: firecrawlApiUrl contains an actual value', |
| | ), |
| | ); |
| | expect(logger.warn).toHaveBeenCalledWith( |
| | expect.stringContaining('Current value: "https://ap..."'), |
| | ); |
| | }); |
| |
|
| | it('should include documentation link in warning message', () => { |
| | const config = { |
| | firecrawlApiKey: 'fc-actual-key', |
| | }; |
| |
|
| | extractVariableName.mockReturnValue(null); |
| |
|
| | checkWebSearchConfig(config); |
| |
|
| | expect(logger.warn).toHaveBeenCalledWith( |
| | expect.stringContaining( |
| | 'More info: https://www.librechat.ai/docs/configuration/librechat_yaml/web_search', |
| | ), |
| | ); |
| | }); |
| | }); |
| |
|
| | describe('when config contains mixed value types', () => { |
| | it('should only process string values and ignore non-string values', () => { |
| | const config = { |
| | serperApiKey: '${SERPER_API_KEY}', |
| | safeSearch: 1, |
| | scraperTimeout: 7500, |
| | jinaApiKey: 'actual-key', |
| | }; |
| |
|
| | extractVariableName.mockReturnValueOnce('SERPER_API_KEY').mockReturnValueOnce(null); |
| |
|
| | process.env.SERPER_API_KEY = 'test-key'; |
| |
|
| | checkWebSearchConfig(config); |
| |
|
| | expect(extractVariableName).toHaveBeenCalledTimes(2); |
| | expect(logger.debug).toHaveBeenCalledTimes(1); |
| | expect(logger.warn).toHaveBeenCalledTimes(1); |
| | }); |
| | }); |
| |
|
| | describe('edge cases', () => { |
| | it('should handle config with no web search keys', () => { |
| | const config = { |
| | someOtherKey: 'value', |
| | anotherKey: '${SOME_VAR}', |
| | }; |
| |
|
| | |
| | |
| | checkWebSearchConfig(config); |
| |
|
| | expect(extractVariableName).not.toHaveBeenCalled(); |
| | expect(logger.debug).not.toHaveBeenCalled(); |
| | expect(logger.warn).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should truncate long values in warning messages', () => { |
| | const config = { |
| | serperApiKey: 'this-is-a-very-long-api-key-that-should-be-truncated-in-the-warning-message', |
| | }; |
| |
|
| | extractVariableName.mockReturnValue(null); |
| |
|
| | checkWebSearchConfig(config); |
| |
|
| | expect(logger.warn).toHaveBeenCalledWith( |
| | expect.stringContaining('Current value: "this-is-a-..."'), |
| | ); |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('handleRateLimits', () => { |
| | let originalEnv: NodeJS.ProcessEnv; |
| |
|
| | beforeEach(() => { |
| | |
| | originalEnv = process.env; |
| |
|
| | |
| | process.env = { ...originalEnv }; |
| | }); |
| |
|
| | afterEach(() => { |
| | |
| | process.env = originalEnv; |
| | }); |
| |
|
| | it('should correctly set FILE_UPLOAD environment variables based on rate limits', () => { |
| | const rateLimits = { |
| | fileUploads: { |
| | ipMax: 100, |
| | ipWindowInMinutes: 60, |
| | userMax: 50, |
| | userWindowInMinutes: 30, |
| | }, |
| | }; |
| |
|
| | handleRateLimits(rateLimits); |
| |
|
| | |
| | expect(process.env.FILE_UPLOAD_IP_MAX).toEqual('100'); |
| | expect(process.env.FILE_UPLOAD_IP_WINDOW).toEqual('60'); |
| | expect(process.env.FILE_UPLOAD_USER_MAX).toEqual('50'); |
| | expect(process.env.FILE_UPLOAD_USER_WINDOW).toEqual('30'); |
| | }); |
| |
|
| | it('should correctly set IMPORT environment variables based on rate limits', () => { |
| | const rateLimits = { |
| | conversationsImport: { |
| | ipMax: 150, |
| | ipWindowInMinutes: 60, |
| | userMax: 50, |
| | userWindowInMinutes: 30, |
| | }, |
| | }; |
| |
|
| | handleRateLimits(rateLimits); |
| |
|
| | |
| | expect(process.env.IMPORT_IP_MAX).toEqual('150'); |
| | expect(process.env.IMPORT_IP_WINDOW).toEqual('60'); |
| | expect(process.env.IMPORT_USER_MAX).toEqual('50'); |
| | expect(process.env.IMPORT_USER_WINDOW).toEqual('30'); |
| | }); |
| |
|
| | it('should not modify FILE_UPLOAD environment variables without rate limits', () => { |
| | |
| | process.env.FILE_UPLOAD_IP_MAX = '10'; |
| | process.env.FILE_UPLOAD_IP_WINDOW = '15'; |
| | process.env.FILE_UPLOAD_USER_MAX = '5'; |
| | process.env.FILE_UPLOAD_USER_WINDOW = '20'; |
| |
|
| | const initialEnv = { ...process.env }; |
| |
|
| | handleRateLimits({}); |
| |
|
| | |
| | expect(process.env.FILE_UPLOAD_IP_MAX).toEqual(initialEnv.FILE_UPLOAD_IP_MAX); |
| | expect(process.env.FILE_UPLOAD_IP_WINDOW).toEqual(initialEnv.FILE_UPLOAD_IP_WINDOW); |
| | expect(process.env.FILE_UPLOAD_USER_MAX).toEqual(initialEnv.FILE_UPLOAD_USER_MAX); |
| | expect(process.env.FILE_UPLOAD_USER_WINDOW).toEqual(initialEnv.FILE_UPLOAD_USER_WINDOW); |
| | }); |
| |
|
| | it('should not modify IMPORT environment variables without rate limits', () => { |
| | |
| | process.env.IMPORT_IP_MAX = '10'; |
| | process.env.IMPORT_IP_WINDOW = '15'; |
| | process.env.IMPORT_USER_MAX = '5'; |
| | process.env.IMPORT_USER_WINDOW = '20'; |
| |
|
| | const initialEnv = { ...process.env }; |
| |
|
| | handleRateLimits({}); |
| |
|
| | |
| | expect(process.env.IMPORT_IP_MAX).toEqual(initialEnv.IMPORT_IP_MAX); |
| | expect(process.env.IMPORT_IP_WINDOW).toEqual(initialEnv.IMPORT_IP_WINDOW); |
| | expect(process.env.IMPORT_USER_MAX).toEqual(initialEnv.IMPORT_USER_MAX); |
| | expect(process.env.IMPORT_USER_WINDOW).toEqual(initialEnv.IMPORT_USER_WINDOW); |
| | }); |
| |
|
| | it('should handle undefined rateLimits parameter', () => { |
| | |
| | process.env.FILE_UPLOAD_IP_MAX = 'initial'; |
| | process.env.IMPORT_IP_MAX = 'initial'; |
| |
|
| | handleRateLimits(undefined); |
| |
|
| | |
| | expect(process.env.FILE_UPLOAD_IP_MAX).toEqual('initial'); |
| | expect(process.env.IMPORT_IP_MAX).toEqual('initial'); |
| | }); |
| |
|
| | it('should handle partial rate limit configurations', () => { |
| | const rateLimits = { |
| | fileUploads: { |
| | ipMax: 200, |
| | |
| | }, |
| | }; |
| |
|
| | handleRateLimits(rateLimits); |
| |
|
| | expect(process.env.FILE_UPLOAD_IP_MAX).toEqual('200'); |
| | |
| | expect(process.env.FILE_UPLOAD_IP_WINDOW).toBeUndefined(); |
| | expect(process.env.FILE_UPLOAD_USER_MAX).toBeUndefined(); |
| | expect(process.env.FILE_UPLOAD_USER_WINDOW).toBeUndefined(); |
| | }); |
| |
|
| | it('should correctly set TTS and STT environment variables based on rate limits', () => { |
| | const rateLimits = { |
| | tts: { |
| | ipMax: 75, |
| | ipWindowInMinutes: 45, |
| | userMax: 25, |
| | userWindowInMinutes: 15, |
| | }, |
| | stt: { |
| | ipMax: 80, |
| | ipWindowInMinutes: 50, |
| | userMax: 30, |
| | userWindowInMinutes: 20, |
| | }, |
| | }; |
| |
|
| | handleRateLimits(rateLimits); |
| |
|
| | |
| | expect(process.env.TTS_IP_MAX).toEqual('75'); |
| | expect(process.env.TTS_IP_WINDOW).toEqual('45'); |
| | expect(process.env.TTS_USER_MAX).toEqual('25'); |
| | expect(process.env.TTS_USER_WINDOW).toEqual('15'); |
| |
|
| | |
| | expect(process.env.STT_IP_MAX).toEqual('80'); |
| | expect(process.env.STT_IP_WINDOW).toEqual('50'); |
| | expect(process.env.STT_USER_MAX).toEqual('30'); |
| | expect(process.env.STT_USER_WINDOW).toEqual('20'); |
| | }); |
| | }); |
| |
|