diff --git a/frontends/nextjs/src/lib/db/auth/authenticate-user.test.ts b/frontends/nextjs/src/lib/db/auth/authenticate-user.test.ts new file mode 100644 index 000000000..8cf3b4b05 --- /dev/null +++ b/frontends/nextjs/src/lib/db/auth/authenticate-user.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockList = vi.fn() +const mockFindFirst = vi.fn() +const mockAdapter = { list: mockList, findFirst: mockFindFirst } + +const mockVerifyPassword = vi.fn() + +vi.mock('../dbal-client', () => ({ + getAdapter: () => mockAdapter, +})) + +vi.mock('../verify-password', () => ({ + verifyPassword: mockVerifyPassword, +})) + +import { authenticateUser } from './authenticate-user' + +describe('authenticateUser', () => { + beforeEach(() => { + mockList.mockReset() + mockFindFirst.mockReset() + mockVerifyPassword.mockReset() + }) + + it('returns invalid credentials when credential is missing', async () => { + mockList.mockResolvedValue({ data: [] }) + + const result = await authenticateUser('alice', 'password') + + expect(mockList).toHaveBeenCalledWith('Credential', { filter: { username: 'alice' } }) + expect(mockVerifyPassword).not.toHaveBeenCalled() + expect(result).toEqual({ success: false, user: null, error: 'invalid_credentials' }) + }) + + it('returns invalid credentials when password is wrong', async () => { + mockList.mockResolvedValue({ data: [{ username: 'alice', passwordHash: 'hash' }] }) + mockVerifyPassword.mockResolvedValue(false) + + const result = await authenticateUser('alice', 'password') + + expect(mockVerifyPassword).toHaveBeenCalledWith('password', 'hash') + expect(mockFindFirst).not.toHaveBeenCalled() + expect(result).toEqual({ success: false, user: null, error: 'invalid_credentials' }) + }) + + it('returns user_not_found when credential is valid but user missing', async () => { + mockList.mockResolvedValue({ data: [{ username: 'alice', passwordHash: 'hash' }] }) + mockVerifyPassword.mockResolvedValue(true) + mockFindFirst.mockResolvedValue(null) + + const result = await authenticateUser('alice', 'password') + + expect(mockFindFirst).toHaveBeenCalledWith('User', { where: { username: 'alice' } }) + expect(result).toEqual({ success: false, user: null, error: 'user_not_found' }) + }) + + it.each([ + { firstLogin: true, expected: true }, + { firstLogin: false, expected: false }, + ])('returns requiresPasswordChange=$expected when firstLogin=$firstLogin', async ({ firstLogin, expected }) => { + mockList.mockResolvedValue({ data: [{ username: 'alice', passwordHash: 'hash' }] }) + mockVerifyPassword.mockResolvedValue(true) + mockFindFirst.mockResolvedValue({ + id: 'user_1', + username: 'alice', + email: 'alice@example.com', + role: 'user', + profilePicture: null, + bio: null, + createdAt: BigInt(1000), + tenantId: null, + isInstanceOwner: false, + firstLogin, + }) + + const result = await authenticateUser('alice', 'password') + + expect(result).toEqual({ + success: true, + user: { + id: 'user_1', + username: 'alice', + email: 'alice@example.com', + role: 'user', + profilePicture: undefined, + bio: undefined, + createdAt: 1000, + tenantId: undefined, + isInstanceOwner: false, + }, + requiresPasswordChange: expected, + }) + }) +}) diff --git a/frontends/nextjs/src/lib/nerd-mode-ide/delete-node.ts b/frontends/nextjs/src/lib/nerd-mode-ide/delete-node.ts new file mode 100644 index 000000000..fc821ae82 --- /dev/null +++ b/frontends/nextjs/src/lib/nerd-mode-ide/delete-node.ts @@ -0,0 +1,10 @@ +import type { FileNode } from './types' + +export function deleteNode(nodes: FileNode[], id: string): FileNode[] { + return nodes + .filter((node) => node.id !== id) + .map((node) => { + if (!node.children) return node + return { ...node, children: deleteNode(node.children, id) } + }) +} diff --git a/frontends/nextjs/src/lib/nerd-mode-ide/find-node-by-id.ts b/frontends/nextjs/src/lib/nerd-mode-ide/find-node-by-id.ts new file mode 100644 index 000000000..c2d956216 --- /dev/null +++ b/frontends/nextjs/src/lib/nerd-mode-ide/find-node-by-id.ts @@ -0,0 +1,12 @@ +import type { FileNode } from './types' + +export function findNodeById(nodes: FileNode[], id: string): FileNode | null { + for (const node of nodes) { + if (node.id === id) return node + if (node.children) { + const found = findNodeById(node.children, id) + if (found) return found + } + } + return null +} diff --git a/frontends/nextjs/src/lib/nerd-mode-ide/update-node.ts b/frontends/nextjs/src/lib/nerd-mode-ide/update-node.ts new file mode 100644 index 000000000..ccd97ff1d --- /dev/null +++ b/frontends/nextjs/src/lib/nerd-mode-ide/update-node.ts @@ -0,0 +1,13 @@ +import type { FileNode } from './types' + +export function updateNode(nodes: FileNode[], id: string, updates: Partial): FileNode[] { + return nodes.map((node) => { + if (node.id === id) { + return { ...node, ...updates } + } + if (node.children) { + return { ...node, children: updateNode(node.children, id, updates) } + } + return node + }) +}