# Plan de Pruebas - MGN-001 Auth ## Identificacion | Campo | Valor | |-------|-------| | **ID** | TP-AUTH-001 | | **Modulo** | MGN-001 | | **Nombre** | Auth - Autenticacion | | **Version** | 1.0 | | **Estado** | En diseƱo | | **Autor** | System | | **Fecha** | 2025-12-05 | --- ## Alcance ### En Alcance - Login con email y password - Generacion y validacion de JWT tokens - Refresh token y renovacion de sesion - Logout individual y global - Recuperacion de password - Rate limiting - Bloqueo de cuentas ### Fuera de Alcance - Autenticacion OAuth/Social - Two-Factor Authentication (2FA) - Single Sign-On (SSO) - Biometricos --- ## Estrategia de Pruebas ### Niveles de Prueba | Nivel | Cobertura | Herramientas | |-------|-----------|--------------| | Unit Tests | 80% | Jest | | Integration Tests | 70% | Jest + Supertest | | E2E Tests | Casos criticos | Playwright/Cypress | | Security Tests | OWASP Top 10 | OWASP ZAP, manual | | Performance Tests | Endpoints criticos | k6, Artillery | ### Ambiente de Pruebas | Ambiente | Proposito | Base de Datos | |----------|-----------|---------------| | Local | Desarrollo | PostgreSQL local | | Test | CI/CD | PostgreSQL Docker | | Staging | UAT | PostgreSQL Cloud | --- ## Casos de Prueba ### TC-AUTH-001: Login Exitoso | Campo | Valor | |-------|-------| | **ID** | TC-AUTH-001 | | **Tipo** | Funcional | | **Prioridad** | Critica | | **RF** | RF-AUTH-001 | #### Precondiciones - Usuario registrado con email "test@example.com" - Password "SecurePass123!" - Usuario activo (is_active = true) - Cuenta no bloqueada #### Pasos | # | Accion | Datos | Resultado Esperado | |---|--------|-------|-------------------| | 1 | POST /api/v1/auth/login | `{"email": "test@example.com", "password": "SecurePass123!"}` | Status 200 | | 2 | Verificar body | - | Contiene accessToken, refreshToken, user | | 3 | Verificar cookies | - | Cookie httpOnly refresh_token establecida | | 4 | Verificar session_history | - | Registro de login creado | #### Codigo de Test ```typescript describe('AuthController - Login', () => { it('should login successfully with valid credentials', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'test@example.com', password: 'SecurePass123!', }) .expect(200); expect(response.body).toHaveProperty('accessToken'); expect(response.body).toHaveProperty('refreshToken'); expect(response.body).toHaveProperty('user'); expect(response.body.user).toHaveProperty('id'); expect(response.body.user.email).toBe('test@example.com'); // Verify cookie expect(response.headers['set-cookie']).toBeDefined(); expect(response.headers['set-cookie'][0]).toContain('refresh_token'); expect(response.headers['set-cookie'][0]).toContain('HttpOnly'); }); }); ``` --- ### TC-AUTH-002: Login con Credenciales Invalidas | Campo | Valor | |-------|-------| | **ID** | TC-AUTH-002 | | **Tipo** | Funcional / Seguridad | | **Prioridad** | Critica | | **RF** | RF-AUTH-001 | #### Escenarios | Escenario | Email | Password | Resultado | |-----------|-------|----------|-----------| | Email no existe | noexiste@test.com | cualquier | 401, "Credenciales invalidas" | | Password incorrecto | test@example.com | wrongpass | 401, "Credenciales invalidas" | | Email vacio | "" | pass | 400, "Email es requerido" | | Password vacio | test@test.com | "" | 400, "Password es requerido" | | Email invalido | "notanemail" | pass | 400, "Email invalido" | | Password muy corto | test@test.com | "abc" | 400, "Password minimo 8 caracteres" | #### Codigo de Test ```typescript describe('AuthController - Login Failures', () => { it('should return 401 for wrong password', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'test@example.com', password: 'wrongpassword123!', }) .expect(401); expect(response.body.message).toBe('Credenciales invalidas'); }); it('should return 401 for non-existent email without revealing', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'noexiste@example.com', password: 'anypassword123!', }) .expect(401); // Mensaje identico para no revelar existencia de email expect(response.body.message).toBe('Credenciales invalidas'); }); it('should return 400 for invalid email format', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'notanemail', password: 'password123!', }) .expect(400); expect(response.body.message).toContain('Email'); }); }); ``` --- ### TC-AUTH-003: Bloqueo de Cuenta por Intentos Fallidos | Campo | Valor | |-------|-------| | **ID** | TC-AUTH-003 | | **Tipo** | Seguridad | | **Prioridad** | Critica | | **RF** | RF-AUTH-001 | #### Pasos | # | Accion | Resultado Esperado | |---|--------|-------------------| | 1 | Login con password incorrecto (1) | 401, failed_attempts = 1 | | 2 | Login con password incorrecto (2) | 401, failed_attempts = 2 | | 3 | Login con password incorrecto (3) | 401, failed_attempts = 3 | | 4 | Login con password incorrecto (4) | 401, failed_attempts = 4 | | 5 | Login con password incorrecto (5) | 401, failed_attempts = 5 | | 6 | Login con password CORRECTO | 423, cuenta bloqueada | | 7 | Esperar 30 minutos | - | | 8 | Login con password correcto | 200, login exitoso | #### Codigo de Test ```typescript describe('AuthController - Account Lockout', () => { it('should lock account after 5 failed attempts', async () => { // 5 intentos fallidos for (let i = 0; i < 5; i++) { await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'locktest@example.com', password: 'wrongpassword', }) .expect(401); } // 6to intento con credenciales correctas const response = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'locktest@example.com', password: 'CorrectPass123!', }) .expect(423); expect(response.body.message).toContain('bloqueada'); }); it('should unlock account after 30 minutes', async () => { // Mock time to simulate 30 minutes passing jest.useFakeTimers(); jest.advanceTimersByTime(30 * 60 * 1000); const response = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'locktest@example.com', password: 'CorrectPass123!', }) .expect(200); jest.useRealTimers(); }); }); ``` --- ### TC-AUTH-004: Generacion de JWT Token | Campo | Valor | |-------|-------| | **ID** | TC-AUTH-004 | | **Tipo** | Funcional | | **Prioridad** | Critica | | **RF** | RF-AUTH-002 | #### Verificaciones | Verificacion | Valor Esperado | |--------------|----------------| | Algoritmo | RS256 | | Issuer (iss) | "erp-core" | | Audience (aud) | "erp-api" | | Access Token exp | 15 minutos | | Refresh Token exp | 7 dias | | Contiene sub | User ID | | Contiene tid | Tenant ID | | Contiene roles | Array de roles | | Contiene jti | Unique ID | #### Codigo de Test ```typescript describe('TokenService', () => { it('should generate valid JWT with correct claims', async () => { const user = { id: 'user-uuid', tenantId: 'tenant-uuid', email: 'test@example.com', roles: [{ name: 'admin' }], }; const tokens = await tokenService.generateTokenPair(user, { ipAddress: '127.0.0.1', userAgent: 'test', }); // Decode access token const decoded = jwt.decode(tokens.accessToken, { complete: true }); expect(decoded.header.alg).toBe('RS256'); expect(decoded.payload.iss).toBe('erp-core'); expect(decoded.payload.aud).toBe('erp-api'); expect(decoded.payload.sub).toBe('user-uuid'); expect(decoded.payload.tid).toBe('tenant-uuid'); expect(decoded.payload.roles).toContain('admin'); expect(decoded.payload.jti).toBeDefined(); // Verify expiration const exp = new Date(decoded.payload.exp * 1000); const now = new Date(); const diffMinutes = (exp.getTime() - now.getTime()) / 1000 / 60; expect(diffMinutes).toBeCloseTo(15, 0); }); it('should verify token signature with public key', async () => { const tokens = await tokenService.generateTokenPair(mockUser, mockMetadata); const verified = jwt.verify(tokens.accessToken, publicKey, { algorithms: ['RS256'], }); expect(verified).toBeDefined(); expect(verified.sub).toBe(mockUser.id); }); }); ``` --- ### TC-AUTH-005: Refresh Token | Campo | Valor | |-------|-------| | **ID** | TC-AUTH-005 | | **Tipo** | Funcional | | **Prioridad** | Critica | | **RF** | RF-AUTH-003 | #### Escenarios | Escenario | Resultado Esperado | |-----------|-------------------| | Refresh con token valido | 200, nuevos tokens | | Refresh con token expirado | 401, "Refresh token expirado" | | Refresh con token revocado | 401, "Token revocado" | | Refresh con token ya usado | 401, "Sesion comprometida" | | Refresh sin token | 400, "Refresh token requerido" | #### Codigo de Test ```typescript describe('AuthController - Refresh Token', () => { it('should refresh tokens successfully', async () => { // Login first const loginResponse = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'test@example.com', password: 'SecurePass123!' }); const refreshToken = loginResponse.body.refreshToken; // Refresh const response = await request(app.getHttpServer()) .post('/api/v1/auth/refresh') .send({ refreshToken }) .expect(200); expect(response.body.accessToken).toBeDefined(); expect(response.body.refreshToken).toBeDefined(); expect(response.body.accessToken).not.toBe(loginResponse.body.accessToken); expect(response.body.refreshToken).not.toBe(refreshToken); }); it('should detect token replay attack', async () => { const loginResponse = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'test@example.com', password: 'SecurePass123!' }); const originalRefreshToken = loginResponse.body.refreshToken; // First refresh (valid) await request(app.getHttpServer()) .post('/api/v1/auth/refresh') .send({ refreshToken: originalRefreshToken }) .expect(200); // Second refresh with same token (replay attack) const response = await request(app.getHttpServer()) .post('/api/v1/auth/refresh') .send({ refreshToken: originalRefreshToken }) .expect(401); expect(response.body.message).toContain('comprometida'); }); }); ``` --- ### TC-AUTH-006: Logout | Campo | Valor | |-------|-------| | **ID** | TC-AUTH-006 | | **Tipo** | Funcional | | **Prioridad** | Alta | | **RF** | RF-AUTH-004 | #### Codigo de Test ```typescript describe('AuthController - Logout', () => { it('should logout successfully', async () => { // Login const loginResponse = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'test@example.com', password: 'SecurePass123!' }); const accessToken = loginResponse.body.accessToken; // Logout const response = await request(app.getHttpServer()) .post('/api/v1/auth/logout') .set('Authorization', `Bearer ${accessToken}`) .expect(200); expect(response.body.message).toContain('cerrada'); // Verify cookie is cleared expect(response.headers['set-cookie'][0]).toContain('Max-Age=0'); // Verify token is blacklisted const protectedResponse = await request(app.getHttpServer()) .get('/api/v1/protected-resource') .set('Authorization', `Bearer ${accessToken}`) .expect(401); }); it('should logout all sessions', async () => { // Create multiple sessions const session1 = await login('test@example.com', 'SecurePass123!'); const session2 = await login('test@example.com', 'SecurePass123!'); const session3 = await login('test@example.com', 'SecurePass123!'); // Logout all const response = await request(app.getHttpServer()) .post('/api/v1/auth/logout-all') .set('Authorization', `Bearer ${session1.accessToken}`) .expect(200); expect(response.body.sessionsRevoked).toBe(3); // All sessions should be invalid for (const session of [session1, session2, session3]) { await request(app.getHttpServer()) .get('/api/v1/protected-resource') .set('Authorization', `Bearer ${session.accessToken}`) .expect(401); } }); }); ``` --- ### TC-AUTH-007: Password Recovery | Campo | Valor | |-------|-------| | **ID** | TC-AUTH-007 | | **Tipo** | Funcional | | **Prioridad** | Alta | | **RF** | RF-AUTH-005 | #### Codigo de Test ```typescript describe('AuthController - Password Recovery', () => { it('should send recovery email for existing user', async () => { const emailSpy = jest.spyOn(emailService, 'sendPasswordResetEmail'); const response = await request(app.getHttpServer()) .post('/api/v1/auth/password/request-reset') .send({ email: 'test@example.com' }) .expect(200); expect(response.body.message).toContain('Si el email esta registrado'); expect(emailSpy).toHaveBeenCalledWith( 'test@example.com', expect.any(String), expect.any(String), ); }); it('should return same message for non-existent email', async () => { const emailSpy = jest.spyOn(emailService, 'sendPasswordResetEmail'); const response = await request(app.getHttpServer()) .post('/api/v1/auth/password/request-reset') .send({ email: 'noexiste@example.com' }) .expect(200); // Same message to not reveal email existence expect(response.body.message).toContain('Si el email esta registrado'); expect(emailSpy).not.toHaveBeenCalled(); }); it('should reset password with valid token', async () => { // Request reset await request(app.getHttpServer()) .post('/api/v1/auth/password/request-reset') .send({ email: 'test@example.com' }); // Get token from DB (in real test, from email mock) const resetToken = await getResetTokenForUser('test@example.com'); // Reset password const response = await request(app.getHttpServer()) .post('/api/v1/auth/password/reset') .send({ token: resetToken, newPassword: 'NewSecurePass456!', confirmPassword: 'NewSecurePass456!', }) .expect(200); // Verify can login with new password await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'test@example.com', password: 'NewSecurePass456!', }) .expect(200); }); it('should reject expired reset token', async () => { // Create expired token const expiredToken = await createExpiredResetToken('test@example.com'); const response = await request(app.getHttpServer()) .post('/api/v1/auth/password/reset') .send({ token: expiredToken, newPassword: 'NewSecurePass456!', confirmPassword: 'NewSecurePass456!', }) .expect(400); expect(response.body.message).toContain('expirado'); }); it('should reject reused password', async () => { // Set up user with password history const user = await createUserWithPasswordHistory(); const resetToken = await createResetToken(user.email); const response = await request(app.getHttpServer()) .post('/api/v1/auth/password/reset') .send({ token: resetToken, newPassword: user.previousPasswords[0], // Try old password confirmPassword: user.previousPasswords[0], }) .expect(400); expect(response.body.message).toContain('anterior'); }); }); ``` --- ### TC-AUTH-008: Rate Limiting | Campo | Valor | |-------|-------| | **ID** | TC-AUTH-008 | | **Tipo** | Seguridad / Performance | | **Prioridad** | Alta | | **RF** | Todos | #### Limites por Endpoint | Endpoint | Limite | Ventana | |----------|--------|---------| | POST /auth/login | 10 | 1 minuto | | POST /auth/refresh | 1 | 1 segundo | | POST /auth/logout | 10 | 1 minuto | | POST /password/request-reset | 3 | 1 hora | | POST /password/reset | 5 | 1 hora | #### Codigo de Test ```typescript describe('Rate Limiting', () => { it('should limit login attempts to 10 per minute', async () => { // Make 10 requests for (let i = 0; i < 10; i++) { await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'test@example.com', password: 'wrongpass' }); } // 11th request should be rate limited const response = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'test@example.com', password: 'wrongpass' }) .expect(429); expect(response.body.message).toContain('Too many requests'); }); it('should limit password reset requests to 3 per hour', async () => { for (let i = 0; i < 3; i++) { await request(app.getHttpServer()) .post('/api/v1/auth/password/request-reset') .send({ email: 'test@example.com' }); } const response = await request(app.getHttpServer()) .post('/api/v1/auth/password/request-reset') .send({ email: 'test@example.com' }) .expect(429); }); }); ``` --- ## Pruebas de Seguridad ### OWASP Top 10 Checklist | # | Vulnerabilidad | Test | Estado | |---|----------------|------|--------| | A01 | Broken Access Control | Token validation en cada request | [ ] | | A02 | Cryptographic Failures | JWT RS256, bcrypt | [ ] | | A03 | Injection | SQL injection en login | [ ] | | A04 | Insecure Design | Token replay detection | [ ] | | A05 | Security Misconfiguration | Headers de seguridad | [ ] | | A06 | Vulnerable Components | Audit de dependencias | [ ] | | A07 | Authentication Failures | Bloqueo, rate limiting | [ ] | | A08 | Integrity Failures | Firma de tokens | [ ] | | A09 | Logging Failures | Auditoria de eventos | [ ] | | A10 | SSRF | N/A para este modulo | [ ] | ### Pruebas de Penetracion | Test | Descripcion | Resultado Esperado | |------|-------------|-------------------| | SQL Injection en email | `' OR '1'='1` | 400, sanitizado | | XSS en email | `` | 400, sanitizado | | JWT tampering | Modificar payload | 401, firma invalida | | Token brute force | Intentar adivinar token | Rate limited | | Session fixation | Reusar token post-login | Token rotado | --- ## Pruebas de Performance ### Load Test con k6 ```javascript // k6 script import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '1m', target: 50 }, // Ramp up { duration: '3m', target: 50 }, // Stay { duration: '1m', target: 100 }, // Peak { duration: '1m', target: 0 }, // Ramp down ], thresholds: { http_req_duration: ['p(95)<500'], // 95% requests < 500ms http_req_failed: ['rate<0.01'], // Error rate < 1% }, }; export default function () { const loginRes = http.post( 'http://localhost:3000/api/v1/auth/login', JSON.stringify({ email: `user${__VU}@example.com`, password: 'TestPass123!', }), { headers: { 'Content-Type': 'application/json' } } ); check(loginRes, { 'login status is 200': (r) => r.status === 200, 'login has access token': (r) => r.json('accessToken') !== undefined, }); sleep(1); } ``` ### Metricas Objetivo | Endpoint | P50 | P95 | P99 | |----------|-----|-----|-----| | POST /auth/login | 200ms | 500ms | 1000ms | | POST /auth/refresh | 50ms | 200ms | 500ms | | POST /auth/logout | 50ms | 200ms | 500ms | --- ## Matriz de Trazabilidad | RF | Casos de Prueba | |----|-----------------| | RF-AUTH-001 | TC-AUTH-001, TC-AUTH-002, TC-AUTH-003 | | RF-AUTH-002 | TC-AUTH-004 | | RF-AUTH-003 | TC-AUTH-005 | | RF-AUTH-004 | TC-AUTH-006 | | RF-AUTH-005 | TC-AUTH-007 | | Rate Limiting | TC-AUTH-008 | --- ## Criterios de Salida ### Para Desarrollo - [ ] Cobertura de codigo > 80% - [ ] Todos los tests unitarios pasan - [ ] Tests de integracion pasan - [ ] Analisis estatico sin errores criticos ### Para QA - [ ] Todos los casos de prueba ejecutados - [ ] 0 bugs criticos - [ ] 0 bugs mayores - [ ] Pruebas de seguridad completadas - [ ] Pruebas de performance completadas ### Para Release - [ ] UAT aprobado - [ ] Security review aprobado - [ ] Performance baselines establecidos - [ ] Documentacion actualizada --- ## Reportes ### Template de Reporte de Bugs ```markdown ## Bug Report **ID:** BUG-AUTH-XXX **Titulo:** [Descripcion corta] **Severidad:** Critica/Mayor/Menor/Trivial **Componente:** Auth Module **RF:** RF-AUTH-XXX ### Pasos para Reproducir 1. ... 2. ... 3. ... ### Resultado Actual [Que sucede] ### Resultado Esperado [Que deberia suceder] ### Evidencia [Screenshots, logs] ### Ambiente - OS: - Browser: - Backend version: ``` --- ## Historial de Cambios | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | System | Creacion inicial | --- ## Aprobaciones | Rol | Nombre | Fecha | Firma | |-----|--------|-------|-------| | QA Lead | - | - | [ ] | | Tech Lead | - | - | [ ] | | Security | - | - | [ ] |