diff --git a/test/import-batch.test.js b/test/import-batch.test.js index 0435722..689c0bc 100644 --- a/test/import-batch.test.js +++ b/test/import-batch.test.js @@ -24,27 +24,33 @@ function run(args, env = {}) { }); } -test('repo import-batch continues after one item has no source token', async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-import-batch-')); - const configFile = path.join(dir, 'config.json'); - const manifestFile = path.join(dir, 'manifest.json'); - const emptyPath = path.join(dir, 'bin'); - const forgeToken = 'forge-token-must-not-be-printed'; +async function startMigrationServer() { const requests = []; const server = http.createServer((req, res) => { let body = ''; req.setEncoding('utf8'); req.on('data', (chunk) => { body += chunk; }); req.on('end', () => { - requests.push({ method: req.method, url: req.url, body: JSON.parse(body) }); + const payload = JSON.parse(body); + requests.push({ method: req.method, url: req.url, body: payload }); res.writeHead(201, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ - full_name: 'destination/imported-second', - html_url: 'https://forge.test/destination/imported-second', + full_name: `destination/${payload.repo_name}`, + html_url: `https://forge.test/destination/${payload.repo_name}`, })); }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + return { server, requests }; +} + +test('repo import-batch continues after one item has no source token', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-import-batch-')); + const configFile = path.join(dir, 'config.json'); + const manifestFile = path.join(dir, 'manifest.json'); + const emptyPath = path.join(dir, 'bin'); + const forgeToken = 'forge-token-must-not-be-printed'; + const { server, requests } = await startMigrationServer(); fs.mkdirSync(emptyPath); fs.writeFileSync(configFile, JSON.stringify({ url: `http://127.0.0.1:${server.address().port}`, @@ -91,3 +97,90 @@ test('repo import-batch continues after one item has no source token', async () fs.rmSync(dir, { recursive: true, force: true }); } }); + +test('repo import-batch preserves successful batch output', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-import-batch-success-')); + const configFile = path.join(dir, 'config.json'); + const manifestFile = path.join(dir, 'manifest.json'); + const { server, requests } = await startMigrationServer(); + fs.writeFileSync(configFile, JSON.stringify({ + url: `http://127.0.0.1:${server.address().port}`, + login: 'destination', + token: 'forge-token-must-not-be-printed', + })); + fs.writeFileSync(manifestFile, JSON.stringify([ + { name: 'first', from: 'https://git.example/source/first.git', service: 'git' }, + { name: 'second', from: 'https://git.example/source/second.git', service: 'git' }, + ])); + + try { + const result = await run(['--config', configFile, 'repo', 'import-batch', '--file', manifestFile]); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stderr, ''); + assert.equal(result.stdout, + 'Imported: destination/first -> https://forge.test/destination/first\n' + + 'Imported: destination/second -> https://forge.test/destination/second\n' + + '\nBatch complete: 2/2 imported.\n'); + assert.deepEqual(requests.map(({ body }) => body.repo_name), ['first', 'second']); + } finally { + await new Promise((resolve) => server.close(resolve)); + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +test('repo import-batch keeps file and JSON errors at batch level', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-import-batch-invalid-')); + const configFile = path.join(dir, 'config.json'); + const missingFile = path.join(dir, 'missing.json'); + const malformedFile = path.join(dir, 'malformed.json'); + fs.writeFileSync(configFile, JSON.stringify({ + url: 'https://forge.test', + login: 'destination', + token: 'forge-token-must-not-be-printed', + })); + fs.writeFileSync(malformedFile, '{not json'); + + try { + const missing = await run(['--config', configFile, 'repo', 'import-batch', '--file', missingFile]); + const malformed = await run(['--config', configFile, 'repo', 'import-batch', '--file', malformedFile]); + + assert.equal(missing.status, 1); + assert.match(missing.stderr, /^Batch import failed: ENOENT:/); + assert.equal(missing.stdout, ''); + assert.equal(malformed.status, 1); + assert.match(malformed.stderr, /^Batch import failed: /); + assert.match(malformed.stderr, /JSON/); + assert.equal(malformed.stdout, ''); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +test('repo import-batch excludes skipped invalid entries from the summary', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-import-batch-skip-')); + const configFile = path.join(dir, 'config.json'); + const manifestFile = path.join(dir, 'manifest.json'); + const { server, requests } = await startMigrationServer(); + fs.writeFileSync(configFile, JSON.stringify({ + url: `http://127.0.0.1:${server.address().port}`, + login: 'destination', + token: 'forge-token-must-not-be-printed', + })); + fs.writeFileSync(manifestFile, JSON.stringify([ + { name: 'missing-source' }, + { name: 'valid', from: 'https://git.example/source/valid.git', service: 'git' }, + ])); + + try { + const result = await run(['--config', configFile, 'repo', 'import-batch', '--file', manifestFile]); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stderr, 'Skipping invalid manifest entry: {"name":"missing-source"}\n'); + assert.match(result.stdout, /Batch complete: 1\/1 imported\./); + assert.deepEqual(requests.map(({ body }) => body.repo_name), ['valid']); + } finally { + await new Promise((resolve) => server.close(resolve)); + fs.rmSync(dir, { recursive: true, force: true }); + } +});