import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const { scrypt } = vi.hoisted(() => ({ scrypt: vi.fn() })); vi.mock('node:crypto', async (importOriginal) => ({ ...(await importOriginal()), scrypt })); let passwords: typeof import('../../src/lib/server/auth/passwords.js'); let running: Array<(error: Error | null, result: Buffer) => void>; const key = Buffer.alloc(64, 7); const salt = 'a'.repeat(64); const encoded = `scrypt$131072$8$1$${salt}$${key.toString('hex')}`; beforeEach(async () => { vi.resetModules(); vi.useFakeTimers(); running = []; scrypt.mockReset().mockImplementation((_password, _salt, _length, _parameters, callback) => { running.push(callback); }); passwords = await import('../../src/lib/server/auth/passwords.js'); }); afterEach(() => { vi.useRealTimers(); }); function finishNext(error: Error | null = null) { const callback = running.shift(); if (!callback) throw new Error('No running scrypt operation'); callback(error, key); } describe('password work queue', () => { it('shares two slots across hashing and verification and serves waiting requests in order', async () => { const first = passwords.hashPassword('first password'); const second = passwords.verifyPassword('second password', encoded); const third = passwords.verifyPassword('unknown account', null); const fourth = passwords.hashPassword('fourth password'); await Promise.resolve(); expect(scrypt).toHaveBeenCalledTimes(2); expect(vi.getTimerCount()).toBe(2); finishNext(); const fifth = passwords.verifyPassword('later arrival', encoded); await first; expect(scrypt).toHaveBeenCalledTimes(3); expect(scrypt.mock.calls[2]).toEqual([ 'unknown account', '0'.repeat(64), 64, { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 }, expect.any(Function) ]); expect(running).toHaveLength(2); finishNext(); expect(await second).toBe(true); expect(scrypt.mock.calls[3][0]).toBe('fourth password'); expect(running).toHaveLength(2); finishNext(); expect(await third).toBe(false); expect(scrypt.mock.calls[4][0]).toBe('later arrival'); expect(running).toHaveLength(2); expect(vi.getTimerCount()).toBe(0); finishNext(); finishNext(); await expect(fourth).resolves.toMatch(/^scrypt\$/); await expect(fifth).resolves.toBe(true); expect(running).toHaveLength(0); }); it('expires waiting requests after two seconds without deriving them or blocking later work', async () => { const first = passwords.hashPassword('first password'); const second = passwords.hashPassword('second password'); const expired = expect( passwords.verifyPassword('expired password', encoded) ).rejects.toBeInstanceOf(passwords.PasswordServiceBusyError); await vi.advanceTimersByTimeAsync(1999); expect(vi.getTimerCount()).toBe(1); expect(scrypt).toHaveBeenCalledTimes(2); await vi.advanceTimersByTimeAsync(1); await expired; expect(vi.getTimerCount()).toBe(0); const later = passwords.verifyPassword('later password', encoded); finishNext(); await first; expect(scrypt).toHaveBeenCalledTimes(3); expect(scrypt.mock.calls[2][0]).toBe('later password'); expect(vi.getTimerCount()).toBe(0); finishNext(); finishNext(); await second; await expect(later).resolves.toBe(true); }); it('bounds the waiting queue and recovers capacity after overload', async () => { const active = [ passwords.hashPassword('first password'), passwords.hashPassword('second password') ]; const queued = Promise.allSettled( Array.from({ length: 16 }, (_, index) => passwords.verifyPassword(`queued ${index}`, encoded) ) ); await expect(passwords.hashPassword('overflow password')).rejects.toBeInstanceOf( passwords.PasswordServiceBusyError ); expect(scrypt).toHaveBeenCalledTimes(2); expect(vi.getTimerCount()).toBe(16); await vi.advanceTimersByTimeAsync(2000); for (const result of await queued) { expect(result.status).toBe('rejected'); if (result.status === 'rejected') expect(result.reason).toBeInstanceOf(passwords.PasswordServiceBusyError); } expect(scrypt).toHaveBeenCalledTimes(2); expect(vi.getTimerCount()).toBe(0); finishNext(); finishNext(); await Promise.all(active); const recovered = [ passwords.hashPassword('recovered password'), passwords.verifyPassword('recovered verification', encoded) ]; await Promise.resolve(); expect(running).toHaveLength(2); expect(vi.getTimerCount()).toBe(0); finishNext(); finishNext(); await expect(Promise.all(recovered)).resolves.toEqual([expect.any(String), true]); }); it.each(['callback', 'throw'] as const)( 'releases a slot after a scrypt %s failure', async (mode) => { const failure = new Error('scrypt failed'); if (mode === 'throw') scrypt.mockImplementationOnce(() => { throw failure; }); const failed = expect(passwords.hashPassword('failing password')).rejects.toBe(failure); const second = passwords.hashPassword('second password'); const queued = passwords.verifyPassword('queued password', encoded); await Promise.resolve(); if (mode === 'callback') finishNext(failure); await failed; expect(scrypt).toHaveBeenCalledTimes(3); expect(running).toHaveLength(2); expect(vi.getTimerCount()).toBe(0); finishNext(); finishNext(); await second; await expect(queued).resolves.toBe(true); } ); });