@flusys/nestjs-storage 6.0.1 → 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.
- package/cjs/controllers/file-manager.controller.spec.js +135 -0
- package/cjs/controllers/folder.controller.spec.js +86 -0
- package/cjs/controllers/storage-config.controller.spec.js +85 -0
- package/cjs/controllers/upload.controller.spec.js +132 -0
- package/cjs/docs/storage-swagger.config.spec.js +72 -0
- package/cjs/entities/index.spec.js +18 -0
- package/cjs/middlewares/file-serve.middleware.spec.js +211 -0
- package/cjs/modules/storage.module.spec.js +184 -0
- package/cjs/providers/azure-provider.optional.spec.js +228 -0
- package/cjs/providers/local-provider.spec.js +239 -0
- package/cjs/providers/s3-provider.optional.spec.js +243 -0
- package/cjs/providers/sftp-provider.optional.spec.js +217 -0
- package/cjs/providers/storage-factory.service.spec.js +253 -0
- package/cjs/providers/storage-provider.registry.spec.js +67 -0
- package/cjs/services/file-manager.service.spec.js +549 -0
- package/cjs/services/folder.service.spec.js +163 -0
- package/cjs/services/storage-config.service.spec.js +257 -0
- package/cjs/services/storage-datasource.provider.spec.js +217 -0
- package/cjs/services/storage-provider-config.service.spec.js +243 -0
- package/cjs/services/upload.service.spec.js +372 -0
- package/cjs/utils/file-validator.util.spec.js +259 -0
- package/cjs/utils/image-compressor.util.spec.js +166 -0
- package/fesm/controllers/file-manager.controller.spec.js +131 -0
- package/fesm/controllers/folder.controller.spec.js +82 -0
- package/fesm/controllers/storage-config.controller.spec.js +81 -0
- package/fesm/controllers/upload.controller.spec.js +128 -0
- package/fesm/docs/storage-swagger.config.spec.js +68 -0
- package/fesm/entities/index.spec.js +14 -0
- package/fesm/middlewares/file-serve.middleware.spec.js +207 -0
- package/fesm/modules/storage.module.spec.js +180 -0
- package/fesm/providers/azure-provider.optional.spec.js +224 -0
- package/fesm/providers/local-provider.spec.js +194 -0
- package/fesm/providers/s3-provider.optional.spec.js +239 -0
- package/fesm/providers/sftp-provider.optional.spec.js +213 -0
- package/fesm/providers/storage-factory.service.spec.js +249 -0
- package/fesm/providers/storage-provider.registry.spec.js +63 -0
- package/fesm/services/file-manager.service.spec.js +545 -0
- package/fesm/services/folder.service.spec.js +159 -0
- package/fesm/services/storage-config.service.spec.js +253 -0
- package/fesm/services/storage-datasource.provider.spec.js +172 -0
- package/fesm/services/storage-provider-config.service.spec.js +239 -0
- package/fesm/services/upload.service.spec.js +368 -0
- package/fesm/utils/file-validator.util.spec.js +255 -0
- package/fesm/utils/image-compressor.util.spec.js +162 -0
- package/package.json +3 -3
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { FileValidator } from './file-validator.util';
|
|
2
|
+
const JPEG_BYTES = Buffer.from([
|
|
3
|
+
0xff,
|
|
4
|
+
0xd8,
|
|
5
|
+
0xff,
|
|
6
|
+
0xe0,
|
|
7
|
+
0x00,
|
|
8
|
+
0x10
|
|
9
|
+
]);
|
|
10
|
+
const PNG_BYTES = Buffer.from([
|
|
11
|
+
0x89,
|
|
12
|
+
0x50,
|
|
13
|
+
0x4e,
|
|
14
|
+
0x47,
|
|
15
|
+
0x0d,
|
|
16
|
+
0x0a,
|
|
17
|
+
0x1a,
|
|
18
|
+
0x0a,
|
|
19
|
+
0x00,
|
|
20
|
+
0x00
|
|
21
|
+
]);
|
|
22
|
+
const PDF_BYTES = Buffer.from([
|
|
23
|
+
0x25,
|
|
24
|
+
0x50,
|
|
25
|
+
0x44,
|
|
26
|
+
0x46,
|
|
27
|
+
0x2d,
|
|
28
|
+
0x31,
|
|
29
|
+
0x2e,
|
|
30
|
+
0x34
|
|
31
|
+
]);
|
|
32
|
+
const ZIP_BYTES = Buffer.from([
|
|
33
|
+
0x50,
|
|
34
|
+
0x4b,
|
|
35
|
+
0x03,
|
|
36
|
+
0x04,
|
|
37
|
+
0x00,
|
|
38
|
+
0x00
|
|
39
|
+
]);
|
|
40
|
+
const GIF87_BYTES = Buffer.from([
|
|
41
|
+
0x47,
|
|
42
|
+
0x49,
|
|
43
|
+
0x46,
|
|
44
|
+
0x38,
|
|
45
|
+
0x37,
|
|
46
|
+
0x61
|
|
47
|
+
]);
|
|
48
|
+
const UNKNOWN_BYTES = Buffer.from([
|
|
49
|
+
0x01,
|
|
50
|
+
0x02,
|
|
51
|
+
0x03,
|
|
52
|
+
0x04
|
|
53
|
+
]);
|
|
54
|
+
describe('FileValidator', ()=>{
|
|
55
|
+
describe('detectFileType', ()=>{
|
|
56
|
+
it('detects JPEG from magic bytes', ()=>{
|
|
57
|
+
expect(FileValidator.detectFileType(JPEG_BYTES)).toBe('image/jpeg');
|
|
58
|
+
});
|
|
59
|
+
it('detects PNG from magic bytes', ()=>{
|
|
60
|
+
expect(FileValidator.detectFileType(PNG_BYTES)).toBe('image/png');
|
|
61
|
+
});
|
|
62
|
+
it('detects PDF from magic bytes', ()=>{
|
|
63
|
+
expect(FileValidator.detectFileType(PDF_BYTES)).toBe('application/pdf');
|
|
64
|
+
});
|
|
65
|
+
it('detects ZIP-based formats (docx/xlsx/pptx share this signature)', ()=>{
|
|
66
|
+
expect(FileValidator.detectFileType(ZIP_BYTES)).toBe('application/zip');
|
|
67
|
+
});
|
|
68
|
+
it('detects legacy GIF87a signature', ()=>{
|
|
69
|
+
expect(FileValidator.detectFileType(GIF87_BYTES)).toBe('image/gif');
|
|
70
|
+
});
|
|
71
|
+
it('returns null for unknown/unrecognized signatures', ()=>{
|
|
72
|
+
expect(FileValidator.detectFileType(UNKNOWN_BYTES)).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
it('returns null for a buffer shorter than any known signature', ()=>{
|
|
75
|
+
expect(FileValidator.detectFileType(Buffer.from([
|
|
76
|
+
0xff
|
|
77
|
+
]))).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
it('returns null for an empty buffer', ()=>{
|
|
80
|
+
expect(FileValidator.detectFileType(Buffer.alloc(0))).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('isTextBasedType', ()=>{
|
|
84
|
+
it('recognizes plain text and structured text types', ()=>{
|
|
85
|
+
expect(FileValidator.isTextBasedType('text/plain')).toBe(true);
|
|
86
|
+
expect(FileValidator.isTextBasedType('application/json')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('rejects a non text-based type', ()=>{
|
|
89
|
+
expect(FileValidator.isTextBasedType('image/png')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('isDangerousTextType', ()=>{
|
|
93
|
+
it('flags text/html as dangerous', ()=>{
|
|
94
|
+
expect(FileValidator.isDangerousTextType('text/html')).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
it('flags image/svg+xml as dangerous (can embed scripts)', ()=>{
|
|
97
|
+
expect(FileValidator.isDangerousTextType('image/svg+xml')).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it('does not flag a safe type', ()=>{
|
|
100
|
+
expect(FileValidator.isDangerousTextType('image/png')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('mimeTypesMatch', ()=>{
|
|
104
|
+
it('matches identical types', ()=>{
|
|
105
|
+
expect(FileValidator.mimeTypesMatch('image/png', 'image/png')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it('matches known aliases (jpeg <-> jpg)', ()=>{
|
|
108
|
+
expect(FileValidator.mimeTypesMatch('image/jpeg', 'image/jpg')).toBe(true);
|
|
109
|
+
expect(FileValidator.mimeTypesMatch('image/jpg', 'image/jpeg')).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
it('matches same broad category (image/* vs image/*)', ()=>{
|
|
112
|
+
expect(FileValidator.mimeTypesMatch('image/png', 'image/webp')).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it('matches ZIP-detected content against an office document declared type', ()=>{
|
|
115
|
+
expect(FileValidator.mimeTypesMatch('application/zip', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
it('does not match unrelated categories', ()=>{
|
|
118
|
+
expect(FileValidator.mimeTypesMatch('image/png', 'application/pdf')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('isTypeAllowed', ()=>{
|
|
122
|
+
it('allows anything when wildcard */* is present', ()=>{
|
|
123
|
+
expect(FileValidator.isTypeAllowed('application/pdf', [
|
|
124
|
+
'*/*'
|
|
125
|
+
])).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
it('allows category wildcard image/*', ()=>{
|
|
128
|
+
expect(FileValidator.isTypeAllowed('image/png', [
|
|
129
|
+
'image/*'
|
|
130
|
+
])).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
it('rejects a type outside the category wildcard', ()=>{
|
|
133
|
+
expect(FileValidator.isTypeAllowed('application/pdf', [
|
|
134
|
+
'image/*'
|
|
135
|
+
])).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
it('allows exact match', ()=>{
|
|
138
|
+
expect(FileValidator.isTypeAllowed('application/pdf', [
|
|
139
|
+
'application/pdf'
|
|
140
|
+
])).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
it('rejects when not in the explicit allow-list', ()=>{
|
|
143
|
+
expect(FileValidator.isTypeAllowed('application/pdf', [
|
|
144
|
+
'image/png',
|
|
145
|
+
'image/jpeg'
|
|
146
|
+
])).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('validateFileContent', ()=>{
|
|
150
|
+
it('passes when magic bytes match declared type and type is allowed', ()=>{
|
|
151
|
+
const result = FileValidator.validateFileContent(JPEG_BYTES, 'image/jpeg', [
|
|
152
|
+
'image/*'
|
|
153
|
+
]);
|
|
154
|
+
expect(result.valid).toBe(true);
|
|
155
|
+
expect(result.detectedType).toBe('image/jpeg');
|
|
156
|
+
});
|
|
157
|
+
it('fails when declared type does not match detected content (MIME spoofing)', ()=>{
|
|
158
|
+
const result = FileValidator.validateFileContent(PNG_BYTES, 'application/pdf', [
|
|
159
|
+
'*/*'
|
|
160
|
+
]);
|
|
161
|
+
expect(result.valid).toBe(false);
|
|
162
|
+
expect(result.message).toContain('does not match declared type');
|
|
163
|
+
});
|
|
164
|
+
it('fails when detected type is not in the allow-list', ()=>{
|
|
165
|
+
const result = FileValidator.validateFileContent(PDF_BYTES, 'application/pdf', [
|
|
166
|
+
'image/*'
|
|
167
|
+
]);
|
|
168
|
+
expect(result.valid).toBe(false);
|
|
169
|
+
expect(result.message).toContain('not allowed');
|
|
170
|
+
});
|
|
171
|
+
it('trusts declared type for safe text-based content without magic bytes', ()=>{
|
|
172
|
+
const result = FileValidator.validateFileContent(Buffer.from('name,age\nJohn,30'), 'text/csv', [
|
|
173
|
+
'text/*'
|
|
174
|
+
]);
|
|
175
|
+
expect(result.valid).toBe(true);
|
|
176
|
+
expect(result.detectedType).toBe('text/csv');
|
|
177
|
+
});
|
|
178
|
+
it('rejects dangerous text/html content unless explicitly allow-listed', ()=>{
|
|
179
|
+
const result = FileValidator.validateFileContent(Buffer.from('<html><body>hi</body></html>'), 'text/html', [
|
|
180
|
+
'*/*'
|
|
181
|
+
]);
|
|
182
|
+
expect(result.valid).toBe(false);
|
|
183
|
+
expect(result.message).toContain('potentially dangerous');
|
|
184
|
+
});
|
|
185
|
+
it('rejects image/svg+xml by default (can contain embedded scripts)', ()=>{
|
|
186
|
+
const result = FileValidator.validateFileContent(Buffer.from('<svg onload="alert(1)"></svg>'), 'image/svg+xml', [
|
|
187
|
+
'*/*'
|
|
188
|
+
]);
|
|
189
|
+
expect(result.valid).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
it('still fails text/html even when explicitly allow-listed, since it is not in TEXT_BASED_TYPES', ()=>{
|
|
192
|
+
// NOTE: validateUndetectedType() skips the "dangerous type" rejection when the type is
|
|
193
|
+
// explicitly allow-listed, but text/html is absent from TEXT_BASED_TYPES so it falls through
|
|
194
|
+
// to the generic "Unable to verify file type" failure. Documenting actual behavior here;
|
|
195
|
+
// see final report for this being flagged as a potential production logic quirk.
|
|
196
|
+
const result = FileValidator.validateFileContent(Buffer.from('<html></html>'), 'text/html', [
|
|
197
|
+
'text/html'
|
|
198
|
+
]);
|
|
199
|
+
expect(result.valid).toBe(false);
|
|
200
|
+
expect(result.message).toContain('Unable to verify file type');
|
|
201
|
+
});
|
|
202
|
+
it('fails for binary content with no recognizable signature and no text fallback', ()=>{
|
|
203
|
+
const result = FileValidator.validateFileContent(UNKNOWN_BYTES, 'application/octet-stream', [
|
|
204
|
+
'*/*'
|
|
205
|
+
]);
|
|
206
|
+
expect(result.valid).toBe(false);
|
|
207
|
+
expect(result.message).toContain('Unable to verify file type');
|
|
208
|
+
});
|
|
209
|
+
it('defaults allowedTypes to */* when not provided', ()=>{
|
|
210
|
+
const result = FileValidator.validateFileContent(JPEG_BYTES, 'image/jpeg');
|
|
211
|
+
expect(result.valid).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
it('returns a failure result instead of throwing on unexpected internal errors', ()=>{
|
|
214
|
+
// Passing a non-buffer-like value to force detectFileType internals to misbehave gracefully
|
|
215
|
+
const brokenBuffer = {
|
|
216
|
+
length: 10
|
|
217
|
+
};
|
|
218
|
+
const result = FileValidator.validateFileContent(brokenBuffer, 'image/jpeg', [
|
|
219
|
+
'*/*'
|
|
220
|
+
]);
|
|
221
|
+
expect(result.valid).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('sanitizeFilename', ()=>{
|
|
225
|
+
it('strips directory components to prevent path traversal', ()=>{
|
|
226
|
+
expect(FileValidator.sanitizeFilename('../../etc/passwd')).toBe('passwd');
|
|
227
|
+
});
|
|
228
|
+
it('strips Windows-style directory components', ()=>{
|
|
229
|
+
expect(FileValidator.sanitizeFilename('..\\..\\windows\\system32\\evil.exe')).toBe('evil.exe');
|
|
230
|
+
});
|
|
231
|
+
it('removes null bytes', ()=>{
|
|
232
|
+
expect(FileValidator.sanitizeFilename('file\0.txt')).toBe('file.txt');
|
|
233
|
+
});
|
|
234
|
+
it('collapses multiple consecutive dots into one', ()=>{
|
|
235
|
+
expect(FileValidator.sanitizeFilename('archive....tar.gz')).toBe('archive.tar.gz');
|
|
236
|
+
});
|
|
237
|
+
it('replaces special/unsafe characters with underscores', ()=>{
|
|
238
|
+
expect(FileValidator.sanitizeFilename('my file (final)!.txt')).toBe('my_file__final__.txt');
|
|
239
|
+
});
|
|
240
|
+
it('truncates filenames longer than 255 characters', ()=>{
|
|
241
|
+
const longName = `${'a'.repeat(300)}.txt`;
|
|
242
|
+
const sanitized = FileValidator.sanitizeFilename(longName);
|
|
243
|
+
expect(sanitized.length).toBeLessThanOrEqual(255);
|
|
244
|
+
});
|
|
245
|
+
it('preserves a simple safe filename unchanged', ()=>{
|
|
246
|
+
expect(FileValidator.sanitizeFilename('report-2024_final.pdf')).toBe('report-2024_final.pdf');
|
|
247
|
+
});
|
|
248
|
+
it('neutralizes an absolute-path traversal attempt embedded mid-string', ()=>{
|
|
249
|
+
const malicious = '/var/www/../../etc/passwd';
|
|
250
|
+
const sanitized = FileValidator.sanitizeFilename(malicious);
|
|
251
|
+
expect(sanitized).not.toContain('/');
|
|
252
|
+
expect(sanitized).not.toContain('..');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const mockToBuffer = jest.fn();
|
|
2
|
+
const mockJpeg = jest.fn();
|
|
3
|
+
const mockPng = jest.fn();
|
|
4
|
+
const mockWebp = jest.fn();
|
|
5
|
+
const mockAvif = jest.fn();
|
|
6
|
+
const mockTiff = jest.fn();
|
|
7
|
+
const mockGif = jest.fn();
|
|
8
|
+
const mockJp2 = jest.fn();
|
|
9
|
+
const mockResize = jest.fn();
|
|
10
|
+
function buildChainableImage() {
|
|
11
|
+
const image = {};
|
|
12
|
+
image.resize = mockResize.mockReturnValue(image);
|
|
13
|
+
image.jpeg = mockJpeg.mockReturnValue(image);
|
|
14
|
+
image.png = mockPng.mockReturnValue(image);
|
|
15
|
+
image.webp = mockWebp.mockReturnValue(image);
|
|
16
|
+
image.avif = mockAvif.mockReturnValue(image);
|
|
17
|
+
image.tiff = mockTiff.mockReturnValue(image);
|
|
18
|
+
image.gif = mockGif.mockReturnValue(image);
|
|
19
|
+
image.jp2 = mockJp2.mockReturnValue(image);
|
|
20
|
+
image.toBuffer = mockToBuffer;
|
|
21
|
+
return image;
|
|
22
|
+
}
|
|
23
|
+
const mockSharpFactory = jest.fn();
|
|
24
|
+
jest.mock('sharp', ()=>{
|
|
25
|
+
const factory = (...args)=>mockSharpFactory(...args);
|
|
26
|
+
return {
|
|
27
|
+
__esModule: true,
|
|
28
|
+
default: factory
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
import { ImageCompressor } from './image-compressor.util';
|
|
32
|
+
describe('ImageCompressor', ()=>{
|
|
33
|
+
beforeEach(()=>{
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
mockSharpFactory.mockImplementation(()=>buildChainableImage());
|
|
36
|
+
mockToBuffer.mockResolvedValue(Buffer.from('compressed-image-data'));
|
|
37
|
+
});
|
|
38
|
+
it('returns the buffer unchanged for image/svg+xml without invoking sharp', async ()=>{
|
|
39
|
+
const buffer = Buffer.from('<svg></svg>');
|
|
40
|
+
const result = await ImageCompressor.compress(buffer, 'image/svg+xml');
|
|
41
|
+
expect(result.buffer).toBe(buffer);
|
|
42
|
+
expect(result.format).toBe('image/svg+xml');
|
|
43
|
+
expect(mockSharpFactory).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
it('resizes using default maxWidth/maxHeight when no options are provided', async ()=>{
|
|
46
|
+
await ImageCompressor.compress(Buffer.from('jpeg-bytes'), 'image/jpeg');
|
|
47
|
+
expect(mockResize).toHaveBeenCalledWith(expect.objectContaining({
|
|
48
|
+
width: 1280,
|
|
49
|
+
height: 1280,
|
|
50
|
+
fit: 'inside',
|
|
51
|
+
withoutEnlargement: true
|
|
52
|
+
}));
|
|
53
|
+
});
|
|
54
|
+
it('resizes using custom maxWidth/maxHeight when provided', async ()=>{
|
|
55
|
+
await ImageCompressor.compress(Buffer.from('jpeg-bytes'), 'image/jpeg', {
|
|
56
|
+
maxWidth: 500,
|
|
57
|
+
maxHeight: 600
|
|
58
|
+
});
|
|
59
|
+
expect(mockResize).toHaveBeenCalledWith(expect.objectContaining({
|
|
60
|
+
width: 500,
|
|
61
|
+
height: 600
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
it('preserves the original format (derived from mimetype) when format is "original"', async ()=>{
|
|
65
|
+
const result = await ImageCompressor.compress(Buffer.from('png-bytes'), 'image/png', {
|
|
66
|
+
format: 'original'
|
|
67
|
+
});
|
|
68
|
+
expect(mockPng).toHaveBeenCalled();
|
|
69
|
+
expect(result.format).toBe('image/png');
|
|
70
|
+
});
|
|
71
|
+
it('applies jpeg-specific compression options with quality', async ()=>{
|
|
72
|
+
await ImageCompressor.compress(Buffer.from('bytes'), 'image/jpeg', {
|
|
73
|
+
quality: 70
|
|
74
|
+
});
|
|
75
|
+
expect(mockJpeg).toHaveBeenCalledWith(expect.objectContaining({
|
|
76
|
+
quality: 70,
|
|
77
|
+
mozjpeg: true
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
it('applies png-specific compression options', async ()=>{
|
|
81
|
+
await ImageCompressor.compress(Buffer.from('bytes'), 'image/png', {
|
|
82
|
+
quality: 90
|
|
83
|
+
});
|
|
84
|
+
expect(mockPng).toHaveBeenCalledWith(expect.objectContaining({
|
|
85
|
+
quality: 90,
|
|
86
|
+
colors: 256
|
|
87
|
+
}));
|
|
88
|
+
});
|
|
89
|
+
it('applies webp-specific options and sets lossless=true only when quality is 100', async ()=>{
|
|
90
|
+
await ImageCompressor.compress(Buffer.from('bytes'), 'image/webp', {
|
|
91
|
+
quality: 100,
|
|
92
|
+
format: 'webp'
|
|
93
|
+
});
|
|
94
|
+
expect(mockWebp).toHaveBeenCalledWith(expect.objectContaining({
|
|
95
|
+
quality: 100,
|
|
96
|
+
lossless: true
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
99
|
+
it('applies webp-specific options with lossless=false for quality < 100', async ()=>{
|
|
100
|
+
await ImageCompressor.compress(Buffer.from('bytes'), 'image/webp', {
|
|
101
|
+
quality: 80,
|
|
102
|
+
format: 'webp'
|
|
103
|
+
});
|
|
104
|
+
expect(mockWebp).toHaveBeenCalledWith(expect.objectContaining({
|
|
105
|
+
quality: 80,
|
|
106
|
+
lossless: false
|
|
107
|
+
}));
|
|
108
|
+
});
|
|
109
|
+
it('applies avif-specific compression options', async ()=>{
|
|
110
|
+
await ImageCompressor.compress(Buffer.from('bytes'), 'image/avif', {
|
|
111
|
+
format: 'avif'
|
|
112
|
+
});
|
|
113
|
+
expect(mockAvif).toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
it('applies tiff-specific compression options', async ()=>{
|
|
116
|
+
await ImageCompressor.compress(Buffer.from('bytes'), 'image/tiff', {
|
|
117
|
+
format: 'tiff'
|
|
118
|
+
});
|
|
119
|
+
expect(mockTiff).toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
it('applies gif-specific compression options', async ()=>{
|
|
122
|
+
await ImageCompressor.compress(Buffer.from('bytes'), 'image/gif', {
|
|
123
|
+
format: 'gif'
|
|
124
|
+
});
|
|
125
|
+
expect(mockGif).toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
it('applies jp2-specific compression options', async ()=>{
|
|
128
|
+
await ImageCompressor.compress(Buffer.from('bytes'), 'image/jp2', {
|
|
129
|
+
format: 'jp2'
|
|
130
|
+
});
|
|
131
|
+
expect(mockJp2).toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
it('falls back to webp for an unrecognized target format', async ()=>{
|
|
134
|
+
const result = await ImageCompressor.compress(Buffer.from('bytes'), 'image/unknownformat', {
|
|
135
|
+
format: 'original'
|
|
136
|
+
});
|
|
137
|
+
expect(mockWebp).toHaveBeenCalled();
|
|
138
|
+
expect(result.format).toBe('image/webp');
|
|
139
|
+
});
|
|
140
|
+
it('returns the compressed buffer and content-type prefixed format on success', async ()=>{
|
|
141
|
+
mockToBuffer.mockResolvedValue(Buffer.from('final-bytes'));
|
|
142
|
+
const result = await ImageCompressor.compress(Buffer.from('bytes'), 'image/jpeg', {
|
|
143
|
+
format: 'jpeg'
|
|
144
|
+
});
|
|
145
|
+
expect(result.buffer).toEqual(Buffer.from('final-bytes'));
|
|
146
|
+
expect(result.format).toBe('image/jpeg');
|
|
147
|
+
});
|
|
148
|
+
it('falls back to the original buffer and mimetype when sharp processing throws', async ()=>{
|
|
149
|
+
const originalBuffer = Buffer.from('original-bytes');
|
|
150
|
+
mockToBuffer.mockRejectedValue(new Error('corrupt image'));
|
|
151
|
+
const result = await ImageCompressor.compress(originalBuffer, 'image/png');
|
|
152
|
+
expect(result.buffer).toBe(originalBuffer);
|
|
153
|
+
expect(result.format).toBe('image/png');
|
|
154
|
+
});
|
|
155
|
+
it('initializes sharp with failOnError=false and a density hint', async ()=>{
|
|
156
|
+
await ImageCompressor.compress(Buffer.from('bytes'), 'image/jpeg');
|
|
157
|
+
expect(mockSharpFactory).toHaveBeenCalledWith(expect.any(Buffer), expect.objectContaining({
|
|
158
|
+
failOnError: false,
|
|
159
|
+
density: 300
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
162
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-storage",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "Modular storage package with optional AWS S3, Azure Blob, and SFTP providers",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
}
|
|
130
130
|
},
|
|
131
131
|
"dependencies": {
|
|
132
|
-
"@flusys/nestjs-core": "6.0
|
|
133
|
-
"@flusys/nestjs-shared": "6.0
|
|
132
|
+
"@flusys/nestjs-core": "6.1.0",
|
|
133
|
+
"@flusys/nestjs-shared": "6.1.0"
|
|
134
134
|
}
|
|
135
135
|
}
|