27 KiB
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:
{
"email": "nuevo@empresa.com",
"firstName": "Juan",
"lastName": "Perez",
"phone": "+521234567890",
"roleIds": ["role-uuid"]
}
Pasos:
- POST /api/v1/users con datos validos
- Verificar response status 201
- Verificar estructura del usuario retornado
- Verificar que no incluye passwordHash
- Verificar status = "pending_activation"
- Verificar email de invitacion enviado
Resultado esperado:
{
"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:
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:
{
"data": [...10 usuarios],
"meta": {
"total": 50,
"page": 2,
"limit": 10,
"totalPages": 5,
"hasNext": true,
"hasPrev": true
}
}
Implementacion Jest:
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:
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:
- DELETE /api/v1/users/:id
- Verificar response 200
- Verificar deleted_at establecido
- Verificar deleted_by = admin_id
- Verificar usuario no aparece en listados
- Verificar usuario no puede hacer login
Implementacion Jest:
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:
- DELETE /api/v1/users/:ownId
- Verificar response 400
- Verificar mensaje de error
Resultado esperado:
{
"statusCode": 400,
"message": "No puedes eliminarte a ti mismo"
}
Implementacion Jest:
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:
- GET /api/v1/users/me
- Verificar response 200
- Verificar datos del perfil
- Verificar NO incluye passwordHash
Implementacion Jest:
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:
{
"firstName": "Carlos",
"lastName": "Lopez",
"phone": "+521234567890"
}
Resultado esperado:
- Perfil actualizado
- Email NO puede cambiarse por este endpoint
- Campos no incluidos permanecen igual
Implementacion Jest:
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:
- POST /api/v1/users/me/avatar con imagen
- Verificar response 200
- Verificar avatarUrl y avatarThumbnailUrl retornados
- Verificar imagen redimensionada a 200x200
- Verificar thumbnail 50x50
Implementacion Jest:
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:
{
"newEmail": "nuevo@empresa.com",
"currentPassword": "Password123!"
}
Pasos:
- POST /api/v1/users/me/email/request
- Verificar response 200
- Verificar email de verificacion enviado al nuevo email
- Verificar email actual NO cambia aun
Implementacion Jest:
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:
- POST /api/v1/users/me/email/confirm con token
- Verificar email actualizado
- Verificar token consumido (no reutilizable)
Implementacion Jest:
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:
{
"currentPassword": "OldPass123!",
"newPassword": "NewPass456!",
"confirmPassword": "NewPass456!",
"logoutOtherSessions": true
}
Pasos:
- POST /api/v1/users/me/password
- Verificar response 200
- Verificar password actualizado (login funciona con nuevo)
- Verificar password guardado en historial
- Verificar otras sesiones invalidadas
- Verificar email de notificacion enviado
Implementacion Jest:
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:
{
"currentPassword": "OldPass123!",
"newPassword": "abc123",
"confirmPassword": "abc123"
}
Resultado esperado:
{
"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:
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:
{
"currentPassword": "CurrentPass!",
"newPassword": "MiPass123!",
"confirmPassword": "MiPass123!"
}
Resultado esperado:
{
"statusCode": 400,
"message": "No puedes usar un password que hayas usado anteriormente"
}
Implementacion Jest:
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:
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:
{
"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:
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:
- POST /api/v1/users/me/preferences/reset
- Verificar preferencias vuelven a defaults del tenant
Implementacion Jest:
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:
- GET /api/v1/users (Tenant A)
- Verificar solo retorna usuarios de Tenant A
- Verificar no retorna usuarios de Tenant B
Implementacion Jest:
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:
- Usuario sin permiso
users:createno puede crear usuarios - Usuario sin permiso
users:deleteno puede eliminar usuarios - Usuario puede ver/editar su propio perfil sin permisos especiales
Implementacion Jest:
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
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
// 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 |