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.
127 lines
6.0 KiB
Bash
Executable File
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
|