feat: initial API skeleton — routes, controllers, migrations, deploy docs
Implements the JSON API for Le Donut Infolab per INSTRUCTIONS.md.
Schema (migrations/001-002):
- 8 tables: categories, members, projects, project_members, roadmap_steps,
discussions, resources, activity_log
- 6 categories seeded with colors / dimensions (social, ecologique, transversal)
- FK contraintes + soft delete sur projects, CHECK sur progress_percent
Bare PHP framework (src/):
- Front controller public/index.php + router :params + auth flag par route
- PSR-4 autoload sans Composer (bootstrap.php)
- Database PDO singleton + helpers fetchOne/All/insert/update/delete
- Validator fluent -> 422 {error: validation_failed, fields: {...}}
- Bearer token admin (hash_equals) sur POST/PATCH/DELETE
- Slugger avec unicité (suffixe -2, -3...) via intl Transliterator
- ActivityLogger best-effort sur activity_log
- Erreurs uniformes : 400/401/404/405/409/422/500 + CORS configurable
10 contrôleurs (Health, Categories, Members, Projects, ProjectMembers,
Roadmap, Discussions, Resources, Activity, Stats). Transitions de statut
loggées (created_project, completed_step, joined, etc.).
Cible prod :
- Dockerfile (php:8.3-apache + pdo_mysql + intl + opcache)
- docker-compose.yml sur réseau donut-net, bind 127.0.0.1:8003
- caddy.conf pour infolab.ledonut-marseille.org
- migrations/apply.sh idempotent (table schema_migrations)
- DEPLOY.md avec commandes SSH exactes
Tests :
- tests/smoke.sh exerce 10 endpoints clés, body envoyé via stdin
(--data-binary @-) pour gérer l UTF-8 sous Git Bash Windows.
Vérifié localement sous Laragon Apache + MySQL 8.4 : 10/10 OK + cas
négatifs (401, 404, 405, 409, 422) conformes au contrat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+141
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# tests/smoke.sh
|
||||
#
|
||||
# Appelle les endpoints clés de l'API et vérifie les codes HTTP attendus.
|
||||
# Sortie non-zéro si au moins un appel échoue.
|
||||
#
|
||||
# Usage :
|
||||
# ADMIN_TOKEN=xxxxx [INFOLAB_URL=http://localhost:8003] ./tests/smoke.sh
|
||||
#
|
||||
# Idempotent : un cleanup best-effort en début de script supprime les ressources
|
||||
# de test (projet "nos-ecoles" + membre "collectif-des-ecoles-de-marseille").
|
||||
# -----------------------------------------------------------------------------
|
||||
set -uo pipefail
|
||||
|
||||
URL="${INFOLAB_URL:-http://localhost:8003}"
|
||||
TOKEN="${ADMIN_TOKEN:-}"
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
echo "ERR: variable d'env ADMIN_TOKEN requise" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
BODY="$(mktemp -t smoke.XXXXXX 2>/dev/null || mktemp)"
|
||||
trap 'rm -f "$BODY"' EXIT
|
||||
|
||||
# Appelle un endpoint et vérifie le code HTTP. Logue un extrait du body.
|
||||
# Args : label expected_code method path [body_json]
|
||||
call() {
|
||||
local label="$1" expected="$2" method="$3" path="$4" body="${5:-}"
|
||||
local -a headers=(-H 'Accept: application/json')
|
||||
if [[ -n "$body" ]]; then
|
||||
headers+=(-H 'Content-Type: application/json')
|
||||
fi
|
||||
if [[ "$method" != "GET" ]]; then
|
||||
headers+=(-H "Authorization: Bearer $TOKEN")
|
||||
fi
|
||||
|
||||
local code
|
||||
if [[ -n "$body" ]]; then
|
||||
# Important : on pipe le body via stdin avec --data-binary @-, pas via argv.
|
||||
# Sur Windows/Git Bash, passer un body UTF-8 en argument à curl convertit
|
||||
# les bytes multi-octets en cp1252 et casse le JSON. stdin = binary safe.
|
||||
code=$(printf '%s' "$body" \
|
||||
| curl -sS -o "$BODY" -w '%{http_code}' -X "$method" "${headers[@]}" --data-binary @- "$URL$path")
|
||||
else
|
||||
code=$(curl -sS -o "$BODY" -w '%{http_code}' -X "$method" "${headers[@]}" "$URL$path")
|
||||
fi
|
||||
|
||||
local snip
|
||||
snip=$(head -c 220 "$BODY" 2>/dev/null || echo '')
|
||||
|
||||
if [[ "$code" == "$expected" ]]; then
|
||||
printf ' OK [%s] %-6s %s — %s\n' "$code" "$method" "$path" "$label"
|
||||
printf ' > %s\n' "$snip"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
printf ' FAIL [%s, attendu %s] %-6s %s — %s\n' "$code" "$expected" "$method" "$path" "$label"
|
||||
printf ' > %s\n' "$snip"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Extrait le 1er "id":N du body courant.
|
||||
extract_id() {
|
||||
sed -n 's/.*"id":[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$BODY" | head -1
|
||||
}
|
||||
|
||||
# Compte les occurrences de "id":N dans le body (approximation suffisante pour les listes).
|
||||
count_ids() {
|
||||
grep -oE '"id":[[:space:]]*[0-9]+' "$BODY" | wc -l | tr -d ' '
|
||||
}
|
||||
|
||||
echo "== Smoke test infolab @ $URL =="
|
||||
|
||||
# Cleanup best-effort : autorise les ré-exécutions.
|
||||
curl -sS -o /dev/null -X DELETE -H "Authorization: Bearer $TOKEN" "$URL/api/projects/nos-ecoles" >/dev/null 2>&1 || true
|
||||
curl -sS -o /dev/null -X DELETE -H "Authorization: Bearer $TOKEN" "$URL/api/members/collectif-des-ecoles-de-marseille" >/dev/null 2>&1 || true
|
||||
|
||||
# 1) Sanity
|
||||
call "Health" 200 GET /api/health
|
||||
|
||||
# 2) Catégories seedées
|
||||
call "Categories list" 200 GET /api/categories
|
||||
count=$(count_ids)
|
||||
if [[ "$count" == "6" ]]; then
|
||||
echo " OK categories.length = 6"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo " FAIL categories.length = $count (attendu 6)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
# Récupère l'id de "education-ecoles" pour ne pas dépendre de l'auto-increment.
|
||||
curl -sS -o "$BODY" "$URL/api/categories/education-ecoles" >/dev/null
|
||||
CAT_ID=$(extract_id)
|
||||
CAT_ID="${CAT_ID:-6}"
|
||||
|
||||
# 3) Création du membre
|
||||
MEMBER_BODY=$(cat <<'JSON'
|
||||
{"display_name":"Collectif des écoles de Marseille","kind":"collective","bio":"Réseau citoyen pour les écoles publiques marseillaises."}
|
||||
JSON
|
||||
)
|
||||
call "Create member" 201 POST /api/members "$MEMBER_BODY"
|
||||
|
||||
# 4) Création du projet (ré-utilise CAT_ID dynamique)
|
||||
PROJECT_BODY=$(cat <<JSON
|
||||
{"title":"Nos Écoles","category_id":$CAT_ID,"status":"active","short_description":"Plateforme citoyenne pour partager l'état des écoles publiques.","context_description":"Issu de la mobilisation citoyenne de 2024.","objectives":"Cartographier, partager, alerter.","progress_percent":25}
|
||||
JSON
|
||||
)
|
||||
call "Create project" 201 POST /api/projects "$PROJECT_BODY"
|
||||
|
||||
# 5) Rattachement du membre au projet
|
||||
call "Add team member" 201 POST /api/projects/nos-ecoles/members \
|
||||
'{"member_slug":"collectif-des-ecoles-de-marseille","role":"admin"}'
|
||||
|
||||
# 6) Étape de roadmap (apostrophe via heredoc pour éviter les soucis de quoting)
|
||||
ROADMAP_BODY=$(cat <<'JSON'
|
||||
{"title":"Création de l'outil 15min","description":"Premier MVP en 15 minutes.","is_done":false}
|
||||
JSON
|
||||
)
|
||||
call "Add roadmap step" 201 POST /api/projects/nos-ecoles/roadmap "$ROADMAP_BODY"
|
||||
|
||||
# 7) Vérifie le détail complet du projet
|
||||
call "Project detail" 200 GET /api/projects/nos-ecoles
|
||||
|
||||
# 8) KPIs et flux d'activité
|
||||
call "Stats" 200 GET /api/stats
|
||||
call "Activity" 200 GET /api/activity
|
||||
|
||||
echo
|
||||
echo "==== Résumé ===="
|
||||
echo " OK : $PASS"
|
||||
echo " FAIL : $FAIL"
|
||||
|
||||
if [[ "$FAIL" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user