# Validación gRPC del DAO — Reporte de pruebas

**Fecha:** 12 de agosto de 2026
**Componente:** `nube_services_dao` (csharp-dao)
**Entorno validado:** QA, vía `kubectl port-forward` al puerto 8440
**Rama:** `feature/backups-velero-tracking`

---

## 1. Resumen ejecutivo

Se construyó y ejecutó una suite automatizada de caja negra contra el servicio DAO ya desplegado en QA. Habla gRPC con los clientes tipados generados de los protos del repositorio; no abre conexión a la base de datos ni levanta el servidor en proceso.

| Métrica | Valor |
|---|---:|
| Pruebas ejecutadas | **367** |
| Exitosas | **362** |
| Fallidas | **5** |

**Los 5 fallos son defectos reales del producto**, no fallas de las pruebas. Se clasifican en tres rpcs declarados en los contratos pero nunca implementados, y dos consultas que devuelven vacío sobre datos que sí existen.

### Severidad

| # | Hallazgo | Severidad | Origen |
|---|---|---|---|
| H-1 | `getUserVCD` filtra por organización hardcodeada | **Alta** | Código C# |
| H-2 | `getServicesToDeploy` descarta el filtro recibido | **Alta** | Código C# |
| H-3 | `getStatusCatalog` nunca aplica su filtro | **Media** | Stored procedure |
| H-4 | `GetUserPermissions` enmascara `NotFound` como `Internal` | **Media** | Código C# |
| H-5 | 3 rpcs declarados sin implementación | **Media** | Contrato vs. código |

H-2 y H-4 no provienen de un fallo de la suite: se identificaron al investigar la causa raíz de los fallos observados.

---

## 2. Alcance

### Qué se validó

Los **34 rpcs de lectura** de los cinco servicios expuestos:

| Servicio | Rpcs totales | De lectura | Cubiertos |
|---|---:|---:|---:|
| `DynamicCrudService` | 13 | 5 | 5 |
| `servicioDAOService` | 30 | 19 | 19 |
| `administrationService` | 7 | 3 | 3 |
| `ProfileService` | 6 | 4 | 4 |
| `CatalogService` | 3 | 3 | 3 |
| **Total** | **59** | **34** | **34** |

### Qué NO se validó

**Los 25 rpcs de escritura quedan fuera de cobertura por decisión de alcance.** La suite no invoca `Create`, `Update`, `Delete`, `Upsert`, `BulkCreate`, `BulkUpdate`, `BulkDelete`, `ExecuteBatch` ni ningún `set*` de la capa legacy. Esto la hace idempotente y segura de correr contra QA cuantas veces se quiera, pero deja sin verificar la mitad de la superficie —incluidas las operaciones donde un defecto tiene consecuencias destructivas.

Verificar escrituras requiere un entorno donde se pueda escribir sin consecuencias: un schema copia, o pruebas en proceso envueltas en transacción con rollback.

---

## 3. Cómo se ejecuta

```bash
# 1. Exponer el servicio
kubectl port-forward svc/csharp-dao-service 8440:8440 -n <namespace>

# 2. Correr la suite
make test-grpc
```

El endpoint por defecto es `https://127.0.0.1:8440`. Para apuntar a otro:

```bash
make test-grpc DAO_ENDPOINT=http://127.0.0.1:50051
```

**Nota sobre TLS:** el certificado del servidor está emitido para su nombre dentro del cluster, así que a través de un port-forward la suite cifra pero no valida el nombre del certificado — equivalente a `grpcurl -insecure`.

Si el port-forward no está activo, la suite aborta en el primer segundo con el comando de `kubectl` en el mensaje, en lugar de acumular timeouts.

---

## 4. Metodología

### 4.1 CRUD dinámico — 328 pruebas

Ocho verificaciones aplicadas a los **43 modelos** del `ModelRegistry`. Cuatro de ellas requieren clave primaria simple, y 4 modelos tienen clave compuesta (`role_menu_access`, `role_service_access`, `user_role`, `users_profiles`), de ahí el conteo: 43×4 + 39×4 = 328.

| Verificación | Qué afirma |
|---|---|
| `GetFields` | Cada campo declara nombre, tipo y columna |
| `GetAll` | Respeta el límite; no devuelve campos fuera de los declarados por `GetFields` |
| `Search` vs `Count` | **Dos rpcs distintos reportan el mismo total** |
| Paginación | Páginas consecutivas no comparten registros; el total es estable entre ellas |
| `orderBy` | Las secuencias asc y desc vienen efectivamente ordenadas |
| `GetById` | Con un id tomado de `GetAll`, devuelve ese mismo registro |
| Filtro `eq` | Sobre la clave primaria devuelve exactamente un registro |
| Proyección | `columns` devuelve solo lo pedido, ni un campo más |

El diseño evita datos hardcodeados: cada aserción se construye a partir de lo que el propio servicio acaba de responder. Un id de `GetAll` alimenta el `GetById`; los nombres de campo salen de `GetFields`.

### 4.2 Rutas de error — 10 pruebas

El contrato de `DynamicCrudService` es devolver los errores dentro de la respuesta (`success=false`, `status="error"`) y **no** propagar excepciones gRPC. Se verifica con modelo inexistente, columna inválida, campo de filtro inválido, operador inválido, `order_by` inválido, id inexistente y paginación negativa. Un `RpcException` en cualquiera de estos casos falla la prueba.

### 4.3 Capa legacy — 29 pruebas

Estos rpcs resuelven contra stored procedures. Dos decisiones de diseño importan para leer los resultados:

**Los parámetros se descubren, no se hardcodean.** Antes de cada llamada, la prueba consulta el CRUD dinámico para obtener un id real de la tabla correspondiente. Esto es necesario porque varios procedures interpretan el parámetro nulo como *"no coincide con nada"* en lugar de *"sin filtro"* — llamarlos en vacío no ejerce su lógica y produce falsos positivos. Si la tabla origen está vacía, el caso termina temprano en lugar de fallar.

**Hay dos niveles de exigencia.** La capa legacy atrapa las excepciones de MySQL y las convierte en `NotFound`, de modo que *un procedure inexistente se ve desde fuera exactamente igual que una tabla vacía*. Para poder distinguirlos:

- Los rpcs que consultan **una sola tabla** con un id recién leído de esa misma tabla tratan `NotFound` como **fallo**: la fila existe, así que "sin resultados" significa que la consulta está rota.
- Los que dependen de joins conservan el criterio laxo, porque un join puede legítimamente no producir filas.

Esta distinción es la que destapó H-1 y H-3.

---

## 5. Hallazgos

### H-1 · `getUserVCD` filtra por una organización hardcodeada

**Severidad:** Alta · **Ubicación:** `ServicesDAO/Controllers/ServicesDAO.cs:378`

#### Síntoma

`getUserVCD` invocado solo con `user_id` devuelve `NotFound` aunque el usuario exista.

#### Cómo se validó

Se tomó un usuario real del CRUD dinámico y se comparó el comportamiento con y sin `org_id`:

```bash
# El usuario 462 existe y pertenece a la organización 714
grpcurl -insecure -d '{"model":"user_vcd","id":"462","columns":["UserVcdID","OrgId"]}' \
  127.0.0.1:8440 cloudservicescrud.DynamicCrudService/GetById
# → { "OrgId": 714, "UserVcdID": 462 }

# Solo con user_id
grpcurl -insecure -d '{"user_id":462}' \
  127.0.0.1:8440 cloudServiceDAO.servicioDAOService/getUserVCD
# → ERROR  Code: NotFound   "No results for user information"

# Con user_id + su org_id real
grpcurl -insecure -d '{"user_id":462,"org_id":714}' \
  127.0.0.1:8440 cloudServiceDAO.servicioDAOService/getUserVCD
# → { "users": [ { "userVCDId": 462, "username": "gustavo.victoria", "org_id": 714 } ] }
```

#### Causa raíz

```csharp
_param[0] = new MySqlParameter("@user_name",  String.IsNullOrEmpty(userName) ? DBNull.Value : userName);
_param[1] = new MySqlParameter("@public_id",  public_id > 0 ? public_id : DBNull.Value);
_param[2] = new MySqlParameter("@user_id",    user_id   > 0 ? user_id   : DBNull.Value);
_param[3] = new MySqlParameter("@org_id",     org_id    > 0 ? org_id    : 1);   // ← aquí
```

Los tres primeros parámetros usan `DBNull.Value` para expresar "sin filtro". El cuarto pone **`1`**. El procedure `spskGetUserVCD` maneja correctamente el nulo (`(org_id = @org_id AND @org_id IS NOT NULL) OR (@org_id IS NULL)`), pero nunca recibe uno.

#### Qué revisar para corregir

1. Cambiar la línea 378 a `org_id > 0 ? org_id : DBNull.Value`, alineándola con las otras tres.
2. **Revisar quién consume este rpc antes de cambiarlo.** Si algún cliente depende hoy de que la organización 1 sea el default implícito, el arreglo cambia su comportamiento. Buscar llamadas a `getUserVCD` sin `org_id` en los servicios consumidores.
3. Revisar el resto de `ServicesDAO.cs` por el mismo patrón: es un archivo con muchos métodos que arman parámetros a mano y el criterio de "sin filtro" no está aplicado de forma uniforme.

---

### H-2 · `getServicesToDeploy` descarta el filtro que recibe

**Severidad:** Alta · **Ubicación:** `ServicesDAO/Controllers/ServicesDAO.cs:922`

#### Síntoma

El parámetro `status` se comporta al revés: cuando se envía vacío el procedure recibe cadena vacía, y cuando se envía un valor real recibe `NULL`.

#### Cómo se validó

Inspección del código tras investigar por qué el rpc respondía `Unknown: Exception was thrown by handler` con parámetros vacíos.

#### Causa raíz

```csharp
_param[0] = new MySqlParameter("@str_status", String.IsNullOrEmpty(status) ? status : DBNull.Value);
```

El ternario está invertido. La intención era `String.IsNullOrEmpty(status) ? DBNull.Value : status`.

Consecuencias:

| Entrada | Se envía al SP | Efecto |
|---|---|---|
| `status = ""` | `""` | Compara `requests_queue.status = ''` → no coincide con nada |
| `status = "pending"` | `NULL` | **Se ignora el filtro**; devuelve las filas con status nulo |

Cuando no hay resultados, el método lanza una excepción que se atrapa y devuelve `null`, y la capa gRPC la convierte en un error sin detalle útil.

#### Qué revisar para corregir

1. Invertir el ternario en la línea 922.
2. Verificar el contrato esperado con los consumidores: hoy nadie puede estar filtrando por status correctamente, así que la lógica que dependa de este rpc probablemente tiene un workaround construido encima.
3. El patrón `if (rows > 0) return data; else throw new Exception(...)` seguido de `catch { return null; }` aparece en varios métodos del archivo. Convierte "sin resultados" en excepción y luego la silencia — vale la pena unificarlo.

---

### H-3 · `getStatusCatalog` nunca aplica su filtro

**Severidad:** Media · **Ubicación:** procedure `spskGetStatusCatalog` (schema `cloud_services`)

#### Síntoma

`getStatusCatalog` devuelve `NotFound` con un `status_catalog_id` que existe en la tabla.

#### Cómo se validó

La prueba `GetStatusCatalog_Responde` obtiene un `StatusCatalogID` real vía el CRUD dinámico y lo pasa al rpc legacy. Confirmado además de forma directa:

```bash
grpcurl -insecure -d '{"status_catalog_id":1}' \
  127.0.0.1:8440 cloudServiceDAO.servicioDAOService/getStatusCatalog
# → ERROR  Code: NotFound
```

#### Causa raíz

En el cuerpo del procedure:

```sql
and
(
    (`status_catalog`.`StatusCatalogID` = status_catalog_id and status_catalog_id is not null)
    or (status_catalog_id is not null)     -- ← debería ser IS NULL
)
and
(
    (`status_catalog`.`id_type` = id_type and id_type is not null)
    or (id_type is not null)                -- ← mismo error
)
```

El patrón correcto, usado en el resto de los procedures del schema, es `or (param is null)` para expresar "sin filtro". Con `is not null` la condición se vuelve inútil: con parámetro nulo ambos lados son falsos y no devuelve nada.

El procedure `spskGetTermsConditions` tiene exactamente el mismo defecto en sus dos filtros.

#### Qué revisar para corregir

1. Corregir ambas cláusulas a `is null` en `spskGetStatusCatalog`.
2. Aplicar la misma corrección a `spskGetTermsConditions`.
3. `spskGetTermsConditions` además referencia `terms_conditions.active`, y la columna real se llama **`bactive`** — ese procedure no puede ejecutarse con éxito en su estado actual.

---

### H-4 · `GetUserPermissions` enmascara sus errores

**Severidad:** Media · **Ubicación:** `server/Services/ProfileService.cs:207` y `:223`

#### Síntoma

Un usuario sin permisos produce `Internal` en lugar de `NotFound`. El cliente recibe:

```
Internal: Status(StatusCode="NotFound", Detail="Permisos de usuario no encontrados")
```

es decir, el status correcto viajando como texto dentro del status equivocado.

#### Cómo se validó

Observado en la primera corrida de la suite; confirmado por inspección del código.

#### Causa raíz

```csharp
if (result == null)
{
    throw new RpcException(new Status(StatusCode.NotFound, "Permisos de usuario no encontrados"));
}
// ...
catch (System.Exception ex)          // ← atrapa también el RpcException de arriba
{
    _logger.LogError(ex, "Error en GetUserPermissions");
    throw new RpcException(new Status(StatusCode.Internal, ex.Message));
}
```

`RpcException` hereda de `Exception`, así que el `catch` genérico lo captura y lo reenvuelve.

#### Qué revisar para corregir

1. Agregar `catch (RpcException) { throw; }` antes del `catch` general.
2. Revisar los demás métodos de `ProfileService.cs` y de la capa de servicios: el patrón `catch (Exception) → throw RpcException(Internal)` se repite y tiene el mismo problema en cualquier método que lance un status intencionado.
3. Un cliente que distinga "no encontrado" de "error del servidor" hoy no puede hacerlo con este rpc.

---

### H-5 · Tres rpcs declarados en los contratos pero sin implementación

**Severidad:** Media · **Ubicación:** protos vs. servicios

#### Síntoma

El servidor responde `Unimplemented` a tres métodos publicados en los `.proto`.

| Rpc | Declarado en | Estado en el código |
|---|---|---|
| `StatusSupplyService` | `cloudServiceDAO.proto:491` | Sin referencias en el código |
| `ServiceStatusService` | `cloudServiceDAO.proto:492` | Sin referencias en el código |
| `GetServicesCatalogEF` | `EntityFrameworkDAOServices.proto:60` | `CatalogService.cs` solo implementa los otros dos métodos |

#### Cómo se validó

Las pruebas reciben `StatusCode.Unimplemented`. Se descartó que fuera imagen desactualizada mediante búsqueda en el código fuente:

```bash
grep -rn "StatusSupplyService\|ServiceStatusService" server/ --include=*.cs | grep -v models/
# → sin resultados

grep -n "public override" server/Services/CatalogService.cs
# → solo GetAllowedProductTypes y GetAllowedServices
```

También se confirmó por reflection que los cinco servicios sí están registrados en el servidor desplegado:

```bash
grpcurl -insecure 127.0.0.1:8440 list
# → cloudServiceDAO.servicioDAOService, cloudservicescrud.DynamicCrudService,
#   administrationservices.administrationService, administrationservices.ProfileService,
#   EntityFrameworkDAOServices.CatalogService, grpc.health.v1.Health, ...
```

El problema es de métodos faltantes, no de servicios ausentes.

#### Qué revisar para corregir

Decidir para cada uno cuál de las dos vías aplica:

1. **Implementarlos**, si algún consumidor los espera. `GetServicesCatalogEF` parece ser la versión Entity Framework del legacy `getServicesCatalog`, así que probablemente quedó a medias.
2. **Retirarlos de los protos**, si son contrato muerto. Un rpc publicado que siempre responde `Unimplemented` es una trampa para quien lea el contrato.

Antes de retirar, verificar que ningún cliente los invoque: un `Unimplemented` en producción es indistinguible de una falla de despliegue.

---

## 6. Observaciones adicionales

Detectadas durante el análisis; no las cubre la suite.

**Mensajes de error copiados entre operaciones.** Varios rpcs devuelven texto que no corresponde a lo que hacen:

```bash
grpcurl -insecure -d '{"idTerms_conditions":1}' \
  127.0.0.1:8440 cloudServiceDAO.servicioDAOService/getTermsandConditions
# → NotFound: "No results for ConsumptionHistory information"

grpcurl -insecure -d '{"status_catalog_id":1}' \
  127.0.0.1:8440 cloudServiceDAO.servicioDAOService/getStatusCatalog
# → NotFound: "No results for vdc information"
```

Dificulta el diagnóstico en producción, donde el mensaje es a menudo lo único disponible.

**Procedures invocados que no existen en el schema de QA.** Cruce entre los 38 procedures que invoca `ServicesDAO.cs` y los 50 presentes en la base:

| Procedure | Invocado en |
|---|---|
| `spskGetTermsAndConditions` | `ServicesDAO.cs:793` — en la base se llama `spskGetTermsConditions`, sin el "And" |
| `getInformationReport` | `ServicesDAO.cs:574` |

**Otros procedures con defectos** (detectados por inspección del dump, no ejercitados por la suite):

- `spupUpdatevdc_VCD` — la rama de UPDATE hace `WHERE id = vcd_id`, pero el parámetro se llama `vdc_id`. Dispara el `EXIT HANDLER` y devuelve `'error'` siempre.
- `spupLicenses` — `where idlicenses=idlicenses` compara la columna consigo misma; la tabla temporal se llena con todas las licencias.
- `spinupSetTermsAndConditions` — `where terms_conditions.idterms_conditions=idterms_conditions`, misma colisión; el `IF EXISTS` siempre da verdadero y nunca entra a la rama de INSERT.

**Quince procedures huérfanos** existen en QA sin que ningún código los invoque, incluido `spup_setConsumptionHistoryIdentificator2test`.

**`MapGrpcService<ProfileInternalService>()`** en `server/Services/GrpcServerHost.cs:82` registra una clase cuya herencia de la base generada está comentada (`ProfileInternalService.cs:14`), igual que el servicio en el proto. `MapGrpcService<T>` lanza `InvalidOperationException` cuando no encuentra el método de binding, lo que impediría el arranque. La imagen desplegada en QA es anterior a esta rama y no lo incluye — se confirmó por reflection que `ProfileInternalService` no aparece entre los servicios expuestos.

---

## 7. Cobertura no alcanzada

Limitaciones conocidas de estos resultados:

**Las escrituras.** 25 de 59 rpcs, incluidas todas las operaciones destructivas. Es la brecha más importante.

**Rpcs cuya tabla origen está vacía en QA.** La suite termina el caso temprano en lugar de fallar. El más relevante es `getTermsandConditions`: invoca un procedure inexistente, pero `terms_conditions` no tiene filas en QA, así que el defecto no se manifiesta y la prueba pasa. El mismo punto ciego aplica a cualquier tabla sin datos.

**Rpcs que dependen de joins.** Se verifica que respondan sin reventar, pero no se afirma nada sobre el contenido: un join puede legítimamente no producir filas y no hay forma de distinguirlo de una consulta rota desde fuera.

**El contenido de los datos.** Fuera de las verificaciones cruzadas del CRUD dinámico, la suite no valida reglas de negocio ni la corrección de los valores devueltos.

---

## 8. Recomendaciones

1. **Corregir H-1 y H-2 primero.** Son errores de una línea cada uno, con impacto directo en resultados que hoy salen mal de forma silenciosa.
2. **Auditar `ServicesDAO.cs` completo** por el patrón de parámetros. Los dos hallazgos altos son del mismo tipo en un archivo de 1095 líneas donde cada método arma sus parámetros a mano; es improbable que sean los únicos.
3. **Decidir el destino de los 3 rpcs sin implementar** antes de que alguien construya encima de un contrato muerto.
4. **Habilitar la validación de escrituras.** Requiere un entorno desechable — un schema copia con `mysqldump --routines`, o pruebas en proceso envueltas en transacción con rollback. Sin esto, la mitad de la superficie del servicio permanece sin verificar.
5. **Integrar la suite al pipeline.** Hoy el `Jenkinsfile` construye, publica y despliega a `daas-stg-qa` sin ejecutar ninguna prueba.
