#!/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 ' 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"