/** * Auth Service Unit Tests * * Tests for: * - Login with correct credentials * - Login with incorrect password * - Register with duplicate email * - Token generation and validation */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; // Mock external dependencies FIRST before importing anything else vi.mock('bcrypt', () => ({ default: { hash: vi.fn(), compare: vi.fn(), }, })); vi.mock('jsonwebtoken', () => { const mockSign = vi.fn(); const mockVerify = vi.fn(); return { default: { sign: mockSign, verify: mockVerify, TokenExpiredError: class TokenExpiredError extends Error { constructor(message: string) { super(message); this.name = 'TokenExpiredError'; } }, JsonWebTokenError: class JsonWebTokenError extends Error { constructor(message: string) { super(message); this.name = 'JsonWebTokenError'; } }, }, }; }); // Mock internal dependencies vi.mock('../../src/shared/database/prisma.client', () => ({ prisma: { user: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn(), }, refreshToken: { create: vi.fn(), deleteMany: vi.fn(), findUnique: vi.fn(), delete: vi.fn(), }, passwordResetToken: { create: vi.fn(), findUnique: vi.fn(), delete: vi.fn(), }, }, })); vi.mock('../../src/shared/database/redis.client', () => ({ blacklistToken: vi.fn().mockResolvedValue(undefined), isTokenBlacklisted: vi.fn().mockResolvedValue(false), })); vi.mock('../../src/shared/utils/logger', () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); vi.mock('../../src/modules/notification/telegram/telegram.client', () => ({ telegramClient: { sendMessage: vi.fn(), }, })); vi.mock('../../src/config/telegram', () => ({ isTelegramEnabled: vi.fn().mockReturnValue(false), getTelegramAdminChatId: vi.fn().mockReturnValue(null), })); // NOW import the mocked modules and the service import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { AuthService } from '../../src/modules/auth/auth.service'; import { prisma } from '../../src/shared/database/prisma.client'; import { ConflictError, AuthenticationError, NotFoundError } from '../../src/shared/types'; const authService = new AuthService(); describe('AuthService', () => { beforeEach(() => { vi.clearAllMocks(); process.env.JWT_SECRET = 'test-jwt-secret'; }); afterEach(() => { vi.restoreAllMocks(); }); // ============================================ // LOGIN TESTS // ============================================ describe('login', () => { it('should login successfully with correct credentials', async () => { const mockUser = { id: 'user-123', email: 'test@example.com', username: 'testuser', passwordHash: 'hashed-password', isActive: true, lastLoginAt: null, role: 'STUDENT', }; (prisma.user.findUnique as any).mockResolvedValueOnce(mockUser); (prisma.user.update as any).mockResolvedValueOnce({ ...mockUser, lastLoginAt: new Date(), }); (prisma.refreshToken.deleteMany as any).mockResolvedValueOnce({ count: 0 }); (prisma.refreshToken.create as any).mockResolvedValueOnce({}); (bcrypt.compare as any).mockResolvedValueOnce(true); (jwt.sign as any).mockReturnValueOnce('mock-jwt-token'); const result = await authService.login({ email: 'test@example.com', password: 'correct-password', }); expect(result.user.email).toBe('test@example.com'); expect(result.user.username).toBe('testuser'); expect(result.token).toBe('mock-jwt-token'); }); it('should throw AuthenticationError with incorrect password', async () => { const mockUser = { id: 'user-123', email: 'test@example.com', username: 'testuser', passwordHash: 'hashed-password', isActive: true, role: 'STUDENT', }; (prisma.user.findUnique as any).mockResolvedValueOnce(mockUser); (bcrypt.compare as any).mockResolvedValueOnce(false); await expect( authService.login({ email: 'test@example.com', password: 'wrong-password', }) ).rejects.toThrow(AuthenticationError); expect(prisma.user.update).not.toHaveBeenCalled(); }); it('should throw AuthenticationError when user does not exist', async () => { (prisma.user.findUnique as any).mockResolvedValueOnce(null); await expect( authService.login({ email: 'nonexistent@example.com', password: 'any-password', }) ).rejects.toThrow(AuthenticationError); }); it('should throw AuthenticationError when user is inactive', async () => { const mockUser = { id: 'user-123', email: 'test@example.com', username: 'testuser', passwordHash: 'hashed-password', isActive: false, role: 'STUDENT', }; (prisma.user.findUnique as any).mockResolvedValueOnce(mockUser); await expect( authService.login({ email: 'test@example.com', password: 'any-password', }) ).rejects.toThrow(AuthenticationError); expect(bcrypt.compare).not.toHaveBeenCalled(); }); }); // ============================================ // REGISTER TESTS // ============================================ describe('register', () => { it('should register a new user successfully', async () => { (prisma.user.findUnique as any).mockResolvedValueOnce(null); // email check (prisma.user.findUnique as any).mockResolvedValueOnce(null); // username check (bcrypt.hash as any).mockResolvedValueOnce('hashed-password'); const mockCreatedUser = { id: 'new-user-123', email: 'newuser@example.com', username: 'newuser', isActive: true, createdAt: new Date(), role: 'STUDENT', }; (prisma.user.create as any).mockResolvedValueOnce(mockCreatedUser); (prisma.refreshToken.create as any).mockResolvedValueOnce({}); (jwt.sign as any).mockReturnValueOnce('mock-jwt-token'); const result = await authService.register({ email: 'newuser@example.com', username: 'newuser', password: 'secure-password', }); expect(result.user.email).toBe('newuser@example.com'); expect(result.user.username).toBe('newuser'); expect(result.token).toBe('mock-jwt-token'); expect(bcrypt.hash).toHaveBeenCalledWith('secure-password', 10); }); it('should throw ConflictError with duplicate email', async () => { (prisma.user.findUnique as any).mockResolvedValueOnce({ id: 'existing-user', role: 'STUDENT' }); await expect( authService.register({ email: 'existing@example.com', username: 'newuser', password: 'password', }) ).rejects.toThrow(ConflictError); expect(prisma.user.create).not.toHaveBeenCalled(); }); it('should throw ConflictError with duplicate username', async () => { (prisma.user.findUnique as any).mockResolvedValueOnce(null); // email unique (prisma.user.findUnique as any).mockResolvedValueOnce({ id: 'existing-user', role: 'STUDENT' }); // username exists await expect( authService.register({ email: 'newemail@example.com', username: 'existingusername', password: 'password', }) ).rejects.toThrow(ConflictError); }); }); // ============================================ // TOKEN TESTS // ============================================ describe('verifyToken', () => { it('should verify a valid token successfully', async () => { const mockPayload = { userId: 'user-123', email: 'test@example.com', username: 'testuser', }; (jwt.verify as any).mockReturnValueOnce(mockPayload); const result = authService.verifyToken('valid-token'); expect(result.userId).toBe('user-123'); expect(result.email).toBe('test@example.com'); }); it('should throw AuthenticationError for expired token', async () => { const TokenExpiredError = (jwt as any).TokenExpiredError; (jwt.verify as any).mockImplementationOnce(() => { throw new TokenExpiredError('jwt expired'); }); expect(() => authService.verifyToken('expired-token')).toThrow(AuthenticationError); }); it('should throw AuthenticationError for invalid token', async () => { const JsonWebTokenError = (jwt as any).JsonWebTokenError; (jwt.verify as any).mockImplementationOnce(() => { throw new JsonWebTokenError('invalid signature'); }); expect(() => authService.verifyToken('invalid-token')).toThrow(AuthenticationError); }); }); // ============================================ // PROFILE TESTS // ============================================ describe('getProfile', () => { it('should return user profile successfully', async () => { const mockProfile = { id: 'user-123', email: 'test@example.com', username: 'testuser', isActive: true, telegramChatId: null, createdAt: new Date(), updatedAt: new Date(), lastLoginAt: new Date(), role: 'STUDENT', }; (prisma.user.findUnique as any).mockResolvedValueOnce(mockProfile); const result = await authService.getProfile('user-123'); expect(result.id).toBe('user-123'); expect(result.email).toBe('test@example.com'); expect(result.username).toBe('testuser'); }); it('should throw NotFoundError when user not found', async () => { (prisma.user.findUnique as any).mockResolvedValueOnce(null); await expect(authService.getProfile('nonexistent-user')).rejects.toThrow(NotFoundError); }); }); });