import { describe, it, expect, vi, beforeEach } from 'vitest' const { redirectMock, verifyUserMock, headerGetMock, sessionSaveMock, requireSessionMock, prismaUserUpdateMock, } = vi.hoisted(() => ({ redirectMock: vi.fn((path: string) => { throw new Error(`REDIRECT:${path}`) }), verifyUserMock: vi.fn(), headerGetMock: vi.fn(), sessionSaveMock: vi.fn(), requireSessionMock: vi.fn(), prismaUserUpdateMock: vi.fn(), })) vi.mock('next/navigation', () => ({ redirect: redirectMock })) vi.mock('next/headers', () => ({ cookies: vi.fn().mockResolvedValue({}), headers: vi.fn().mockResolvedValue({ get: headerGetMock }), })) vi.mock('iron-session', () => ({ getIronSession: vi.fn().mockResolvedValue({ userId: '', isDemo: false, save: sessionSaveMock, }), })) vi.mock('@/lib/session', () => ({ sessionOptions: { cookieName: 't', password: 't' } })) vi.mock('@/lib/auth', () => ({ verifyUser: verifyUserMock, registerUser: vi.fn(), hashPassword: vi.fn().mockResolvedValue('hashed'), })) vi.mock('@/lib/auth-guard', () => ({ requireSession: requireSessionMock })) vi.mock('@/lib/prisma', () => ({ prisma: { user: { update: prismaUserUpdateMock } }, })) vi.mock('@/lib/rate-limit', () => ({ checkRateLimit: vi.fn().mockReturnValue(true) })) import { loginAction, resetPasswordAction } from '@/actions/auth' const IPHONE_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) Mobile/15E148 Safari/604.1' const IPAD_UA = 'Mozilla/5.0 (iPad; CPU OS 17_4 like Mac OS X) Safari/604.1' const DESKTOP_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) Chrome/124.0.0.0 Safari/537.36' function fd(username: string, password: string) { const f = new FormData() f.set('username', username) f.set('password', password) return f } beforeEach(() => { redirectMock.mockClear() verifyUserMock.mockReset() headerGetMock.mockReset() sessionSaveMock.mockReset() requireSessionMock.mockReset() prismaUserUpdateMock.mockReset() }) describe('loginAction UA-redirect', () => { it('phone-UA + actief product → /m/products/[id]/solo', async () => { verifyUserMock.mockResolvedValue({ id: 'u1', is_demo: false, active_product_id: 'p1' }) headerGetMock.mockReturnValue(IPHONE_UA) await expect(loginAction(undefined, fd('alice', 'secret123'))).rejects.toThrow('REDIRECT:/m/products/p1/solo') }) it('phone-UA zonder actief product → /m/settings', async () => { verifyUserMock.mockResolvedValue({ id: 'u1', is_demo: false, active_product_id: null }) headerGetMock.mockReturnValue(IPHONE_UA) await expect(loginAction(undefined, fd('alice', 'secret123'))).rejects.toThrow('REDIRECT:/m/settings') }) it('tablet-UA (iPad) → /dashboard', async () => { verifyUserMock.mockResolvedValue({ id: 'u1', is_demo: false, active_product_id: 'p1' }) headerGetMock.mockReturnValue(IPAD_UA) await expect(loginAction(undefined, fd('alice', 'secret123'))).rejects.toThrow('REDIRECT:/dashboard') }) it('desktop-UA → /dashboard', async () => { verifyUserMock.mockResolvedValue({ id: 'u1', is_demo: false, active_product_id: 'p1' }) headerGetMock.mockReturnValue(DESKTOP_UA) await expect(loginAction(undefined, fd('alice', 'secret123'))).rejects.toThrow('REDIRECT:/dashboard') }) it('geen UA-header → /dashboard', async () => { verifyUserMock.mockResolvedValue({ id: 'u1', is_demo: false, active_product_id: 'p1' }) headerGetMock.mockReturnValue(null) await expect(loginAction(undefined, fd('alice', 'secret123'))).rejects.toThrow('REDIRECT:/dashboard') }) it('demo-user op phone volgt dezelfde routing', async () => { verifyUserMock.mockResolvedValue({ id: 'demo', is_demo: true, active_product_id: 'p1' }) headerGetMock.mockReturnValue(IPHONE_UA) await expect(loginAction(undefined, fd('demo', 'demo123pw'))).rejects.toThrow('REDIRECT:/m/products/p1/solo') }) }) describe('resetPasswordAction', () => { function fdReset(password: string, confirm: string) { const f = new FormData() f.set('password', password) f.set('confirm', confirm) return f } it('redirect /dashboard na succesvolle reset', async () => { requireSessionMock.mockResolvedValue({ userId: 'u1' }) prismaUserUpdateMock.mockResolvedValue({}) await expect(resetPasswordAction(undefined, fdReset('nieuwpass1', 'nieuwpass1'))).rejects.toThrow('REDIRECT:/dashboard') expect(prismaUserUpdateMock).toHaveBeenCalledWith( expect.objectContaining({ where: { id: 'u1' }, data: expect.objectContaining({ password_hash: 'hashed', must_reset_password: false }), }) ) }) it('fout als wachtwoorden niet overeenkomen', async () => { requireSessionMock.mockResolvedValue({ userId: 'u1' }) const result = await resetPasswordAction(undefined, fdReset('nieuwpass1', 'anderpass1')) expect(result).toMatchObject({ error: expect.objectContaining({ confirm: expect.any(Array) }) }) expect(prismaUserUpdateMock).not.toHaveBeenCalled() }) it('fout als wachtwoord te kort is', async () => { requireSessionMock.mockResolvedValue({ userId: 'u1' }) const result = await resetPasswordAction(undefined, fdReset('kort', 'kort')) expect(result).toMatchObject({ error: expect.objectContaining({ password: expect.any(Array) }) }) }) })