# Test Plan: MGN-003 Roles/RBAC

## Identificacion

| Campo | Valor |
|-------|-------|
| **ID** | TP-MGN003 |
| **Modulo** | MGN-003 Roles/RBAC |
| **Version** | 1.0 |
| **Fecha** | 2025-12-05 |
| **Autor** | System |
| **Estado** | Ready |

---

## Alcance

Este plan de pruebas cubre todos los flujos del sistema de control de acceso basado en roles:

| RF | Nombre | Prioridad |
|----|--------|-----------|
| RF-ROLE-001 | CRUD de Roles | P0 |
| RF-ROLE-002 | Gestion de Permisos | P0 |
| RF-ROLE-003 | Asignacion Roles-Usuarios | P0 |
| RF-ROLE-004 | Guards y Middlewares RBAC | P0 |

---

## Estrategia de Pruebas

### Niveles de Testing

```
┌─────────────────────────────────────────────────────────────────┐
│                        E2E Tests (10%)                          │
│   Cypress - Flujos completos de gestion de roles                │
├─────────────────────────────────────────────────────────────────┤
│                   Integration Tests (30%)                        │
│   Supertest - API endpoints, guards, validacion permisos        │
├─────────────────────────────────────────────────────────────────┤
│                     Unit Tests (60%)                             │
│   Jest - Services, guards, decorators, cache                    │
└─────────────────────────────────────────────────────────────────┘
```

### Cobertura Objetivo

| Tipo | Coverage Target |
|------|-----------------|
| Unit Tests | > 85% |
| Integration Tests | > 75% |
| E2E Tests | Flujos criticos |

---

## Test Cases

### TC-RBAC-001: Crear rol exitosamente

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-001 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Precondiciones:**
- Admin autenticado con permiso `roles:create`
- Permisos existentes en el sistema

**Datos de entrada:**
```json
{
  "name": "Vendedor",
  "description": "Equipo de ventas",
  "permissionIds": ["perm-uuid-1", "perm-uuid-2"]
}
```

**Pasos:**
1. POST /api/v1/roles con datos validos
2. Verificar response status 201
3. Verificar estructura del rol retornado
4. Verificar slug generado
5. Verificar isBuiltIn = false

**Implementacion Jest:**
```typescript
describe('RolesController - Create', () => {
  it('should create role with permissions', async () => {
    const createDto = {
      name: 'Vendedor',
      description: 'Equipo de ventas',
      permissionIds: [testPermission1.id, testPermission2.id],
    };

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

    expect(response.body).toMatchObject({
      name: 'Vendedor',
      slug: 'vendedor',
      description: 'Equipo de ventas',
      isBuiltIn: false,
    });
    expect(response.body.permissions).toHaveLength(2);
  });
});
```

---

### TC-RBAC-002: No crear rol con nombre duplicado

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-001 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
it('should not create role with duplicate name', async () => {
  // Crear primer rol
  await rolesService.create(tenantId, {
    name: 'Vendedor',
    permissionIds: [testPermission.id],
  }, adminUser.id);

  // Intentar crear duplicado
  const response = await request(app.getHttpServer())
    .post('/api/v1/roles')
    .set('Authorization', `Bearer ${adminToken}`)
    .send({ name: 'Vendedor', permissionIds: [testPermission.id] })
    .expect(409);

  expect(response.body.message).toBe('Ya existe un rol con este nombre');
});
```

---

### TC-RBAC-003: No eliminar rol built-in

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-001 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
it('should not delete built-in role', async () => {
  const adminRole = await roleRepository.findOne({
    where: { slug: 'admin', tenantId },
  });

  const response = await request(app.getHttpServer())
    .delete(`/api/v1/roles/${adminRole.id}`)
    .set('Authorization', `Bearer ${superAdminToken}`)
    .expect(400);

  expect(response.body.message).toBe('No se pueden eliminar roles del sistema');
});
```

---

### TC-RBAC-004: Listar permisos agrupados

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-002 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
describe('PermissionsController - List', () => {
  it('should return permissions grouped by module', async () => {
    const response = await request(app.getHttpServer())
      .get('/api/v1/permissions')
      .set('Authorization', `Bearer ${adminToken}`)
      .expect(200);

    expect(response.body.data).toBeInstanceOf(Array);
    expect(response.body.data[0]).toHaveProperty('module');
    expect(response.body.data[0]).toHaveProperty('moduleName');
    expect(response.body.data[0]).toHaveProperty('permissions');
    expect(response.body.data[0].permissions[0]).toHaveProperty('code');
    expect(response.body.data[0].permissions[0]).toHaveProperty('name');
  });

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

    const allCodes = response.body.data.flatMap(g => g.permissions.map(p => p.code));
    expect(allCodes.every(c => c.includes('users'))).toBe(true);
  });
});
```

---

### TC-RBAC-005: Asignar roles a usuario

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-003 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
describe('User Roles Assignment', () => {
  it('should assign multiple roles to user', async () => {
    const vendedorRole = await createRole('Vendedor');
    const contadorRole = await createRole('Contador');

    const response = await request(app.getHttpServer())
      .put(`/api/v1/users/${testUser.id}/roles`)
      .set('Authorization', `Bearer ${adminToken}`)
      .send({ roleIds: [vendedorRole.id, contadorRole.id] })
      .expect(200);

    expect(response.body.roles).toHaveLength(2);
    expect(response.body.roles.map(r => r.name)).toContain('Vendedor');
    expect(response.body.roles.map(r => r.name)).toContain('Contador');
  });

  it('should calculate effective permissions from multiple roles', async () => {
    const response = await request(app.getHttpServer())
      .get(`/api/v1/users/${testUser.id}/permissions`)
      .set('Authorization', `Bearer ${adminToken}`)
      .expect(200);

    expect(response.body.roles).toContain('vendedor');
    expect(response.body.all).toBeInstanceOf(Array);
    expect(response.body.all.length).toBeGreaterThan(0);
  });
});
```

---

### TC-RBAC-006: Proteccion de rol super_admin

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-003 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
describe('Super Admin Protection', () => {
  it('should not allow admin to assign super_admin role', async () => {
    const superAdminRole = await roleRepository.findOne({
      where: { slug: 'super_admin', tenantId },
    });

    const response = await request(app.getHttpServer())
      .put(`/api/v1/users/${testUser.id}/roles`)
      .set('Authorization', `Bearer ${adminToken}`) // admin, no super_admin
      .send({ roleIds: [superAdminRole.id] })
      .expect(403);

    expect(response.body.message).toBe('Solo Super Admin puede asignar este rol');
  });

  it('should not allow removing last super_admin', async () => {
    // Solo hay un super_admin en el tenant
    const response = await request(app.getHttpServer())
      .put(`/api/v1/users/${superAdminUser.id}/roles`)
      .set('Authorization', `Bearer ${superAdminToken}`)
      .send({ roleIds: [] }) // Quitar todos los roles
      .expect(400);

    expect(response.body.message).toBe('Debe existir al menos un Super Admin');
  });
});
```

---

### TC-RBAC-007: RbacGuard permite acceso con permiso

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-004 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
describe('RbacGuard', () => {
  it('should allow access with required permission', async () => {
    // Usuario con permiso users:read
    await assignPermissionToUser(testUser.id, 'users:read');

    await request(app.getHttpServer())
      .get('/api/v1/users')
      .set('Authorization', `Bearer ${testUserToken}`)
      .expect(200);
  });

  it('should deny access without required permission', async () => {
    // Usuario sin permiso users:delete
    const response = await request(app.getHttpServer())
      .delete('/api/v1/users/some-id')
      .set('Authorization', `Bearer ${testUserToken}`)
      .expect(403);

    expect(response.body.message).toBe('No tienes permiso para realizar esta accion');
    // No debe revelar que permiso falta
    expect(response.body.requiredPermission).toBeUndefined();
  });
});
```

---

### TC-RBAC-008: Wildcard permite acceso

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-004 |
| **Tipo** | Unit |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
describe('Wildcard Permissions', () => {
  it('should expand wildcard to include all module permissions', async () => {
    // Asignar users:* al usuario
    await assignPermissionToUser(testUser.id, 'users:*');

    const effective = await permissionsService.getEffectivePermissions(testUser.id);

    expect(effective.direct).toContain('users:*');
    expect(effective.all).toContain('users:read');
    expect(effective.all).toContain('users:create');
    expect(effective.all).toContain('users:delete');
  });

  it('should allow access via wildcard', async () => {
    await assignPermissionToUser(testUser.id, 'users:*');

    // Endpoint requiere users:delete
    await request(app.getHttpServer())
      .delete(`/api/v1/users/${anotherUser.id}`)
      .set('Authorization', `Bearer ${testUserToken}`)
      .expect(200);
  });
});
```

---

### TC-RBAC-009: Super Admin bypass

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-004 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
describe('Super Admin Bypass', () => {
  it('should bypass permission check for super_admin', async () => {
    // Endpoint que requiere permiso especial
    await request(app.getHttpServer())
      .delete('/api/v1/tenants/config/dangerous')
      .set('Authorization', `Bearer ${superAdminToken}`)
      .expect(200);
  });

  it('should not bypass for regular admin', async () => {
    await request(app.getHttpServer())
      .delete('/api/v1/tenants/config/dangerous')
      .set('Authorization', `Bearer ${adminToken}`)
      .expect(403);
  });
});
```

---

### TC-RBAC-010: Owner puede acceder

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-004 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
describe('Owner Access', () => {
  it('should allow owner to update own resource', async () => {
    // Usuario sin permiso users:update
    // pero es owner del recurso (su propio perfil)
    await request(app.getHttpServer())
      .patch(`/api/v1/users/${testUser.id}`)
      .set('Authorization', `Bearer ${testUserToken}`)
      .send({ firstName: 'Nuevo Nombre' })
      .expect(200);
  });

  it('should deny non-owner without permission', async () => {
    // Usuario sin permiso intentando modificar otro usuario
    await request(app.getHttpServer())
      .patch(`/api/v1/users/${anotherUser.id}`)
      .set('Authorization', `Bearer ${testUserToken}`)
      .send({ firstName: 'Hack' })
      .expect(403);
  });
});
```

---

### TC-RBAC-011: Cache de permisos

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-004 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
describe('Permissions Cache', () => {
  it('should cache permissions', async () => {
    const spy = jest.spyOn(permissionsService, 'calculateEffective');

    // Primera llamada - calcula
    await permissionsService.getEffectivePermissions(testUser.id);
    expect(spy).toHaveBeenCalledTimes(1);

    // Segunda llamada - usa cache
    await permissionsService.getEffectivePermissions(testUser.id);
    expect(spy).toHaveBeenCalledTimes(1); // No incrementa
  });

  it('should invalidate cache on role change', async () => {
    // Obtener permisos (cachea)
    const before = await permissionsService.getEffectivePermissions(testUser.id);

    // Cambiar roles del usuario
    await rolesService.assignRoles(testUser.id, [newRole.id], adminUser.id);

    // Cache invalidado, recalcula
    const after = await permissionsService.getEffectivePermissions(testUser.id);

    expect(after.roles).not.toEqual(before.roles);
  });
});
```

---

### TC-RBAC-012: AnyPermission (OR)

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-004 |
| **Tipo** | Integration |
| **Prioridad** | P1 |

**Implementacion Jest:**
```typescript
describe('AnyPermission Decorator', () => {
  // Endpoint decorado con @AnyPermission('users:update', 'users:admin')
  it('should allow access with first permission', async () => {
    await assignPermissionToUser(testUser.id, 'users:update');

    await request(app.getHttpServer())
      .patch('/api/v1/users/special-action')
      .set('Authorization', `Bearer ${testUserToken}`)
      .expect(200);
  });

  it('should allow access with second permission', async () => {
    await assignPermissionToUser(testUser.id, 'users:admin');

    await request(app.getHttpServer())
      .patch('/api/v1/users/special-action')
      .set('Authorization', `Bearer ${testUserToken}`)
      .expect(200);
  });

  it('should deny without any of the permissions', async () => {
    // Usuario solo con users:read
    await assignPermissionToUser(testUser.id, 'users:read');

    await request(app.getHttpServer())
      .patch('/api/v1/users/special-action')
      .set('Authorization', `Bearer ${testUserToken}`)
      .expect(403);
  });
});
```

---

### TC-RBAC-013: Multi-tenant isolation

| Campo | Valor |
|-------|-------|
| **RF** | Transversal |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
describe('Multi-tenant Isolation', () => {
  let tenantA, tenantB;
  let roleA, roleB;

  beforeAll(async () => {
    tenantA = await createTenant('Tenant A');
    tenantB = await createTenant('Tenant B');

    roleA = await createRole('Custom Role', tenantA.id);
    roleB = await createRole('Custom Role', tenantB.id);
  });

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

    const roleIds = response.body.data.map(r => r.id);
    expect(roleIds).toContain(roleA.id);
    expect(roleIds).not.toContain(roleB.id);
  });

  it('should not access role from another tenant', async () => {
    await request(app.getHttpServer())
      .get(`/api/v1/roles/${roleB.id}`)
      .set('Authorization', `Bearer ${tenantAAdminToken}`)
      .expect(404);
  });
});
```

---

### TC-RBAC-014: Soft delete con reasignacion

| Campo | Valor |
|-------|-------|
| **RF** | RF-ROLE-001 |
| **Tipo** | Integration |
| **Prioridad** | P0 |

**Implementacion Jest:**
```typescript
describe('Role Soft Delete', () => {
  it('should soft delete role and reassign users', async () => {
    const customRole = await createRole('ToDelete');
    const userRole = await roleRepository.findOne({ where: { slug: 'user' } });

    // Asignar rol a usuarios
    await assignRoleToUsers(customRole.id, [user1.id, user2.id, user3.id]);

    // Eliminar con reasignacion
    await request(app.getHttpServer())
      .delete(`/api/v1/roles/${customRole.id}?reassignTo=${userRole.id}`)
      .set('Authorization', `Bearer ${adminToken}`)
      .expect(200);

    // Verificar soft delete
    const deleted = await roleRepository.findOne({
      where: { id: customRole.id },
      withDeleted: true,
    });
    expect(deleted.deletedAt).not.toBeNull();

    // Verificar reasignacion
    const user1Roles = await userRoleRepository.find({ where: { userId: user1.id } });
    expect(user1Roles.some(ur => ur.roleId === userRole.id)).toBe(true);
    expect(user1Roles.some(ur => ur.roleId === customRole.id)).toBe(false);
  });
});
```

---

## Datos de Prueba

### Permisos de Prueba

```typescript
const testPermissions = [
  { code: 'users:read', name: 'Leer usuarios', module: 'users' },
  { code: 'users:create', name: 'Crear usuarios', module: 'users' },
  { code: 'users:update', name: 'Actualizar usuarios', module: 'users' },
  { code: 'users:delete', name: 'Eliminar usuarios', module: 'users' },
  { code: 'users:*', name: 'Todos usuarios', module: 'users' },
  { code: 'roles:read', name: 'Leer roles', module: 'roles' },
  { code: 'roles:create', name: 'Crear roles', module: 'roles' },
];
```

### Usuarios de Prueba

```typescript
const testUsers = {
  superAdmin: {
    email: 'super@test.com',
    roles: ['super_admin'],
  },
  admin: {
    email: 'admin@test.com',
    roles: ['admin'],
  },
  regular: {
    email: 'user@test.com',
    roles: ['user'],
  },
};
```

---

## Setup y Teardown

```typescript
beforeAll(async () => {
  // Crear tenant de prueba
  testTenant = await createTestTenant();

  // Seed permisos
  await seedPermissions();

  // Crear roles built-in
  await seedBuiltInRoles(testTenant.id);

  // Crear usuarios de prueba
  superAdminUser = await createUser('super_admin');
  adminUser = await createUser('admin');
  testUser = await createUser('user');

  // Generar tokens
  superAdminToken = await generateToken(superAdminUser);
  adminToken = await generateToken(adminUser);
  testUserToken = await generateToken(testUser);
});

afterAll(async () => {
  await cleanDatabase();
  await closeConnections();
});

beforeEach(async () => {
  // Limpiar roles custom y asignaciones entre tests
  await truncateTables([
    'core_rbac.user_roles',
    'core_rbac.role_permissions',
  ]);
  await roleRepository.delete({ isBuiltIn: false });

  // Limpiar cache
  await cacheManager.reset();
});
```

---

## Matriz de Trazabilidad

| Test Case | RF | User Story | Criterio |
|-----------|-----|------------|----------|
| TC-RBAC-001 | RF-ROLE-001 | US-MGN003-001 | Escenario 1 |
| TC-RBAC-002 | RF-ROLE-001 | US-MGN003-001 | Escenario 3 |
| TC-RBAC-003 | RF-ROLE-001 | US-MGN003-001 | Escenario 4 |
| TC-RBAC-004 | RF-ROLE-002 | US-MGN003-002 | Escenario 1 |
| TC-RBAC-005 | RF-ROLE-003 | US-MGN003-003 | Escenario 1 |
| TC-RBAC-006 | RF-ROLE-003 | US-MGN003-003 | Escenarios 4-5 |
| TC-RBAC-007 | RF-ROLE-004 | US-MGN003-004 | Escenarios 1-2 |
| TC-RBAC-008 | RF-ROLE-004 | US-MGN003-004 | Escenario 3 |
| TC-RBAC-009 | RF-ROLE-004 | US-MGN003-004 | Escenario 4 |
| TC-RBAC-010 | RF-ROLE-004 | US-MGN003-004 | Escenario 6 |
| TC-RBAC-011 | RF-ROLE-004 | US-MGN003-004 | Escenario 7 |
| TC-RBAC-012 | RF-ROLE-004 | US-MGN003-004 | Escenario 5 |
| TC-RBAC-013 | Transversal | - | Multi-tenant |
| TC-RBAC-014 | RF-ROLE-001 | US-MGN003-001 | Escenario 5 |

---

## Metricas de Calidad

| Metrica | Objetivo |
|---------|----------|
| Tests pasando | 100% |
| Code coverage | > 85% |
| Bugs criticos | 0 |
| Bugs mayores | < 2 |

---

## Historial

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