import { describe, test, expect, beforeEach, mock } from 'bun:test'; import { MemoryStorage } from '@shade/crypto-web'; import { FingerprintNotVerifiedError } from '@shade/core'; import { FingerprintGateRegistry } from '../src/gates.js'; describe('FingerprintGateRegistry — unit', () => { let storage: MemoryStorage; let gates: FingerprintGateRegistry; beforeEach(() => { storage = new MemoryStorage(); gates = new FingerprintGateRegistry(storage); }); test('default first-large-file threshold is 10 MiB', () => { expect(gates.getFirstLargeFileThreshold()).toBe(10 * 1024 * 1024); }); test('threshold becomes the registered value', () => { gates.registerFirstLargeFile(2048, () => true); expect(gates.getFirstLargeFileThreshold()).toBe(2048); }); test('rejects negative thresholds', () => { expect(() => gates.registerFirstLargeFile(-1, () => true)).toThrow(); }); test('checkFirstLargeFile is a no-op when size < threshold', async () => { const handler = mock(() => true); gates.registerFirstLargeFile(10_000, handler); await gates.checkFirstLargeFile('bob', 'fp', 1_000); expect(handler).not.toHaveBeenCalled(); }); test('checkFirstLargeFile invokes handler and approves on true', async () => { let called = false; gates.registerFirstLargeFile(10, (ctx) => { called = true; expect(ctx.gate).toBe('first-large-file'); expect(ctx.fileSize).toBe(100); expect(ctx.peerAddress).toBe('bob'); expect(ctx.fingerprint).toBe('FP'); return true; }); await gates.checkFirstLargeFile('bob', 'FP', 100); expect(called).toBe(true); // Subsequent calls: peer is verified, handler not consulted. let secondCalled = false; gates.registerFirstLargeFile(10, () => { secondCalled = true; return false; }); await gates.checkFirstLargeFile('bob', 'FP', 100); expect(secondCalled).toBe(false); }); test('handler false → throws FingerprintNotVerifiedError', async () => { gates.registerFirstLargeFile(10, () => false); await expect(gates.checkFirstLargeFile('bob', 'FP', 100)).rejects.toBeInstanceOf( FingerprintNotVerifiedError, ); }); test('handler throw → throws FingerprintNotVerifiedError', async () => { gates.registerFirstLargeFile(10, () => { throw new Error('user closed modal'); }); await expect(gates.checkFirstLargeFile('bob', 'FP', 100)).rejects.toBeInstanceOf( FingerprintNotVerifiedError, ); }); test('no handler registered → TOFU + warn + persists verification', async () => { const originalWarn = console.warn; let warnings = 0; console.warn = () => { warnings += 1; }; try { // backup-import always fires (no threshold) await gates.checkBackupImport('bob', 'FP'); } finally { console.warn = originalWarn; } expect(warnings).toBe(1); // The peer is now considered verified at FP under the // tofu-after-warning source. expect(await gates.isVerified('bob', 'FP')).toBe(true); const v = await storage.getPeerVerification('bob'); expect(v?.verifiedBy).toBe('tofu-after-warning'); }); test('warn fires only once per peer', async () => { const originalWarn = console.warn; let warnings = 0; console.warn = () => { warnings += 1; }; try { await gates.checkBackupImport('bob', 'FP'); await gates.checkBackupImport('bob', 'FP'); await gates.checkBackupImport('bob', 'FP'); } finally { console.warn = originalWarn; } expect(warnings).toBe(1); }); test('isVerified is fingerprint-sensitive', async () => { await gates.markVerified('bob', 'FP_OLD'); expect(await gates.isVerified('bob', 'FP_OLD')).toBe(true); expect(await gates.isVerified('bob', 'FP_NEW')).toBe(false); }); test('identity-version bump invalidates verification', async () => { await gates.markVerified('bob', 'FP'); expect(await gates.isVerified('bob', 'FP')).toBe(true); await storage.bumpPeerIdentityVersion('bob'); expect(await gates.isVerified('bob', 'FP')).toBe(false); }); test('revoke removes saved verification', async () => { await gates.markVerified('bob', 'FP'); expect(await gates.isVerified('bob', 'FP')).toBe(true); await gates.revoke('bob'); expect(await gates.isVerified('bob', 'FP')).toBe(false); }); test('backup-import and new-device are minimum-gates (no threshold bypass)', async () => { let backupCalled = false; let newDeviceCalled = false; gates.registerBackupImport(() => { backupCalled = true; return true; }); gates.registerNewDeviceTrust(() => { newDeviceCalled = true; return true; }); await gates.checkBackupImport('me', 'FP1'); await gates.checkNewDeviceTrust('bob', 'FP2'); expect(backupCalled).toBe(true); expect(newDeviceCalled).toBe(true); }); });