e115934061
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
179 lines
5.6 KiB
JavaScript
179 lines
5.6 KiB
JavaScript
const { ViolationTypes } = require('librechat-data-provider');
|
|
|
|
jest.mock('@librechat/api', () => ({
|
|
handleError: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('~/server/controllers/ModelController', () => ({
|
|
getModelsConfig: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('~/server/services/Config', () => ({
|
|
getEndpointsConfig: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('~/cache', () => ({
|
|
logViolation: jest.fn(),
|
|
}));
|
|
|
|
const { handleError } = require('@librechat/api');
|
|
const { getModelsConfig } = require('~/server/controllers/ModelController');
|
|
const { getEndpointsConfig } = require('~/server/services/Config');
|
|
const { logViolation } = require('~/cache');
|
|
const validateModel = require('../validateModel');
|
|
|
|
describe('validateModel', () => {
|
|
let req, res, next;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
req = { body: { model: 'gpt-4o', endpoint: 'openAI' } };
|
|
res = {};
|
|
next = jest.fn();
|
|
getEndpointsConfig.mockResolvedValue({
|
|
openAI: { userProvide: false },
|
|
});
|
|
getModelsConfig.mockResolvedValue({
|
|
openAI: ['gpt-4o', 'gpt-4o-mini'],
|
|
});
|
|
});
|
|
|
|
describe('format validation', () => {
|
|
it('rejects missing model', async () => {
|
|
req.body.model = undefined;
|
|
await validateModel(req, res, next);
|
|
expect(handleError).toHaveBeenCalledWith(res, { text: 'Model not provided' });
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects non-string model', async () => {
|
|
req.body.model = 12345;
|
|
await validateModel(req, res, next);
|
|
expect(handleError).toHaveBeenCalledWith(res, { text: 'Model not provided' });
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects model exceeding 256 chars', async () => {
|
|
req.body.model = 'a'.repeat(257);
|
|
await validateModel(req, res, next);
|
|
expect(handleError).toHaveBeenCalledWith(res, { text: 'Invalid model identifier' });
|
|
});
|
|
|
|
it('rejects model with leading special character', async () => {
|
|
req.body.model = '.bad-model';
|
|
await validateModel(req, res, next);
|
|
expect(handleError).toHaveBeenCalledWith(res, { text: 'Invalid model identifier' });
|
|
});
|
|
|
|
it('rejects model with script injection', async () => {
|
|
req.body.model = '<script>alert(1)</script>';
|
|
await validateModel(req, res, next);
|
|
expect(handleError).toHaveBeenCalledWith(res, { text: 'Invalid model identifier' });
|
|
});
|
|
|
|
it('trims whitespace before validation', async () => {
|
|
req.body.model = ' gpt-4o ';
|
|
getModelsConfig.mockResolvedValue({ openAI: ['gpt-4o'] });
|
|
await validateModel(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
expect(handleError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects model with spaces in the middle', async () => {
|
|
req.body.model = 'gpt 4o';
|
|
await validateModel(req, res, next);
|
|
expect(handleError).toHaveBeenCalledWith(res, { text: 'Invalid model identifier' });
|
|
});
|
|
|
|
it('accepts standard model IDs', async () => {
|
|
const validModels = [
|
|
'gpt-4o',
|
|
'claude-3-5-sonnet-20241022',
|
|
'us.amazon.nova-pro-v1:0',
|
|
'qwen/qwen3.6-plus-preview:free',
|
|
'Meta-Llama-3-8B-Instruct-4bit',
|
|
];
|
|
for (const model of validModels) {
|
|
jest.clearAllMocks();
|
|
req.body.model = model;
|
|
getEndpointsConfig.mockResolvedValue({ openAI: { userProvide: false } });
|
|
getModelsConfig.mockResolvedValue({ openAI: [model] });
|
|
next.mockClear();
|
|
|
|
await validateModel(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
expect(handleError).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('userProvide early-return', () => {
|
|
it('calls next() immediately for userProvide endpoints without checking model list', async () => {
|
|
getEndpointsConfig.mockResolvedValue({
|
|
openAI: { userProvide: true },
|
|
});
|
|
req.body.model = 'any-model-from-user-key';
|
|
|
|
await validateModel(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
expect(getModelsConfig).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not call getModelsConfig for userProvide endpoints', async () => {
|
|
getEndpointsConfig.mockResolvedValue({
|
|
CustomEndpoint: { userProvide: true },
|
|
});
|
|
req.body = { model: 'custom-model', endpoint: 'CustomEndpoint' };
|
|
|
|
await validateModel(req, res, next);
|
|
|
|
expect(getModelsConfig).not.toHaveBeenCalled();
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('system endpoint list validation', () => {
|
|
it('rejects a model not in the available list', async () => {
|
|
req.body.model = 'not-in-list';
|
|
|
|
await validateModel(req, res, next);
|
|
|
|
expect(logViolation).toHaveBeenCalledWith(
|
|
req,
|
|
res,
|
|
ViolationTypes.ILLEGAL_MODEL_REQUEST,
|
|
expect.any(Object),
|
|
expect.anything(),
|
|
);
|
|
expect(handleError).toHaveBeenCalledWith(res, { text: 'Illegal model request' });
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('accepts a model in the available list', async () => {
|
|
req.body.model = 'gpt-4o';
|
|
|
|
await validateModel(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
expect(handleError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects when endpoint has no models loaded', async () => {
|
|
getModelsConfig.mockResolvedValue({ openAI: undefined });
|
|
|
|
await validateModel(req, res, next);
|
|
|
|
expect(handleError).toHaveBeenCalledWith(res, { text: 'Endpoint models not loaded' });
|
|
});
|
|
|
|
it('rejects when modelsConfig is null', async () => {
|
|
getModelsConfig.mockResolvedValue(null);
|
|
|
|
await validateModel(req, res, next);
|
|
|
|
expect(handleError).toHaveBeenCalledWith(res, { text: 'Models not loaded' });
|
|
});
|
|
});
|
|
});
|