diff --git a/frontends/nextjs/src/lib/security/secure-db/operations/verify-credentials.test.ts b/frontends/nextjs/src/lib/security/secure-db/operations/verify-credentials.test.ts new file mode 100644 index 000000000..e1e79973f --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/operations/verify-credentials.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import type { SecurityContext } from '../types' +import { + loginAttemptTracker, + DEFAULT_AUTH_LOCKOUT_MAX_ATTEMPTS, +} from '../login-attempt-tracker' + +const mockExecuteQuery = vi.fn() + +vi.mock('../execute-query', () => ({ + executeQuery: mockExecuteQuery, +})) + +import { verifyCredentials } from './verify-credentials' + +const createContext = (): SecurityContext => ({ + user: { + id: 'user_1', + username: 'user', + email: 'user@example.com', + role: 'user', + createdAt: 0, + }, +}) + +describe('verifyCredentials', () => { + beforeEach(() => { + loginAttemptTracker.reset() + mockExecuteQuery.mockReset() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('blocks attempts when user is locked out', async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(0)) + + for (let i = 0; i < DEFAULT_AUTH_LOCKOUT_MAX_ATTEMPTS; i += 1) { + loginAttemptTracker.registerFailure('user') + } + + mockExecuteQuery.mockResolvedValue(true) + + const result = await verifyCredentials(createContext(), 'user', 'pass') + + expect(result).toBe(false) + expect(mockExecuteQuery).not.toHaveBeenCalled() + }) + + it('locks out after repeated failures', async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(0)) + + mockExecuteQuery.mockResolvedValue(false) + + for (let i = 0; i < DEFAULT_AUTH_LOCKOUT_MAX_ATTEMPTS; i += 1) { + const result = await verifyCredentials(createContext(), 'user', 'pass') + expect(result).toBe(false) + } + + expect(loginAttemptTracker.isLocked('user')).toBe(true) + + mockExecuteQuery.mockReset() + + const blocked = await verifyCredentials(createContext(), 'user', 'pass') + + expect(blocked).toBe(false) + expect(mockExecuteQuery).not.toHaveBeenCalled() + }) + + it('clears failure state after success', async () => { + mockExecuteQuery + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true) + + const first = await verifyCredentials(createContext(), 'user', 'pass') + expect(first).toBe(false) + expect(loginAttemptTracker.getState('user')).not.toBeNull() + + const second = await verifyCredentials(createContext(), 'user', 'pass') + expect(second).toBe(true) + expect(loginAttemptTracker.getState('user')).toBeNull() + }) +})