📏 refactor: Add File Size Limits to Conversation Imports (#12221)

* fix: add file size limits to conversation import multer instance

* fix: address review findings for conversation import file size limits

* fix: use local jest.mock for data-schemas instead of global moduleNameMapper

The global @librechat/data-schemas mock in jest.config.js only provided
logger, breaking all tests that depend on createModels from the same
package. Replace with a virtual jest.mock scoped to the import spec file.

* fix: move import to top of file, pre-compute upload middleware, assert logger.warn in tests

* refactor: move resolveImportMaxFileSize to packages/api

New backend logic belongs in packages/api as TypeScript. Delete the
api/server/utils/import/limits.js wrapper and import directly from
@librechat/api in convos.js and importConversations.js. Resolver unit
tests move to packages/api; the api/ spec retains only multer behavior
tests.

* chore: rename importLimits to import

* fix: stale type reference and mock isolation in import tests

Update typeof import path from '../importLimits' to '../import' after
the rename. Clear mockLogger.warn in beforeEach to prevent cross-test
accumulation.

* fix: add resolveImportMaxFileSize to @librechat/api mock in convos.spec.js

* fix: resolve jest.mock hoisting issue in import tests

jest.mock factories are hoisted above const declarations, so the
mockLogger reference was undefined at factory evaluation time. Use a
direct import of the mocked logger module instead.

* fix: remove virtual flag from data-schemas mock for CI compatibility

virtual: true prevents the mock from intercepting the real module in
CI where @librechat/data-schemas is built, causing import.ts to use
the real logger while the test asserts against the mock.
This commit is contained in:
Danny Avila 2026-03-14 03:06:29 -04:00 committed by GitHub
parent c6982dc180
commit 35a35dc2e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 223 additions and 7 deletions

View file

@ -0,0 +1,76 @@
jest.mock('@librechat/data-schemas', () => ({
logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
}));
import { DEFAULT_IMPORT_MAX_FILE_SIZE, resolveImportMaxFileSize } from '../import';
import { logger } from '@librechat/data-schemas';
describe('resolveImportMaxFileSize', () => {
let originalEnv: string | undefined;
beforeEach(() => {
originalEnv = process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES;
jest.clearAllMocks();
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = originalEnv;
} else {
delete process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES;
}
});
it('returns 262144000 (250 MiB) when env var is not set', () => {
delete process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES;
expect(resolveImportMaxFileSize()).toBe(262144000);
expect(DEFAULT_IMPORT_MAX_FILE_SIZE).toBe(262144000);
});
it('returns default when env var is empty string', () => {
process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '';
expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE);
});
it('respects a custom numeric value', () => {
process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '5242880';
expect(resolveImportMaxFileSize()).toBe(5242880);
});
it('parses string env var to number', () => {
process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '1048576';
expect(resolveImportMaxFileSize()).toBe(1048576);
});
it('falls back to default and warns for non-numeric string', () => {
process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = 'abc';
expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'),
);
});
it('falls back to default and warns for negative values', () => {
process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '-100';
expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'),
);
});
it('falls back to default and warns for zero', () => {
process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '0';
expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'),
);
});
it('falls back to default and warns for Infinity', () => {
process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = 'Infinity';
expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'),
);
});
});

View file

@ -0,0 +1,20 @@
import { logger } from '@librechat/data-schemas';
/** 250 MiB — default max file size for conversation imports */
export const DEFAULT_IMPORT_MAX_FILE_SIZE = 262144000;
/** Resolves the import file-size limit from the env var, falling back to the 250 MiB default */
export function resolveImportMaxFileSize(): number {
const raw = process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES;
if (!raw) {
return DEFAULT_IMPORT_MAX_FILE_SIZE;
}
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) {
logger.warn(
`[imports] Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES="${raw}"; using default ${DEFAULT_IMPORT_MAX_FILE_SIZE}`,
);
return DEFAULT_IMPORT_MAX_FILE_SIZE;
}
return parsed;
}

View file

@ -6,6 +6,7 @@ export * from './email';
export * from './env';
export * from './events';
export * from './files';
export * from './import';
export * from './generators';
export * from './graph';
export * from './path';