@quatrain/worker 1.2.7 → 1.2.9
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/dist/FileSystem.test.js +52 -5
- package/dist/Helpers.test.d.ts +1 -0
- package/dist/Helpers.test.js +60 -0
- package/package.json +2 -2
- package/src/FileSystem.test.ts +62 -5
- package/src/Helpers.test.ts +66 -0
package/dist/FileSystem.test.js
CHANGED
|
@@ -88,17 +88,17 @@ describe('FileSystem Utilities (Worker)', () => {
|
|
|
88
88
|
return node_stream_1.Readable.from(Buffer.alloc(35 * 1024));
|
|
89
89
|
});
|
|
90
90
|
jest.spyOn(node_fs_1.default, 'existsSync').mockImplementation((p) => {
|
|
91
|
-
if (typeof p === 'string' && (p.endsWith('test.txt') || p.endsWith('small.mp4') || p.endsWith('large.mp4') || p.endsWith('large-fail.mp4'))) {
|
|
91
|
+
if (typeof p === 'string' && (p.endsWith('test.txt') || p.endsWith('small.mp4') || p.endsWith('large.mp4') || p.endsWith('large-fail.mp4') || p.endsWith('small-fail.mp4') || p.endsWith('large-sync-fail.mp4'))) {
|
|
92
92
|
return true;
|
|
93
93
|
}
|
|
94
94
|
return originalExistsSync(p);
|
|
95
95
|
});
|
|
96
96
|
jest.spyOn(node_fs_1.default, 'statSync').mockImplementation((p) => {
|
|
97
97
|
if (typeof p === 'string') {
|
|
98
|
-
if (p.endsWith('small.mp4')) {
|
|
98
|
+
if (p.endsWith('small.mp4') || p.endsWith('small-fail.mp4')) {
|
|
99
99
|
return { size: 13 };
|
|
100
100
|
}
|
|
101
|
-
if (p.endsWith('large.mp4') || p.endsWith('large-fail.mp4')) {
|
|
101
|
+
if (p.endsWith('large.mp4') || p.endsWith('large-fail.mp4') || p.endsWith('large-sync-fail.mp4')) {
|
|
102
102
|
return { size: 35 * 1024 };
|
|
103
103
|
}
|
|
104
104
|
}
|
|
@@ -252,8 +252,18 @@ describe('FileSystem Utilities (Worker)', () => {
|
|
|
252
252
|
ffmpeg.ffprobe.mockImplementation((file, callback) => {
|
|
253
253
|
callback(null, mockMetadata);
|
|
254
254
|
});
|
|
255
|
-
node_fetch_native_1.default.
|
|
256
|
-
|
|
255
|
+
node_fetch_native_1.default.mockImplementation((url, options) => {
|
|
256
|
+
return new Promise((resolve) => {
|
|
257
|
+
if (options.body && typeof options.body.on === 'function') {
|
|
258
|
+
options.body.on('data', () => { });
|
|
259
|
+
options.body.on('end', () => {
|
|
260
|
+
resolve({ ok: true });
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
resolve({ ok: true });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
257
267
|
});
|
|
258
268
|
const meta = {
|
|
259
269
|
bucket: 'mock-bucket',
|
|
@@ -281,5 +291,42 @@ describe('FileSystem Utilities (Worker)', () => {
|
|
|
281
291
|
};
|
|
282
292
|
await expect(FileSystem_1.FileSystem.uploadFile(testFile, meta)).rejects.toThrow('Connection abort');
|
|
283
293
|
});
|
|
294
|
+
it('should reject small uploads if HTTP request fails', async () => {
|
|
295
|
+
const testFile = node_path_1.default.join(testBaseDir, 'small-fail.mp4');
|
|
296
|
+
const mockMetadata = {
|
|
297
|
+
streams: [{ width: 320, height: 240, duration: '1', bit_rate: '100', nb_frames: '10' }]
|
|
298
|
+
};
|
|
299
|
+
ffmpeg.ffprobe.mockImplementation((file, callback) => {
|
|
300
|
+
callback(null, mockMetadata);
|
|
301
|
+
});
|
|
302
|
+
node_fetch_native_1.default.mockRejectedValue(new Error('Upload failed'));
|
|
303
|
+
// Stub readFileSync for small content read
|
|
304
|
+
jest.spyOn(node_fs_1.default, 'readFileSync').mockReturnValue(Buffer.from('small content'));
|
|
305
|
+
const meta = {
|
|
306
|
+
bucket: 'mock-bucket',
|
|
307
|
+
ref: 'remote/small-fail.mp4',
|
|
308
|
+
uploadUrl: 'https://upload.com/small-fail.mp4'
|
|
309
|
+
};
|
|
310
|
+
await expect(FileSystem_1.FileSystem.uploadFile(testFile, meta)).rejects.toThrow('Upload failed');
|
|
311
|
+
});
|
|
312
|
+
it('should handle synchronous errors inside stream initialization', async () => {
|
|
313
|
+
const testFile = node_path_1.default.join(testBaseDir, 'large-sync-fail.mp4');
|
|
314
|
+
const mockMetadata = {
|
|
315
|
+
streams: [{ width: 1280, height: 720, duration: '5', bit_rate: '2000', nb_frames: '150' }]
|
|
316
|
+
};
|
|
317
|
+
ffmpeg.ffprobe.mockImplementation((file, callback) => {
|
|
318
|
+
callback(null, mockMetadata);
|
|
319
|
+
});
|
|
320
|
+
// Make createReadStream throw a synchronous error
|
|
321
|
+
jest.spyOn(node_fs_1.default, 'createReadStream').mockImplementationOnce(() => {
|
|
322
|
+
throw new Error('Sync stream error');
|
|
323
|
+
});
|
|
324
|
+
const meta = {
|
|
325
|
+
bucket: 'mock-bucket',
|
|
326
|
+
ref: 'remote/large-sync-fail.mp4',
|
|
327
|
+
uploadUrl: 'https://upload.com/large-sync-fail.mp4'
|
|
328
|
+
};
|
|
329
|
+
await expect(FileSystem_1.FileSystem.uploadFile(testFile, meta)).rejects.toThrow('Sync stream error');
|
|
330
|
+
});
|
|
284
331
|
});
|
|
285
332
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const Helpers_1 = require("./Helpers");
|
|
4
|
+
const Worker_1 = require("./Worker");
|
|
5
|
+
jest.mock('./Worker', () => {
|
|
6
|
+
const original = jest.requireActual('./Worker');
|
|
7
|
+
return {
|
|
8
|
+
Worker: {
|
|
9
|
+
...original.Worker,
|
|
10
|
+
getSystemCommandPath: jest.fn().mockResolvedValue('/usr/bin/ffmpeg'),
|
|
11
|
+
execPromise: jest.fn().mockResolvedValue(true),
|
|
12
|
+
info: jest.fn(),
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
describe('Helpers', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
it('should generate video thumbnail parameters correctly and execute ffmpeg', async () => {
|
|
21
|
+
const videoPath = 'test.mp4';
|
|
22
|
+
const outputPath = 'output.jpg';
|
|
23
|
+
const result = await Helpers_1.Helpers.generateVideoThumbnail(videoPath, outputPath, 5, 640);
|
|
24
|
+
expect(result).toBe(true);
|
|
25
|
+
expect(Worker_1.Worker.execPromise).toHaveBeenCalledWith('/usr/bin/ffmpeg', [
|
|
26
|
+
'-i',
|
|
27
|
+
'test.mp4',
|
|
28
|
+
'-vframes',
|
|
29
|
+
'1',
|
|
30
|
+
'-vf',
|
|
31
|
+
'select=gte(n\\,5)',
|
|
32
|
+
'-s',
|
|
33
|
+
'640x480',
|
|
34
|
+
'-ss',
|
|
35
|
+
'1',
|
|
36
|
+
'output.jpg',
|
|
37
|
+
'-y'
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
it('should use default values for frame and width', async () => {
|
|
41
|
+
const videoPath = 'test.mp4';
|
|
42
|
+
const outputPath = 'output.jpg';
|
|
43
|
+
const result = await Helpers_1.Helpers.generateVideoThumbnail(videoPath, outputPath);
|
|
44
|
+
expect(result).toBe(true);
|
|
45
|
+
expect(Worker_1.Worker.execPromise).toHaveBeenCalledWith('/usr/bin/ffmpeg', [
|
|
46
|
+
'-i',
|
|
47
|
+
'test.mp4',
|
|
48
|
+
'-vframes',
|
|
49
|
+
'1',
|
|
50
|
+
'-vf',
|
|
51
|
+
'select=gte(n\\,0)',
|
|
52
|
+
'-s',
|
|
53
|
+
'320x240',
|
|
54
|
+
'-ss',
|
|
55
|
+
'1',
|
|
56
|
+
'output.jpg',
|
|
57
|
+
'-y'
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quatrain/worker",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "Container Worker helpers",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"typescript": "^5.1.5"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@quatrain/core": "^1.2.
|
|
37
|
+
"@quatrain/core": "^1.2.15",
|
|
38
38
|
"@quatrain/queue": "^1.2.3",
|
|
39
39
|
"@quatrain/storage": "^1.2.9",
|
|
40
40
|
"axios": "^1.7.7",
|
package/src/FileSystem.test.ts
CHANGED
|
@@ -57,7 +57,7 @@ describe('FileSystem Utilities (Worker)', () => {
|
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
jest.spyOn(fs, 'existsSync').mockImplementation((p) => {
|
|
60
|
-
if (typeof p === 'string' && (p.endsWith('test.txt') || p.endsWith('small.mp4') || p.endsWith('large.mp4') || p.endsWith('large-fail.mp4'))) {
|
|
60
|
+
if (typeof p === 'string' && (p.endsWith('test.txt') || p.endsWith('small.mp4') || p.endsWith('large.mp4') || p.endsWith('large-fail.mp4') || p.endsWith('small-fail.mp4') || p.endsWith('large-sync-fail.mp4'))) {
|
|
61
61
|
return true
|
|
62
62
|
}
|
|
63
63
|
return originalExistsSync(p)
|
|
@@ -65,10 +65,10 @@ describe('FileSystem Utilities (Worker)', () => {
|
|
|
65
65
|
|
|
66
66
|
jest.spyOn(fs, 'statSync').mockImplementation((p) => {
|
|
67
67
|
if (typeof p === 'string') {
|
|
68
|
-
if (p.endsWith('small.mp4')) {
|
|
68
|
+
if (p.endsWith('small.mp4') || p.endsWith('small-fail.mp4')) {
|
|
69
69
|
return { size: 13 } as any
|
|
70
70
|
}
|
|
71
|
-
if (p.endsWith('large.mp4') || p.endsWith('large-fail.mp4')) {
|
|
71
|
+
if (p.endsWith('large.mp4') || p.endsWith('large-fail.mp4') || p.endsWith('large-sync-fail.mp4')) {
|
|
72
72
|
return { size: 35 * 1024 } as any
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -255,8 +255,17 @@ describe('FileSystem Utilities (Worker)', () => {
|
|
|
255
255
|
callback(null, mockMetadata)
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
-
(fetch as any).
|
|
259
|
-
|
|
258
|
+
(fetch as any).mockImplementation((url: string, options: any) => {
|
|
259
|
+
return new Promise((resolve) => {
|
|
260
|
+
if (options.body && typeof options.body.on === 'function') {
|
|
261
|
+
options.body.on('data', () => {})
|
|
262
|
+
options.body.on('end', () => {
|
|
263
|
+
resolve({ ok: true })
|
|
264
|
+
})
|
|
265
|
+
} else {
|
|
266
|
+
resolve({ ok: true })
|
|
267
|
+
}
|
|
268
|
+
})
|
|
260
269
|
})
|
|
261
270
|
|
|
262
271
|
const meta = {
|
|
@@ -291,5 +300,53 @@ describe('FileSystem Utilities (Worker)', () => {
|
|
|
291
300
|
|
|
292
301
|
await expect(FileSystem.uploadFile(testFile, meta)).rejects.toThrow('Connection abort')
|
|
293
302
|
})
|
|
303
|
+
|
|
304
|
+
it('should reject small uploads if HTTP request fails', async () => {
|
|
305
|
+
const testFile = path.join(testBaseDir, 'small-fail.mp4')
|
|
306
|
+
|
|
307
|
+
const mockMetadata = {
|
|
308
|
+
streams: [{ width: 320, height: 240, duration: '1', bit_rate: '100', nb_frames: '10' }]
|
|
309
|
+
};
|
|
310
|
+
(ffmpeg.ffprobe as any).mockImplementation((file: string, callback: any) => {
|
|
311
|
+
callback(null, mockMetadata)
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
(fetch as any).mockRejectedValue(new Error('Upload failed'))
|
|
315
|
+
|
|
316
|
+
// Stub readFileSync for small content read
|
|
317
|
+
jest.spyOn(fs, 'readFileSync').mockReturnValue(Buffer.from('small content'))
|
|
318
|
+
|
|
319
|
+
const meta = {
|
|
320
|
+
bucket: 'mock-bucket',
|
|
321
|
+
ref: 'remote/small-fail.mp4',
|
|
322
|
+
uploadUrl: 'https://upload.com/small-fail.mp4'
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await expect(FileSystem.uploadFile(testFile, meta)).rejects.toThrow('Upload failed')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('should handle synchronous errors inside stream initialization', async () => {
|
|
329
|
+
const testFile = path.join(testBaseDir, 'large-sync-fail.mp4')
|
|
330
|
+
|
|
331
|
+
const mockMetadata = {
|
|
332
|
+
streams: [{ width: 1280, height: 720, duration: '5', bit_rate: '2000', nb_frames: '150' }]
|
|
333
|
+
};
|
|
334
|
+
(ffmpeg.ffprobe as any).mockImplementation((file: string, callback: any) => {
|
|
335
|
+
callback(null, mockMetadata)
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Make createReadStream throw a synchronous error
|
|
339
|
+
jest.spyOn(fs, 'createReadStream').mockImplementationOnce(() => {
|
|
340
|
+
throw new Error('Sync stream error')
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const meta = {
|
|
344
|
+
bucket: 'mock-bucket',
|
|
345
|
+
ref: 'remote/large-sync-fail.mp4',
|
|
346
|
+
uploadUrl: 'https://upload.com/large-sync-fail.mp4'
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await expect(FileSystem.uploadFile(testFile, meta)).rejects.toThrow('Sync stream error')
|
|
350
|
+
})
|
|
294
351
|
})
|
|
295
352
|
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Helpers } from './Helpers'
|
|
2
|
+
import { Worker } from './Worker'
|
|
3
|
+
|
|
4
|
+
jest.mock('./Worker', () => {
|
|
5
|
+
const original = jest.requireActual('./Worker')
|
|
6
|
+
return {
|
|
7
|
+
Worker: {
|
|
8
|
+
...original.Worker,
|
|
9
|
+
getSystemCommandPath: jest.fn().mockResolvedValue('/usr/bin/ffmpeg'),
|
|
10
|
+
execPromise: jest.fn().mockResolvedValue(true),
|
|
11
|
+
info: jest.fn(),
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('Helpers', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should generate video thumbnail parameters correctly and execute ffmpeg', async () => {
|
|
22
|
+
const videoPath = 'test.mp4'
|
|
23
|
+
const outputPath = 'output.jpg'
|
|
24
|
+
|
|
25
|
+
const result = await Helpers.generateVideoThumbnail(videoPath, outputPath, 5, 640)
|
|
26
|
+
|
|
27
|
+
expect(result).toBe(true)
|
|
28
|
+
expect(Worker.execPromise).toHaveBeenCalledWith('/usr/bin/ffmpeg', [
|
|
29
|
+
'-i',
|
|
30
|
+
'test.mp4',
|
|
31
|
+
'-vframes',
|
|
32
|
+
'1',
|
|
33
|
+
'-vf',
|
|
34
|
+
'select=gte(n\\,5)',
|
|
35
|
+
'-s',
|
|
36
|
+
'640x480',
|
|
37
|
+
'-ss',
|
|
38
|
+
'1',
|
|
39
|
+
'output.jpg',
|
|
40
|
+
'-y'
|
|
41
|
+
])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should use default values for frame and width', async () => {
|
|
45
|
+
const videoPath = 'test.mp4'
|
|
46
|
+
const outputPath = 'output.jpg'
|
|
47
|
+
|
|
48
|
+
const result = await Helpers.generateVideoThumbnail(videoPath, outputPath)
|
|
49
|
+
|
|
50
|
+
expect(result).toBe(true)
|
|
51
|
+
expect(Worker.execPromise).toHaveBeenCalledWith('/usr/bin/ffmpeg', [
|
|
52
|
+
'-i',
|
|
53
|
+
'test.mp4',
|
|
54
|
+
'-vframes',
|
|
55
|
+
'1',
|
|
56
|
+
'-vf',
|
|
57
|
+
'select=gte(n\\,0)',
|
|
58
|
+
'-s',
|
|
59
|
+
'320x240',
|
|
60
|
+
'-ss',
|
|
61
|
+
'1',
|
|
62
|
+
'output.jpg',
|
|
63
|
+
'-y'
|
|
64
|
+
])
|
|
65
|
+
})
|
|
66
|
+
})
|