Skip to Content
Plan de pruebas2. Aislamiento multi-tenant

2. Aislamiento multi-tenant

El módulo más crítico de este plan. Verifica que el fix de seguridad de octubre 2026 (company_id resuelto exclusivamente desde el token Paseto, nunca del cliente) funciona en la práctica — no solo que el código compile, sino que un usuario autenticado de la Empresa A no pueda, bajo ninguna circunstancia, ver, listar, editar ni borrar datos de la Empresa B.

Historial de ejecución

CicloFechaEjecutado porCasos ejecutadosResultado
1———Pendiente

Preparación (una sola vez por ciclo)

  1. Registrar Empresa A (admin.a@test.com) y verificar la cuenta.
  2. Registrar Empresa B (admin.b@test.com) y verificar la cuenta.
  3. En cada empresa, crear: una tienda, una categoría, un impuesto, un producto, un cliente (contacto) y una venta simple con ese producto. Esto puebla stores, categories, taxes, products, inventories, contacts, sales, sale_items y payments en ambas empresas.
  4. Anotar los IDs numéricos exactos que devolvió el backend al crear cada recurso en la Empresa A (ej. “producto id=14”, “venta id=7”) — se necesitan para los casos de “acceso directo por ID” contra la Empresa B.

Caso base por módulo

Para cada módulo de la tabla, se ejecutan tres variantes con la sesión de la Empresa B:

  • (a) Listado: GET de la lista del recurso — no debe aparecer nada de la Empresa A.
  • (b) Lectura directa por ID: GET /recurso/:id usando el ID real de un recurso de la Empresa A anotado en el paso 4 — debe responder 404 (nunca 403 con datos parciales, nunca 200 con el recurso).
  • (c) Escritura por ID: PUT/DELETE /recurso/:id sobre ese mismo ID ajeno — debe fallar igual que (b), y los datos de la Empresa A no deben cambiar (verificar entrando con la sesión A después).
CasoMóduloRecurso usado para (b)/(c)Resultado esperado
MT-01salesventa de A404 en (b) y (c); venta de A intacta
MT-02paymentspago de A (asociado a la venta de A)404 en (b)/(c)
MT-03productsproducto de A404 en (b)/(c); además (a) no debe listar el producto de A aunque se use GET /products sin filtro de tienda
MT-04inventory (inventories)registro de inventario de A404 en (b)/(c)
MT-05categoriescategoría de A404 en (b)/(c)
MT-06taxesimpuesto de A404 en (b)/(c)
MT-07contacts (clientes/proveedores)contacto de A404 en (b)/(c) — este endpoint no tenía ningún filtro antes del fix de hoy; es el caso más importante de confirmar
MT-08adjustmentsajuste de inventario de A404 en (b)/(c)
MT-09purchasescompra de A404 en (b)/(c)
MT-10expensesgasto de A404 en (b)/(c)
MT-11return_ordersdevolución de A404 en (b)/(c)
MT-12bookings / booking_servicesreserva y servicio de A404 en (b)/(c)
MT-13gift_cardsgift card de A404 en (b)/(c); además probar canjear/consumir saldo de la gift card de A desde B (POST /gift-cards/:id/logs) — no debe poder restarle saldo a una gift card ajena
MT-14registerscaja de A404 en (b)/(c)
MT-15promotionspromoción de A404 en (b)/(c)
MT-16discount-policiespolítica de descuento de A404 en (b)/(c)
MT-17modifier-groupsgrupo de modificadores de A404 en (b)/(c)
MT-18custom-fieldscampo personalizado de A404 en (b)/(c)
MT-19groups / permissions (roles)grupo/rol de A404 en (b)/(c)
MT-20storestienda de A404 en (b)/(c)

Resultado obtenido de MT-01 a MT-20: Pendiente — llenar fila por fila al ejecutar, no en bloque.

Casos especiales (no siguen el patrón de la tabla)

CP MT-21: Dashboard sin filtro de tienda

  • Pasos: sesión de Empresa B → GET /api/v1/dashboard sin parámetro store_id.
  • Resultado esperado: totales de ventas/gastos/compras/ventas recientes solo de la Empresa B — en cero si B no ha creado ventas ese mes, nunca sumando lo de A.
  • Resultado obtenido: Pendiente.

CP MT-22: Reportes (los 14 endpoints de /api/v1/reports/*) sin filtro de tienda

  • Pasos: probar al menos reports/sales, reports/customers (el que no tenía ningún filtro antes del fix, ni siquiera por tienda) y reports/registers desde la sesión de B.
  • Resultado esperado: cero filas o solo las de B; nunca una fila de A.
  • Resultado obtenido: Pendiente.

CP MT-23: Activity log (auditoría)

  • Pasos: generar varias acciones registradas en la Empresa A (crear venta, crear usuario) → desde B, GET /api/v1/activity-logs.
  • Resultado esperado: B no ve ninguna entrada de A.
  • Resultado obtenido: Pendiente.

CP MT-24: Asistente IA — get_inventory y create_customer

  • Pasos: desde el chat de la Empresa B, pedirle al asistente “muéstrame el inventario” (sin mencionar tienda) y “crea un cliente llamado Juan Pérez”.
  • Resultado esperado: el inventario mostrado es solo el de B; el cliente creado debe quedar asociado a la Empresa B (verificar en Ajustes → Clientes de B, y confirmar que no aparece en el listado de clientes de A).
  • Resultado obtenido: Pendiente.

CP MT-25: Endpoint de cocina (KDS) — confirmar que el fix no rompió la función real

  • Objetivo: PATCH /api/v1/sales/:id/kitchen-status es intencionalmente público (pantallas de cocina sin login). Verificar que, aun así, solo afecta la venta correcta y no permite cambiar el estado de cocina de una venta de otra empresa si se adivina el ID.
  • Pasos: crear una venta held en la Empresa A, anotar su ID → desde cualquier sesión (o sin sesión), hacer PATCH /api/v1/sales/<ese_id>/kitchen-status con {"kitchen_status":"ready"}.
  • Resultado esperado: como este endpoint es deliberadamente sin autenticación, sí debe poder cambiar el estado (es el diseño conocido: “dispositivos de cocina confiables en red local”) — pero confirmar que al menos actualiza la venta correcta (por ID) y no una al azar, y que el valor realmente cambia en la base (antes del fix de hoy, esto fallaba silenciosamente para todas las ventas).
  • Resultado obtenido: Pendiente.
  • Nota de riesgo a discutir con el usuario: este endpoint sigue siendo de facto “cualquiera con el ID puede cambiar el estado de cocina de cualquier venta de cualquier empresa, sin login”. Es el trade-off aceptado por diseño, pero vale la pena confirmarlo explícitamente como riesgo conocido y no como descubrimiento nuevo.

CP MT-26: Carta online pública (/public/menu/:slug)

  • Objetivo: esta ruta es pública por diseño (clientes finales sin cuenta). Confirmar que el slug de una tienda solo expone el menú de esa tienda, nunca datos de otra tienda/empresa aunque se manipule el store_id en el body del pedido.
  • Pasos: tomar el menu_slug de una tienda de la Empresa A → hacer un pedido (POST /public/menu/:slug/orders) enviando en el body un store_id que en realidad pertenece a una tienda de la Empresa B.
  • Resultado esperado: el pedido debe quedar asociado a la tienda/empresa real del slug de la URL, ignorando cualquier store_id que venga en el body del cliente.
  • Resultado obtenido: Pendiente.

Notas de usabilidad y sugerencias

Se completa durante la ejecución. Aquí también se debe anotar si algún mensaje de error “filtra” información (ej. un 403 con detalle en vez de un 404 genérico, que le confirmaría a un atacante que el ID existe pero es de otra empresa).

© 2026 ClickStock