workspace-v1/projects/erp-core/docs/06-test-plans/TP-users.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

27 KiB

Test Plan: MGN-002 Users

Identificacion

Campo Valor
ID TP-MGN002
Modulo MGN-002 Users
Version 1.0
Fecha 2025-12-05
Autor System
Estado Ready

Alcance

Este plan de pruebas cubre todos los flujos del modulo de gestion de usuarios:

RF Nombre Prioridad
RF-USER-001 CRUD de Usuarios P0
RF-USER-002 Perfil de Usuario P1
RF-USER-003 Cambio de Email P1
RF-USER-004 Cambio de Password P0
RF-USER-005 Preferencias de Usuario P2

Estrategia de Pruebas

Niveles de Testing

┌─────────────────────────────────────────────────────────────────┐
│                        E2E Tests (10%)                          │
│   Cypress - Flujos completos de usuario                         │
├─────────────────────────────────────────────────────────────────┤
│                   Integration Tests (30%)                        │
│   Supertest - API endpoints con base de datos real               │
├─────────────────────────────────────────────────────────────────┤
│                     Unit Tests (60%)                             │
│   Jest - Services, DTOs, validators, guards                      │
└─────────────────────────────────────────────────────────────────┘

Cobertura Objetivo

Tipo Coverage Target
Unit Tests > 80%
Integration Tests > 70%
E2E Tests Flujos criticos

Test Cases

TC-USR-001: Crear usuario como admin

Campo Valor
RF RF-USER-001
Tipo Integration
Prioridad P0

Precondiciones:

  • Admin autenticado con permiso users:create
  • Rol valido existente

Datos de entrada:

{
  "email": "nuevo@empresa.com",
  "firstName": "Juan",
  "lastName": "Perez",
  "phone": "+521234567890",
  "roleIds": ["role-uuid"]
}

Pasos:

  1. POST /api/v1/users con datos validos
  2. Verificar response status 201
  3. Verificar estructura del usuario retornado
  4. Verificar que no incluye passwordHash
  5. Verificar status = "pending_activation"
  6. Verificar email de invitacion enviado

Resultado esperado:

{
  "id": "user-uuid",
  "email": "nuevo@empresa.com",
  "firstName": "Juan",
  "lastName": "Perez",
  "status": "pending_activation",
  "createdAt": "2025-12-05T10:00:00Z",
  "roles": [{ "id": "role-uuid", "name": "admin" }]
}

Implementacion Jest:

describe('UsersController - Create', () => {
  it('should create user with pending_activation status', async () => {
    const createUserDto = {
      email: 'nuevo@empresa.com',
      firstName: 'Juan',
      lastName: 'Perez',
      roleIds: [testRole.id],
    };

    const response = await request(app.getHttpServer())
      .post('/api/v1/users')
      .set('Authorization', `Bearer ${adminToken}`)
      .send(createUserDto)
      .expect(201);

    expect(response.body).toMatchObject({
      email: createUserDto.email,
      firstName: createUserDto.firstName,
      status: 'pending_activation',
    });
    expect(response.body.passwordHash).toBeUndefined();
    expect(emailService.sendInvitation).toHaveBeenCalledWith(
      createUserDto.email,
      expect.any(String),
    );
  });
});

TC-USR-002: Listar usuarios con paginacion

Campo Valor
RF RF-USER-001
Tipo Integration
Prioridad P0

Precondiciones:

  • Admin autenticado con permiso users:read
  • 50 usuarios en el tenant

Datos de entrada:

GET /api/v1/users?page=2&limit=10&sortBy=createdAt&sortOrder=DESC

Resultado esperado:

{
  "data": [...10 usuarios],
  "meta": {
    "total": 50,
    "page": 2,
    "limit": 10,
    "totalPages": 5,
    "hasNext": true,
    "hasPrev": true
  }
}

Implementacion Jest:

describe('UsersController - List', () => {
  beforeAll(async () => {
    // Crear 50 usuarios de prueba
    await Promise.all(
      Array.from({ length: 50 }, (_, i) =>
        userRepository.save({
          email: `user${i}@test.com`,
          firstName: `User${i}`,
          lastName: 'Test',
          tenantId: testTenant.id,
          status: 'active',
        }),
      ),
    );
  });

  it('should return paginated users', async () => {
    const response = await request(app.getHttpServer())
      .get('/api/v1/users?page=2&limit=10')
      .set('Authorization', `Bearer ${adminToken}`)
      .expect(200);

    expect(response.body.data).toHaveLength(10);
    expect(response.body.meta).toMatchObject({
      total: 50,
      page: 2,
      limit: 10,
      totalPages: 5,
      hasNext: true,
      hasPrev: true,
    });
  });
});

TC-USR-003: Buscar usuarios por texto

Campo Valor
RF RF-USER-001
Tipo Integration
Prioridad P1

Precondiciones:

  • Usuarios: "Juan Perez", "Juana Garcia", "Pedro Lopez"

Datos de entrada:

GET /api/v1/users?search=juan

Resultado esperado:

  • Retorna "Juan Perez" y "Juana Garcia"
  • No retorna "Pedro Lopez"
  • Busqueda case-insensitive

Implementacion Jest:

describe('UsersController - Search', () => {
  beforeAll(async () => {
    await userRepository.save([
      { email: 'juan@test.com', firstName: 'Juan', lastName: 'Perez', ...baseUser },
      { email: 'juana@test.com', firstName: 'Juana', lastName: 'Garcia', ...baseUser },
      { email: 'pedro@test.com', firstName: 'Pedro', lastName: 'Lopez', ...baseUser },
    ]);
  });

  it('should search users by name', async () => {
    const response = await request(app.getHttpServer())
      .get('/api/v1/users?search=juan')
      .set('Authorization', `Bearer ${adminToken}`)
      .expect(200);

    expect(response.body.data).toHaveLength(2);
    expect(response.body.data.map(u => u.firstName)).toEqual(
      expect.arrayContaining(['Juan', 'Juana']),
    );
  });
});

TC-USR-004: Soft delete de usuario

Campo Valor
RF RF-USER-001
Tipo Integration
Prioridad P0

Precondiciones:

  • Admin autenticado con permiso users:delete
  • Usuario activo existente

Pasos:

  1. DELETE /api/v1/users/:id
  2. Verificar response 200
  3. Verificar deleted_at establecido
  4. Verificar deleted_by = admin_id
  5. Verificar usuario no aparece en listados
  6. Verificar usuario no puede hacer login

Implementacion Jest:

describe('UsersController - Delete', () => {
  it('should soft delete user', async () => {
    const userToDelete = await userRepository.save({
      email: 'todelete@test.com',
      firstName: 'ToDelete',
      lastName: 'User',
      tenantId: testTenant.id,
      status: 'active',
    });

    await request(app.getHttpServer())
      .delete(`/api/v1/users/${userToDelete.id}`)
      .set('Authorization', `Bearer ${adminToken}`)
      .expect(200);

    // Verificar soft delete
    const deletedUser = await userRepository.findOne({
      where: { id: userToDelete.id },
      withDeleted: true,
    });
    expect(deletedUser.deletedAt).not.toBeNull();
    expect(deletedUser.deletedBy).toBe(adminUser.id);

    // Verificar no aparece en listados
    const listResponse = await request(app.getHttpServer())
      .get('/api/v1/users')
      .set('Authorization', `Bearer ${adminToken}`);

    const ids = listResponse.body.data.map(u => u.id);
    expect(ids).not.toContain(userToDelete.id);
  });
});

TC-USR-005: No puede eliminarse a si mismo

Campo Valor
RF RF-USER-001
Tipo Integration
Prioridad P0

Precondiciones:

  • Admin autenticado

Pasos:

  1. DELETE /api/v1/users/:ownId
  2. Verificar response 400
  3. Verificar mensaje de error

Resultado esperado:

{
  "statusCode": 400,
  "message": "No puedes eliminarte a ti mismo"
}

Implementacion Jest:

it('should not allow self-deletion', async () => {
  const response = await request(app.getHttpServer())
    .delete(`/api/v1/users/${adminUser.id}`)
    .set('Authorization', `Bearer ${adminToken}`)
    .expect(400);

  expect(response.body.message).toBe('No puedes eliminarte a ti mismo');
});

TC-USR-006: Ver perfil propio

Campo Valor
RF RF-USER-002
Tipo Integration
Prioridad P0

Precondiciones:

  • Usuario autenticado

Pasos:

  1. GET /api/v1/users/me
  2. Verificar response 200
  3. Verificar datos del perfil
  4. Verificar NO incluye passwordHash

Implementacion Jest:

describe('ProfileController - Get', () => {
  it('should return own profile', async () => {
    const response = await request(app.getHttpServer())
      .get('/api/v1/users/me')
      .set('Authorization', `Bearer ${userToken}`)
      .expect(200);

    expect(response.body).toMatchObject({
      id: testUser.id,
      email: testUser.email,
      firstName: testUser.firstName,
      lastName: testUser.lastName,
    });
    expect(response.body.passwordHash).toBeUndefined();
    expect(response.body.roles).toBeDefined();
  });
});

TC-USR-007: Actualizar perfil propio

Campo Valor
RF RF-USER-002
Tipo Integration
Prioridad P1

Datos de entrada:

{
  "firstName": "Carlos",
  "lastName": "Lopez",
  "phone": "+521234567890"
}

Resultado esperado:

  • Perfil actualizado
  • Email NO puede cambiarse por este endpoint
  • Campos no incluidos permanecen igual

Implementacion Jest:

it('should update own profile', async () => {
  const updateDto = {
    firstName: 'Carlos',
    lastName: 'Lopez',
    phone: '+521234567890',
  };

  const response = await request(app.getHttpServer())
    .patch('/api/v1/users/me')
    .set('Authorization', `Bearer ${userToken}`)
    .send(updateDto)
    .expect(200);

  expect(response.body.firstName).toBe('Carlos');
  expect(response.body.lastName).toBe('Lopez');
  expect(response.body.email).toBe(testUser.email); // No cambio
});

TC-USR-008: Subir avatar

Campo Valor
RF RF-USER-002
Tipo Integration
Prioridad P1

Precondiciones:

  • Usuario autenticado
  • Imagen JPG de 2MB

Pasos:

  1. POST /api/v1/users/me/avatar con imagen
  2. Verificar response 200
  3. Verificar avatarUrl y avatarThumbnailUrl retornados
  4. Verificar imagen redimensionada a 200x200
  5. Verificar thumbnail 50x50

Implementacion Jest:

describe('ProfileController - Avatar', () => {
  it('should upload and resize avatar', async () => {
    const imagePath = path.join(__dirname, 'fixtures', 'test-avatar.jpg');

    const response = await request(app.getHttpServer())
      .post('/api/v1/users/me/avatar')
      .set('Authorization', `Bearer ${userToken}`)
      .attach('avatar', imagePath)
      .expect(200);

    expect(response.body.avatarUrl).toContain('avatar-200');
    expect(response.body.avatarThumbnailUrl).toContain('avatar-50');

    // Verificar dimensiones si es posible
    // const metadata = await sharp(response.body.avatarUrl).metadata();
    // expect(metadata.width).toBe(200);
    // expect(metadata.height).toBe(200);
  });

  it('should reject avatar > 10MB', async () => {
    const largePath = path.join(__dirname, 'fixtures', 'large-image.jpg');

    await request(app.getHttpServer())
      .post('/api/v1/users/me/avatar')
      .set('Authorization', `Bearer ${userToken}`)
      .attach('avatar', largePath)
      .expect(400);
  });
});

TC-USR-009: Solicitar cambio de email

Campo Valor
RF RF-USER-003
Tipo Integration
Prioridad P1

Datos de entrada:

{
  "newEmail": "nuevo@empresa.com",
  "currentPassword": "Password123!"
}

Pasos:

  1. POST /api/v1/users/me/email/request
  2. Verificar response 200
  3. Verificar email de verificacion enviado al nuevo email
  4. Verificar email actual NO cambia aun

Implementacion Jest:

describe('EmailChangeController', () => {
  it('should request email change', async () => {
    const dto = {
      newEmail: 'nuevo@empresa.com',
      currentPassword: 'Password123!',
    };

    const response = await request(app.getHttpServer())
      .post('/api/v1/users/me/email/request')
      .set('Authorization', `Bearer ${userToken}`)
      .send(dto)
      .expect(200);

    expect(response.body.message).toContain('verificacion');

    // Email actual sin cambio
    const user = await userRepository.findOne({ where: { id: testUser.id } });
    expect(user.email).toBe(testUser.email);

    // Email de verificacion enviado
    expect(emailService.sendEmailVerification).toHaveBeenCalledWith(
      dto.newEmail,
      expect.any(String),
    );
  });
});

TC-USR-010: Confirmar cambio de email

Campo Valor
RF RF-USER-003
Tipo Integration
Prioridad P1

Precondiciones:

  • Solicitud de cambio pendiente con token valido

Pasos:

  1. POST /api/v1/users/me/email/confirm con token
  2. Verificar email actualizado
  3. Verificar token consumido (no reutilizable)

Implementacion Jest:

it('should confirm email change with valid token', async () => {
  // Crear solicitud pendiente
  const changeRequest = await emailChangeRepository.save({
    userId: testUser.id,
    newEmail: 'nuevo@empresa.com',
    token: 'valid-token-hash',
    expiresAt: new Date(Date.now() + 3600000),
  });

  await request(app.getHttpServer())
    .post('/api/v1/users/me/email/confirm')
    .send({ token: 'valid-token' })
    .expect(200);

  const updatedUser = await userRepository.findOne({ where: { id: testUser.id } });
  expect(updatedUser.email).toBe('nuevo@empresa.com');

  // Token no reutilizable
  await request(app.getHttpServer())
    .post('/api/v1/users/me/email/confirm')
    .send({ token: 'valid-token' })
    .expect(400);
});

TC-USR-011: Cambiar password exitosamente

Campo Valor
RF RF-USER-004
Tipo Integration
Prioridad P0

Datos de entrada:

{
  "currentPassword": "OldPass123!",
  "newPassword": "NewPass456!",
  "confirmPassword": "NewPass456!",
  "logoutOtherSessions": true
}

Pasos:

  1. POST /api/v1/users/me/password
  2. Verificar response 200
  3. Verificar password actualizado (login funciona con nuevo)
  4. Verificar password guardado en historial
  5. Verificar otras sesiones invalidadas
  6. Verificar email de notificacion enviado

Implementacion Jest:

describe('PasswordController', () => {
  it('should change password and logout other sessions', async () => {
    const dto = {
      currentPassword: 'OldPass123!',
      newPassword: 'NewPass456!',
      confirmPassword: 'NewPass456!',
      logoutOtherSessions: true,
    };

    // Crear sesiones adicionales
    await sessionService.createSession(testUser.id, 'device-1');
    await sessionService.createSession(testUser.id, 'device-2');

    const response = await request(app.getHttpServer())
      .post('/api/v1/users/me/password')
      .set('Authorization', `Bearer ${userToken}`)
      .send(dto)
      .expect(200);

    expect(response.body.sessionsInvalidated).toBe(2);

    // Verificar login con nuevo password
    await request(app.getHttpServer())
      .post('/api/v1/auth/login')
      .send({ email: testUser.email, password: dto.newPassword })
      .expect(200);

    // Verificar historial
    const history = await passwordHistoryRepository.find({
      where: { userId: testUser.id },
    });
    expect(history.length).toBeGreaterThan(0);
  });
});

TC-USR-012: Password no cumple requisitos

Campo Valor
RF RF-USER-004
Tipo Unit
Prioridad P0

Datos de entrada:

{
  "currentPassword": "OldPass123!",
  "newPassword": "abc123",
  "confirmPassword": "abc123"
}

Resultado esperado:

{
  "statusCode": 400,
  "message": "El password no cumple los requisitos",
  "errors": [
    "Debe tener al menos 8 caracteres",
    "Debe incluir una mayuscula",
    "Debe incluir caracter especial"
  ]
}

Implementacion Jest:

describe('PasswordValidator', () => {
  const validator = new PasswordPolicyValidator();

  it('should reject weak password', () => {
    const result = validator.validate('abc123');

    expect(result.isValid).toBe(false);
    expect(result.errors).toContain('Debe tener al menos 8 caracteres');
    expect(result.errors).toContain('Debe incluir una mayuscula');
    expect(result.errors).toContain('Debe incluir caracter especial');
  });

  it('should accept strong password', () => {
    const result = validator.validate('NewPass456!');
    expect(result.isValid).toBe(true);
    expect(result.errors).toHaveLength(0);
  });
});

TC-USR-013: No reutilizar password anterior

Campo Valor
RF RF-USER-004
Tipo Integration
Prioridad P0

Precondiciones:

  • Usuario uso "MiPass123!" hace 2 meses (en historial)

Datos de entrada:

{
  "currentPassword": "CurrentPass!",
  "newPassword": "MiPass123!",
  "confirmPassword": "MiPass123!"
}

Resultado esperado:

{
  "statusCode": 400,
  "message": "No puedes usar un password que hayas usado anteriormente"
}

Implementacion Jest:

it('should not allow reusing recent passwords', async () => {
  // Agregar password al historial
  await passwordHistoryRepository.save({
    userId: testUser.id,
    passwordHash: await bcrypt.hash('MiPass123!', 12),
    createdAt: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000), // 60 dias atras
  });

  const response = await request(app.getHttpServer())
    .post('/api/v1/users/me/password')
    .set('Authorization', `Bearer ${userToken}`)
    .send({
      currentPassword: 'CurrentPass!',
      newPassword: 'MiPass123!',
      confirmPassword: 'MiPass123!',
    })
    .expect(400);

  expect(response.body.message).toContain('usado anteriormente');
});

TC-USR-014: Obtener preferencias

Campo Valor
RF RF-USER-005
Tipo Integration
Prioridad P1

Precondiciones:

  • Usuario autenticado
  • Sin preferencias personalizadas

Resultado esperado:

  • Retorna defaults del tenant si no hay preferencias
  • Estructura completa de preferencias

Implementacion Jest:

describe('PreferencesController', () => {
  it('should return tenant defaults if no preferences', async () => {
    const response = await request(app.getHttpServer())
      .get('/api/v1/users/me/preferences')
      .set('Authorization', `Bearer ${userToken}`)
      .expect(200);

    expect(response.body).toMatchObject({
      language: 'es',
      timezone: 'America/Mexico_City',
      theme: 'light',
      notifications: {
        email: { enabled: true },
        push: { enabled: true },
        inApp: { enabled: true },
      },
    });
  });
});

TC-USR-015: Actualizar preferencias parcialmente

Campo Valor
RF RF-USER-005
Tipo Integration
Prioridad P1

Datos de entrada:

{
  "theme": "dark",
  "notifications": {
    "email": { "marketing": false }
  }
}

Resultado esperado:

  • Solo campos enviados se actualizan
  • Deep merge para objetos anidados
  • Otros campos mantienen valor anterior

Implementacion Jest:

it('should partially update preferences with deep merge', async () => {
  // Establecer preferencias iniciales
  await preferencesRepository.save({
    userId: testUser.id,
    language: 'es',
    theme: 'light',
    notifications: {
      email: { enabled: true, marketing: true },
      push: { enabled: true },
    },
  });

  const response = await request(app.getHttpServer())
    .patch('/api/v1/users/me/preferences')
    .set('Authorization', `Bearer ${userToken}`)
    .send({
      theme: 'dark',
      notifications: {
        email: { marketing: false },
      },
    })
    .expect(200);

  expect(response.body.theme).toBe('dark');
  expect(response.body.language).toBe('es'); // Sin cambio
  expect(response.body.notifications.email.enabled).toBe(true); // Sin cambio
  expect(response.body.notifications.email.marketing).toBe(false); // Cambiado
  expect(response.body.notifications.push.enabled).toBe(true); // Sin cambio
});

TC-USR-016: Reset preferencias a defaults

Campo Valor
RF RF-USER-005
Tipo Integration
Prioridad P2

Precondiciones:

  • Usuario con preferencias personalizadas

Pasos:

  1. POST /api/v1/users/me/preferences/reset
  2. Verificar preferencias vuelven a defaults del tenant

Implementacion Jest:

it('should reset preferences to tenant defaults', async () => {
  // Preferencias personalizadas
  await preferencesRepository.save({
    userId: testUser.id,
    language: 'en',
    theme: 'dark',
    timezone: 'America/New_York',
  });

  const response = await request(app.getHttpServer())
    .post('/api/v1/users/me/preferences/reset')
    .set('Authorization', `Bearer ${userToken}`)
    .expect(200);

  // Deberia tener defaults del tenant
  expect(response.body.language).toBe('es');
  expect(response.body.theme).toBe('light');
  expect(response.body.timezone).toBe('America/Mexico_City');
});

TC-USR-017: Aislamiento multi-tenant

Campo Valor
RF Transversal
Tipo Integration
Prioridad P0

Precondiciones:

  • Tenant A con usuarios
  • Tenant B con usuarios
  • Admin de Tenant A autenticado

Pasos:

  1. GET /api/v1/users (Tenant A)
  2. Verificar solo retorna usuarios de Tenant A
  3. Verificar no retorna usuarios de Tenant B

Implementacion Jest:

describe('Multi-tenant Isolation', () => {
  let tenantA, tenantB;
  let adminTokenA, adminTokenB;

  beforeAll(async () => {
    tenantA = await tenantRepository.save({ name: 'Tenant A', slug: 'tenant-a' });
    tenantB = await tenantRepository.save({ name: 'Tenant B', slug: 'tenant-b' });

    // Crear usuarios en cada tenant
    await userRepository.save([
      { email: 'user-a@test.com', tenantId: tenantA.id, ...baseUser },
      { email: 'user-b@test.com', tenantId: tenantB.id, ...baseUser },
    ]);
  });

  it('should only return users from same tenant', async () => {
    const response = await request(app.getHttpServer())
      .get('/api/v1/users')
      .set('Authorization', `Bearer ${adminTokenA}`)
      .expect(200);

    const emails = response.body.data.map(u => u.email);
    expect(emails).toContain('user-a@test.com');
    expect(emails).not.toContain('user-b@test.com');
  });
});

TC-USR-018: Control de acceso RBAC

Campo Valor
RF Transversal
Tipo Integration
Prioridad P0

Escenarios:

  1. Usuario sin permiso users:create no puede crear usuarios
  2. Usuario sin permiso users:delete no puede eliminar usuarios
  3. Usuario puede ver/editar su propio perfil sin permisos especiales

Implementacion Jest:

describe('RBAC - Users', () => {
  it('should deny user creation without users:create permission', async () => {
    // Token de usuario sin permisos de admin
    await request(app.getHttpServer())
      .post('/api/v1/users')
      .set('Authorization', `Bearer ${regularUserToken}`)
      .send(createUserDto)
      .expect(403);
  });

  it('should allow own profile access without special permissions', async () => {
    await request(app.getHttpServer())
      .get('/api/v1/users/me')
      .set('Authorization', `Bearer ${regularUserToken}`)
      .expect(200);
  });
});

Datos de Prueba

Usuarios de Prueba

const testUsers = {
  admin: {
    email: 'admin@test.com',
    password: 'AdminPass123!',
    firstName: 'Admin',
    lastName: 'User',
    roles: ['admin'],
    permissions: ['users:create', 'users:read', 'users:update', 'users:delete'],
  },
  regular: {
    email: 'user@test.com',
    password: 'UserPass123!',
    firstName: 'Regular',
    lastName: 'User',
    roles: ['user'],
    permissions: [],
  },
  manager: {
    email: 'manager@test.com',
    password: 'ManagerPass123!',
    firstName: 'Manager',
    lastName: 'User',
    roles: ['manager'],
    permissions: ['users:read'],
  },
};

Imagenes de Prueba

test/fixtures/
├── test-avatar.jpg      # 500KB, 800x800px
├── large-image.jpg      # 15MB, excede limite
├── invalid-format.gif   # Formato no soportado
└── small-avatar.png     # 50KB, 100x100px

Setup y Teardown

// test/setup.ts
beforeAll(async () => {
  // Crear base de datos de prueba
  await createDatabase('erp_test');

  // Ejecutar migraciones
  await runMigrations();

  // Crear tenant de prueba
  testTenant = await createTestTenant();

  // Crear usuarios de prueba
  adminUser = await createTestUser('admin');
  regularUser = await createTestUser('regular');

  // Generar tokens
  adminToken = await generateToken(adminUser);
  userToken = await generateToken(regularUser);
});

afterAll(async () => {
  // Limpiar base de datos
  await cleanDatabase();

  // Cerrar conexiones
  await closeConnections();
});

beforeEach(async () => {
  // Limpiar tablas entre tests
  await truncateTables([
    'core_users.user_avatars',
    'core_users.user_preferences',
    'core_users.email_change_requests',
  ]);
});

Matriz de Trazabilidad

Test Case RF User Story Criterio Aceptacion
TC-USR-001 RF-USER-001 US-MGN002-001 Escenario 1
TC-USR-002 RF-USER-001 US-MGN002-001 Escenario 2
TC-USR-003 RF-USER-001 US-MGN002-001 Escenario 3
TC-USR-004 RF-USER-001 US-MGN002-001 Escenario 4
TC-USR-005 RF-USER-001 US-MGN002-001 Escenario 5
TC-USR-006 RF-USER-002 US-MGN002-002 Escenario 1
TC-USR-007 RF-USER-002 US-MGN002-002 Escenario 2
TC-USR-008 RF-USER-002 US-MGN002-002 Escenario 3
TC-USR-009 RF-USER-003 - Escenario 1
TC-USR-010 RF-USER-003 - Escenario 2
TC-USR-011 RF-USER-004 US-MGN002-003 Escenario 1
TC-USR-012 RF-USER-004 US-MGN002-003 Escenario 3
TC-USR-013 RF-USER-004 US-MGN002-003 Escenario 4
TC-USR-014 RF-USER-005 US-MGN002-004 Escenario 1
TC-USR-015 RF-USER-005 US-MGN002-004 Escenario 2-4
TC-USR-016 RF-USER-005 US-MGN002-004 Escenario 5
TC-USR-017 Transversal - Multi-tenancy
TC-USR-018 Transversal - RBAC

Metricas de Calidad

Criterios de Exito

Metrica Objetivo
Tests pasando 100%
Code coverage > 80%
Bugs criticos 0
Bugs mayores < 2

Defectos Conocidos

ID Descripcion Severidad Estado
- - - -

Historial

Version Fecha Autor Cambios
1.0 2025-12-05 System Creacion inicial con 18 test cases