mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00

* WIP: app.locals refactoring
WIP: appConfig
fix: update memory configuration retrieval to use getAppConfig based on user role
fix: update comment for AppConfig interface to clarify purpose
🏷️ refactor: Update tests to use getAppConfig for endpoint configurations
ci: Update AppService tests to initialize app config instead of app.locals
ci: Integrate getAppConfig into remaining tests
refactor: Update multer storage destination to use promise-based getAppConfig and improve error handling in tests
refactor: Rename initializeAppConfig to setAppConfig and update related tests
ci: Mock getAppConfig in various tests to provide default configurations
refactor: Update convertMCPToolsToPlugins to use mcpManager for server configuration and adjust related tests
chore: rename `Config/getAppConfig` -> `Config/app`
fix: streamline OpenAI image tools configuration by removing direct appConfig dependency and using function parameters
chore: correct parameter documentation for imageOutputType in ToolService.js
refactor: remove `getCustomConfig` dependency in config route
refactor: update domain validation to use appConfig for allowed domains
refactor: use appConfig registration property
chore: remove app parameter from AppService invocation
refactor: update AppConfig interface to correct registration and turnstile configurations
refactor: remove getCustomConfig dependency and use getAppConfig in PluginController, multer, and MCP services
refactor: replace getCustomConfig with getAppConfig in STTService, TTSService, and related files
refactor: replace getCustomConfig with getAppConfig in Conversation and Message models, update tempChatRetention functions to use AppConfig type
refactor: update getAppConfig calls in Conversation and Message models to include user role for temporary chat expiration
ci: update related tests
refactor: update getAppConfig call in getCustomConfigSpeech to include user role
fix: update appConfig usage to access allowedDomains from actions instead of registration
refactor: enhance AppConfig to include fileStrategies and update related file strategy logic
refactor: update imports to use normalizeEndpointName from @librechat/api and remove redundant definitions
chore: remove deprecated unused RunManager
refactor: get balance config primarily from appConfig
refactor: remove customConfig dependency for appConfig and streamline loadConfigModels logic
refactor: remove getCustomConfig usage and use app config in file citations
refactor: consolidate endpoint loading logic into loadEndpoints function
refactor: update appConfig access to use endpoints structure across various services
refactor: implement custom endpoints configuration and streamline endpoint loading logic
refactor: update getAppConfig call to include user role parameter
refactor: streamline endpoint configuration and enhance appConfig usage across services
refactor: replace getMCPAuthMap with getUserMCPAuthMap and remove unused getCustomConfig file
refactor: add type annotation for loadedEndpoints in loadEndpoints function
refactor: move /services/Files/images/parse to TS API
chore: add missing FILE_CITATIONS permission to IRole interface
refactor: restructure toolkits to TS API
refactor: separate manifest logic into its own module
refactor: consolidate tool loading logic into a new tools module for startup logic
refactor: move interface config logic to TS API
refactor: migrate checkEmailConfig to TypeScript and update imports
refactor: add FunctionTool interface and availableTools to AppConfig
refactor: decouple caching and DB operations from AppService, make part of consolidated `getAppConfig`
WIP: fix tests
* fix: rebase conflicts
* refactor: remove app.locals references
* refactor: replace getBalanceConfig with getAppConfig in various strategies and middleware
* refactor: replace appConfig?.balance with getBalanceConfig in various controllers and clients
* test: add balance configuration to titleConvo method in AgentClient tests
* chore: remove unused `openai-chat-tokens` package
* chore: remove unused imports in initializeMCPs.js
* refactor: update balance configuration to use getAppConfig instead of getBalanceConfig
* refactor: integrate configMiddleware for centralized configuration handling
* refactor: optimize email domain validation by removing unnecessary async calls
* refactor: simplify multer storage configuration by removing async calls
* refactor: reorder imports for better readability in user.js
* refactor: replace getAppConfig calls with req.config for improved performance
* chore: replace getAppConfig calls with req.config in tests for centralized configuration handling
* chore: remove unused override config
* refactor: add configMiddleware to endpoint route and replace getAppConfig with req.config
* chore: remove customConfig parameter from TTSService constructor
* refactor: pass appConfig from request to processFileCitations for improved configuration handling
* refactor: remove configMiddleware from endpoint route and retrieve appConfig directly in getEndpointsConfig if not in `req.config`
* test: add mockAppConfig to processFileCitations tests for improved configuration handling
* fix: pass req.config to hasCustomUserVars and call without await after synchronous refactor
* fix: type safety in useExportConversation
* refactor: retrieve appConfig using getAppConfig in PluginController and remove configMiddleware from plugins route, to avoid always retrieving when plugins are cached
* chore: change `MongoUser` typedef to `IUser`
* fix: Add `user` and `config` fields to ServerRequest and update JSDoc type annotations from Express.Request to ServerRequest
* fix: remove unused setAppConfig mock from Server configuration tests
549 lines
17 KiB
JavaScript
549 lines
17 KiB
JavaScript
/* eslint-disable no-unused-vars */
|
|
/* eslint-disable jest/no-done-callback */
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const { createMulterInstance, storage, importFileFilter } = require('./multer');
|
|
|
|
// Mock only the config service that requires external dependencies
|
|
jest.mock('~/server/services/Config', () => ({
|
|
getAppConfig: jest.fn(),
|
|
}));
|
|
|
|
describe('Multer Configuration', () => {
|
|
let tempDir;
|
|
let mockReq;
|
|
let mockFile;
|
|
|
|
beforeEach(() => {
|
|
// Create a temporary directory for each test
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'multer-test-'));
|
|
|
|
mockReq = {
|
|
user: { id: 'test-user-123' },
|
|
body: {},
|
|
originalUrl: '/api/files/upload',
|
|
config: {
|
|
paths: {
|
|
uploads: tempDir,
|
|
},
|
|
},
|
|
};
|
|
|
|
mockFile = {
|
|
originalname: 'test-file.jpg',
|
|
mimetype: 'image/jpeg',
|
|
size: 1024,
|
|
};
|
|
|
|
// Clear mocks
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up temporary directory
|
|
if (fs.existsSync(tempDir)) {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('Storage Configuration', () => {
|
|
describe('destination function', () => {
|
|
it('should create the correct destination path', (done) => {
|
|
const cb = jest.fn((err, destination) => {
|
|
expect(err).toBeNull();
|
|
expect(destination).toBe(path.join(tempDir, 'temp', 'test-user-123'));
|
|
expect(fs.existsSync(destination)).toBe(true);
|
|
done();
|
|
});
|
|
|
|
storage.getDestination(mockReq, mockFile, cb);
|
|
});
|
|
|
|
it("should create directory recursively if it doesn't exist", (done) => {
|
|
const deepPath = path.join(tempDir, 'deep', 'nested', 'path');
|
|
mockReq.config.paths.uploads = deepPath;
|
|
|
|
const cb = jest.fn((err, destination) => {
|
|
expect(err).toBeNull();
|
|
expect(destination).toBe(path.join(deepPath, 'temp', 'test-user-123'));
|
|
expect(fs.existsSync(destination)).toBe(true);
|
|
done();
|
|
});
|
|
|
|
storage.getDestination(mockReq, mockFile, cb);
|
|
});
|
|
});
|
|
|
|
describe('filename function', () => {
|
|
it('should generate a UUID for req.file_id', (done) => {
|
|
const cb = jest.fn((err, filename) => {
|
|
expect(err).toBeNull();
|
|
expect(mockReq.file_id).toBeDefined();
|
|
expect(mockReq.file_id).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
);
|
|
done();
|
|
});
|
|
|
|
storage.getFilename(mockReq, mockFile, cb);
|
|
});
|
|
|
|
it('should decode URI components in filename', (done) => {
|
|
const encodedFile = {
|
|
...mockFile,
|
|
originalname: encodeURIComponent('test file with spaces.jpg'),
|
|
};
|
|
|
|
const cb = jest.fn((err, filename) => {
|
|
expect(err).toBeNull();
|
|
expect(encodedFile.originalname).toBe('test file with spaces.jpg');
|
|
done();
|
|
});
|
|
|
|
storage.getFilename(mockReq, encodedFile, cb);
|
|
});
|
|
|
|
it('should call real sanitizeFilename with properly encoded filename', (done) => {
|
|
// Test with a properly URI-encoded filename that needs sanitization
|
|
const unsafeFile = {
|
|
...mockFile,
|
|
originalname: encodeURIComponent('test@#$%^&*()file with spaces!.jpg'),
|
|
};
|
|
|
|
const cb = jest.fn((err, filename) => {
|
|
expect(err).toBeNull();
|
|
// The actual sanitizeFilename should have cleaned this up after decoding
|
|
expect(filename).not.toContain('@');
|
|
expect(filename).not.toContain('#');
|
|
expect(filename).not.toContain('*');
|
|
expect(filename).not.toContain('!');
|
|
// Should still preserve dots and hyphens
|
|
expect(filename).toContain('.jpg');
|
|
done();
|
|
});
|
|
|
|
storage.getFilename(mockReq, unsafeFile, cb);
|
|
});
|
|
|
|
it('should handle very long filenames with actual crypto', (done) => {
|
|
const longFile = {
|
|
...mockFile,
|
|
originalname: 'a'.repeat(300) + '.jpg',
|
|
};
|
|
|
|
const cb = jest.fn((err, filename) => {
|
|
expect(err).toBeNull();
|
|
expect(filename.length).toBeLessThanOrEqual(255);
|
|
expect(filename).toMatch(/\.jpg$/); // Should still end with .jpg
|
|
// Should contain a hex suffix if truncated
|
|
if (filename.length === 255) {
|
|
expect(filename).toMatch(/-[a-f0-9]{6}\.jpg$/);
|
|
}
|
|
done();
|
|
});
|
|
|
|
storage.getFilename(mockReq, longFile, cb);
|
|
});
|
|
|
|
it('should generate unique file_id for each call', (done) => {
|
|
let firstFileId;
|
|
|
|
const firstCb = jest.fn((err, filename) => {
|
|
expect(err).toBeNull();
|
|
firstFileId = mockReq.file_id;
|
|
|
|
// Reset req for second call
|
|
delete mockReq.file_id;
|
|
|
|
const secondCb = jest.fn((err, filename) => {
|
|
expect(err).toBeNull();
|
|
expect(mockReq.file_id).toBeDefined();
|
|
expect(mockReq.file_id).not.toBe(firstFileId);
|
|
done();
|
|
});
|
|
|
|
storage.getFilename(mockReq, mockFile, secondCb);
|
|
});
|
|
|
|
storage.getFilename(mockReq, mockFile, firstCb);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Import File Filter', () => {
|
|
it('should accept JSON files by mimetype', (done) => {
|
|
const jsonFile = {
|
|
...mockFile,
|
|
mimetype: 'application/json',
|
|
originalname: 'data.json',
|
|
};
|
|
|
|
const cb = jest.fn((err, result) => {
|
|
expect(err).toBeNull();
|
|
expect(result).toBe(true);
|
|
done();
|
|
});
|
|
|
|
importFileFilter(mockReq, jsonFile, cb);
|
|
});
|
|
|
|
it('should accept files with .json extension', (done) => {
|
|
const jsonFile = {
|
|
...mockFile,
|
|
mimetype: 'text/plain',
|
|
originalname: 'data.json',
|
|
};
|
|
|
|
const cb = jest.fn((err, result) => {
|
|
expect(err).toBeNull();
|
|
expect(result).toBe(true);
|
|
done();
|
|
});
|
|
|
|
importFileFilter(mockReq, jsonFile, cb);
|
|
});
|
|
|
|
it('should reject non-JSON files', (done) => {
|
|
const textFile = {
|
|
...mockFile,
|
|
mimetype: 'text/plain',
|
|
originalname: 'document.txt',
|
|
};
|
|
|
|
const cb = jest.fn((err, result) => {
|
|
expect(err).toBeInstanceOf(Error);
|
|
expect(err.message).toBe('Only JSON files are allowed');
|
|
expect(result).toBe(false);
|
|
done();
|
|
});
|
|
|
|
importFileFilter(mockReq, textFile, cb);
|
|
});
|
|
|
|
it('should handle files with uppercase .JSON extension', (done) => {
|
|
const jsonFile = {
|
|
...mockFile,
|
|
mimetype: 'text/plain',
|
|
originalname: 'DATA.JSON',
|
|
};
|
|
|
|
const cb = jest.fn((err, result) => {
|
|
expect(err).toBeNull();
|
|
expect(result).toBe(true);
|
|
done();
|
|
});
|
|
|
|
importFileFilter(mockReq, jsonFile, cb);
|
|
});
|
|
});
|
|
|
|
describe('File Filter with Real defaultFileConfig', () => {
|
|
it('should use real fileConfig.checkType for validation', async () => {
|
|
// Test with actual librechat-data-provider functions
|
|
const {
|
|
fileConfig,
|
|
imageMimeTypes,
|
|
applicationMimeTypes,
|
|
} = require('librechat-data-provider');
|
|
|
|
// Test that the real checkType function works with regex patterns
|
|
expect(fileConfig.checkType('image/jpeg', [imageMimeTypes])).toBe(true);
|
|
expect(fileConfig.checkType('video/mp4', [imageMimeTypes])).toBe(false);
|
|
expect(fileConfig.checkType('application/pdf', [applicationMimeTypes])).toBe(true);
|
|
expect(fileConfig.checkType('application/pdf', [])).toBe(false);
|
|
});
|
|
|
|
it('should handle audio files for speech-to-text endpoint with real config', async () => {
|
|
mockReq.originalUrl = '/api/speech/stt';
|
|
|
|
const multerInstance = await createMulterInstance();
|
|
expect(multerInstance).toBeDefined();
|
|
expect(typeof multerInstance.single).toBe('function');
|
|
});
|
|
|
|
it('should reject unsupported file types using real config', async () => {
|
|
// Mock defaultFileConfig for this specific test
|
|
const originalCheckType = require('librechat-data-provider').fileConfig.checkType;
|
|
const mockCheckType = jest.fn().mockReturnValue(false);
|
|
require('librechat-data-provider').fileConfig.checkType = mockCheckType;
|
|
|
|
try {
|
|
const multerInstance = await createMulterInstance();
|
|
expect(multerInstance).toBeDefined();
|
|
|
|
// Test the actual file filter behavior would reject unsupported files
|
|
expect(mockCheckType).toBeDefined();
|
|
} finally {
|
|
// Restore original function
|
|
require('librechat-data-provider').fileConfig.checkType = originalCheckType;
|
|
}
|
|
});
|
|
|
|
it('should use real mergeFileConfig function', async () => {
|
|
const { mergeFileConfig, mbToBytes } = require('librechat-data-provider');
|
|
|
|
// Test with actual merge function - note that it converts MB to bytes
|
|
const testConfig = {
|
|
serverFileSizeLimit: 5, // 5 MB
|
|
endpoints: {
|
|
custom: {
|
|
supportedMimeTypes: ['text/plain'],
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = mergeFileConfig(testConfig);
|
|
|
|
// The function converts MB to bytes, so 5 MB becomes 5 * 1024 * 1024 bytes
|
|
expect(result.serverFileSizeLimit).toBe(mbToBytes(5));
|
|
expect(result.endpoints.custom.supportedMimeTypes).toBeDefined();
|
|
// Should still have the default endpoints
|
|
expect(result.endpoints.default).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('createMulterInstance with Real Functions', () => {
|
|
it('should create a multer instance with correct configuration', async () => {
|
|
const multerInstance = await createMulterInstance();
|
|
|
|
expect(multerInstance).toBeDefined();
|
|
expect(typeof multerInstance.single).toBe('function');
|
|
expect(typeof multerInstance.array).toBe('function');
|
|
expect(typeof multerInstance.fields).toBe('function');
|
|
});
|
|
|
|
it('should use real config merging', async () => {
|
|
const { getAppConfig } = require('~/server/services/Config');
|
|
|
|
const multerInstance = await createMulterInstance();
|
|
|
|
expect(getAppConfig).toHaveBeenCalled();
|
|
expect(multerInstance).toBeDefined();
|
|
});
|
|
|
|
it('should create multer instance with expected interface', async () => {
|
|
const multerInstance = await createMulterInstance();
|
|
|
|
expect(multerInstance).toBeDefined();
|
|
expect(typeof multerInstance.single).toBe('function');
|
|
expect(typeof multerInstance.array).toBe('function');
|
|
expect(typeof multerInstance.fields).toBe('function');
|
|
});
|
|
});
|
|
|
|
describe('Real Crypto Integration', () => {
|
|
it('should use actual crypto.randomUUID()', (done) => {
|
|
// Spy on crypto.randomUUID to ensure it's called
|
|
const uuidSpy = jest.spyOn(crypto, 'randomUUID');
|
|
|
|
const cb = jest.fn((err, filename) => {
|
|
expect(err).toBeNull();
|
|
expect(uuidSpy).toHaveBeenCalled();
|
|
expect(mockReq.file_id).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
);
|
|
|
|
uuidSpy.mockRestore();
|
|
done();
|
|
});
|
|
|
|
storage.getFilename(mockReq, mockFile, cb);
|
|
});
|
|
|
|
it('should generate different UUIDs on subsequent calls', (done) => {
|
|
const uuids = [];
|
|
let callCount = 0;
|
|
const totalCalls = 5;
|
|
|
|
const cb = jest.fn((err, filename) => {
|
|
expect(err).toBeNull();
|
|
uuids.push(mockReq.file_id);
|
|
callCount++;
|
|
|
|
if (callCount === totalCalls) {
|
|
// Check that all UUIDs are unique
|
|
const uniqueUuids = new Set(uuids);
|
|
expect(uniqueUuids.size).toBe(totalCalls);
|
|
done();
|
|
} else {
|
|
// Reset for next call
|
|
delete mockReq.file_id;
|
|
storage.getFilename(mockReq, mockFile, cb);
|
|
}
|
|
});
|
|
|
|
// Start the chain
|
|
storage.getFilename(mockReq, mockFile, cb);
|
|
});
|
|
|
|
it('should generate cryptographically secure UUIDs', (done) => {
|
|
const generatedUuids = new Set();
|
|
let callCount = 0;
|
|
const totalCalls = 10;
|
|
|
|
const cb = jest.fn((err, filename) => {
|
|
expect(err).toBeNull();
|
|
|
|
// Verify UUID format and uniqueness
|
|
expect(mockReq.file_id).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
);
|
|
|
|
generatedUuids.add(mockReq.file_id);
|
|
callCount++;
|
|
|
|
if (callCount === totalCalls) {
|
|
// All UUIDs should be unique
|
|
expect(generatedUuids.size).toBe(totalCalls);
|
|
done();
|
|
} else {
|
|
// Reset for next call
|
|
delete mockReq.file_id;
|
|
storage.getFilename(mockReq, mockFile, cb);
|
|
}
|
|
});
|
|
|
|
// Start the chain
|
|
storage.getFilename(mockReq, mockFile, cb);
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle CVE-2024-28870: empty field name DoS vulnerability', async () => {
|
|
// Test for the CVE where empty field name could cause unhandled exception
|
|
const multerInstance = await createMulterInstance();
|
|
|
|
// Create a mock request with empty field name (the vulnerability scenario)
|
|
const mockReqWithEmptyField = {
|
|
...mockReq,
|
|
headers: {
|
|
'content-type': 'multipart/form-data',
|
|
},
|
|
};
|
|
|
|
const mockRes = {
|
|
status: jest.fn().mockReturnThis(),
|
|
json: jest.fn(),
|
|
end: jest.fn(),
|
|
};
|
|
|
|
// This should not crash or throw unhandled exceptions
|
|
const uploadMiddleware = multerInstance.single(''); // Empty field name
|
|
|
|
const mockNext = jest.fn((err) => {
|
|
// If there's an error, it should be handled gracefully, not crash
|
|
if (err) {
|
|
expect(err).toBeInstanceOf(Error);
|
|
// The error should be handled, not crash the process
|
|
}
|
|
});
|
|
|
|
// This should complete without crashing the process
|
|
expect(() => {
|
|
uploadMiddleware(mockReqWithEmptyField, mockRes, mockNext);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should handle file system errors when directory creation fails', () => {
|
|
// Test with a non-existent parent directory to simulate fs issues
|
|
const invalidPath = '/nonexistent/path/that/should/not/exist';
|
|
mockReq.config.paths.uploads = invalidPath;
|
|
|
|
// The current implementation doesn't catch errors, so they're thrown synchronously
|
|
expect(() => {
|
|
storage.getDestination(mockReq, mockFile, jest.fn());
|
|
}).toThrow();
|
|
});
|
|
|
|
it('should handle malformed filenames with real sanitization', (done) => {
|
|
const malformedFile = {
|
|
...mockFile,
|
|
originalname: null, // This should be handled gracefully
|
|
};
|
|
|
|
const cb = jest.fn((err, filename) => {
|
|
// The function should handle this gracefully
|
|
expect(typeof err === 'object' || err === null).toBe(true);
|
|
done();
|
|
});
|
|
|
|
try {
|
|
storage.getFilename(mockReq, malformedFile, cb);
|
|
} catch (error) {
|
|
// If it throws, that's also acceptable behavior
|
|
done();
|
|
}
|
|
});
|
|
|
|
it('should handle edge cases in filename sanitization', (done) => {
|
|
const edgeCaseFiles = [
|
|
{ originalname: '', expected: /_/ },
|
|
{ originalname: '.hidden', expected: /^_\.hidden/ },
|
|
{ originalname: '../../../etc/passwd', expected: /passwd/ },
|
|
{ originalname: 'file\x00name.txt', expected: /file_name\.txt/ },
|
|
];
|
|
|
|
let testCount = 0;
|
|
|
|
const testNextFile = (fileData) => {
|
|
const fileToTest = { ...mockFile, originalname: fileData.originalname };
|
|
|
|
const cb = jest.fn((err, filename) => {
|
|
expect(err).toBeNull();
|
|
expect(filename).toMatch(fileData.expected);
|
|
|
|
testCount++;
|
|
if (testCount === edgeCaseFiles.length) {
|
|
done();
|
|
} else {
|
|
testNextFile(edgeCaseFiles[testCount]);
|
|
}
|
|
});
|
|
|
|
storage.getFilename(mockReq, fileToTest, cb);
|
|
};
|
|
|
|
testNextFile(edgeCaseFiles[0]);
|
|
});
|
|
});
|
|
|
|
describe('Real Configuration Testing', () => {
|
|
it('should handle missing custom config gracefully with real mergeFileConfig', async () => {
|
|
const { getAppConfig } = require('~/server/services/Config');
|
|
|
|
// Mock getAppConfig to return undefined
|
|
getAppConfig.mockResolvedValueOnce(undefined);
|
|
|
|
const multerInstance = await createMulterInstance();
|
|
expect(multerInstance).toBeDefined();
|
|
expect(typeof multerInstance.single).toBe('function');
|
|
});
|
|
|
|
it('should properly integrate real fileConfig with custom endpoints', async () => {
|
|
const { getAppConfig } = require('~/server/services/Config');
|
|
|
|
// Mock appConfig with fileConfig
|
|
getAppConfig.mockResolvedValueOnce({
|
|
paths: {
|
|
uploads: tempDir,
|
|
},
|
|
fileConfig: {
|
|
endpoints: {
|
|
anthropic: {
|
|
supportedMimeTypes: ['text/plain', 'image/png'],
|
|
},
|
|
},
|
|
serverFileSizeLimit: 20971520, // 20 MB in bytes (mergeFileConfig converts)
|
|
},
|
|
});
|
|
|
|
const multerInstance = await createMulterInstance();
|
|
expect(multerInstance).toBeDefined();
|
|
|
|
// Verify that getAppConfig was called
|
|
expect(getAppConfig).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|