chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца (снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной истории. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
1c85091186
184 changed files with 33303 additions and 0 deletions
288
server/tests/smoke.sh
Normal file
288
server/tests/smoke.sh
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
#!/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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue