Ir al contenido

Explorar la API con Bruno

La API se explora con Bruno: un cliente HTTP cuyas colecciones son archivos de texto versionados en el repo, no un espacio en la nube de alguien.

La colección vive en api/bruno/aquazaku/.

Carpeta Qué prueba
01-Auth Señal de vida, login y perfil con permisos resueltos
02-Usuarios Listar, crear, asignar roles y la protección del último admin
03-Auditoria Consulta, filtro de denegados y validación de filtros
04-Productos Catálogo, piso de precio, auditoría del cambio y un pos recibiendo 403
05-Stock Lotes, ajuste, descarte, el libro y un contador recibiendo 403
06-Insumos Alta, entrada en unidades y en kilos, la conversión rechazada sin equivalencia, y un seller recibiendo 403
07-Produccion El cierre del día con sus cuatro escritos, la reposición sin cantidad, la reconciliación que no escribe, y un pos que puede anotar pero no ajustar
08-Clientes El dígito de verificación calculado, el cruce CC/NIT que advierte sin bloquear, el crédito rechazado sin verificación, la desactivación que devuelve bases y botellones al stock físico, y un contador recibiendo 403
09-Ventas El precio congelado, el piso que no se perfora, la anulación que devuelve al mismo lote, la corrección que revierte los botellones como la anulación, el cobro que no puede pasarse de la deuda, y un contador recibiendo 403
10-Retornables La ley de conservación del parque, la entrega que escribe dos filas con signo opuesto, la base que se presta a una dirección, el daño que genera recargo, la venta que despacha un envase y lo deja a nombre del cliente, y un seller que mira y no opera
11-Proveedores La compra que escribe el documento y el inventario juntos, el crédito modelado sin ejercerse, el aviso de vencidas que se apaga al pagar, y un pos que compra pero no da de alta proveedores
12-Sesion Cierre de sesión y que la credencial deje de servir

Las carpetas llevan número porque el orden importa: el login deja la cookie que usan los requests siguientes.

Renumerarla cada vez es más trabajo que dejarla fija, y es a propósito: el número es lo que hace visible el orden en el árbol de archivos. Un 99-Sesion que nunca se mueve esconde que hay un orden. :::

El único lugar donde otro rol pega contra api/

Sección titulada «El único lugar donde otro rol pega contra api/»

Seis carpetas cambian de sesión a mitad de camino y vuelven a admin al terminar.

04-Productos entra como el pos que creó 02-Usuarios, verifica que no pueda crear productos (403) y que sí pueda leer el catálogo (200).

05-Stock crea su propio contador, y no por gusto: el usuario de 02-Usuarios termina con pos + seller, y como los roles se suman (RN-ACC-01), el pos puede ajustar stock. Ese usuario no sirve para probar un rechazo. El contador es el testigo externo —mira todo, no modifica nada— y es el rol correcto para verificar que lectura y escritura están separadas de verdad.

06-Insumos entra como un seller: no toca la planta, así que no ve insumos.

07-Produccion entra como un pos, y es la carpeta donde la separación se ve mejor. El pos sí puede anotar que llegó agua —es un hecho que observa parado en la planta— y no puede ajustar un saldo que no cuadra. Son dos permisos y no uno porque corregir un saldo no es contar un hecho: es decidir cuál de dos números es el bueno, y un registro que quien opera puede cuadrar solo deja de servir como registro.

08-Clientes entra como un contador: ve la cartera —la necesita para saber cuánto debe cada uno— y no puede registrar ni verificar. Verificar un documento es afirmar que se lo tuvo en la mano, y el contador no está en el mostrador ni en la calle.

09-Ventas vuelve a entrar como contador, y por una razón distinta: acá el alcance no es «ve o no ve», es propio contra todo. Un pos ve y anula sus ventas y no las de otro, y eso lo recorta scopedCondition a partir de la sesión. Es la primera carpeta donde el alcance —y no el permiso— es lo que se está probando.

Los tests unitarios ya verifican que la matriz le niegue el permiso, pero esos no atraviesan un socket. Los dos peores bugs de M0 —el header Origin y el logout faltante— vivían justamente en costuras entre capas, donde ninguna suite unitaria mira por diseño.

El request 13-Volver-como-admin no es ceremonia: sin él, 05-Sesion cerraría la sesión del pos y su verificación probaría algo distinto de lo que dice probar.

Ventana de terminal
cd api/bruno/aquazaku && bru run . -r --env local

El entorno local deja adminPassword como variable secreta: no está en el archivo. Se pasa al correr, o se completa desde la interfaz de Bruno.

Ventana de terminal
bru run . -r --env local --env-var adminPassword=tu-contraseña

Cada request lleva aserciones. Algunas verifican el camino feliz; otras están para que fallar sea el resultado correcto:

2-Usuarios/04-Ultimo-admin (409 Conflict)
✓ responde 409
✓ el código dice cuál es el problema
✓ el mensaje explica qué hacer, no solo que falló

Ese request intenta quitarle el rol admin al único administrador activo. Si alguna vez devuelve 200, no es un éxito: es que se rompió RN-ACC-06 y el sistema quedó a un click de ser inadministrable.

Otro par que vale la pena mirar juntos: 2-Usuarios/04 genera un acceso denegado, y 3-Auditoria/02-Solo-denegados lo busca en la bitácora. Entre los dos verifican el ciclo completo de RN-ACC-04 — la acción ocurre, queda registrada y se puede encontrar.

La colección lo pone sola, en un script de nivel colección:

req.setHeader("Origin", bru.getEnvVar("webOrigin"));

No es opcional. Better-Auth rechaza con 403 MISSING_OR_NULL_ORIGIN toda petición que cambie estado y llegue sin él. Está en la colección y no en cada request para que ninguno nuevo nazca sin él — el mismo problema nos rompió todo el login una vez.

El workflow de api/ corre la colección contra un servidor de verdad, después de migrar y sembrar. Es la única verificación del proyecto que atraviesa un socket real: el resto de los tests usan app.inject(), que no pasa por la red.

Esa diferencia no es teórica. El bug del header Origin vivía justo ahí.

  1. Crear el .bru en la carpeta que corresponda, con un seq que respete el orden de dependencias.
  2. Escribirle tests. Un request sin aserciones documenta la forma de la petición, pero no verifica nada.
  3. Correr la colección local antes de abrir el pull request.

Cinco formas de romper la colección sin tocar api/

Sección titulada «Cinco formas de romper la colección sin tocar api/»

El 22-sep-2026 la colección quedó con 42 peticiones rojas, y api-ci con ella. Ninguna causa era del sistema. Están todas acá porque las cinco son fáciles de repetir.

08-Clientes terminaba con dos pedidos que entran como contador y nunca volvía a admin. Toda la carpeta 09-Ventas arrancaba con esa sesión y caía con 403 — y detrás, media colección.

Lo notable es que ya existía una petición para exactamente eso, con su documentación diciendo «sin esto, 09-Ventas arrancaría con la sesión del pos». Alguien agregó pedidos al final de la carpeta y heredó la deuda sin enterarse.

Es configuración de axios:

// ❌ el pedido sale VACÍO y api/ contesta 400 de Zod
await bru.sendRequest({ url, method: "POST", body: JSON.stringify(datos) });
// ✅
const r = await bru.sendRequest({ url, method: "POST", data: (datos) });
r.data.id; // la respuesta también es `.data`, no `.body`

Los pedidos normales llevan la sesión solos. sendRequest no: sale sin cookie y devuelve 401, el script revienta, y con él todos los pedidos que venían después.

La colección guarda la cookie en un script:post-response a nivel colección —no del login— para que siga al rol actual: la colección cambia de usuario varias veces, y capturarla una sola vez apuntaría a la sesión equivocada desde el segundo cambio.

headers: { "content-type": "application/json", cookie: bru.getVar("cookieSesion") }

4 · Un POST con content-type: application/json y sin cuerpo

Sección titulada «4 · Un POST con content-type: application/json y sin cuerpo»

Fastify lo rechaza con 400. Si la ruta no lee ningún cuerpo, hay que mandar data: ({}) igual.

La colección es una narración sobre estado global. Un script de preparación que compra botellones rompe a 10-Retornables/01-La-bodega-arranca-vacia, que afirma que el parque arranca en cero.

Cuando un pedido necesita ensuciar algo compartido, va después de todo lo que necesita eso limpio — aunque su carpeta natural esté antes.


Dos condiciones, y el orden entre ellas importa.

La base se recrea, y el servidor se levanta DESPUÉS

Sección titulada «La base se recrea, y el servidor se levanta DESPUÉS»

Recrear la base debajo de un servidor vivo deja su pool apuntando a una base que ya no existe. Los pedidos fallan por eso y no por el código.

Medido en un mismo commit, cambiando solo el orden: 42, 48 y 134 fallas en tres corridas. Con esos números casi reporto que había roto seis peticiones que nunca rompí.

Ventana de terminal
# 1. base nueva 2. migrar y sembrar 3. recién ahí, el servidor

La configuración api-bruno de .claude/launch.json levanta en el 3003 contra aquazaku_bruno, que no comparte con vitest. Antes se usaba la base de tests, y eso obligaba a elegir: o se siembra —y el siguiente vitest run la trunca— o se corre la suite y Bruno se queda sin admin.

Lo que da verde en tu máquina puede fallar en CI

Sección titulada «Lo que da verde en tu máquina puede fallar en CI»

El caso 13-Reportes/02-El-extracto comparaba el día de la planta (UTC−5) contra el día UTC del movimiento. Entre las 19:00 de Bogotá y la medianoche son días distintos.

Fallaba cinco horas al día y pasaba diecinueve. Diez corridas locales cayeron fuera de esa ventana; CI la agarró a las 21:37 de la planta.