initial commit
[squeep-totp] / test / totp.js
diff --git a/test/totp.js b/test/totp.js
new file mode 100644 (file)
index 0000000..8c08bbf
--- /dev/null
@@ -0,0 +1,139 @@
+/* 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