import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { setupTestDb } from '../../tests/helpers.js'; import type { Database } from './db/types.js'; import { setLocalCredentialPassword } from './local-credentials.js'; import { resetLocalAuthRateLimits, verifyLocalCredentials } from './local-auth.js'; import { upsertUserFromAuth } from './users.js'; let database: Database; beforeEach(() => { process.env.LOCAL_AUTH_ENABLED = 'true'; process.env.LOCAL_AUTH_ARGON2_MEMORY_KB = '8192'; process.env.LOCAL_AUTH_ARGON2_TIME_COST = '2'; process.env.LOCAL_AUTH_ARGON2_PARALLELISM = '1'; process.env.LOCAL_AUTH_MAX_ATTEMPTS = '2'; process.env.LOCAL_AUTH_WINDOW_SECONDS = '60'; process.env.LOCAL_AUTH_LOCKOUT_SECONDS = '10'; database = setupTestDb(); resetLocalAuthRateLimits(); }); afterEach(() => { vi.useRealTimers(); resetLocalAuthRateLimits(); database.close(); }); describe('verifyLocalCredentials', () => { it('authenticates valid local credentials', async () => { upsertUserFromAuth({ id: 'user-1', username: 'jdoe', fullName: 'Jane Doe', email: 'jane@example.com', authSource: 'Local' }); await setLocalCredentialPassword('user-1', 'averysecurepassword'); const result = await verifyLocalCredentials('jdoe', 'averysecurepassword', '127.0.0.1'); expect(result.status).toBe('success'); if (result.status === 'success') { expect(result.user.id).toBe('user-1'); expect(result.user.email).toBe('jane@example.com'); } }); it('returns invalid for bad credentials and locks after max attempts', async () => { upsertUserFromAuth({ id: 'user-2', username: 'sally', fullName: 'Sally Sample' }); await setLocalCredentialPassword('user-2', 'averysecurepassword'); const first = await verifyLocalCredentials('sally', 'wrong-password', '10.0.0.1'); const second = await verifyLocalCredentials('sally', 'wrong-password', '10.0.0.1'); const third = await verifyLocalCredentials('sally', 'wrong-password', '10.0.0.1'); expect(first.status).toBe('invalid'); expect(second.status).toBe('invalid'); expect(third.status).toBe('locked'); }); it('clears lockout after window expires', async () => { upsertUserFromAuth({ id: 'user-3', username: 'morgan', fullName: 'Morgan West' }); await setLocalCredentialPassword('user-3', 'averysecurepassword'); vi.useFakeTimers(); vi.setSystemTime(new Date('2024-01-01T00:00:00Z')); await verifyLocalCredentials('morgan', 'wrong-password', '10.0.0.2'); await verifyLocalCredentials('morgan', 'wrong-password', '10.0.0.2'); const locked = await verifyLocalCredentials('morgan', 'averysecurepassword', '10.0.0.2'); expect(locked.status).toBe('locked'); vi.advanceTimersByTime(11_000); const after = await verifyLocalCredentials('morgan', 'averysecurepassword', '10.0.0.2'); expect(after.status).toBe('success'); }); it('rejects when local auth is disabled', async () => { process.env.LOCAL_AUTH_ENABLED = 'false'; const result = await verifyLocalCredentials('anyone', 'password', '10.0.0.3'); expect(result.status).toBe('disabled'); }); });