diff --git a/test/cli.test.js b/test/cli.test.js index d201900..3589196 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -180,6 +180,51 @@ test('release upload resolves the tag once and applies a single asset name overr } }); +test('release upload streams a large asset through receiver backpressure', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-release-large-upload-')); + const cfg = path.join(dir, 'config.json'); + const asset = path.join(dir, 'large.bin'); + const assetSize = 8 * 1024 * 1024; + fs.writeFileSync(asset, Buffer.alloc(assetSize, 0x61)); + let uploadedBytes = 0; + let paused = false; + const server = http.createServer((req, res) => { + if (req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ id: 7, tag_name: 'v1' })); + return; + } + req.on('data', (chunk) => { + uploadedBytes += chunk.length; + if (!paused) { + paused = true; + req.pause(); + setTimeout(() => req.resume(), 100); + } + }); + req.on('end', () => { + res.writeHead(201, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ id: 8, name: 'large.bin' })); + }); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + fs.writeFileSync(cfg, JSON.stringify({ url: `http://127.0.0.1:${server.address().port}`, token: 'tok' })); + + try { + const result = await spawnAsync( + ['release', 'upload', '-o', 'o', '-r', 'r', '--tag', 'v1', '--asset', asset], + { STOKE_CONFIG_FILE: cfg }, + ); + assert.equal(result.status, 0, result.stderr); + assert.equal(paused, true); + assert.ok(uploadedBytes > assetSize, `multipart body ${uploadedBytes} did not include ${assetSize} asset bytes`); + assert.match(result.stdout, /Asset uploaded: large\.bin/); + } finally { + await new Promise((resolve) => server.close(resolve)); + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + test('release create keeps the release and reports landed and failed assets', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-release-partial-')); const cfg = path.join(dir, 'config.json'); @@ -211,7 +256,7 @@ test('release create keeps the release and reports landed and failed assets', as try { const result = await spawnAsync([ 'release', 'create', '-o', 'o', '-r', 'r', '--tag', 'v1', - '--asset', good, '--asset', bad, + '--asset', bad, '--asset', good, ], { STOKE_CONFIG_FILE: cfg }); assert.equal(result.status, 1); assert.match(result.stdout, /Release id: 42/i); @@ -220,8 +265,8 @@ test('release create keeps the release and reports landed and failed assets', as assert.match(result.stderr, /release was kept/i); assert.deepEqual(requests.map(({ url }) => url), [ '/api/v1/repos/o/r/releases', - '/api/v1/repos/o/r/releases/42/assets?name=good.bin', '/api/v1/repos/o/r/releases/42/assets?name=bad.bin', + '/api/v1/repos/o/r/releases/42/assets?name=good.bin', ]); } finally { await new Promise((resolve) => server.close(resolve));