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
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
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
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
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
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
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
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
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 |
<script>alert(1)</script> |
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
// 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
Para QA
Para Release
Reportes
Template de Reporte de Bugs
## 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 |
- |
- |
[ ] |