Files
Shade/packages/shade-sdk/tests/gates-unit.test.ts

150 lines
4.8 KiB
TypeScript
Raw Normal View History

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);
});
});