@flusys/nestjs-storage 7.0.2 → 8.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/config/index.d.ts +3 -3
  2. package/controllers/file-manager.controller.d.ts +4 -4
  3. package/controllers/folder.controller.d.ts +2 -2
  4. package/controllers/index.d.ts +4 -4
  5. package/controllers/storage-config.controller.d.ts +2 -2
  6. package/controllers/upload.controller.d.ts +3 -3
  7. package/docs/index.d.ts +1 -1
  8. package/docs/storage-swagger.config.d.ts +2 -2
  9. package/dtos/index.d.ts +4 -4
  10. package/entities/file-manager-with-company.entity.d.ts +2 -2
  11. package/entities/file-manager.entity.d.ts +1 -1
  12. package/entities/folder-with-company.entity.d.ts +1 -1
  13. package/entities/index.d.ts +15 -15
  14. package/entities/storage-config-with-company.entity.d.ts +1 -1
  15. package/enums/index.d.ts +1 -1
  16. package/fesm/493.js +2768 -0
  17. package/fesm/537.js +1157 -0
  18. package/fesm/562.js +786 -0
  19. package/fesm/{providers/azure-provider.optional.js → chunks/190.js} +33 -30
  20. package/fesm/chunks/252.js +12 -0
  21. package/fesm/chunks/327.js +12 -0
  22. package/fesm/chunks/385.js +12 -0
  23. package/fesm/{providers/s3-provider.optional.js → chunks/591.js} +37 -34
  24. package/fesm/chunks/746.js +12 -0
  25. package/fesm/chunks/798.js +12 -0
  26. package/fesm/{providers/sftp-provider.optional.js → chunks/918.js} +35 -31
  27. package/fesm/config/index.js +119 -3
  28. package/fesm/controllers/index.js +16 -4
  29. package/fesm/docs/index.js +206 -1
  30. package/fesm/dtos/index.js +879 -4
  31. package/fesm/entities/index.js +463 -18
  32. package/fesm/enums/index.js +36 -1
  33. package/fesm/index.js +398 -12
  34. package/fesm/interfaces/index.js +19 -5
  35. package/fesm/modules/index.js +16 -1
  36. package/fesm/providers/index.js +364 -3
  37. package/fesm/runtime.js +252 -0
  38. package/fesm/services/index.js +16 -6
  39. package/fesm/utils/index.js +149 -1
  40. package/index.d.ts +11 -11
  41. package/interfaces/file-manager.interface.d.ts +1 -1
  42. package/interfaces/folder.interface.d.ts +1 -1
  43. package/interfaces/index.d.ts +5 -5
  44. package/interfaces/storage-config.interface.d.ts +1 -1
  45. package/interfaces/storage-module-options.interface.d.ts +3 -3
  46. package/interfaces/storage-provider.interface.d.ts +1 -1
  47. package/middlewares/file-serve.middleware.d.ts +4 -3
  48. package/middlewares/index.d.ts +1 -1
  49. package/modules/index.d.ts +1 -1
  50. package/modules/storage.module.d.ts +2 -2
  51. package/package.json +30 -41
  52. package/providers/azure-provider.optional.d.ts +2 -2
  53. package/providers/index.d.ts +3 -3
  54. package/providers/local-provider.d.ts +2 -2
  55. package/providers/s3-provider.optional.d.ts +2 -2
  56. package/providers/sftp-provider.optional.d.ts +2 -2
  57. package/providers/storage-factory.service.d.ts +3 -3
  58. package/providers/storage-provider.registry.d.ts +2 -2
  59. package/services/file-manager.service.d.ts +9 -8
  60. package/services/folder.service.d.ts +6 -6
  61. package/services/index.d.ts +6 -6
  62. package/services/storage-config.service.d.ts +3 -3
  63. package/services/storage-datasource.provider.d.ts +3 -3
  64. package/services/storage-provider-config.service.d.ts +7 -7
  65. package/services/upload.service.d.ts +6 -6
  66. package/utils/image-compressor.util.d.ts +1 -1
  67. package/utils/index.d.ts +1 -1
  68. package/cjs/config/event-actions.js +0 -34
  69. package/cjs/config/index.js +0 -20
  70. package/cjs/config/message-keys.js +0 -46
  71. package/cjs/config/storage.constants.js +0 -28
  72. package/cjs/controllers/file-manager.controller.js +0 -152
  73. package/cjs/controllers/file-manager.controller.spec.js +0 -135
  74. package/cjs/controllers/folder.controller.js +0 -102
  75. package/cjs/controllers/folder.controller.spec.js +0 -86
  76. package/cjs/controllers/index.js +0 -21
  77. package/cjs/controllers/storage-config.controller.js +0 -102
  78. package/cjs/controllers/storage-config.controller.spec.js +0 -85
  79. package/cjs/controllers/upload.controller.js +0 -234
  80. package/cjs/controllers/upload.controller.spec.js +0 -132
  81. package/cjs/docs/index.js +0 -11
  82. package/cjs/docs/storage-swagger.config.js +0 -188
  83. package/cjs/docs/storage-swagger.config.spec.js +0 -72
  84. package/cjs/dtos/file-manager.dto.js +0 -327
  85. package/cjs/dtos/folder.dto.js +0 -105
  86. package/cjs/dtos/index.js +0 -21
  87. package/cjs/dtos/storage-config.dto.js +0 -170
  88. package/cjs/dtos/upload.dto.js +0 -272
  89. package/cjs/entities/file-manager-with-company.entity.js +0 -61
  90. package/cjs/entities/file-manager.entity.js +0 -122
  91. package/cjs/entities/folder-with-company.entity.js +0 -55
  92. package/cjs/entities/folder.entity.js +0 -64
  93. package/cjs/entities/index.js +0 -63
  94. package/cjs/entities/index.spec.js +0 -18
  95. package/cjs/entities/storage-config-with-company.entity.js +0 -51
  96. package/cjs/entities/storage-config.entity.js +0 -96
  97. package/cjs/enums/file-location.enum.js +0 -17
  98. package/cjs/enums/index.js +0 -18
  99. package/cjs/index.js +0 -29
  100. package/cjs/interfaces/file-manager.interface.js +0 -4
  101. package/cjs/interfaces/folder.interface.js +0 -4
  102. package/cjs/interfaces/index.js +0 -22
  103. package/cjs/interfaces/storage-config.interface.js +0 -4
  104. package/cjs/interfaces/storage-module-options.interface.js +0 -4
  105. package/cjs/interfaces/storage-provider.interface.js +0 -4
  106. package/cjs/middlewares/file-serve.middleware.js +0 -241
  107. package/cjs/middlewares/file-serve.middleware.spec.js +0 -211
  108. package/cjs/middlewares/index.js +0 -18
  109. package/cjs/modules/index.js +0 -18
  110. package/cjs/modules/storage.module.js +0 -165
  111. package/cjs/modules/storage.module.spec.js +0 -184
  112. package/cjs/providers/azure-provider.optional.js +0 -155
  113. package/cjs/providers/azure-provider.optional.spec.js +0 -228
  114. package/cjs/providers/index.js +0 -26
  115. package/cjs/providers/local-provider.js +0 -230
  116. package/cjs/providers/local-provider.spec.js +0 -239
  117. package/cjs/providers/s3-provider.optional.js +0 -175
  118. package/cjs/providers/s3-provider.optional.spec.js +0 -243
  119. package/cjs/providers/sftp-provider.optional.js +0 -203
  120. package/cjs/providers/sftp-provider.optional.spec.js +0 -217
  121. package/cjs/providers/storage-factory.service.js +0 -175
  122. package/cjs/providers/storage-factory.service.spec.js +0 -253
  123. package/cjs/providers/storage-provider.registry.js +0 -41
  124. package/cjs/providers/storage-provider.registry.spec.js +0 -67
  125. package/cjs/services/file-manager.service.js +0 -387
  126. package/cjs/services/file-manager.service.spec.js +0 -580
  127. package/cjs/services/folder.service.js +0 -101
  128. package/cjs/services/folder.service.spec.js +0 -167
  129. package/cjs/services/index.js +0 -23
  130. package/cjs/services/storage-config.service.js +0 -129
  131. package/cjs/services/storage-config.service.spec.js +0 -257
  132. package/cjs/services/storage-datasource.provider.js +0 -137
  133. package/cjs/services/storage-datasource.provider.spec.js +0 -217
  134. package/cjs/services/storage-provider-config.service.js +0 -171
  135. package/cjs/services/storage-provider-config.service.spec.js +0 -265
  136. package/cjs/services/upload.service.js +0 -376
  137. package/cjs/services/upload.service.spec.js +0 -426
  138. package/cjs/utils/file-validator.util.js +0 -440
  139. package/cjs/utils/file-validator.util.spec.js +0 -259
  140. package/cjs/utils/image-compressor.util.js +0 -162
  141. package/cjs/utils/image-compressor.util.spec.js +0 -166
  142. package/cjs/utils/index.js +0 -18
  143. package/fesm/config/event-actions.js +0 -13
  144. package/fesm/config/message-keys.js +0 -22
  145. package/fesm/config/storage.constants.js +0 -7
  146. package/fesm/controllers/file-manager.controller.js +0 -142
  147. package/fesm/controllers/file-manager.controller.spec.js +0 -131
  148. package/fesm/controllers/folder.controller.js +0 -92
  149. package/fesm/controllers/folder.controller.spec.js +0 -82
  150. package/fesm/controllers/storage-config.controller.js +0 -92
  151. package/fesm/controllers/storage-config.controller.spec.js +0 -81
  152. package/fesm/controllers/upload.controller.js +0 -224
  153. package/fesm/controllers/upload.controller.spec.js +0 -128
  154. package/fesm/docs/storage-swagger.config.js +0 -181
  155. package/fesm/docs/storage-swagger.config.spec.js +0 -68
  156. package/fesm/dtos/file-manager.dto.js +0 -300
  157. package/fesm/dtos/folder.dto.js +0 -84
  158. package/fesm/dtos/storage-config.dto.js +0 -149
  159. package/fesm/dtos/upload.dto.js +0 -245
  160. package/fesm/entities/file-manager-with-company.entity.js +0 -51
  161. package/fesm/entities/file-manager.entity.js +0 -112
  162. package/fesm/entities/folder-with-company.entity.js +0 -45
  163. package/fesm/entities/folder.entity.js +0 -54
  164. package/fesm/entities/index.spec.js +0 -14
  165. package/fesm/entities/storage-config-with-company.entity.js +0 -41
  166. package/fesm/entities/storage-config.entity.js +0 -86
  167. package/fesm/enums/file-location.enum.js +0 -7
  168. package/fesm/interfaces/file-manager.interface.js +0 -1
  169. package/fesm/interfaces/folder.interface.js +0 -1
  170. package/fesm/interfaces/storage-config.interface.js +0 -1
  171. package/fesm/interfaces/storage-module-options.interface.js +0 -3
  172. package/fesm/interfaces/storage-provider.interface.js +0 -4
  173. package/fesm/middlewares/file-serve.middleware.js +0 -190
  174. package/fesm/middlewares/file-serve.middleware.spec.js +0 -207
  175. package/fesm/middlewares/index.js +0 -1
  176. package/fesm/modules/storage.module.js +0 -156
  177. package/fesm/modules/storage.module.spec.js +0 -180
  178. package/fesm/providers/azure-provider.optional.spec.js +0 -224
  179. package/fesm/providers/local-provider.js +0 -182
  180. package/fesm/providers/local-provider.spec.js +0 -194
  181. package/fesm/providers/s3-provider.optional.spec.js +0 -239
  182. package/fesm/providers/sftp-provider.optional.spec.js +0 -213
  183. package/fesm/providers/storage-factory.service.js +0 -124
  184. package/fesm/providers/storage-factory.service.spec.js +0 -249
  185. package/fesm/providers/storage-provider.registry.js +0 -34
  186. package/fesm/providers/storage-provider.registry.spec.js +0 -63
  187. package/fesm/services/file-manager.service.js +0 -336
  188. package/fesm/services/file-manager.service.spec.js +0 -576
  189. package/fesm/services/folder.service.js +0 -91
  190. package/fesm/services/folder.service.spec.js +0 -163
  191. package/fesm/services/storage-config.service.js +0 -119
  192. package/fesm/services/storage-config.service.spec.js +0 -253
  193. package/fesm/services/storage-datasource.provider.js +0 -86
  194. package/fesm/services/storage-datasource.provider.spec.js +0 -172
  195. package/fesm/services/storage-provider-config.service.js +0 -161
  196. package/fesm/services/storage-provider-config.service.spec.js +0 -261
  197. package/fesm/services/upload.service.js +0 -366
  198. package/fesm/services/upload.service.spec.js +0 -422
  199. package/fesm/utils/file-validator.util.js +0 -433
  200. package/fesm/utils/file-validator.util.spec.js +0 -255
  201. package/fesm/utils/image-compressor.util.js +0 -114
  202. package/fesm/utils/image-compressor.util.spec.js +0 -162
@@ -1,422 +0,0 @@
1
- import { Test } from '@nestjs/testing';
2
- import { EventBusRegistry } from '@flusys/nestjs-shared/classes';
3
- import { UploadService } from './upload.service';
4
- import { StorageFactoryService } from '../providers/storage-factory.service';
5
- import { StorageConfigService } from './storage-config.service';
6
- import { StorageProviderConfigService } from './storage-provider-config.service';
7
- import { FileLocationEnum } from '../enums/file-location.enum';
8
- import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
9
- function buildMulterFile(overrides = {}) {
10
- return {
11
- fieldname: 'file',
12
- originalname: 'photo.jpg',
13
- encoding: '7bit',
14
- mimetype: 'image/jpeg',
15
- buffer: Buffer.from([
16
- 0xff,
17
- 0xd8,
18
- 0xff,
19
- 0xe0
20
- ]),
21
- size: 1024,
22
- stream: undefined,
23
- destination: '',
24
- filename: '',
25
- path: '',
26
- ...overrides
27
- };
28
- }
29
- describe('UploadService', ()=>{
30
- let service;
31
- let mockStorageFactory;
32
- let mockStorageConfigService;
33
- let mockStorageProviderConfigService;
34
- let mockProvider;
35
- beforeEach(async ()=>{
36
- mockProvider = {
37
- uploadFile: jest.fn().mockResolvedValue({
38
- name: 'photo.jpg',
39
- key: 'uploads/photo.jpg',
40
- size: 1024,
41
- contentType: 'image/jpeg'
42
- }),
43
- uploadMultipleFiles: jest.fn().mockResolvedValue([]),
44
- deleteFile: jest.fn().mockResolvedValue(undefined),
45
- deleteMultipleFiles: jest.fn().mockResolvedValue(undefined),
46
- generatePresignedUrl: jest.fn().mockResolvedValue('https://signed.example.com/uploads/photo.jpg')
47
- };
48
- mockStorageFactory = {
49
- createProvider: jest.fn().mockResolvedValue(mockProvider),
50
- getLocalProviderBasePath: jest.fn().mockReturnValue(null)
51
- };
52
- mockStorageConfigService = {
53
- validateFileSize: jest.fn().mockReturnValue({
54
- valid: true
55
- }),
56
- validateFileType: jest.fn().mockReturnValue({
57
- valid: true
58
- }),
59
- getAllowedFileTypes: jest.fn().mockReturnValue([
60
- '*/*'
61
- ]),
62
- getDefaultLocalStoragePath: jest.fn().mockReturnValue('./uploads'),
63
- isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
64
- };
65
- mockStorageProviderConfigService = {
66
- findByIdDirect: jest.fn(),
67
- getDefaultConfig: jest.fn(),
68
- getConfigByType: jest.fn()
69
- };
70
- const module = await Test.createTestingModule({
71
- providers: [
72
- UploadService,
73
- {
74
- provide: StorageFactoryService,
75
- useValue: mockStorageFactory
76
- },
77
- {
78
- provide: StorageConfigService,
79
- useValue: mockStorageConfigService
80
- },
81
- {
82
- provide: StorageProviderConfigService,
83
- useValue: mockStorageProviderConfigService
84
- }
85
- ]
86
- }).compile();
87
- service = await module.resolve(UploadService);
88
- });
89
- describe('uploadSingleFile', ()=>{
90
- it('validates, resolves the default storage config, and uploads via the resolved provider', async ()=>{
91
- mockStorageProviderConfigService.getDefaultConfig.mockResolvedValue({
92
- id: 'cfg-default',
93
- storage: 'local',
94
- config: {}
95
- });
96
- const result = await service.uploadSingleFile(buildMulterFile(), {}, buildMockUser());
97
- expect(mockStorageConfigService.validateFileSize).toHaveBeenCalled();
98
- expect(mockStorageConfigService.validateFileType).toHaveBeenCalled();
99
- expect(mockProvider.uploadFile).toHaveBeenCalled();
100
- expect(result.location).toBe('local');
101
- expect(result.storageConfigId).toBe('cfg-default');
102
- });
103
- it('throws BadRequestException when file size exceeds the configured maximum', async ()=>{
104
- mockStorageConfigService.validateFileSize.mockReturnValue({
105
- valid: false,
106
- message: 'too big',
107
- messageKey: 'upload.file.too.large'
108
- });
109
- await expect(service.uploadSingleFile(buildMulterFile(), {}, buildMockUser())).rejects.toThrow();
110
- });
111
- it('throws BadRequestException when the declared MIME type is not allowed', async ()=>{
112
- mockStorageConfigService.validateFileType.mockReturnValue({
113
- valid: false,
114
- message: 'not allowed',
115
- messageKey: 'upload.invalid.type'
116
- });
117
- await expect(service.uploadSingleFile(buildMulterFile(), {}, buildMockUser())).rejects.toThrow();
118
- });
119
- it('rejects a file whose content does not match its declared MIME type (spoofing protection)', async ()=>{
120
- const file = buildMulterFile({
121
- mimetype: 'application/pdf',
122
- buffer: Buffer.from([
123
- 0xff,
124
- 0xd8,
125
- 0xff
126
- ])
127
- });
128
- await expect(service.uploadSingleFile(file, {}, buildMockUser())).rejects.toThrow();
129
- });
130
- it('sanitizes the original filename to prevent path traversal before uploading', async ()=>{
131
- mockStorageProviderConfigService.getDefaultConfig.mockResolvedValue({
132
- id: 'cfg-default',
133
- storage: 'local',
134
- config: {}
135
- });
136
- const file = buildMulterFile({
137
- originalname: '../../etc/passwd.jpg'
138
- });
139
- await service.uploadSingleFile(file, {}, buildMockUser());
140
- expect(file.originalname).toBe('passwd.jpg');
141
- });
142
- it('uses an explicit storageConfigId when provided and validates company ownership', async ()=>{
143
- mockStorageProviderConfigService.findByIdDirect.mockResolvedValue({
144
- id: 'cfg-1',
145
- storage: 'aws',
146
- config: {}
147
- });
148
- const result = await service.uploadSingleFile(buildMulterFile(), {
149
- storageConfigId: 'cfg-1'
150
- }, buildMockUser());
151
- expect(mockStorageProviderConfigService.findByIdDirect).toHaveBeenCalledWith('cfg-1');
152
- expect(result.location).toBe('aws');
153
- });
154
- it('throws NotFoundException when the explicit storageConfigId does not resolve to a config', async ()=>{
155
- mockStorageProviderConfigService.findByIdDirect.mockResolvedValue(null);
156
- await expect(service.uploadSingleFile(buildMulterFile(), {
157
- storageConfigId: 'missing'
158
- }, buildMockUser())).rejects.toThrow();
159
- });
160
- it('throws NotFoundException when no default storage config exists and none was specified', async ()=>{
161
- mockStorageProviderConfigService.getDefaultConfig.mockResolvedValue(null);
162
- await expect(service.uploadSingleFile(buildMulterFile(), {}, buildMockUser())).rejects.toThrow();
163
- });
164
- it('rejects uploading a config belonging to a different company when company feature is enabled', async ()=>{
165
- mockStorageConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
166
- mockStorageProviderConfigService.findByIdDirect.mockResolvedValue({
167
- id: 'cfg-1',
168
- storage: 'aws',
169
- config: {},
170
- companyId: 'other-company'
171
- });
172
- await expect(service.uploadSingleFile(buildMulterFile(), {
173
- storageConfigId: 'cfg-1'
174
- }, buildMockUser({
175
- companyId: 'my-company'
176
- }))).rejects.toThrow();
177
- });
178
- });
179
- describe('uploadMultipleFiles', ()=>{
180
- it('validates every file before uploading any', async ()=>{
181
- mockStorageProviderConfigService.getDefaultConfig.mockResolvedValue({
182
- id: 'cfg-default',
183
- storage: 'local',
184
- config: {}
185
- });
186
- mockProvider.uploadMultipleFiles.mockResolvedValue([
187
- {
188
- name: 'a.jpg',
189
- key: 'a.jpg',
190
- size: 10,
191
- contentType: 'image/jpeg'
192
- },
193
- {
194
- name: 'b.jpg',
195
- key: 'b.jpg',
196
- size: 20,
197
- contentType: 'image/jpeg'
198
- }
199
- ]);
200
- const files = [
201
- buildMulterFile({
202
- originalname: 'a.jpg'
203
- }),
204
- buildMulterFile({
205
- originalname: 'b.jpg'
206
- })
207
- ];
208
- const result = await service.uploadMultipleFiles(files, {}, buildMockUser());
209
- expect(result).toHaveLength(2);
210
- expect(mockStorageConfigService.validateFileSize).toHaveBeenCalledTimes(2);
211
- });
212
- it('rejects the whole batch if any single file fails validation', async ()=>{
213
- mockStorageConfigService.validateFileSize.mockReturnValueOnce({
214
- valid: true
215
- }).mockReturnValueOnce({
216
- valid: false,
217
- message: 'too big',
218
- messageKey: 'upload.file.too.large'
219
- });
220
- const files = [
221
- buildMulterFile({
222
- originalname: 'a.jpg'
223
- }),
224
- buildMulterFile({
225
- originalname: 'b.jpg'
226
- })
227
- ];
228
- await expect(service.uploadMultipleFiles(files, {}, buildMockUser())).rejects.toThrow();
229
- expect(mockProvider.uploadMultipleFiles).not.toHaveBeenCalled();
230
- });
231
- });
232
- describe('deleteSingleFile', ()=>{
233
- it('throws BadRequestException when no key is provided', async ()=>{
234
- await expect(service.deleteSingleFile('')).rejects.toThrow();
235
- });
236
- it('resolves a provider by storageConfigId and deletes the file', async ()=>{
237
- mockStorageProviderConfigService.findByIdDirect.mockResolvedValue({
238
- id: 'cfg-1',
239
- storage: 'aws',
240
- config: {}
241
- });
242
- const result = await service.deleteSingleFile('uploads/photo.jpg', 'cfg-1', buildMockUser());
243
- expect(mockProvider.deleteFile).toHaveBeenCalledWith('uploads/photo.jpg');
244
- expect(result).toBe(true);
245
- });
246
- it('falls back to a matching config by locationHint when storageConfigId is missing/invalid', async ()=>{
247
- mockStorageProviderConfigService.findByIdDirect.mockResolvedValue(null);
248
- mockStorageProviderConfigService.getConfigByType.mockResolvedValue([
249
- {
250
- id: 'cfg-2',
251
- storage: 'local',
252
- config: {}
253
- }
254
- ]);
255
- const result = await service.deleteSingleFile('uploads/photo.jpg', 'stale-id', buildMockUser(), 'local');
256
- expect(mockStorageProviderConfigService.getConfigByType).toHaveBeenCalledWith('local', buildMockUser());
257
- expect(result).toBe(true);
258
- });
259
- it('falls back to a local provider when locationHint is local and no config matches', async ()=>{
260
- mockStorageProviderConfigService.findByIdDirect.mockResolvedValue(null);
261
- mockStorageProviderConfigService.getConfigByType.mockResolvedValue([]);
262
- await service.deleteSingleFile('uploads/photo.jpg', undefined, buildMockUser(), FileLocationEnum.LOCAL);
263
- expect(mockStorageFactory.createProvider).toHaveBeenCalledWith(expect.objectContaining({
264
- provider: FileLocationEnum.LOCAL
265
- }));
266
- });
267
- it('falls back to the default config as a last resort', async ()=>{
268
- mockStorageProviderConfigService.getConfigByType.mockResolvedValue([]);
269
- mockStorageProviderConfigService.getDefaultConfig.mockResolvedValue({
270
- id: 'cfg-default',
271
- storage: 'aws',
272
- config: {}
273
- });
274
- await service.deleteSingleFile('uploads/photo.jpg', undefined, buildMockUser(), 'aws');
275
- expect(mockStorageProviderConfigService.getDefaultConfig).toHaveBeenCalled();
276
- });
277
- });
278
- describe('deleteMultipleFile', ()=>{
279
- it('throws BadRequestException when no keys are provided', async ()=>{
280
- await expect(service.deleteMultipleFile([])).rejects.toThrow();
281
- });
282
- it('deletes all provided keys via the resolved provider', async ()=>{
283
- mockStorageProviderConfigService.findByIdDirect.mockResolvedValue({
284
- id: 'cfg-1',
285
- storage: 'aws',
286
- config: {}
287
- });
288
- const result = await service.deleteMultipleFile([
289
- 'a.jpg',
290
- 'b.jpg'
291
- ], 'cfg-1', buildMockUser());
292
- expect(mockProvider.deleteMultipleFiles).toHaveBeenCalledWith([
293
- 'a.jpg',
294
- 'b.jpg'
295
- ]);
296
- expect(result).toBe(true);
297
- });
298
- });
299
- describe('bytesToKb', ()=>{
300
- it('converts bytes to kilobytes rounded to 2 decimals', ()=>{
301
- expect(service.bytesToKb(1024)).toBe(1.02);
302
- });
303
- it('returns 0 for a 0-byte file', ()=>{
304
- expect(service.bytesToKb(0)).toBe(0);
305
- });
306
- });
307
- describe('getLocalStorageBasePath', ()=>{
308
- it('returns the cached basePath if already available', async ()=>{
309
- mockStorageFactory.getLocalProviderBasePath.mockReturnValue('./cached-uploads');
310
- const result = await service.getLocalStorageBasePath();
311
- expect(result).toBe('./cached-uploads');
312
- expect(mockStorageProviderConfigService.getConfigByType).not.toHaveBeenCalled();
313
- });
314
- it('initializes the local provider from DB config when not yet cached', async ()=>{
315
- mockStorageFactory.getLocalProviderBasePath.mockReturnValueOnce(null).mockReturnValueOnce('./db-uploads');
316
- mockStorageProviderConfigService.getConfigByType.mockResolvedValue([
317
- {
318
- id: 'cfg-local',
319
- storage: 'local',
320
- config: {
321
- basePath: './db-uploads'
322
- }
323
- }
324
- ]);
325
- const result = await service.getLocalStorageBasePath();
326
- expect(result).toBe('./db-uploads');
327
- });
328
- it('returns null when there is no local config in the database', async ()=>{
329
- mockStorageProviderConfigService.getConfigByType.mockResolvedValue([]);
330
- const result = await service.getLocalStorageBasePath();
331
- expect(result).toBeNull();
332
- });
333
- it('returns null (swallows errors) instead of throwing when lookup fails', async ()=>{
334
- mockStorageProviderConfigService.getConfigByType.mockRejectedValue(new Error('db down'));
335
- await expect(service.getLocalStorageBasePath()).resolves.toBeNull();
336
- });
337
- });
338
- describe('makeFileUrl', ()=>{
339
- it('delegates to the provider generatePresignedUrl when supported', async ()=>{
340
- mockStorageProviderConfigService.findByIdDirect.mockResolvedValue({
341
- id: 'cfg-1',
342
- storage: 'aws',
343
- config: {}
344
- });
345
- const url = await service.makeFileUrl('uploads/photo.jpg', 'cfg-1', 900, buildMockUser());
346
- expect(mockProvider.generatePresignedUrl).toHaveBeenCalledWith('uploads/photo.jpg', 900);
347
- expect(url).toBe('https://signed.example.com/uploads/photo.jpg');
348
- });
349
- it('returns the raw key when the provider does not support presigned URLs', async ()=>{
350
- mockStorageProviderConfigService.findByIdDirect.mockResolvedValue({
351
- id: 'cfg-1',
352
- storage: 'sftp',
353
- config: {}
354
- });
355
- delete mockProvider.generatePresignedUrl;
356
- const url = await service.makeFileUrl('uploads/photo.jpg', 'cfg-1', 900, buildMockUser());
357
- expect(url).toBe('uploads/photo.jpg');
358
- });
359
- it('defaults expiry to 3600 seconds when not specified', async ()=>{
360
- mockStorageProviderConfigService.findByIdDirect.mockResolvedValue({
361
- id: 'cfg-1',
362
- storage: 'aws',
363
- config: {}
364
- });
365
- await service.makeFileUrl('uploads/photo.jpg', 'cfg-1');
366
- expect(mockProvider.generatePresignedUrl).toHaveBeenCalledWith('uploads/photo.jpg', 3600);
367
- });
368
- });
369
- describe('domain events', ()=>{
370
- let publish;
371
- beforeEach(()=>{
372
- publish = jest.fn().mockResolvedValue(undefined);
373
- EventBusRegistry.reset();
374
- EventBusRegistry.setBus({
375
- publish,
376
- subscribe: jest.fn(),
377
- close: jest.fn()
378
- });
379
- EventBusRegistry.configureModule('storage', {
380
- enabled: true
381
- });
382
- mockStorageProviderConfigService.getDefaultConfig.mockResolvedValue({
383
- id: 'cfg-default',
384
- storage: 'local',
385
- config: {}
386
- });
387
- });
388
- afterEach(()=>{
389
- EventBusRegistry.reset();
390
- });
391
- it('publishes storage.file_manager.uploaded after a single upload', async ()=>{
392
- await service.uploadSingleFile(buildMulterFile(), {}, buildMockUser());
393
- expect(publish).toHaveBeenCalledWith(expect.objectContaining({
394
- name: 'storage.file_manager.uploaded',
395
- payload: expect.objectContaining({
396
- metadata: expect.objectContaining({
397
- count: 1,
398
- storageConfigId: 'cfg-default'
399
- })
400
- })
401
- }));
402
- });
403
- it('publishes storage.file_manager.removed after a delete', async ()=>{
404
- await service.deleteSingleFile('uploads/photo.jpg', 'cfg-1', buildMockUser());
405
- expect(publish).toHaveBeenCalledWith(expect.objectContaining({
406
- name: 'storage.file_manager.removed',
407
- payload: expect.objectContaining({
408
- ids: [
409
- 'uploads/photo.jpg'
410
- ]
411
- })
412
- }));
413
- });
414
- it('does not publish when the storage module has events disabled', async ()=>{
415
- EventBusRegistry.configureModule('storage', {
416
- enabled: false
417
- });
418
- await service.uploadSingleFile(buildMulterFile(), {}, buildMockUser());
419
- expect(publish).not.toHaveBeenCalled();
420
- });
421
- });
422
- });