@bernierllc/file-uploader 0.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.
package/.eslintrc.cjs ADDED
@@ -0,0 +1,33 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ module.exports = {
10
+ parser: '@typescript-eslint/parser',
11
+ extends: [
12
+ 'eslint:recommended',
13
+ 'plugin:@typescript-eslint/recommended'
14
+ ],
15
+ plugins: ['@typescript-eslint'],
16
+ parserOptions: {
17
+ ecmaVersion: 2020,
18
+ sourceType: 'module'
19
+ },
20
+ env: {
21
+ node: true,
22
+ es6: true,
23
+ jest: true
24
+ },
25
+ rules: {
26
+ '@typescript-eslint/explicit-function-return-type': 'off',
27
+ '@typescript-eslint/no-explicit-any': 'error',
28
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
29
+ '@typescript-eslint/no-non-null-assertion': 'warn'
30
+ },
31
+ ignorePatterns: ['dist/', 'node_modules/', '*.cjs']
32
+ };
33
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @bernierllc/file-uploader
2
+
3
+ Multipart file upload handler with validation and progress tracking
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bernierllc/file-uploader
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Multipart form parsing** - Handles multipart/form-data uploads
14
+ - **File validation** - Size limits, MIME type checking, file count limits
15
+ - **Progress tracking** - Real-time upload progress callbacks
16
+ - **Framework agnostic** - Works with Express, Koa, Fastify, etc.
17
+ - **Stream-based** - Efficient handling of large files
18
+ - **TypeScript support** - Full type safety
19
+
20
+ ## Usage
21
+
22
+ ### Basic Usage
23
+
24
+ ```typescript
25
+ import { FileUploader } from '@bernierllc/file-uploader';
26
+ import express from 'express';
27
+
28
+ const app = express();
29
+ const uploader = new FileUploader({
30
+ maxFileSize: 10 * 1024 * 1024, // 10MB
31
+ allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
32
+ onProgress: (progress) => {
33
+ console.log(`Upload progress: ${progress.percentage}%`);
34
+ }
35
+ });
36
+
37
+ app.post('/upload', async (req, res) => {
38
+ try {
39
+ const results = await uploader.handleMultipart(req);
40
+ res.json({ files: results });
41
+ } catch (error) {
42
+ res.status(400).json({ error: error.message });
43
+ }
44
+ });
45
+ ```
46
+
47
+ ### File Validation
48
+
49
+ ```typescript
50
+ const uploader = new FileUploader({
51
+ maxFileSize: 5 * 1024 * 1024, // 5MB
52
+ allowedMimeTypes: ['image/jpeg', 'image/png'],
53
+ maxFiles: 5
54
+ });
55
+
56
+ // Validate before upload
57
+ const fileInfo = {
58
+ fieldname: 'file',
59
+ filename: 'photo.jpg',
60
+ encoding: '7bit',
61
+ mimeType: 'image/jpeg',
62
+ size: 1024 * 1024
63
+ };
64
+
65
+ const validation = uploader.validateFile(fileInfo);
66
+ if (!validation.valid) {
67
+ console.error(validation.error);
68
+ }
69
+ ```
70
+
71
+ ### Progress Tracking
72
+
73
+ ```typescript
74
+ const uploader = new FileUploader({
75
+ onProgress: (progress) => {
76
+ console.log(`File ${progress.fileId}: ${progress.percentage}%`);
77
+ console.log(`Uploaded: ${progress.bytesUploaded}/${progress.totalBytes}`);
78
+ }
79
+ });
80
+ ```
81
+
82
+ ### Custom Destination
83
+
84
+ ```typescript
85
+ const uploader = new FileUploader({
86
+ destination: async (file) => {
87
+ // Generate custom path
88
+ return `/uploads/${Date.now()}-${file.filename}`;
89
+ }
90
+ });
91
+ ```
92
+
93
+ ## API
94
+
95
+ ### `new FileUploader(config?: UploadConfig)`
96
+
97
+ Creates a new file uploader instance.
98
+
99
+ ### `uploader.handleMultipart(req: IncomingMessage): Promise<UploadResult[]>`
100
+
101
+ Processes a multipart form upload request.
102
+
103
+ ### `uploader.validateFile(file: FileInfo): { valid: boolean; error?: string }`
104
+
105
+ Validates a file against configuration rules.
106
+
107
+ ### `uploader.getProgress(fileId: string): UploadProgress | null`
108
+
109
+ Gets upload progress for a specific file.
110
+
111
+ ## Integration Status
112
+
113
+ ### Logger Integration
114
+ **Status**: ✅ Integrated
115
+
116
+ Uses `@bernierllc/logger` for upload events, errors, and progress tracking.
117
+
118
+ ### NeverHub Integration
119
+ **Status**: ⚠️ Optional
120
+
121
+ Can emit upload events to NeverHub for distributed monitoring and observability. File uploader can publish events like `upload.started`, `upload.progress`, `upload.completed`, and `upload.failed` to NeverHub when available.
122
+
123
+ **Pattern**: Optional service discovery integration - package can emit upload lifecycle events to NeverHub for distributed monitoring.
124
+
125
+ **Example Integration**:
126
+ ```typescript
127
+ // If NeverHub is available, emit events
128
+ if (typeof detectNeverHub === 'function') {
129
+ uploader.on('upload.completed', (result) => {
130
+ neverhub.emit('file-uploader.upload.completed', result);
131
+ });
132
+ }
133
+ ```
134
+
135
+ ### Docs-Suite Integration
136
+ **Status**: ✅ Ready
137
+
138
+ TypeDoc-compatible JSDoc comments are included throughout the source code. All public APIs are documented with examples and type information.
139
+
140
+ ## License
141
+
142
+ Copyright (c) 2025 Bernier LLC. All rights reserved.
143
+
@@ -0,0 +1,517 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { FileUploader } from '../src';
10
+ import { IncomingMessage } from 'http';
11
+ import { Readable } from 'stream';
12
+ import type { FileInfo } from '../src/types';
13
+
14
+ // Mock @bernierllc/logger
15
+ jest.mock('@bernierllc/logger', () => ({
16
+ createLogger: jest.fn(() => ({
17
+ debug: jest.fn(),
18
+ info: jest.fn(),
19
+ warn: jest.fn(),
20
+ error: jest.fn()
21
+ }))
22
+ }));
23
+
24
+ /**
25
+ * Create a mock multipart request
26
+ */
27
+ function createMockRequest(parts: { fieldname: string; filename: string; mimeType: string; data: Buffer }[]): IncomingMessage {
28
+ const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW';
29
+
30
+ let body = '';
31
+ for (const part of parts) {
32
+ body += `--${boundary}\r\n`;
33
+ body += `Content-Disposition: form-data; name="${part.fieldname}"; filename="${part.filename}"\r\n`;
34
+ body += `Content-Type: ${part.mimeType}\r\n\r\n`;
35
+ body += part.data.toString('binary');
36
+ body += '\r\n';
37
+ }
38
+ body += `--${boundary}--\r\n`;
39
+
40
+ const stream = new Readable();
41
+ stream.push(Buffer.from(body, 'binary'));
42
+ stream.push(null);
43
+
44
+ const req = stream as IncomingMessage;
45
+ req.headers = {
46
+ 'content-type': `multipart/form-data; boundary=${boundary}`
47
+ };
48
+
49
+ return req;
50
+ }
51
+
52
+ describe('FileUploader', () => {
53
+ let uploader: FileUploader;
54
+
55
+ beforeEach(() => {
56
+ uploader = new FileUploader({
57
+ maxFileSize: 1024 * 1024, // 1MB
58
+ allowedMimeTypes: ['image/jpeg', 'image/png']
59
+ });
60
+ });
61
+
62
+ describe('constructor', () => {
63
+ it('should create a file uploader with default config', () => {
64
+ const defaultUploader = new FileUploader();
65
+ expect(defaultUploader).toBeInstanceOf(FileUploader);
66
+ });
67
+
68
+ it('should create a file uploader with custom config', () => {
69
+ expect(uploader).toBeInstanceOf(FileUploader);
70
+ });
71
+
72
+ it('should accept destination as string', () => {
73
+ const uploaderWithDestination = new FileUploader({
74
+ destination: '/custom/path'
75
+ });
76
+ expect(uploaderWithDestination).toBeInstanceOf(FileUploader);
77
+ });
78
+
79
+ it('should accept destination as function', () => {
80
+ const uploaderWithDestination = new FileUploader({
81
+ destination: async (file: FileInfo) => `/custom/${file.filename}`
82
+ });
83
+ expect(uploaderWithDestination).toBeInstanceOf(FileUploader);
84
+ });
85
+
86
+ it('should accept onProgress callback', () => {
87
+ const onProgress = jest.fn();
88
+ const uploaderWithProgress = new FileUploader({
89
+ onProgress
90
+ });
91
+ expect(uploaderWithProgress).toBeInstanceOf(FileUploader);
92
+ });
93
+ });
94
+
95
+ describe('validateFile', () => {
96
+ it('should validate file with allowed MIME type', () => {
97
+ const fileInfo: FileInfo = {
98
+ fieldname: 'file',
99
+ filename: 'test.jpg',
100
+ encoding: '7bit',
101
+ mimeType: 'image/jpeg',
102
+ size: 1000
103
+ };
104
+
105
+ const result = uploader.validateFile(fileInfo);
106
+ expect(result.valid).toBe(true);
107
+ expect(result.error).toBeUndefined();
108
+ });
109
+
110
+ it('should reject invalid MIME types', () => {
111
+ const fileInfo: FileInfo = {
112
+ fieldname: 'file',
113
+ filename: 'test.exe',
114
+ encoding: '7bit',
115
+ mimeType: 'application/x-msdownload',
116
+ size: 1000
117
+ };
118
+
119
+ const result = uploader.validateFile(fileInfo);
120
+ expect(result.valid).toBe(false);
121
+ expect(result.error).toContain('MIME type');
122
+ expect(result.error).toContain('application/x-msdownload');
123
+ });
124
+
125
+ it('should reject files exceeding size limit', () => {
126
+ const fileInfo: FileInfo = {
127
+ fieldname: 'file',
128
+ filename: 'test.jpg',
129
+ encoding: '7bit',
130
+ mimeType: 'image/jpeg',
131
+ size: 2 * 1024 * 1024 // 2MB
132
+ };
133
+
134
+ const result = uploader.validateFile(fileInfo);
135
+ expect(result.valid).toBe(false);
136
+ expect(result.error).toContain('File size');
137
+ expect(result.error).toContain('exceeds maximum');
138
+ });
139
+
140
+ it('should accept files when no MIME type restrictions', () => {
141
+ const noMimeUploader = new FileUploader({
142
+ maxFileSize: 1024 * 1024
143
+ });
144
+
145
+ const fileInfo: FileInfo = {
146
+ fieldname: 'file',
147
+ filename: 'test.exe',
148
+ encoding: '7bit',
149
+ mimeType: 'application/x-msdownload',
150
+ size: 1000
151
+ };
152
+
153
+ const result = noMimeUploader.validateFile(fileInfo);
154
+ expect(result.valid).toBe(true);
155
+ });
156
+
157
+ it('should accept files at size limit boundary', () => {
158
+ const fileInfo: FileInfo = {
159
+ fieldname: 'file',
160
+ filename: 'test.jpg',
161
+ encoding: '7bit',
162
+ mimeType: 'image/jpeg',
163
+ size: 1024 * 1024 // Exactly 1MB
164
+ };
165
+
166
+ const result = uploader.validateFile(fileInfo);
167
+ expect(result.valid).toBe(true);
168
+ });
169
+ });
170
+
171
+ describe('handleMultipart', () => {
172
+ it('should handle single file upload', async () => {
173
+ const mockData = Buffer.from('test file content');
174
+ const req = createMockRequest([
175
+ {
176
+ fieldname: 'file',
177
+ filename: 'test.jpg',
178
+ mimeType: 'image/jpeg',
179
+ data: mockData
180
+ }
181
+ ]);
182
+
183
+ const results = await uploader.handleMultipart(req);
184
+
185
+ expect(results).toHaveLength(1);
186
+ expect(results[0].filename).toBe('test.jpg');
187
+ expect(results[0].mimeType).toBe('image/jpeg');
188
+ expect(results[0].size).toBe(mockData.length);
189
+ expect(results[0].fileId).toBeDefined();
190
+ expect(results[0].destination).toContain('test.jpg');
191
+ });
192
+
193
+ it('should handle multiple file uploads', async () => {
194
+ const mockData1 = Buffer.from('file 1 content');
195
+ const mockData2 = Buffer.from('file 2 content');
196
+
197
+ const req = createMockRequest([
198
+ {
199
+ fieldname: 'file1',
200
+ filename: 'test1.jpg',
201
+ mimeType: 'image/jpeg',
202
+ data: mockData1
203
+ },
204
+ {
205
+ fieldname: 'file2',
206
+ filename: 'test2.png',
207
+ mimeType: 'image/png',
208
+ data: mockData2
209
+ }
210
+ ]);
211
+
212
+ const results = await uploader.handleMultipart(req);
213
+
214
+ expect(results).toHaveLength(2);
215
+ expect(results[0].filename).toBe('test1.jpg');
216
+ expect(results[1].filename).toBe('test2.png');
217
+ });
218
+
219
+ it('should reject when file count exceeds limit', async () => {
220
+ const uploaderWithLimit = new FileUploader({
221
+ maxFiles: 1,
222
+ allowedMimeTypes: ['image/jpeg', 'image/png']
223
+ });
224
+
225
+ const mockData1 = Buffer.from('file 1');
226
+ const mockData2 = Buffer.from('file 2');
227
+
228
+ const req = createMockRequest([
229
+ {
230
+ fieldname: 'file1',
231
+ filename: 'test1.jpg',
232
+ mimeType: 'image/jpeg',
233
+ data: mockData1
234
+ },
235
+ {
236
+ fieldname: 'file2',
237
+ filename: 'test2.png',
238
+ mimeType: 'image/png',
239
+ data: mockData2
240
+ }
241
+ ]);
242
+
243
+ await expect(uploaderWithLimit.handleMultipart(req)).rejects.toThrow('Maximum file count exceeded');
244
+ });
245
+
246
+ it('should reject files with invalid MIME type during upload', async () => {
247
+ const mockData = Buffer.from('malicious content');
248
+ const req = createMockRequest([
249
+ {
250
+ fieldname: 'file',
251
+ filename: 'malware.exe',
252
+ mimeType: 'application/x-msdownload',
253
+ data: mockData
254
+ }
255
+ ]);
256
+
257
+ await expect(uploader.handleMultipart(req)).rejects.toThrow('MIME type');
258
+ });
259
+
260
+ it('should reject files exceeding size during upload', async () => {
261
+ const mockData = Buffer.alloc(2 * 1024 * 1024); // 2MB
262
+ const req = createMockRequest([
263
+ {
264
+ fieldname: 'file',
265
+ filename: 'large.jpg',
266
+ mimeType: 'image/jpeg',
267
+ data: mockData
268
+ }
269
+ ]);
270
+
271
+ await expect(uploader.handleMultipart(req)).rejects.toThrow('File size exceeded');
272
+ });
273
+
274
+ it('should call onProgress callback during upload', async () => {
275
+ const onProgress = jest.fn();
276
+ const uploaderWithProgress = new FileUploader({
277
+ maxFileSize: 1024 * 1024,
278
+ allowedMimeTypes: ['image/jpeg'],
279
+ onProgress
280
+ });
281
+
282
+ const mockData = Buffer.from('test content for progress tracking');
283
+ const req = createMockRequest([
284
+ {
285
+ fieldname: 'file',
286
+ filename: 'test.jpg',
287
+ mimeType: 'image/jpeg',
288
+ data: mockData
289
+ }
290
+ ]);
291
+
292
+ await uploaderWithProgress.handleMultipart(req);
293
+
294
+ expect(onProgress).toHaveBeenCalled();
295
+ const lastCall = onProgress.mock.calls[onProgress.mock.calls.length - 1][0];
296
+ expect(lastCall.percentage).toBe(100);
297
+ expect(lastCall.bytesUploaded).toBe(mockData.length);
298
+ expect(lastCall.totalBytes).toBe(mockData.length);
299
+ });
300
+
301
+ it('should use custom destination string', async () => {
302
+ const uploaderWithDest = new FileUploader({
303
+ destination: '/custom/uploads',
304
+ allowedMimeTypes: ['image/jpeg']
305
+ });
306
+
307
+ const mockData = Buffer.from('test');
308
+ const req = createMockRequest([
309
+ {
310
+ fieldname: 'file',
311
+ filename: 'test.jpg',
312
+ mimeType: 'image/jpeg',
313
+ data: mockData
314
+ }
315
+ ]);
316
+
317
+ const results = await uploaderWithDest.handleMultipart(req);
318
+ expect(results[0].destination).toBe('/custom/uploads');
319
+ });
320
+
321
+ it('should use custom destination function', async () => {
322
+ const uploaderWithDest = new FileUploader({
323
+ destination: async (file: FileInfo) => `/custom/${file.filename}`,
324
+ allowedMimeTypes: ['image/jpeg']
325
+ });
326
+
327
+ const mockData = Buffer.from('test');
328
+ const req = createMockRequest([
329
+ {
330
+ fieldname: 'file',
331
+ filename: 'test.jpg',
332
+ mimeType: 'image/jpeg',
333
+ data: mockData
334
+ }
335
+ ]);
336
+
337
+ const results = await uploaderWithDest.handleMultipart(req);
338
+ expect(results[0].destination).toBe('/custom/test.jpg');
339
+ });
340
+
341
+ it('should handle busboy error', async () => {
342
+ const req = new Readable() as IncomingMessage;
343
+ req.headers = {
344
+ 'content-type': 'invalid-content-type'
345
+ };
346
+ req.push('invalid data');
347
+ req.push(null);
348
+
349
+ await expect(uploader.handleMultipart(req)).rejects.toThrow();
350
+ });
351
+
352
+ it('should calculate progress percentage correctly', async () => {
353
+ const progressUpdates: number[] = [];
354
+ const uploaderWithProgress = new FileUploader({
355
+ maxFileSize: 1024 * 1024,
356
+ allowedMimeTypes: ['image/jpeg'],
357
+ onProgress: (progress) => {
358
+ progressUpdates.push(progress.percentage);
359
+ }
360
+ });
361
+
362
+ const mockData = Buffer.from('test content');
363
+ const req = createMockRequest([
364
+ {
365
+ fieldname: 'file',
366
+ filename: 'test.jpg',
367
+ mimeType: 'image/jpeg',
368
+ data: mockData
369
+ }
370
+ ]);
371
+
372
+ await uploaderWithProgress.handleMultipart(req);
373
+
374
+ expect(progressUpdates.length).toBeGreaterThan(0);
375
+ expect(progressUpdates[progressUpdates.length - 1]).toBe(100);
376
+ });
377
+ });
378
+
379
+ describe('getProgress', () => {
380
+ it('should return null for unknown file ID', () => {
381
+ const progress = uploader.getProgress('unknown-id');
382
+ expect(progress).toBeNull();
383
+ });
384
+
385
+ it('should return progress during active upload', async () => {
386
+ const onProgress = jest.fn();
387
+ const uploaderWithProgress = new FileUploader({
388
+ maxFileSize: 1024 * 1024,
389
+ allowedMimeTypes: ['image/jpeg'],
390
+ onProgress
391
+ });
392
+
393
+ const mockData = Buffer.from('test');
394
+ const req = createMockRequest([
395
+ {
396
+ fieldname: 'file',
397
+ filename: 'test.jpg',
398
+ mimeType: 'image/jpeg',
399
+ data: mockData
400
+ }
401
+ ]);
402
+
403
+ await uploaderWithProgress.handleMultipart(req);
404
+
405
+ // Progress should be cleared after upload completes
406
+ const fileId = onProgress.mock.calls[0][0].fileId;
407
+ const progress = uploaderWithProgress.getProgress(fileId);
408
+ expect(progress).toBeNull(); // Cleared after completion
409
+ });
410
+ });
411
+
412
+ describe('edge cases', () => {
413
+ it('should handle empty file', async () => {
414
+ const mockData = Buffer.alloc(0);
415
+ const req = createMockRequest([
416
+ {
417
+ fieldname: 'file',
418
+ filename: 'empty.jpg',
419
+ mimeType: 'image/jpeg',
420
+ data: mockData
421
+ }
422
+ ]);
423
+
424
+ const results = await uploader.handleMultipart(req);
425
+ expect(results).toHaveLength(1);
426
+ expect(results[0].size).toBe(0);
427
+ });
428
+
429
+ it('should handle special characters in filename', async () => {
430
+ const mockData = Buffer.from('test');
431
+ const req = createMockRequest([
432
+ {
433
+ fieldname: 'file',
434
+ filename: 'test file (1) [copy].jpg',
435
+ mimeType: 'image/jpeg',
436
+ data: mockData
437
+ }
438
+ ]);
439
+
440
+ const results = await uploader.handleMultipart(req);
441
+ expect(results).toHaveLength(1);
442
+ expect(results[0].filename).toBe('test file (1) [copy].jpg');
443
+ });
444
+
445
+ it('should handle no files in request', async () => {
446
+ const req = createMockRequest([]);
447
+
448
+ const results = await uploader.handleMultipart(req);
449
+ expect(results).toHaveLength(0);
450
+ });
451
+
452
+ it('should handle mixed valid and invalid files', async () => {
453
+ const mockData1 = Buffer.from('valid file');
454
+ const mockData2 = Buffer.from('invalid file');
455
+
456
+ const req = createMockRequest([
457
+ {
458
+ fieldname: 'file1',
459
+ filename: 'test.jpg',
460
+ mimeType: 'image/jpeg',
461
+ data: mockData1
462
+ },
463
+ {
464
+ fieldname: 'file2',
465
+ filename: 'malware.exe',
466
+ mimeType: 'application/x-msdownload',
467
+ data: mockData2
468
+ }
469
+ ]);
470
+
471
+ await expect(uploader.handleMultipart(req)).rejects.toThrow('MIME type');
472
+ });
473
+
474
+ it('should handle destination function throwing non-Error', async () => {
475
+ const uploaderWithThrowingDest = new FileUploader({
476
+ destination: () => {
477
+ // eslint-disable-next-line @typescript-eslint/no-throw-literal
478
+ throw 'String error';
479
+ },
480
+ allowedMimeTypes: ['image/jpeg']
481
+ });
482
+
483
+ const mockData = Buffer.from('test');
484
+ const req = createMockRequest([
485
+ {
486
+ fieldname: 'file',
487
+ filename: 'test.jpg',
488
+ mimeType: 'image/jpeg',
489
+ data: mockData
490
+ }
491
+ ]);
492
+
493
+ await expect(uploaderWithThrowingDest.handleMultipart(req)).rejects.toThrow();
494
+ });
495
+
496
+ it('should handle destination function throwing Error', async () => {
497
+ const uploaderWithErrorDest = new FileUploader({
498
+ destination: () => {
499
+ throw new Error('Custom destination error');
500
+ },
501
+ allowedMimeTypes: ['image/jpeg']
502
+ });
503
+
504
+ const mockData = Buffer.from('test');
505
+ const req = createMockRequest([
506
+ {
507
+ fieldname: 'file',
508
+ filename: 'test.jpg',
509
+ mimeType: 'image/jpeg',
510
+ data: mockData
511
+ }
512
+ ]);
513
+
514
+ await expect(uploaderWithErrorDest.handleMultipart(req)).rejects.toThrow('Custom destination error');
515
+ });
516
+ });
517
+ });