码桶

发现社区成员的开源项目

main
totp.test.js3.1 KB
import {
    base32Encode,
    base32Decode,
    generateTOTPSecret,
    generateTOTPCode,
    verifyTOTP,
    generateOTPAuthURI,
} from '../../functions/api/_totp.js';

describe('base32', () => {
    it('roundtrip: encode then decode returns original bytes', () => {
        const original = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
        const encoded = base32Encode(original);
        const decoded = base32Decode(encoded);
        expect(Array.from(decoded)).toEqual(Array.from(original));
    });
});

describe('generateTOTPSecret', () => {
    it('returns a non-empty string of valid base32 chars', () => {
        const secret = generateTOTPSecret();
        expect(secret.length).toBeGreaterThan(0);
        expect(/^[A-Z2-7]+$/.test(secret)).toBe(true);
    });
});

describe('generateTOTPCode', () => {
    it('returns a 6-digit string', async () => {
        const secret = generateTOTPSecret();
        const code = await generateTOTPCode(secret, 1000000);
        expect(code).toHaveLength(6);
        expect(/^\d{6}$/.test(code)).toBe(true);
    });

    it('is deterministic for same secret and time', async () => {
        const secret = generateTOTPSecret();
        const time = 1700000000;
        const code1 = await generateTOTPCode(secret, time);
        const code2 = await generateTOTPCode(secret, time);
        expect(code1).toBe(code2);
    });
});

describe('verifyTOTP', () => {
    it('accepts valid code (generate then verify)', async () => {
        const secret = generateTOTPSecret();
        const now = Math.floor(Date.now() / 1000);
        const code = await generateTOTPCode(secret, now);
        const result = await verifyTOTP(secret, code);
        expect(result).toBe(true);
    });

    it('rejects wrong code', async () => {
        const secret = generateTOTPSecret();
        const result = await verifyTOTP(secret, '000000');
        // There is a very small chance this could be a valid code, but practically it won't be
        // We use a fixed secret to make it deterministic
        const fixedSecret = 'JBSWY3DPEHPK3PXP';
        const wrongCode = '999999';
        const result2 = await verifyTOTP(fixedSecret, wrongCode);
        // The chance of 999999 being valid across 3 windows is extremely small
        expect(result2).toBe(false);
    });

    it('rejects empty code', async () => {
        const secret = generateTOTPSecret();
        expect(await verifyTOTP(secret, '')).toBe(false);
    });

    it('rejects null code', async () => {
        const secret = generateTOTPSecret();
        expect(await verifyTOTP(secret, null)).toBe(false);
    });
});

describe('generateOTPAuthURI', () => {
    it('returns otpauth:// URI with correct format', () => {
        const uri = generateOTPAuthURI('JBSWY3DPEHPK3PXP', 'admin', 'CF-DNS-Manager');
        expect(uri).toMatch(/^otpauth:\/\/totp\//);
        expect(uri).toContain('secret=JBSWY3DPEHPK3PXP');
        expect(uri).toContain('issuer=CF-DNS-Manager');
        expect(uri).toContain('algorithm=SHA1');
        expect(uri).toContain('digits=6');
        expect(uri).toContain('period=30');
    });
});