mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00

* ✨ feat: Add support for forced in-memory cache keys configuration
* refactor: Update cache keys to use uppercase constants and moved cache for `librechat.yaml` into its own cache namespace (STATIC_CONFIG) and with a more descriptive key (LIBRECHAT_YAML_CONFIG)
296 lines
8.8 KiB
JavaScript
296 lines
8.8 KiB
JavaScript
const { Time } = require('librechat-data-provider');
|
|
|
|
// Mock dependencies first
|
|
const mockKeyvRedis = {
|
|
namespace: '',
|
|
keyPrefixSeparator: '',
|
|
};
|
|
|
|
const mockKeyv = jest.fn().mockReturnValue({ mock: 'keyv' });
|
|
const mockConnectRedis = jest.fn().mockReturnValue({ mock: 'connectRedis' });
|
|
const mockMemoryStore = jest.fn().mockReturnValue({ mock: 'memoryStore' });
|
|
const mockRedisStore = jest.fn().mockReturnValue({ mock: 'redisStore' });
|
|
|
|
const mockIoredisClient = {
|
|
call: jest.fn(),
|
|
};
|
|
|
|
const mockKeyvRedisClient = {};
|
|
const mockViolationFile = {};
|
|
|
|
// Mock modules before requiring the main module
|
|
jest.mock('@keyv/redis', () => ({
|
|
default: jest.fn().mockImplementation(() => mockKeyvRedis),
|
|
}));
|
|
|
|
jest.mock('keyv', () => ({
|
|
Keyv: mockKeyv,
|
|
}));
|
|
|
|
jest.mock('./cacheConfig', () => ({
|
|
cacheConfig: {
|
|
USE_REDIS: false,
|
|
REDIS_KEY_PREFIX: 'test',
|
|
FORCED_IN_MEMORY_CACHE_NAMESPACES: [],
|
|
},
|
|
}));
|
|
|
|
jest.mock('./redisClients', () => ({
|
|
keyvRedisClient: mockKeyvRedisClient,
|
|
ioredisClient: mockIoredisClient,
|
|
GLOBAL_PREFIX_SEPARATOR: '::',
|
|
}));
|
|
|
|
jest.mock('./keyvFiles', () => ({
|
|
violationFile: mockViolationFile,
|
|
}));
|
|
|
|
jest.mock('connect-redis', () => ({ RedisStore: mockConnectRedis }));
|
|
|
|
jest.mock('memorystore', () => jest.fn(() => mockMemoryStore));
|
|
|
|
jest.mock('rate-limit-redis', () => ({
|
|
RedisStore: mockRedisStore,
|
|
}));
|
|
|
|
// Import after mocking
|
|
const { standardCache, sessionCache, violationCache, limiterCache } = require('./cacheFactory');
|
|
const { cacheConfig } = require('./cacheConfig');
|
|
|
|
describe('cacheFactory', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
// Reset cache config mock
|
|
cacheConfig.USE_REDIS = false;
|
|
cacheConfig.REDIS_KEY_PREFIX = 'test';
|
|
cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = [];
|
|
});
|
|
|
|
describe('redisCache', () => {
|
|
it('should create Redis cache when USE_REDIS is true', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
const namespace = 'test-namespace';
|
|
const ttl = 3600;
|
|
|
|
standardCache(namespace, ttl);
|
|
|
|
expect(require('@keyv/redis').default).toHaveBeenCalledWith(mockKeyvRedisClient);
|
|
expect(mockKeyv).toHaveBeenCalledWith(mockKeyvRedis, { namespace, ttl });
|
|
expect(mockKeyvRedis.namespace).toBe(cacheConfig.REDIS_KEY_PREFIX);
|
|
expect(mockKeyvRedis.keyPrefixSeparator).toBe('::');
|
|
});
|
|
|
|
it('should create Redis cache with undefined ttl when not provided', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
const namespace = 'test-namespace';
|
|
|
|
standardCache(namespace);
|
|
|
|
expect(mockKeyv).toHaveBeenCalledWith(mockKeyvRedis, { namespace, ttl: undefined });
|
|
});
|
|
|
|
it('should use fallback store when USE_REDIS is false and fallbackStore is provided', () => {
|
|
cacheConfig.USE_REDIS = false;
|
|
const namespace = 'test-namespace';
|
|
const ttl = 3600;
|
|
const fallbackStore = { some: 'store' };
|
|
|
|
standardCache(namespace, ttl, fallbackStore);
|
|
|
|
expect(mockKeyv).toHaveBeenCalledWith({ store: fallbackStore, namespace, ttl });
|
|
});
|
|
|
|
it('should create default Keyv instance when USE_REDIS is false and no fallbackStore', () => {
|
|
cacheConfig.USE_REDIS = false;
|
|
const namespace = 'test-namespace';
|
|
const ttl = 3600;
|
|
|
|
standardCache(namespace, ttl);
|
|
|
|
expect(mockKeyv).toHaveBeenCalledWith({ namespace, ttl });
|
|
});
|
|
|
|
it('should handle namespace and ttl as undefined', () => {
|
|
cacheConfig.USE_REDIS = false;
|
|
|
|
standardCache();
|
|
|
|
expect(mockKeyv).toHaveBeenCalledWith({ namespace: undefined, ttl: undefined });
|
|
});
|
|
|
|
it('should use fallback when namespace is in FORCED_IN_MEMORY_CACHE_NAMESPACES', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = ['forced-memory'];
|
|
const namespace = 'forced-memory';
|
|
const ttl = 3600;
|
|
|
|
standardCache(namespace, ttl);
|
|
|
|
expect(require('@keyv/redis').default).not.toHaveBeenCalled();
|
|
expect(mockKeyv).toHaveBeenCalledWith({ namespace, ttl });
|
|
});
|
|
|
|
it('should use Redis when namespace is not in FORCED_IN_MEMORY_CACHE_NAMESPACES', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = ['other-namespace'];
|
|
const namespace = 'test-namespace';
|
|
const ttl = 3600;
|
|
|
|
standardCache(namespace, ttl);
|
|
|
|
expect(require('@keyv/redis').default).toHaveBeenCalledWith(mockKeyvRedisClient);
|
|
expect(mockKeyv).toHaveBeenCalledWith(mockKeyvRedis, { namespace, ttl });
|
|
});
|
|
});
|
|
|
|
describe('violationCache', () => {
|
|
it('should create violation cache with prefixed namespace', () => {
|
|
const namespace = 'test-violations';
|
|
const ttl = 7200;
|
|
|
|
// We can't easily mock the internal redisCache call since it's in the same module
|
|
// But we can test that the function executes without throwing
|
|
expect(() => violationCache(namespace, ttl)).not.toThrow();
|
|
});
|
|
|
|
it('should create violation cache with undefined ttl', () => {
|
|
const namespace = 'test-violations';
|
|
|
|
violationCache(namespace);
|
|
|
|
// The function should call redisCache with violations: prefixed namespace
|
|
// Since we can't easily mock the internal redisCache call, we test the behavior
|
|
expect(() => violationCache(namespace)).not.toThrow();
|
|
});
|
|
|
|
it('should handle undefined namespace', () => {
|
|
expect(() => violationCache(undefined)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('sessionCache', () => {
|
|
it('should return MemoryStore when USE_REDIS is false', () => {
|
|
cacheConfig.USE_REDIS = false;
|
|
const namespace = 'sessions';
|
|
const ttl = 86400;
|
|
|
|
const result = sessionCache(namespace, ttl);
|
|
|
|
expect(mockMemoryStore).toHaveBeenCalledWith({ ttl, checkPeriod: Time.ONE_DAY });
|
|
expect(result).toBe(mockMemoryStore());
|
|
});
|
|
|
|
it('should return ConnectRedis when USE_REDIS is true', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
const namespace = 'sessions';
|
|
const ttl = 86400;
|
|
|
|
const result = sessionCache(namespace, ttl);
|
|
|
|
expect(mockConnectRedis).toHaveBeenCalledWith({
|
|
client: mockIoredisClient,
|
|
ttl,
|
|
prefix: `${namespace}:`,
|
|
});
|
|
expect(result).toBe(mockConnectRedis());
|
|
});
|
|
|
|
it('should add colon to namespace if not present', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
const namespace = 'sessions';
|
|
|
|
sessionCache(namespace);
|
|
|
|
expect(mockConnectRedis).toHaveBeenCalledWith({
|
|
client: mockIoredisClient,
|
|
ttl: undefined,
|
|
prefix: 'sessions:',
|
|
});
|
|
});
|
|
|
|
it('should not add colon to namespace if already present', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
const namespace = 'sessions:';
|
|
|
|
sessionCache(namespace);
|
|
|
|
expect(mockConnectRedis).toHaveBeenCalledWith({
|
|
client: mockIoredisClient,
|
|
ttl: undefined,
|
|
prefix: 'sessions:',
|
|
});
|
|
});
|
|
|
|
it('should handle undefined ttl', () => {
|
|
cacheConfig.USE_REDIS = false;
|
|
const namespace = 'sessions';
|
|
|
|
sessionCache(namespace);
|
|
|
|
expect(mockMemoryStore).toHaveBeenCalledWith({
|
|
ttl: undefined,
|
|
checkPeriod: Time.ONE_DAY,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('limiterCache', () => {
|
|
it('should return undefined when USE_REDIS is false', () => {
|
|
cacheConfig.USE_REDIS = false;
|
|
const result = limiterCache('prefix');
|
|
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should return RedisStore when USE_REDIS is true', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
const result = limiterCache('rate-limit');
|
|
|
|
expect(mockRedisStore).toHaveBeenCalledWith({
|
|
sendCommand: expect.any(Function),
|
|
prefix: `rate-limit:`,
|
|
});
|
|
expect(result).toBe(mockRedisStore());
|
|
});
|
|
|
|
it('should add colon to prefix if not present', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
limiterCache('rate-limit');
|
|
|
|
expect(mockRedisStore).toHaveBeenCalledWith({
|
|
sendCommand: expect.any(Function),
|
|
prefix: 'rate-limit:',
|
|
});
|
|
});
|
|
|
|
it('should not add colon to prefix if already present', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
limiterCache('rate-limit:');
|
|
|
|
expect(mockRedisStore).toHaveBeenCalledWith({
|
|
sendCommand: expect.any(Function),
|
|
prefix: 'rate-limit:',
|
|
});
|
|
});
|
|
|
|
it('should pass sendCommand function that calls ioredisClient.call', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
limiterCache('rate-limit');
|
|
|
|
const sendCommandCall = mockRedisStore.mock.calls[0][0];
|
|
const sendCommand = sendCommandCall.sendCommand;
|
|
|
|
// Test that sendCommand properly delegates to ioredisClient.call
|
|
const args = ['GET', 'test-key'];
|
|
sendCommand(...args);
|
|
|
|
expect(mockIoredisClient.call).toHaveBeenCalledWith(...args);
|
|
});
|
|
|
|
it('should handle undefined prefix', () => {
|
|
cacheConfig.USE_REDIS = true;
|
|
expect(() => limiterCache()).toThrow('prefix is required');
|
|
});
|
|
});
|
|
});
|