HormoneTrack-web/scripts/container-test.sh
Siphonight e078650045 Cross-arch : container-test accepte une platform cible (incident « exec format error »)
Le premier déploiement NAS a échoué avec « exec /docker-entrypoint.sh:
exec format error » : image arm64 (buildée sur Mac Apple Silicon) exécutée
sur un hôte amd64. Le test réel l'a épiné : container-test.sh accepte
désormais un paramètre PLATFORM (build cross-arch SANS émulation — le
Dockerfile n'a aucun RUN — puis run émulé + E2E complet) :
  bash scripts/container-test.sh <port> linux/amd64
→ validé vert sur linux/amd64 (healthcheck durci, E2E complet).

Docs §10 : leçon cross-arch (les 2 procédures de build — sur le serveur
cible ou multi-arch push via buildx), statut : les DEUX archs testées ;
note : l'« io_setup() failed » sous émulation Rosetta est inoffensif.
2026-09-08 21:30:18 +02:00

147 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# container-test.sh — validation RÉELLE de l'image Docker HormoneTrack Web.
#
# C'est le test qui doit passer AVANT toute release web déployable en
# conteneur (cf docs/DEVELOPPEMENT.md §10) : il attrape ce qu'aucun test
# statique ne voit — MIME des modules ES (requis par les navigateurs),
# headers réellement émis, gzip appliqué, fichiers exclus non servis,
# healthcheck, et le comportement de l'app COMPLÈTE servie par le conteneur
# (via l'E2E playwright branclé sur le conteneur).
#
# Prérequis : docker (daemon) ; node + playwright pour l'E2E final (skippé
# sinon — cf check.sh pour l'installation).
#
# Usage : bash scripts/container-test.sh [port] [platform]
# [port] défaut 8975
# [platform] platform Docker optionnelle (ex. linux/amd64) — permet de
# tester l'image pour L'HÔTE CIBLE depuis un Mac arm64 : sans
# elle, on build+run l'arch native (arm64) ; avec platform,
# buildx produit l'image pour CETTE platform (build natif —
# le Dockerfile ne contient AUCUN RUN, cross-compile sans
# émulation de build) et le run passe par l'émulation de
# l'hôte (Rosetta/qemu). ⚠️ Le bug réel du 8 sept. —
# « exec /docker-entrypoint.sh: exec format error » sur le
# NAS amd64 — venait d'une image arm64 uniquement : C'EST CE
# TEST cross-arch qui l'eût attrapé.
#
# ⚠️ Un SEUL invocation fait tout (leçon Android #43 : les invocations
# rapprochées se remplacent mutuellement) et l'échec est BRUYANT (exit 1
# à la première vérification fausse — jamais de « probablement bon »).
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
cd "$(dirname "$0")/.." # racine du dépôt
PORT="${1:-8975}"
PLATFORM="${2:-}"
HOST_PORT="127.0.0.1:${PORT}"
# Suffixe de tag par platevorme pour ne jamais mélanger les images
TAG_SUFFIX="smoke"
PLATFORM_ARG=()
if [[ -n "${PLATFORM:-}" ]]; then
TAG_SUFFIX="smoke-${PLATFORM##*/}" # linux/amd64 → smoke-amd64
PLATFORM_ARG=(--platform "$PLATFORM")
fi
IMAGE="hormonetrack-web:${TAG_SUFFIX}"
CONTAINER="hrt-web-${TAG_SUFFIX}"
ROOM=0 # compteur d'échecs
fail() { echo " ✗ $1"; ROOM=$((ROOM + 1)); }
ok() { echo " ✓ $1"; }
cleanup() {
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
}
trap cleanup EXIT
echo "── 1. Build de l'image${PLATFORM:+ [${PLATFORM}]} ──"
docker build "${PLATFORM_ARG[@]}" -t "$IMAGE" . 2>&1 | tail -2
echo "── 2. Run (durcissement compose : read_only + no-new-privileges) ──"
# On teste l'image dans la configuration DURCIE de docker-compose.yml —
# si ces options ne passent pas ici, elles n'auraient pas mieux passé en
# prod. ⚠️ tmpfs /tmp requis : l'image nginx-unprivileged écrit ses
# temporaires (proxy_temp…) dans /tmp — en read-only pur, nginx émerge
# « mkdir /tmp/proxy_temp failed » (constaté au premier container-test).
docker run -d --rm --name "$CONTAINER" \
-p "${HOST_PORT}:8080" \
"${PLATFORM_ARG[@]}" \
--read-only \
--tmpfs /var/cache/nginx --tmpfs /run --tmpfs /tmp \
--security-opt no-new-privileges \
"$IMAGE" >/dev/null
echo "── 3. Healthcheck (statut docker healthy) ──"
state=""
for _ in $(seq 1 30); do
state=$(docker inspect --format '{{.State.Health.Status}}' "$CONTAINER" 2>/dev/null || echo "")
if [[ "$state" == "healthy" ]]; then ok "healthcheck : healthy"; break; fi
sleep 1
done
[[ "$state" == "healthy" ]] || fail "healthcheck jamais healthy (état: '${state:-inconnu}')"
echo "── 4. Endpoints clés (HTTP 200) ──"
# Les MEMES chemins que le smoke HTTP de check.sh — le conteneur doit les
# servir TOUS (l'oubli de docs/ casserait le dialog « Nouveautés »).
for path in / /css/style.css /assets/pk_profiles.json /js/ui/app.js /docs/CHANGELOG.md; do
code=$(curl -s -o /dev/null -w '%{http_code}' "http://${HOST_PORT}${path}")
if [[ "$code" == "200" ]]; then ok "200 ${path}"; else fail "${path} → HTTP ${code}"; fi
done
echo "── 5. Headers (no-cache + sécurité) ──"
hdr=$(curl -s -D - -o /dev/null "http://${HOST_PORT}/")
for want in "no-cache" "nosniff" "DENY" "no-referrer"; do
if echo "$hdr" | grep -qi "$want"; then ok "header présent : $want"; else fail "header manquant : $want"; fi
done
echo "── 5. gzip effectif sur l'asset PK (550 Ko → ~150 Ko) ──"
enc=$(curl -s -H 'Accept-Encoding: gzip' -o /dev/null -w '%{header_json}' \
"http://${HOST_PORT}/assets/pk_profiles.json")
# ⚠️ curl %header_json produit des clés MINUSCULES (convention JSON) —
# chercher « content-encoding » pas « Content-Encoding ».
if echo "$enc" | grep -qi 'content-encoding.*gzip'; then ok "gzip appliqué"; else fail "pas de gzip sur l'asset — ${enc}"; fi
echo "── 6. MIME des modules ES (requête STRICTE des navigateurs) ──"
# Le piège réel du portage : un .js servi en text/html → « disallowed MIME
# type » et l'app ne charge PAS (cf §11 #6). On vérifie le CType réel.
mime=$(curl -s -o /dev/null -w '%{content_type}' "http://${HOST_PORT}/js/ui/app.js")
if echo "$mime" | grep -qi 'javascript'; then ok "module ES : ${mime}"; else fail "CType module ES = ${mime} (javascript requis)"; fi
echo "── 7. Fichiers cachés NON servis (défensif) ──"
code=$(curl -s -o /dev/null -w '%{http_code}' "http://${HOST_PORT}/.gitignore")
if [[ "$code" == "403" || "$code" == "404" ]]; then ok "fichier caché non servi (${code})"; else fail "fichier caché servi → HTTP ${code}"; fi
echo "── 8. Le conteneur ne contient AUCUNE donnée de dev ──"
# Audit de contenu : ce qui est dans l'image (garde double de .dockerignore)
for excl in package-lock.json node_modules; do
if docker exec "$CONTAINER" find /usr/share/nginx/html -name "$excl" 2>/dev/null | grep -q .; then
fail "$excl trouvé dans l'image"
else
ok "image propre : pas de ${excl}"
fi
done
# docs/ DOIT être présent (dialog changelog)
if docker exec "$CONTAINER" test -f /usr/share/nginx/html/docs/CHANGELOG.md; then
ok "docs/CHANGELOG.md embarqué (dialog « Nouveautés »)"
else
fail "docs/CHANGELOG.md ABSENT de l'image — le dialog « Nouveautés » planerait"
fi
echo "── 9. E2E navigateur complet contre le conteneur (si playwright dispo) ──"
if [[ -d node_modules/playwright-core && -d "$HOME/Library/Caches/ms-playwright" ]]; then
if HRT_E2E_BASE="http://${HOST_PORT}" node scripts/e2e.mjs; then
ok "E2E complet contre le conteneur"
else
fail "E2E contre le conteneur"
fi
else
echo " (ignoré : playwright non installé — npm i && npx playwright install firefox)"
fi
if [[ $ROOM -eq 0 ]]; then
echo "✓ CONTAINER TEST OK"
else
echo "✗ CONTAINER TEST : ${ROOM} échec(s)"
exit 1
fi