go-learn/server/tests/smoke.sh
sab.code.lab 1c85091186 chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца
(снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной
истории.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 09:34:02 +02:00

288 lines
11 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# Smoke-тест серверного API этапов 8 и 8.1 (контракт: docs/INTERFACES.md).
# sh, без bashизмов. Поднимает php -S на server/ с временной БД в /tmp
# (через env GOLEARN_DB) и гонит curl-сценарий. Код выхода != 0 при любом
# фейле; php -S убивается, временные файлы удаляются.
#
# Этап 8.1: сервер поднимается с GOLEARN_TEST=1 — captcha.php отдаёт код
# в заголовке X-Captcha-Debug, а письма восстановления пароля пишутся
# в файл GOLEARN_TEST_MAIL вместо mail().
#
# Использование:
# sh server/tests/smoke.sh # PHP по умолчанию: /tmp/php-run
# PHP=/path/to/php sh server/tests/smoke.sh
# PORT=8100 sh server/tests/smoke.sh
set -u
PHP=${PHP:-/tmp/php-run}
PORT=${PORT:-8099}
BASE="http://127.0.0.1:$PORT"
SERVER_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
TMPD=$(mktemp -d /tmp/golearn-smoke.XXXXXX) || exit 1
DB="$TMPD/test.sqlite"
COOKIES="$TMPD/cookies.txt"
RESP="$TMPD/resp.txt"
SRVLOG="$TMPD/server.log"
BIG="$TMPD/big.json"
HEADERS="$TMPD/headers.txt"
CAPTCHA="$TMPD/captcha.svg"
MAIL="$TMPD/mail.log"
export GOLEARN_DB="$DB"
export GOLEARN_TEST=1
export GOLEARN_TEST_MAIL="$MAIL"
SRV_PID=""
FAIL=0
cleanup() {
if [ -n "$SRV_PID" ]; then
kill "$SRV_PID" 2>/dev/null
wait "$SRV_PID" 2>/dev/null
fi
rm -f "$DB" "$DB-wal" "$DB-shm" "$COOKIES" "$RESP" "$SRVLOG" "$BIG" \
"$HEADERS" "$CAPTCHA" "$MAIL"
rmdir "$TMPD" 2>/dev/null
}
trap cleanup EXIT
# req METHOD PATH DATA CLIENT
# DATA: строка JSON, @path — файл, '-' — без тела
# CLIENT: 'yes' — добавить X-GoLearn-Client: web, 'no' — без заголовка
# Печатает HTTP-код; тело ответа остаётся в $RESP.
req() {
_method=$1
_path=$2
_data=$3
_client=$4
set -- -s -o "$RESP" -w '%{http_code}' -X "$_method" \
-b "$COOKIES" -c "$COOKIES"
if [ "$_client" = "yes" ]; then
set -- "$@" -H 'X-GoLearn-Client: web'
fi
if [ "$_data" != "-" ]; then
set -- "$@" -H 'Content-Type: application/json' \
--data-binary "$_data"
fi
curl "$@" "$BASE$_path"
}
# get_captcha — GET /api/captcha.php с куки-джаром; заголовки ответа
# остаются в $HEADERS, SVG — в $CAPTCHA; печатает код из X-Captcha-Debug
# (тестовый режим GOLEARN_TEST=1).
get_captcha() {
curl -s -D "$HEADERS" -o "$CAPTCHA" \
-b "$COOKIES" -c "$COOKIES" "$BASE/api/captcha.php"
tr -d '\r' <"$HEADERS" \
| sed -n 's/^[Xx]-[Cc]aptcha-[Dd]ebug:[[:space:]]*//p' | head -n 1
}
check() {
# check ОПИСАНИЕ ОЖИДАЕМЫЙ_КОД ФАКТИЧЕСКИЙ_КОД
if [ "$2" = "$3" ]; then
echo "ok [$3] $1"
else
echo "FAIL [$3, ожидали $2] $1"
FAIL=1
fi
}
check_body() {
# check_body ОПИСАНИЕ ПАТТЕРН (фиксированная строка, grep -qF по $RESP)
if grep -qF "$2" "$RESP"; then
echo "ok [body] $1"
else
echo "FAIL [body] $1 — тело: $(cat "$RESP")"
FAIL=1
fi
}
check_hdr() {
# check_hdr ОПИСАНИЕ ПАТТЕРН (regexp, grep -qiE по $HEADERS)
if grep -qiE "$2" "$HEADERS"; then
echo "ok [hdr] $1"
else
echo "FAIL [hdr] $1 — заголовки: $(tr '\n' ' ' <"$HEADERS")"
FAIL=1
fi
}
# --- поднять сервер ---
"$PHP" -S 127.0.0.1:"$PORT" -t "$SERVER_DIR" >"$SRVLOG" 2>&1 &
SRV_PID=$!
_i=0
_ready=0
while [ "$_i" -lt 50 ]; do
if curl -s "$BASE/api/health.php" 2>/dev/null | grep -q '"status":"ok"'; then
_ready=1
break
fi
sleep 0.2 2>/dev/null || sleep 1
_i=$((_i + 1))
done
if [ "$_ready" != "1" ]; then
echo "FAIL: php -S не поднялся, лог:"
cat "$SRVLOG"
exit 1
fi
EMAIL="smoke@example.com"
PASS="password123"
NEWPASS="newpassword456"
# --- сценарий по контракту ---
_code=$(req GET /api/health.php - no)
check "GET health" 200 "$_code"
check_body "health status ok" '"status":"ok"'
# --- этап 8.1: капча ---
_code=$(curl -s -D "$HEADERS" -o "$CAPTCHA" -w '%{http_code}' \
-b "$COOKIES" -c "$COOKIES" "$BASE/api/captcha.php")
check "GET captcha" 200 "$_code"
check_hdr "captcha: Content-Type image/svg+xml" \
'^content-type:[[:space:]]*image/svg\+xml'
check_hdr "captcha: X-Captcha-Debug (GOLEARN_TEST=1)" '^x-captcha-debug:'
check_hdr "captcha: Set-Cookie сессии" '^set-cookie:'
if grep -qF '<svg' "$CAPTCHA"; then
echo "ok [body] captcha: тело — SVG"
else
echo "FAIL [body] captcha: тело не SVG — $(cat "$CAPTCHA")"
FAIL=1
fi
# register без капчи → 422 captcha (код в сессии сгорает при попытке)
_code=$(req POST /api/register.php "{\"email\":\"$EMAIL\",\"password\":\"$PASS\"}" yes)
check "register без капчи" 422 "$_code"
check_body "register без капчи: error=captcha" '"error":"captcha"'
# register с неверной капчей → 422 captcha
CAP=$(get_captcha)
_code=$(req POST /api/register.php "{\"email\":\"$EMAIL\",\"password\":\"$PASS\",\"captcha\":\"00000\"}" yes)
check "register с неверной капчей" 422 "$_code"
check_body "register неверная капча: error=captcha" '"error":"captcha"'
# register с верной капчей (код из X-Captcha-Debug, общий куки-джар) → 201
CAP=$(get_captcha)
if [ -z "$CAP" ]; then
echo "FAIL [hdr] X-Captcha-Debug пуст — дальнейшая регистрация невозможна"
FAIL=1
fi
_code=$(req POST /api/register.php "{\"email\":\"$EMAIL\",\"password\":\"$PASS\",\"captcha\":\"$CAP\"}" yes)
check "register с верной капчей" 201 "$_code"
# повторное использование той же капчи → 422 captcha (код одноразовый)
_code=$(req POST /api/register.php "{\"email\":\"$EMAIL\",\"password\":\"$PASS\",\"captcha\":\"$CAP\"}" yes)
check "register с той же капчей повторно" 422 "$_code"
check_body "повтор капчи: error=captcha" '"error":"captcha"'
# повторный register того же email (со свежей капчей) → 409
CAP=$(get_captcha)
_code=$(req POST /api/register.php "{\"email\":\"$EMAIL\",\"password\":\"$PASS\",\"captcha\":\"$CAP\"}" yes)
check "повторный register" 409 "$_code"
_code=$(req POST /api/register.php "{\"email\":\"other@example.com\",\"password\":\"$PASS\"}" no)
check "register без X-GoLearn-Client" 403 "$_code"
_code=$(req POST /api/login.php "{\"email\":\"$EMAIL\",\"password\":\"wrong-password\"}" yes)
check "login с неверным паролем" 401 "$_code"
_code=$(req POST /api/login.php "{\"email\":\"$EMAIL\",\"password\":\"$PASS\"}" yes)
check "login" 200 "$_code"
_code=$(req GET /api/me.php - no)
check "me" 200 "$_code"
check_body "me возвращает email" "\"email\":\"$EMAIL\""
_code=$(req PUT /api/progress.php '{"payload":{"lesson":3,"done":["a","b"]}}' yes)
check "PUT progress" 200 "$_code"
check_body "PUT progress отдаёт updatedAt" '"updatedAt":"'
_code=$(req GET /api/progress.php - no)
check "GET progress" 200 "$_code"
check_body "GET progress: payload совпадает" '"lesson":3'
check_body "GET progress: массив done совпадает" '"done":["a","b"]'
# тело > 256 КБ → 413
{ printf '{"payload":{"blob":"'; head -c 300000 /dev/zero | tr '\0' 'a'; printf '"}}'; } >"$BIG"
_code=$(req PUT /api/progress.php "@$BIG" yes)
check "PUT progress с телом >256 КБ" 413 "$_code"
# --- этап 8.1: восстановление пароля ---
_code=$(req POST /api/password-reset/request.php '{"email":"no-such-user@example.com"}' yes)
check "reset request: несуществующий email" 200 "$_code"
check_body "reset request несуществующий: ok" '"ok":true'
_code=$(req POST /api/password-reset/request.php "{\"email\":\"$EMAIL\"}" yes)
check "reset request: существующий email" 200 "$_code"
check_body "reset request существующий: ok" '"ok":true'
# письмо в тестовом режиме пишется в файл GOLEARN_TEST_MAIL
if [ -f "$MAIL" ] && grep -qF 'token=' "$MAIL"; then
echo "ok [mail] письмо содержит ссылку с token="
else
echo "FAIL [mail] письмо без token= — файл: $(cat "$MAIL" 2>/dev/null)"
FAIL=1
fi
TOKEN=$(tr -d '\r' <"$MAIL" 2>/dev/null \
| sed -n 's/.*token=\([0-9a-f]\{64\}\).*/\1/p' | tail -n 1)
if [ -n "$TOKEN" ]; then
echo "ok [mail] токен извлечён из письма"
else
echo "FAIL [mail] токен не извлекается из письма"
FAIL=1
fi
_code=$(req POST /api/password-reset/confirm.php '{"token":"0000000000000000000000000000000000000000000000000000000000000000","password":"whatever123"}' yes)
check "reset confirm: неверный токен" 400 "$_code"
check_body "reset confirm неверный: error=token_invalid" '"error":"token_invalid"'
_code=$(req POST /api/password-reset/confirm.php "{\"token\":\"$TOKEN\",\"password\":\"short\"}" yes)
check "reset confirm: пароль < 8" 422 "$_code"
_code=$(req POST /api/password-reset/confirm.php "{\"token\":\"$TOKEN\",\"password\":\"$NEWPASS\"}" yes)
check "reset confirm: верный токен" 200 "$_code"
check_body "reset confirm верный: ok" '"ok":true'
_code=$(req POST /api/login.php "{\"email\":\"$EMAIL\",\"password\":\"$PASS\"}" yes)
check "login со СТАРЫМ паролем после reset" 401 "$_code"
_code=$(req POST /api/login.php "{\"email\":\"$EMAIL\",\"password\":\"$NEWPASS\"}" yes)
check "login с НОВЫМ паролем после reset" 200 "$_code"
# rate limit reset request: 5 за 10 минут; уже сделано 2, 6-я → 429
_i=3
while [ "$_i" -le 5 ]; do
_code=$(req POST /api/password-reset/request.php "{\"email\":\"$EMAIL\"}" yes)
check "reset request, попытка $_i/5" 200 "$_code"
_i=$((_i + 1))
done
_code=$(req POST /api/password-reset/request.php "{\"email\":\"$EMAIL\"}" yes)
check "reset request: 6-я попытка за 10 минут" 429 "$_code"
check_body "reset request 429: error=rate_limited" '"error":"rate_limited"'
# rate limit login: 10 попыток за 10 минут, 11-я → 429
_i=1
while [ "$_i" -le 10 ]; do
_code=$(req POST /api/login.php "{\"email\":\"$EMAIL\",\"password\":\"bad-pass-$_i\"}" yes)
check "login неверный, попытка $_i/10" 401 "$_code"
_i=$((_i + 1))
done
_code=$(req POST /api/login.php "{\"email\":\"$EMAIL\",\"password\":\"bad-pass-11\"}" yes)
check "login: 11-я попытка за 10 минут" 429 "$_code"
check_body "429 error=rate_limited" '"error":"rate_limited"'
_code=$(req POST /api/logout.php - yes)
check "logout" 200 "$_code"
_code=$(req GET /api/me.php - no)
check "me после logout" 401 "$_code"
echo "---"
if [ "$FAIL" = "0" ]; then
echo "SMOKE OK"
else
echo "SMOKE FAILED"
fi
exit "$FAIL"