@flusys/nestjs-storage 6.0.2 → 6.1.0

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 (45) hide show
  1. package/cjs/controllers/file-manager.controller.spec.js +135 -0
  2. package/cjs/controllers/folder.controller.spec.js +86 -0
  3. package/cjs/controllers/storage-config.controller.spec.js +85 -0
  4. package/cjs/controllers/upload.controller.spec.js +132 -0
  5. package/cjs/docs/storage-swagger.config.spec.js +72 -0
  6. package/cjs/entities/index.spec.js +18 -0
  7. package/cjs/middlewares/file-serve.middleware.spec.js +211 -0
  8. package/cjs/modules/storage.module.spec.js +184 -0
  9. package/cjs/providers/azure-provider.optional.spec.js +228 -0
  10. package/cjs/providers/local-provider.spec.js +239 -0
  11. package/cjs/providers/s3-provider.optional.spec.js +243 -0
  12. package/cjs/providers/sftp-provider.optional.spec.js +217 -0
  13. package/cjs/providers/storage-factory.service.spec.js +253 -0
  14. package/cjs/providers/storage-provider.registry.spec.js +67 -0
  15. package/cjs/services/file-manager.service.spec.js +549 -0
  16. package/cjs/services/folder.service.spec.js +163 -0
  17. package/cjs/services/storage-config.service.spec.js +257 -0
  18. package/cjs/services/storage-datasource.provider.spec.js +217 -0
  19. package/cjs/services/storage-provider-config.service.spec.js +243 -0
  20. package/cjs/services/upload.service.spec.js +372 -0
  21. package/cjs/utils/file-validator.util.spec.js +259 -0
  22. package/cjs/utils/image-compressor.util.spec.js +166 -0
  23. package/fesm/controllers/file-manager.controller.spec.js +131 -0
  24. package/fesm/controllers/folder.controller.spec.js +82 -0
  25. package/fesm/controllers/storage-config.controller.spec.js +81 -0
  26. package/fesm/controllers/upload.controller.spec.js +128 -0
  27. package/fesm/docs/storage-swagger.config.spec.js +68 -0
  28. package/fesm/entities/index.spec.js +14 -0
  29. package/fesm/middlewares/file-serve.middleware.spec.js +207 -0
  30. package/fesm/modules/storage.module.spec.js +180 -0
  31. package/fesm/providers/azure-provider.optional.spec.js +224 -0
  32. package/fesm/providers/local-provider.spec.js +194 -0
  33. package/fesm/providers/s3-provider.optional.spec.js +239 -0
  34. package/fesm/providers/sftp-provider.optional.spec.js +213 -0
  35. package/fesm/providers/storage-factory.service.spec.js +249 -0
  36. package/fesm/providers/storage-provider.registry.spec.js +63 -0
  37. package/fesm/services/file-manager.service.spec.js +545 -0
  38. package/fesm/services/folder.service.spec.js +159 -0
  39. package/fesm/services/storage-config.service.spec.js +253 -0
  40. package/fesm/services/storage-datasource.provider.spec.js +172 -0
  41. package/fesm/services/storage-provider-config.service.spec.js +239 -0
  42. package/fesm/services/upload.service.spec.js +368 -0
  43. package/fesm/utils/file-validator.util.spec.js +255 -0
  44. package/fesm/utils/image-compressor.util.spec.js +162 -0
  45. package/package.json +3 -3
@@ -0,0 +1,81 @@
1
+ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
2
+ import { StorageConfigController } from './storage-config.controller';
3
+ function buildStorageConfig(overrides = {}) {
4
+ return {
5
+ id: 'storage-config-1',
6
+ provider: 'local',
7
+ isActive: true,
8
+ isDefault: true,
9
+ readOnly: false,
10
+ createdAt: new Date(),
11
+ updatedAt: new Date(),
12
+ ...overrides
13
+ };
14
+ }
15
+ describe('StorageConfigController', ()=>{
16
+ let controller;
17
+ let mockStorageConfigService;
18
+ beforeEach(()=>{
19
+ mockStorageConfigService = {
20
+ insert: jest.fn(),
21
+ findById: jest.fn(),
22
+ getAll: jest.fn(),
23
+ update: jest.fn(),
24
+ delete: jest.fn()
25
+ };
26
+ controller = new StorageConfigController(mockStorageConfigService);
27
+ });
28
+ it('wires the injected service onto the generated controller base', ()=>{
29
+ expect(controller.storageConfigService).toBe(mockStorageConfigService);
30
+ expect(controller.service).toBe(mockStorageConfigService);
31
+ });
32
+ describe('insert', ()=>{
33
+ it('creates a storage config and returns the mapped response', async ()=>{
34
+ const user = buildMockUser();
35
+ mockStorageConfigService.insert.mockResolvedValue(buildStorageConfig());
36
+ const result = await controller.insert({
37
+ provider: 'local'
38
+ }, user);
39
+ expect(mockStorageConfigService.insert).toHaveBeenCalledWith({
40
+ provider: 'local'
41
+ }, user);
42
+ expect(result.success).toBe(true);
43
+ expect(result.data).toEqual(expect.objectContaining({
44
+ id: 'storage-config-1'
45
+ }));
46
+ });
47
+ });
48
+ describe('getAll', ()=>{
49
+ it('paginates storage configs and maps the response list', async ()=>{
50
+ mockStorageConfigService.getAll.mockResolvedValue({
51
+ data: [
52
+ buildStorageConfig()
53
+ ],
54
+ total: 1
55
+ });
56
+ const result = await controller.getAll({}, buildMockUser(), '');
57
+ expect(mockStorageConfigService.getAll).toHaveBeenCalledWith('', {}, buildMockUser());
58
+ expect(result.data).toHaveLength(1);
59
+ });
60
+ });
61
+ describe('delete', ()=>{
62
+ it('delegates soft-delete to the service', async ()=>{
63
+ mockStorageConfigService.delete.mockResolvedValue({
64
+ count: 1
65
+ });
66
+ const result = await controller.delete({
67
+ id: [
68
+ 'storage-config-1'
69
+ ],
70
+ type: 'delete'
71
+ }, buildMockUser());
72
+ expect(mockStorageConfigService.delete).toHaveBeenCalledWith({
73
+ id: [
74
+ 'storage-config-1'
75
+ ],
76
+ type: 'delete'
77
+ }, buildMockUser());
78
+ expect(result.success).toBe(true);
79
+ });
80
+ });
81
+ });
@@ -0,0 +1,128 @@
1
+ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
2
+ import { UploadController } from './upload.controller';
3
+ describe('UploadController', ()=>{
4
+ let controller;
5
+ let mockUploadService;
6
+ beforeEach(()=>{
7
+ mockUploadService = {
8
+ bytesToKb: jest.fn((bytes)=>bytes / 1024),
9
+ uploadSingleFile: jest.fn(),
10
+ uploadMultipleFiles: jest.fn(),
11
+ deleteSingleFile: jest.fn(),
12
+ deleteMultipleFile: jest.fn()
13
+ };
14
+ controller = new UploadController(mockUploadService);
15
+ });
16
+ describe('uploadSingleFile', ()=>{
17
+ it('uploads a file and maps the response payload', async ()=>{
18
+ const user = buildMockUser();
19
+ const file = {
20
+ originalname: 'photo.png'
21
+ };
22
+ mockUploadService.uploadSingleFile.mockResolvedValue({
23
+ size: 2048,
24
+ name: 'photo.png',
25
+ key: 'files/photo.png',
26
+ contentType: 'image/png',
27
+ location: 's3',
28
+ storageConfigId: 'storage-1'
29
+ });
30
+ const result = await controller.uploadSingleFile(file, {
31
+ folderPath: 'photos'
32
+ }, user);
33
+ expect(mockUploadService.uploadSingleFile).toHaveBeenCalledWith(file, {
34
+ folderPath: 'photos'
35
+ }, user);
36
+ expect(mockUploadService.bytesToKb).toHaveBeenCalledWith(2048);
37
+ expect(result).toEqual({
38
+ success: true,
39
+ message: 'File uploaded successfully',
40
+ messageKey: expect.any(String),
41
+ data: {
42
+ size: 2,
43
+ name: 'photo.png',
44
+ key: 'files/photo.png',
45
+ contentType: 'image/png',
46
+ location: 's3',
47
+ storageConfigId: 'storage-1'
48
+ }
49
+ });
50
+ });
51
+ it('defaults location to "local" and storageConfigId to an empty string when absent', async ()=>{
52
+ const user = buildMockUser();
53
+ mockUploadService.uploadSingleFile.mockResolvedValue({
54
+ size: 100,
55
+ name: 'file.txt',
56
+ key: 'files/file.txt',
57
+ contentType: 'text/plain'
58
+ });
59
+ const result = await controller.uploadSingleFile({}, {}, user);
60
+ expect(result.data.location).toBe('local');
61
+ expect(result.data.storageConfigId).toBe('');
62
+ });
63
+ });
64
+ describe('uploadMultipleFiles', ()=>{
65
+ it('uploads multiple files and maps each response payload', async ()=>{
66
+ const user = buildMockUser();
67
+ const files = [
68
+ {
69
+ originalname: 'a.png'
70
+ },
71
+ {
72
+ originalname: 'b.png'
73
+ }
74
+ ];
75
+ mockUploadService.uploadMultipleFiles.mockResolvedValue([
76
+ {
77
+ size: 100,
78
+ name: 'a.png',
79
+ key: 'files/a.png',
80
+ contentType: 'image/png'
81
+ },
82
+ {
83
+ size: 200,
84
+ name: 'b.png',
85
+ key: 'files/b.png',
86
+ contentType: 'image/png'
87
+ }
88
+ ]);
89
+ const result = await controller.uploadMultipleFiles(files, {}, user);
90
+ expect(mockUploadService.uploadMultipleFiles).toHaveBeenCalledWith(files, {}, user);
91
+ expect(result.data).toHaveLength(2);
92
+ expect(result.data[0]).toEqual(expect.objectContaining({
93
+ name: 'a.png',
94
+ key: 'files/a.png'
95
+ }));
96
+ });
97
+ });
98
+ describe('deleteSingleFile', ()=>{
99
+ it('deletes a file by key and storage config', async ()=>{
100
+ const user = buildMockUser();
101
+ mockUploadService.deleteSingleFile.mockResolvedValue(true);
102
+ const result = await controller.deleteSingleFile({
103
+ key: 'files/a.png',
104
+ storageConfigId: 'storage-1'
105
+ }, user);
106
+ expect(mockUploadService.deleteSingleFile).toHaveBeenCalledWith('files/a.png', 'storage-1', user);
107
+ expect(result.data).toBe(true);
108
+ });
109
+ });
110
+ describe('deleteMultipleFile', ()=>{
111
+ it('deletes multiple files by keys and storage config', async ()=>{
112
+ const user = buildMockUser();
113
+ mockUploadService.deleteMultipleFile.mockResolvedValue(true);
114
+ const result = await controller.deleteMultipleFile({
115
+ keys: [
116
+ 'files/a.png',
117
+ 'files/b.png'
118
+ ],
119
+ storageConfigId: 'storage-1'
120
+ }, user);
121
+ expect(mockUploadService.deleteMultipleFile).toHaveBeenCalledWith([
122
+ 'files/a.png',
123
+ 'files/b.png'
124
+ ], 'storage-1', user);
125
+ expect(result.data).toBe(true);
126
+ });
127
+ });
128
+ });
@@ -0,0 +1,68 @@
1
+ import { storageSwaggerConfig } from './storage-swagger.config';
2
+ describe('storageSwaggerConfig', ()=>{
3
+ it('should default to company feature enabled and single-database mode when no config is given', ()=>{
4
+ const config = storageSwaggerConfig();
5
+ expect(config.title).toBe('Storage API');
6
+ expect(config.path).toBe('api/docs/storage');
7
+ expect(config.bearerAuth).toBe(true);
8
+ expect(config.excludeTags).toEqual([]);
9
+ expect(config.excludeSchemaProperties).toBeUndefined();
10
+ expect(config.description).not.toContain('Multi-Tenant Mode');
11
+ expect(config.description).toContain('with multi-tenant support');
12
+ });
13
+ it('should exclude companyId schema properties when the company feature is disabled', ()=>{
14
+ const config = storageSwaggerConfig({
15
+ enableCompanyFeature: false
16
+ });
17
+ expect(config.excludeSchemaProperties).toEqual([
18
+ {
19
+ schemaName: 'FileManagerResponseDto',
20
+ properties: [
21
+ 'companyId'
22
+ ]
23
+ },
24
+ {
25
+ schemaName: 'FolderResponseDto',
26
+ properties: [
27
+ 'companyId'
28
+ ]
29
+ },
30
+ {
31
+ schemaName: 'StorageConfigResponseDto',
32
+ properties: [
33
+ 'companyId'
34
+ ]
35
+ }
36
+ ]);
37
+ });
38
+ it('should not exclude any schema properties when the company feature is enabled', ()=>{
39
+ const config = storageSwaggerConfig({
40
+ enableCompanyFeature: true
41
+ });
42
+ expect(config.excludeSchemaProperties).toBeUndefined();
43
+ });
44
+ it('should describe company isolation and multi-tenant sections only when the company feature is enabled', ()=>{
45
+ const withCompany = storageSwaggerConfig({
46
+ enableCompanyFeature: true
47
+ });
48
+ const withoutCompany = storageSwaggerConfig({
49
+ enableCompanyFeature: false
50
+ });
51
+ expect(withCompany.description).toContain('Company isolation');
52
+ expect(withCompany.description).toContain('## Multi-Tenant Support');
53
+ expect(withCompany.description).toContain('## Examples');
54
+ expect(withoutCompany.description).not.toContain('Company isolation');
55
+ expect(withoutCompany.description).not.toContain('## Multi-Tenant Support');
56
+ expect(withoutCompany.description).not.toContain('## Examples');
57
+ });
58
+ it('should mention multi-tenant mode in the description only for multi-tenant database mode', ()=>{
59
+ const multiTenant = storageSwaggerConfig({
60
+ databaseMode: 'multi-tenant'
61
+ });
62
+ const single = storageSwaggerConfig({
63
+ databaseMode: 'single'
64
+ });
65
+ expect(multiTenant.description).toContain('Multi-Tenant Mode');
66
+ expect(single.description).not.toContain('Multi-Tenant Mode');
67
+ });
68
+ });
@@ -0,0 +1,14 @@
1
+ import { getStorageEntitiesByConfig, StorageCoreEntities, StorageCompanyEntities } from './index';
2
+ describe('getStorageEntitiesByConfig', ()=>{
3
+ it('returns the core entities when the company feature is disabled', ()=>{
4
+ expect(getStorageEntitiesByConfig(false)).toBe(StorageCoreEntities);
5
+ });
6
+ it('returns the company-scoped entities when the company feature is enabled', ()=>{
7
+ expect(getStorageEntitiesByConfig(true)).toBe(StorageCompanyEntities);
8
+ });
9
+ it('core and company entity sets are disjoint (never mixed)', ()=>{
10
+ const core = getStorageEntitiesByConfig(false);
11
+ const company = getStorageEntitiesByConfig(true);
12
+ expect(core.some((e)=>company.includes(e))).toBe(false);
13
+ });
14
+ });
@@ -0,0 +1,207 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) {
3
+ Object.defineProperty(obj, key, {
4
+ value: value,
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true
8
+ });
9
+ } else {
10
+ obj[key] = value;
11
+ }
12
+ return obj;
13
+ }
14
+ import { EventEmitter } from 'events';
15
+ import { join } from 'path';
16
+ const mockCreateReadStream = jest.fn();
17
+ const mockExistsSync = jest.fn();
18
+ const mockStatSync = jest.fn();
19
+ jest.mock('fs', ()=>({
20
+ ...jest.requireActual('fs'),
21
+ createReadStream: (...args)=>mockCreateReadStream(...args),
22
+ existsSync: (...args)=>mockExistsSync(...args),
23
+ statSync: (...args)=>mockStatSync(...args)
24
+ }));
25
+ const mockMimeLookup = jest.fn();
26
+ jest.mock('mime-types', ()=>({
27
+ lookup: (...args)=>mockMimeLookup(...args)
28
+ }));
29
+ import { FileServeMiddleware } from './file-serve.middleware';
30
+ let FakeStream = class FakeStream extends EventEmitter {
31
+ constructor(...args){
32
+ super(...args), _define_property(this, "pipe", jest.fn()), _define_property(this, "destroy", jest.fn());
33
+ }
34
+ };
35
+ function buildRes() {
36
+ const listeners = {};
37
+ return {
38
+ headersSent: false,
39
+ set: jest.fn(),
40
+ status: jest.fn().mockReturnThis(),
41
+ json: jest.fn(),
42
+ on: jest.fn((event, cb)=>{
43
+ listeners[event] = listeners[event] || [];
44
+ listeners[event].push(cb);
45
+ }),
46
+ emitClose: ()=>(listeners['close'] || []).forEach((cb)=>cb())
47
+ };
48
+ }
49
+ function buildReq(urlPath) {
50
+ return {
51
+ path: urlPath,
52
+ url: urlPath
53
+ };
54
+ }
55
+ describe('FileServeMiddleware', ()=>{
56
+ let middleware;
57
+ let mockUploadService;
58
+ let next;
59
+ beforeEach(()=>{
60
+ jest.clearAllMocks();
61
+ mockUploadService = {
62
+ getLocalStorageBasePath: jest.fn().mockResolvedValue(null)
63
+ };
64
+ middleware = new FileServeMiddleware(mockUploadService);
65
+ next = jest.fn();
66
+ });
67
+ it('responds 404 when the file path segment is missing from the URL', async ()=>{
68
+ const res = buildRes();
69
+ await middleware.use(buildReq('/storage/upload/file/'), res, next);
70
+ expect(res.status).toHaveBeenCalledWith(404);
71
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
72
+ success: false,
73
+ statusCode: 404
74
+ }));
75
+ expect(mockExistsSync).not.toHaveBeenCalled();
76
+ });
77
+ it('blocks path traversal attempts that resolve outside the upload directory', async ()=>{
78
+ const res = buildRes();
79
+ mockExistsSync.mockReturnValue(false);
80
+ await middleware.use(buildReq('/storage/upload/file/../../etc/passwd'), res, next);
81
+ expect(res.status).toHaveBeenCalledWith(404);
82
+ expect(mockStatSync).not.toHaveBeenCalled();
83
+ });
84
+ it('serves a file that exists directly under the upload directory with inline disposition for viewable types', async ()=>{
85
+ const res = buildRes();
86
+ mockExistsSync.mockReturnValue(true);
87
+ mockStatSync.mockReturnValue({
88
+ isFile: ()=>true,
89
+ size: 1234
90
+ });
91
+ mockMimeLookup.mockReturnValue('application/pdf');
92
+ const stream = new FakeStream();
93
+ mockCreateReadStream.mockReturnValue(stream);
94
+ await middleware.use(buildReq('/storage/upload/file/report.pdf'), res, next);
95
+ expect(res.set).toHaveBeenCalledWith(expect.objectContaining({
96
+ 'Content-Type': 'application/pdf',
97
+ 'Content-Length': 1234,
98
+ 'Content-Disposition': 'inline'
99
+ }));
100
+ expect(stream.pipe).toHaveBeenCalledWith(res);
101
+ });
102
+ it('sets an attachment content-disposition with the filename for non-viewable mime types', async ()=>{
103
+ const res = buildRes();
104
+ mockExistsSync.mockReturnValue(true);
105
+ mockStatSync.mockReturnValue({
106
+ isFile: ()=>true,
107
+ size: 500
108
+ });
109
+ mockMimeLookup.mockReturnValue('application/zip');
110
+ mockCreateReadStream.mockReturnValue(new FakeStream());
111
+ await middleware.use(buildReq('/storage/upload/file/archive.zip'), res, next);
112
+ expect(res.set).toHaveBeenCalledWith(expect.objectContaining({
113
+ 'Content-Disposition': expect.stringContaining('attachment; filename="archive.zip"')
114
+ }));
115
+ });
116
+ it('always serves .svg files as image/svg+xml regardless of mime-types lookup', async ()=>{
117
+ const res = buildRes();
118
+ mockExistsSync.mockReturnValue(true);
119
+ mockStatSync.mockReturnValue({
120
+ isFile: ()=>true,
121
+ size: 200
122
+ });
123
+ mockCreateReadStream.mockReturnValue(new FakeStream());
124
+ await middleware.use(buildReq('/storage/upload/file/logo.svg'), res, next);
125
+ expect(mockMimeLookup).not.toHaveBeenCalled();
126
+ expect(res.set).toHaveBeenCalledWith(expect.objectContaining({
127
+ 'Content-Type': 'image/svg+xml'
128
+ }));
129
+ });
130
+ it('returns 404 when the resolved path exists but is not a regular file', async ()=>{
131
+ const res = buildRes();
132
+ mockExistsSync.mockReturnValue(true);
133
+ mockStatSync.mockReturnValue({
134
+ isFile: ()=>false
135
+ });
136
+ await middleware.use(buildReq('/storage/upload/file/some-dir'), res, next);
137
+ expect(res.status).toHaveBeenCalledWith(404);
138
+ expect(mockCreateReadStream).not.toHaveBeenCalled();
139
+ });
140
+ it('falls back to the configured local storage base path (strategy 2: path already prefixed with base)', async ()=>{
141
+ const res = buildRes();
142
+ const fallbackPath = join('uploads', 'report.pdf');
143
+ mockUploadService.getLocalStorageBasePath.mockResolvedValue('./uploads');
144
+ mockExistsSync.mockImplementation((p)=>p === fallbackPath);
145
+ mockStatSync.mockReturnValue({
146
+ isFile: ()=>true,
147
+ size: 10
148
+ });
149
+ mockMimeLookup.mockReturnValue('application/pdf');
150
+ mockCreateReadStream.mockReturnValue(new FakeStream());
151
+ await middleware.use(buildReq('/storage/upload/file/uploads/report.pdf'), res, next);
152
+ expect(mockCreateReadStream).toHaveBeenCalledWith(fallbackPath);
153
+ });
154
+ it('falls back to the configured local storage base path (strategy 3: legacy path without base prefix)', async ()=>{
155
+ const res = buildRes();
156
+ const fallbackPath = join('uploads', 'report.pdf');
157
+ mockUploadService.getLocalStorageBasePath.mockResolvedValue('uploads');
158
+ mockExistsSync.mockImplementation((p)=>p === fallbackPath);
159
+ mockStatSync.mockReturnValue({
160
+ isFile: ()=>true,
161
+ size: 10
162
+ });
163
+ mockMimeLookup.mockReturnValue('application/pdf');
164
+ mockCreateReadStream.mockReturnValue(new FakeStream());
165
+ await middleware.use(buildReq('/storage/upload/file/report.pdf'), res, next);
166
+ expect(mockCreateReadStream).toHaveBeenCalledWith(fallbackPath);
167
+ });
168
+ it('returns the underlying not-found message when no direct or fallback path resolves', async ()=>{
169
+ const res = buildRes();
170
+ mockExistsSync.mockReturnValue(false);
171
+ mockUploadService.getLocalStorageBasePath.mockResolvedValue(null);
172
+ await middleware.use(buildReq('/storage/upload/file/missing.pdf'), res, next);
173
+ expect(res.status).toHaveBeenCalledWith(404);
174
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
175
+ message: expect.stringContaining('File not found'),
176
+ statusCode: 404
177
+ }));
178
+ });
179
+ it('sends a 404 if the file stream errors before headers are sent', async ()=>{
180
+ const res = buildRes();
181
+ mockExistsSync.mockReturnValue(true);
182
+ mockStatSync.mockReturnValue({
183
+ isFile: ()=>true,
184
+ size: 10
185
+ });
186
+ mockMimeLookup.mockReturnValue('application/pdf');
187
+ const stream = new FakeStream();
188
+ mockCreateReadStream.mockReturnValue(stream);
189
+ await middleware.use(buildReq('/storage/upload/file/report.pdf'), res, next);
190
+ stream.emit('error', new Error('boom'));
191
+ expect(res.status).toHaveBeenCalledWith(404);
192
+ });
193
+ it('destroys the stream when the response is closed early', async ()=>{
194
+ const res = buildRes();
195
+ mockExistsSync.mockReturnValue(true);
196
+ mockStatSync.mockReturnValue({
197
+ isFile: ()=>true,
198
+ size: 10
199
+ });
200
+ mockMimeLookup.mockReturnValue('application/pdf');
201
+ const stream = new FakeStream();
202
+ mockCreateReadStream.mockReturnValue(stream);
203
+ await middleware.use(buildReq('/storage/upload/file/report.pdf'), res, next);
204
+ res.emitClose();
205
+ expect(stream.destroy).toHaveBeenCalled();
206
+ });
207
+ });
@@ -0,0 +1,180 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) {
3
+ Object.defineProperty(obj, key, {
4
+ value: value,
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true
8
+ });
9
+ } else {
10
+ obj[key] = value;
11
+ }
12
+ return obj;
13
+ }
14
+ import { StorageModule } from './storage.module';
15
+ import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
16
+ import { FileManagerController, FolderController, StorageConfigController, UploadController } from '../controllers';
17
+ import { FileServeMiddleware } from '../middlewares';
18
+ function providerTokens(providers) {
19
+ return providers.map((p)=>typeof p === 'function' ? p : p.provide);
20
+ }
21
+ describe('StorageModule', ()=>{
22
+ describe('forRoot', ()=>{
23
+ it('should default to a non-global module with all controllers registered', ()=>{
24
+ const dynamicModule = StorageModule.forRoot({});
25
+ expect(dynamicModule.module).toBe(StorageModule);
26
+ expect(dynamicModule.global).toBe(false);
27
+ expect(dynamicModule.controllers).toEqual([
28
+ FileManagerController,
29
+ FolderController,
30
+ StorageConfigController,
31
+ UploadController
32
+ ]);
33
+ });
34
+ it('should respect global: true', ()=>{
35
+ expect(StorageModule.forRoot({
36
+ global: true
37
+ }).global).toBe(true);
38
+ });
39
+ it('should register no controllers when includeController is false', ()=>{
40
+ expect(StorageModule.forRoot({
41
+ includeController: false
42
+ }).controllers).toEqual([]);
43
+ });
44
+ it('should register controllers when includeController is anything other than exactly false', ()=>{
45
+ expect(StorageModule.forRoot({
46
+ includeController: true
47
+ }).controllers).toHaveLength(4);
48
+ expect(StorageModule.forRoot({}).controllers).toHaveLength(4);
49
+ });
50
+ it('should register a STORAGE_MODULE_OPTIONS provider carrying the given options', ()=>{
51
+ const options = {
52
+ bootstrapAppConfig: {
53
+ enableCompanyFeature: true
54
+ }
55
+ };
56
+ const dynamicModule = StorageModule.forRoot(options);
57
+ const provider = dynamicModule.providers.find((p)=>p.provide === STORAGE_MODULE_OPTIONS);
58
+ expect(provider.useValue).toBe(options);
59
+ });
60
+ it('should always export the same fixed set of services', ()=>{
61
+ const dynamicModule = StorageModule.forRoot({});
62
+ expect(dynamicModule.exports).toHaveLength(7);
63
+ });
64
+ });
65
+ describe('forRootAsync', ()=>{
66
+ it('should forward external imports alongside CacheModule/UtilsModule', ()=>{
67
+ let FakeImportedModule = class FakeImportedModule {
68
+ };
69
+ const dynamicModule = StorageModule.forRootAsync({
70
+ bootstrapAppConfig: {},
71
+ imports: [
72
+ FakeImportedModule
73
+ ]
74
+ });
75
+ expect(dynamicModule.imports).toEqual(expect.arrayContaining([
76
+ FakeImportedModule
77
+ ]));
78
+ });
79
+ it('should register no controllers when includeController is false', ()=>{
80
+ const dynamicModule = StorageModule.forRootAsync({
81
+ bootstrapAppConfig: {},
82
+ includeController: false
83
+ });
84
+ expect(dynamicModule.controllers).toEqual([]);
85
+ });
86
+ it('useFactory: builds a STORAGE_MODULE_OPTIONS provider that merges resolved config into the static options', async ()=>{
87
+ const useFactory = jest.fn().mockResolvedValue({
88
+ maxFileSize: 1024
89
+ });
90
+ const dynamicModule = StorageModule.forRootAsync({
91
+ bootstrapAppConfig: {},
92
+ useFactory,
93
+ inject: [
94
+ 'SOME_TOKEN'
95
+ ]
96
+ });
97
+ const provider = dynamicModule.providers.find((p)=>p.provide === STORAGE_MODULE_OPTIONS);
98
+ expect(provider.inject).toEqual([
99
+ 'SOME_TOKEN'
100
+ ]);
101
+ const resolved = await provider.useFactory('injected');
102
+ expect(useFactory).toHaveBeenCalledWith('injected');
103
+ expect(resolved.config).toEqual({
104
+ maxFileSize: 1024
105
+ });
106
+ });
107
+ it('useClass: registers both the class provider and an options provider calling createStorageOptions()', async ()=>{
108
+ let MyOptionsFactory = class MyOptionsFactory {
109
+ constructor(){
110
+ _define_property(this, "createStorageOptions", jest.fn().mockResolvedValue({
111
+ maxFileSize: 2048
112
+ }));
113
+ }
114
+ };
115
+ const dynamicModule = StorageModule.forRootAsync({
116
+ bootstrapAppConfig: {},
117
+ useClass: MyOptionsFactory
118
+ });
119
+ const classProvider = dynamicModule.providers.find((p)=>p.provide === MyOptionsFactory);
120
+ expect(classProvider.useClass).toBe(MyOptionsFactory);
121
+ const optionsProvider = dynamicModule.providers.find((p)=>p.provide === STORAGE_MODULE_OPTIONS);
122
+ const instance = new MyOptionsFactory();
123
+ const resolved = await optionsProvider.useFactory(instance);
124
+ expect(instance.createStorageOptions).toHaveBeenCalled();
125
+ expect(resolved.config).toEqual({
126
+ maxFileSize: 2048
127
+ });
128
+ });
129
+ it('useExisting: registers a single options provider without a duplicate class registration', async ()=>{
130
+ let MyOptionsFactory = class MyOptionsFactory {
131
+ constructor(){
132
+ _define_property(this, "createStorageOptions", jest.fn().mockResolvedValue({
133
+ maxFileSize: 4096
134
+ }));
135
+ }
136
+ };
137
+ const dynamicModule = StorageModule.forRootAsync({
138
+ bootstrapAppConfig: {},
139
+ useExisting: MyOptionsFactory
140
+ });
141
+ expect(dynamicModule.providers.filter((p)=>p.provide === MyOptionsFactory)).toHaveLength(0);
142
+ const optionsProvider = dynamicModule.providers.find((p)=>p.provide === STORAGE_MODULE_OPTIONS);
143
+ expect(optionsProvider.inject).toEqual([
144
+ MyOptionsFactory
145
+ ]);
146
+ });
147
+ it('should register no STORAGE_MODULE_OPTIONS provider when neither useFactory, useClass, nor useExisting is given', ()=>{
148
+ const dynamicModule = StorageModule.forRootAsync({
149
+ bootstrapAppConfig: {}
150
+ });
151
+ expect(providerTokens(dynamicModule.providers)).not.toContain(STORAGE_MODULE_OPTIONS);
152
+ });
153
+ it('should still register the fixed exported services as providers alongside async options', ()=>{
154
+ const dynamicModule = StorageModule.forRootAsync({
155
+ bootstrapAppConfig: {},
156
+ useFactory: jest.fn()
157
+ });
158
+ expect(dynamicModule.providers).toEqual(expect.arrayContaining([
159
+ ...dynamicModule.exports
160
+ ]));
161
+ });
162
+ });
163
+ describe('configure (NestModule)', ()=>{
164
+ it('should wire FileServeMiddleware to the file GET route', ()=>{
165
+ const forRoutes = jest.fn();
166
+ const apply = jest.fn().mockReturnValue({
167
+ forRoutes
168
+ });
169
+ const consumer = {
170
+ apply
171
+ };
172
+ new StorageModule().configure(consumer);
173
+ expect(apply).toHaveBeenCalledWith(FileServeMiddleware);
174
+ expect(forRoutes).toHaveBeenCalledWith({
175
+ path: 'storage/upload/file/*',
176
+ method: expect.anything()
177
+ });
178
+ });
179
+ });
180
+ });