import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { PairingManager, type PairingStore } from './pairing.js'; describe('PairingManager', () => { let manager: PairingManager; beforeEach(() => { manager = new PairingManager({ enabled: true, codeTtl: 300_000, // 5 minutes codeLength: 6, }); }); afterEach(() => { vi.useRealTimers(); }); it('generateCode returns a code of the configured length', () => { const code = manager.generateCode(); expect(code).toHaveLength(6); expect(code).toMatch(/^[0-9A-F]+$/); }); it('generateCode with a label stores the label', () => { const code = manager.generateCode('for alice'); const pending = manager.listPendingCodes(); expect(pending).toHaveLength(1); expect(pending[0].code).toBe(code); expect(pending[0].label).toBe('for alice'); }); it('validateCode succeeds with a valid code and approves the sender', () => { const code = manager.generateCode(); const result = manager.validateCode('telegram', '12345', code); expect(result).toBe(true); expect(manager.isApproved('telegram', '12345')).toBe(true); }); it('validateCode fails with an invalid code', () => { manager.generateCode(); const result = manager.validateCode('telegram', '12345', 'ZZZZZZ'); expect(result).toBe(false); expect(manager.isApproved('telegram', '12345')).toBe(false); }); it('validateCode is case-insensitive', () => { const code = manager.generateCode(); const result = manager.validateCode('telegram', '12345', code.toLowerCase()); expect(result).toBe(true); expect(manager.isApproved('telegram', '12345')).toBe(true); }); it('validateCode fails with an expired code', () => { vi.useFakeTimers(); const code = manager.generateCode(); // Advance time past the TTL vi.advanceTimersByTime(300_001); const result = manager.validateCode('telegram', '12345', code); expect(result).toBe(false); expect(manager.isApproved('telegram', '12345')).toBe(false); }); it('validateCode removes the code after successful use', () => { const code = manager.generateCode(); manager.validateCode('telegram', '12345', code); // Code should no longer be pending expect(manager.listPendingCodes()).toHaveLength(0); // Second use of the same code should fail const result = manager.validateCode('discord', '67890', code); expect(result).toBe(false); }); it('isApproved returns true after validation', () => { const code = manager.generateCode(); manager.validateCode('telegram', '12345', code); expect(manager.isApproved('telegram', '12345')).toBe(true); }); it('isApproved returns false for unapproved senders', () => { expect(manager.isApproved('telegram', 'unknown')).toBe(false); }); it('isApproved distinguishes between channels', () => { const code = manager.generateCode(); manager.validateCode('telegram', '12345', code); expect(manager.isApproved('telegram', '12345')).toBe(true); expect(manager.isApproved('discord', '12345')).toBe(false); }); it('revokeApproval removes approval', () => { const code = manager.generateCode(); manager.validateCode('telegram', '12345', code); expect(manager.isApproved('telegram', '12345')).toBe(true); const revoked = manager.revokeApproval('telegram', '12345'); expect(revoked).toBe(true); expect(manager.isApproved('telegram', '12345')).toBe(false); }); it('revokeApproval returns false for non-existent sender', () => { const revoked = manager.revokeApproval('telegram', 'nonexistent'); expect(revoked).toBe(false); }); it('listApproved returns all approved senders', () => { const code1 = manager.generateCode(); const code2 = manager.generateCode(); manager.validateCode('telegram', '111', code1); manager.validateCode('discord', '222', code2); const approved = manager.listApproved(); expect(approved).toHaveLength(2); expect(approved.map(a => a.senderId)).toContain('111'); expect(approved.map(a => a.senderId)).toContain('222'); }); it('listPendingCodes returns only non-expired codes', () => { vi.useFakeTimers(); manager.generateCode('first'); // Advance time so the first code is almost expired vi.advanceTimersByTime(200_000); const code2 = manager.generateCode('second'); // Advance past first code's expiry vi.advanceTimersByTime(100_001); const pending = manager.listPendingCodes(); expect(pending).toHaveLength(1); expect(pending[0].code).toBe(code2); expect(pending[0].label).toBe('second'); }); it('cleanup removes expired codes', () => { vi.useFakeTimers(); manager.generateCode(); vi.advanceTimersByTime(300_001); manager.cleanup(); expect(manager.listPendingCodes()).toHaveLength(0); }); it('enabled getter reflects config', () => { expect(manager.enabled).toBe(true); const disabled = new PairingManager({ enabled: false, codeTtl: 300_000, codeLength: 6, }); expect(disabled.enabled).toBe(false); }); describe('PairingManager with store', () => { it('calls store.saveApproved when a code is validated', () => { const store: PairingStore = { loadApproved: () => [], saveApproved: vi.fn(), removeApproved: vi.fn(), }; const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 }, store); const code = mgr.generateCode(); mgr.validateCode('telegram', '123', code); expect(store.saveApproved).toHaveBeenCalledWith(expect.objectContaining({ channel: 'telegram', senderId: '123', })); }); it('calls store.removeApproved when approval is revoked', () => { const store: PairingStore = { loadApproved: () => [], saveApproved: vi.fn(), removeApproved: vi.fn(), }; const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 }, store); const code = mgr.generateCode(); mgr.validateCode('telegram', '123', code); mgr.revokeApproval('telegram', '123'); expect(store.removeApproved).toHaveBeenCalledWith('telegram', '123'); }); it('loads approved senders from store on construction', () => { const store: PairingStore = { loadApproved: () => [ { channel: 'telegram', senderId: '111', approvedAt: Date.now(), codeUsed: 'AAAAAA' }, { channel: 'discord', senderId: '222', approvedAt: Date.now(), codeUsed: 'BBBBBB' }, ], saveApproved: vi.fn(), removeApproved: vi.fn(), }; const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 }, store); expect(mgr.isApproved('telegram', '111')).toBe(true); expect(mgr.isApproved('discord', '222')).toBe(true); expect(mgr.listApproved()).toHaveLength(2); }); it('works without a store (backward-compatible)', () => { const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 }); const code = mgr.generateCode(); mgr.validateCode('telegram', '123', code); expect(mgr.isApproved('telegram', '123')).toBe(true); }); }); });