// Device → server intake: hash, validate, (scan), admit (plan 016). Needs ffmpeg. import { test, expect, beforeAll } from 'bun:test'; import { mkdtempSync, readFileSync, readdirSync, unlinkSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createHash } from 'node:crypto'; import { Hono } from 'hono'; const root = mkdtempSync(join(tmpdir(), 'ytp-intake-')); process.env.DB_PATH = join(root, 'test.db'); const dbmod = await import('./db.js'); const p2pDb = await import('./p2p-db.js'); const { registerP2pRoutes } = await import('./p2p-routes.js'); const { registerIntakeRoutes } = await import('./p2p-intake.js'); const { admitFile } = await import('./p2p-admit.js'); const { validateMedia } = await import('./media-cache.js'); const quiet = { warn() {}, info() {} }; const intakeDir = join(root, 'intake'); const cfg = { enabled: true, malwareScan: false, scanCmd: 'true', staleDays: 7, intakeDir, intakeMaxBytes: 50 * 1024 * 1024 }; let app; let dev; let good; let goodCid; const adopted = []; const req = (path, { method = 'GET', body, raw } = {}) => app.request(path, { method, headers: { ...(raw ? {} : { 'Content-Type': 'application/json' }), 'X-Device': dev.deviceId + '.' + dev.secret }, body: raw || (body ? JSON.stringify(body) : undefined), }); beforeAll(async () => { await dbmod.initDb(); await p2pDb.initP2pSchema(); const f = join(root, 'good.mp4'); const r = spawnSync('ffmpeg', ['-v', 'error', '-y', '-f', 'lavfi', '-i', 'testsrc=size=320x180:rate=25:duration=4', '-f', 'lavfi', '-i', 'sine=frequency=440:duration=4', '-c:v', 'libx264', '-preset', 'ultrafast', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-shortest', '-movflags', '+faststart', f]); if (r.status !== 0) throw new Error('ffmpeg fixture failed: ' + r.stderr); good = readFileSync(f); goodCid = createHash('sha256').update(good).digest('hex'); app = new Hono(); const p2p = registerP2pRoutes(app, { cfg, p2pDb, fileForCid: async () => null, sha256Range: async () => '', log: quiet }); registerIntakeRoutes(app, { cfg, p2pDb, gate: p2p.gate, requireDevice: p2p.requireDevice, validateMedia, admitFile, log: quiet, adopt: async (videoId, path, info) => { adopted.push({ videoId, sha256: info.sha256, meta: info.meta }); return { adopted: false }; }, }); dev = await (await app.request('/api/p2p/device', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })).json(); }); test('a valid file is hashed by the server, validated, admitted and its uploader becomes a holder', async () => { const t = await (await req('/api/p2p/intake', { method: 'POST', body: { videoId: 'upAAAAAAAA1', cid: goodCid, size: good.length, title: 'Song x', channel: 'Choir' } })).json(); expect(t.ok).toBe(true); const r = await req(t.url, { method: 'PUT', raw: good }); expect(r.status).toBe(200); expect(await r.json()).toEqual({ ok: true, cid: goodCid, adopted: false }); expect((await p2pDb.getContent(goodCid))).toMatchObject({ origin: 'intake', status: 'verified', scan: 'skipped', video_id: 'upAAAAAAAA1' }); expect((await p2pDb.listHolders(goodCid))[0]).toMatchObject({ device_id: dev.deviceId, trust: 'challenged' }); expect(adopted).toEqual([{ videoId: 'upAAAAAAAA1', sha256: goodCid, meta: { title: 'Song b x /b', channel: 'Choir' } }]); expect(JSON.parse((await p2pDb.getContent(goodCid)).meta).title).toBe('Song b x /b'); expect(readdirSync(intakeDir)).toEqual([]); // not adopted → deleted // Already verified → nothing to upload next time. expect(await (await req('/api/p2p/intake', { method: 'POST', body: { videoId: 'upAAAAAAAA1', cid: goodCid, size: good.length } })).json()).toEqual({ ok: true, known: true }); }); test('restore: a known cid is uploaded again only when the server lost its copy', async () => { let serverHas = true; const app3 = new Hono(); const p2p = registerP2pRoutes(app3, { cfg, p2pDb, fileForCid: async () => null, sha256Range: async () => '', log: quiet }); const got = []; registerIntakeRoutes(app3, { cfg, p2pDb, gate: p2p.gate, requireDevice: p2p.requireDevice, validateMedia, admitFile, log: quiet, serverHasCid: async () => serverHas, adopt: async (videoId, path, info) => { got.push(info.sha256); unlinkSync(path); return { adopted: true }; }, // a real adopt moves the file }); const h = { 'X-Device': dev.deviceId + '.' + dev.secret, 'Content-Type': 'application/json' }; const open = async () => (await app3.request('/api/p2p/intake', { method: 'POST', headers: h, body: JSON.stringify({ videoId: 'upAAAAAAAA1', cid: goodCid, size: good.length, restore: true }) })).json(); expect(await open()).toEqual({ ok: true, known: true }); // server still has it serverHas = false; const t = await open(); expect(t.ticket).toMatch(/^[0-9a-f]{32}$/); const r = await (await app3.request(t.url, { method: 'PUT', headers: { 'X-Device': h['X-Device'] }, body: good })).json(); expect(r).toEqual({ ok: true, cid: goodCid, adopted: true }); expect(got).toEqual([goodCid]); }); test('claimed cid mismatch, truncated media, and non-media are rejected and deleted', async () => { const cases = [ [{ cid: 'f'.repeat(64), bytes: good }, 400, /hash mismatch/], [{ bytes: good.subarray(0, Math.floor(good.length * 0.6)) }, 422, /validation/], [{ bytes: Buffer.alloc(200 * 1024, 7) }, 422, /validation/], ]; for (const [c, status, msg] of cases) { const t = await (await req('/api/p2p/intake', { method: 'POST', body: { videoId: 'upAAAAAAAA2', cid: c.cid, size: c.bytes.length } })).json(); const r = await req(t.url, { method: 'PUT', raw: c.bytes }); expect(r.status).toBe(status); expect((await r.json()).error).toMatch(msg); } expect(readdirSync(intakeDir)).toEqual([]); expect((await p2pDb.listContentForVideo('upAAAAAAAA2')).length).toBe(0); }); test('size limits and tickets are enforced', async () => { expect((await req('/api/p2p/intake', { method: 'POST', body: { videoId: 'upAAAAAAAA3', size: cfg.intakeMaxBytes + 1 } })).status).toBe(413); const t = await (await req('/api/p2p/intake', { method: 'POST', body: { videoId: 'upAAAAAAAA3', size: 10 } })).json(); expect((await req('/api/p2p/intake', { method: 'POST', body: { videoId: 'upAAAAAAAA4', size: 10 } })).status).toBe(429); // one open per device const r = await req(t.url, { method: 'PUT', raw: Buffer.alloc(11) }); expect(r.status).toBe(413); expect((await req(t.url, { method: 'PUT', raw: Buffer.alloc(10) })).status).toBe(404); // ticket consumed }); test('malware scan on: an infected verdict is never admitted', async () => { const cfg2 = { ...cfg, malwareScan: true, scanCmd: 'false' }; // exit 1 = infected const app2 = new Hono(); const p2p = registerP2pRoutes(app2, { cfg: cfg2, p2pDb, fileForCid: async () => null, sha256Range: async () => '', log: quiet }); registerIntakeRoutes(app2, { cfg: cfg2, p2pDb, gate: p2p.gate, requireDevice: p2p.requireDevice, validateMedia, admitFile, log: quiet }); const h = { 'X-Device': dev.deviceId + '.' + dev.secret }; const t = await (await app2.request('/api/p2p/intake', { method: 'POST', headers: { ...h, 'Content-Type': 'application/json' }, body: JSON.stringify({ videoId: 'upAAAAAAAA5', size: good.length }) })).json(); await p2pDb.revokeContent(goodCid); // make sure it is not simply "already known" const r = await app2.request(t.url, { method: 'PUT', headers: h, body: good }); expect(r.status).toBe(422); expect((await r.json()).error).toMatch(/scan infected/); });