| import mongoose from 'mongoose'; |
| import { MongoMemoryServer } from 'mongodb-memory-server'; |
| import { logger, balanceSchema } from '@librechat/data-schemas'; |
| import type { NextFunction, Request as ServerRequest, Response as ServerResponse } from 'express'; |
| import type { IBalance } from '@librechat/data-schemas'; |
| import { createSetBalanceConfig } from './balance'; |
|
|
| jest.mock('@librechat/data-schemas', () => ({ |
| ...jest.requireActual('@librechat/data-schemas'), |
| logger: { |
| error: jest.fn(), |
| }, |
| })); |
|
|
| let mongoServer: MongoMemoryServer; |
| let Balance: mongoose.Model<IBalance>; |
|
|
| beforeAll(async () => { |
| mongoServer = await MongoMemoryServer.create(); |
| const mongoUri = mongoServer.getUri(); |
| Balance = mongoose.models.Balance || mongoose.model('Balance', balanceSchema); |
| await mongoose.connect(mongoUri); |
| }); |
|
|
| afterAll(async () => { |
| await mongoose.disconnect(); |
| await mongoServer.stop(); |
| }); |
|
|
| beforeEach(async () => { |
| await mongoose.connection.dropDatabase(); |
| jest.clearAllMocks(); |
| jest.restoreAllMocks(); |
| }); |
|
|
| describe('createSetBalanceConfig', () => { |
| const createMockRequest = (userId: string | mongoose.Types.ObjectId): Partial<ServerRequest> => ({ |
| user: { |
| _id: userId, |
| id: userId.toString(), |
| email: 'test@example.com', |
| }, |
| }); |
|
|
| const createMockResponse = (): Partial<ServerResponse> => ({ |
| status: jest.fn().mockReturnThis(), |
| json: jest.fn().mockReturnThis(), |
| }); |
|
|
| const mockNext: NextFunction = jest.fn(); |
| describe('Basic Functionality', () => { |
| test('should create balance record for new user with start balance', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(getAppConfig).toHaveBeenCalled(); |
| expect(mockNext).toHaveBeenCalled(); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord).toBeTruthy(); |
| expect(balanceRecord?.tokenCredits).toBe(1000); |
| expect(balanceRecord?.autoRefillEnabled).toBe(true); |
| expect(balanceRecord?.refillIntervalValue).toBe(30); |
| expect(balanceRecord?.refillIntervalUnit).toBe('days'); |
| expect(balanceRecord?.refillAmount).toBe(500); |
| expect(balanceRecord?.lastRefill).toBeInstanceOf(Date); |
| }); |
|
|
| test('should skip if balance config is not enabled', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: false, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(mockNext).toHaveBeenCalled(); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord).toBeNull(); |
| }); |
|
|
| test('should skip if startBalance is null', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: null, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(mockNext).toHaveBeenCalled(); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord).toBeNull(); |
| }); |
|
|
| test('should handle user._id as string', async () => { |
| const userId = new mongoose.Types.ObjectId().toString(); |
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(mockNext).toHaveBeenCalled(); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord).toBeTruthy(); |
| expect(balanceRecord?.tokenCredits).toBe(1000); |
| }); |
|
|
| test('should skip if user is not present in request', async () => { |
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = {} as ServerRequest; |
| const res = createMockResponse(); |
|
|
| await middleware(req, res as ServerResponse, mockNext); |
|
|
| expect(mockNext).toHaveBeenCalled(); |
| expect(getAppConfig).toHaveBeenCalled(); |
| }); |
| }); |
|
|
| describe('Edge Case: Auto-refill without lastRefill', () => { |
| test('should initialize lastRefill when enabling auto-refill for existing user without lastRefill', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
|
|
| |
| |
| const doc = await Balance.create({ |
| user: userId, |
| tokenCredits: 500, |
| autoRefillEnabled: false, |
| }); |
|
|
| |
| await Balance.updateOne({ _id: doc._id }, { $unset: { lastRefill: 1 } }); |
|
|
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| const beforeTime = new Date(); |
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
| const afterTime = new Date(); |
|
|
| expect(mockNext).toHaveBeenCalled(); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord).toBeTruthy(); |
| expect(balanceRecord?.tokenCredits).toBe(500); |
| expect(balanceRecord?.autoRefillEnabled).toBe(true); |
| expect(balanceRecord?.lastRefill).toBeInstanceOf(Date); |
|
|
| |
| const lastRefillTime = balanceRecord?.lastRefill?.getTime() || 0; |
| expect(lastRefillTime).toBeGreaterThanOrEqual(beforeTime.getTime()); |
| expect(lastRefillTime).toBeLessThanOrEqual(afterTime.getTime()); |
| }); |
|
|
| test('should not update lastRefill if it already exists', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const existingLastRefill = new Date('2024-01-01'); |
|
|
| |
| await Balance.create({ |
| user: userId, |
| tokenCredits: 500, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| lastRefill: existingLastRefill, |
| }); |
|
|
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(mockNext).toHaveBeenCalled(); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord?.lastRefill?.getTime()).toBe(existingLastRefill.getTime()); |
| }); |
|
|
| test('should handle existing user with auto-refill enabled but missing lastRefill', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
|
|
| |
| |
| const doc = await Balance.create({ |
| user: userId, |
| tokenCredits: 500, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }); |
|
|
| |
| await Balance.updateOne({ _id: doc._id }, { $unset: { lastRefill: 1 } }); |
|
|
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(mockNext).toHaveBeenCalled(); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord).toBeTruthy(); |
| expect(balanceRecord?.autoRefillEnabled).toBe(true); |
| expect(balanceRecord?.lastRefill).toBeInstanceOf(Date); |
| |
| }); |
|
|
| test('should not set lastRefill when auto-refill is disabled', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
|
|
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
|
|
| startBalance: 1000, |
| autoRefillEnabled: false, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(mockNext).toHaveBeenCalled(); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord).toBeTruthy(); |
| expect(balanceRecord?.tokenCredits).toBe(1000); |
| expect(balanceRecord?.autoRefillEnabled).toBe(false); |
| |
| expect(balanceRecord?.lastRefill).toBeInstanceOf(Date); |
| }); |
| }); |
|
|
| describe('Update Scenarios', () => { |
| test('should update auto-refill settings for existing user', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
|
|
| |
| await Balance.create({ |
| user: userId, |
| tokenCredits: 500, |
| autoRefillEnabled: false, |
| refillIntervalValue: 7, |
| refillIntervalUnit: 'days', |
| refillAmount: 100, |
| }); |
|
|
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord?.tokenCredits).toBe(500); |
| expect(balanceRecord?.autoRefillEnabled).toBe(true); |
| expect(balanceRecord?.refillIntervalValue).toBe(30); |
| expect(balanceRecord?.refillIntervalUnit).toBe('days'); |
| expect(balanceRecord?.refillAmount).toBe(500); |
| }); |
|
|
| test('should not update if values are already the same', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const lastRefillTime = new Date(); |
|
|
| |
| await Balance.create({ |
| user: userId, |
| tokenCredits: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| lastRefill: lastRefillTime, |
| }); |
|
|
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| |
| const updateSpy = jest.spyOn(Balance, 'findOneAndUpdate'); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(mockNext).toHaveBeenCalled(); |
| expect(updateSpy).not.toHaveBeenCalled(); |
| }); |
|
|
| test('should set tokenCredits for user with null tokenCredits', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
|
|
| |
| await Balance.create({ |
| user: userId, |
| tokenCredits: null, |
| }); |
|
|
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
|
|
| startBalance: 2000, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord?.tokenCredits).toBe(2000); |
| }); |
| }); |
|
|
| describe('Error Handling', () => { |
| test('should handle database errors gracefully', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
| const dbError = new Error('Database error'); |
|
|
| |
| jest.spyOn(Balance, 'findOne').mockImplementationOnce((() => { |
| return { |
| lean: jest.fn().mockRejectedValue(dbError), |
| }; |
| }) as unknown as mongoose.Model<IBalance>['findOne']); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(logger.error).toHaveBeenCalledWith('Error setting user balance:', dbError); |
| expect(mockNext).toHaveBeenCalledWith(dbError); |
| }); |
|
|
| test('should handle getAppConfig errors', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const configError = new Error('Config error'); |
| const getAppConfig = jest.fn().mockRejectedValue(configError); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(logger.error).toHaveBeenCalledWith('Error setting user balance:', configError); |
| expect(mockNext).toHaveBeenCalledWith(configError); |
| }); |
|
|
| test('should handle invalid auto-refill configuration', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
|
|
| |
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
|
|
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: null, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| expect(mockNext).toHaveBeenCalled(); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord).toBeTruthy(); |
| expect(balanceRecord?.tokenCredits).toBe(1000); |
| |
| expect(balanceRecord?.autoRefillEnabled).toBe(false); |
| }); |
| }); |
|
|
| describe('Concurrent Updates', () => { |
| test('should handle concurrent middleware calls for same user', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 30, |
| refillIntervalUnit: 'days', |
| refillAmount: 500, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res1 = createMockResponse(); |
| const res2 = createMockResponse(); |
| const mockNext1 = jest.fn(); |
| const mockNext2 = jest.fn(); |
|
|
| |
| await Promise.all([ |
| middleware(req as ServerRequest, res1 as ServerResponse, mockNext1), |
| middleware(req as ServerRequest, res2 as ServerResponse, mockNext2), |
| ]); |
|
|
| expect(mockNext1).toHaveBeenCalled(); |
| expect(mockNext2).toHaveBeenCalled(); |
|
|
| |
| const balanceRecords = await Balance.find({ user: userId }); |
| expect(balanceRecords).toHaveLength(1); |
| expect(balanceRecords[0].tokenCredits).toBe(1000); |
| }); |
| }); |
|
|
| describe('Integration with Different refillIntervalUnits', () => { |
| test.each(['seconds', 'minutes', 'hours', 'days', 'weeks', 'months'])( |
| 'should handle refillIntervalUnit: %s', |
| async (unit) => { |
| const userId = new mongoose.Types.ObjectId(); |
|
|
| const getAppConfig = jest.fn().mockResolvedValue({ |
| balance: { |
| enabled: true, |
|
|
| startBalance: 1000, |
| autoRefillEnabled: true, |
| refillIntervalValue: 10, |
| refillIntervalUnit: unit, |
| refillAmount: 100, |
| }, |
| }); |
|
|
| const middleware = createSetBalanceConfig({ |
| getAppConfig, |
| Balance, |
| }); |
|
|
| const req = createMockRequest(userId); |
| const res = createMockResponse(); |
|
|
| await middleware(req as ServerRequest, res as ServerResponse, mockNext); |
|
|
| const balanceRecord = await Balance.findOne({ user: userId }); |
| expect(balanceRecord?.refillIntervalUnit).toBe(unit); |
| expect(balanceRecord?.refillIntervalValue).toBe(10); |
| expect(balanceRecord?.lastRefill).toBeInstanceOf(Date); |
| }, |
| ); |
| }); |
| }); |
|
|