- requireAdmin() checkt nu de database i.p.v. session.isAdmin (was altijd undefined)
- loginAction stelt session.isAdmin in op basis van UserRole in de DB
- registerAction stelt session.isAdmin = false expliciet in
- NavBar toont 'Admin'-link conditioneel als roles.includes('ADMIN')
- UserMenu ROLE_LABELS uitgebreid met ADMIN → 'Admin'
- Tests aangepast: prismaUserRole.findFirst mock toegevoegd
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
5.4 KiB
TypeScript
141 lines
5.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const {
|
|
redirectMock,
|
|
verifyUserMock,
|
|
headerGetMock,
|
|
sessionSaveMock,
|
|
requireSessionMock,
|
|
prismaUserUpdateMock,
|
|
prismaUserRoleFindFirstMock,
|
|
} = 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(),
|
|
prismaUserRoleFindFirstMock: vi.fn().mockResolvedValue(null),
|
|
}))
|
|
|
|
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 },
|
|
userRole: { findFirst: prismaUserRoleFindFirstMock },
|
|
},
|
|
}))
|
|
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()
|
|
prismaUserRoleFindFirstMock.mockResolvedValue(null)
|
|
})
|
|
|
|
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) }) })
|
|
})
|
|
})
|