2
0

minio.integration.test.ts 10 KB

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