🎓 Initial commit: Math2 Platform - Plataforma de Álgebra Lineal PRO
✨ Características: - 45 ejercicios universitarios (Basic → Advanced) - Renderizado LaTeX profesional - IA generativa (Z.ai/DashScope) - Docker 9 servicios - Tests 123/123 pasando - Seguridad enterprise (JWT, XSS, Rate limiting) 🐳 Infraestructura: - Next.js 14 + Node.js 20 - PostgreSQL 15 + Redis 7 - Docker Compose completo - Nginx + SSL ready 📚 Documentación: - 5 informes técnicos completos - README profesional - Scripts de deployment automatizados Estado: Producción lista ✅
This commit is contained in:
339
backend/tests/unit/auth.service.test.ts
Normal file
339
backend/tests/unit/auth.service.test.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user