#!/usr/bin/env bash # Vérifie l'image de production (deploy/) avec le site assemblé (make package) : # construit l'image en local, la lance, et contrôle ce que reçoit un # navigateur (application, pages de test du moteur, aide, manifeste, service # worker, types MIME, compression, politique de sécurité, version). set -euo pipefail cd "$(dirname "$0")/.." unset MAKEFLAGS MFLAGS MAKELEVEL make -s package >/dev/null image="ustagammon/site-test:local" docker build -q -t "$image" build/deploy >/dev/null container="$(docker run -d --rm -p 127.0.0.1::80 "$image")" trap 'docker stop "$container" >/dev/null' EXIT port="$(docker port "$container" 80/tcp | head -n1 | sed 's/.*://')" base="http://127.0.0.1:$port" # Caddy démarre en une fraction de seconde : on attend qu'il réponde. for _ in $(seq 1 50); do curl -sf -o /dev/null "$base/" && break sleep 0.1 done errors=0 report() { echo "$1" errors=$((errors + 1)) } # check check() { local headers headers="$(curl -s -D - -o /dev/null "$base/$1")" grep -qE "^HTTP/1.1 200" <<<"$headers" || report "/$1 : pas de réponse 200" grep -qiE "^Content-Type: $2" <<<"$headers" || report "/$1 : type MIME attendu $2" } check "" "text/html" check "app.css" "text/css" check "app/main.js" "text/javascript" check "app/ui/app.js" "text/javascript" check "app/domain/game/game.js" "text/javascript" check "manifest.webmanifest" "application/manifest\+json" # Charte graphique (ADR 0010) : icônes et logo copiés de docs/identite/logo/, # police Alegreya Sans et sa licence. check "icons/favicon.svg" "image/svg\+xml" check "icons/favicon-32.png" "image/png" check "icons/apple-touch-icon-180.png" "image/png" check "icons/logo-horizontal-couleur.svg" "image/svg\+xml" check "icons/logo-horizontal-sombre.svg" "image/svg\+xml" check "icons/symbole-couleur.svg" "image/svg\+xml" check "icons/symbole-sombre.svg" "image/svg\+xml" cmp -s <(curl -s "$base/icons/icone-512.png") docs/identite/logo/icone-512.png \ || report "/icons/icone-512.png diffère de docs/identite/logo/" for weight in Regular Italic Medium Bold ExtraBold; do check "fonts/AlegreyaSans-$weight.woff2" "font/woff2" done check "fonts/OFL.txt" "text/plain" # Chaque fichier désigné par la page (icônes, police, style) est servi. page="$(curl -s "$base/")" while read -r ref; do curl -sf -o /dev/null "$base/$ref" || report "index.html : $ref introuvable" done < <(grep -oE '(href|src|srcset)="[^"]*"' <<<"$page" | sed -E 's/^[a-z]+="(.*)"$/\1/') grep -qF "url(\"fonts/AlegreyaSans-Regular.woff2\")" <(curl -s "$base/app.css") \ || report "app.css : police Alegreya Sans non déclarée" check "sw.js" "text/javascript" check "engine-worker.mjs" "text/javascript" check "wasm/engine.mjs" "text/javascript" check "wasm/engine.wasm" "application/wasm" check "wasm/engine.data" "application/octet-stream" check "version.json" "application/json" # Pages de test du moteur (étape 0b). check "moteur/" "text/html" check "moteur/bench.html" "text/html" check "moteur/engine-client.mjs" "text/javascript" # Aide : copies du glossaire et des scénarios (ADR 0008). check "docs/scenarios/jouer-une-partie.feature" "text/plain" check "docs/glossaire/README.md" "text/markdown" check "docs/glossaire/videau.md" "text/markdown" cmp -s <(curl -s "$base/docs/glossaire/videau.md") docs/glossaire/videau.md \ || report "/docs/glossaire/videau.md diffère de la source" # Le manifeste désigne des icônes servies, et l'application à la racine. manifest="$(curl -s "$base/manifest.webmanifest")" grep -q '"start_url": "./"' <<<"$manifest" || report "manifeste : start_url absent" grep -q '"theme_color": "#4A2E1F"' <<<"$manifest" || report "manifeste : theme_color autre que le noyer" grep -q '"purpose": "maskable"' <<<"$manifest" || report "manifeste : pas d'icône maskable" while read -r icon; do curl -sf -o /dev/null "$base/$icon" || report "manifeste : icône $icon introuvable" done < <(grep -o '"src": "[^"]*"' <<<"$manifest" | sed 's/"src": "\(.*\)"/\1/') # Le service worker met en cache chaque fichier du site, avec une version. sw="$(curl -s "$base/sw.js")" grep -qE '^const VERSION = "[0-9a-f]{16}";' <<<"$sw" || report "sw.js : version absente" for file in ./ index.html app/main.js wasm/engine.data wasm/engine.wasm moteur/ \ docs/glossaire/videau.md version.json fonts/AlegreyaSans-Bold.woff2 icons/favicon.svg; do grep -qF " \"$file\"," <<<"$sw" || report "sw.js : $file absent de la liste à mettre en cache" done listed="$(grep -c '^ "' <<<"$sw")" present="$(find build/site -type f ! -name sw.js | wc -l)" # Chaque fichier, plus « ./ » et « moteur/ » pour les deux index.html. [ "$listed" -eq "$((present + 2))" ] \ || report "sw.js liste $listed adresses pour $present fichiers (plus 2 dossiers)" grep -o '^ "[^"]*"' <<<"$sw" | tr -d ' "' | while read -r file; do curl -sf -o /dev/null "$base/$file" || echo "sw.js : $file introuvable sur le site" done | grep . && report "sw.js liste des fichiers introuvables" # Politique de sécurité du contenu : WebAssembly permis, rien en ligne. csp="$(curl -s -D - -o /dev/null "$base/" | grep -i '^Content-Security-Policy:')" grep -q "script-src 'self' 'wasm-unsafe-eval';" <<<"$csp" \ || report "CSP absente ou sans 'wasm-unsafe-eval'" grep -q "unsafe-inline" <<<"$csp" && report "CSP : unsafe-inline ne doit pas être permis" curl -s -D - -o /dev/null "$base/engine-worker.mjs" | grep -qi '^Content-Security-Policy:' \ || report "CSP absente du Web Worker du moteur" curl -s -H "Accept-Encoding: gzip" -D - -o /dev/null "$base/wasm/engine.data" \ | grep -qi "^Content-Encoding: gzip" || report "/wasm/engine.data : pas de compression gzip" curl -s -D - -o /dev/null "$base/" | grep -qi "^Cache-Control: no-cache" \ || report "/ : en-tête Cache-Control absent" [ "$(curl -s "$base/version.json" | sed -n 's/.*"commit": "\([0-9a-f]*\)".*/\1/p')" = "$(git rev-parse HEAD)" ] \ || report "version.json ne donne pas le commit courant" [ "$errors" -eq 0 ]