607 lines
13 KiB
Markdown
607 lines
13 KiB
Markdown
# Plan de Pruebas - MGN-004 Multi-Tenancy
|
|
|
|
## Identificacion
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Modulo** | MGN-004 |
|
|
| **Nombre** | Multi-Tenancy |
|
|
| **Version** | 1.0 |
|
|
| **Fecha** | 2025-12-05 |
|
|
| **Autor** | System |
|
|
|
|
---
|
|
|
|
## Alcance
|
|
|
|
### En Alcance
|
|
|
|
- CRUD de Tenants (Platform Admin)
|
|
- Configuracion de Tenant (Settings)
|
|
- Aislamiento de datos (RLS)
|
|
- Guards: TenantGuard, LimitGuard, ModuleGuard
|
|
- Subscripciones y Planes
|
|
- Verificacion de limites
|
|
|
|
### Fuera de Alcance
|
|
|
|
- Integracion real con pasarela de pagos
|
|
- UI de plataforma admin
|
|
- Performance testing exhaustivo (otro documento)
|
|
|
|
---
|
|
|
|
## Casos de Prueba
|
|
|
|
### TP-TENANT-001: Crear Tenant
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario / Integracion |
|
|
| **Prioridad** | Alta |
|
|
| **US Relacionada** | US-MGN004-001 |
|
|
|
|
**Precondiciones:**
|
|
- Platform Admin autenticado
|
|
- Schema core_tenants existe
|
|
|
|
**Pasos:**
|
|
1. Enviar POST /api/v1/platform/tenants con datos validos
|
|
2. Verificar respuesta 201
|
|
3. Verificar tenant creado en BD
|
|
4. Verificar settings creados por defecto
|
|
|
|
**Datos de Prueba:**
|
|
```json
|
|
{
|
|
"name": "Empresa Test",
|
|
"slug": "empresa-test",
|
|
"subdomain": "test"
|
|
}
|
|
```
|
|
|
|
**Resultado Esperado:**
|
|
- Tenant creado con status "created"
|
|
- tenant_settings creado con valores vacios
|
|
- created_at y created_by populados
|
|
|
|
---
|
|
|
|
### TP-TENANT-002: Slug Duplicado
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Alta |
|
|
| **US Relacionada** | US-MGN004-001 |
|
|
|
|
**Precondiciones:**
|
|
- Existe tenant con slug "empresa-abc"
|
|
|
|
**Pasos:**
|
|
1. Intentar crear tenant con slug "empresa-abc"
|
|
2. Verificar respuesta 409 Conflict
|
|
|
|
**Resultado Esperado:**
|
|
- Error 409 con mensaje "Slug ya existe"
|
|
- No se crea nuevo tenant
|
|
|
|
---
|
|
|
|
### TP-TENANT-003: Transicion de Estados
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Alta |
|
|
| **US Relacionada** | US-MGN004-001 |
|
|
|
|
**Pasos:**
|
|
1. Crear tenant (status: created)
|
|
2. Iniciar trial (status: trial, trial_ends_at set)
|
|
3. Activar (status: active)
|
|
4. Suspender (status: suspended, suspended_at set)
|
|
5. Reactivar (status: active, suspended_at cleared)
|
|
6. Programar eliminacion (status: pending_deletion)
|
|
7. Restaurar (status: active, deletion_scheduled_at cleared)
|
|
|
|
**Resultado Esperado:**
|
|
- Cada transicion actualiza los campos correspondientes
|
|
- Campos de fecha se setean/limpian correctamente
|
|
|
|
---
|
|
|
|
### TP-TENANT-004: Actualizar Settings
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Media |
|
|
| **US Relacionada** | US-MGN004-002 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant existente
|
|
- Tenant Admin autenticado
|
|
|
|
**Pasos:**
|
|
1. GET /api/v1/tenant/settings (verificar defaults)
|
|
2. PATCH con company.companyName = "Nueva Empresa"
|
|
3. GET settings, verificar merge correcto
|
|
|
|
**Datos de Prueba:**
|
|
```json
|
|
{
|
|
"company": {
|
|
"companyName": "Nueva Empresa S.A."
|
|
}
|
|
}
|
|
```
|
|
|
|
**Resultado Esperado:**
|
|
- Solo company.companyName cambia
|
|
- Otros campos de company mantienen defaults
|
|
- Otras secciones no se modifican
|
|
|
|
---
|
|
|
|
### TP-TENANT-005: Merge con Defaults
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Media |
|
|
| **US Relacionada** | US-MGN004-002 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant con regional.defaultTimezone = "America/New_York"
|
|
- Default de plataforma regional.dateFormat = "DD/MM/YYYY"
|
|
|
|
**Pasos:**
|
|
1. GET /api/v1/tenant/settings
|
|
2. Verificar que timezone es "America/New_York" (override)
|
|
3. Verificar que dateFormat es "DD/MM/YYYY" (default)
|
|
|
|
**Resultado Esperado:**
|
|
- Valores del tenant sobrescriben defaults
|
|
- Valores no seteados usan defaults de plataforma
|
|
|
|
---
|
|
|
|
### TP-TENANT-006: RLS - Solo Ver Datos Propios
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Integracion / Seguridad |
|
|
| **Prioridad** | Critica |
|
|
| **US Relacionada** | US-MGN004-003 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant A con 100 productos
|
|
- Tenant B con 50 productos
|
|
- Usuario de Tenant A autenticado
|
|
|
|
**Pasos:**
|
|
1. GET /api/v1/products
|
|
2. Verificar que solo retorna productos de Tenant A
|
|
|
|
**Resultado Esperado:**
|
|
- Exactamente 100 productos retornados
|
|
- Ningun producto de Tenant B visible
|
|
- RLS filtra automaticamente
|
|
|
|
---
|
|
|
|
### TP-TENANT-007: RLS - Recurso de Otro Tenant No Accesible
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Integracion / Seguridad |
|
|
| **Prioridad** | Critica |
|
|
| **US Relacionada** | US-MGN004-003 |
|
|
|
|
**Precondiciones:**
|
|
- Producto "P-001" pertenece a Tenant B
|
|
- Usuario de Tenant A autenticado
|
|
|
|
**Pasos:**
|
|
1. GET /api/v1/products/P-001
|
|
2. Verificar respuesta 404
|
|
|
|
**Resultado Esperado:**
|
|
- Status 404 Not Found
|
|
- Mensaje generico "Recurso no encontrado"
|
|
- NO revelar que existe en otro tenant
|
|
|
|
---
|
|
|
|
### TP-TENANT-008: RLS - Query Sin Contexto
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario / Seguridad |
|
|
| **Prioridad** | Critica |
|
|
| **US Relacionada** | US-MGN004-003 |
|
|
|
|
**Precondiciones:**
|
|
- Datos en tabla users de multiples tenants
|
|
|
|
**Pasos:**
|
|
1. Ejecutar query directa sin SET app.current_tenant_id
|
|
2. SELECT * FROM core_users.users
|
|
|
|
**Resultado Esperado:**
|
|
- Query retorna 0 registros
|
|
- RLS previene acceso sin contexto
|
|
|
|
---
|
|
|
|
### TP-TENANT-009: TenantGuard - Tenant Suspendido
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Alta |
|
|
| **US Relacionada** | US-MGN004-003 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant en status "suspended"
|
|
- Usuario con token valido de ese tenant
|
|
|
|
**Pasos:**
|
|
1. Hacer cualquier request autenticado
|
|
2. Verificar respuesta 403
|
|
|
|
**Resultado Esperado:**
|
|
- Status 403 Forbidden
|
|
- Mensaje "Tenant suspendido: acceso denegado"
|
|
|
|
---
|
|
|
|
### TP-TENANT-010: TenantGuard - Trial Expirado
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Alta |
|
|
| **US Relacionada** | US-MGN004-003 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant en status "trial"
|
|
- trial_ends_at = hace 2 dias
|
|
|
|
**Pasos:**
|
|
1. Hacer cualquier request autenticado
|
|
2. Verificar respuesta 403
|
|
|
|
**Resultado Esperado:**
|
|
- Status 403 Forbidden
|
|
- Mensaje "Periodo de prueba expirado"
|
|
|
|
---
|
|
|
|
### TP-TENANT-011: LimitGuard - Limite de Usuarios
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Alta |
|
|
| **US Relacionada** | US-MGN004-004 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant con plan Starter (max 10 usuarios)
|
|
- 10 usuarios existentes
|
|
|
|
**Pasos:**
|
|
1. POST /api/v1/users (crear usuario #11)
|
|
2. Verificar respuesta 402
|
|
|
|
**Resultado Esperado:**
|
|
- Status 402 Payment Required
|
|
- Mensaje indica limite alcanzado
|
|
- Incluye upgradeOptions con planes disponibles
|
|
|
|
---
|
|
|
|
### TP-TENANT-012: LimitGuard - Bajo el Limite
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Media |
|
|
| **US Relacionada** | US-MGN004-004 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant con plan Starter (max 10 usuarios)
|
|
- 5 usuarios existentes
|
|
|
|
**Pasos:**
|
|
1. POST /api/v1/users (crear usuario #6)
|
|
2. Verificar respuesta 201
|
|
|
|
**Resultado Esperado:**
|
|
- Usuario creado exitosamente
|
|
- LimitGuard permite la operacion
|
|
|
|
---
|
|
|
|
### TP-TENANT-013: ModuleGuard - Modulo No Incluido
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Alta |
|
|
| **US Relacionada** | US-MGN004-004 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant con plan Starter (sin CRM)
|
|
- Endpoint de CRM con @CheckModule('crm')
|
|
|
|
**Pasos:**
|
|
1. GET /api/v1/crm/contacts
|
|
2. Verificar respuesta 402
|
|
|
|
**Resultado Esperado:**
|
|
- Status 402 Payment Required
|
|
- Mensaje "Modulo CRM no incluido en tu plan"
|
|
|
|
---
|
|
|
|
### TP-TENANT-014: Verificar Limite
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Media |
|
|
| **US Relacionada** | US-MGN004-004 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant con plan Professional (50 usuarios)
|
|
- 35 usuarios existentes
|
|
|
|
**Pasos:**
|
|
1. GET /api/v1/tenant/subscription/check-limit?type=users
|
|
2. Verificar respuesta
|
|
|
|
**Resultado Esperado:**
|
|
```json
|
|
{
|
|
"type": "users",
|
|
"current": 35,
|
|
"limit": 50,
|
|
"canAdd": true,
|
|
"remaining": 15
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### TP-TENANT-015: Upgrade de Plan
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Integracion |
|
|
| **Prioridad** | Alta |
|
|
| **US Relacionada** | US-MGN004-004 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant con plan Starter ($29/mes)
|
|
- Dia 15 del mes
|
|
|
|
**Pasos:**
|
|
1. POST /api/v1/tenant/subscription/upgrade con plan Professional
|
|
2. Verificar prorrateo calculado
|
|
3. Verificar plan actualizado
|
|
|
|
**Resultado Esperado:**
|
|
- Prorrateo: ($99 - $29) / 2 = $35
|
|
- Plan cambia a Professional
|
|
- Nuevos limites aplican inmediatamente
|
|
|
|
---
|
|
|
|
### TP-TENANT-016: Cancelar Subscripcion
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Media |
|
|
| **US Relacionada** | US-MGN004-004 |
|
|
|
|
**Precondiciones:**
|
|
- Tenant con subscripcion activa
|
|
|
|
**Pasos:**
|
|
1. POST /api/v1/tenant/subscription/cancel
|
|
2. Verificar subscripcion marcada para cancelar
|
|
|
|
**Resultado Esperado:**
|
|
- cancel_at_period_end = true
|
|
- canceled_at = timestamp actual
|
|
- Status sigue "active" hasta fin de periodo
|
|
|
|
---
|
|
|
|
### TP-TENANT-017: Platform Admin Switch Tenant
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Integracion |
|
|
| **Prioridad** | Media |
|
|
| **US Relacionada** | US-MGN004-001 |
|
|
|
|
**Precondiciones:**
|
|
- Platform Admin autenticado
|
|
- Tenant A y Tenant B existen
|
|
|
|
**Pasos:**
|
|
1. POST /api/v1/platform/switch-tenant/tenant-a-id
|
|
2. GET /api/v1/users (ve usuarios de Tenant A)
|
|
3. POST /api/v1/platform/switch-tenant/tenant-b-id
|
|
4. GET /api/v1/users (ve usuarios de Tenant B)
|
|
|
|
**Resultado Esperado:**
|
|
- Contexto cambia correctamente
|
|
- Datos de tenant correcto visibles
|
|
- Accion queda auditada
|
|
|
|
---
|
|
|
|
### TP-TENANT-018: Tenant Auto-Asignado en Creacion
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Tipo** | Unitario |
|
|
| **Prioridad** | Media |
|
|
| **US Relacionada** | US-MGN004-003 |
|
|
|
|
**Precondiciones:**
|
|
- Usuario de Tenant A autenticado
|
|
|
|
**Pasos:**
|
|
1. POST /api/v1/products sin tenant_id en body
|
|
2. Verificar producto creado
|
|
|
|
**Resultado Esperado:**
|
|
- Producto creado con tenant_id = Tenant A
|
|
- No se permite especificar tenant_id diferente
|
|
|
|
---
|
|
|
|
## Resumen de Casos de Prueba
|
|
|
|
| Categoria | Cantidad | Prioridad Critica | Prioridad Alta | Prioridad Media |
|
|
|-----------|----------|-------------------|----------------|-----------------|
|
|
| CRUD Tenants | 3 | 0 | 3 | 0 |
|
|
| Settings | 2 | 0 | 0 | 2 |
|
|
| RLS/Aislamiento | 5 | 3 | 2 | 0 |
|
|
| Guards | 3 | 0 | 2 | 1 |
|
|
| Subscripciones | 4 | 0 | 2 | 2 |
|
|
| Platform Admin | 1 | 0 | 0 | 1 |
|
|
| **Total** | **18** | **3** | **9** | **6** |
|
|
|
|
---
|
|
|
|
## Tests de Seguridad Adicionales
|
|
|
|
### SEC-001: Cross-Tenant Data Access
|
|
|
|
```typescript
|
|
describe('Cross-Tenant Security', () => {
|
|
it('should NOT allow access to other tenant resources', async () => {
|
|
// Crear datos en Tenant B
|
|
const productB = await createProduct(tenantBId);
|
|
|
|
// Autenticar como usuario de Tenant A
|
|
const tokenA = await login(userTenantA);
|
|
|
|
// Intentar acceder a recurso de Tenant B
|
|
const response = await request(app)
|
|
.get(`/api/v1/products/${productB.id}`)
|
|
.set('Authorization', `Bearer ${tokenA}`);
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should NOT allow UPDATE of other tenant resources', async () => {
|
|
const productB = await createProduct(tenantBId);
|
|
const tokenA = await login(userTenantA);
|
|
|
|
const response = await request(app)
|
|
.patch(`/api/v1/products/${productB.id}`)
|
|
.set('Authorization', `Bearer ${tokenA}`)
|
|
.send({ name: 'Hacked' });
|
|
|
|
expect(response.status).toBe(404);
|
|
|
|
// Verificar que no se modifico
|
|
const product = await getProduct(productB.id, tenantBId);
|
|
expect(product.name).not.toBe('Hacked');
|
|
});
|
|
|
|
it('should NOT allow DELETE of other tenant resources', async () => {
|
|
const productB = await createProduct(tenantBId);
|
|
const tokenA = await login(userTenantA);
|
|
|
|
const response = await request(app)
|
|
.delete(`/api/v1/products/${productB.id}`)
|
|
.set('Authorization', `Bearer ${tokenA}`);
|
|
|
|
expect(response.status).toBe(404);
|
|
|
|
// Verificar que no se elimino
|
|
const product = await getProduct(productB.id, tenantBId);
|
|
expect(product).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
### SEC-002: Tenant ID Injection
|
|
|
|
```typescript
|
|
describe('Tenant ID Injection Prevention', () => {
|
|
it('should NOT allow tenant_id in request body', async () => {
|
|
const tokenA = await login(userTenantA);
|
|
|
|
const response = await request(app)
|
|
.post('/api/v1/products')
|
|
.set('Authorization', `Bearer ${tokenA}`)
|
|
.send({
|
|
name: 'Product',
|
|
tenant_id: tenantBId, // Intento de inyeccion
|
|
});
|
|
|
|
expect(response.status).toBe(201);
|
|
// Verificar que se uso tenant del token, no del body
|
|
expect(response.body.tenant_id).toBe(tenantAId);
|
|
});
|
|
|
|
it('should NOT allow tenant_id modification via UPDATE', async () => {
|
|
const product = await createProduct(tenantAId);
|
|
const tokenA = await login(userTenantA);
|
|
|
|
const response = await request(app)
|
|
.patch(`/api/v1/products/${product.id}`)
|
|
.set('Authorization', `Bearer ${tokenA}`)
|
|
.send({
|
|
tenant_id: tenantBId, // Intento de cambiar tenant
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Cobertura de Codigo
|
|
|
|
| Componente | Objetivo |
|
|
|------------|----------|
|
|
| TenantsService | > 85% |
|
|
| TenantSettingsService | > 80% |
|
|
| SubscriptionsService | > 85% |
|
|
| TenantGuard | > 90% |
|
|
| LimitGuard | > 90% |
|
|
| ModuleGuard | > 90% |
|
|
| TenantContextMiddleware | > 80% |
|
|
| **Total Modulo** | **> 85%** |
|
|
|
|
---
|
|
|
|
## Matriz de Trazabilidad
|
|
|
|
| User Story | Casos de Prueba |
|
|
|------------|-----------------|
|
|
| US-MGN004-001 | TP-001, TP-002, TP-003, TP-017 |
|
|
| US-MGN004-002 | TP-004, TP-005 |
|
|
| US-MGN004-003 | TP-006, TP-007, TP-008, TP-009, TP-010, TP-018 |
|
|
| US-MGN004-004 | TP-011, TP-012, TP-013, TP-014, TP-015, TP-016 |
|
|
|
|
---
|
|
|
|
## Historial
|
|
|
|
| Version | Fecha | Autor | Cambios |
|
|
|---------|-------|-------|---------|
|
|
| 1.0 | 2025-12-05 | System | Creacion inicial con 18 casos |
|