Version: 1.0
Fecha: 25 de Marzo, 2026
Contexto: Fase 5 del plan de migracion backend — definicion del plan de pruebas por capa, tests de integracion con SimappeOAuth2 y SimappeAdmin, tests de aislamiento multi-tenant, y entorno de desarrollo integrado
Arquitecto: Carlos Alberto Torres Camargo
Clasificacion: Interno — Arquitectura
Definir la estrategia completa de pruebas para validar que la migracion del backend DataVault al ecosistema Simappe funciona correctamente en todos los niveles: unitario, integracion, E2E, seguridad y rendimiento.
┌───────────┐
│ E2E │ 5 tests
│ (Slow) │ Via Gateway + Angular
├───────────┤
│Integration│ 15 tests
│ (Medium) │ FastAPI + SimappeAdmin + PostgreSQL
├───────────┤
│ Unit │ 30+ tests
│ (Fast) │ Funciones puras, mocks
└───────────┘
Archivo: tests/test_auth.py
import pytest
from unittest.mock import patch
from app.utils.auth import validate_simappe_jwt, SimappeUserSession
# JWT de prueba generado con la clave de Simappe
VALID_JWT_PAYLOAD = {
"id": 42,
"username": "carlos.torres",
"firstName": "Carlos",
"lastName": "Torres",
"email": "carlos@centrica.com",
"customerId": 100,
"companyId": 200,
"subsidiaryId": None,
"environment": "dev",
"connectionContext": "BUSINESS",
"roles": [{"id": 1, "name": "ADMIN"}],
"exp": 9999999999, # Fecha futura lejana
}
class TestValidateSimappeJwt:
def test_valid_token_extracts_all_claims(self):
"""JWT valido debe retornar SimappeUserSession completo."""
with patch("app.utils.auth.jwt.decode", return_value=VALID_JWT_PAYLOAD):
session = validate_simappe_jwt("fake.token.here")
assert session is not None
assert session.id == 42
assert session.username == "carlos.torres"
assert session.customer_id == 100
assert session.company_id == 200
assert session.environment == "dev"
assert "ADMIN" in session.roles
def test_expired_token_returns_none(self):
"""JWT expirado debe retornar None."""
from jose import JWTError
with patch("app.utils.auth.jwt.decode", side_effect=JWTError("expired")):
session = validate_simappe_jwt("expired.token")
assert session is None
def test_token_without_customer_id_returns_none(self):
"""JWT sin customerId debe retornar None (campo obligatorio)."""
payload = {**VALID_JWT_PAYLOAD, "customerId": 0}
with patch("app.utils.auth.jwt.decode", return_value=payload):
session = validate_simappe_jwt("token.without.customer")
assert session is None
def test_token_without_username_returns_none(self):
"""JWT sin username debe retornar None."""
payload = {**VALID_JWT_PAYLOAD, "username": ""}
with patch("app.utils.auth.jwt.decode", return_value=payload):
session = validate_simappe_jwt("token.without.username")
assert session is None
def test_has_company_property(self):
"""has_company debe ser True solo si companyId > 0."""
with patch("app.utils.auth.jwt.decode", return_value=VALID_JWT_PAYLOAD):
session = validate_simappe_jwt("token")
assert session.has_company is True
payload_no_company = {**VALID_JWT_PAYLOAD, "companyId": None}
with patch("app.utils.auth.jwt.decode", return_value=payload_no_company):
session = validate_simappe_jwt("token")
assert session.has_company is False
def test_roles_extracted_from_objects(self):
"""Roles como objetos [{name: 'ADMIN'}] se extraen correctamente."""
with patch("app.utils.auth.jwt.decode", return_value=VALID_JWT_PAYLOAD):
session = validate_simappe_jwt("token")
assert session.roles == ["ADMIN"]
def test_roles_extracted_from_strings(self):
"""Roles como strings ['ADMIN'] se extraen correctamente."""
payload = {**VALID_JWT_PAYLOAD, "roles": ["ADMIN", "USER"]}
with patch("app.utils.auth.jwt.decode", return_value=payload):
session = validate_simappe_jwt("token")
assert session.roles == ["ADMIN", "USER"]
Archivo: tests/test_roles.py
import pytest
from app.utils.auth import SimappeUserSession
from app.utils.security import (
get_datavault_role,
has_minimum_role,
is_superadmin,
)
def make_session(roles: list[str]) -> SimappeUserSession:
return SimappeUserSession(
id=1, username="test", customer_id=1, roles=roles,
)
class TestRoleMapping:
def test_super_admin_maps_to_superadmin(self):
session = make_session(["SUPER_ADMIN"])
assert get_datavault_role(session) == "superadmin"
def test_admin_maps_to_admin_empresa(self):
session = make_session(["ADMIN"])
assert get_datavault_role(session) == "admin_empresa"
def test_user_maps_to_archivista(self):
session = make_session(["USER"])
assert get_datavault_role(session) == "archivista"
def test_auditor_maps_to_auditor(self):
session = make_session(["AUDITOR"])
assert get_datavault_role(session) == "auditor"
def test_viewer_maps_to_consulta(self):
session = make_session(["VIEWER"])
assert get_datavault_role(session) == "consulta"
def test_multiple_roles_takes_highest(self):
"""Si tiene USER y ADMIN, debe tomar ADMIN (mayor jerarquia)."""
session = make_session(["USER", "ADMIN"])
assert get_datavault_role(session) == "admin_empresa"
def test_empty_roles_defaults_to_consulta(self):
session = make_session([])
assert get_datavault_role(session) == "consulta"
def test_unknown_role_defaults_to_consulta(self):
session = make_session(["UNKNOWN_ROLE"])
assert get_datavault_role(session) == "consulta"
class TestRoleHierarchy:
def test_superadmin_has_all_roles(self):
session = make_session(["SUPER_ADMIN"])
assert has_minimum_role(session, "consulta") is True
assert has_minimum_role(session, "auditor") is True
assert has_minimum_role(session, "archivista") is True
assert has_minimum_role(session, "admin_empresa") is True
assert has_minimum_role(session, "superadmin") is True
def test_consulta_only_has_consulta(self):
session = make_session(["VIEWER"])
assert has_minimum_role(session, "consulta") is True
assert has_minimum_role(session, "auditor") is False
assert has_minimum_role(session, "archivista") is False
def test_is_superadmin(self):
assert is_superadmin(make_session(["SUPER_ADMIN"])) is True
assert is_superadmin(make_session(["ADMIN"])) is False
assert is_superadmin(make_session(["USER"])) is False
Archivo: tests/test_simappe_client.py
import pytest
import httpx
from unittest.mock import AsyncMock, patch
from app.services.simappe_client import (
SimappeAdminClient,
TenantDatabaseConfig,
SimappeAdminError,
)
MOCK_DB_CONFIG_RESPONSE = {
"id": 1,
"code": "VAULT_TEST",
"name": "Test Tenant DB",
"host": "localhost",
"port": 5432,
"database": "vault_test",
"schema": "public",
"username": "vault_user",
"password": "vault_pass",
"type": "POSTGRESQL",
"clientId": 100,
"companyId": 200,
"environment": "dev",
"maxPoolSize": 10,
"minIdle": 2,
"connectionTimeout": 30000,
}
class TestSimappeAdminClient:
@pytest.fixture
def client(self):
return SimappeAdminClient()
@pytest.mark.asyncio
async def test_get_config_success(self, client):
"""Llamada exitosa a SimappeAdmin retorna config valida."""
mock_response = AsyncMock()
mock_response.status_code = 200
mock_response.json.return_value = MOCK_DB_CONFIG_RESPONSE
with patch.object(client, "_get_client") as mock_http:
mock_http.return_value.get = AsyncMock(return_value=mock_response)
config = await client.get_tenant_db_config("fake.jwt.token")
assert config.database == "vault_test"
assert config.host == "localhost"
assert config.asyncpg_url == "postgresql+asyncpg://vault_user:vault_pass@localhost:5432/vault_test"
@pytest.mark.asyncio
async def test_cache_returns_same_result(self, client):
"""Segunda llamada con mismo token debe venir del cache."""
mock_response = AsyncMock()
mock_response.status_code = 200
mock_response.json.return_value = MOCK_DB_CONFIG_RESPONSE
with patch.object(client, "_get_client") as mock_http:
mock_get = AsyncMock(return_value=mock_response)
mock_http.return_value.get = mock_get
config1 = await client.get_tenant_db_config("same.token")
config2 = await client.get_tenant_db_config("same.token")
# Solo una llamada HTTP (la segunda viene del cache)
assert mock_get.call_count == 1
assert config1.database == config2.database
@pytest.mark.asyncio
async def test_404_raises_error(self, client):
"""Tenant sin config BD debe retornar error 404."""
mock_response = AsyncMock()
mock_response.status_code = 404
with patch.object(client, "_get_client") as mock_http:
mock_http.return_value.get = AsyncMock(return_value=mock_response)
with pytest.raises(SimappeAdminError) as exc_info:
await client.get_tenant_db_config("no.config.token")
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_network_error_raises_503(self, client):
"""Error de red al contactar SimappeAdmin debe retornar 503."""
with patch.object(client, "_get_client") as mock_http:
mock_http.return_value.get = AsyncMock(
side_effect=httpx.RequestError("Connection refused")
)
with pytest.raises(SimappeAdminError) as exc_info:
await client.get_tenant_db_config("network.error.token")
assert exc_info.value.status_code == 503
@pytest.mark.asyncio
async def test_incomplete_config_raises_error(self, client):
"""Config sin host o database debe fallar."""
bad_response = {**MOCK_DB_CONFIG_RESPONSE, "host": "", "database": ""}
mock_response = AsyncMock()
mock_response.status_code = 200
mock_response.json.return_value = bad_response
with patch.object(client, "_get_client") as mock_http:
mock_http.return_value.get = AsyncMock(return_value=mock_response)
with pytest.raises(SimappeAdminError):
await client.get_tenant_db_config("bad.config.token")
Archivo: tests/test_database.py
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from app.database import TenantDatabaseManager
from app.services.simappe_client import TenantDatabaseConfig
def make_config(company_id: int) -> TenantDatabaseConfig:
return TenantDatabaseConfig(
id=company_id,
code=f"TENANT_{company_id}",
name=f"Tenant {company_id}",
host="localhost",
port=5432,
database=f"vault_{company_id}",
schema="public",
username="user",
password="pass",
db_type="POSTGRESQL",
client_id=100,
company_id=company_id,
subsidiary_id=None,
environment="dev",
)
class TestTenantDatabaseManager:
@pytest.fixture
def manager(self):
return TenantDatabaseManager(max_pools=3)
@pytest.mark.asyncio
async def test_creates_pool_for_new_tenant(self, manager):
"""Primer request para un tenant crea un nuevo pool."""
config = make_config(200)
with patch("app.database.create_async_engine") as mock_engine:
mock_engine.return_value = MagicMock()
factory = await manager.get_session_factory(config)
assert manager.active_pools == 1
assert factory is not None
@pytest.mark.asyncio
async def test_reuses_pool_for_same_tenant(self, manager):
"""Segundo request para mismo tenant reutiliza el pool."""
config = make_config(200)
with patch("app.database.create_async_engine") as mock_engine:
mock_engine.return_value = MagicMock()
factory1 = await manager.get_session_factory(config)
factory2 = await manager.get_session_factory(config)
assert manager.active_pools == 1
assert factory1 is factory2
@pytest.mark.asyncio
async def test_evicts_lru_when_max_reached(self, manager):
"""Cuando se alcanza max_pools, se evicta el menos usado."""
with patch("app.database.create_async_engine") as mock_engine:
mock_engine.return_value = MagicMock()
mock_engine.return_value.dispose = AsyncMock()
# Crear 3 pools (max)
await manager.get_session_factory(make_config(1))
await manager.get_session_factory(make_config(2))
await manager.get_session_factory(make_config(3))
assert manager.active_pools == 3
# Crear pool 4 → evicta pool 1 (LRU)
await manager.get_session_factory(make_config(4))
assert manager.active_pools == 3
@pytest.mark.asyncio
async def test_different_tenants_get_different_pools(self, manager):
"""Tenants diferentes deben tener pools diferentes."""
with patch("app.database.create_async_engine") as mock_engine:
mock_engine.return_value = MagicMock()
factory_a = await manager.get_session_factory(make_config(200))
factory_b = await manager.get_session_factory(make_config(300))
assert manager.active_pools == 2
assert factory_a is not factory_b
Prerequisito: SimappeOAuth2 y SimappeAdmin corriendo (Docker Compose).
# tests/integration/test_full_flow.py
import pytest
import httpx
SIMAPPE_OAUTH_URL = "http://localhost:8080"
SIMAPPE_ADMIN_URL = "http://localhost:8081"
DATAVAULT_URL = "http://localhost:8000"
class TestFullIntegrationFlow:
@pytest.fixture
async def jwt_token(self):
"""Obtener JWT real de SimappeOAuth2."""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{SIMAPPE_OAUTH_URL}/oauth/token",
data={
"grant_type": "password",
"username": "admin",
"password": "admin123",
"client_id": "simappe-client",
"client_secret": "secret",
},
)
assert response.status_code == 200
return response.json()["access_token"]
@pytest.mark.asyncio
async def test_datavault_accepts_simappe_jwt(self, jwt_token):
"""DataVault debe aceptar JWT de SimappeOAuth2."""
async with httpx.AsyncClient() as client:
response = await client.get(
f"{DATAVAULT_URL}/api/auth/me",
headers={"Authorization": f"Bearer {jwt_token}"},
)
assert response.status_code == 200
data = response.json()
assert "username" in data
assert "customer_id" in data
@pytest.mark.asyncio
async def test_datavault_resolves_tenant_db(self, jwt_token):
"""DataVault debe resolver la BD del tenant via SimappeAdmin."""
async with httpx.AsyncClient() as client:
response = await client.get(
f"{DATAVAULT_URL}/api/repository/files",
headers={"Authorization": f"Bearer {jwt_token}"},
)
# 200 si hay archivos, 404 si no hay — ambos son validos
assert response.status_code in (200, 404)
@pytest.mark.asyncio
async def test_invalid_token_returns_401(self):
"""Token invalido debe retornar 401."""
async with httpx.AsyncClient() as client:
response = await client.get(
f"{DATAVAULT_URL}/api/auth/me",
headers={"Authorization": "Bearer invalid.token.here"},
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_no_token_returns_401(self):
"""Request sin token debe retornar 401."""
async with httpx.AsyncClient() as client:
response = await client.get(
f"{DATAVAULT_URL}/api/repository/files",
)
assert response.status_code in (401, 403)
# tests/integration/test_tenant_isolation.py
class TestTenantIsolation:
"""
CRITICO: Verifica que los datos de un tenant NO son visibles
desde otro tenant.
"""
@pytest.fixture
async def tenant_a_token(self):
"""JWT para tenant A (companyId=200)."""
# ... obtener token con companyId=200
@pytest.fixture
async def tenant_b_token(self):
"""JWT para tenant B (companyId=300)."""
# ... obtener token con companyId=300
@pytest.mark.asyncio
async def test_tenant_a_cannot_see_tenant_b_data(
self, tenant_a_token, tenant_b_token
):
"""Datos creados en tenant A no deben ser visibles en tenant B."""
async with httpx.AsyncClient() as client:
# Crear dato en tenant A
create_response = await client.post(
f"{DATAVAULT_URL}/api/ingest/projects",
headers={"Authorization": f"Bearer {tenant_a_token}"},
json={"name": "Proyecto Secreto Tenant A", "description": "Test"},
)
assert create_response.status_code == 201
project_id = create_response.json()["id"]
# Intentar leer desde tenant B
read_response = await client.get(
f"{DATAVAULT_URL}/api/ingest/projects/{project_id}",
headers={"Authorization": f"Bearer {tenant_b_token}"},
)
# Debe ser 404 (no existe en BD de tenant B)
assert read_response.status_code == 404
@pytest.mark.asyncio
async def test_tenant_a_list_excludes_tenant_b(
self, tenant_a_token, tenant_b_token
):
"""Listado de tenant A no debe incluir datos de tenant B."""
async with httpx.AsyncClient() as client:
# Crear dato en tenant B
await client.post(
f"{DATAVAULT_URL}/api/ingest/projects",
headers={"Authorization": f"Bearer {tenant_b_token}"},
json={"name": "Proyecto Tenant B", "description": "Test"},
)
# Listar desde tenant A
list_response = await client.get(
f"{DATAVAULT_URL}/api/ingest/projects",
headers={"Authorization": f"Bearer {tenant_a_token}"},
)
projects = list_response.json()
# Ningun proyecto de tenant B debe aparecer
for project in projects:
assert "Tenant B" not in project.get("name", "")
# docker-compose.test-simappe.yml
# Entorno completo para pruebas de integracion
version: "3.8"
services:
# ─── SIMAPPE STACK ───────────────────────────────────
simappe-oauth2:
image: simappe-oauth2:latest
ports: ["8080:8080"]
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://simappe-db:5432/simappe_oauth2
JWT_EXPIRATION_TIME: 3600000
depends_on:
simappe-db:
condition: service_healthy
simappe-admin:
image: simappe-admin:latest
ports: ["8081:8081"]
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://simappe-db:5432/simappe_admin
depends_on:
simappe-db:
condition: service_healthy
simappe-oauth2:
condition: service_started
simappe-db:
image: postgres:15-alpine
ports: ["5434:5432"]
environment:
POSTGRES_USER: simappe
POSTGRES_PASSWORD: simappe_pass
volumes:
- ./tests/fixtures/init-simappe-db.sql:/docker-entrypoint-initdb.d/01-init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U simappe"]
interval: 5s
timeout: 5s
retries: 5
# ─── DATAVAULT BACKEND ───────────────────────────────
datavault-backend:
build: ./backend
ports: ["8000:8000"]
environment:
SIMAPPE_ADMIN_URL: http://simappe-admin:8081
SIMAPPE_OAUTH2_URL: http://simappe-oauth2:8080
SIMAPPE_JWT_SECRET: "${SIMAPPE_JWT_SECRET}"
LEGACY_JWT_ENABLED: "false"
TENANT_DB_CACHE_TTL_MINUTES: "5"
MAX_TENANT_POOLS: "5"
DATABASE_URL: postgresql+asyncpg://vault_user:vault_pass@tenant-db-1:5432/vault_tenant_1
depends_on:
simappe-admin:
condition: service_started
tenant-db-1:
condition: service_healthy
tenant-db-2:
condition: service_healthy
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
# ─── BASES DE DATOS POR TENANT ──────────────────────
tenant-db-1:
image: postgres:15-alpine
ports: ["5435:5432"]
environment:
POSTGRES_DB: vault_tenant_1
POSTGRES_USER: vault_user
POSTGRES_PASSWORD: vault_pass
volumes:
- ./tests/fixtures/init-vault-schema.sql:/docker-entrypoint-initdb.d/01-schema.sql
- ./tests/fixtures/seed-tenant-1.sql:/docker-entrypoint-initdb.d/02-seed.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vault_user -d vault_tenant_1"]
interval: 5s
timeout: 5s
retries: 5
tenant-db-2:
image: postgres:15-alpine
ports: ["5436:5432"]
environment:
POSTGRES_DB: vault_tenant_2
POSTGRES_USER: vault_user
POSTGRES_PASSWORD: vault_pass
volumes:
- ./tests/fixtures/init-vault-schema.sql:/docker-entrypoint-initdb.d/01-schema.sql
- ./tests/fixtures/seed-tenant-2.sql:/docker-entrypoint-initdb.d/02-seed.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vault_user -d vault_tenant_2"]
interval: 5s
timeout: 5s
retries: 5
tests/fixtures/init-simappe-db.sql:
-- Configuracion de BD en SimappeAdmin para que resuelva tenant 1 y tenant 2
-- Debe insertarse en la tabla database_config_v2 de simappe_admin
CREATE DATABASE simappe_admin;
CREATE DATABASE simappe_oauth2;
\c simappe_admin
INSERT INTO database_config_v2 (
client_id, company_id, environment, host, port, database_name,
schema_name, username, password, type, is_global, status,
max_pool_size, min_idle, connection_timeout, connection_context
) VALUES
(100, 200, 'dev', 'tenant-db-1', 5432, 'vault_tenant_1',
'public', 'vault_user', 'vault_pass', 'POSTGRESQL', false, 'ACTIVE',
10, 2, 30000, 'BUSINESS'),
(100, 300, 'dev', 'tenant-db-2', 5432, 'vault_tenant_2',
'public', 'vault_user', 'vault_pass', 'POSTGRESQL', false, 'ACTIVE',
10, 2, 30000, 'BUSINESS');
Nebula Vault - Simappe Integration
├── 01. Auth Flow
│ ├── Login via SimappeOAuth2 (pre-request: guarda {{token}})
│ ├── Get Current User (/api/auth/me)
│ └── Invalid Token (expect 401)
│
├── 02. Tenant Resolution
│ ├── Resolve DB Config (via SimappeAdmin)
│ ├── List Files Tenant 1 (expect data from vault_tenant_1)
│ └── List Files Tenant 2 (expect data from vault_tenant_2)
│
├── 03. CRUD Operations
│ ├── Create Project
│ ├── Upload File
│ ├── List Projects
│ ├── Get Project Detail
│ └── Delete Project
│
├── 04. Authorization
│ ├── Admin Only Endpoint (with USER token → expect 403)
│ ├── Admin Only Endpoint (with ADMIN token → expect 200)
│ └── Superadmin Endpoint (with ADMIN token → expect 403)
│
├── 05. Tenant Isolation
│ ├── Create in Tenant A
│ ├── Read from Tenant B (expect 404)
│ └── List from Tenant B (expect 0 results from A)
│
└── 06. Edge Cases
├── Expired Token (expect 401)
├── No Company Selected (expect 403)
├── SimappeAdmin Down (expect 503 or cached response)
└── Health Check (/actuator/health → expect UP)
{
"simappe_oauth_url": "http://localhost:8080",
"simappe_admin_url": "http://localhost:8081",
"datavault_url": "http://localhost:8000",
"username_tenant_a": "user_a@centrica.com",
"password_tenant_a": "password123",
"username_tenant_b": "user_b@centrica.com",
"password_tenant_b": "password123",
"token_tenant_a": "",
"token_tenant_b": ""
}
// Pre-request script para requests que requieren auth
const oauthUrl = pm.environment.get("simappe_oauth_url");
const username = pm.environment.get("username_tenant_a");
const password = pm.environment.get("password_tenant_a");
pm.sendRequest({
url: `${oauthUrl}/oauth/token`,
method: "POST",
header: { "Content-Type": "application/x-www-form-urlencoded" },
body: {
mode: "urlencoded",
urlencoded: [
{ key: "grant_type", value: "password" },
{ key: "username", value: username },
{ key: "password", value: password },
{ key: "client_id", value: "simappe-client" },
{ key: "client_secret", value: "secret" },
],
},
}, function (err, res) {
if (!err && res.code === 200) {
const token = res.json().access_token;
pm.environment.set("token_tenant_a", token);
}
});
# ─── Levantar entorno de pruebas ───
docker compose -f docker-compose.test-simappe.yml up -d
# ─── Esperar a que todos los servicios esten listos ───
# (verificar health endpoints)
curl http://localhost:8080/actuator/health # SimappeOAuth2
curl http://localhost:8081/actuator/health # SimappeAdmin
curl http://localhost:8000/actuator/health # DataVault
# ─── Ejecutar tests unitarios ───
cd backend
pytest tests/test_auth.py tests/test_roles.py tests/test_simappe_client.py tests/test_database.py -v
# ─── Ejecutar tests de integracion ───
pytest tests/integration/ -v --timeout=30
# ─── Ejecutar tests de aislamiento ───
pytest tests/integration/test_tenant_isolation.py -v
# ─── Ejecutar Postman collection ───
newman run postman/Nebula_Vault_Simappe_Integration.json \
--environment postman/env_local.json \
--reporters cli,htmlextra
# ─── Limpiar entorno ───
docker compose -f docker-compose.test-simappe.yml down -v
| # | Criterio | Metrica | Estado |
|---|---|---|---|
| CA-1 | JWT de SimappeOAuth2 se valida correctamente | 7/7 tests unitarios passing | Pendiente |
| CA-2 | Roles se mapean correctamente | 9/9 tests unitarios passing | Pendiente |
| CA-3 | BD se resuelve via SimappeAdmin | 5/5 tests unitarios passing | Pendiente |
| CA-4 | Pool dinamico funciona | 4/4 tests unitarios passing | Pendiente |
| CA-5 | Auth E2E funciona | 3/3 tests integracion passing | Pendiente |
| CA-6 | Aislamiento multi-tenant verificado | 2/2 tests aislamiento passing | Pendiente |
| CA-7 | Postman collection completa | 15+ requests exitosos | Pendiente |
| CA-8 | Zero data leaks entre tenants | Test de aislamiento 100% | Pendiente |
| CA-9 | Latencia resolucion tenant < 200ms | Medicion con cache activo | Pendiente |
| CA-10 | Health endpoint compatible Simappe | /actuator/health retorna UP |
Pendiente |
| Version | Fecha | Autor | Descripcion |
|---|---|---|---|
| 1.0.0 | 2026-03-25 | Carlos Torres | Creacion del plan de pruebas y validacion |