# 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 | - | - | [ ] |
