#!/usr/bin/env bash set -Eeuo pipefail dhv2_base_url="${DHV2_BASE_URL:-https://dhv2.korexlabs.com}" dhv2_base_url="${dhv2_base_url%/}" dhv2_tmp="$(mktemp -d)" umask 077 cleanup() { rm -rf "$dhv2_tmp" unset dhv2_username dhv2_password dhv2_csrf } trap cleanup EXIT fail() { echo "ERROR: $*" >&2 exit 1 } command -v curl >/dev/null || fail "curl no está instalado" command -v python3 >/dev/null || fail "python3 no está instalado" echo "========== A7 · PRUEBAS SIN SESIÓN ==========" for dhv2_path in dashboard/summary users roles audit; do dhv2_code="$(curl -sS --connect-timeout 10 --max-time 30 \ -o "$dhv2_tmp/public.json" -w '%{http_code}' \ "$dhv2_base_url/api/v3/$dhv2_path")" test "$dhv2_code" = "401" || fail "/api/v3/$dhv2_path devolvió HTTP $dhv2_code; se esperaba 401" echo "/api/v3/$dhv2_path sin sesión: 401 OK" done echo echo "========== A7 · LOGIN INVÁLIDO GENÉRICO ==========" DHV2_PAYLOAD_PATH="$dhv2_tmp/invalid-payload.json" python3 - <<'PY' import json import os from pathlib import Path Path(os.environ['DHV2_PAYLOAD_PATH']).write_text(json.dumps({ 'identifier': 'a7_usuario_inexistente', 'password': 'A7-invalida-2026', 'deviceLabel': 'Prueba A7', }), encoding='utf-8') PY dhv2_invalid_code="$(curl -sS --connect-timeout 10 --max-time 30 \ -o "$dhv2_tmp/invalid.json" -w '%{http_code}' \ -H 'Content-Type: application/json' \ --data-binary "@$dhv2_tmp/invalid-payload.json" \ "$dhv2_base_url/api/v3/auth/login")" test "$dhv2_invalid_code" = "401" || fail "El login inválido devolvió HTTP $dhv2_invalid_code; se esperaba 401" grep -q '"code":"INVALID_CREDENTIALS"' "$dhv2_tmp/invalid.json" || fail "El login inválido no devolvió el error genérico esperado" echo "Login inválido genérico: 401 OK" echo echo "========== A7 · LOGIN ADMINISTRATIVO ==========" dhv2_username="${DHV2_TEST_USERNAME:-}" dhv2_password="${DHV2_TEST_PASSWORD:-}" if [ -z "$dhv2_username" ]; then read -r -p "Usuario administrador: " dhv2_username fi if [ -z "$dhv2_password" ]; then read -r -s -p "Contraseña: " dhv2_password echo fi test -n "$dhv2_username" || fail "El usuario es obligatorio" test -n "$dhv2_password" || fail "La contraseña es obligatoria" DHV2_USERNAME="$dhv2_username" \ DHV2_PASSWORD="$dhv2_password" \ DHV2_PAYLOAD_PATH="$dhv2_tmp/login-payload.json" \ python3 - <<'PY' import json import os from pathlib import Path Path(os.environ['DHV2_PAYLOAD_PATH']).write_text(json.dumps({ 'identifier': os.environ['DHV2_USERNAME'], 'password': os.environ['DHV2_PASSWORD'], 'deviceLabel': 'Aceptación Fase A7', }), encoding='utf-8') PY dhv2_login_code="$(curl -sS --connect-timeout 10 --max-time 30 \ -D "$dhv2_tmp/login.headers" \ -c "$dhv2_tmp/cookies.txt" \ -o "$dhv2_tmp/login.json" \ -w '%{http_code}' \ -H 'Content-Type: application/json' \ --data-binary "@$dhv2_tmp/login-payload.json" \ "$dhv2_base_url/api/v3/auth/login")" rm -f "$dhv2_tmp/login-payload.json" unset dhv2_password test "$dhv2_login_code" = "200" || fail "El login administrativo devolvió HTTP $dhv2_login_code" grep -q '"mustChangePassword":false' "$dhv2_tmp/login.json" || fail "La cuenta todavía exige cambiar la contraseña desde el panel" dhv2_csrf="$(DHV2_RESPONSE_PATH="$dhv2_tmp/login.json" python3 - <<'PY' import json import os from pathlib import Path data = json.loads(Path(os.environ['DHV2_RESPONSE_PATH']).read_text(encoding='utf-8')) print(data.get('csrfToken', '')) PY )" test -n "$dhv2_csrf" || fail "El login no devolvió un token CSRF" dhv2_secure_cookie_count="$(grep -Ei '^set-cookie:' "$dhv2_tmp/login.headers" | grep -Eic 'Secure.*SameSite=Strict' || true)" dhv2_http_only_count="$(grep -Ei '^set-cookie:' "$dhv2_tmp/login.headers" | grep -Eic 'HttpOnly.*Secure.*SameSite=Strict' || true)" test "$dhv2_secure_cookie_count" -ge 3 || fail "No se detectaron las tres cookies Secure + SameSite=Strict" test "$dhv2_http_only_count" -ge 2 || fail "Las cookies de acceso y refresh no tienen todos los atributos requeridos" echo "Login y atributos de cookies: OK" echo echo "========== A7 · ENDPOINTS AUTENTICADOS ==========" for dhv2_path in auth/me dashboard/summary 'users?page=1&pageSize=1' roles 'audit?page=1&pageSize=1'; do dhv2_safe_name="${dhv2_path//[^a-zA-Z0-9]/_}" dhv2_code="$(curl -sS --connect-timeout 10 --max-time 30 \ -b "$dhv2_tmp/cookies.txt" \ -o "$dhv2_tmp/$dhv2_safe_name.json" \ -w '%{http_code}' \ "$dhv2_base_url/api/v3/$dhv2_path")" test "$dhv2_code" = "200" || fail "/api/v3/$dhv2_path devolvió HTTP $dhv2_code; se esperaba 200" echo "/api/v3/$dhv2_path autenticado: 200 OK" done echo echo "========== A7 · CSRF Y LOGOUT ==========" dhv2_csrf_failure="$(curl -sS --connect-timeout 10 --max-time 30 \ -b "$dhv2_tmp/cookies.txt" \ -o "$dhv2_tmp/csrf-failure.json" \ -w '%{http_code}' \ -X POST "$dhv2_base_url/api/v3/auth/logout")" test "$dhv2_csrf_failure" = "403" || fail "Una escritura sin CSRF devolvió HTTP $dhv2_csrf_failure; se esperaba 403" grep -q '"code":"CSRF_INVALID"' "$dhv2_tmp/csrf-failure.json" || fail "No se devolvió CSRF_INVALID" echo "Escritura sin CSRF: 403 OK" dhv2_logout_code="$(curl -sS --connect-timeout 10 --max-time 30 \ -b "$dhv2_tmp/cookies.txt" \ -c "$dhv2_tmp/cookies.txt" \ -o "$dhv2_tmp/logout.json" \ -w '%{http_code}' \ -H "x-csrf-token: $dhv2_csrf" \ -X POST "$dhv2_base_url/api/v3/auth/logout")" test "$dhv2_logout_code" = "200" || fail "Logout devolvió HTTP $dhv2_logout_code" dhv2_after_logout="$(curl -sS --connect-timeout 10 --max-time 30 \ -b "$dhv2_tmp/cookies.txt" \ -o "$dhv2_tmp/after-logout.json" \ -w '%{http_code}' \ "$dhv2_base_url/api/v3/auth/me")" test "$dhv2_after_logout" = "401" || fail "La sesión siguió activa después del logout" echo "Logout y revocación de sesión: OK" echo echo "ACEPTACIÓN AUTENTICADA DE FASE A: OK"