🎓 Initial commit: Math2 Platform - Plataforma de Álgebra Lineal PRO
Some checks failed
Test Suite / test-backend (push) Has been cancelled
Test Suite / test-frontend (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / coverage-check (push) Has been cancelled

 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:
Renato
2026-03-31 11:27:11 -03:00
commit bc43c9e772
309 changed files with 84845 additions and 0 deletions

View 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);
});
});
});