tos-upload-short-read.test.js
1 /** 2 * Tests for the fs.readSync short-read guard in tosUpload. 3 * 4 * This file is separate from tos-upload.test.ts because vi.mock is hoisted and 5 * would interfere with the real-fs tests there. 6 * 7 * Strategy: 8 * - Use setReadSyncOverride (exported testing seam) to force readSync to return 0 9 * - Mock global fetch to satisfy initMultipartUpload so the code path reaches readSync 10 */ 11 import * as actualFs from 'node:fs'; 12 import * as os from 'node:os'; 13 import * as path from 'node:path'; 14 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 15 import { CommandExecutionError } from '@jackwener/opencli/errors'; 16 import { setReadSyncOverride, tosUpload } from './tos-upload.js'; 17 /** Build a minimal fetch mock that satisfies initMultipartUpload (POST ?uploads → 200 + UploadId XML). */ 18 function makeFetchMock() { 19 return vi.fn().mockResolvedValue({ 20 status: 200, 21 text: async () => '<InitiateMultipartUploadResult><UploadId>mock-upload-id</UploadId></InitiateMultipartUploadResult>', 22 headers: { forEach: (_cb) => { } }, 23 }); 24 } 25 describe('tosUpload short-read guard', () => { 26 let tmpDir; 27 let tmpFile; 28 let originalFetch; 29 beforeEach(() => { 30 originalFetch = globalThis.fetch; 31 tmpDir = actualFs.mkdtempSync(path.join(os.tmpdir(), 'tos-upload-shortread-')); 32 tmpFile = path.join(tmpDir, 'video.mp4'); 33 // 100-byte file — fits in a single part 34 actualFs.writeFileSync(tmpFile, Buffer.alloc(100, 0xff)); 35 }); 36 afterEach(() => { 37 setReadSyncOverride(null); 38 globalThis.fetch = originalFetch; 39 actualFs.rmSync(tmpDir, { recursive: true, force: true }); 40 }); 41 it('throws CommandExecutionError on short read', async () => { 42 // Mock fetch so initMultipartUpload succeeds and code reaches readSync 43 globalThis.fetch = makeFetchMock(); 44 // Override readSync to return 0 (fewer bytes than requested) 45 setReadSyncOverride(() => 0); 46 const mockCredentials = { 47 access_key_id: 'AKIAIOSFODNN7EXAMPLE', 48 secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', 49 session_token: 'test-session-token', 50 expired_time: Date.now() / 1000 + 3600, 51 }; 52 const uploadInfo = { 53 tos_upload_url: 'https://tos-cn-i-alisg.volces.com/bucket/key', 54 auth: 'AWS4-HMAC-SHA256 Credential=test', 55 video_id: 'test-video-id', 56 }; 57 await expect(tosUpload({ 58 filePath: tmpFile, 59 uploadInfo, 60 credentials: mockCredentials, 61 })).rejects.toThrow(CommandExecutionError); 62 }); 63 it('error message identifies the part number and byte counts', async () => { 64 globalThis.fetch = makeFetchMock(); 65 setReadSyncOverride(() => 0); 66 const mockCredentials = { 67 access_key_id: 'AKIAIOSFODNN7EXAMPLE', 68 secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', 69 session_token: 'test-session-token', 70 expired_time: Date.now() / 1000 + 3600, 71 }; 72 const uploadInfo = { 73 tos_upload_url: 'https://tos-cn-i-alisg.volces.com/bucket/key', 74 auth: 'AWS4-HMAC-SHA256 Credential=test', 75 video_id: 'test-video-id', 76 }; 77 await expect(tosUpload({ 78 filePath: tmpFile, 79 uploadInfo, 80 credentials: mockCredentials, 81 })).rejects.toThrow(/Short read on part 1: expected 100 bytes, got 0/); 82 }); 83 });