204 lines
16 KiB
Bash
204 lines
16 KiB
Bash
#!/usr/bin/env bash
|
|
# Pruebas del 28-oct: compras.py — las órdenes de compra del cotizador dentro del centro de mando.
|
|
# Arranca app.py con carpetas, htpasswd y base SQLite temporales (como tests-1027.sh). No toca el servidor.
|
|
# Uso: bash backend/tests-1028.sh <ruta-a-hbox-docs> [puerto]
|
|
set -u
|
|
REPO=${1:?ruta a hbox-docs}; PORT=${2:-8098}
|
|
T=$(mktemp -d); SEC="secreto-de-pruebas-1028"
|
|
export ACTAS_DIR=$T/actas HTPASSWD_FILE=$T/htpasswd HTPASSWD_LOCK=$T/.lock SESION_COOKIE_INSEGURA=1 \
|
|
ACTAS_ADMINS=leo PORT INCIDENCIAS_DIR=$T/incidencias INVENTARIO_DIR=$T/inventario TAREAS_DIR=$T/tareas \
|
|
EQUIPO_DIR=$T/equipo COMPRAS_DB=$T/drogheda.db COTIZADOR_SECRETO=$SEC
|
|
mkdir -p $ACTAS_DIR $T/equipo $T/tareas $T/inventario
|
|
${PYTHON:-python3} - "$T" <<'EOF'
|
|
import sys, os, json
|
|
t = sys.argv[1]
|
|
for u, n in [("leo", "Leo"), ("adriana", "Adriana"), ("jesush", "Jesús Honorato"), ("alan", "Alán"),
|
|
("miguel", "Miguel"), ("antonio", "Antonio"), ("jesus", "Jesús Romero"), ("claude", "Claude")]:
|
|
json.dump({"id": u, "nombre": n, "area": "operaciones", "activo": True},
|
|
open(os.path.join(t, "equipo", u + ".json"), "w"))
|
|
EOF
|
|
PY=${PYTHON:-python3}
|
|
$PY -c 'import flask, sqlite3' 2>/dev/null || { echo "!! $PY no tiene flask (si acabas de cambiar de versión de"; \
|
|
echo " Python, se quedó atrás). Cualquiera de las dos:"; \
|
|
echo " pip3 install --break-system-packages flask"; \
|
|
echo " python3 -m venv ~/.venv-hbox && ~/.venv-hbox/bin/pip -q install flask \\"; \
|
|
echo " && PYTHON=~/.venv-hbox/bin/python bash backend/tests-1028.sh \"$REPO\""; exit 2; }
|
|
if command -v lsof >/dev/null && lsof -nP -iTCP:$PORT -sTCP:LISTEN >/dev/null 2>&1; then
|
|
echo "!! el puerto $PORT ya está ocupado. Dame otro: bash backend/tests-1028.sh \"$REPO\" 8123"; exit 2
|
|
fi
|
|
$PY $REPO/backend/app.py > $T/app.log 2>&1 &
|
|
APP=$!; trap "kill $APP 2>/dev/null" EXIT
|
|
U="http://127.0.0.1:$PORT"
|
|
# Esperar a que conteste (hasta 15 s): si no arranca, el resto de las pruebas no dice nada útil.
|
|
arriba=""
|
|
for _ in $(seq 1 30); do
|
|
if curl -fsS -o /dev/null "$U/api/salud" 2>/dev/null; then arriba=si; break; fi
|
|
kill -0 $APP 2>/dev/null || break
|
|
sleep 0.5
|
|
done
|
|
if [[ -z "$arriba" ]]; then
|
|
echo "!! app.py no arrancó en el puerto $PORT. Su bitácora:"; echo "────────────"
|
|
sed -n '1,40p' $T/app.log; echo "────────────"
|
|
echo "Pistas: falta flask en $PY · el puerto está ocupado · o un módulo de backend/ truena al cargar."
|
|
exit 2
|
|
fi
|
|
ok=0; fail=0
|
|
check() { if [[ "$3" == *"$2"* ]]; then ok=$((ok+1)); echo " ✓ $1"; else fail=$((fail+1)); echo " ✕ $1 — esperaba «$2», obtuve: ${3:0:240}"; fi; }
|
|
no() { if [[ "$3" != *"$2"* ]]; then ok=$((ok+1)); echo " ✓ $1"; else fail=$((fail+1)); echo " ✕ $1 — NO debía traer «$2»: ${3:0:240}"; fi; }
|
|
as() { curl -s -H "X-Remote-User: $1" -H "Content-Type: application/json" "${@:2}"; }
|
|
|
|
# La hora y la firma se calculan aparte para que la cabecera y lo firmado coincidan exactamente.
|
|
# firma_t(cuerpo [secreto] [desfase]) -> la hora ISO con zona · firma_f(... [hora]) -> el hex del HMAC
|
|
firma_t() { $PY -c '
|
|
import sys, datetime
|
|
desf = int(sys.argv[1] or 0)
|
|
print((datetime.datetime.now().astimezone() + datetime.timedelta(seconds=desf)).replace(microsecond=0).isoformat())
|
|
' "${3:-0}"; }
|
|
firma_f() { $PY -c '
|
|
import sys, hmac, hashlib
|
|
sec_def, cuerpo, sec, t = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
|
print(hmac.new((sec or sec_def).encode(), t.encode() + b"\n" + cuerpo.encode(), hashlib.sha256).hexdigest())
|
|
' "$SEC" "$1" "${2:-}" "${4:-$(firma_t "$1" "${2:-}" "${3:-0}")}"; }
|
|
# Las cabeceras se pasan en dos variables (nada de mapfile: el bash 3.2 de macOS no lo trae).
|
|
post() { local cuerpo="$1"; local t f
|
|
t=$(firma_t "$cuerpo" "${2:-}" "${3:-0}"); f=$(firma_f "$cuerpo" "${2:-}" "${3:-0}" "$t")
|
|
curl -s -H "Content-Type: application/json" -H "X-Hbox-Tiempo: $t" -H "X-Hbox-Firma: sha256=$f" \
|
|
-d "$cuerpo" $U/api/integracion/cotizador/evento; }
|
|
get() { local t f
|
|
t=$(firma_t ""); f=$(firma_f "" "" 0 "$t")
|
|
curl -s -H "X-Hbox-Tiempo: $t" -H "X-Hbox-Firma: sha256=$f" "$U$1"; }
|
|
|
|
echo "## arranque"
|
|
check "módulos cargados sin error" "" "$(grep -i 'no se cargó' $T/app.log)"
|
|
check "la base se creó" "drogheda.db" "$(ls $T/)"
|
|
|
|
echo
|
|
echo "## ítems del inventario (los que ligan los renglones)"
|
|
as leo -d '{"codigo":"SOP-RTG","descripcion":"Soporte para RTG","categoria":"instalacion","unidad":"pza","por_instalacion":2}' $U/api/inventario > /dev/null
|
|
as leo -d '{"codigo":"CBL-10","descripcion":"Cable calibre 10","categoria":"cable","unidad":"m"}' $U/api/inventario > /dev/null
|
|
check "dos ítems dados de alta" '"total":2' "$(as leo $U/api/inventario)"
|
|
|
|
echo
|
|
echo "## firma"
|
|
check "sin firma: 401" '"error":"Firma inv' "$(curl -s -H 'Content-Type: application/json' -d '{}' $U/api/integracion/cotizador/evento)"
|
|
check "firma con otro secreto: 401" '"error":"Firma inv' "$(post '{"evento_id":"x","tipo":"oc.pagada","oc":{"folio":"OC-1","sitio":"ZLO"}}' 'otro-secreto')"
|
|
check "firma de hace 10 min: 401" '"error":"Firma inv' "$(post '{"evento_id":"x","tipo":"oc.pagada","oc":{"folio":"OC-1","sitio":"ZLO"}}' '' -600)"
|
|
check "items pide firma" '"ok":true' "$(get /api/integracion/inventario/items)"
|
|
check "items trae los códigos" 'SOP-RTG' "$(get /api/integracion/inventario/items)"
|
|
check "el navegador no entra a integración" '"error":"Firma inv' "$(as leo $U/api/integracion/inventario/items)"
|
|
|
|
echo
|
|
echo "## oc.autorizada"
|
|
OC1='{"evento_id":"OC-2026-0012:oc.autorizada:1","tipo":"oc.autorizada","cuando":"2026-10-28T10:15:00-06:00","quien":"tania@helgen.tech","prueba":false,"oc":{"folio":"OC-2026-0012","req_folio":"REQ-2026-0031","proyecto_folio":"PRY-2026-0002","proyecto_clave":"DRO","sitio":"ZLO","proveedor":"Ferretería del Pacífico","fecha_entrega":"2026-10-30","solicitante":"jesus@helgen.tech","autorizo":"adriana@helgen.tech","total":99999,"renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":4,"item":"SOP-RTG","importe":1234},{"n":2,"descripcion":"Tornillo 3/8 x 2 in","unidad":"pza","cantidad":32,"item":null}]}}'
|
|
R1="$(post "$OC1")"
|
|
check "la OC entra" '"estado_portal":"autorizada"' "$R1"
|
|
check "el renglón sin ítem queda por ligar" '"por_ligar":[2]' "$R1"
|
|
check "abre su pendiente" '"tarea_id":"t-oc-oc-2026-0012"' "$R1"
|
|
check "el pendiente se llama «Recibir … en ZLO»" 'Recibir OC-2026-0012 en ZLO' "$(cat $T/tareas/t-oc-oc-2026-0012.json)"
|
|
check "…es de compras y del sitio de entrega" 'timsa|compras' "$($PY -c "
|
|
import json;d=json.load(open('$T/tareas/t-oc-oc-2026-0012.json'));print('|'.join(d['sitios'])+'|'+d['pista'])")"
|
|
check "…vence el día de entrega" '2026-10-30' "$(grep -o '"fecha_limite": "[^"]*"' $T/tareas/t-oc-oc-2026-0012.json)"
|
|
check "…es del responsable de ZLO" 'jesush' "$(grep -o '"responsable": "[^"]*"' $T/tareas/t-oc-oc-2026-0012.json)"
|
|
no "los montos NUNCA se guardan (total)" '99999' "$($PY -c "
|
|
import sqlite3;c=sqlite3.connect('$T/drogheda.db');print([r for r in c.execute('SELECT * FROM eventos')])")"
|
|
no "los montos NUNCA se guardan (importe)" '1234' "$($PY -c "
|
|
import sqlite3;c=sqlite3.connect('$T/drogheda.db');print([r for r in c.execute('SELECT * FROM eventos')])")"
|
|
check "el mismo aviso otra vez no duplica" '"repetido":true' "$(post "$OC1")"
|
|
check "sitio inválido: lo dice claro" 'usa VER, ZLO, ESE, LZC o QRO' "$(post '{"evento_id":"e-malo","tipo":"oc.autorizada","oc":{"folio":"OC-MALA","sitio":"MZO","renglones":[]}}')"
|
|
check "tipo inválido" 'Tipo de aviso inv' "$(post '{"evento_id":"e2","tipo":"oc.inventada","oc":{"folio":"OC-1","sitio":"ZLO"}}')"
|
|
check "nada en camino todavía" '"en_camino":{}' "$(as leo $U/api/inventario | tr -d ' ')"
|
|
|
|
echo
|
|
echo "## oc.en_camino: el material aparece en camino en la pestaña del sitio"
|
|
R2="$(post '{"evento_id":"OC-2026-0012:oc.en_camino:1","tipo":"oc.en_camino","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0012","sitio":"ZLO","proveedor":"Ferretería del Pacífico","fecha_entrega":"2026-10-30","paqueteria":"Estafeta","guia":"123456789","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":4,"item":"SOP-RTG"},{"n":2,"descripcion":"Tornillo 3/8 x 2 in","unidad":"pza","cantidad":32,"item":null}]}}')"
|
|
check "la OC queda en camino" '"estado_portal":"en-camino"' "$R2"
|
|
check "4 soportes en camino a TIMSA" '"en_camino":{"timsa":4}' "$(as leo $U/api/inventario | tr -d ' ')"
|
|
check "el envío trae el folio como referencia" 'OC-2026-0012' "$(as leo "$U/api/inventario/movimientos")"
|
|
check "…y la guía en el motivo" '123456789' "$(as leo "$U/api/inventario/movimientos")"
|
|
check "el pendiente se enteró" 'el proveedor ya lo mandó' "$(cat $T/tareas/t-oc-oc-2026-0012.json)"
|
|
check "el renglón 2 sigue por ligar" '"por_ligar":[2]' "$(as leo $U/api/compras | tr -d ' ')"
|
|
|
|
echo
|
|
echo "## ligar el renglón que venía sin ítem"
|
|
check "no se liga a un ítem que no existe" 'tem NO-HAY' "$(as leo -d '{"oc":"OC-2026-0012","n":2,"item":"NO-HAY"}' $U/api/compras/ligar)"
|
|
check "se liga al cable" '"por_ligar":[]' "$(as leo -d '{"oc":"OC-2026-0012","n":2,"item":"CBL-10"}' $U/api/compras/ligar | tr -d ' ')"
|
|
check "…y como la OC ya iba en camino, se manda solo" '"timsa":32' "$(as leo $U/api/inventario | tr -d ' ' | grep -o '"en_camino":{"timsa":32}')"
|
|
check "no se liga dos veces" 'en camino en el inventario' "$(as leo -d '{"oc":"OC-2026-0012","n":2,"item":"CBL-10"}' $U/api/compras/ligar)"
|
|
|
|
echo
|
|
echo "## recibir en el centro de mando (lo que llegó de verdad)"
|
|
check "no pueden llegar más de los que se pidieron" 'no pueden llegar' "$(as jesush -d '{"oc":"OC-2026-0012","renglones":[{"n":1,"recibido":9}]}' $U/api/compras/recibir)"
|
|
R3="$(as jesush -d '{"oc":"OC-2026-0012","renglones":[{"n":1,"recibido":4},{"n":2,"recibido":20}]}' $U/api/compras/recibir)"
|
|
check "se recibe parcial" '"estado":"recibida-parcial"' "$(echo $R3 | tr -d ' ')"
|
|
check "los 4 soportes entraron a TIMSA" '"por_sitio":{"timsa":4}' "$(as leo $U/api/inventario | tr -d ' ' | head -c 4000)"
|
|
check "el pendiente sigue abierto y dice qué falta" 'llegó parte de OC-2026-0012' "$(cat $T/tareas/t-oc-oc-2026-0012.json)"
|
|
check "el pendiente NO se cerró" '"estado": "por-hacer"' "$(grep -o '"estado": "[^"]*"' $T/tareas/t-oc-oc-2026-0012.json | head -1)"
|
|
|
|
echo
|
|
echo "## el cotizador consulta las recepciones"
|
|
REC="$(get '/api/integracion/cotizador/recepciones')"
|
|
check "trae la recepción" '"oc_folio":"OC-2026-0012"' "$(echo $REC | tr -d ' ')"
|
|
check "…marcada como incompleta" '"completa":false' "$(echo $REC | tr -d ' ')"
|
|
check "…con quién recibió" '"recibio":"jesush"' "$(echo $REC | tr -d ' ')"
|
|
check "…y cuánto llegó por renglón" '"pedido":32.0,"recibido":20.0' "$(echo $REC | tr -d ' ')"
|
|
check "con «desde» en el futuro no trae nada" '"recepciones":[]' "$(get '/api/integracion/cotizador/recepciones?desde=2030-01-01T00:00:00-06:00' | tr -d ' ')"
|
|
|
|
echo
|
|
echo "## una OC que llega completa cierra su pendiente"
|
|
post '{"evento_id":"OC-2026-0013:oc.autorizada:1","tipo":"oc.autorizada","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0013","req_folio":"REQ-2026-0032","sitio":"ESE","proveedor":"Edwin","fecha_entrega":"2026-11-03","renglones":[{"n":1,"descripcion":"Cable calibre 10","unidad":"m","cantidad":30,"item":"CBL-10"}]}}' > /dev/null
|
|
post '{"evento_id":"OC-2026-0013:oc.en_camino:1","tipo":"oc.en_camino","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0013","sitio":"ESE","guia":"ABC","renglones":[{"n":1,"descripcion":"Cable calibre 10","unidad":"m","cantidad":30,"item":"CBL-10"}]}}' > /dev/null
|
|
check "el pendiente de ESE es de Miguel" 'miguel' "$(grep -o '"responsable": "[^"]*"' $T/tareas/t-oc-oc-2026-0013.json)"
|
|
R4="$(as miguel -d '{"oc":"OC-2026-0013","renglones":[{"n":1,"recibido":30}]}' $U/api/compras/recibir)"
|
|
check "queda recibida" '"estado":"recibida"' "$(echo $R4 | tr -d ' ')"
|
|
check "y su pendiente se cerró solo" '"estado": "hecho"' "$(grep -o '"estado": "[^"]*"' $T/tareas/t-oc-oc-2026-0013.json | head -1)"
|
|
check "el cotizador la ve completa" 'OC-2026-0013=completa' "$(get '/api/integracion/cotizador/recepciones' | $PY -c "
|
|
import sys,json;d=json.load(sys.stdin)
|
|
print(' '.join(r['oc_folio']+('=completa' if r['completa'] else '=parcial') for r in d['recepciones']))")"
|
|
|
|
echo
|
|
echo "## recibir desde Inventario (la otra puerta) también se concilia"
|
|
post '{"evento_id":"OC-2026-0014:oc.autorizada:1","tipo":"oc.autorizada","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0014","req_folio":"REQ-2026-0033","sitio":"VER","fecha_entrega":"2026-11-05","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":2,"item":"SOP-RTG"}]}}' > /dev/null
|
|
post '{"evento_id":"OC-2026-0014:oc.en_camino:1","tipo":"oc.en_camino","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0014","sitio":"VER","guia":"XYZ","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":2,"item":"SOP-RTG"}]}}' > /dev/null
|
|
ENV=$($PY -c "
|
|
import sqlite3;c=sqlite3.connect('$T/drogheda.db');print(c.execute(\"SELECT envio_id FROM renglones WHERE oc_folio='OC-2026-0014'\").fetchone()[0])")
|
|
as alan -d "{\"tipo\":\"recepcion\",\"envio_id\":\"$ENV\",\"cantidad\":2}" $U/api/inventario/movimiento > /dev/null
|
|
check "Compras se entera de la recepción hecha en Inventario" 'OC-2026-0014=recibida' "$(as leo $U/api/compras | $PY -c "
|
|
import sys,json;d=json.load(sys.stdin)
|
|
print(' '.join(o['folio']+'='+o['estado'] for o in d['ordenes']))")"
|
|
check "y cierra su pendiente" '"estado": "hecho"' "$(grep -o '"estado": "[^"]*"' $T/tareas/t-oc-oc-2026-0014.json | head -1)"
|
|
|
|
echo
|
|
echo "## cancelar: lo que iba en camino deja de contar"
|
|
post '{"evento_id":"OC-2026-0015:oc.autorizada:1","tipo":"oc.autorizada","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0015","sitio":"LZC","fecha_entrega":"2026-11-06","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":7,"item":"SOP-RTG"}]}}' > /dev/null
|
|
post '{"evento_id":"OC-2026-0015:oc.en_camino:1","tipo":"oc.en_camino","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0015","sitio":"LZC","guia":"Q","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":7,"item":"SOP-RTG"}]}}' > /dev/null
|
|
check "7 en camino a LCT" '"lzc":7' "$(as leo $U/api/inventario | tr -d ' ')"
|
|
post '{"evento_id":"OC-2026-0015:oc.cancelada:1","tipo":"oc.cancelada","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0015","sitio":"LZC","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":7,"item":"SOP-RTG"}]}}' > /dev/null
|
|
no "ya no hay nada en camino a LCT" '"lzc":7' "$(as leo $U/api/inventario | tr -d ' ')"
|
|
check "y su pendiente se cerró" '"estado": "hecho"' "$(grep -o '"estado": "[^"]*"' $T/tareas/t-oc-oc-2026-0015.json | head -1)"
|
|
|
|
echo
|
|
echo "## las rondas de PRUEBA no tocan nada"
|
|
ANTES=$(ls $T/tareas | wc -l)
|
|
post '{"evento_id":"OC-PRUEBA-1:oc.autorizada:1","tipo":"oc.autorizada","prueba":true,"quien":"efrain@helgen.tech","oc":{"folio":"OC-PRUEBA-1","sitio":"QRO","renglones":[{"n":1,"descripcion":"Soporte","unidad":"pza","cantidad":5,"item":"SOP-RTG"}]}}' > /dev/null
|
|
check "la de prueba no abre pendiente" "$ANTES" "$(ls $T/tareas | wc -l)"
|
|
no "ni sale en la página de Compras" 'OC-PRUEBA-1' "$(as leo $U/api/compras)"
|
|
no "ni en las recepciones del cotizador" 'OC-PRUEBA-1' "$(get '/api/integracion/cotizador/recepciones')"
|
|
|
|
echo
|
|
echo "## sin secreto configurado el sitio sigue funcionando"
|
|
kill $APP 2>/dev/null; sleep 0.5
|
|
unset COTIZADOR_SECRETO
|
|
$PY $REPO/backend/app.py > $T/app2.log 2>&1 &
|
|
APP=$!
|
|
for _ in $(seq 1 30); do curl -fsS -o /dev/null "$U/api/salud" 2>/dev/null && break; sleep 0.5; done
|
|
check "integración apagada: 503 con mensaje claro" 'COTIZADOR_SECRETO' "$(curl -s -d '{}' $U/api/integracion/cotizador/evento)"
|
|
check "la página de Compras lo dice" '"integracion":false' "$(as leo $U/api/compras | tr -d ' ')"
|
|
check "…y sigue listando las OC de antes" 'OC-2026-0012' "$(as leo $U/api/compras)"
|
|
check "el inventario sigue igual" '"ok":true' "$(as leo $U/api/inventario)"
|
|
|
|
echo
|
|
echo "────────────────────────────────────────"
|
|
echo " $ok bien · $fail mal"
|
|
[[ $fail -eq 0 ]] || echo " log: $T/app.log"
|
|
[[ $fail -eq 0 ]]
|