🗂️ refactor: Migrate S3 Storage to TypeScript in packages/api (#11947)

* Migrate S3 storage module with unit and integration tests

  - Migrate S3 CRUD and image operations to packages/api/src/storage/s3/
  - Add S3ImageService class with dependency injection
  - Add unit tests using aws-sdk-client-mock
  - Add integration tests with real s3 bucket (condition presence of  AWS_TEST_BUCKET_NAME)

* AI Review Findings Fixes

* chore: tests and refactor S3 storage types

- Added mock implementations for the 'sharp' library in various test files to improve image processing testing.
- Updated type references in S3 storage files from MongoFile to TFile for consistency and type safety.
- Refactored S3 CRUD operations to ensure proper handling of file types and improve code clarity.
- Enhanced integration tests to validate S3 file operations and error handling more effectively.

* chore: rename test file

* Remove duplicate import of refreshS3Url

* chore: imports order

* fix: remove duplicate imports for S3 URL handling in UserController

* fix: remove duplicate import of refreshS3FileUrls in files.js

* test: Add mock implementations for 'sharp' and '@librechat/api' in UserController tests

- Introduced mock functions for the 'sharp' library to facilitate image processing tests, including metadata retrieval and buffer conversion.
- Enhanced mocking for '@librechat/api' to ensure consistent behavior in tests, particularly for the needsRefresh and getNewS3URL functions.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Atef Bellaaj 2026-03-09 20:42:01 +01:00 committed by Danny Avila
parent 428ef2eb15
commit ca6ce8fceb
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
27 changed files with 2455 additions and 1697 deletions

View file

@ -3,6 +3,7 @@ const fs = require('fs').promises;
const { nanoid } = require('nanoid');
const { logger } = require('@librechat/data-schemas');
const {
refreshS3Url,
agentCreateSchema,
agentUpdateSchema,
refreshListAvatars,
@ -33,7 +34,6 @@ const {
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
const { getFileStrategy } = require('~/server/utils/getFileStrategy');
const { refreshS3Url } = require('~/server/services/Files/S3/crud');
const { filterFile } = require('~/server/services/Files/process');
const { getCachedTools } = require('~/server/services/Config');
const { getLogStores } = require('~/cache');

View file

@ -22,7 +22,16 @@ jest.mock('~/server/services/Files/images/avatar', () => ({
resizeAvatar: jest.fn(),
}));
jest.mock('~/server/services/Files/S3/crud', () => ({
jest.mock('sharp', () =>
jest.fn(() => ({
metadata: jest.fn().mockResolvedValue({}),
toFormat: jest.fn().mockReturnThis(),
toBuffer: jest.fn().mockResolvedValue(Buffer.alloc(0)),
})),
);
jest.mock('@librechat/api', () => ({
...jest.requireActual('@librechat/api'),
refreshS3Url: jest.fn(),
}));
@ -72,7 +81,7 @@ const {
findPubliclyAccessibleResources,
} = require('~/server/services/PermissionService');
const { refreshS3Url } = require('~/server/services/Files/S3/crud');
const { refreshS3Url } = require('@librechat/api');
/**
* @type {import('mongoose').Model<import('@librechat/data-schemas').IAgent>}