--- /dev/null
+/* eslint-env mocha */
+'use strict';
+
+const assert = require('assert');
+const sinon = require('sinon');
+const TOTP = require('../lib/totp');
+
+describe('TOTP', function () {
+ let options, totp;
+
+ const tests = [
+ { epoch: 59, count: BigInt(0x1), algorithm: 'sha1', totp: '94287082' },
+ { epoch: 59, count: BigInt(0x1), algorithm: 'sha256', totp: '46119246' },
+ { epoch: 59, count: BigInt(0x1), algorithm: 'sha512', totp: '90693936' },
+ { epoch: 1111111109, count: BigInt(0x23523EC), algorithm: 'sha1', totp: '07081804' },
+ { epoch: 1111111109, count: BigInt(0x23523EC), algorithm: 'sha256', totp: '68084774' },
+ { epoch: 1111111109, count: BigInt(0x23523EC), algorithm: 'sha512', totp: '25091201' },
+ { epoch: 1111111111, count: BigInt(0x23523ED), algorithm: 'sha1', totp: '14050471' },
+ { epoch: 1111111111, count: BigInt(0x23523ED), algorithm: 'sha256', totp: '67062674' },
+ { epoch: 1111111111, count: BigInt(0x23523ED), algorithm: 'sha512', totp: '99943326' },
+ { epoch: 1234567890, count: BigInt(0x273EF07), algorithm: 'sha1', totp: '89005924' },
+ { epoch: 1234567890, count: BigInt(0x273EF07), algorithm: 'sha256', totp: '91819424' },
+ { epoch: 1234567890, count: BigInt(0x273EF07), algorithm: 'sha512', totp: '93441116' },
+ { epoch: 2000000000, count: BigInt(0x3F940AA), algorithm: 'sha1', totp: '69279037' },
+ { epoch: 2000000000, count: BigInt(0x3F940AA), algorithm: 'sha256', totp: '90698825' },
+ { epoch: 2000000000, count: BigInt(0x3F940AA), algorithm: 'sha512', totp: '38618901' },
+ { epoch: 20000000000, count: BigInt(0x27BC86AA), algorithm: 'sha1', totp: '65353130' },
+ { epoch: 20000000000, count: BigInt(0x27BC86AA), algorithm: 'sha256', totp: '77737706' },
+ { epoch: 20000000000, count: BigInt(0x27BC86AA), algorithm: 'sha512', totp: '47863826' },
+ ];
+
+ const algorithmKeys = {
+ 'sha1': Buffer.from('12345678901234567890'),
+ 'sha256': Buffer.from('12345678901234567890123456789012'),
+ 'sha512': Buffer.from('1234567890123456789012345678901234567890123456789012345678901234'),
+ };
+
+ beforeEach(function () {
+ sinon.restore();
+ options = {
+ codeLength: 8,
+ };
+ });
+
+ describe('constructor', function () {
+ it('covers invalid algorithm', function () {
+ options.algorithm = 'md5';
+ options.key = Buffer.from('');
+ assert.throws(() => new TOTP(options), RangeError);
+ });
+
+ it('covers invalid key size', function () {
+ options.algorithm = 'sha1';
+ options.key = Buffer.from('smol');
+ assert.throws(() => new TOTP(options), RangeError);
+ });
+ }); // constructor
+
+ describe('counter', function () {
+ for (const test of tests) {
+ it(`epoch ${test.epoch}`, function () {
+ sinon.stub(Date, 'now').returns(test.epoch * 1000);
+ options.algorithm = test.algorithm;
+ options.key = algorithmKeys[test.algorithm];
+ totp = new TOTP(options);
+ assert.strictEqual(totp.counter, test.count);
+ });
+ }
+ it('covers setting counter as nop', function () {
+ options.algorithm = 'sha1';
+ options.key = algorithmKeys[options.algorithm];
+ totp = new TOTP(options);
+ totp.counter = 0;
+ assert.notStrictEqual(totp.counter, 0);
+ });
+ }); // counter
+
+ describe('generate', function () {
+ for (const test of tests) {
+ it(`count ${test.count} ${test.algorithm}`, function () {
+ options.algorithm = test.algorithm;
+ options.key = algorithmKeys[test.algorithm];
+ totp = new TOTP(options);
+ const result = totp.generate(test.count);
+ assert.strictEqual(result, test.totp);
+ });
+ it(`epoch ${test.epoch} ${test.algorithm}`, function () {
+ sinon.stub(Date, 'now').returns(test.epoch * 1000);
+ options.algorithm = test.algorithm;
+ options.key = algorithmKeys[test.algorithm];
+ totp = new TOTP(options);
+ const result = totp.generate();
+ assert.strictEqual(result, test.totp);
+ });
+ }
+ }); // generate
+
+ describe('validate', function () {
+ const totpOffsets = {
+ '-2': '73751819',
+ '-1': '73039595',
+ '0': '45301026', // oathtool --totp -N 'Sept 29 2023 02:58:00' --digits=8 3132333435363738393031323334353637383930
+ '1': '16636878',
+ '2': '37492012',
+ }
+ beforeEach(function () {
+ const when = new Date('2023-09-29T09:58:00.000Z');
+ sinon.stub(Date, 'now').returns(when.getTime());
+ options.algorithm = 'sha1';
+ options.key = algorithmKeys['sha1'];
+ totp = new TOTP(options);
+ });
+ it('validates correct totp', function () {
+ const result = totp.validate(totpOffsets[0]);
+ assert.strictEqual(result, true);
+ });
+ it('validates totp within future window', function () {
+ const result = totp.validate(totpOffsets[1]);
+ assert.strictEqual(result, true);
+ });
+ it('validates totp within past window', function () {
+ const result = totp.validate(totpOffsets[-1]);
+ assert.strictEqual(result, true);
+ });
+ it('fails totp outside future window', function () {
+ const result = totp.validate(totpOffsets[2]);
+ assert.strictEqual(result, false);
+ });
+ it('fails totp outside past window', function () {
+ const result = totp.validate(totpOffsets[-2]);
+ assert.strictEqual(result, false);
+ });
+ it('fails invalid totp', function () {
+ const result = totp.validate('00000000');
+ assert.strictEqual(result, false);
+ });
+ }); // validate
+
+}); // TOTP
\ No newline at end of file