/** * Unit tests for authentication service * * This file demonstrates testing patterns for service functions in the API. * See docs/TEST_EXAMPLES.md for more examples. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import bcrypt from 'bcryptjs' import jwt from 'jsonwebtoken' import { login } from './auth' import { getDb } from '../db' import { AppErrors } from '../lib/errors' // Mock dependencies vi.mock('../db') vi.mock('../lib/errors') vi.mock('bcryptjs') vi.mock('jsonwebtoken') vi.mock('../lib/secret-validation', () => ({ requireJWTSecret: () => 'test-secret' })) describe('auth service', () => { let mockDb: any beforeEach(() => { vi.clearAllMocks() mockDb = { query: vi.fn() } vi.mocked(getDb).mockReturnValue(mockDb as any) }) describe('login', () => { it('should authenticate valid user and return token', async () => { // Arrange const mockUser = { id: '1', email: 'user@example.com', name: 'Test User', password_hash: '$2a$10$hashed', role: 'USER', created_at: new Date('2024-01-01'), updated_at: new Date('2024-01-01'), } mockDb.query.mockResolvedValue({ rows: [mockUser] }) vi.mocked(bcrypt.compare).mockResolvedValue(true as never) vi.mocked(jwt.sign).mockReturnValue('mock-jwt-token' as any) // Act const result = await login('user@example.com', 'password123') // Assert expect(result).toHaveProperty('token') expect(result.token).toBe('mock-jwt-token') expect(result.user.email).toBe('user@example.com') expect(result.user.name).toBe('Test User') expect(result.user.role).toBe('USER') expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('SELECT'), ['user@example.com'] ) expect(bcrypt.compare).toHaveBeenCalledWith('password123', mockUser.password_hash) expect(jwt.sign).toHaveBeenCalled() }) it('should throw error for invalid email', async () => { // Arrange mockDb.query.mockResolvedValue({ rows: [] }) // Act & Assert await expect(login('invalid@example.com', 'password123')).rejects.toThrow() expect(bcrypt.compare).not.toHaveBeenCalled() expect(jwt.sign).not.toHaveBeenCalled() }) it('should throw error for invalid password', async () => { // Arrange const mockUser = { id: '1', email: 'user@example.com', name: 'Test User', password_hash: '$2a$10$hashed', role: 'USER', created_at: new Date('2024-01-01'), updated_at: new Date('2024-01-01'), } mockDb.query.mockResolvedValue({ rows: [mockUser] }) vi.mocked(bcrypt.compare).mockResolvedValue(false as never) // Act & Assert await expect(login('user@example.com', 'wrongpassword')).rejects.toThrow() expect(bcrypt.compare).toHaveBeenCalledWith('wrongpassword', mockUser.password_hash) expect(jwt.sign).not.toHaveBeenCalled() }) it('should include user role in JWT token', async () => { // Arrange const mockUser = { id: '1', email: 'admin@example.com', name: 'Admin User', password_hash: '$2a$10$hashed', role: 'ADMIN', created_at: new Date('2024-01-01'), updated_at: new Date('2024-01-01'), } mockDb.query.mockResolvedValue({ rows: [mockUser] }) vi.mocked(bcrypt.compare).mockResolvedValue(true as never) vi.mocked(jwt.sign).mockReturnValue('mock-jwt-token' as any) // Act await login('admin@example.com', 'password123') // Assert expect(jwt.sign).toHaveBeenCalledWith( expect.objectContaining({ id: '1', email: 'admin@example.com', name: 'Admin User', role: 'ADMIN', }), 'test-secret', expect.objectContaining({ expiresIn: expect.any(String) }) ) }) }) })