From c1e3039d15368d07e213e5167b08b5ead066d14f Mon Sep 17 00:00:00 2001 From: Naeel Date: Sun, 3 May 2026 14:02:12 +0300 Subject: [PATCH] =?UTF-8?q?tests:=20=D1=82=D0=B5=D1=81=D1=82-=D1=81=D0=BA?= =?UTF-8?q?=D1=80=D0=B8=D0=BF=D1=82=D1=8B=20v2=20=E2=80=94=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D1=8B=D0=B9=20=D1=81=D1=8C=D1=8E=D1=82=20PASS=3D8?= =?UTF-8?q?=20FAIL=3D0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib.sh: утилиты (json_field, wait_invoke, print_summary) - run_all.sh: запуск всех скриптов + финальная сводка - pre_cleanup.sh: очистка тест-namespace-ов перед прогоном - test_auth.sh: 14/14 PASS — авторизация, namespace детерминизм - test_functions_crud.sh: 29/29 PASS — CRUD, валидация, >1MB - test_ttl.sh: 10/10 PASS — TTL=15m/1h/2d, невалидные - test_isolation.sh: 11/11 PASS — изоляция пользователей - test_update.sh: 10/10 PASS — PUT /code v1→v2→v3 (livetest) - test_invoke.sh: 9/9 PASS — NodeJS/Python invoke (livetest) - test_stress.sh: 3/3 PASS — 8 пользователей параллельно (livetest) - test_linters.sh: 31/31 PASS — синтаксис 5 языков --- scripts/lib.sh | 91 +++++++++++++++ scripts/pre_cleanup.sh | 38 ++++++ scripts/run_all.sh | 101 ++++++++++++++++ scripts/test_auth.sh | 107 +++++++++++++++++ scripts/test_functions_crud.sh | 207 +++++++++++++++++++++++++++++++++ scripts/test_invoke.sh | 90 ++++++++++++++ scripts/test_isolation.sh | 162 ++++++++++++++++++++++++++ scripts/test_linters.sh | 0 scripts/test_stress.sh | 158 +++++++++++++++++++++++++ scripts/test_ttl.sh | 93 +++++++++++++++ scripts/test_update.sh | 105 +++++++++++++++++ 11 files changed, 1152 insertions(+) create mode 100755 scripts/lib.sh create mode 100755 scripts/pre_cleanup.sh create mode 100755 scripts/run_all.sh create mode 100755 scripts/test_auth.sh create mode 100755 scripts/test_functions_crud.sh create mode 100755 scripts/test_invoke.sh create mode 100755 scripts/test_isolation.sh mode change 100644 => 100755 scripts/test_linters.sh create mode 100755 scripts/test_stress.sh create mode 100755 scripts/test_ttl.sh create mode 100755 scripts/test_update.sh diff --git a/scripts/lib.sh b/scripts/lib.sh new file mode 100755 index 0000000..1f547a8 --- /dev/null +++ b/scripts/lib.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# lib.sh — общие утилиты для тест-скриптов fission-console. +# Подключается через: source "$(dirname "$0")/lib.sh" + +BASE="${BASE:-https://fission.kube5s.ru/console/api}" +PASS=0 +FAIL=0 + +pass() { PASS=$((PASS+1)); echo " PASS: $1"; } +fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; } + +check_http() { + local got="$1" want="$2" label="$3" + [ "$got" = "$want" ] && pass "$label (HTTP $got)" || fail "$label — got $got, want $want" +} + +# json_field PAYLOAD KEY — извлекает строковое значение ключа из JSON +json_field() { + local payload="$1" key="$2" + printf '%s' "$payload" | python3 -c " +import json, sys +key = sys.argv[1] +try: + d = json.load(sys.stdin) + v = d.get(key) + if v is None: + print('') + elif isinstance(v, (dict, list)): + print(json.dumps(v)) + else: + print(v) +except Exception: + print('') +" "$key" 2>/dev/null +} + +# json_escape — читает stdin, возвращает JSON-строку +json_escape() { + python3 -c 'import json, sys; print(json.dumps(sys.stdin.read()))' +} + +# json_len PAYLOAD — длина JSON-массива +json_len() { + printf '%s' "$1" | python3 -c "import json,sys +try: + print(len(json.load(sys.stdin))) +except Exception: + print(0)" 2>/dev/null +} + +# wait_invoke SUB FN EXPECTED [TRIES=12] [DELAY=5] +# Ждёт пока invoke вернёт status=200 и тело содержит EXPECTED. +# Возвращает 0 при успехе, 1 при таймауте. +wait_invoke() { + local sub="$1" fn="$2" expected="$3" + local tries="${4:-12}" delay="${5:-5}" + local try resp status raw err + for try in $(seq 1 "$tries"); do + resp=$(curl -s --max-time 50 -X POST "${BASE}/functions/${fn}/invoke" \ + -H "X-Auth-Token: ${sub}" -H "Content-Type: application/json" -d '{}') + status=$(json_field "$resp" status) + raw=$(json_field "$resp" response_raw) + err=$(json_field "$resp" error) + echo " TRY=$try fn=$fn status=$status raw=$(printf '%s' "$raw" | head -c 80) err=$(printf '%s' "$err" | head -c 80)" + if [ "$status" = "200" ] && printf '%s' "$raw" | grep -qF "$expected"; then + return 0 + fi + [ "$try" -lt "$tries" ] && sleep "$delay" + done + return 1 +} + +# mk_run_id — возвращает уникальный 8-символьный hex ID +mk_run_id() { + (date +%s%N 2>/dev/null; od -An -N4 -tu4 /dev/urandom 2>/dev/null) \ + | sha256sum | head -c 8 +} + +# setup_cleanup CLEANUP_FN — регистрирует trap EXIT +setup_cleanup() { + trap "$1" EXIT +} + +# print_summary — итог тестов +print_summary() { + echo "" + echo "========================================" + printf " ИТОГ: PASS=%d FAIL=%d\n" "$PASS" "$FAIL" + echo "========================================" + [ "$FAIL" -eq 0 ] +} diff --git a/scripts/pre_cleanup.sh b/scripts/pre_cleanup.sh new file mode 100755 index 0000000..15f03e0 --- /dev/null +++ b/scripts/pre_cleanup.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# pre_cleanup.sh — удаляет ВСЕ тест-namespace-ы fission от прошлых прогонов. +# Паттерны: fission- для namespace-ов созданных тест-субами вида +# crud-*/ttl-*/invoke-*/update-*/isolation-*/stress-*/alice-*/bob-*/ +# linter-*@test.local +# Вызывается из run_all.sh с KUBECTL_DIRECT=1. +set -uo pipefail + +if [ "${KUBECTL_DIRECT:-0}" != "1" ]; then + echo "pre_cleanup.sh: нужен KUBECTL_DIRECT=1 (должен запускаться на VM)" >&2 + exit 1 +fi + +CLEANUP="${BASH_SOURCE%/*}/cleanup_test_namespaces.sh" +if [ ! -x "$CLEANUP" ]; then + echo "pre_cleanup.sh: cleanup_test_namespaces.sh не найден" >&2 + exit 1 +fi + +# Список тест-субов создаёт namespace с именем fission-. +# Получить все fission-namespace-ы кроме системного fission: +NS_LIST=$(kubectl get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | \ + grep -E '^fission-[0-9a-f]{16}$' || true) + +if [ -z "$NS_LIST" ]; then + echo "pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем." + exit 0 +fi + +COUNT=$(echo "$NS_LIST" | wc -l) +echo "pre_cleanup.sh: найдено $COUNT тест-namespace-ов, удаляем..." + +# cleanup_test_namespaces.sh принимает email-субы, а не namespace-ы. +# Но мы можем вызвать kubectl delete напрямую для namespace-ов. +echo "$NS_LIST" | xargs -r kubectl delete namespace --ignore-not-found 2>&1 | \ + grep -v "^$" || true + +echo "pre_cleanup.sh: очистка завершена." diff --git a/scripts/run_all.sh b/scripts/run_all.sh new file mode 100755 index 0000000..be828bd --- /dev/null +++ b/scripts/run_all.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# run_all.sh — запускает все тест-скрипты по очереди, выводит итог. +# Каждый скрипт независим и имеет собственный cleanup. +# При падении одного скрипта остальные продолжают выполняться. +# +# Запуск: bash run_all.sh +# Запуск одного: bash run_all.sh test_auth.sh +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +TOTAL_PASS=0 +TOTAL_FAIL=0 +FAILED_SCRIPTS=() + +run_script() { + local script="$1" + local name + name=$(basename "$script") + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " ЗАПУСК: $name" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + local exit_code=0 + bash "$script" || exit_code=$? + + if [ "$exit_code" -eq 0 ]; then + echo -e "${GREEN} ✓ $name — PASSED${NC}" + TOTAL_PASS=$((TOTAL_PASS+1)) + else + echo -e "${RED} ✗ $name — FAILED (exit $exit_code)${NC}" + TOTAL_FAIL=$((TOTAL_FAIL+1)) + FAILED_SCRIPTS+=("$name") + fi +} + +# Список скриптов в порядке выполнения +# (быстрые сначала, invoke/stress — последними) +SCRIPTS=( + "${ROOT_DIR}/test_auth.sh" + "${ROOT_DIR}/test_functions_crud.sh" + "${ROOT_DIR}/test_ttl.sh" + "${ROOT_DIR}/test_isolation.sh" + "${ROOT_DIR}/test_update.sh" + "${ROOT_DIR}/test_invoke.sh" + "${ROOT_DIR}/test_linters.sh" + "${ROOT_DIR}/test_stress.sh" +) + +# Если передан аргумент — запускаем только его +if [ "$#" -ge 1 ]; then + SCRIPTS=("${ROOT_DIR}/$1") +fi + +# Предварительная очистка тест-namespace-ов от прошлых прогонов. +# Запускается только если cleanup_test_namespaces.sh умеет wildcard. +PRE_CLEANUP="${ROOT_DIR}/pre_cleanup.sh" +if [ -x "$PRE_CLEANUP" ]; then + echo "" + echo ">>> Предварительная очистка тест-namespace-ов..." + KUBECTL_DIRECT=1 bash "$PRE_CLEANUP" 2>/dev/null || true +fi + +echo "" +echo "╔══════════════════════════════════════╗" +echo "║ fission-console — полный тест-сьют ║" +echo "╚══════════════════════════════════════╝" + +START_TIME=$(date +%s) + +for script in "${SCRIPTS[@]}"; do + if [ ! -f "$script" ]; then + echo -e "${YELLOW} SKIP: $script не найден${NC}" + continue + fi + run_script "$script" +done + +END_TIME=$(date +%s) +ELAPSED=$((END_TIME - START_TIME)) + +echo "" +echo "╔══════════════════════════════════════╗" +printf "║ Скриптов: PASS=%-3d FAIL=%-3d %3ds ║\n" "$TOTAL_PASS" "$TOTAL_FAIL" "$ELAPSED" +echo "╚══════════════════════════════════════╝" + +if [ "${#FAILED_SCRIPTS[@]}" -gt 0 ]; then + echo "" + echo "Упавшие скрипты:" + for s in "${FAILED_SCRIPTS[@]}"; do + echo -e " ${RED}✗ $s${NC}" + done +fi + +[ "$TOTAL_FAIL" -eq 0 ] diff --git a/scripts/test_auth.sh b/scripts/test_auth.sh new file mode 100755 index 0000000..3aea09c --- /dev/null +++ b/scripts/test_auth.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# test_auth.sh — тестирует аутентификацию: +# - POST /console/api/auth (login endpoint) +# - X-Auth-Token header (authMiddleware) +# - Authorization: Bearer header +# - DemoAuth: любая строка ≥6 символов +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" +RUN_ID=$(mk_run_id) +SUB="auth-test-${RUN_ID}@test.local" + +cleanup() { + if [ -x "$NS_CLEANUP_SCRIPT" ]; then + KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$SUB" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_auth.sh run=$RUN_ID" +echo "========================================" + +# ── POST /auth ────────────────────────────────────────────────────────────── + +echo "" +echo ">>> POST /auth: пустой токен → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \ + -H "Content-Type: application/json" -d '{"token":""}') +check_http "$(echo "$R" | tail -1)" "400" "POST /auth пустой token → 400" + +echo "" +echo ">>> POST /auth: токен < 6 символов → 401" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \ + -H "Content-Type: application/json" -d '{"token":"ab"}') +check_http "$(echo "$R" | tail -1)" "401" "POST /auth токен 2 символа → 401" + +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \ + -H "Content-Type: application/json" -d '{"token":"abcde"}') +check_http "$(echo "$R" | tail -1)" "401" "POST /auth токен 5 символов → 401" + +echo "" +echo ">>> POST /auth: токен ≥6 символов → 200" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \ + -H "Content-Type: application/json" -d "{\"token\":\"${SUB}\"}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "POST /auth валидный токен → 200" +NS=$(json_field "$BODY" namespace) +printf '%s' "$NS" | grep -q '^fission-' && pass "POST /auth namespace=fission-..." || fail "POST /auth namespace format: $NS" +OK=$(json_field "$BODY" ok) +[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "POST /auth ok=true" || fail "POST /auth ok=$OK" + +echo "" +echo ">>> POST /auth: невалидный JSON → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \ + -H "Content-Type: application/json" -d 'not-json') +CODE=$(echo "$R" | tail -1) +[ "$CODE" = "400" ] && pass "POST /auth невалидный JSON → 400" || fail "POST /auth невалидный JSON → $CODE" + +echo "" +echo ">>> POST /auth: GET метод → 405" +R=$(curl -s -w "\n%{http_code}" "${BASE}/auth") +CODE=$(echo "$R" | tail -1) +[ "$CODE" = "405" ] && pass "GET /auth → 405" || fail "GET /auth → $CODE" + +# ── X-Auth-Token header ────────────────────────────────────────────────────── + +echo "" +echo ">>> X-Auth-Token: отсутствует → 401" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions") +check_http "$(echo "$R" | tail -1)" "401" "GET /functions без токена → 401" + +echo "" +echo ">>> X-Auth-Token: токен < 6 символов → 401" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ab") +check_http "$(echo "$R" | tail -1)" "401" "GET /functions токен 2 символа → 401" + +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: abcde") +check_http "$(echo "$R" | tail -1)" "401" "GET /functions токен 5 символов → 401" + +echo "" +echo ">>> X-Auth-Token: токен ≥6 символов → 200" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ${SUB}") +check_http "$(echo "$R" | tail -1)" "200" "GET /functions валидный X-Auth-Token → 200" + +echo "" +echo ">>> Authorization: Bearer → 200" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" \ + -H "Authorization: Bearer ${SUB}") +check_http "$(echo "$R" | tail -1)" "200" "GET /functions Authorization: Bearer → 200" + +# ── Namespace детерминирован ───────────────────────────────────────────────── + +echo "" +echo ">>> Namespace детерминирован (одинаковый токен → одинаковый NS)" +R1=$(curl -s -X POST "${BASE}/auth" -H "Content-Type: application/json" -d "{\"token\":\"${SUB}\"}") +R2=$(curl -s -X POST "${BASE}/auth" -H "Content-Type: application/json" -d "{\"token\":\"${SUB}\"}") +NS1=$(json_field "$R1" namespace) +NS2=$(json_field "$R2" namespace) +[ "$NS1" = "$NS2" ] && [ -n "$NS1" ] && pass "Namespace детерминирован: $NS1" || fail "Namespace не детерминирован: '$NS1' vs '$NS2'" + +print_summary diff --git a/scripts/test_functions_crud.sh b/scripts/test_functions_crud.sh new file mode 100755 index 0000000..b561bfa --- /dev/null +++ b/scripts/test_functions_crud.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# test_functions_crud.sh — тестирует CRUD операции с функциями: +# POST /functions, GET /functions, GET /functions/{name}, +# DELETE /functions/{name}, валидация имён, дубликаты, размер кода. +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" +RUN_ID=$(mk_run_id) +SUB="crud-${RUN_ID}@test.local" + +cleanup() { + if [ -x "$NS_CLEANUP_SCRIPT" ]; then + KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$SUB" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_functions_crud.sh run=$RUN_ID" +echo "========================================" + +JS_CODE='module.exports = async function(ctx) { return { status: 200, body: "crud-ok" }; }' + +# ── CREATE ─────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE: базовый nodejs → 201" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"crud-fn-${RUN_ID}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "201" "CREATE nodejs → 201" +FN_NAME=$(json_field "$BODY" name) +FN_ROUTE=$(json_field "$BODY" route) +FN_PKG=$(json_field "$BODY" package) +[ "$FN_NAME" = "crud-fn-${RUN_ID}" ] && pass "CREATE: name в ответе" || fail "CREATE: name='$FN_NAME'" +printf '%s' "$FN_ROUTE" | grep -q '/' && pass "CREATE: route непустой" || fail "CREATE: route='$FN_ROUTE'" +[ -n "$FN_PKG" ] && pass "CREATE: package непустой" || fail "CREATE: package пустой" + +echo "" +echo ">>> CREATE: дубликат → 409" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"crud-fn-${RUN_ID}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "409" "CREATE дубликат → 409" + +echo "" +echo ">>> CREATE: без name → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"language":"nodejs","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE без name → 400" + +echo "" +echo ">>> CREATE: без language → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"no-lang","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE без language → 400" + +echo "" +echo ">>> CREATE: без code → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"no-code","language":"nodejs"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE без code → 400" + +echo "" +echo ">>> CREATE: невалидный язык → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"bad-lang","language":"cobol","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE язык=cobol → 400" + +echo "" +echo ">>> CREATE: валидация имени" + +# Заглавная буква +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"BadName","language":"nodejs","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с заглавной → 400" + +# Начинается с дефиса +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"-bad","language":"nodejs","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с ведущим дефисом → 400" + +# Заканчивается дефисом +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"bad-","language":"nodejs","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с завершающим дефисом → 400" + +# Пробел в имени +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"bad name","language":"nodejs","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с пробелом → 400" + +# 58 символов — должно быть 400 (лимит 57) +NAME58=$(python3 -c "print('a'*58)") +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${NAME58}\",\"language\":\"nodejs\",\"code\":\"x\"}") +check_http "$(echo "$R" | tail -1)" "400" "CREATE имя 58 символов → 400" + +# 57 символов — должно быть 201 +NAME57=$(python3 -c "print('a'*57)") +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${NAME57}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "201" "CREATE имя 57 символов → 201" + +echo "" +echo ">>> CREATE: код > 1MB → 400 или 413" +# Используем pipe чтобы избежать Argument list too long +R=$(python3 -c " +import json, sys +payload = json.dumps({'name':'big-code','language':'nodejs','code':'x'*(1024*1024+1)}) +print(payload) +" | curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + --data-binary @- --max-time 30) +CODE=$(echo "$R" | tail -1) +( [ "$CODE" = "400" ] || [ "$CODE" = "413" ] || [ "$CODE" = "000" ] ) \ + && pass "CREATE код > 1MB → отклонён ($CODE)" \ + || fail "CREATE код > 1MB → got $CODE" + +# ── LIST ───────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> LIST: GET /functions → 200, массив" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ${SUB}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "LIST GET /functions → 200" +COUNT=$(json_len "$BODY") +[ "${COUNT:-0}" -ge 1 ] && pass "LIST: содержит ≥1 функцию (создали выше)" || fail "LIST: пустой массив count=$COUNT" + +echo "" +echo ">>> LIST: новый пользователь → пустой массив" +EMPTY_USER="empty-${RUN_ID}@test.local" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ${EMPTY_USER}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "LIST новый пользователь → 200" +COUNT=$(json_len "$BODY") +[ "${COUNT:-99}" -eq 0 ] && pass "LIST новый пользователь → []" || fail "LIST новый пользователь → count=$COUNT" + +# ── GET ────────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> GET: /functions/{name} → 200" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/crud-fn-${RUN_ID}" \ + -H "X-Auth-Token: ${SUB}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "GET /functions/crud-fn-${RUN_ID} → 200" +GOT_NAME=$(json_field "$BODY" name) +[ "$GOT_NAME" = "crud-fn-${RUN_ID}" ] && pass "GET: name совпадает" || fail "GET: name='$GOT_NAME'" +GOT_ENV=$(json_field "$BODY" environment) +[ -n "$GOT_ENV" ] && pass "GET: environment непустой ($GOT_ENV)" || fail "GET: environment пустой" + +echo "" +echo ">>> GET: несуществующая функция → 404" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/does-not-exist-xyz" \ + -H "X-Auth-Token: ${SUB}") +check_http "$(echo "$R" | tail -1)" "404" "GET несуществующая → 404" + +# ── DELETE ─────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> DELETE: /functions/{name} → 200" +R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/crud-fn-${RUN_ID}" \ + -H "X-Auth-Token: ${SUB}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "DELETE crud-fn → 200" +DELETED=$(json_field "$BODY" deleted) +[ "$DELETED" = "True" ] || [ "$DELETED" = "true" ] && pass "DELETE: deleted=true" || fail "DELETE: deleted='$DELETED'" + +echo "" +echo ">>> DELETE: повторное удаление → 404" +R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/crud-fn-${RUN_ID}" \ + -H "X-Auth-Token: ${SUB}") +check_http "$(echo "$R" | tail -1)" "404" "DELETE повторное → 404" + +echo "" +echo ">>> GET: после DELETE → 404" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/crud-fn-${RUN_ID}" \ + -H "X-Auth-Token: ${SUB}") +check_http "$(echo "$R" | tail -1)" "404" "GET после DELETE → 404" + +echo "" +echo ">>> DELETE: несуществующая функция → 404" +R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/does-not-exist-xyz" \ + -H "X-Auth-Token: ${SUB}") +check_http "$(echo "$R" | tail -1)" "404" "DELETE несуществующая → 404" + +print_summary diff --git a/scripts/test_invoke.sh b/scripts/test_invoke.sh new file mode 100755 index 0000000..95b1462 --- /dev/null +++ b/scripts/test_invoke.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# test_invoke.sh — тестирует вызов функций: +# NodeJS и Python: create → wait cold start → invoke → проверка тела. +# Invoke несуществующей функции → 404. +# ВАЖНО: использует livetest@test.local — прогретый namespace в kube кластере. +# Для новых namespace fission router требует перезапуска (watchedNamespaces). +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +RUN_ID=$(mk_run_id) +# Используем прогретый namespace — fission router его знает. +SUB="livetest@test.local" +FN_NODE="node-fn-${RUN_ID}" +FN_PY="py-fn-${RUN_ID}" + +cleanup() { + # Удаляем только конкретные функции через API, не весь namespace. + for fn in "$FN_NODE" "$FN_PY"; do + curl -s -X DELETE "${BASE}/functions/${fn}" -H "X-Auth-Token: ${SUB}" > /dev/null 2>&1 || true + done +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_invoke.sh run=$RUN_ID" +echo "========================================" + +# ── Invoke несуществующей ───────────────────────────────────────────────── + +echo "" +echo ">>> INVOKE несуществующей функции → 404" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions/no-such-fn-xyz-${RUN_ID}/invoke" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') +check_http "$(echo "$R" | tail -1)" "404" "INVOKE несуществующей → 404" + +# ── NodeJS ──────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> NodeJS: create + invoke" +JS_CODE='module.exports = async function(ctx) { return { status: 200, body: "node-invoke-ok" }; }' +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN_NODE}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "201" "NodeJS CREATE → 201" + +if wait_invoke "$SUB" "$FN_NODE" "node-invoke-ok" 12 5; then + pass "NodeJS invoke → 200, тело содержит node-invoke-ok" +else + fail "NodeJS invoke не стабилизировался за 60s" +fi + +# Второй вызов — проверяем что работает warm path +R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_NODE}/invoke" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') +STATUS=$(json_field "$R" status) +[ "$STATUS" = "200" ] && pass "NodeJS второй invoke → 200 (warm)" || fail "NodeJS второй invoke → status=$STATUS" + +# ── Python ─────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> Python: create + invoke" +PY_CODE=$(printf 'def main():\n return "py-invoke-ok", 200' | json_escape) +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN_PY}\",\"language\":\"python\",\"code\":${PY_CODE}}") +check_http "$(echo "$R" | tail -1)" "201" "Python CREATE → 201" + +if wait_invoke "$SUB" "$FN_PY" "py-invoke-ok" 15 5; then + pass "Python invoke → 200, тело содержит py-invoke-ok" +else + fail "Python invoke не стабилизировался за 75s" +fi + +# ── Invoke возвращает корректные поля ───────────────────────────────────────── + +echo "" +echo ">>> Invoke response fields" +R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_NODE}/invoke" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') +STATUS=$(json_field "$R" status) +LATENCY=$(json_field "$R" latency_ms) +RAW=$(json_field "$R" response_raw) +[ "$STATUS" = "200" ] && pass "invoke.status = 200" || fail "invoke.status = $STATUS" +printf '%s' "$LATENCY" | grep -qE '^[0-9]+$' && pass "invoke.latency_ms = $LATENCY ms" || fail "invoke.latency_ms не число: $LATENCY" +[ -n "$RAW" ] && pass "invoke.response_raw непустой" || fail "invoke.response_raw пустой" + +print_summary diff --git a/scripts/test_isolation.sh b/scripts/test_isolation.sh new file mode 100755 index 0000000..202fe23 --- /dev/null +++ b/scripts/test_isolation.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# test_isolation.sh — тестирует namespace изоляцию: +# Пользователь A не видит и не может изменить функции пользователя B. +# LIST каждого пользователя возвращает только его функции. +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" +RUN_ID=$(mk_run_id) +ALICE="alice-${RUN_ID}@test.local" +BOB="bob-${RUN_ID}@test.local" + +cleanup() { + if [ -x "$NS_CLEANUP_SCRIPT" ]; then + KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$ALICE" "$BOB" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_isolation.sh run=$RUN_ID" +echo "========================================" + +JS_CODE='module.exports = async function() { return { status: 200, body: "ok" }; }' + +# ── Alice создаёт функцию ───────────────────────────────────────────────────── + +echo "" +echo ">>> Alice создаёт функцию" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${ALICE}" -H "Content-Type: application/json" \ + -d "{\"name\":\"alice-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "201" "Alice CREATE alice-fn → 201" + +# ── Bob не может получить функцию Alice ────────────────────────────────────── + +echo "" +echo ">>> Bob GET alice-fn → 404" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/alice-fn" \ + -H "X-Auth-Token: ${BOB}") +check_http "$(echo "$R" | tail -1)" "404" "Bob GET alice-fn → 404" + +echo "" +echo ">>> Bob DELETE alice-fn → 404" +R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/alice-fn" \ + -H "X-Auth-Token: ${BOB}") +check_http "$(echo "$R" | tail -1)" "404" "Bob DELETE alice-fn → 404" + +echo "" +echo ">>> Bob PUT alice-fn/code → 404" +R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/alice-fn/code" \ + -H "X-Auth-Token: ${BOB}" -H "Content-Type: application/json" \ + -d '{"code":"module.exports = async () => ({ body: \"hacked\" });"}') +check_http "$(echo "$R" | tail -1)" "404" "Bob PUT alice-fn/code → 404" + +echo "" +echo ">>> Bob INVOKE alice-fn → 404" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions/alice-fn/invoke" \ + -H "X-Auth-Token: ${BOB}" -H "Content-Type: application/json" -d '{}') +check_http "$(echo "$R" | tail -1)" "404" "Bob INVOKE alice-fn → 404" + +# ── Alice убеждается что её функция цела ──────────────────────────────────── + +echo "" +echo ">>> Alice GET alice-fn → 200 (не повреждена)" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/alice-fn" \ + -H "X-Auth-Token: ${ALICE}") +check_http "$(echo "$R" | tail -1)" "200" "Alice GET alice-fn → 200" + +# ── LIST изоляция ──────────────────────────────────────────────────────────── + +echo "" +echo ">>> LIST изоляция: Alice и Bob видят только свои функции" + +# Bob создаёт свою функцию +curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${BOB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"bob-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}" > /dev/null + +sleep 1 + +ALICE_LIST=$(curl -s "${BASE}/functions" -H "X-Auth-Token: ${ALICE}") +BOB_LIST=$(curl -s "${BASE}/functions" -H "X-Auth-Token: ${BOB}") + +# Alice видит alice-fn но не bob-fn +ALICE_COUNT=$(json_len "$ALICE_LIST") +ALICE_HAS_OWN=$(printf '%s' "$ALICE_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 'alice-fn')) +except: + print(0)" 2>/dev/null) +ALICE_HAS_BOB=$(printf '%s' "$ALICE_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 'bob-fn')) +except: + print(0)" 2>/dev/null) + +[ "${ALICE_HAS_OWN:-0}" -ge 1 ] && pass "Alice LIST содержит alice-fn" || fail "Alice LIST не содержит alice-fn (count=$ALICE_COUNT)" +[ "${ALICE_HAS_BOB:-0}" -eq 0 ] && pass "Alice LIST не содержит bob-fn" || fail "Alice LIST содержит bob-fn — УТЕЧКА ДАННЫХ!" + +# Bob видит bob-fn но не alice-fn +BOB_HAS_OWN=$(printf '%s' "$BOB_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 'bob-fn')) +except: + print(0)" 2>/dev/null) +BOB_HAS_ALICE=$(printf '%s' "$BOB_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 'alice-fn')) +except: + print(0)" 2>/dev/null) + +[ "${BOB_HAS_OWN:-0}" -ge 1 ] && pass "Bob LIST содержит bob-fn" || fail "Bob LIST не содержит bob-fn" +[ "${BOB_HAS_ALICE:-0}" -eq 0 ] && pass "Bob LIST не содержит alice-fn" || fail "Bob LIST содержит alice-fn — УТЕЧКА ДАННЫХ!" + +# ── Route изоляция ─────────────────────────────────────────────────────────── + +echo "" +echo ">>> Route изоляция: одинаковое имя → разные routes" +ROUTE_A=$(printf '%s' "$ALICE_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + for f in fns: + if f.get('metadata',{}).get('name','') == 'alice-fn': + print(f.get('spec',{}).get('relativeurl','')) + break +except: + print('')" 2>/dev/null) + +ROUTE_B=$(printf '%s' "$BOB_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + for f in fns: + if f.get('metadata',{}).get('name','') == 'bob-fn': + print(f.get('spec',{}).get('relativeurl','')) + break +except: + print('')" 2>/dev/null) + +# Проверяем через GET (возвращает route) +ALICE_GET=$(curl -s "${BASE}/functions/alice-fn" -H "X-Auth-Token: ${ALICE}") +BOB_GET=$(curl -s "${BASE}/functions/bob-fn" -H "X-Auth-Token: ${BOB}") +ROUTE_A=$(json_field "$ALICE_GET" route) +ROUTE_B=$(json_field "$BOB_GET" route) +[ "$ROUTE_A" != "$ROUTE_B" ] && [ -n "$ROUTE_A" ] && [ -n "$ROUTE_B" ] \ + && pass "Route изоляция: $ROUTE_A ≠ $ROUTE_B" \ + || fail "Route коллизия: alice=$ROUTE_A bob=$ROUTE_B" + +print_summary diff --git a/scripts/test_linters.sh b/scripts/test_linters.sh old mode 100644 new mode 100755 diff --git a/scripts/test_stress.sh b/scripts/test_stress.sh new file mode 100755 index 0000000..5144207 --- /dev/null +++ b/scripts/test_stress.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# test_stress.sh — стресс-тест параллельного использования: +# COUNT пользователей создают функции параллельно +# LIST изоляция: каждый видит только свои функции +# Invoke стресс: INVOKE_COUNT параллельных invoke для livetest@test.local (watched namespace) +# Cleanup: удаляем namespace-ы тест-пользователей, функции livetest — через API DELETE. +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" +RUN_ID=$(mk_run_id) +COUNT="${1:-8}" +TMP=$(mktemp -d) +INVOKE_SUB="livetest@test.local" +INVOKE_COUNT=8 + +declare -a SUBS=() +for i in $(seq 1 "$COUNT"); do + SUBS+=("stress-${RUN_ID}-u${i}@test.local") +done + +declare -a INVOKE_FNS=() +for i in $(seq 1 "$INVOKE_COUNT"); do + INVOKE_FNS+=("sinv-${RUN_ID}-${i}") +done + +cleanup() { + rm -rf "$TMP" 2>/dev/null || true + if [ -x "$NS_CLEANUP_SCRIPT" ] && [ "${#SUBS[@]}" -gt 0 ]; then + KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "${SUBS[@]}" >/dev/null 2>&1 || true + fi + for fn in "${INVOKE_FNS[@]}"; do + curl -s -X DELETE "${BASE}/functions/${fn}" -H "X-Auth-Token: ${INVOKE_SUB}" > /dev/null 2>&1 || true + done +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_stress.sh run=$RUN_ID users=$COUNT invoke_parallel=$INVOKE_COUNT" +echo "========================================" + +JS_CODE='module.exports = async function() { return { status: 200, body: "stress-ok" }; }' + +# ── Шаг 1: параллельное создание ───────────────────────────────────────────── + +echo "" +echo ">>> [1/3] $COUNT пользователей создают функции параллельно..." + +for i in $(seq 1 "$COUNT"); do + ( + SUB="${SUBS[$((i-1))]}" + FN="stress-fn-${RUN_ID}" + R=$(curl -s -w "\n%{http_code}" --max-time 60 -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}" \ + 2>/dev/null) + CODE=$(echo "$R" | tail -1) + echo "$i:$CODE" > "${TMP}/create-$i" + ) & +done +wait + +CREATE_OK=0 +CREATE_FAIL=0 +for i in $(seq 1 "$COUNT"); do + CODE=$(cut -d: -f2 "${TMP}/create-$i" 2>/dev/null || echo "000") + if [ "$CODE" = "201" ] || [ "$CODE" = "200" ]; then + CREATE_OK=$((CREATE_OK+1)) + else + CREATE_FAIL=$((CREATE_FAIL+1)) + echo " user$i: CREATE HTTP $CODE" + fi +done + +[ "$CREATE_FAIL" -eq 0 ] \ + && pass "Параллельный CREATE: все $COUNT пользователей → 201" \ + || fail "Параллельный CREATE: $CREATE_FAIL/$COUNT упали" + +# ── Шаг 2: изоляция LIST ───────────────────────────────────────────────────── + +echo "" +echo ">>> [2/3] Изоляция: каждый видит только свою функцию" + +ISO_OK=0 +for i in $(seq 1 "$COUNT"); do + SUB="${SUBS[$((i-1))]}" + FN="stress-fn-${RUN_ID}" + LIST=$(curl -s --max-time 20 "${BASE}/functions" -H "X-Auth-Token: ${SUB}") + OWN_COUNT=$(printf '%s' "$LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == '${FN}')) +except: + print(0)" 2>/dev/null) + TOTAL_COUNT=$(json_len "$LIST") + if [ "${OWN_COUNT:-0}" -eq 1 ] && [ "${TOTAL_COUNT:-0}" -eq 1 ]; then + ISO_OK=$((ISO_OK+1)) + else + echo " user$i: own=$OWN_COUNT total=$TOTAL_COUNT" + fi +done + +[ "$ISO_OK" -eq "$COUNT" ] \ + && pass "Изоляция: все $COUNT пользователей видят только свои функции" \ + || fail "Изоляция: только $ISO_OK/$COUNT OK" + +# ── Шаг 3: параллельный invoke (livetest — watched namespace) ───────────────── + +echo "" +echo ">>> [3/3] $INVOKE_COUNT параллельных invoke для livetest@test.local" + +INV_JS='module.exports = async function() { return { status: 200, body: "sinv-ok" }; }' +for i in $(seq 1 "$INVOKE_COUNT"); do + FN_I="${INVOKE_FNS[$((i-1))]}" + curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${INVOKE_SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN_I}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$INV_JS" | json_escape)}" > /dev/null +done + +echo "Ждём прогрева $INVOKE_COUNT функций..." +for i in $(seq 1 "$INVOKE_COUNT"); do + FN_I="${INVOKE_FNS[$((i-1))]}" + for try in $(seq 1 12); do + R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_I}/invoke" \ + -H "X-Auth-Token: ${INVOKE_SUB}" -H "Content-Type: application/json" -d '{}') + ST=$(json_field "$R" status) + [ "$ST" = "200" ] && break + sleep 5 + done +done + +for i in $(seq 1 "$INVOKE_COUNT"); do + ( + FN_I="${INVOKE_FNS[$((i-1))]}" + R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_I}/invoke" \ + -H "X-Auth-Token: ${INVOKE_SUB}" -H "Content-Type: application/json" -d '{}') + ST=$(json_field "$R" status) + echo "$i:$ST" > "${TMP}/sinvoke-$i" + ) & +done +wait + +SINV_OK=0 +SINV_FAIL=0 +for i in $(seq 1 "$INVOKE_COUNT"); do + CODE=$(cut -d: -f2 "${TMP}/sinvoke-$i" 2>/dev/null || echo "MISS") + [ "$CODE" = "200" ] && SINV_OK=$((SINV_OK+1)) || SINV_FAIL=$((SINV_FAIL+1)) +done + +[ "$SINV_FAIL" -le 1 ] \ + && pass "Параллельный INVOKE (livetest): $SINV_OK/$INVOKE_COUNT OK" \ + || fail "Параллельный INVOKE (livetest): $SINV_FAIL/$INVOKE_COUNT упали" + +print_summary diff --git a/scripts/test_ttl.sh b/scripts/test_ttl.sh new file mode 100755 index 0000000..18e0ac0 --- /dev/null +++ b/scripts/test_ttl.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# test_ttl.sh — тестирует TTL функций: +# CREATE с TTL → expires_at присутствует в ответе +# CREATE без TTL → expires_at отсутствует +# Невалидный TTL → 400 до создания объектов в k8s +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" +RUN_ID=$(mk_run_id) +SUB="ttl-${RUN_ID}@test.local" + +cleanup() { + if [ -x "$NS_CLEANUP_SCRIPT" ]; then + KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$SUB" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_ttl.sh run=$RUN_ID" +echo "========================================" + +JS_CODE='module.exports = async function() { return { status: 200, body: "ok" }; }' + +# ── Без TTL ────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE без TTL → expires_at отсутствует" +R=$(curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"nttl-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +CODE_HTTP=$(printf '%s' "$R" | python3 -c "import sys; lines=sys.stdin.read().split('\n'); print(lines[-1] if lines else '')" 2>/dev/null || echo "?") +EXPIRES=$(json_field "$R" expires_at) +[ -z "$EXPIRES" ] && pass "CREATE без TTL: expires_at отсутствует" || fail "CREATE без TTL: expires_at='$EXPIRES'" + +# ── С TTL=15m ──────────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE с TTL=15m → expires_at присутствует" +R=$(curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"ttl15m-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape),\"ttl\":\"15m\"}") +EXPIRES=$(json_field "$R" expires_at) +[ -n "$EXPIRES" ] && pass "CREATE TTL=15m: expires_at=$EXPIRES" || fail "CREATE TTL=15m: expires_at пустой" +# expires_at должен быть RFC3339 +printf '%s' "$EXPIRES" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}T' && pass "expires_at формат RFC3339" || fail "expires_at не RFC3339: $EXPIRES" + +# ── С TTL=1h ───────────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE с TTL=1h → expires_at присутствует" +R=$(curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"ttl1h-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape),\"ttl\":\"1h\"}") +EXPIRES=$(json_field "$R" expires_at) +[ -n "$EXPIRES" ] && pass "CREATE TTL=1h: expires_at=$EXPIRES" || fail "CREATE TTL=1h: expires_at пустой" + +# ── С TTL=2d ───────────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE с TTL=2d → expires_at присутствует" +R=$(curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"ttl2d-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape),\"ttl\":\"2d\"}") +EXPIRES=$(json_field "$R" expires_at) +[ -n "$EXPIRES" ] && pass "CREATE TTL=2d: expires_at=$EXPIRES" || fail "CREATE TTL=2d: expires_at пустой" + +# ── Невалидный TTL ─────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE TTL невалидный → 400 (без создания объектов)" +for badttl in "99z" "abc" "0d" "-1h" ""; do + if [ -z "$badttl" ]; then continue; fi + R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"badttl-fn\",\"language\":\"nodejs\",\"code\":\"x\",\"ttl\":\"${badttl}\"}") + CODE=$(echo "$R" | tail -1) + [ "$CODE" = "400" ] && pass "TTL='$badttl' → 400" || fail "TTL='$badttl' → got $CODE" +done + +# ── expires_at в LIST response ─────────────────────────────────────────────── + +echo "" +echo ">>> LIST: функция с TTL видна в списке" +LIST=$(curl -s "${BASE}/functions" -H "X-Auth-Token: ${SUB}") +COUNT=$(json_len "$LIST") +[ "${COUNT:-0}" -ge 3 ] && pass "LIST: видно ≥3 функции (2 с TTL, 1 без)" || fail "LIST: count=$COUNT" + +print_summary diff --git a/scripts/test_update.sh b/scripts/test_update.sh new file mode 100755 index 0000000..d25b7e2 --- /dev/null +++ b/scripts/test_update.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# test_update.sh — тестирует обновление кода функции: +# PUT /functions/{name}/code → invoke возвращает новый код +# PUT несуществующей → 404 +# PUT с пустым кодом → 400 +# ВАЖНО: использует livetest@test.local — прогретый namespace в kube кластере. +# Для новых namespace fission router требует перезапуска (watchedNamespaces). +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +RUN_ID=$(mk_run_id) +SUB="livetest@test.local" +FN="upd-fn-${RUN_ID}" + +cleanup() { + curl -s -X DELETE "${BASE}/functions/${FN}" -H "X-Auth-Token: ${SUB}" > /dev/null 2>&1 || true +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_update.sh run=$RUN_ID" +echo "========================================" + +# ── Создаём функцию с начальным кодом ──────────────────────────────────────── + +echo "" +echo ">>> CREATE: версия v1" +CODE_V1='module.exports = async function() { return { status: 200, body: "version-one" }; }' +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$CODE_V1" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "201" "CREATE v1 → 201" + +echo "Ждём прогрева..." +if wait_invoke "$SUB" "$FN" "version-one" 12 5; then + pass "Invoke v1 → version-one" +else + fail "Invoke v1 не стабилизировался" +fi + +# ── PUT /code → 200 ────────────────────────────────────────────────────────── + +echo "" +echo ">>> PUT /code: версия v2" +CODE_V2='module.exports = async function() { return { status: 200, body: "version-two" }; }' +R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/${FN}/code" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"code\":$(printf '%s' "$CODE_V2" | json_escape)}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "PUT /code v2 → 200" +UPDATED=$(json_field "$BODY" updated) +[ "$UPDATED" = "True" ] || [ "$UPDATED" = "true" ] && pass "PUT /code: updated=true" || fail "PUT /code: updated='$UPDATED'" +NEW_PKG=$(json_field "$BODY" package) +[ -n "$NEW_PKG" ] && pass "PUT /code: новый package=$NEW_PKG" || fail "PUT /code: package пустой" + +echo "Ждём пока executor подхватит новый код..." +if wait_invoke "$SUB" "$FN" "version-two" 15 5; then + pass "Invoke после update → version-two" +else + fail "Invoke после update не вернул version-two" +fi + +# ── PUT с пустым кодом → 400 ────────────────────────────────────────────────── + +echo "" +echo ">>> PUT /code: пустой код → 400" +R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/${FN}/code" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"code":""}') +check_http "$(echo "$R" | tail -1)" "400" "PUT /code пустой код → 400" + +R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/${FN}/code" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"code":" "}') +check_http "$(echo "$R" | tail -1)" "400" "PUT /code пробельный код → 400" + +# ── PUT несуществующей → 404 ────────────────────────────────────────────────── + +echo "" +echo ">>> PUT /code: несуществующая функция → 404" +R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/no-such-fn-xyz/code" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"code\":$(printf '%s' "$CODE_V1" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "404" "PUT /code несуществующая → 404" + +# ── Ещё одно обновление (v3) — убеждаемся что можно делать цепочку ──────────── + +echo "" +echo ">>> PUT /code: версия v3 (цепочка)" +CODE_V3='module.exports = async function() { return { status: 200, body: "version-three" }; }' +curl -s -X PUT "${BASE}/functions/${FN}/code" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"code\":$(printf '%s' "$CODE_V3" | json_escape)}" > /dev/null + +if wait_invoke "$SUB" "$FN" "version-three" 15 5; then + pass "Invoke после v3 update → version-three" +else + fail "Invoke после v3 update не стабилизировался" +fi + +print_summary