@flusys/nestjs-storage 7.1.0 → 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.
- package/config/index.d.ts +3 -3
- package/controllers/file-manager.controller.d.ts +4 -4
- package/controllers/folder.controller.d.ts +2 -2
- package/controllers/index.d.ts +4 -4
- package/controllers/storage-config.controller.d.ts +2 -2
- package/controllers/upload.controller.d.ts +3 -3
- package/docs/index.d.ts +1 -1
- package/docs/storage-swagger.config.d.ts +2 -2
- package/dtos/index.d.ts +4 -4
- package/entities/file-manager-with-company.entity.d.ts +2 -2
- package/entities/file-manager.entity.d.ts +1 -1
- package/entities/folder-with-company.entity.d.ts +1 -1
- package/entities/index.d.ts +15 -15
- package/entities/storage-config-with-company.entity.d.ts +1 -1
- package/enums/index.d.ts +1 -1
- package/fesm/493.js +2768 -0
- package/fesm/537.js +1157 -0
- package/fesm/562.js +786 -0
- package/fesm/{providers/azure-provider.optional.js → chunks/190.js} +33 -30
- package/fesm/chunks/252.js +12 -0
- package/fesm/chunks/327.js +12 -0
- package/fesm/chunks/385.js +12 -0
- package/fesm/{providers/s3-provider.optional.js → chunks/591.js} +37 -34
- package/fesm/chunks/746.js +12 -0
- package/fesm/chunks/798.js +12 -0
- package/fesm/{providers/sftp-provider.optional.js → chunks/918.js} +35 -31
- package/fesm/config/index.js +119 -3
- package/fesm/controllers/index.js +16 -4
- package/fesm/docs/index.js +206 -1
- package/fesm/dtos/index.js +879 -4
- package/fesm/entities/index.js +463 -18
- package/fesm/enums/index.js +36 -1
- package/fesm/index.js +398 -12
- package/fesm/interfaces/index.js +19 -5
- package/fesm/modules/index.js +16 -1
- package/fesm/providers/index.js +364 -3
- package/fesm/runtime.js +252 -0
- package/fesm/services/index.js +16 -6
- package/fesm/utils/index.js +149 -1
- package/index.d.ts +11 -11
- package/interfaces/file-manager.interface.d.ts +1 -1
- package/interfaces/folder.interface.d.ts +1 -1
- package/interfaces/index.d.ts +5 -5
- package/interfaces/storage-config.interface.d.ts +1 -1
- package/interfaces/storage-module-options.interface.d.ts +3 -3
- package/interfaces/storage-provider.interface.d.ts +1 -1
- package/middlewares/file-serve.middleware.d.ts +4 -3
- package/middlewares/index.d.ts +1 -1
- package/modules/index.d.ts +1 -1
- package/modules/storage.module.d.ts +2 -2
- package/package.json +30 -41
- package/providers/azure-provider.optional.d.ts +2 -2
- package/providers/index.d.ts +3 -3
- package/providers/local-provider.d.ts +2 -2
- package/providers/s3-provider.optional.d.ts +2 -2
- package/providers/sftp-provider.optional.d.ts +2 -2
- package/providers/storage-factory.service.d.ts +3 -3
- package/providers/storage-provider.registry.d.ts +2 -2
- package/services/file-manager.service.d.ts +9 -8
- package/services/folder.service.d.ts +6 -6
- package/services/index.d.ts +6 -6
- package/services/storage-config.service.d.ts +3 -3
- package/services/storage-datasource.provider.d.ts +3 -3
- package/services/storage-provider-config.service.d.ts +7 -7
- package/services/upload.service.d.ts +6 -6
- package/utils/image-compressor.util.d.ts +1 -1
- package/utils/index.d.ts +1 -1
- package/cjs/config/event-actions.js +0 -34
- package/cjs/config/index.js +0 -20
- package/cjs/config/message-keys.js +0 -46
- package/cjs/config/storage.constants.js +0 -28
- package/cjs/controllers/file-manager.controller.js +0 -152
- package/cjs/controllers/file-manager.controller.spec.js +0 -135
- package/cjs/controllers/folder.controller.js +0 -102
- package/cjs/controllers/folder.controller.spec.js +0 -86
- package/cjs/controllers/index.js +0 -21
- package/cjs/controllers/storage-config.controller.js +0 -102
- package/cjs/controllers/storage-config.controller.spec.js +0 -85
- package/cjs/controllers/upload.controller.js +0 -234
- package/cjs/controllers/upload.controller.spec.js +0 -132
- package/cjs/docs/index.js +0 -11
- package/cjs/docs/storage-swagger.config.js +0 -188
- package/cjs/docs/storage-swagger.config.spec.js +0 -72
- package/cjs/dtos/file-manager.dto.js +0 -327
- package/cjs/dtos/folder.dto.js +0 -105
- package/cjs/dtos/index.js +0 -21
- package/cjs/dtos/storage-config.dto.js +0 -170
- package/cjs/dtos/upload.dto.js +0 -272
- package/cjs/entities/file-manager-with-company.entity.js +0 -61
- package/cjs/entities/file-manager.entity.js +0 -122
- package/cjs/entities/folder-with-company.entity.js +0 -55
- package/cjs/entities/folder.entity.js +0 -64
- package/cjs/entities/index.js +0 -63
- package/cjs/entities/index.spec.js +0 -18
- package/cjs/entities/storage-config-with-company.entity.js +0 -51
- package/cjs/entities/storage-config.entity.js +0 -96
- package/cjs/enums/file-location.enum.js +0 -17
- package/cjs/enums/index.js +0 -18
- package/cjs/index.js +0 -29
- package/cjs/interfaces/file-manager.interface.js +0 -4
- package/cjs/interfaces/folder.interface.js +0 -4
- package/cjs/interfaces/index.js +0 -22
- package/cjs/interfaces/storage-config.interface.js +0 -4
- package/cjs/interfaces/storage-module-options.interface.js +0 -4
- package/cjs/interfaces/storage-provider.interface.js +0 -4
- package/cjs/middlewares/file-serve.middleware.js +0 -241
- package/cjs/middlewares/file-serve.middleware.spec.js +0 -211
- package/cjs/middlewares/index.js +0 -18
- package/cjs/modules/index.js +0 -18
- package/cjs/modules/storage.module.js +0 -165
- package/cjs/modules/storage.module.spec.js +0 -184
- package/cjs/providers/azure-provider.optional.js +0 -155
- package/cjs/providers/azure-provider.optional.spec.js +0 -228
- package/cjs/providers/index.js +0 -26
- package/cjs/providers/local-provider.js +0 -230
- package/cjs/providers/local-provider.spec.js +0 -239
- package/cjs/providers/s3-provider.optional.js +0 -175
- package/cjs/providers/s3-provider.optional.spec.js +0 -243
- package/cjs/providers/sftp-provider.optional.js +0 -203
- package/cjs/providers/sftp-provider.optional.spec.js +0 -217
- package/cjs/providers/storage-factory.service.js +0 -175
- package/cjs/providers/storage-factory.service.spec.js +0 -253
- package/cjs/providers/storage-provider.registry.js +0 -41
- package/cjs/providers/storage-provider.registry.spec.js +0 -67
- package/cjs/services/file-manager.service.js +0 -387
- package/cjs/services/file-manager.service.spec.js +0 -580
- package/cjs/services/folder.service.js +0 -101
- package/cjs/services/folder.service.spec.js +0 -167
- package/cjs/services/index.js +0 -23
- package/cjs/services/storage-config.service.js +0 -129
- package/cjs/services/storage-config.service.spec.js +0 -257
- package/cjs/services/storage-datasource.provider.js +0 -137
- package/cjs/services/storage-datasource.provider.spec.js +0 -217
- package/cjs/services/storage-provider-config.service.js +0 -171
- package/cjs/services/storage-provider-config.service.spec.js +0 -265
- package/cjs/services/upload.service.js +0 -376
- package/cjs/services/upload.service.spec.js +0 -426
- package/cjs/utils/file-validator.util.js +0 -440
- package/cjs/utils/file-validator.util.spec.js +0 -259
- package/cjs/utils/image-compressor.util.js +0 -162
- package/cjs/utils/image-compressor.util.spec.js +0 -166
- package/cjs/utils/index.js +0 -18
- package/fesm/config/event-actions.js +0 -13
- package/fesm/config/message-keys.js +0 -22
- package/fesm/config/storage.constants.js +0 -7
- package/fesm/controllers/file-manager.controller.js +0 -142
- package/fesm/controllers/file-manager.controller.spec.js +0 -131
- package/fesm/controllers/folder.controller.js +0 -92
- package/fesm/controllers/folder.controller.spec.js +0 -82
- package/fesm/controllers/storage-config.controller.js +0 -92
- package/fesm/controllers/storage-config.controller.spec.js +0 -81
- package/fesm/controllers/upload.controller.js +0 -224
- package/fesm/controllers/upload.controller.spec.js +0 -128
- package/fesm/docs/storage-swagger.config.js +0 -181
- package/fesm/docs/storage-swagger.config.spec.js +0 -68
- package/fesm/dtos/file-manager.dto.js +0 -300
- package/fesm/dtos/folder.dto.js +0 -84
- package/fesm/dtos/storage-config.dto.js +0 -149
- package/fesm/dtos/upload.dto.js +0 -245
- package/fesm/entities/file-manager-with-company.entity.js +0 -51
- package/fesm/entities/file-manager.entity.js +0 -112
- package/fesm/entities/folder-with-company.entity.js +0 -45
- package/fesm/entities/folder.entity.js +0 -54
- package/fesm/entities/index.spec.js +0 -14
- package/fesm/entities/storage-config-with-company.entity.js +0 -41
- package/fesm/entities/storage-config.entity.js +0 -86
- package/fesm/enums/file-location.enum.js +0 -7
- package/fesm/interfaces/file-manager.interface.js +0 -1
- package/fesm/interfaces/folder.interface.js +0 -1
- package/fesm/interfaces/storage-config.interface.js +0 -1
- package/fesm/interfaces/storage-module-options.interface.js +0 -3
- package/fesm/interfaces/storage-provider.interface.js +0 -4
- package/fesm/middlewares/file-serve.middleware.js +0 -190
- package/fesm/middlewares/file-serve.middleware.spec.js +0 -207
- package/fesm/middlewares/index.js +0 -1
- package/fesm/modules/storage.module.js +0 -156
- package/fesm/modules/storage.module.spec.js +0 -180
- package/fesm/providers/azure-provider.optional.spec.js +0 -224
- package/fesm/providers/local-provider.js +0 -182
- package/fesm/providers/local-provider.spec.js +0 -194
- package/fesm/providers/s3-provider.optional.spec.js +0 -239
- package/fesm/providers/sftp-provider.optional.spec.js +0 -213
- package/fesm/providers/storage-factory.service.js +0 -124
- package/fesm/providers/storage-factory.service.spec.js +0 -249
- package/fesm/providers/storage-provider.registry.js +0 -34
- package/fesm/providers/storage-provider.registry.spec.js +0 -63
- package/fesm/services/file-manager.service.js +0 -336
- package/fesm/services/file-manager.service.spec.js +0 -576
- package/fesm/services/folder.service.js +0 -91
- package/fesm/services/folder.service.spec.js +0 -163
- package/fesm/services/storage-config.service.js +0 -119
- package/fesm/services/storage-config.service.spec.js +0 -253
- package/fesm/services/storage-datasource.provider.js +0 -86
- package/fesm/services/storage-datasource.provider.spec.js +0 -172
- package/fesm/services/storage-provider-config.service.js +0 -161
- package/fesm/services/storage-provider-config.service.spec.js +0 -261
- package/fesm/services/upload.service.js +0 -366
- package/fesm/services/upload.service.spec.js +0 -422
- package/fesm/utils/file-validator.util.js +0 -433
- package/fesm/utils/file-validator.util.spec.js +0 -255
- package/fesm/utils/image-compressor.util.js +0 -114
- 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
|
-
});
|