import { describe, it, expect, vi, beforeEach } from 'vitest' import '../../../__mocks__/prisma' import { NextRequest } from 'next/server' import { POST } from '@/app/api/auth/login/route' import { prisma } from '@/lib/prisma' import { mockUser } from '../../../fixtures/users' vi.mock('@/lib/auth', async (importOriginal) => { const actual = await importOriginal() return { ...actual, verifyPassword: vi.fn(), createSession: vi.fn().mockResolvedValue('mock-session-token'), setSessionCookie: vi.fn(), } }) import { verifyPassword, createSession, setSessionCookie } from '@/lib/auth' function makeRequest(body: unknown, ip = '1.2.3.4') { return new NextRequest( new Request('http://localhost/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-forwarded-for': ip }, body: JSON.stringify(body), }) ) } beforeEach(() => { vi.clearAllMocks() vi.mocked(prisma.loginAttempt.count).mockResolvedValue(0) vi.mocked(prisma.loginAttempt.create).mockResolvedValue({ id: '1', key: '1.2.3.4', createdAt: new Date() }) vi.mocked(prisma.loginAttempt.deleteMany).mockResolvedValue({ count: 0 }) }) describe('POST /api/auth/login', () => { it('returns 429 when rate limit exceeded', async () => { vi.mocked(prisma.loginAttempt.count).mockResolvedValue(10) const res = await POST(makeRequest({ email: 'user@example.com', password: 'pass' })) expect(res.status).toBe(429) const data = await res.json() expect(data.error).toMatch(/Too many/) }) it('returns 400 for invalid email format', async () => { const res = await POST(makeRequest({ email: 'not-an-email', password: 'pass' })) expect(res.status).toBe(400) }) it('returns 400 for empty password', async () => { const res = await POST(makeRequest({ email: 'user@example.com', password: '' })) expect(res.status).toBe(400) }) it('returns 400 for malformed JSON body', async () => { const req = new NextRequest( new Request('http://localhost/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-forwarded-for': '1.2.3.4' }, body: 'not-json', }) ) const res = await POST(req) expect(res.status).toBe(400) }) it('returns 401 when user not found', async () => { vi.mocked(prisma.user.findUnique).mockResolvedValue(null) const res = await POST(makeRequest({ email: 'notfound@example.com', password: 'SomePass1!' })) expect(res.status).toBe(401) const data = await res.json() expect(data.error).toBe('Invalid email or password') }) it('records a failed attempt when user not found', async () => { vi.mocked(prisma.user.findUnique).mockResolvedValue(null) await POST(makeRequest({ email: 'notfound@example.com', password: 'SomePass1!' })) expect(prisma.loginAttempt.create).toHaveBeenCalledWith({ data: { key: '1.2.3.4' } }) }) it('returns 401 when password is wrong', async () => { vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser as any) vi.mocked(verifyPassword).mockResolvedValue(false) const res = await POST(makeRequest({ email: mockUser.email, password: 'WrongPass1!' })) expect(res.status).toBe(401) expect((await res.json()).error).toBe('Invalid email or password') }) it('returns 200 with user data on successful login', async () => { vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser as any) vi.mocked(verifyPassword).mockResolvedValue(true) vi.mocked(prisma.session.create).mockResolvedValue({} as any) const res = await POST(makeRequest({ email: mockUser.email, password: 'ValidPass1!' })) expect(res.status).toBe(200) const data = await res.json() expect(data.user.email).toBe(mockUser.email) expect(data.user.role).toBe(mockUser.role) expect(data.user).not.toHaveProperty('passwordHash') }) it('creates a session and sets cookie on successful login', async () => { vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser as any) vi.mocked(verifyPassword).mockResolvedValue(true) vi.mocked(prisma.session.create).mockResolvedValue({} as any) await POST(makeRequest({ email: mockUser.email, password: 'ValidPass1!' })) expect(createSession).toHaveBeenCalledWith(mockUser.id) expect(setSessionCookie).toHaveBeenCalledWith('mock-session-token') }) })