#!/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