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