🎓 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,265 @@
/**
* Integration Tests - Auth API
*
* Tests for:
* - User registration
* - User login
* - JWT token validation
* - Protected routes
* - Password strength validation
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import request from 'supertest';
import express, { ErrorRequestHandler } from 'express';
import { prisma } from '../../src/shared/database/prisma.client';
import { authRoutes } from '../../src/modules/auth/auth.routes';
// Simple error handler for tests
const testErrorHandler: ErrorRequestHandler = (err, req, res, _next) => {
const statusCode = err.statusCode || err.status || 500;
const code = err.code || 'INTERNAL_ERROR';
const message = err.message || 'An unexpected error occurred';
res.status(statusCode).json({
success: false,
error: {
code,
message,
},
meta: {
timestamp: new Date().toISOString(),
},
});
};
describe('Auth API Integration', () => {
let app: express.Application;
beforeAll(async () => {
// Ensure test database is clean
await prisma.$connect();
});
afterAll(async () => {
await prisma.$disconnect();
});
beforeEach(async () => {
// Clean up test data before each test
await prisma.refreshToken.deleteMany({});
await prisma.passwordResetToken.deleteMany({});
await prisma.exerciseAttempt.deleteMany({});
await prisma.progress.deleteMany({});
await prisma.user.deleteMany({
where: {
email: {
contains: 'test@',
},
},
});
// Create fresh app instance
app = express();
app.use(express.json());
app.use('/api/auth', authRoutes);
// Add error handler last
app.use(testErrorHandler);
});
// ============================================
// REGISTRATION TESTS
// ============================================
describe('POST /api/auth/register', () => {
it('should register a new user successfully', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'SecurePass123!',
username: 'testuser',
});
expect(response.status).toBe(201);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('token');
expect(response.body.data.user).toHaveProperty('id');
expect(response.body.data.user.email).toBe('test@example.com');
expect(response.body.data.user).not.toHaveProperty('passwordHash');
});
it('should reject weak passwords', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test2@example.com',
password: '123',
username: 'testuser2',
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
it('should reject duplicate emails', async () => {
// Create first user
await request(app)
.post('/api/auth/register')
.send({
email: 'duplicate@example.com',
password: 'SecurePass123!',
username: 'user1',
});
// Try to create second user with same email
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'duplicate@example.com',
password: 'SecurePass123!',
username: 'user2',
});
expect(response.status).toBe(409);
expect(response.body.success).toBe(false);
});
it('should reject duplicate usernames', async () => {
// Create first user
await request(app)
.post('/api/auth/register')
.send({
email: 'user1@example.com',
password: 'SecurePass123!',
username: 'uniqueuser',
});
// Try to create second user with same username
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'user2@example.com',
password: 'SecurePass123!',
username: 'uniqueuser',
});
expect(response.status).toBe(409);
expect(response.body.success).toBe(false);
});
});
// ============================================
// LOGIN TESTS
// ============================================
describe('POST /api/auth/login', () => {
beforeEach(async () => {
// Register a user for login tests
await request(app)
.post('/api/auth/register')
.send({
email: 'login@example.com',
password: 'SecurePass123!',
username: 'loginuser',
});
});
it('should login with correct credentials', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'login@example.com',
password: 'SecurePass123!',
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('token');
expect(response.body.data.user.email).toBe('login@example.com');
});
it('should reject incorrect password', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'login@example.com',
password: 'WrongPass123!',
});
expect(response.status).toBe(401);
expect(response.body.success).toBe(false);
});
it('should reject non-existent user', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'SecurePass123!',
});
expect(response.status).toBe(401);
expect(response.body.success).toBe(false);
});
});
// ============================================
// PROFILE TESTS
// ============================================
describe('GET /api/auth/profile', () => {
let authToken: string;
let userId: string;
beforeEach(async () => {
// Clean up any existing profile test user
await prisma.refreshToken.deleteMany({});
await prisma.exerciseAttempt.deleteMany({});
await prisma.progress.deleteMany({});
await prisma.user.deleteMany({
where: {
email: 'profile@example.com',
},
});
const registerResponse = await request(app)
.post('/api/auth/register')
.send({
email: 'profile@example.com',
password: 'SecurePass123!',
username: 'profileuser',
});
authToken = registerResponse.body.data.token;
userId = registerResponse.body.data.user.id;
});
it('should get user profile with valid token', async () => {
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.id).toBe(userId);
expect(response.body.data.email).toBe('profile@example.com');
});
it('should reject request without token', async () => {
const response = await request(app)
.get('/api/auth/me');
expect(response.status).toBe(401);
});
it('should reject request with invalid token', async () => {
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', 'Bearer invalid-token');
expect(response.status).toBe(401);
});
});
});

View File

@@ -0,0 +1,316 @@
/**
* Integration Tests - Exercise API
*
* Tests for:
* - Fetching exercises
* - Submitting answers
* - Progress tracking
* - Concurrent submission handling
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import request from 'supertest';
import express, { ErrorRequestHandler } from 'express';
import { prisma } from '../../src/shared/database/prisma.client';
import exerciseRoutes from '../../src/modules/exercise/exercise.routes';
import { authRoutes } from '../../src/modules/auth/auth.routes';
import { randomUUID } from 'crypto';
// Simple error handler for tests
const testErrorHandler: ErrorRequestHandler = (err, req, res, _next) => {
const statusCode = err.statusCode || err.status || 500;
const code = err.code || 'INTERNAL_ERROR';
const message = err.message || 'An unexpected error occurred';
res.status(statusCode).json({
success: false,
error: {
code,
message,
},
meta: {
timestamp: new Date().toISOString(),
},
});
};
describe('Exercise API Integration', () => {
let app: express.Application;
let authToken: string;
let userId: string;
let moduleId: string;
let exerciseId: string;
beforeAll(async () => {
await prisma.$connect();
});
afterAll(async () => {
await prisma.$disconnect();
});
beforeEach(async () => {
// Clean up test data in correct order (respecting FK constraints)
await prisma.exerciseAttempt.deleteMany({});
await prisma.progress.deleteMany({});
await prisma.exercise.deleteMany({
where: {
statement: {
contains: 'Test Exercise',
},
},
});
// Delete test modules to avoid unique constraint violations
await prisma.modules.deleteMany({
where: {
name: {
contains: 'Test Module',
},
},
});
await prisma.refreshToken.deleteMany({});
await prisma.user.deleteMany({
where: {
email: {
contains: 'test@',
},
},
});
// Create fresh app
app = express();
app.use(express.json());
app.use('/api/auth', authRoutes);
app.use('/api/exercises', exerciseRoutes);
// Add error handler last
app.use(testErrorHandler);
// Create test user
const registerResponse = await request(app)
.post('/api/auth/register')
.send({
email: `test-${randomUUID()}@example.com`,
password: 'SecurePass123!',
username: `testuser-${randomUUID()}`,
});
authToken = registerResponse.body.data.token;
userId = registerResponse.body.data.user.id;
// Create test module with unique order (random to avoid collisions)
const testModule = await prisma.modules.create({
data: {
id: randomUUID(),
name: 'Test Module',
description: 'Test module for exercises',
order: Math.floor(Math.random() * 1000000), // Random unique order
type: 'FUNDAMENTOS',
updatedAt: new Date(),
},
});
moduleId = testModule.id;
// Create test exercise
const exercise = await prisma.exercise.create({
data: {
moduleId: moduleId,
topicId: null,
type: 'CALCULATION',
difficulty: 'BASIC',
order: 1,
statement: 'Test Exercise: What is 2+2?',
correctAnswer: '4',
points: 10,
timeLimitSeconds: 120,
hints: [],
solutionSteps: [],
updatedAt: new Date(),
},
});
exerciseId = exercise.id;
});
// ============================================
// GET EXERCISE TESTS
// ============================================
describe('GET /api/exercises/:id', () => {
it('should get exercise details without exposing correct answer', async () => {
const response = await request(app)
.get(`/api/exercises/${exerciseId}`)
.set('Authorization', `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.id).toBe(exerciseId);
expect(response.body.data.statement).toBe('Test Exercise: What is 2+2?');
expect(response.body.data.correctAnswer).toBe('');
});
it('should return 404 for non-existent exercise', async () => {
const response = await request(app)
.get('/api/exercises/non-existent-id')
.set('Authorization', `Bearer ${authToken}`);
expect(response.status).toBe(404);
});
});
// ============================================
// SUBMIT ATTEMPT TESTS
// ============================================
describe('POST /api/exercises/:id/attempt', () => {
it('should submit correct answer successfully', async () => {
const response = await request(app)
.post(`/api/exercises/${exerciseId}/attempt`)
.set('Authorization', `Bearer ${authToken}`)
.send({
answer: '4',
timeSpent: 30,
hintsUsed: 0,
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.isCorrect).toBe(true);
expect(response.body.data.points).toBeGreaterThan(0);
});
it('should handle incorrect answer', async () => {
const response = await request(app)
.post(`/api/exercises/${exerciseId}/attempt`)
.set('Authorization', `Bearer ${authToken}`)
.send({
answer: '5',
timeSpent: 30,
hintsUsed: 0,
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.isCorrect).toBe(false);
expect(response.body.data.points).toBe(0);
});
it('should validate LaTeX answer format', async () => {
// Create exercise with LaTeX answer
const latexExercise = await prisma.exercise.create({
data: {
moduleId: moduleId,
type: 'CALCULATION',
difficulty: 'BASIC',
order: 2,
statement: 'Test LaTeX: What is the fraction?',
correctAnswer: '\\frac{1}{2}',
points: 15,
timeLimitSeconds: 120,
},
});
const response = await request(app)
.post(`/api/exercises/${latexExercise.id}/attempt`)
.set('Authorization', `Bearer ${authToken}`)
.send({
answer: '\\frac{1}{2}',
timeSpent: 30,
hintsUsed: 0,
});
expect(response.status).toBe(200);
expect(response.body.data.isCorrect).toBe(true);
});
it('should detect and reject XSS attempts in LaTeX', async () => {
const response = await request(app)
.post(`/api/exercises/${exerciseId}/attempt`)
.set('Authorization', `Bearer ${authToken}`)
.send({
answer: '\\href{javascript:alert(1)}{x}',
timeSpent: 30,
hintsUsed: 0,
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
it('should handle skipped exercises', async () => {
const response = await request(app)
.post(`/api/exercises/${exerciseId}/attempt`)
.set('Authorization', `Bearer ${authToken}`)
.send({
answer: '',
timeSpent: 0,
skipped: true,
});
expect(response.status).toBe(200);
expect(response.body.data.isCorrect).toBe(false);
expect(response.body.data.skipped).toBe(true);
});
});
// ============================================
// CONCURRENT SUBMISSION TESTS
// ============================================
describe('Concurrent submission handling', () => {
it('should handle multiple rapid submissions gracefully', async () => {
const submissions = Array(5).fill(null).map(() =>
request(app)
.post(`/api/exercises/${exerciseId}/attempt`)
.set('Authorization', `Bearer ${authToken}`)
.send({
answer: '4',
timeSpent: 30,
hintsUsed: 0,
})
);
const results = await Promise.all(submissions);
// All requests should complete without server errors
results.forEach(result => {
expect(result.status).toBeLessThan(500);
});
});
});
// ============================================
// PROGRESS TESTS
// ============================================
describe('GET /api/exercises/:id/attempts', () => {
it('should get user attempts for exercise', async () => {
// Submit a few attempts first
await request(app)
.post(`/api/exercises/${exerciseId}/attempt`)
.set('Authorization', `Bearer ${authToken}`)
.send({
answer: '5',
timeSpent: 30,
hintsUsed: 0,
});
await request(app)
.post(`/api/exercises/${exerciseId}/attempt`)
.set('Authorization', `Bearer ${authToken}`)
.send({
answer: '4',
timeSpent: 30,
hintsUsed: 0,
});
const response = await request(app)
.get(`/api/exercises/${exerciseId}/attempts`)
.set('Authorization', `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.length).toBeGreaterThanOrEqual(2);
expect(response.body.meta.hasCompleted).toBe(true);
});
});
});