#!/usr/bin/env bash set -Eeuo pipefail dhv2_base_url="${DHV2_BASE_URL:-https://dhv2.korexlabs.com}" dhv2_base_url="${dhv2_base_url%/}" dhv2_nil_uuid="00000000-0000-4000-8000-000000000000" fail() { echo "ERROR: $*" >&2 exit 1 } echo "========== B4 · MIGRACIONES ==========" dhv2_migration_status="$(docker compose --profile tools run --rm migrate npm run migration:show 2>&1)" grep -q 'Pending migrations: no' <<<"$dhv2_migration_status" \ || fail "Quedaron migraciones pendientes" echo "Migraciones pendientes: no" echo echo "========== B4 · HEALTH ==========" dhv2_internal="$(curl -fsS --connect-timeout 10 --max-time 30 http://127.0.0.1:3101/api/v3/health)" dhv2_external="$(curl -fsS --connect-timeout 10 --max-time 30 "$dhv2_base_url/api/v3/health")" grep -q '"version":"0.11.0"' <<<"$dhv2_internal" || fail "Health interno no informa v0.11.0" grep -q '"phase":"B4"' <<<"$dhv2_internal" || fail "Health interno no informa B4" grep -q '"version":"0.11.0"' <<<"$dhv2_external" || fail "Health externo no informa v0.11.0" grep -q '"database":"ok"' <<<"$dhv2_external" || fail "La base no está saludable" echo "$dhv2_external" echo echo "========== B4 · RUTAS PROTEGIDAS ==========" for dhv2_path in \ "assets/$dhv2_nil_uuid/media" \ "asset-media/$dhv2_nil_uuid/content"; do dhv2_code="$(curl -sS --connect-timeout 10 --max-time 30 \ -o /tmp/dhv2-b4-response.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 rm -f /tmp/dhv2-b4-response.json echo echo "========== B4 · MODELO Y PERMISOS ==========" dhv2_db_check="$(docker compose exec -T db sh -lc \ 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v app_user="$APP_DB_USER" -At' <<'SQL' SELECT 'media_table', COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'asset_media'; SELECT 'media_columns', COUNT(*) FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'asset_media' AND column_name IN ( 'id', 'asset_id', 'kind', 'original_name', 'stored_name', 'mime_type', 'size_bytes', 'sha256', 'title', 'description', 'captured_at', 'latitude', 'longitude', 'accuracy_m', 'source', 'uploaded_by', 'created_at', 'updated_at', 'deleted_at', 'deleted_by' ); SELECT 'media_indexes', COUNT(*) FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'asset_media' AND indexname IN ( 'idx_asset_media_asset_created', 'idx_asset_media_sha256', 'idx_asset_media_active' ); SELECT 'history_media_changes', COUNT(*) FROM pg_constraint WHERE conname = 'chk_asset_versions_change_type' AND pg_get_constraintdef(oid) LIKE '%MEDIA_UPLOADED%' AND pg_get_constraintdef(oid) LIKE '%MEDIA_UPDATED%' AND pg_get_constraintdef(oid) LIKE '%MEDIA_REMOVED%'; SELECT 'permissions', COUNT(*) FROM permissions WHERE code IN ('assets.read_media', 'assets.manage_media'); SELECT 'read_roles', COUNT(*) FROM role_permissions role_permission INNER JOIN roles role ON role.id = role_permission.role_id INNER JOIN permissions permission ON permission.id = role_permission.permission_id WHERE role.code IN ('admin', 'director', 'supervisor', 'inspector', 'auditor') AND permission.code = 'assets.read_media'; SELECT 'manage_roles', COUNT(*) FROM role_permissions role_permission INNER JOIN roles role ON role.id = role_permission.role_id INNER JOIN permissions permission ON permission.id = role_permission.permission_id WHERE role.code IN ('admin', 'supervisor', 'inspector') AND permission.code = 'assets.manage_media'; SELECT 'app_select', has_table_privilege(:'app_user', 'asset_media', 'SELECT'); SELECT 'app_insert', has_table_privilege(:'app_user', 'asset_media', 'INSERT'); SELECT 'app_update', has_table_privilege(:'app_user', 'asset_media', 'UPDATE'); SELECT 'app_delete', has_table_privilege(:'app_user', 'asset_media', 'DELETE'); SQL )" grep -q 'media_table|1' <<<"$dhv2_db_check" || fail "No se creó asset_media" grep -q 'media_columns|20' <<<"$dhv2_db_check" || fail "asset_media no tiene el modelo esperado" grep -q 'media_indexes|3' <<<"$dhv2_db_check" || fail "Faltan índices de archivos" grep -q 'history_media_changes|1' <<<"$dhv2_db_check" || fail "El historial no reconoce cambios de archivos" grep -q 'permissions|2' <<<"$dhv2_db_check" || fail "No se crearon los dos permisos de archivos" grep -q 'read_roles|5' <<<"$dhv2_db_check" || fail "Los cinco roles base no recibieron lectura de archivos" grep -q 'manage_roles|3' <<<"$dhv2_db_check" || fail "Los tres roles operativos no recibieron gestión de archivos" grep -q 'app_select|t' <<<"$dhv2_db_check" || fail "La API no puede consultar archivos" grep -q 'app_insert|t' <<<"$dhv2_db_check" || fail "La API no puede registrar archivos" grep -q 'app_update|t' <<<"$dhv2_db_check" || fail "La API no puede retirar archivos" grep -q 'app_delete|f' <<<"$dhv2_db_check" || fail "La API no debe borrar físicamente registros de archivos" echo "$dhv2_db_check" echo echo "========== B4 · ALMACENAMIENTO ==========" docker volume inspect dhv2_asset_media >/dev/null \ || fail "No existe el volumen dhv2_asset_media" docker compose exec -T api sh -lc \ 'test "$ASSET_MEDIA_ROOT" = "/app/storage/asset-media" && test -d "$ASSET_MEDIA_ROOT" && test -w "$ASSET_MEDIA_ROOT"' \ || fail "El almacenamiento de archivos no está montado y escribible" echo "Volumen dhv2_asset_media montado y escribible: OK" echo echo "========== B4 · FRONTEND ==========" docker compose exec -T web sh -lc \ 'grep -R "0.11.0" /usr/share/nginx/html/assets >/dev/null && grep -R "Fotografías y documentos de activos" /usr/share/nginx/html/assets >/dev/null && grep -R "Fotografías y documentos" /usr/share/nginx/html/assets >/dev/null && grep -R "Incorporar archivo" /usr/share/nginx/html/assets >/dev/null' echo "Galería y footer v0.11.0: OK" echo echo "ACEPTACIÓN DE FASE B4: OK"