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' } }) }) })