OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / unit / challenge-manager.test.js

236 lines · javascript ref:
/**
 * 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')
})