minio.integration.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  2. import { MinioService } from '@/server/modules/files/minio.service';
  3. import { Client } from 'minio';
  4. import { logger } from '@/server/utils/logger';
  5. // Mock dependencies
  6. vi.mock('minio');
  7. vi.mock('@/server/utils/logger');
  8. // Mock process.env using vi.stubEnv for proper isolation
  9. beforeEach(() => {
  10. vi.stubEnv('MINIO_HOST', 'localhost');
  11. vi.stubEnv('MINIO_PORT', '9000');
  12. vi.stubEnv('MINIO_USE_SSL', 'false');
  13. vi.stubEnv('MINIO_ACCESS_KEY', 'minioadmin');
  14. vi.stubEnv('MINIO_SECRET_KEY', 'minioadmin');
  15. vi.stubEnv('MINIO_BUCKET_NAME', 'test-bucket');
  16. });
  17. afterEach(() => {
  18. vi.unstubAllEnvs();
  19. });
  20. describe('MinIO Integration Tests', () => {
  21. let minioService: MinioService;
  22. let mockClient: Client;
  23. beforeEach(() => {
  24. mockClient = new Client({} as any);
  25. (Client as any).mockClear();
  26. (Client as any).mockImplementation(() => mockClient);
  27. // Create MinioService with mock client
  28. minioService = new MinioService();
  29. });
  30. afterEach(() => {
  31. vi.clearAllMocks();
  32. });
  33. describe('Bucket Operations', () => {
  34. it('should ensure bucket exists and set policy', async () => {
  35. // Mock bucket doesn't exist
  36. mockClient.bucketExists = vi.fn().mockResolvedValue(false);
  37. mockClient.makeBucket = vi.fn().mockResolvedValue(undefined);
  38. mockClient.setBucketPolicy = vi.fn().mockResolvedValue(undefined);
  39. const result = await minioService.ensureBucketExists();
  40. expect(result).toBe(true);
  41. expect(mockClient.bucketExists).toHaveBeenCalledWith('test-bucket');
  42. expect(mockClient.makeBucket).toHaveBeenCalledWith('test-bucket');
  43. expect(mockClient.setBucketPolicy).toHaveBeenCalled();
  44. expect(logger.db).toHaveBeenCalledWith('Created new bucket: test-bucket');
  45. });
  46. it('should handle existing bucket', async () => {
  47. mockClient.bucketExists = vi.fn().mockResolvedValue(true);
  48. const result = await minioService.ensureBucketExists();
  49. expect(result).toBe(true);
  50. expect(mockClient.bucketExists).toHaveBeenCalledWith('test-bucket');
  51. expect(mockClient.makeBucket).not.toHaveBeenCalled();
  52. expect(mockClient.setBucketPolicy).not.toHaveBeenCalled();
  53. });
  54. });
  55. describe('File Operations', () => {
  56. it('should upload and download file successfully', async () => {
  57. const testContent = Buffer.from('Hello, MinIO!');
  58. const mockUrl = 'http://localhost:9000/test-bucket/test.txt';
  59. // Mock bucket operations
  60. mockClient.bucketExists = vi.fn().mockResolvedValue(true);
  61. mockClient.putObject = vi.fn().mockResolvedValue(undefined);
  62. mockClient.statObject = vi.fn().mockResolvedValue({ size: testContent.length } as any);
  63. mockClient.getObject = vi.fn().mockReturnValue({
  64. on: (event: string, callback: Function) => {
  65. if (event === 'data') callback(testContent);
  66. if (event === 'end') callback();
  67. }
  68. } as any);
  69. // Upload file
  70. const uploadUrl = await minioService.createObject('test-bucket', 'test.txt', testContent, 'text/plain');
  71. expect(uploadUrl).toBe(mockUrl);
  72. expect(mockClient.putObject).toHaveBeenCalledWith(
  73. 'test-bucket',
  74. 'test.txt',
  75. testContent,
  76. testContent.length,
  77. { 'Content-Type': 'text/plain' }
  78. );
  79. // Check file exists
  80. const exists = await minioService.objectExists('test-bucket', 'test.txt');
  81. expect(exists).toBe(true);
  82. expect(mockClient.statObject).toHaveBeenCalledWith('test-bucket', 'test.txt');
  83. });
  84. it('should handle file not found', async () => {
  85. const notFoundError = new Error('Object not found');
  86. notFoundError.message = 'not found';
  87. mockClient.statObject = vi.fn().mockRejectedValue(notFoundError);
  88. const exists = await minioService.objectExists('test-bucket', 'nonexistent.txt');
  89. expect(exists).toBe(false);
  90. });
  91. it('should delete file successfully', async () => {
  92. mockClient.removeObject = vi.fn().mockResolvedValue(undefined);
  93. await minioService.deleteObject('test-bucket', 'test.txt');
  94. expect(mockClient.removeObject).toHaveBeenCalledWith('test-bucket', 'test.txt');
  95. expect(logger.db).toHaveBeenCalledWith('Deleted object: test-bucket/test.txt');
  96. });
  97. });
  98. describe('Presigned URL Operations', () => {
  99. it('should generate presigned URLs correctly', async () => {
  100. const mockPresignedUrl = 'https://minio.example.com/presigned-url';
  101. mockClient.presignedGetObject = vi.fn().mockResolvedValue(mockPresignedUrl);
  102. // Test regular presigned URL
  103. const url = await minioService.getPresignedFileUrl('test-bucket', 'file.txt', 3600);
  104. expect(url).toBe(mockPresignedUrl);
  105. expect(mockClient.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'file.txt', 3600);
  106. // Test download URL with content disposition
  107. const downloadUrl = await minioService.getPresignedFileDownloadUrl(
  108. 'test-bucket',
  109. 'file.txt',
  110. '测试文件.txt',
  111. 1800
  112. );
  113. expect(downloadUrl).toBe(mockPresignedUrl);
  114. expect(mockClient.presignedGetObject).toHaveBeenCalledWith(
  115. 'test-bucket',
  116. 'file.txt',
  117. 1800,
  118. {
  119. 'response-content-disposition': 'attachment; filename="%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.txt"',
  120. 'response-content-type': 'application/octet-stream'
  121. }
  122. );
  123. });
  124. });
  125. describe('Multipart Upload Operations', () => {
  126. it('should handle multipart upload workflow', async () => {
  127. const mockUploadId = 'upload-123';
  128. const mockPartUrls = ['url1', 'url2', 'url3'];
  129. const mockStat = { size: 3072 };
  130. // Mock multipart operations
  131. mockClient.initiateNewMultipartUpload = vi.fn().mockResolvedValue(mockUploadId);
  132. mockClient.presignedUrl = vi.fn()
  133. .mockResolvedValueOnce('url1')
  134. .mockResolvedValueOnce('url2')
  135. .mockResolvedValueOnce('url3');
  136. mockClient.completeMultipartUpload = vi.fn().mockResolvedValue(undefined);
  137. mockClient.statObject = vi.fn().mockResolvedValue(mockStat as any);
  138. // Create multipart upload
  139. const uploadId = await minioService.createMultipartUpload('test-bucket', 'large-file.zip');
  140. expect(uploadId).toBe(mockUploadId);
  141. expect(mockClient.initiateNewMultipartUpload).toHaveBeenCalledWith(
  142. 'test-bucket',
  143. 'large-file.zip',
  144. {}
  145. );
  146. // Generate part URLs
  147. const partUrls = await minioService.generateMultipartUploadUrls(
  148. 'test-bucket',
  149. 'large-file.zip',
  150. mockUploadId,
  151. 3
  152. );
  153. expect(partUrls).toEqual(mockPartUrls);
  154. expect(mockClient.presignedUrl).toHaveBeenCalledTimes(3);
  155. // Complete multipart upload
  156. const parts = [
  157. { ETag: 'etag1', PartNumber: 1 },
  158. { ETag: 'etag2', PartNumber: 2 },
  159. { ETag: 'etag3', PartNumber: 3 }
  160. ];
  161. const result = await minioService.completeMultipartUpload(
  162. 'test-bucket',
  163. 'large-file.zip',
  164. mockUploadId,
  165. parts
  166. );
  167. expect(result).toEqual({ size: 3072 });
  168. expect(mockClient.completeMultipartUpload).toHaveBeenCalledWith(
  169. 'test-bucket',
  170. 'large-file.zip',
  171. mockUploadId,
  172. [{ part: 1, etag: 'etag1' }, { part: 2, etag: 'etag2' }, { part: 3, etag: 'etag3' }]
  173. );
  174. });
  175. });
  176. describe('Error Handling', () => {
  177. it('should handle MinIO connection errors', async () => {
  178. const connectionError = new Error('Connection refused');
  179. mockClient.bucketExists = vi.fn().mockRejectedValue(connectionError);
  180. await expect(minioService.ensureBucketExists()).rejects.toThrow(connectionError);
  181. expect(logger.error).toHaveBeenCalledWith(
  182. 'Failed to ensure bucket exists: test-bucket',
  183. connectionError
  184. );
  185. });
  186. it('should handle file operation errors', async () => {
  187. const operationError = new Error('Operation failed');
  188. // 确保桶存在成功
  189. mockClient.bucketExists = vi.fn().mockResolvedValue(true);
  190. // 但文件操作失败
  191. mockClient.putObject = vi.fn().mockRejectedValue(operationError);
  192. await expect(minioService.createObject(
  193. 'test-bucket',
  194. 'test.txt',
  195. Buffer.from('test'),
  196. 'text/plain'
  197. )).rejects.toThrow(operationError);
  198. expect(logger.error).toHaveBeenCalledWith(
  199. 'Failed to create object test-bucket/test.txt:',
  200. operationError
  201. );
  202. });
  203. it('should handle permission errors gracefully', async () => {
  204. const permissionError = new Error('Permission denied');
  205. mockClient.statObject = vi.fn().mockRejectedValue(permissionError);
  206. await expect(minioService.objectExists('test-bucket', 'file.txt')).rejects.toThrow(permissionError);
  207. expect(logger.error).toHaveBeenCalledWith(
  208. 'Error checking existence of object test-bucket/file.txt:',
  209. permissionError
  210. );
  211. });
  212. });
  213. describe('Configuration Validation', () => {
  214. it('should validate MinIO configuration', () => {
  215. expect(minioService.bucketName).toBe('test-bucket');
  216. // Test URL generation with different configurations
  217. const url = minioService.getFileUrl('test-bucket', 'file.txt');
  218. expect(url).toBe('http://localhost:9000/test-bucket/file.txt');
  219. });
  220. it('should handle SSL configuration', async () => {
  221. // Create new instance with SSL
  222. vi.stubEnv('MINIO_USE_SSL', 'true');
  223. vi.stubEnv('MINIO_PORT', '443');
  224. const sslService = new MinioService();
  225. const url = sslService.getFileUrl('test-bucket', 'file.txt');
  226. expect(url).toBe('https://localhost:443/test-bucket/file.txt');
  227. });
  228. });
  229. describe('Performance Testing', () => {
  230. it('should handle concurrent operations', async () => {
  231. mockClient.presignedGetObject = vi.fn().mockResolvedValue('https://minio.example.com/file');
  232. // Test concurrent URL generation with smaller concurrency
  233. const promises = Array(5).fill(0).map((_, i) =>
  234. minioService.getPresignedFileUrl('test-bucket', `file${i}.txt`)
  235. );
  236. const results = await Promise.all(promises);
  237. expect(results).toHaveLength(5);
  238. expect(results.every(url => url === 'https://minio.example.com/file')).toBe(true);
  239. });
  240. it('should handle large file operations', async () => {
  241. // Use smaller buffer size to avoid memory issues
  242. const largeBuffer = Buffer.alloc(1 * 1024 * 1024); // 1MB instead of 10MB
  243. mockClient.bucketExists = vi.fn().mockResolvedValue(true);
  244. mockClient.putObject = vi.fn().mockResolvedValue({ etag: 'etag123', versionId: null });
  245. await minioService.createObject('test-bucket', 'large-file.bin', largeBuffer, 'application/octet-stream');
  246. expect(mockClient.putObject).toHaveBeenCalledWith(
  247. 'test-bucket',
  248. 'large-file.bin',
  249. largeBuffer,
  250. largeBuffer.length,
  251. { 'Content-Type': 'application/octet-stream' }
  252. );
  253. });
  254. });
  255. });