'use strict'; // Phase 1 — totpService is the 2FA core. Pins: a valid TOTP token verifies and a // wrong one is rejected (raw + through the encrypted-secret path), recovery codes // are single-use, and login challenges are single-use + expiring. // (webauthnService full verify needs a real authenticator harness — deferred.) const test = require('node:test'); const assert = require('node:assert/strict'); const os = require('node:os'); const path = require('node:path'); const fs = require('node:fs'); const { generateSync } = require('otplib'); const dbPath = path.join(os.tmpdir(), `bill-tracker-totp-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database.cts'); const { encryptSecret } = require('../services/encryptionService.cts'); const totp = require('../services/totpService.cts'); let db, userId; test.before(() => { db = getDb(); userId = db .prepare( "INSERT INTO users (username, password_hash, role, active) VALUES ('totp-user','x','user',1)", ) .run().lastInsertRowid; }); test.after(() => { closeDb(); for (const s of ['', '-wal', '-shm']) { try { fs.rmSync(dbPath + s); } catch {} } }); test('verifyTokenRaw: a current token validates, a wrong one is rejected', () => { const secret = totp.generateSecret(); const token = generateSync({ secret, type: 'totp' }); assert.equal(totp.verifyTokenRaw(secret, token), true, 'the current token verifies'); assert.equal(totp.verifyTokenRaw(secret, '000000'), false, 'a wrong token is rejected'); assert.equal(totp.verifyTokenRaw(secret, ''), false); assert.equal(totp.verifyTokenRaw('', token), false); }); test('verifyToken: works through the encrypted-secret path', () => { const secret = totp.generateSecret(); const enc = encryptSecret(secret); const token = generateSync({ secret, type: 'totp' }); assert.equal(totp.verifyToken(enc, token), true); assert.equal(totp.verifyToken(enc, '000000'), false); assert.equal(totp.verifyToken(null, token), false, 'no secret → false, no throw'); }); test('recovery codes are single-use', () => { const codes = ['ABCDE-12345', 'FGHIJ-67890']; db.prepare('UPDATE users SET totp_recovery_codes = ? WHERE id = ?').run( encryptSecret(JSON.stringify(codes.map(totp.hashRecoveryCode))), userId, ); const first = totp.consumeRecoveryCode(db, userId, 'ABCDE-12345'); assert.deepEqual(first, { used: true, remaining: 1 }, 'a valid code is consumed'); assert.equal( totp.consumeRecoveryCode(db, userId, 'ABCDE-12345').used, false, 'the same code cannot be reused', ); assert.equal( totp.consumeRecoveryCode(db, userId, 'ZZZZZ-99999').used, false, 'an unknown code is rejected', ); assert.equal( totp.consumeRecoveryCode(db, userId, 'fghij67890').used, true, 'formatting/spacing/case are normalized', ); }); test('challenges are single-use and reject the unknown/expired', () => { const id = totp.createChallenge(db, userId); assert.equal(totp.consumeChallenge(db, id), userId, 'a fresh challenge returns its user'); assert.equal(totp.consumeChallenge(db, id), null, 'the same challenge cannot be consumed twice'); assert.equal(totp.consumeChallenge(db, 'no-such-challenge'), null); // expired challenge is not honoured db.prepare('INSERT INTO totp_challenges (id, user_id, expires_at) VALUES (?, ?, ?)').run( 'expired-ch', userId, '2000-01-01 00:00:00', ); assert.equal(totp.consumeChallenge(db, 'expired-ch'), null, 'expired challenge rejected'); });