#!/bin/bash # ══════════════════════════════════════════════════════════════════════ # Smoke-тесты Morozovka Portal — без GUI, через curl + psql # Запуск: sudo bash tests/run_smoke.sh # Вывод: цветной PASS/FAIL + tests/last_run.log # ══════════════════════════════════════════════════════════════════════ set -uo pipefail # ── Настройки ───────────────────────────────────────────────────────── BASE="http://127.0.0.1:8000" PG_HOST="127.0.0.1" PG_USER="morozovka" PG_PASS="${PG_PASS:-rf-slf-1945}" PG_DB="morozovka_portal" LOG_FILE="$(dirname "$0")/last_run.log" COOKIE="/tmp/smoke_cookies_$$.txt" TMP="/tmp/smoke_$$" TS="$(date +%s)" TEST_USER="smoke_test_${TS}" TEST_PASS="SmokePass_${TS}!" TEST_EMAIL="${TEST_USER}@test.local" mkdir -p "$TMP" : > "$LOG_FILE" PASS=0 FAIL=0 TOTAL=0 START_TIME=$(date +%s) # ── Цвета ───────────────────────────────────────────────────────────── if [ -t 1 ]; then C_GREEN='\033[0;32m'; C_RED='\033[0;31m'; C_YELLOW='\033[1;33m' C_CYAN='\033[0;36m'; C_BOLD='\033[1m'; C_RESET='\033[0m' else C_GREEN=''; C_RED=''; C_YELLOW=''; C_CYAN=''; C_BOLD=''; C_RESET='' fi # ── Хелперы печати ──────────────────────────────────────────────────── section() { local msg="$1" echo echo -e "${C_BOLD}═══ $msg ═══${C_RESET}" echo "--- $msg ---" >> "$LOG_FILE" } info() { echo -e "${C_CYAN} → $1${C_RESET}" echo " INFO: $1" >> "$LOG_FILE" } pass() { PASS=$((PASS+1)); TOTAL=$((TOTAL+1)) echo -e " ${C_GREEN}✓${C_RESET} $1" echo " PASS: $1" >> "$LOG_FILE" } fail() { FAIL=$((FAIL+1)); TOTAL=$((TOTAL+1)) echo -e " ${C_RED}✗${C_RESET} $1" echo " ожидалось: $2" echo " получено: $3" echo " FAIL: $1" >> "$LOG_FILE" echo " expected: $2" >> "$LOG_FILE" echo " actual: $3" >> "$LOG_FILE" } # ── Хелперы проверок ────────────────────────────────────────────────── check_status() { local name="$1" expected="$2" actual="$3" if [ "$actual" = "$expected" ]; then pass "$name"; else fail "$name" "$expected" "$actual"; fi } check_contains() { local name="$1" pattern="$2" file="$3" if grep -qF "$pattern" "$file" 2>/dev/null; then pass "$name"; else fail "$name" "contains '$pattern'" "не найдено в $file"; fi } check_not_empty() { local name="$1" val="$2" if [ -n "$val" ] && [ "$val" != "0" ]; then pass "$name"; else fail "$name" "not empty" "пусто"; fi } check_equal() { local name="$1" expected="$2" actual="$3" if [ "$actual" = "$expected" ]; then pass "$name"; else fail "$name" "$expected" "$actual"; fi } # ── psql ────────────────────────────────────────────────────────────── psql_run() { PGPASSWORD="$PG_PASS" psql -h "$PG_HOST" -U "$PG_USER" -d "$PG_DB" -tAc "$1" 2>/dev/null } psql_val() { psql_run "$1" | tr -d ' \n' } # ── curl обёртки ────────────────────────────────────────────────────── http_code_get() { curl -sS -o "$2" -w "%{http_code}" -b "$COOKIE" "$1" 2>/dev/null } http_code_post_form() { curl -sS -o "$2" -w "%{http_code}" -b "$COOKIE" -c "$COOKIE" -X POST "$1" -d "$3" 2>/dev/null } http_code_post_json() { curl -sS -o "$2" -w "%{http_code}" -b "$COOKIE" -c "$COOKIE" -X POST "$1" \ -H "Content-Type: application/json" -d "$3" 2>/dev/null } # ── Cleanup ─────────────────────────────────────────────────────────── cleanup() { info "Cleanup тестовых данных…" local USER_ID="${TEST_USER_ID:-}" if [ -n "$USER_ID" ]; then # Удаляем строго по ID, ничего чужого не заденем psql_run "DELETE FROM portalfile WHERE uploaded_by = $USER_ID" >/dev/null psql_run "DELETE FROM fileversion WHERE uploaded_by = $USER_ID" >/dev/null psql_run "DELETE FROM filefolderacl WHERE user_id = $USER_ID" >/dev/null psql_run "DELETE FROM filespaceacl WHERE user_id = $USER_ID" >/dev/null psql_run "DELETE FROM filefolder WHERE created_by = $USER_ID" >/dev/null psql_run "DELETE FROM filespace WHERE owner_id = $USER_ID" >/dev/null psql_run "DELETE FROM project WHERE owner_id = $USER_ID" >/dev/null psql_run "DELETE FROM workshop WHERE lead_id = $USER_ID" >/dev/null psql_run "DELETE FROM projecttask WHERE created_by = $USER_ID" >/dev/null psql_run "DELETE FROM projectpost WHERE author_id = $USER_ID" >/dev/null psql_run "DELETE FROM workshoppost WHERE author_id = $USER_ID" >/dev/null psql_run "DELETE FROM useronboarding WHERE user_id = $USER_ID" >/dev/null psql_run "DELETE FROM eventsubscriber WHERE user_id = $USER_ID" >/dev/null psql_run "DELETE FROM usersession WHERE user_id = $USER_ID" >/dev/null psql_run "DELETE FROM notification WHERE user_id = $USER_ID" >/dev/null psql_run "DELETE FROM \"user\" WHERE id = $USER_ID" >/dev/null fi rm -f "$COOKIE" rm -rf "$TMP" info "Cleanup завершён" } trap cleanup EXIT # ══════════════════════════════════════════════════════════════════════ # НАЧАЛО # ══════════════════════════════════════════════════════════════════════ echo -e "${C_BOLD}╔══════════════════════════════════════════════════════╗${C_RESET}" echo -e "${C_BOLD}║ Morozovka Portal — Smoke Tests (no GUI) ║${C_RESET}" echo -e "${C_BOLD}╚══════════════════════════════════════════════════════╝${C_RESET}" echo "Start: $(date -Is)" echo "Base: $BASE" echo "User: $TEST_USER" echo "Log: $LOG_FILE" echo echo "Start: $(date -Is)" >> "$LOG_FILE" echo "Base: $BASE" >> "$LOG_FILE" echo "User: $TEST_USER" >> "$LOG_FILE" # ── 0. Проверка, что сервис жив ─────────────────────────────────────── section "0. Health" CODE=$(http_code_get "$BASE/healthz" "$TMP/healthz.json") check_status "GET /healthz → 200" "200" "$CODE" check_contains "healthz → status:ok" '"status":"ok"' "$TMP/healthz.json" check_contains "healthz → db:ok" '"db":"ok"' "$TMP/healthz.json" # ── 1. Публичные роуты ──────────────────────────────────────────────── section "1. Публичные роуты" check_status "GET / → 200" "200" "$(http_code_get "$BASE/" "$TMP/index.html")" check_status "GET /schedule → 200" "200" "$(http_code_get "$BASE/schedule" "$TMP/schedule.html")" check_status "GET /feed → 200" "200" "$(http_code_get "$BASE/feed" "$TMP/feed.html")" check_status "GET /projects → 200" "200" "$(http_code_get "$BASE/projects" "$TMP/projects.html")" check_status "GET /workshops → 200" "200" "$(http_code_get "$BASE/workshops" "$TMP/workshops.html")" check_status "GET /login → 200" "200" "$(http_code_get "$BASE/login" "$TMP/login.html")" check_status "GET /register → 200" "200" "$(http_code_get "$BASE/register" "$TMP/register.html")" check_status "GET /forgot_password → 200" "200" "$(http_code_get "$BASE/forgot_password" "$TMP/forgot.html")" # Защищённые без cookie → 302 на login rm -f "$COOKIE" check_status "GET /files без cookie → 302" "302" "$(http_code_get "$BASE/files" "$TMP/files_noauth.html")" check_status "GET /dashboard без cookie → 302" "302" "$(http_code_get "$BASE/dashboard" "$TMP/dash_noauth.html")" check_status "GET /onboarding без cookie → 302" "302" "$(http_code_get "$BASE/onboarding" "$TMP/onb_noauth.html")" # ── 2. Регистрация тестового пользователя ──────────────────────────── section "2. Регистрация пользователя" CODE=$(http_code_post_form "$BASE/register" "$TMP/reg_resp.html" \ "username=${TEST_USER}&email=${TEST_EMAIL}&password=${TEST_PASS}&full_name=Smoke+Test&organization=%D0%9D%D0%9D%D0%93%D0%A3") check_status "POST /register → 200" "200" "$CODE" TEST_USER_ID=$(psql_val "SELECT id FROM \"user\" WHERE username = '$TEST_USER'") check_not_empty "user создан в БД" "$TEST_USER_ID" if [ -z "$TEST_USER_ID" ] || [ "$TEST_USER_ID" = "0" ]; then echo -e "${C_RED}КРИТИЧНО: тестовый юзер не создан. Останавливаюсь.${C_RESET}" exit 1 fi info "user_id = $TEST_USER_ID" # Промоут до admin (иначе require_role заблокирует создание проектов/МК) psql_run "UPDATE \"user\" SET role = 'admin', is_verified = true WHERE id = $TEST_USER_ID" >/dev/null check_equal "роль admin установлена" "admin" "$(psql_val "SELECT role FROM \"user\" WHERE id = $TEST_USER_ID")" # ── 3. Логин ────────────────────────────────────────────────────────── section "3. Логин" rm -f "$COOKIE" CODE=$(curl -sS -o "$TMP/login_resp.html" -w "%{http_code}" -c "$COOKIE" -X POST "$BASE/login" \ -d "username=${TEST_USER}&password=${TEST_PASS}" 2>/dev/null) check_status "POST /login → 302" "302" "$CODE" check_status "GET /dashboard после логина → 200" "200" "$(http_code_get "$BASE/dashboard" "$TMP/dashboard.html")" check_contains "dashboard содержит 'Ближайшие события'" "Ближайшие события" "$TMP/dashboard.html" # ── 4. Онбординг ────────────────────────────────────────────────────── section "4. Онбординг" check_status "GET /onboarding → 200" "200" "$(http_code_get "$BASE/onboarding" "$TMP/onb.html")" CODE=$(http_code_post_json "$BASE/onboarding/mark" "$TMP/onb_mark.json" \ '{"key":"visited_schedule","value":true}') check_status "POST /onboarding/mark → 200" "200" "$CODE" CODE=$(http_code_post_form "$BASE/onboarding/complete" "$TMP/onb_complete.html" "") check_status "POST /onboarding/complete → 302" "302" "$CODE" # ── 5. Уведомления ──────────────────────────────────────────────────── section "5. Уведомления" check_status "GET /api/notifications → 200" "200" "$(http_code_get "$BASE/api/notifications" "$TMP/notif.json")" check_contains "notifications JSON содержит 'items'" '"items"' "$TMP/notif.json" CODE=$(http_code_post_json "$BASE/api/notifications/read-all" "$TMP/notif_read.json" "{}") check_status "POST /api/notifications/read-all → 200" "200" "$CODE" # ── 6. Расписание + ассистент ──────────────────────────────────────── section "6. Расписание + ассистент" check_status "GET /schedule → 200" "200" "$(http_code_get "$BASE/schedule" "$TMP/sched.html")" check_contains "/schedule содержит 'cal-body'" 'cal-body' "$TMP/sched.html" check_contains "/schedule содержит 'chat-toggle'" 'chat-toggle' "$TMP/sched.html" # Ассистент — 5 интентов for M in "Где сейчас?" "Когда следующая пара?" "Что сегодня?" "Он опаздывает?" "Как связаться?"; do CODE=$(http_code_post_json "$BASE/api/assistant" "$TMP/assist.json" "{\"message\":\"$M\"}") check_status "assistant '$M' → 200" "200" "$CODE" check_contains " ответ содержит 'reply'" '"reply"' "$TMP/assist.json" done # ICS export CODE=$(http_code_get "$BASE/schedule/export.ics" "$TMP/sched.ics") check_status "GET /schedule/export.ics → 200" "200" "$CODE" check_contains "ICS содержит 'BEGIN:VCALENDAR'" "BEGIN:VCALENDAR" "$TMP/sched.ics" # ── 7. Проекты ──────────────────────────────────────────────────────── section "7. Проекты" PROJ_NAME="SmokeTest Project $TS" CODE=$(http_code_post_form "$BASE/projects/new" "$TMP/proj_new.html" \ "title=$(printf '%s' "$PROJ_NAME" | sed 's/ /+/g')&description=auto&status=in_progress&visibility=internal") check_status "POST /projects/new → 302" "302" "$CODE" PROJ_ID=$(psql_val "SELECT id FROM project WHERE title = '$PROJ_NAME' ORDER BY id DESC LIMIT 1") check_not_empty "проект создан" "$PROJ_ID" info "project_id = $PROJ_ID" check_status "GET /projects/ → 200" "200" "$(http_code_get "$BASE/projects/$PROJ_ID" "$TMP/proj.html")" check_status "GET /projects//board → 200" "200" "$(http_code_get "$BASE/projects/$PROJ_ID/board" "$TMP/board.html")" # Задача CODE=$(http_code_post_form "$BASE/projects/$PROJ_ID/tasks/new" "$TMP/task.html" \ "title=Smoke+Task&priority=medium&status=backlog") check_status "POST /tasks/new → 302" "302" "$CODE" TASK_ID=$(psql_val "SELECT id FROM projecttask WHERE project_id = $PROJ_ID ORDER BY id DESC LIMIT 1") check_not_empty "задача создана" "$TASK_ID" # ── 8. Мастер-классы ───────────────────────────────────────────────── section "8. Мастер-классы" WS_NAME="SmokeTest WS $TS" CODE=$(http_code_post_form "$BASE/workshops/new" "$TMP/ws_new.html" \ "title=$(printf '%s' "$WS_NAME" | sed 's/ /+/g')&description=auto&category=other&difficulty=beginner&status=published&visibility=public&lead_id=$TEST_USER_ID") check_status "POST /workshops/new → 302" "302" "$CODE" WS_ID=$(psql_val "SELECT id FROM workshop WHERE title = '$WS_NAME' ORDER BY id DESC LIMIT 1") check_not_empty "МК создан" "$WS_ID" info "workshop_id = $WS_ID" check_status "GET /workshops/ → 200" "200" "$(http_code_get "$BASE/workshops/$WS_ID" "$TMP/ws.html")" # Регистрация на МК CODE=$(http_code_post_form "$BASE/workshops/$WS_ID/register" "$TMP/ws_reg.html" "") check_status "POST /workshops//register → 302" "302" "$CODE" REG_COUNT=$(psql_val "SELECT COUNT(*) FROM workshopregistration WHERE workshop_id = $WS_ID AND user_id = $TEST_USER_ID") check_equal "регистрация в БД" "1" "$REG_COUNT" # ── 9. Файлы v2.0 ──────────────────────────────────────────────────── section "9. Файлы v2.0 — пространство/папка/файл/версии/ACL/move/delete" SPACE_NAME="SmokeTest Space $TS" CODE=$(http_code_post_form "$BASE/files/space/new" "$TMP/sp_new.html" \ "name=$(printf '%s' "$SPACE_NAME" | sed 's/ /+/g')&description=auto&visibility=internal") check_status "POST /files/space/new → 302" "302" "$CODE" SPACE_ID=$(psql_val "SELECT id FROM filespace WHERE name = '$SPACE_NAME' ORDER BY id DESC LIMIT 1") check_not_empty "пространство создано" "$SPACE_ID" info "space_id = $SPACE_ID" check_status "GET /files/space/ → 200" "200" "$(http_code_get "$BASE/files/space/$SPACE_ID" "$TMP/space.html")" # Папка FOLDER_NAME="TestFolder_$TS" CODE=$(http_code_post_form "$BASE/files/folder/new" "$TMP/folder_new.html" \ "space_id=$SPACE_ID&parent_id=&name=$FOLDER_NAME&description=auto") check_status "POST /files/folder/new → 302" "302" "$CODE" FOLDER_ID=$(psql_val "SELECT id FROM filefolder WHERE name = '$FOLDER_NAME' AND space_id = $SPACE_ID") check_not_empty "папка создана" "$FOLDER_ID" info "folder_id = $FOLDER_ID" check_status "GET /files/folder/ → 200" "200" "$(http_code_get "$BASE/files/folder/$FOLDER_ID" "$TMP/folder.html")" # Файл echo "test content v1 — $(date -Is)" > "$TMP/upload.txt" CODE=$(curl -sS -o "$TMP/up_resp.html" -w "%{http_code}" -b "$COOKIE" -c "$COOKIE" \ -X POST "$BASE/files/upload" \ -F "file=@$TMP/upload.txt;filename=smoke_test_$TS.txt" \ -F "space_id=$SPACE_ID" -F "folder_id=$FOLDER_ID" 2>/dev/null) check_status "POST /files/upload → 302" "302" "$CODE" FILE_ID=$(psql_val "SELECT id FROM portalfile WHERE space_id = $SPACE_ID AND folder_id = $FOLDER_ID ORDER BY id DESC LIMIT 1") check_not_empty "файл создан" "$FILE_ID" info "file_id = $FILE_ID" # Проверка FileVersion(1) FILE_VERSION=$(psql_val "SELECT version FROM portalfile WHERE id = $FILE_ID") check_equal "file.version = 1" "1" "$FILE_VERSION" CUR_VER_ID=$(psql_val "SELECT current_version_id FROM portalfile WHERE id = $FILE_ID") check_not_empty "current_version_id задан" "$CUR_VER_ID" VER_COUNT=$(psql_val "SELECT COUNT(*) FROM fileversion WHERE file_id = $FILE_ID") check_equal "fileversion count = 1" "1" "$VER_COUNT" # Скачать текущую check_status "GET /files/download/ → 200" "200" "$(http_code_get "$BASE/files/download/$FILE_ID" "$TMP/dl1.txt")" check_contains "скачано v1" "test content v1" "$TMP/dl1.txt" # История check_status "GET /files//history → 200" "200" "$(http_code_get "$BASE/files/$FILE_ID/history" "$TMP/history.html")" check_contains "история содержит 'v1'" "v1" "$TMP/history.html" # Новая версия echo "test content v2 — $(date -Is)" > "$TMP/upload_v2.txt" CODE=$(curl -sS -o "$TMP/newv.html" -w "%{http_code}" -b "$COOKIE" -c "$COOKIE" \ -X POST "$BASE/files/$FILE_ID/new_version" \ -F "file=@$TMP/upload_v2.txt;filename=smoke_test_$TS.txt" \ -F "comment=Auto v2" 2>/dev/null) check_status "POST /files//new_version → 302" "302" "$CODE" check_equal "version = 2" "2" "$(psql_val "SELECT version FROM portalfile WHERE id = $FILE_ID")" check_equal "fileversion count = 2" "2" "$(psql_val "SELECT COUNT(*) FROM fileversion WHERE file_id = $FILE_ID")" # Скачать v1 V1_ID=$(psql_val "SELECT id FROM fileversion WHERE file_id = $FILE_ID AND version_num = 1") check_status "GET /files//version//download → 200" "200" \ "$(http_code_get "$BASE/files/$FILE_ID/version/$V1_ID/download" "$TMP/dl_v1.txt")" check_contains "скачано содержимое v1" "test content v1" "$TMP/dl_v1.txt" # Скачать текущую (v2) check_status "GET /files/download/ (v2) → 200" "200" "$(http_code_get "$BASE/files/download/$FILE_ID" "$TMP/dl_v2.txt")" check_contains "скачано v2" "test content v2" "$TMP/dl_v2.txt" # ACL пространства CODE=$(http_code_post_form "$BASE/files/space/$SPACE_ID/acl/add" "$TMP/acl.html" \ "username=$TEST_USER&role=editor") check_status "POST /files/space//acl/add → 302" "302" "$CODE" # (нельзя добавить себя-владельца, роут вернёт redirect без изменений — это OK, проверяем что не 500) ACL_PAGE_CODE=$(http_code_get "$BASE/files/space/$SPACE_ID/acl" "$TMP/acl_page.html") check_status "GET /files/space//acl → 200" "200" "$ACL_PAGE_CODE" # Move: файл из пространства в NULL CODE=$(http_code_post_form "$BASE/files/$FILE_ID/move" "$TMP/move1.html" \ "space_id=&folder_id=") check_status "POST /files//move → NULL → 302" "302" "$CODE" check_equal "space_id обнулён" "" "$(psql_val "SELECT space_id FROM portalfile WHERE id = $FILE_ID AND space_id IS NOT NULL")" check_equal "version сброшен в 1" "1" "$(psql_val "SELECT version FROM portalfile WHERE id = $FILE_ID")" check_equal "current_version_id = NULL" "" "$(psql_val "SELECT current_version_id FROM portalfile WHERE id = $FILE_ID AND current_version_id IS NOT NULL")" check_equal "fileversion удалены" "0" "$(psql_val "SELECT COUNT(*) FROM fileversion WHERE file_id = $FILE_ID")" # Move обратно в пространство CODE=$(http_code_post_form "$BASE/files/$FILE_ID/move" "$TMP/move2.html" \ "space_id=$SPACE_ID&folder_id=$FOLDER_ID") check_status "POST /files//move → обратно в space → 302" "302" "$CODE" check_equal "space_id восстановлен" "$SPACE_ID" "$(psql_val "SELECT space_id FROM portalfile WHERE id = $FILE_ID")" check_equal "fileversion count = 1 (миграция)" "1" "$(psql_val "SELECT COUNT(*) FROM fileversion WHERE file_id = $FILE_ID")" # Удаление файла CODE=$(http_code_post_form "$BASE/files/$FILE_ID/delete" "$TMP/fdel.html" "next=/files/space/$SPACE_ID") check_status "POST /files//delete → 302" "302" "$CODE" FILE_EXISTS=$(psql_val "SELECT COUNT(*) FROM portalfile WHERE id = $FILE_ID") check_equal "файл удалён из БД" "0" "$FILE_EXISTS" # Удаление ПАПКИ (главная проверка!) CODE=$(http_code_post_form "$BASE/files/folder/$FOLDER_ID/delete" "$TMP/folder_del.html" "") check_status "POST /files/folder//delete → 302" "302" "$CODE" FOLDER_EXISTS=$(psql_val "SELECT COUNT(*) FROM filefolder WHERE id = $FOLDER_ID") check_equal "папка удалена из БД" "0" "$FOLDER_EXISTS" # Удаление ПРОСТРАНСТВА CODE=$(http_code_post_form "$BASE/files/space/$SPACE_ID/delete" "$TMP/space_del.html" "") check_status "POST /files/space//delete → 302" "302" "$CODE" SPACE_EXISTS=$(psql_val "SELECT COUNT(*) FROM filespace WHERE id = $SPACE_ID") check_equal "пространство удалено из БД" "0" "$SPACE_EXISTS" # ── 10. Вебхук ─────────────────────────────────────────────────────── section "10. Вебхук Gitea" CODE=$(curl -sS -o "$TMP/wh_bad.json" -w "%{http_code}" -X POST "$BASE/webhooks/gitea" \ -H "X-Gitea-Event: push" -H "X-Gitea-Signature: DEADBEEF" \ -H "Content-Type: application/json" -d '{}' 2>/dev/null) check_status "POST /webhooks/gitea (плохая подпись) → 401" "401" "$CODE" # Валидная подпись SECRET=$(grep '^GITEA_WEBHOOK_SECRET=' /opt/morozovka-portal/.env 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'") if [ -n "$SECRET" ]; then BODY='{"repository":{"full_name":"test/nonexistent"},"pusher":{"username":"smoke"},"ref":"refs/heads/main","commits":[]}' SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" 2>/dev/null | awk '{print $NF}') CODE=$(curl -sS -o "$TMP/wh_ok.json" -w "%{http_code}" -X POST "$BASE/webhooks/gitea" \ -H "X-Gitea-Event: push" -H "X-Gitea-Signature: $SIG" \ -H "Content-Type: application/json" -d "$BODY" 2>/dev/null) check_status "POST /webhooks/gitea (валидная подпись) → 200" "200" "$CODE" else info "GITEA_WEBHOOK_SECRET не найден в .env — пропускаю валидную проверку" fi # ── 11. Логаут ──────────────────────────────────────────────────────── section "11. Логаут" check_status "GET /logout → 302" "302" "$(http_code_get "$BASE/logout" "$TMP/logout.html")" rm -f "$COOKIE" check_status "GET /dashboard после logout → 302" "302" "$(http_code_get "$BASE/dashboard" "$TMP/dash_after.html")" # ══════════════════════════════════════════════════════════════════════ # ИТОГ # ══════════════════════════════════════════════════════════════════════ END_TIME=$(date +%s) DURATION=$((END_TIME - START_TIME)) echo echo -e "${C_BOLD}╔══════════════════════════════════════════════════════╗${C_RESET}" if [ "$FAIL" -eq 0 ]; then echo -e "${C_BOLD}║${C_RESET} ${C_GREEN}${C_BOLD}ВСЕ ТЕСТЫ ПРОШЛИ${C_RESET} ${C_BOLD}║${C_RESET}" else echo -e "${C_BOLD}║${C_RESET} ${C_RED}${C_BOLD}ЕСТЬ ПРОВАЛЫ: $FAIL${C_RESET} ${C_BOLD}║${C_RESET}" fi echo -e "${C_BOLD}╚══════════════════════════════════════════════════════╝${C_RESET}" echo -e " ${C_GREEN}PASS:${C_RESET} $PASS" echo -e " ${C_RED}FAIL:${C_RESET} $FAIL" echo -e " Всего: $TOTAL" echo -e " Длительность: ${DURATION}s" echo -e " Лог: ${C_CYAN}$LOG_FILE${C_RESET}" echo "" >> "$LOG_FILE" echo "=== ИТОГ ===" >> "$LOG_FILE" echo "PASS: $PASS" >> "$LOG_FILE" echo "FAIL: $FAIL" >> "$LOG_FILE" echo "TOTAL: $TOTAL" >> "$LOG_FILE" echo "DURATION: ${DURATION}s" >> "$LOG_FILE" echo "End: $(date -Is)" >> "$LOG_FILE" if [ "$FAIL" -eq 0 ]; then exit 0; else exit 1; fi