diff --git a/api/server/controllers/PluginController.spec.js b/api/server/controllers/PluginController.spec.js index 89dfd72e4b..c0461530d6 100644 --- a/api/server/controllers/PluginController.spec.js +++ b/api/server/controllers/PluginController.spec.js @@ -1,5 +1,5 @@ const { Constants } = require('librechat-data-provider'); -const { getCustomConfig, getCachedTools } = require('~/server/services/Config'); +const { getCustomConfig, getCachedTools, getAppConfig } = require('~/server/services/Config'); const { getLogStores } = require('~/cache'); // Mock the dependencies @@ -13,6 +13,7 @@ jest.mock('@librechat/data-schemas', () => ({ jest.mock('~/server/services/Config', () => ({ getCustomConfig: jest.fn(), getCachedTools: jest.fn(), + getAppConfig: jest.fn(), })); jest.mock('~/server/services/ToolService', () => ({ @@ -64,7 +65,10 @@ describe('PluginController', () => { describe('getAvailablePluginsController', () => { beforeEach(() => { - mockReq.app = { locals: { filteredTools: [], includedTools: [] } }; + getAppConfig.mockResolvedValue({ + filteredTools: [], + includedTools: [], + }); }); it('should use filterUniquePlugins to remove duplicate plugins', async () => { @@ -122,7 +126,10 @@ describe('PluginController', () => { { name: 'Plugin2', pluginKey: 'key2', description: 'Second' }, ]; - mockReq.app.locals.includedTools = ['key1']; + getAppConfig.mockResolvedValue({ + filteredTools: [], + includedTools: ['key1'], + }); mockCache.get.mockResolvedValue(null); filterUniquePlugins.mockReturnValue(mockPlugins); checkPluginAuth.mockReturnValue(false); @@ -480,8 +487,8 @@ describe('PluginController', () => { expect(responseData[0].authConfig).toEqual([]); }); - it('should handle req.app.locals with undefined filteredTools and includedTools', async () => { - mockReq.app = { locals: {} }; + it('should handle undefined filteredTools and includedTools', async () => { + getAppConfig.mockResolvedValue({}); mockCache.get.mockResolvedValue(null); filterUniquePlugins.mockReturnValue([]); checkPluginAuth.mockReturnValue(false); diff --git a/api/server/middleware/spec/validateImages.spec.js b/api/server/middleware/spec/validateImages.spec.js index 8b04ac931f..7411b5aec5 100644 --- a/api/server/middleware/spec/validateImages.spec.js +++ b/api/server/middleware/spec/validateImages.spec.js @@ -1,13 +1,18 @@ const jwt = require('jsonwebtoken'); const validateImageRequest = require('~/server/middleware/validateImageRequest'); +jest.mock('~/server/services/Config/getAppConfig', () => ({ + getAppConfig: jest.fn(), +})); + describe('validateImageRequest middleware', () => { let req, res, next; const validObjectId = '65cfb246f7ecadb8b1e8036b'; + const { getAppConfig } = require('~/server/services/Config/getAppConfig'); beforeEach(() => { + jest.clearAllMocks(); req = { - app: { locals: { secureImageLinks: true } }, headers: {}, originalUrl: '', }; @@ -17,79 +22,86 @@ describe('validateImageRequest middleware', () => { }; next = jest.fn(); process.env.JWT_REFRESH_SECRET = 'test-secret'; + + // Mock getAppConfig to return secureImageLinks: true by default + getAppConfig.mockResolvedValue({ + secureImageLinks: true, + }); }); afterEach(() => { jest.clearAllMocks(); }); - test('should call next() if secureImageLinks is false', () => { - req.app.locals.secureImageLinks = false; - validateImageRequest(req, res, next); + test('should call next() if secureImageLinks is false', async () => { + getAppConfig.mockResolvedValue({ + secureImageLinks: false, + }); + await validateImageRequest(req, res, next); expect(next).toHaveBeenCalled(); }); - test('should return 401 if refresh token is not provided', () => { - validateImageRequest(req, res, next); + test('should return 401 if refresh token is not provided', async () => { + await validateImageRequest(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.send).toHaveBeenCalledWith('Unauthorized'); }); - test('should return 403 if refresh token is invalid', () => { + test('should return 403 if refresh token is invalid', async () => { req.headers.cookie = 'refreshToken=invalid-token'; - validateImageRequest(req, res, next); + await validateImageRequest(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.send).toHaveBeenCalledWith('Access Denied'); }); - test('should return 403 if refresh token is expired', () => { + test('should return 403 if refresh token is expired', async () => { const expiredToken = jwt.sign( { id: validObjectId, exp: Math.floor(Date.now() / 1000) - 3600 }, process.env.JWT_REFRESH_SECRET, ); req.headers.cookie = `refreshToken=${expiredToken}`; - validateImageRequest(req, res, next); + await validateImageRequest(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.send).toHaveBeenCalledWith('Access Denied'); }); - test('should call next() for valid image path', () => { + test('should call next() for valid image path', async () => { const validToken = jwt.sign( { id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 }, process.env.JWT_REFRESH_SECRET, ); req.headers.cookie = `refreshToken=${validToken}`; req.originalUrl = `/images/${validObjectId}/example.jpg`; - validateImageRequest(req, res, next); + await validateImageRequest(req, res, next); expect(next).toHaveBeenCalled(); }); - test('should return 403 for invalid image path', () => { + test('should return 403 for invalid image path', async () => { const validToken = jwt.sign( { id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 }, process.env.JWT_REFRESH_SECRET, ); req.headers.cookie = `refreshToken=${validToken}`; req.originalUrl = '/images/65cfb246f7ecadb8b1e8036c/example.jpg'; // Different ObjectId - validateImageRequest(req, res, next); + await validateImageRequest(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.send).toHaveBeenCalledWith('Access Denied'); }); - test('should return 403 for invalid ObjectId format', () => { + test('should return 403 for invalid ObjectId format', async () => { const validToken = jwt.sign( { id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 }, process.env.JWT_REFRESH_SECRET, ); req.headers.cookie = `refreshToken=${validToken}`; req.originalUrl = '/images/123/example.jpg'; // Invalid ObjectId - validateImageRequest(req, res, next); + await validateImageRequest(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.send).toHaveBeenCalledWith('Access Denied'); }); // File traversal tests - test('should prevent file traversal attempts', () => { + test('should prevent file traversal attempts', async () => { const validToken = jwt.sign( { id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 }, process.env.JWT_REFRESH_SECRET, @@ -103,23 +115,23 @@ describe('validateImageRequest middleware', () => { `/images/${validObjectId}/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd`, ]; - traversalAttempts.forEach((attempt) => { + for (const attempt of traversalAttempts) { req.originalUrl = attempt; - validateImageRequest(req, res, next); + await validateImageRequest(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.send).toHaveBeenCalledWith('Access Denied'); jest.clearAllMocks(); - }); + } }); - test('should handle URL encoded characters in valid paths', () => { + test('should handle URL encoded characters in valid paths', async () => { const validToken = jwt.sign( { id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 }, process.env.JWT_REFRESH_SECRET, ); req.headers.cookie = `refreshToken=${validToken}`; req.originalUrl = `/images/${validObjectId}/image%20with%20spaces.jpg`; - validateImageRequest(req, res, next); + await validateImageRequest(req, res, next); expect(next).toHaveBeenCalled(); }); }); diff --git a/api/server/routes/files/multer.spec.js b/api/server/routes/files/multer.spec.js index 2fb9147aef..9089e5e931 100644 --- a/api/server/routes/files/multer.spec.js +++ b/api/server/routes/files/multer.spec.js @@ -23,6 +23,7 @@ jest.mock('~/server/services/Config', () => ({ }, }), ), + getAppConfig: jest.fn(), })); describe('Multer Configuration', () => { @@ -36,13 +37,6 @@ describe('Multer Configuration', () => { mockReq = { user: { id: 'test-user-123' }, - app: { - locals: { - paths: { - uploads: tempDir, - }, - }, - }, body: {}, originalUrl: '/api/files/upload', }; @@ -53,6 +47,14 @@ describe('Multer Configuration', () => { size: 1024, }; + // Mock getAppConfig to return paths + const { getAppConfig } = require('~/server/services/Config'); + getAppConfig.mockResolvedValue({ + paths: { + uploads: tempDir, + }, + }); + // Clear mocks jest.clearAllMocks(); }); @@ -79,7 +81,12 @@ describe('Multer Configuration', () => { it("should create directory recursively if it doesn't exist", (done) => { const deepPath = path.join(tempDir, 'deep', 'nested', 'path'); - mockReq.app.locals.paths.uploads = deepPath; + const { getAppConfig } = require('~/server/services/Config'); + getAppConfig.mockResolvedValue({ + paths: { + uploads: deepPath, + }, + }); const cb = jest.fn((err, destination) => { expect(err).toBeNull(); @@ -465,7 +472,12 @@ describe('Multer Configuration', () => { it('should handle file system errors when directory creation fails', (done) => { // Test with a non-existent parent directory to simulate fs issues const invalidPath = '/nonexistent/path/that/should/not/exist'; - mockReq.app.locals.paths.uploads = invalidPath; + const { getAppConfig } = require('~/server/services/Config'); + getAppConfig.mockResolvedValue({ + paths: { + uploads: invalidPath, + }, + }); try { // Call getDestination which should fail due to permission/path issues diff --git a/api/server/services/AppService.interface.spec.js b/api/server/services/AppService.interface.spec.js index 31c8d70f3f..e686b6d20a 100644 --- a/api/server/services/AppService.interface.spec.js +++ b/api/server/services/AppService.interface.spec.js @@ -32,15 +32,19 @@ jest.mock('./start/checks', () => ({ checkWebSearchConfig: jest.fn(), })); +jest.mock('./Config/getAppConfig', () => ({ + initializeAppConfig: jest.fn(), + getAppConfig: jest.fn(), +})); + const AppService = require('./AppService'); const { loadDefaultInterface } = require('./start/interface'); describe('AppService interface configuration', () => { - let app; let mockLoadCustomConfig; + const { initializeAppConfig } = require('./Config/getAppConfig'); beforeEach(() => { - app = { locals: {} }; jest.resetModules(); jest.clearAllMocks(); mockLoadCustomConfig = require('./Config/loadCustomConfig'); @@ -50,10 +54,16 @@ describe('AppService interface configuration', () => { mockLoadCustomConfig.mockResolvedValue({}); loadDefaultInterface.mockResolvedValue({ prompts: true, bookmarks: true }); - await AppService(app); + await AppService(); - expect(app.locals.interfaceConfig.prompts).toBe(true); - expect(app.locals.interfaceConfig.bookmarks).toBe(true); + expect(initializeAppConfig).toHaveBeenCalledWith( + expect.objectContaining({ + interfaceConfig: expect.objectContaining({ + prompts: true, + bookmarks: true, + }), + }), + ); expect(loadDefaultInterface).toHaveBeenCalled(); }); @@ -61,10 +71,16 @@ describe('AppService interface configuration', () => { mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: false, bookmarks: false } }); loadDefaultInterface.mockResolvedValue({ prompts: false, bookmarks: false }); - await AppService(app); + await AppService(); - expect(app.locals.interfaceConfig.prompts).toBe(false); - expect(app.locals.interfaceConfig.bookmarks).toBe(false); + expect(initializeAppConfig).toHaveBeenCalledWith( + expect.objectContaining({ + interfaceConfig: expect.objectContaining({ + prompts: false, + bookmarks: false, + }), + }), + ); expect(loadDefaultInterface).toHaveBeenCalled(); }); @@ -72,10 +88,18 @@ describe('AppService interface configuration', () => { mockLoadCustomConfig.mockResolvedValue({}); loadDefaultInterface.mockResolvedValue({}); - await AppService(app); + await AppService(); - expect(app.locals.interfaceConfig.prompts).toBeUndefined(); - expect(app.locals.interfaceConfig.bookmarks).toBeUndefined(); + expect(initializeAppConfig).toHaveBeenCalledWith( + expect.objectContaining({ + interfaceConfig: expect.anything(), + }), + ); + + // Verify that prompts and bookmarks are undefined when not provided + const initCall = initializeAppConfig.mock.calls[0][0]; + expect(initCall.interfaceConfig.prompts).toBeUndefined(); + expect(initCall.interfaceConfig.bookmarks).toBeUndefined(); expect(loadDefaultInterface).toHaveBeenCalled(); }); @@ -83,10 +107,16 @@ describe('AppService interface configuration', () => { mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: true, bookmarks: false } }); loadDefaultInterface.mockResolvedValue({ prompts: true, bookmarks: false }); - await AppService(app); + await AppService(); - expect(app.locals.interfaceConfig.prompts).toBe(true); - expect(app.locals.interfaceConfig.bookmarks).toBe(false); + expect(initializeAppConfig).toHaveBeenCalledWith( + expect.objectContaining({ + interfaceConfig: expect.objectContaining({ + prompts: true, + bookmarks: false, + }), + }), + ); expect(loadDefaultInterface).toHaveBeenCalled(); }); @@ -108,14 +138,19 @@ describe('AppService interface configuration', () => { }, }); - await AppService(app); + await AppService(); - expect(app.locals.interfaceConfig.peoplePicker).toBeDefined(); - expect(app.locals.interfaceConfig.peoplePicker).toMatchObject({ - users: true, - groups: true, - roles: true, - }); + expect(initializeAppConfig).toHaveBeenCalledWith( + expect.objectContaining({ + interfaceConfig: expect.objectContaining({ + peoplePicker: expect.objectContaining({ + users: true, + groups: true, + roles: true, + }), + }), + }), + ); expect(loadDefaultInterface).toHaveBeenCalled(); }); @@ -137,11 +172,19 @@ describe('AppService interface configuration', () => { }, }); - await AppService(app); + await AppService(); - expect(app.locals.interfaceConfig.peoplePicker.users).toBe(true); - expect(app.locals.interfaceConfig.peoplePicker.groups).toBe(false); - expect(app.locals.interfaceConfig.peoplePicker.roles).toBe(true); + expect(initializeAppConfig).toHaveBeenCalledWith( + expect.objectContaining({ + interfaceConfig: expect.objectContaining({ + peoplePicker: expect.objectContaining({ + users: true, + groups: false, + roles: true, + }), + }), + }), + ); }); it('should set default peoplePicker permissions when not provided', async () => { @@ -154,11 +197,18 @@ describe('AppService interface configuration', () => { }, }); - await AppService(app); + await AppService(); - expect(app.locals.interfaceConfig.peoplePicker).toBeDefined(); - expect(app.locals.interfaceConfig.peoplePicker.users).toBe(true); - expect(app.locals.interfaceConfig.peoplePicker.groups).toBe(true); - expect(app.locals.interfaceConfig.peoplePicker.roles).toBe(true); + expect(initializeAppConfig).toHaveBeenCalledWith( + expect.objectContaining({ + interfaceConfig: expect.objectContaining({ + peoplePicker: expect.objectContaining({ + users: true, + groups: true, + roles: true, + }), + }), + }), + ); }); }); diff --git a/api/server/services/Config/loadConfigModels.spec.js b/api/server/services/Config/loadConfigModels.spec.js index e7199c59de..f0fbcfed1a 100644 --- a/api/server/services/Config/loadConfigModels.spec.js +++ b/api/server/services/Config/loadConfigModels.spec.js @@ -1,9 +1,11 @@ const { fetchModels } = require('~/server/services/ModelService'); const { getCustomConfig } = require('./getCustomConfig'); +const { getAppConfig } = require('./getAppConfig'); const loadConfigModels = require('./loadConfigModels'); jest.mock('~/server/services/ModelService'); jest.mock('./getCustomConfig'); +jest.mock('./getAppConfig'); const exampleConfig = { endpoints: { @@ -60,7 +62,7 @@ const exampleConfig = { }; describe('loadConfigModels', () => { - const mockRequest = { app: { locals: {} }, user: { id: 'testUserId' } }; + const mockRequest = { user: { id: 'testUserId' } }; const originalEnv = process.env; @@ -68,6 +70,9 @@ describe('loadConfigModels', () => { jest.resetAllMocks(); jest.resetModules(); process.env = { ...originalEnv }; + + // Default mock for getAppConfig + getAppConfig.mockResolvedValue({}); }); afterEach(() => { @@ -81,7 +86,9 @@ describe('loadConfigModels', () => { }); it('handles azure models and endpoint correctly', async () => { - mockRequest.app.locals.azureOpenAI = { modelNames: ['model1', 'model2'] }; + getAppConfig.mockResolvedValue({ + azureOpenAI: { modelNames: ['model1', 'model2'] }, + }); getCustomConfig.mockResolvedValue({ endpoints: { azureOpenAI: { diff --git a/packages/api/src/agents/resources.test.ts b/packages/api/src/agents/resources.test.ts index 0877ab141e..e5215c9ccb 100644 --- a/packages/api/src/agents/resources.test.ts +++ b/packages/api/src/agents/resources.test.ts @@ -3,6 +3,7 @@ import { logger } from '@librechat/data-schemas'; import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-data-provider'; import type { TAgentsEndpoint, TFile } from 'librechat-data-provider'; import type { Request as ServerRequest } from 'express'; +import type { IUser } from '@librechat/data-schemas'; import type { TGetFiles } from './resources'; import type { AppConfig } from '~/types'; @@ -14,7 +15,7 @@ jest.mock('@librechat/data-schemas', () => ({ })); describe('primeResources', () => { - let mockReq: ServerRequest; + let mockReq: ServerRequest & { user?: IUser }; let mockAppConfig: AppConfig; let mockGetFiles: jest.MockedFunction; let requestFileSet: Set; @@ -24,15 +25,7 @@ describe('primeResources', () => { jest.clearAllMocks(); // Setup mock request - mockReq = { - app: { - locals: { - [EModelEndpoint.agents]: { - capabilities: [AgentCapabilities.ocr], - }, - }, - }, - } as unknown as ServerRequest; + mockReq = {} as unknown as ServerRequest & { user?: IUser }; // Setup mock appConfig mockAppConfig = { @@ -94,7 +87,6 @@ describe('primeResources', () => { describe('when OCR is disabled', () => { it('should not fetch OCR files even if tool_resources has OCR file_ids', async () => { - (mockReq.app as ServerRequest['app']).locals[EModelEndpoint.agents].capabilities = []; (mockAppConfig[EModelEndpoint.agents] as TAgentsEndpoint).capabilities = []; const tool_resources = { @@ -956,8 +948,8 @@ describe('primeResources', () => { }); describe('edge cases', () => { - it('should handle missing app.locals gracefully', async () => { - const reqWithoutLocals = {} as ServerRequest; + it('should handle missing appConfig agents endpoint gracefully', async () => { + const reqWithoutLocals = {} as ServerRequest & { user?: IUser }; const emptyAppConfig = {} as AppConfig; const result = await primeResources({ @@ -974,9 +966,9 @@ describe('primeResources', () => { }); expect(mockGetFiles).not.toHaveBeenCalled(); - // When app.locals is missing and there's an error accessing properties, - // the function falls back to the catch block which returns an empty array - expect(result.attachments).toEqual([]); + // When appConfig agents endpoint is missing, OCR is disabled + // and no attachments are provided, the function returns undefined + expect(result.attachments).toBeUndefined(); }); it('should handle undefined tool_resources', async () => {