import { describe, expect, it } from 'vitest'; import { hashPassword, verifyPassword } from '../../../src/lib/server/auth/password'; import { RateLimiter } from '../../../src/lib/server/auth/rate-limit'; import { createSessionToken, verifySessionToken } from '../../../src/lib/server/auth/session'; describe('password', () => { it('hashes and verifies', async () => { const hash = await hashPassword('correct horse'); expect(hash.startsWith('scrypt$')).toBe(true); expect(await verifyPassword('correct horse', hash)).toBe(true); expect(await verifyPassword('wrong', hash)).toBe(false); }); it('salts: same password gives different hashes', async () => { expect(await hashPassword('x')).not.toBe(await hashPassword('x')); }); it('rejects malformed hashes without throwing', async () => { expect(await verifyPassword('x', 'garbage')).toBe(false); }); it('rejects stored hashes with out-of-bound scrypt params', async () => { const placeholder = ['AAAAAAAAAAAAAAAAAAAAAA==', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=']; const bigP = ['scrypt', 32768, 8, 64, ...placeholder].join('$'); expect(await verifyPassword('whatever', bigP)).toBe(false); const bigN = ['scrypt', 131072, 8, 1, ...placeholder].join('$'); expect(await verifyPassword('whatever', bigN)).toBe(false); }); it('resolves 10 concurrent hashPassword calls (semaphore sanity)', async () => { const results = await Promise.all(Array.from({ length: 10 }, (_, i) => hashPassword(`pw-${i}`))); expect(results).toHaveLength(10); for (const hash of results) expect(hash.startsWith('scrypt$')).toBe(true); }); // 4 slots + a 64-deep wait queue: past that, callers are shed instead of parked forever. it('sheds callers once the scrypt wait queue is full', async () => { const settled = await Promise.allSettled(Array.from({ length: 70 }, (_, i) => hashPassword(`flood-${i}`))); const rejected = settled.filter((r) => r.status === 'rejected'); expect(rejected.length).toBeGreaterThan(0); for (const r of rejected) expect((r.reason as Error).message).toBe('Too many concurrent login attempts'); for (const r of settled.filter((r) => r.status === 'fulfilled')) { expect((r as PromiseFulfilledResult).value.startsWith('scrypt$')).toBe(true); } }, 60_000); }); describe('session token', () => { const secret = 'test-secret-with-enough-length-0123456789'; it('round-trips a payload', () => { const token = createSessionToken({ uid: 'u_1', sv: 3 }, secret, new Date('2026-01-01T00:00:00Z'), 60); expect(verifySessionToken(token, secret, new Date('2026-01-01T00:00:30Z'))).toEqual({ uid: 'u_1', sv: 3, exp: Date.parse('2026-01-01T00:01:00Z') }); }); it('rejects tampering, wrong secret, and expiry', () => { const token = createSessionToken({ uid: 'u_1', sv: 3 }, secret, new Date('2026-01-01T00:00:00Z'), 60); expect(verifySessionToken(token + 'x', secret, new Date('2026-01-01T00:00:00Z'))).toBeNull(); expect(verifySessionToken(token, 'other-secret', new Date('2026-01-01T00:00:00Z'))).toBeNull(); expect(verifySessionToken(token, secret, new Date('2026-01-01T00:01:01Z'))).toBeNull(); const [payload, sig] = token.split('.'); const forged = Buffer.from(JSON.stringify({ ...JSON.parse(Buffer.from(payload, 'base64url').toString()), uid: 'u_admin' })).toString('base64url'); expect(verifySessionToken(`${forged}.${sig}`, secret, new Date('2026-01-01T00:00:00Z'))).toBeNull(); expect(verifySessionToken('nonsense', secret, new Date())).toBeNull(); }); }); describe('RateLimiter', () => { it('allows up to max attempts per window, then blocks until the window slides', () => { let now = 0; const rl = new RateLimiter(3, 60_000, () => now); expect(rl.allow('bob')).toBe(true); expect(rl.allow('bob')).toBe(true); expect(rl.allow('bob')).toBe(true); expect(rl.allow('bob')).toBe(false); expect(rl.allow('alice')).toBe(true); now = 60_001; expect(rl.allow('bob')).toBe(true); }); it('reset() clears a key so it is immediately allowed again', () => { let now = 0; const rl = new RateLimiter(5, 60_000, () => now); expect(rl.allow('bob')).toBe(true); expect(rl.allow('bob')).toBe(true); expect(rl.allow('bob')).toBe(true); expect(rl.allow('bob')).toBe(true); expect(rl.allow('bob')).toBe(true); expect(rl.allow('bob')).toBe(false); rl.reset('bob'); expect(rl.allow('bob')).toBe(true); }); it('evicts stale keys once the tracked-key cap is exceeded', () => { let now = 0; const rl = new RateLimiter(5, 1_000, () => now); for (let i = 0; i < 10_001; i++) rl.allow(`key-${i}`); expect(rl.size).toBe(10_001); now = 2_000; // window has fully passed for every key inserted above expect(rl.allow('new-key')).toBe(true); expect(rl.size).toBe(1); }); });