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:
{
"name": "Vendedor",
"description": "Equipo de ventas",
"permissionIds": ["perm-uuid-1", "perm-uuid-2"]
}
Pasos:
- POST /api/v1/roles con datos validos
- Verificar response status 201
- Verificar estructura del rol retornado
- Verificar slug generado
- Verificar isBuiltIn = false
Implementacion Jest:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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
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
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
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 |