import { describe, it, expect, vi, beforeEach } from 'vitest' const { mockGetSession, mockFindFirstProduct, mockCreateProduct, mockUpdateProduct, mockCreateMember, mockExecuteRaw, mockTransaction, } = vi.hoisted(() => ({ mockGetSession: vi.fn(), mockFindFirstProduct: vi.fn(), mockCreateProduct: vi.fn(), mockUpdateProduct: vi.fn(), mockCreateMember: vi.fn(), mockExecuteRaw: vi.fn().mockResolvedValue(undefined), mockTransaction: vi.fn(), })) vi.mock('next/cache', () => ({ revalidatePath: vi.fn() })) vi.mock('next/navigation', () => ({ redirect: vi.fn() })) vi.mock('next/headers', () => ({ cookies: vi.fn().mockResolvedValue({}) })) vi.mock('iron-session', () => ({ getIronSession: vi.fn().mockResolvedValue({ userId: 'user-1', isDemo: false }), })) vi.mock('@/lib/session', () => ({ sessionOptions: { cookieName: 'test', password: 'test' }, })) vi.mock('@/lib/auth', () => ({ getSession: mockGetSession })) vi.mock('@/lib/product-access', () => ({ productAccessFilter: vi.fn().mockReturnValue({ OR: [{ user_id: 'user-1' }] }), })) vi.mock('@/lib/prisma', () => ({ prisma: { product: { findFirst: mockFindFirstProduct, create: mockCreateProduct, update: mockUpdateProduct }, productMember: { create: mockCreateMember }, $executeRaw: mockExecuteRaw, $transaction: mockTransaction, }, })) import { createProductAction, updateProductAction } from '@/actions/products' import { getIronSession } from 'iron-session' const mockSession = getIronSession as ReturnType const SESSION_USER = { userId: 'user-1', isDemo: false } const SESSION_DEMO = { userId: 'demo-1', isDemo: true } const PRODUCT_ID = 'product-1' const VALID_DATA = { name: 'Test Product', code: 'TP', description: 'Een product', repo_url: 'https://github.com/org/repo', definition_of_done: 'Alles groen', auto_pr: false, } beforeEach(() => { vi.clearAllMocks() mockExecuteRaw.mockResolvedValue(undefined) mockSession.mockResolvedValue(SESSION_USER) }) // ============================================================= // createProductAction // ============================================================= describe('createProductAction', () => { it('happy path: maakt product + member aan en retourneert productId', async () => { mockFindFirstProduct.mockResolvedValue(null) // geen dubbele code mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => { return fn({ product: { create: vi.fn().mockResolvedValue({ id: PRODUCT_ID }), }, productMember: { create: vi.fn().mockResolvedValue({}), }, }) }) const result = await createProductAction(VALID_DATA) expect(result).toEqual({ success: true, productId: PRODUCT_ID }) }) it('demo-user → error', async () => { mockSession.mockResolvedValue(SESSION_DEMO) const result = await createProductAction(VALID_DATA) expect(result).toMatchObject({ error: expect.stringContaining('demo') }) expect(mockTransaction).not.toHaveBeenCalled() }) it('ongeldige repo_url (niet github) → validatiefout', async () => { const result = await createProductAction({ ...VALID_DATA, repo_url: 'https://gitlab.com/org/repo' }) expect(result).toMatchObject({ error: expect.any(String) }) expect(mockTransaction).not.toHaveBeenCalled() }) it('dubbele code → error', async () => { mockFindFirstProduct.mockResolvedValue({ id: 'other-product' }) const result = await createProductAction(VALID_DATA) expect(result).toMatchObject({ code: 422, fieldErrors: { code: expect.arrayContaining([expect.stringContaining('gebruik')]) }, }) expect(mockTransaction).not.toHaveBeenCalled() }) it('naam ontbreekt → validatiefout', async () => { const result = await createProductAction({ ...VALID_DATA, name: '' }) expect(result).toMatchObject({ error: expect.any(String) }) }) }) // ============================================================= // updateProductAction // ============================================================= describe('updateProductAction', () => { it('happy path: werkt product bij en stuurt pg_notify', async () => { mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID }) mockUpdateProduct.mockResolvedValue({ id: PRODUCT_ID }) const result = await updateProductAction(PRODUCT_ID, VALID_DATA) expect(result).toEqual({ success: true }) expect(mockUpdateProduct).toHaveBeenCalled() expect(mockExecuteRaw).toHaveBeenCalledTimes(1) }) it('demo-user → error', async () => { mockSession.mockResolvedValue(SESSION_DEMO) const result = await updateProductAction(PRODUCT_ID, VALID_DATA) expect(result).toMatchObject({ error: expect.stringContaining('demo') }) expect(mockUpdateProduct).not.toHaveBeenCalled() }) it('geen toegang tot product → error', async () => { mockFindFirstProduct.mockResolvedValue(null) const result = await updateProductAction(PRODUCT_ID, VALID_DATA) expect(result).toMatchObject({ error: expect.stringContaining('toegang') }) expect(mockUpdateProduct).not.toHaveBeenCalled() }) it('ongeldige repo_url → validatiefout', async () => { const result = await updateProductAction(PRODUCT_ID, { ...VALID_DATA, repo_url: 'https://bitbucket.org/x' }) expect(result).toMatchObject({ error: expect.any(String) }) expect(mockUpdateProduct).not.toHaveBeenCalled() }) })