ci: Integrate getAppConfig into remaining tests

This commit is contained in:
Danny Avila 2025-08-17 17:32:53 -04:00
parent 5b43bf6c95
commit 5bb731764c
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
6 changed files with 164 additions and 84 deletions

View file

@ -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);

View file

@ -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();
});
});

View file

@ -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

View file

@ -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({
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,
}),
}),
}),
);
});
});

View file

@ -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: {

View file

@ -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<TGetFiles>;
let requestFileSet: Set<string>;
@ -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 () => {