import { base32Decode, base32Encode, generateTotpSecret, hashPassword, hotp, InMemorySessionRepo, totp, totpUri, verifyPassword, verifyTotp, } from '../src/accounts'; describe('password hashing (scrypt)', () => { it('round-trips a correct password', () => { const stored = hashPassword('correct horse battery staple'); expect(verifyPassword('correct horse battery staple', stored)).toBe(true); }); it('rejects a wrong password', () => { const stored = hashPassword('correct horse battery staple'); expect(verifyPassword('wrong', stored)).toBe(false); }); it('uses a random salt per hash', () => { expect(hashPassword('same')).not.toBe(hashPassword('same')); }); it('rejects malformed stored hashes', () => { expect(verifyPassword('x', 'not-a-hash')).toBe(false); expect(verifyPassword('x', '')).toBe(false); }); }); describe('base32', () => { it('round-trips bytes', () => { const buf = Buffer.from('hello world, this is base32'); expect(base32Decode(base32Encode(buf)).equals(buf)).toBe(true); }); it('encodes without padding', () => { expect(base32Encode(Buffer.from('f'))).toBe('MY'); expect(base32Encode(Buffer.from('fo'))).toBe('MZXQ'); }); }); describe('TOTP (RFC 6238)', () => { // RFC 6238 SHA-1 seed, ASCII "12345678901234567890", base32-encoded. const RFC_SEED_B32 = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; it.each([ [59_000, '287082'], [1_111_111_109_000, '081804'], [1_234_567_890_000, '005924'], ])('produces the RFC 6-digit truncation at %ims', (atMs, expected) => { expect(totp(RFC_SEED_B32, atMs)).toBe(expected); }); it('hotp pads short codes to 6 digits', () => { expect(hotp(RFC_SEED_B32, Math.floor(1_234_567_890 / 30))).toBe('005924'); }); it('verifyTotp accepts codes within the window and rejects outside', () => { const at = 1_111_111_109_000; const code = totp(RFC_SEED_B32, at); expect(verifyTotp(RFC_SEED_B32, code, at)).toBe(true); expect(verifyTotp(RFC_SEED_B32, code, at + 30_000)).toBe(true); // one step later, window 1 expect(verifyTotp(RFC_SEED_B32, code, at + 90_000)).toBe(false); // three steps later }); it('verifyTotp rejects malformed codes', () => { expect(verifyTotp(RFC_SEED_B32, '12345', 59_000)).toBe(false); expect(verifyTotp(RFC_SEED_B32, 'abcdef', 59_000)).toBe(false); }); it('generateTotpSecret returns a decodable 160-bit base32 secret', () => { const secret = generateTotpSecret(); expect(secret).toMatch(/^[A-Z2-7]{32}$/); expect(base32Decode(secret).length).toBe(20); }); it('totpUri builds an otpauth URI', () => { const uri = totpUri('ABC234', 'ada@example.com', 'Zappier'); expect(uri).toBe( 'otpauth://totp/Zappier:ada%40example.com?secret=ABC234&issuer=Zappier', ); }); }); describe('InMemorySessionRepo', () => { it('creates and retrieves sessions within the TTL', () => { const repo = new InMemorySessionRepo(); const s = repo.create('cust_1', 60_000); expect(s.customerId).toBe('cust_1'); expect(repo.get(s.token, s.createdMs + 30_000)?.customerId).toBe('cust_1'); }); it('expires sessions after the TTL', () => { const repo = new InMemorySessionRepo(); const s = repo.create('cust_1', 60_000); expect(repo.get(s.token, s.createdMs + 61_000)).toBeUndefined(); }); it('deletes sessions (logout)', () => { const repo = new InMemorySessionRepo(); const s = repo.create('cust_1', 60_000); repo.delete(s.token); expect(repo.get(s.token, s.createdMs)).toBeUndefined(); }); it('returns undefined for unknown tokens', () => { expect(new InMemorySessionRepo().get('nope')).toBeUndefined(); }); });