import { createHash } from 'crypto'; import { base64Url, generateCodeChallenge, generateCodeVerifier, generateState, } from './pkce.util'; describe('pkce.util', () => { describe('base64Url', () => { it('strips padding and replaces + and / with - and _', () => { const input = Buffer.from([0xfb, 0xff, 0xbf, 0xfe]); const out = base64Url(input); expect(out).not.toMatch(/[+/=]/); }); }); describe('generateCodeVerifier', () => { it('returns a base64url-safe string', () => { expect(generateCodeVerifier()).toMatch(/^[A-Za-z0-9_-]+$/); }); it('produces unique values across calls', () => { const a = generateCodeVerifier(); const b = generateCodeVerifier(); expect(a).not.toEqual(b); }); it('produces at least 43 characters (RFC 7636 minimum)', () => { expect(generateCodeVerifier().length).toBeGreaterThanOrEqual(43); }); }); describe('generateCodeChallenge', () => { it('equals base64url(sha256(verifier))', () => { const verifier = 'fixed-test-verifier'; const expected = createHash('sha256') .update(verifier) .digest('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); expect(generateCodeChallenge(verifier)).toBe(expected); }); it('is deterministic for the same verifier', () => { const verifier = generateCodeVerifier(); expect(generateCodeChallenge(verifier)).toBe(generateCodeChallenge(verifier)); }); it('differs for different verifiers', () => { expect(generateCodeChallenge('a')).not.toBe(generateCodeChallenge('b')); }); }); describe('generateState', () => { it('returns a base64url-safe string', () => { expect(generateState()).toMatch(/^[A-Za-z0-9_-]+$/); }); it('produces unique values across calls', () => { expect(generateState()).not.toEqual(generateState()); }); }); });