// P2P tables against a real temp libsql DB (docs/p2p-architecture.md). import { test, expect, beforeAll } from 'bun:test'; import { mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const root = mkdtempSync(join(tmpdir(), 'ytp-p2p-test-')); process.env.DB_PATH = join(root, 'test.db'); const dbmod = await import('./db.js'); const P = await import('./p2p-db.js'); const { loadP2pConfig } = await import('./p2p-config.js'); const CID = 'a'.repeat(64); const CID2 = 'b'.repeat(64); const T0 = Date.UTC(2026, 8, 29, 12); beforeAll(async () => { await dbmod.initDb(); await P.initP2pSchema(); await P.initP2pSchema(); // idempotent (ALTER TABLE duplicate column is ignored) }); test('config defaults: P2P on, malware scan off', () => { const c = loadP2pConfig({}); expect(c.enabled).toBe(true); expect(c.malwareScan).toBe(false); expect(c.staleDays).toBe(7); expect(loadP2pConfig({ P2P_ENABLED: '0', P2P_MALWARE_SCAN: '1' })).toMatchObject({ enabled: false, malwareScan: true }); }); test('content upsert/get/known/revoke', async () => { await P.upsertContent({ cid: CID, videoId: 'dQw4w9WgXcQ', size: 1000, height: 720, vcodec: 'h264', acodec: 'aac', duration: 212, meta: { title: 'x' }, origin: 'server', now: T0 }); const c = await P.getContent(CID); expect(c.video_id).toBe('dQw4w9WgXcQ'); expect(c.status).toBe('verified'); expect([...(await P.knownCids([CID, CID2]))]).toEqual([CID]); expect((await P.listContentForVideo('dQw4w9WgXcQ')).length).toBe(1); await P.upsertContent({ cid: CID2, videoId: 'dQw4w9WgXcQ', size: 5, origin: 'intake', now: T0 }); await P.revokeContent(CID2); expect([...(await P.knownCids([CID2]))]).toEqual([]); }); test('holders persist, never expire, and a full report removes missing ones', async () => { await P.createDevice({ deviceId: 'dev_1', secretHash: 'h', fingerprint: 'fp', now: T0 }); await P.upsertHolder({ cid: CID, deviceId: 'dev_1', now: T0 }); // 90 days later with no new report: still listed (UI marks it stale). let hs = await P.listHolders(CID); expect(hs.length).toBe(1); expect(hs[0].last_verified_at).toBe(T0); await P.setHolderTrust({ cid: CID, deviceId: 'dev_1', trust: 'challenged', now: T0 + 1000 }); await P.upsertHolder({ cid: CID, deviceId: 'dev_1', trust: 'reported', now: T0 + 2000 }); hs = await P.listHolders(CID); expect(hs[0].trust).toBe('challenged'); // a later plain report never downgrades trust expect(hs[0].last_verified_at).toBe(T0 + 2000); expect(await P.removeHoldersExcept({ deviceId: 'dev_1', keep: [], now: T0 + 3000 })).toBe(1); expect((await P.listHolders(CID)).length).toBe(0); expect(await P.activeHoldingsOf('dev_1')).toEqual([]); await P.upsertHolder({ cid: CID, deviceId: 'dev_1', now: T0 + 4000 }); // re-added expect(await P.activeHoldingsOf('dev_1')).toEqual([CID]); }); test('views per day and window sums', async () => { await P.addView('vid00000001', T0); await P.addView('vid00000001', T0); await P.addView('vid00000001', T0 - 40 * 86400_000); expect(await P.viewsSince('vid00000001', T0 - 30 * 86400_000)).toBe(2); expect(await P.viewsSince('vid00000001', T0 - 50 * 86400_000)).toBe(3); const s = await P.p2pStats(); expect(s.content).toBe(1); expect(s.devices).toBe(1); }); test('listSharedContent: newest verified file per video, only with sharing holders counted', async () => { const V = 'sharedVid01', C1 = 'c'.repeat(64), C2 = 'd'.repeat(64); await P.createDevice({ deviceId: 'dev_00000000000000a1', secretHash: 'x', fingerprint: null, profile: null, now: T0 }); await P.createDevice({ deviceId: 'dev_00000000000000a2', secretHash: 'x', fingerprint: null, profile: null, now: T0 }); await P.touchDevice('dev_00000000000000a2', { now: T0, share: 0 }); await P.upsertContent({ cid: C1, videoId: V, size: 10, duration: 5, meta: { title: 'Old' }, origin: 'server', now: T0 }); await P.upsertContent({ cid: C2, videoId: V, size: 20, duration: 5, meta: { title: 'New' }, origin: 'intake', now: T0 + 5000 }); await P.upsertHolder({ cid: C2, deviceId: 'dev_00000000000000a1', now: T0 }); await P.upsertHolder({ cid: C2, deviceId: 'dev_00000000000000a2', now: T0 }); // does not share const rows = (await P.listSharedContent(50)).filter((r) => r.video_id === V); expect(rows.length).toBe(1); expect(rows[0]).toMatchObject({ cid: C2, holders: 1 }); });