HormoneTrack-web/scripts/container-test.sh
Siphonight a0245e72c2 Conteneurisation Docker + tests de release (container-test, mode --release)
Conteneurisation (site 100 % statique — l'image ne détient AUCUNE donnée,
vie privée identique) :
- Dockerfile : nginxinc/nginx-unprivileged:alpine (uid 101, port 8080,
  pas de root), healthcheck wget intégré, labels OCI
- nginx.conf : no-cache systématique (cohérence du jeu de fichiers à
  chaque mise à jour d'image, coût nul : ~600 Ko + gzip sur l'asset PK),
  gzip, en-têtes de sécurité, deny des dotfiles
- .dockerignore : runtime uniquement (docs/ inclus — dialog changelog) ;
  local-test-data exclu en filet de sécurité
- docker-compose.yml : read_only + tmpfs (/var/cache/nginx, /run, /tmp —
  les temporaires de nginx-unprivileged, constat au premier test réel)
  + no-new-privileges

Nouveaux tests avant release :
- scripts/container-test.sh : build + run DURCI (config compose) +
  healthcheck healthy + endpoints 200 + headers (no-cache/nosniff/DENY/
  no-referrer) + gzip réel + MIME strict des modules ES + fichiers cachés
  non servis + image propre (pas de node_modules/npm) + docs/ embarqué +
  E2E playwright complet CONTRE LE CONTENEUR (HRT_E2E_BASE)
- scripts/e2e.mjs : mode externe HRT_E2E_BASE (pilote un site déjà déployé
  — conteneur inclus, serveur local non démarré)
- scripts/check.sh --release : supplée les 9 vérifications (version ↔
  changelog ↔ tag ↔ arbre propre + test conteneur si daemon)

Runtime Docker local installé pour le CI-like (colima 2 CPU / 2 Go) :
premier build + run réel = 2 problèmes testés et corrigés (tmpfs /tmp,
key case header_json curl). CONTAINER TEST OK en configuration durcie.
2026-09-08 21:07:23 +02:00

127 lines
6.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] (défaut 8975)
#
# ⚠️ 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}"
HOST_PORT="127.0.0.1:${PORT}"
IMAGE="hormonetrack-web:smoke"
CONTAINER="hrt-web-smoke"
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 ──"
docker build -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" \
--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