hbox-docs/backend/tests-1027.sh

102 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# Pruebas del 27-oct: página de entrada (acceso.py), incidencias (incidencias.py), standup (standup.py), vigía y cuentas.
# Arranca app.py con carpetas y htpasswd temporales (como tests-1023.sh). No toca el servidor.
# Uso: bash backend/tests-1027.sh <ruta-a-hbox-docs> [puerto]
set -u
REPO=${1:?ruta a hbox-docs}; PORT=${2:-8097}
T=$(mktemp -d); export ACTAS_DIR=$T/actas HTPASSWD_FILE=$T/htpasswd HTPASSWD_LOCK=$T/.lock SESION_COOKIE_INSEGURA=1 \
ACTAS_ADMINS=leo,gibranlp PORT INCIDENCIAS_DIR=$T/incidencias
mkdir -p $ACTAS_DIR $T/equipo $T/tareas
python3 - "$REPO" "$T" <<'EOF'
import sys, os, json, importlib.util
repo, t = sys.argv[1], sys.argv[2]
spec = importlib.util.spec_from_file_location("app", os.path.join(repo, "backend", "app.py")); A = importlib.util.module_from_spec(spec); spec.loader.exec_module(A)
with open(os.path.join(t, "htpasswd"), "w") as f:
for u, p in {"leo": "helgen-2026-leo", "adriana": "Puerto-Cobre-1234", "jesush": "faro-ancla-9911"}.items():
f.write(u + ":" + A.apr1(p, ("salt" + u)[:8]) + "\n")
for u, n, c in [("leo", "Leo", "leo@helgen.tech"), ("adriana", "Adriana", "adriana@helgen.tech"), ("jesush", "Jesús Honorato", "jesus@helgen.tech"), ("claude", "Claude", "")]:
json.dump({"id": u, "nombre": n, "area": "ingenieria", "correo": c, "activo": True}, open(os.path.join(t, "equipo", u + ".json"), "w"))
for i, (ti, r, e) in enumerate([("Revisar kit", "adriana", "por-hacer"), ("Instalar 2 en TIMSA", "jesush", "en-curso"), ("Cotizar gabinetes", "adriana, leo", "bloqueado")]):
json.dump({"id": f"t-p{i}", "titulo": ti, "responsable": r, "estado": e, "pista": "compras", "sitios": ["qro"], "historial": []}, open(os.path.join(t, "tareas", f"t-p{i}.json"), "w"))
EOF
python3 $REPO/backend/app.py > $T/app.log 2>&1 &
APP=$!; trap "kill $APP 2>/dev/null" EXIT; sleep 1.8
U="http://127.0.0.1:$PORT"; MOZ="Mozilla/5.0 (pruebas)"
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:200}"; fi; }
J() { echo "-H Content-Type:application/json"; }
as() { curl -s -H "X-Remote-User: $1" -H "Content-Type: application/json" "${@:2}"; }
echo "## entrada (acceso.py)"
check "módulos cargados" "" "$(grep -i 'no se cargó' $T/app.log)"
check "_auth sin sesión 401" "401" "$(curl -s -o /dev/null -w '%{http_code}' $U/api/_auth)"
check "entrar con mayúscula y espacios" '"user":"adriana"' "$(curl -s -A "$MOZ" -c $T/j $(J) -d '{"usuario":" Adriana","password":"Puerto-Cobre-1234"}' $U/api/entrar)"
check "_auth con cookie 204 + X-Hbox-User" "X-Hbox-User: adriana" "$(curl -s -i -b $T/j $U/api/_auth | tr -d '\r')"
check "entrar por correo" '"user":"jesush"' "$(curl -s -A "$MOZ" -c $T/j2 $(J) -d '{"usuario":"jesus@helgen.tech","password":"faro-ancla-9911"}' $U/api/entrar)"
check "contraseña mala con Bloq Mayús" '"codigo":"contrasena"' "$(curl -s -A "$MOZ" $(J) -d '{"usuario":"jesush","password":"FARO-ANCLA-9911"}' $U/api/entrar)"
check "pista bloq-mayus registrada" "bloq-mayus" "$(cat $T/acceso/intentos-*.jsonl)"
check "la contraseña nunca se guarda" "" "$(grep -i 'faro-ancla\|FARO-ANCLA' $T/acceso/intentos-*.jsonl)"
curl -s -o /dev/null -A "$MOZ" $(J) -d '{"usuario":"mi-clave-secreta-123","password":"x"}' $U/api/entrar
check "lo tecleado en «usuario» se guarda enmascarado" "mi-…(20)" "$(cat $T/acceso/intentos-*.jsonl)"
check "…y nunca completo" "" "$(grep 'mi-clave-secreta' $T/acceso/intentos-*.jsonl)"
for i in 1 2 3 4 5 6 7 8; do curl -s -o /dev/null -A "$MOZ" -H 'X-Real-IP: 10.1.1.1' $(J) -d '{"usuario":"leo","password":"mala-1234"}' $U/api/entrar; done
check "8 fallas de leo desde una IP: esa IP queda en espera" '"codigo":"bloqueado"' "$(curl -s -A "$MOZ" -H 'X-Real-IP: 10.1.1.1' $(J) -d '{"usuario":"leo","password":"helgen-2026-leo"}' $U/api/entrar)"
check "…pero leo entra desde su IP (nadie lo deja fuera)" '"user":"leo"' "$(curl -s -A "$MOZ" -H 'X-Real-IP: 10.2.2.2' $(J) -d '{"usuario":"leo","password":"helgen-2026-leo"}' $U/api/entrar)"
check "Basic ya no entra (ni de script)" "401" "$(curl -s -o /dev/null -w '%{http_code}' -A curl -u leo:helgen-2026-leo $U/api/_auth)"
check "Basic de navegador no" "401" "$(curl -s -o /dev/null -w '%{http_code}' -A "$MOZ" -u leo:helgen-2026-leo $U/api/_auth)"
check "cookie con acento → 401 (no 500)" "401" "$(curl -s -o /dev/null -w '%{http_code}' -b 'hbox_sesion=é~1~2~3' $U/api/_auth)"
check "POST desde otro sitio → 403" "otro sitio" "$(curl -s -H 'X-Remote-User: adriana' -H 'Origin: https://malo.example' $(J) -d '{"descripcion":"csrf"}' $U/api/incidencias)"
check "POST con Sec-Fetch-Site same-site → 403" "otro sitio" "$(curl -s -H 'X-Remote-User: adriana' -H 'Sec-Fetch-Site: same-site' $(J) -d '{"descripcion":"csrf"}' $U/api/incidencias)"
check "cambio propio: cookie nueva" "hbox_sesion=adriana" "$(curl -s -i -b $T/j -c $T/j -H 'X-Remote-User: adriana' $(J) -d '{"actual":"Puerto-Cobre-1234","nueva":"marea-radar-5521","confirma":"marea-radar-5521"}' $U/api/password | tr -d '\r')"
check "la cookie nueva vale" "204" "$(curl -s -o /dev/null -w '%{http_code}' -b $T/j $U/api/_auth)"
check "reset de admin mata la sesión de jesush" "401" "$(as leo -d '{"usuario":"jesush","nueva":"cometa-trigo-4410","confirma":"cometa-trigo-4410"}' $U/api/password >/dev/null; curl -s -o /dev/null -w '%{http_code}' -b $T/j2 $U/api/_auth)"
check "salir" "hbox_sesion=;" "$(curl -s -i -X POST -b $T/j $U/api/salir | tr -d '\r')"
echo "## incidencias"
check "crear (con contexto)" '"id":"INC-0001"' "$(as adriana -d '{"descripcion":"El inventario no carga en el teléfono","tipo":"error","pagina":"/tableros/inventario/","contexto":{"errores":[{"mensaje":"x is undefined","fuente":"inventario.js:12"}]}}' $U/api/incidencias)"
check "sin descripción → 400" "aunque sea una l" "$(as adriana -d '{"tipo":"error"}' $U/api/incidencias)"
check "resumen" '"abiertas":1' "$(as jesush $U/api/incidencias/resumen)"
check "lista abiertas" '"INC-0001"' "$(as jesush $U/api/incidencias)"
check "comentar cualquiera" '"comentarios":[{' "$(as jesush -X PATCH -d '{"comentario":"A mí también"}' "$U/api/incidencia?id=INC-0001")"
check "no editor no cambia gravedad" "Solo quien atiende" "$(as jesush -X PATCH -d '{"gravedad":"baja"}' "$U/api/incidencia?id=INC-0001")"
check "claude atiende: en curso + asignado" '"estado":"en-curso"' "$(as claude -X PATCH -d '{"estado":"en-curso","asignado":"claude","comentario":"Lo reproduje: falta una validación."}' "$U/api/incidencia?id=INC-0001")"
check "quien reportó la marca resuelta" '"estado":"resuelta"' "$(as adriana -X PATCH -d '{"estado":"resuelta"}' "$U/api/incidencia?id=INC-0001")"
check "vigía abre con firma" '"nueva":true' "$(as vigia -d '{"titulo":"leo no puede entrar","descripcion":"3 fallas","tipo":"acceso","origen":"vigia","firma":"acceso:leo"}' $U/api/incidencias)"
check "misma firma suma ocurrencia" '"ocurrencias":2' "$(as vigia -d '{"titulo":"leo no puede entrar","descripcion":"4 fallas","tipo":"acceso","origen":"vigia","firma":"acceso:leo"}' $U/api/incidencias)"
check "un usuario normal no se hace pasar por vigía" '"origen": "sitio"' "$(as adriana -d '{"descripcion":"prueba de origen","origen":"vigia","firma":"x"}' $U/api/incidencias >/dev/null; python3 -c "import json,glob; print(json.dumps([json.load(open(f)) for f in sorted(glob.glob('$T/incidencias/INC-*.json'))][-1]))")"
check "reporte público (sin sesión)" '"ok":true' "$(curl -s $(J) -d '{"nombre":"miguel","contacto":"333 000 0000","descripcion":"No puedo entrar desde ayer","tipo":"acceso"}' $U/api/incidencias/publica)"
check "público: campo trampa (robot) no guarda" '"INC-0000"' "$(curl -s $(J) -d '{"nombre":"x","descripcion":"spam spam","sitio_web":"http://spam"}' $U/api/incidencias/publica)"
check "avisos públicos vacíos" '"avisos":[]' "$(curl -s $U/api/incidencias/publicas)"
check "publicar aviso" '"publica":true' "$(as leo -X PATCH -d '{"publica":true,"resumen_publico":"Algunas personas no podían entrar; ya hay página de entrada nueva."}' "$U/api/incidencia?id=INC-0002")"
check "aviso visible sin sesión (texto público)" "entrada nueva" "$(curl -s $U/api/incidencias/publicas)"
check "publicar sin texto público → 400" "texto corto" "$(as leo -X PATCH -d '{"publica":true}' "$U/api/incidencia?id=INC-0001")"
check "error de navegador" '"ok":true' "$(as jesush -d '{"tipo":"js","mensaje":"TypeError: a is null","fuente":"/tableros/kanban.js","linea":40,"pagina":"/tableros/pendientes/"}' $U/api/incidencias/error)"
check "errores agrupados (editor)" "TypeError" "$(as leo $U/api/incidencias/errores)"
check "errores: no editor 403" "Solo quien atiende" "$(as jesush $U/api/incidencias/errores)"
echo "## standup"
check "genérico sin bot: mis pendientes" "Revisar kit" "$(as adriana $U/api/standup/hoy)"
check "estado: nada preparado" '"preparado":false' "$(as adriana $U/api/standup/estado)"
check "solo el bot prepara" "Solo el bot" "$(as adriana -d '{"personas":[]}' $U/api/standup/preparar)"
HOY=$(date +%F)
check "el bot prepara" '"preparados":["adriana","jesush"]' "$(as claude -d '{"fecha":"'$HOY'","personas":[{"usuario":"adriana","instrucciones":["Cierra el kit con Leo","Revisa la cotización de gabinetes"],"preguntas":[{"tarea_id":"t-p0","titulo":"Revisar kit","pregunta":"¿Ya tienes las cantidades por sitio?"}]},{"usuario":"jesush","instrucciones":[{"texto":"Instala H0031 en la RTG 12","liga":"/formularios/instalacion.html"}],"preguntas":[{"tarea_id":"t-p1","titulo":"Instalar 2"}]},{"usuario":"nadie","instrucciones":["x"]}]}' $U/api/standup/preparar)"
check "estado: pendiente" '"pendiente":true' "$(as adriana $U/api/standup/estado)"
check "instrucciones del bot" "Cierra el kit" "$(as adriana $U/api/standup/hoy)"
check "respuesta vacía → 400" "Contesta" "$(as adriana -d '{"fecha":"'$HOY'"}' $U/api/standup/respuesta)"
check "contestar" '"enviada_en"' "$(as adriana -d '{"fecha":"'$HOY'","hecho":"Kit listo","falta":"Precios","bloqueos":"Espero a Leo","por_tarea":[{"tarea_id":"t-p0","estado":"hecho","nota":"ok"}]}' $U/api/standup/respuesta)"
check "ya no pendiente" '"pendiente":false' "$(as adriana $U/api/standup/estado)"
check "el bot no reescribe a quien ya contestó" "no se reescribe" "$(as claude -d '{"fecha":"'$HOY'","personas":[{"usuario":"adriana","instrucciones":["otra"]}]}' $U/api/standup/preparar)"
check "vista equipo (leo)" '"contestaron":1' "$(as leo "$U/api/standup?fecha=$HOY")"
check "vista equipo: jesush no" "La vista del equipo" "$(as jesush $U/api/standup)"
echo "## vigía (en seco)"
cp $REPO/backend/actas-hbox.service $T/ 2>/dev/null
check "vigía corre" '"hallazgos"' "$(VIGIA_API=$U INCIDENCIAS_DIR=$T/incidencias python3 $REPO/backend/vigia.py --seco 2>&1 | tail -1)"
echo "## sitios con código de 3 letras"
check "ZLO = TIMSA en pendientes" '"ok":true' "$(as leo "$U/api/tareas?sitio=zlo")"
echo
echo "RESULTADO: $ok correctas · $fail fallidas · datos en $T"
[ "$fail" = 0 ]