793 lines
21 KiB
Markdown
793 lines
21 KiB
Markdown
# 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 | `<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
|
|
|
|
```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 | - | - | [ ] |
|