const { test, beforeEach, afterEach } = require('node:test') const assert = require('node:assert/strict') // Exercise password-reset create/lookup/single-use consume against an in-memory // fake by monkeypatching the shared db module the model require()s. No DB. const db = require('../src/model/passwordResets/passwordResets.db') const passwordResets = require('../src/model/passwordResets/passwordResets.model') let rows let nextId const saved = {} beforeEach(() => { rows = [] nextId = 1 for (const k of ['insert', 'getById', 'findByTokenHash', 'markUsed', 'invalidatePendingForUser']) saved[k] = db[k] db.insert = async ({ tokenHash, userId, requestedIp, expiresAt }) => { const id = nextId++ rows.push({ id, token_hash: tokenHash, user_id: userId, status: 'pending', requested_ip: requestedIp ?? null, expires_at: expiresAt, created_at: new Date(), used_at: null }) return id } db.getById = async (id) => rows.find((r) => r.id === id) || null db.findByTokenHash = async (h) => rows.find((r) => r.token_hash === h) || null db.markUsed = async (id) => { const row = rows.find((r) => r.id === id && r.status === 'pending') if (!row) return 0 row.status = 'used' row.used_at = new Date() return 1 } db.invalidatePendingForUser = async (userId) => { let n = 0 for (const r of rows) if (r.user_id === userId && r.status === 'pending') { r.status = 'used'; n++ } return n } }) afterEach(() => { for (const k of Object.keys(saved)) db[k] = saved[k] }) test('create stores only the token hash, never the plaintext token', async () => { const { token } = await passwordResets.create({ userId: 7, requestedIp: '1.2.3.4' }) assert.ok(token && token.length >= 20) assert.equal(rows[0].token_hash, passwordResets.hashToken(token)) assert.notEqual(rows[0].token_hash, token) // hash, not the raw token assert.equal(rows[0].user_id, 7) assert.equal(rows[0].status, 'pending') }) test('findValidByToken resolves a pending token and rejects a wrong one', async () => { const { token } = await passwordResets.create({ userId: 7 }) const row = await passwordResets.findValidByToken(token) assert.ok(row) assert.equal(row.user_id, 7) assert.equal(await passwordResets.findValidByToken('not-a-real-token'), null) assert.equal(await passwordResets.findValidByToken(''), null) }) test('consume is single-use — the second consume loses the race', async () => { const { token } = await passwordResets.create({ userId: 7 }) const row = await passwordResets.findValidByToken(token) assert.equal(await passwordResets.consume(row.id), true) assert.equal(await passwordResets.consume(row.id), false) // already used assert.equal(await passwordResets.findValidByToken(token), null) // no longer pending }) test('an expired reset is not valid (exercises the expiry branch, not a bad token)', async () => { const { token } = await passwordResets.create({ userId: 7, ttlMinutes: -1 }) assert.ok(rows[0] && rows[0].status === 'pending') // token correct, row pending assert.equal(await passwordResets.findValidByToken(token), null) // only expiry rejects it }) test('invalidatePendingForUser retires every outstanding link for a user', async () => { const a = await passwordResets.create({ userId: 7 }) const b = await passwordResets.create({ userId: 7 }) await passwordResets.create({ userId: 99 }) // a different user's link is untouched await passwordResets.invalidatePendingForUser(7) assert.equal(await passwordResets.findValidByToken(a.token), null) assert.equal(await passwordResets.findValidByToken(b.token), null) assert.equal(rows.filter((r) => r.status === 'pending' && r.user_id === 99).length, 1) })