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