workspace-v1/projects/erp-core/docs/06-test-plans/TP-auth.md
rckrdmrd 66161b1566 feat: Workspace-v1 complete migration with NEXUS v3.4
Sistema NEXUS v3.4 migrado con:

Estructura principal:
- core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles)
- core/catalog: Catalogo de funcionalidades reutilizables
- shared/knowledge-base: Base de conocimiento compartida
- devtools/scripts: Herramientas de desarrollo
- control-plane/registries: Control de servicios y CI/CD
- orchestration/: Configuracion de orquestacion de agentes

Proyectos incluidos (11):
- gamilit (submodule -> GitHub)
- trading-platform (OrbiquanTIA)
- erp-suite con 5 verticales:
  - erp-core, construccion, vidrio-templado
  - mecanicas-diesel, retail, clinicas
- betting-analytics
- inmobiliaria-analytics
- platform_marketing_content
- pos-micro, erp-basico

Configuracion:
- .gitignore completo para Node.js/Python/Docker
- gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git)
- Sistema de puertos estandarizado (3005-3199)

Generated with NEXUS v3.4 Migration System
EPIC-010: Configuracion Git y Repositorios
2026-01-04 03:37:42 -06:00

21 KiB

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

  • 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

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