# 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:**
```json
{
  "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:**
```json
{
  "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:**
```typescript
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:**
```json
{
  "data": [...10 usuarios],
  "meta": {
    "total": 50,
    "page": 2,
    "limit": 10,
    "totalPages": 5,
    "hasNext": true,
    "hasPrev": true
  }
}
```

**Implementacion Jest:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```json
{
  "statusCode": 400,
  "message": "No puedes eliminarte a ti mismo"
}
```

**Implementacion Jest:**
```typescript
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:**
```typescript
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:**
```json
{
  "firstName": "Carlos",
  "lastName": "Lopez",
  "phone": "+521234567890"
}
```

**Resultado esperado:**
- Perfil actualizado
- Email NO puede cambiarse por este endpoint
- Campos no incluidos permanecen igual

**Implementacion Jest:**
```typescript
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:**
```typescript
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:**
```json
{
  "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:**
```typescript
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:**
```typescript
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:**
```json
{
  "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:**
```typescript
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:**
```json
{
  "currentPassword": "OldPass123!",
  "newPassword": "abc123",
  "confirmPassword": "abc123"
}
```

**Resultado esperado:**
```json
{
  "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:**
```typescript
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:**
```json
{
  "currentPassword": "CurrentPass!",
  "newPassword": "MiPass123!",
  "confirmPassword": "MiPass123!"
}
```

**Resultado esperado:**
```json
{
  "statusCode": 400,
  "message": "No puedes usar un password que hayas usado anteriormente"
}
```

**Implementacion Jest:**
```typescript
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:**
```typescript
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:**
```json
{
  "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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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

```typescript
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

```typescript
// 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 |
