const test = require('node:test'); const assert = require('node:assert'); const http = require('node:http'); const { deleteGiteaPathsBatched } = require('../src/git/apiHandler.js'); // Minimaler Gitea-Nachbau: Repo-Info, Trees-API (seitenweise) und Mehrfach-Commit-Endpunkt. function fakeGitea(paths, { perPage = 1000, failPath = null } = {}) { const files = new Map(paths.map((p, i) => [p, `sha${i}`])); const commits = []; 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 (req.method === 'GET' && u.pathname === '/api/v1/repos/o/r') return send(200, { default_branch: 'main' }); if (req.method === 'GET' && u.pathname === '/api/v1/repos/o/r/git/trees/main') { const page = Number(u.searchParams.get('page') || 1); const all = [...files.keys()].map(p => ({ path: p, type: 'blob', sha: files.get(p) })); const slice = all.slice((page - 1) * perPage, page * perPage); return send(200, { tree: slice, truncated: page * perPage < all.length, total_count: all.length, page }); } if (req.method === 'POST' && u.pathname === '/api/v1/repos/o/r/contents') { let body = ''; req.on('data', c => body += c); req.on('end', () => { const { files: ops } = JSON.parse(body); if (failPath && ops.some(o => o.path === failPath)) return send(422, { message: 'kaputt' }); for (const o of ops) { assert.strictEqual(o.operation, 'delete'); assert.strictEqual(o.sha, files.get(o.path)); files.delete(o.path); } commits.push(ops.length); send(201, {}); }); return; } send(404, { message: 'object does not exist' }); }); return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, files, commits, url: `http://127.0.0.1:${server.address().port}` }))); } test('Ordner wird paketweise gelöscht, Nachbarn bleiben', async () => { const paths = [...Array.from({ length: 2500 }, (_, i) => `mobile/build/f${i}.dex`), 'mobile/build.gradle.kts', 'mobile/src/A.kt']; const g = await fakeGitea(paths, { perPage: 1000 }); try { const r = await deleteGiteaPathsBatched({ token: 't', url: g.url, owner: 'o', repo: 'r', prefix: 'mobile/build' }); assert.strictEqual(r.deleted, 2500); assert.deepStrictEqual(g.commits, Array(25).fill(100)); assert.deepStrictEqual([...g.files.keys()].sort(), ['mobile/build.gradle.kts', 'mobile/src/A.kt']); } finally { g.server.close(); } }); test('eine fehlerhafte Datei blockiert nicht das ganze Paket', async () => { const paths = Array.from({ length: 10 }, (_, i) => `x/f${i}`); const g = await fakeGitea(paths, { failPath: 'x/f3' }); try { const r = await deleteGiteaPathsBatched({ token: 't', url: g.url, owner: 'o', repo: 'r', paths }); assert.strictEqual(r.deleted, 9); assert.deepStrictEqual(r.failed.map(f => f.path), ['x/f3']); } finally { g.server.close(); } });