/**
* Unit tests for lib/auth/challenge-manager.js
*
* Tests challenge generation, expiry, consumption, replay prevention, and cleanup.
*/
import test from 'brittle'
import { ChallengeManager } from '../../lib/auth/challenge-manager.js'
function createManager (opts = {}) {
return new ChallengeManager({ ttl: opts.ttl || 5000, ...opts })
}
// ========================================
// Challenge Generation
// ========================================
test('generate - returns challengeId, challenge, and expiresAt', async (t) => {
const mgr = createManager()
const result = await mgr.generate()
t.ok(result.challengeId, 'has challengeId')
t.ok(result.challenge, 'has challenge')
t.ok(result.expiresAt > Date.now(), 'expiresAt is in the future')
t.is(result.challengeId.length, 32, 'challengeId is 32 hex chars')
mgr.destroy()
})
test('generate - each challenge is unique', async (t) => {
const mgr = createManager()
const a = await mgr.generate()
const b = await mgr.generate()
t.not(a.challengeId, b.challengeId, 'different IDs')
t.not(a.challenge, b.challenge, 'different challenge values')
mgr.destroy()
})
// ========================================
// Challenge Retrieval
// ========================================
test('get - returns valid challenge', async (t) => {
const mgr = createManager()
const { challengeId } = await mgr.generate()
const record = await mgr.get(challengeId)
t.ok(record, 'record exists')
t.ok(record.challenge, 'has challenge value')
t.is(record.used, false, 'not used')
mgr.destroy()
})
test('get - returns null for unknown challengeId', async (t) => {
const mgr = createManager()
const record = await mgr.get('nonexistent')
t.is(record, null, 'null for unknown ID')
mgr.destroy()
})
test('get - returns null for expired challenge', async (t) => {
const mgr = createManager({ ttl: 1 }) // 1ms TTL
const { challengeId } = await mgr.generate()
// Wait for expiry
await new Promise(r => setTimeout(r, 10))
const record = await mgr.get(challengeId)
t.is(record, null, 'expired challenge returns null')
mgr.destroy()
})
// ========================================
// Challenge Consumption
// ========================================
test('consume - marks challenge as used', async (t) => {
const mgr = createManager()
const { challengeId } = await mgr.generate()
const consumed = await mgr.consume(challengeId)
t.ok(consumed, 'consumed successfully')
// Second consume should fail (replay prevention)
const record = await mgr.get(challengeId)
t.is(record, null, 'used challenge returns null from get')
mgr.destroy()
})
test('consume - prevents replay (double consumption)', async (t) => {
const mgr = createManager()
const { challengeId } = await mgr.generate()
const first = await mgr.consume(challengeId)
t.ok(first, 'first consume succeeds')
// Try consuming via verify with the challenge
const record = await mgr.get(challengeId)
t.is(record, null, 'challenge no longer available after consumption')
mgr.destroy()
})
test('consume - fails for expired challenge', async (t) => {
const mgr = createManager({ ttl: 1 })
const { challengeId } = await mgr.generate()
await new Promise(r => setTimeout(r, 10))
const consumed = await mgr.consume(challengeId)
t.not(consumed, 'expired challenge not consumed')
mgr.destroy()
})
test('consume - fails for unknown challengeId', async (t) => {
const mgr = createManager()
const consumed = await mgr.consume('nonexistent')
t.not(consumed, 'unknown challenge not consumed')
mgr.destroy()
})
// ========================================
// Challenge Verification
// ========================================
test('verify - accepts valid challenge', async (t) => {
const mgr = createManager()
const { challengeId, challenge } = await mgr.generate()
const valid = await mgr.verify(challengeId, challenge)
t.ok(valid, 'valid challenge accepted')
mgr.destroy()
})
test('verify - rejects wrong challenge value', async (t) => {
const mgr = createManager()
const { challengeId } = await mgr.generate()
const valid = await mgr.verify(challengeId, 'wrong-value')
t.not(valid, 'wrong challenge rejected')
mgr.destroy()
})
test('verify - rejects already-verified challenge (replay)', async (t) => {
const mgr = createManager()
const { challengeId, challenge } = await mgr.generate()
const first = await mgr.verify(challengeId, challenge)
t.ok(first, 'first verify succeeds')
const second = await mgr.verify(challengeId, challenge)
t.not(second, 'second verify fails (replay prevention)')
mgr.destroy()
})
test('verify - rejects expired challenge', async (t) => {
const mgr = createManager({ ttl: 1 })
const { challengeId, challenge } = await mgr.generate()
await new Promise(r => setTimeout(r, 10))
const valid = await mgr.verify(challengeId, challenge)
t.not(valid, 'expired challenge rejected')
mgr.destroy()
})
// ========================================
// Cleanup
// ========================================
test('cleanup - removes expired challenges from cache', async (t) => {
const mgr = createManager({ ttl: 1 })
await mgr.generate()
await mgr.generate()
await new Promise(r => setTimeout(r, 10))
await mgr.cleanup()
t.is(mgr.cache.size, 0, 'cache empty after cleanup')
mgr.destroy()
})
test('cleanup - keeps active challenges', async (t) => {
const mgr = createManager({ ttl: 60000 })
await mgr.generate()
await mgr.generate()
await mgr.cleanup()
t.is(mgr.cache.size, 2, 'active challenges kept')
mgr.destroy()
})
// ========================================
// Stats
// ========================================
test('stats - counts challenges', async (t) => {
const mgr = createManager()
await mgr.generate()
await mgr.generate()
const { challengeId } = await mgr.generate()
await mgr.consume(challengeId)
const stats = await mgr.stats()
t.is(stats.total, 3, '3 total')
t.is(stats.active, 2, '2 active')
t.is(stats.used, 1, '1 used')
mgr.destroy()
})
// ========================================
// Destroy
// ========================================
test('destroy - clears cache and stops cleanup', (t) => {
const mgr = createManager()
mgr.cache.set('test', {})
t.is(mgr.cache.size, 1, 'cache has entry')
mgr.destroy()
t.is(mgr.cache.size, 0, 'cache cleared')
})