const test = require('node:test'); const assert = require('node:assert'); const http = require('node:http'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); require('../src/git/gitBinary.js').useBundledGit(); const { registerFeatureIpc } = require('../src/features/featuresIpc.js'); // Drei Repos; das ZIP von "langsam" kommt in kleinen Stücken über ~3 s, damit ein Abbruch mitten hineinfällt function fakeGitea() { const server = http.createServer((req, res) => { const u = new URL(req.url, 'http://x'); const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); }; if (u.pathname === '/api/v1/user/repos') { if (u.searchParams.get('page') !== '1') return send(200, []); const port = server.address().port; return send(200, ['eins', 'langsam', 'drei'].map(name => ({ name, owner: { login: 'M_Viper' }, default_branch: 'main', empty: false, clone_url: `http://127.0.0.1:${port}/M_Viper/${name}.git` }))); } const m = u.pathname.match(/^\/api\/v1\/repos\/M_Viper\/(\w+)\/archive\/main\.zip$/); if (m) { const total = 64 * 1024; res.writeHead(200, { 'Content-Type': 'application/zip', 'Content-Length': String(total) }); if (m[1] !== 'langsam') return res.end(Buffer.alloc(total, 1)); let sent = 0; const t = setInterval(() => { if (res.destroyed) return clearInterval(t); res.write(Buffer.alloc(2048, 1)); sent += 2048; if (sent >= total) { clearInterval(t); res.end(); } }, 100); return; } send(404, { message: 'not found' }); }); return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, url: `http://127.0.0.1:${server.address().port}` }))); } function setup(url) { const handlers = {}; const events = []; registerFeatureIpc({ ipcMain: { handle: (name, fn) => { handlers[name] = fn; } }, app: { getPath: () => fs.mkdtempSync(path.join(os.tmpdir(), 'gm-ud-')) }, readCredentials: () => ({ giteaURL: url, giteaToken: 't' }), buildItemsFromPaths: async () => ({ items: [], skipped: [], ignored: [] }), clearGitTransportBlock: () => false, broadcast: (channel, p) => { if (channel === 'repo-backup-progress') events.push(p); } }); return { call: (name, data) => handlers[name]({ sender: { send() {} } }, data), events }; } test('Backup meldet Fortschritt: Phasen, aktuelles Repo, Bytes und Ergebnis je Repo', async () => { const g = await fakeGitea(); try { const { call, events } = setup(g.url); const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-')); await call('repo-backup-set-config', { targetDir: target }); const r = await call('repo-backup-run-now', {}); assert.strictEqual(r.summary.ok, 3, JSON.stringify(r)); const phases = [...new Set(events.map(e => e.phase))]; assert.deepStrictEqual(phases, ['start', 'list', 'running', 'done']); assert.deepStrictEqual(events.find(e => e.phase === 'running').repos, ['M_Viper/eins', 'M_Viper/langsam', 'M_Viper/drei']); assert.ok(events.some(e => e.repo === 'M_Viper/langsam' && e.bytes > 0 && e.bytesTotal === 64 * 1024), 'Download-Fortschritt fehlt'); const last = events[events.length - 1]; assert.strictEqual(last.results.length, 3); assert.ok(last.results.every(x => x.ok && x.method === 'ZIP')); assert.ok(!events.some(e => 'abort' in e), 'interne Felder dürfen nicht nach außen gehen'); // Während kein Backup läuft, gibt es auch keinen Zwischenstand const state = await call('repo-backup-get-config', {}); assert.strictEqual(state.running, false); assert.strictEqual(state.progress, null); } finally { g.server.close(); } }); test('Abbrechen beendet den laufenden ZIP-Download sofort und überspringt den Rest', async () => { const g = await fakeGitea(); try { const { call, events } = setup(g.url); const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-')); await call('repo-backup-set-config', { targetDir: target }); const started = Date.now(); const running = call('repo-backup-run-now', {}); // warten, bis der langsame Download läuft while (!events.some(e => e.repo === 'M_Viper/langsam' && e.bytes > 0)) { if (Date.now() - started > 10000) throw new Error('Download startete nicht'); await new Promise(r => setTimeout(r, 50)); } const state = await call('repo-backup-get-config', {}); assert.strictEqual(state.running, true); assert.strictEqual(state.progress.repo, 'M_Viper/langsam'); assert.strictEqual((await call('repo-backup-cancel', {})).ok, true); const r = await running; assert.ok(Date.now() - started < 2500, 'Abbruch muss den Download sofort beenden'); assert.strictEqual(r.ok, false); assert.strictEqual(r.summary.cancelled, true); assert.strictEqual(r.summary.ok, 1); assert.strictEqual(r.summary.skipped, 2); // keine halbe Datei zurücklassen const archiv = path.join(target, 'M_Viper', 'langsam-archiv'); assert.deepStrictEqual(fs.existsSync(archiv) ? fs.readdirSync(archiv) : [], []); assert.strictEqual((await call('repo-backup-cancel', {})).ok, false); } finally { g.server.close(); } });