Files
ecommerce-platform/test/unit/lib/auth.test.ts
T
davide 5428eeccc1 test: add unit tests for lib/ (validate, auth, storage, email, rate-limit)
49 tests for all 10 Zod schemas in validate.ts, 26 tests for auth (hashPassword,
verifyPassword, createSession, getSession, getCurrentUser, deleteSession), 11 for
storage (magic-byte validation, saveImage, deleteImageFile), 9 for email (sendMail
scenarios), and 6 for rate limiting logic.
2026-05-19 14:07:38 +02:00

223 lines
8.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import '../../../test/__mocks__/prisma'
import {
hashToken,
hashPassword,
verifyPassword,
validatePasswordStrength,
createSession,
getSessionToken,
getSession,
getCurrentUser,
deleteSession,
deleteAllUserSessions,
} from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { mockUser, mockSession } from '../../fixtures/users'
import { cookies } from 'next/headers'
// ─── hashToken ───────────────────────────────────────────────────────────────
describe('hashToken', () => {
it('returns a 64-char hex string', () => {
const hash = hashToken('sometoken')
expect(hash).toMatch(/^[a-f0-9]{64}$/)
})
it('is deterministic for the same input', () => {
expect(hashToken('abc')).toBe(hashToken('abc'))
})
it('produces different hashes for different inputs', () => {
expect(hashToken('abc')).not.toBe(hashToken('xyz'))
})
})
// ─── hashPassword / verifyPassword ───────────────────────────────────────────
describe('hashPassword / verifyPassword', () => {
it('hashes a password and verifies it correctly', async () => {
const hash = await hashPassword('MyPassword1!')
expect(await verifyPassword('MyPassword1!', hash)).toBe(true)
})
it('returns false for wrong password', async () => {
const hash = await hashPassword('MyPassword1!')
expect(await verifyPassword('WrongPassword1!', hash)).toBe(false)
})
it('produces different hashes for the same password (bcrypt salt)', async () => {
const h1 = await hashPassword('SamePass1!')
const h2 = await hashPassword('SamePass1!')
expect(h1).not.toBe(h2)
})
})
// ─── validatePasswordStrength ─────────────────────────────────────────────────
describe('validatePasswordStrength', () => {
it('returns null for a strong password', () => {
expect(validatePasswordStrength('StrongPass1!')).toBeNull()
})
it('rejects password shorter than 12 chars', () => {
expect(validatePasswordStrength('Short1!')).toMatch(/12 characters/)
})
it('rejects password without uppercase', () => {
expect(validatePasswordStrength('nouppercase1!')).toMatch(/uppercase/)
})
it('rejects password without lowercase', () => {
expect(validatePasswordStrength('NOLOWERCASE1!')).toMatch(/lowercase/)
})
it('rejects password without number', () => {
expect(validatePasswordStrength('NoNumberHere!')).toMatch(/number/)
})
it('rejects password without symbol', () => {
expect(validatePasswordStrength('NoSymbolHere12')).toMatch(/symbol/)
})
it('checks length first (shortest error message path)', () => {
expect(validatePasswordStrength('short')).toMatch(/12 characters/)
})
})
// ─── createSession ────────────────────────────────────────────────────────────
describe('createSession', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(prisma.session.create).mockResolvedValue(mockSession)
})
it('creates a session in DB and returns a token string', async () => {
const token = await createSession(mockUser.id)
expect(typeof token).toBe('string')
expect(token.length).toBeGreaterThan(0)
})
it('calls prisma.session.create with userId and tokenHash', async () => {
const token = await createSession(mockUser.id)
expect(prisma.session.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ userId: mockUser.id }),
})
)
const callArg = vi.mocked(prisma.session.create).mock.calls[0][0]
const tokenHash = hashToken(token)
expect(callArg.data.tokenHash).toBe(tokenHash)
})
it('sets expiry ~30 days from now', async () => {
await createSession(mockUser.id)
const callArg = vi.mocked(prisma.session.create).mock.calls[0][0]
const expiresAt = callArg.data.expiresAt as Date
const diffDays = (expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
expect(diffDays).toBeCloseTo(30, 0)
})
})
// ─── getSessionToken ──────────────────────────────────────────────────────────
describe('getSessionToken', () => {
it('returns token from cookie', async () => {
const mockCookieStore = { get: vi.fn().mockReturnValue({ value: 'mytoken' }), set: vi.fn(), delete: vi.fn() }
vi.mocked(cookies).mockReturnValue(mockCookieStore as any)
expect(await getSessionToken()).toBe('mytoken')
})
it('returns null when cookie is missing', async () => {
const mockCookieStore = { get: vi.fn().mockReturnValue(undefined), set: vi.fn(), delete: vi.fn() }
vi.mocked(cookies).mockReturnValue(mockCookieStore as any)
expect(await getSessionToken()).toBeNull()
})
})
// ─── getSession ───────────────────────────────────────────────────────────────
describe('getSession', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns null when no token cookie', async () => {
const mockCookieStore = { get: vi.fn().mockReturnValue(undefined), set: vi.fn(), delete: vi.fn() }
vi.mocked(cookies).mockReturnValue(mockCookieStore as any)
expect(await getSession()).toBeNull()
})
it('returns session when token is valid and not expired', async () => {
const mockCookieStore = { get: vi.fn().mockReturnValue({ value: 'validtoken' }), set: vi.fn(), delete: vi.fn() }
vi.mocked(cookies).mockReturnValue(mockCookieStore as any)
vi.mocked(prisma.session.findUnique).mockResolvedValue(mockSession as any)
const session = await getSession()
expect(session).not.toBeNull()
expect(session?.user.id).toBe(mockUser.id)
})
it('returns null and deletes session when expired', async () => {
const mockCookieStore = { get: vi.fn().mockReturnValue({ value: 'expiredtoken' }), set: vi.fn(), delete: vi.fn() }
vi.mocked(cookies).mockReturnValue(mockCookieStore as any)
const expiredSession = { ...mockSession, expiresAt: new Date(Date.now() - 1000) }
vi.mocked(prisma.session.findUnique).mockResolvedValue(expiredSession as any)
vi.mocked(prisma.session.delete).mockResolvedValue(expiredSession as any)
const result = await getSession()
expect(result).toBeNull()
expect(prisma.session.delete).toHaveBeenCalledWith({ where: { id: expiredSession.id } })
})
it('returns null when session not found in DB', async () => {
const mockCookieStore = { get: vi.fn().mockReturnValue({ value: 'unknowntoken' }), set: vi.fn(), delete: vi.fn() }
vi.mocked(cookies).mockReturnValue(mockCookieStore as any)
vi.mocked(prisma.session.findUnique).mockResolvedValue(null)
expect(await getSession()).toBeNull()
})
})
// ─── getCurrentUser ───────────────────────────────────────────────────────────
describe('getCurrentUser', () => {
it('returns user from valid session', async () => {
const mockCookieStore = { get: vi.fn().mockReturnValue({ value: 'validtoken' }), set: vi.fn(), delete: vi.fn() }
vi.mocked(cookies).mockReturnValue(mockCookieStore as any)
vi.mocked(prisma.session.findUnique).mockResolvedValue(mockSession as any)
const user = await getCurrentUser()
expect(user?.id).toBe(mockUser.id)
})
it('returns null when no session', async () => {
const mockCookieStore = { get: vi.fn().mockReturnValue(undefined), set: vi.fn(), delete: vi.fn() }
vi.mocked(cookies).mockReturnValue(mockCookieStore as any)
expect(await getCurrentUser()).toBeNull()
})
})
// ─── deleteSession / deleteAllUserSessions ────────────────────────────────────
describe('deleteSession', () => {
it('calls prisma.session.deleteMany with the correct tokenHash', async () => {
vi.mocked(prisma.session.deleteMany).mockResolvedValue({ count: 1 })
await deleteSession('mytoken')
expect(prisma.session.deleteMany).toHaveBeenCalledWith({
where: { tokenHash: hashToken('mytoken') },
})
})
})
describe('deleteAllUserSessions', () => {
it('calls prisma.session.deleteMany with the userId', async () => {
vi.mocked(prisma.session.deleteMany).mockResolvedValue({ count: 3 })
await deleteAllUserSessions('user-123')
expect(prisma.session.deleteMany).toHaveBeenCalledWith({ where: { userId: 'user-123' } })
})
})