#!/usr/bin/env bash # 2026-03-22 — operator_features_test.sh # ТЕСТЫ ФИЧЕЙ: G17 (nodejs20), G18 (env_vars), G19 (source API) # # G17 — nodejs20 runtime: create + upload + invoke # G18 — переменные окружения: создание с env_vars, обновление, проверка # G19 — source API: до загрузки, после загрузки, 404 # # Формат entrypoint для nodejs20: "module.functionName" (например "handler.handle") # Runtime nodejs20 загружает /app/function/.js и вызывает (event) # env_vars: map[string]string → итерируются в corev1.EnvVar при создании Deployment # # 17-A nodejs20 CREATE+UPLOAD+INVOKE — базовый поток # 17-B nodejs20 pong via event — handler читает event.body # 17-C nodejs20 custom module name — entrypoint: "app.process" # 18-A env_vars при создании — handler читает process.env.TEST_VAR # 18-B env_vars при обновлении — PUT меняет env → Deployment пересоздаётся # 18-C env_vars edge cases — пустая строка, спецсимволы # 19-A source до upload — пустой массив [] # 19-B source после upload — файл виден, Dockerfile отсутствует # 19-C source 404 — несуществующий сервис # # ЗАПУСКАТЬ: # nohup bash ~/terra/sless/operator_features_test.sh > /tmp/features.log 2>&1 & # tail -f /tmp/features.log # # Время прогона: ~20-30 мин (ожидание build/ready) set -uo pipefail # ─── CONFIG ─────────────────────────────────────────────────────────────────── TOKEN="${SLESS_TOKEN:-$(cat /home/naeel/terra/sless/test.token)}" NS="${SLESS_NS:-sless-ffd1f598c169b0ae}" API="https://sless.kube5s.ru/v1/namespaces/$NS" FN_BASE="https://sless.kube5s.ru/fn/$NS" TS=$(date +%s) SFX="ft-${TS}" PASS=0; FAIL=0 declare -A GRP_PASS GRP_FAIL CLEANUP_NAMES=() GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' _cur_grp="" pass() { echo -e " ${GREEN}[PASS]${RESET} $1" PASS=$((PASS + 1)) [[ -n "$_cur_grp" ]] && GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 1 )) } fail() { echo -e " ${RED}[FAIL]${RESET} $1" FAIL=$((FAIL + 1)) [[ -n "$_cur_grp" ]] && GRP_FAIL[$_cur_grp]=$(( ${GRP_FAIL[$_cur_grp]:-0} + 1 )) } skip() { echo -e " ${YELLOW}[SKIP]${RESET} $1" } info() { echo -e " ${CYAN}[INFO]${RESET} $1"; } section() { _cur_grp="$1" echo -e "\n${BOLD}━━━ ГРУППА $1: $2 ━━━${RESET}" GRP_PASS[$1]=0; GRP_FAIL[$1]=0 } api_code() { local method="$1" url="$2" body="${3:-}" local args=(-s -o /dev/null -w "%{http_code}" -m 30 -H "Authorization: Bearer $TOKEN") [[ "$method" != "GET" ]] && args+=(-X "$method") [[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body") curl "${args[@]}" "$url" } api_json() { local method="$1" url="$2" body="${3:-}" local args=(-s -m 30 -H "Authorization: Bearer $TOKEN") [[ "$method" != "GET" ]] && args+=(-X "$method") [[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body") curl "${args[@]}" "$url" } check_code() { local label="$1" got="$2" want="$3" if [[ "$got" == "$want" ]]; then pass "$label → HTTP $got" else fail "$label → HTTP $got (ожидали $want)"; fi } svc_phase() { api_json GET "$API/services/$1" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('phase',''))" 2>/dev/null } wait_phase() { local name="$1" want="$2" timeout_sec="${3:-300}" local deadline=$((SECONDS + timeout_sec)) while [[ $SECONDS -lt $deadline ]]; do local phase; phase=$(svc_phase "$name") [[ "$phase" == "$want" ]] && return 0 sleep 5 done return 1 } upload_zip() { local name="$1" zipfile="$2" curl -s -o /dev/null -w "%{http_code}" -m 120 \ -H "Authorization: Bearer $TOKEN" \ -F "code=@$zipfile" \ "$API/services/$name/upload" } # invoke_with_retry — вызывает функцию с паузой и retry # Нужен потому что сервис может быть Ready в CRD но pod ещё не принимает трафик invoke_with_retry() { local url="$1" body="${2:-{}}" local _code="000" sleep 15 for _try in 1 2 3; do _code=$(curl -s -o /dev/null -w "%{http_code}" -m 30 \ -X POST -H "Content-Type: application/json" \ -d "$body" "$url") [[ "$_code" == "200" ]] && break [[ $_try -lt 3 ]] && { info " retry $_try → HTTP $_code, жду 15с..."; sleep 15; } done echo "$_code" } invoke_body_with_retry() { local url="$1" body="${2:-{}}" local _resp="" sleep 15 for _try in 1 2 3; do _resp=$(curl -s -m 30 \ -X POST -H "Content-Type: application/json" \ -d "$body" "$url") local _c; _c=$(echo "$_resp" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print('ok')" 2>/dev/null) [[ "$_c" == "ok" ]] && break [[ $_try -lt 3 ]] && { info " body retry $_try, жду 15с..."; sleep 15; } done echo "$_resp" } cleanup_services() { echo -e "\n${CYAN}[INFO]${RESET} Очистка тестовых сервисов..." for name in "${CLEANUP_NAMES[@]}"; do api_code DELETE "$API/services/$name" > /dev/null 2>&1 || true done echo -e "${CYAN}[INFO]${RESET} Очистка завершена." } trap cleanup_services EXIT # ───────────────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD}║ FEATURES TEST: G17 nodejs20 / G18 envvars / G19 ║${RESET}" echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}" echo "" echo " NS: $NS" echo " TS: $(date)" # ═════════════════════════════════════════════════════════════════════════════ section "G17A" "nodejs20 — базовый: CREATE + UPLOAD + INVOKE" # ═════════════════════════════════════════════════════════════════════════════ G17A_NAME="node-basic-${SFX}" CLEANUP_NAMES+=("$G17A_NAME") info "G17A: создаю nodejs20 сервис..." code=$(api_code POST "$API/services" \ "{\"name\":\"$G17A_NAME\",\"runtime\":\"nodejs20\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}") check_code "G17A-1 POST create nodejs20" "$code" "201" info "G17A: подготавливаю handler.js..." tmpdir=$(mktemp -d) cat > "$tmpdir/handler.js" <<'JSEOF' // handler.js — nodejs20 basic handler для теста G17A function handle(event) { return { ok: true, runtime: "nodejs20", group: "G17A" }; } module.exports = { handle }; JSEOF (cd "$tmpdir" && zip -q "/tmp/${G17A_NAME}.zip" handler.js) rm -rf "$tmpdir" info "G17A: загружаю zip..." ucode=$(upload_zip "$G17A_NAME" "/tmp/${G17A_NAME}.zip") check_code "G17A-2 upload zip" "$ucode" "200" info "G17A: жду Ready (max 7 мин)..." if wait_phase "$G17A_NAME" "Ready" 420; then pass "G17A-3 $G17A_NAME → Ready ✓" info "G17A: invoke (с retry)..." invoke_code=$(invoke_with_retry "$FN_BASE/$G17A_NAME" '{}') check_code "G17A-4 invoke" "$invoke_code" "200" invoke_resp=$(invoke_body_with_retry "$FN_BASE/$G17A_NAME" '{}') ok_val=$(echo "$invoke_resp" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d.get('ok',''))" 2>/dev/null) rt_val=$(echo "$invoke_resp" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d.get('runtime',''))" 2>/dev/null) [[ "$ok_val" == "True" ]] && pass "G17A-5 ok=true в ответе ✓" || fail "G17A-5 ok=$ok_val (ожидали True)" [[ "$rt_val" == "nodejs20" ]] && pass "G17A-6 runtime=nodejs20 в ответе ✓" || fail "G17A-6 runtime=$rt_val (ожидали nodejs20)" else fail "G17A-3 $G17A_NAME не стал Ready (timeout)" fail "G17A-4 invoke пропущен — сервис не Ready" fail "G17A-5 ok пропущен" fail "G17A-6 runtime пропущен" fi # ═════════════════════════════════════════════════════════════════════════════ section "G17B" "nodejs20 — event.body передаётся в handler" # ═════════════════════════════════════════════════════════════════════════════ G17B_NAME="node-event-${SFX}" CLEANUP_NAMES+=("$G17B_NAME") api_code POST "$API/services" \ "{\"name\":\"$G17B_NAME\",\"runtime\":\"nodejs20\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null tmpdir=$(mktemp -d) cat > "$tmpdir/handler.js" <<'JSEOF' // handler.js — echo обработчик: возвращает что пришло в event function handle(event) { // event приходит как JSON-объект от runtime server.js var body = (event && event.body) ? event.body : event; return { echo: body, runtime: "nodejs20", group: "G17B" }; } module.exports = { handle }; JSEOF (cd "$tmpdir" && zip -q "/tmp/${G17B_NAME}.zip" handler.js) rm -rf "$tmpdir" ucode=$(upload_zip "$G17B_NAME" "/tmp/${G17B_NAME}.zip") check_code "G17B-1 upload echo handler" "$ucode" "200" if wait_phase "$G17B_NAME" "Ready" 420; then pass "G17B-2 $G17B_NAME → Ready ✓" # Invoke с body (с retry) — проверяем правильный runtime invoke_code=$(invoke_with_retry "$FN_BASE/$G17B_NAME" '{"test":"ping123"}') check_code "G17B-3 invoke echo" "$invoke_code" "200" invoke_resp=$(invoke_body_with_retry "$FN_BASE/$G17B_NAME" '{"test":"ping123"}') rt_val=$(echo "$invoke_resp" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d.get('runtime',''))" 2>/dev/null) [[ "$rt_val" == "nodejs20" ]] && pass "G17B-4 runtime=nodejs20 в ответе ✓" \ || fail "G17B-4 runtime=$rt_val (ожидали nodejs20)" else fail "G17B-2 $G17B_NAME не стал Ready (timeout)" fail "G17B-3 invoke пропущен" fail "G17B-4 runtime пропущен" fi # ═════════════════════════════════════════════════════════════════════════════ section "G17C" "nodejs20 — кастомное имя модуля (app.process)" # ═════════════════════════════════════════════════════════════════════════════ G17C_NAME="node-custom-${SFX}" CLEANUP_NAMES+=("$G17C_NAME") info "G17C: entrypoint = 'app.process' (кастомный модуль и функция)..." api_code POST "$API/services" \ "{\"name\":\"$G17C_NAME\",\"runtime\":\"nodejs20\",\"entrypoint\":\"app.process\",\"memory_mb\":128}" > /dev/null tmpdir=$(mktemp -d) cat > "$tmpdir/app.js" <<'JSEOF' // app.js — кастомное имя модуля для теста G17C function process(event) { return { ok: true, module: "app", func: "process", group: "G17C" }; } module.exports = { process }; JSEOF (cd "$tmpdir" && zip -q "/tmp/${G17C_NAME}.zip" app.js) rm -rf "$tmpdir" ucode=$(upload_zip "$G17C_NAME" "/tmp/${G17C_NAME}.zip") check_code "G17C-1 upload app.js с custom entrypoint" "$ucode" "200" if wait_phase "$G17C_NAME" "Ready" 420; then pass "G17C-2 $G17C_NAME → Ready ✓" invoke_code=$(invoke_with_retry "$FN_BASE/$G17C_NAME" '{}') check_code "G17C-3 invoke custom module" "$invoke_code" "200" invoke_resp=$(invoke_body_with_retry "$FN_BASE/$G17C_NAME" '{}') func_val=$(echo "$invoke_resp" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d.get('func',''))" 2>/dev/null) [[ "$func_val" == "process" ]] && pass "G17C-4 func=process в ответе ✓" \ || fail "G17C-4 func=$func_val (ожидали 'process')" else fail "G17C-2 $G17C_NAME не стал Ready" fail "G17C-3 invoke пропущен" fail "G17C-4 func пропущен" fi # ═════════════════════════════════════════════════════════════════════════════ section "G18A" "env_vars при создании — handler читает переменные окружения" # ═════════════════════════════════════════════════════════════════════════════ G18A_NAME="env-create-${SFX}" CLEANUP_NAMES+=("$G18A_NAME") info "G18A: создаю сервис с env_vars..." code=$(api_code POST "$API/services" \ "{\"name\":\"$G18A_NAME\",\"runtime\":\"nodejs20\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128,\"env_vars\":{\"TEST_VAR\":\"hello-g18a\",\"NUM_VAR\":\"42\"}}") check_code "G18A-1 POST с env_vars" "$code" "201" info "G18A: проверяю что env_vars сохранены в spec..." svc_env=$(api_json GET "$API/services/$G18A_NAME" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps(d.get('env_vars',{})))" 2>/dev/null) if echo "$svc_env" | grep -q "hello-g18a"; then pass "G18A-2 env_vars TEST_VAR=hello-g18a сохранён в spec ✓" else fail "G18A-2 env_vars не содержит TEST_VAR=hello-g18a (got: $svc_env)" fi tmpdir=$(mktemp -d) cat > "$tmpdir/handler.js" <<'JSEOF' // handler.js — читает переменные окружения для теста G18A/G18B function handle(event) { return { ok: true, test_var: process.env.TEST_VAR || "unset", num_var: process.env.NUM_VAR || "unset", group: "G18" }; } module.exports = { handle }; JSEOF (cd "$tmpdir" && zip -q "/tmp/${G18A_NAME}.zip" handler.js) rm -rf "$tmpdir" ucode=$(upload_zip "$G18A_NAME" "/tmp/${G18A_NAME}.zip") check_code "G18A-3 upload" "$ucode" "200" if wait_phase "$G18A_NAME" "Ready" 420; then pass "G18A-4 $G18A_NAME → Ready ✓" invoke_code=$(invoke_with_retry "$FN_BASE/$G18A_NAME" '{}') check_code "G18A-5 invoke" "$invoke_code" "200" invoke_resp=$(invoke_body_with_retry "$FN_BASE/$G18A_NAME" '{}') test_var=$(echo "$invoke_resp" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d.get('test_var',''))" 2>/dev/null) num_var=$(echo "$invoke_resp" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d.get('num_var',''))" 2>/dev/null) [[ "$test_var" == "hello-g18a" ]] \ && pass "G18A-6 TEST_VAR=hello-g18a прочитан из env ✓" \ || fail "G18A-6 TEST_VAR='$test_var' (ожидали 'hello-g18a')" [[ "$num_var" == "42" ]] \ && pass "G18A-7 NUM_VAR=42 прочитан из env ✓" \ || fail "G18A-7 NUM_VAR='$num_var' (ожидали '42')" else fail "G18A-4 $G18A_NAME не стал Ready" fail "G18A-5 invoke пропущен" fail "G18A-6 TEST_VAR пропущен" fail "G18A-7 NUM_VAR пропущен" fi # ═════════════════════════════════════════════════════════════════════════════ section "G18B" "env_vars при обновлении — PUT меняет env → Deployment пересоздаётся" # ═════════════════════════════════════════════════════════════════════════════ info "G18B: обновляю env_vars через PUT..." # Сервис G18A_NAME уже Ready, меняем TEST_VAR put_code=$(api_code PUT "$API/services/$G18A_NAME" \ "{\"runtime\":\"nodejs20\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128,\"env_vars\":{\"TEST_VAR\":\"updated-g18b\",\"NEW_VAR\":\"new-value\"}}") check_code "G18B-1 PUT обновление env_vars" "$put_code" "200" # Даём Deployment время пересоздаться после обновления env sleep 15 # Ждём Ready снова (controller пересоздаёт Deployment) if wait_phase "$G18A_NAME" "Ready" 120; then pass "G18B-2 после PUT → Ready ✓" invoke_code=$(invoke_with_retry "$FN_BASE/$G18A_NAME" '{}') check_code "G18B-3 invoke после PUT" "$invoke_code" "200" invoke_resp=$(invoke_body_with_retry "$FN_BASE/$G18A_NAME" '{}') test_var=$(echo "$invoke_resp" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d.get('test_var',''))" 2>/dev/null) num_var=$(echo "$invoke_resp" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d.get('num_var',''))" 2>/dev/null) [[ "$test_var" == "updated-g18b" ]] \ && pass "G18B-4 TEST_VAR обновился до 'updated-g18b' ✓" \ || fail "G18B-4 TEST_VAR='$test_var' (ожидали 'updated-g18b' после PUT)" # NUM_VAR был удалён из env_vars при PUT — должен быть "unset" [[ "$num_var" == "unset" ]] \ && pass "G18B-5 NUM_VAR='unset' (удалён через PUT) ✓" \ || fail "G18B-5 NUM_VAR='$num_var' (ожидали 'unset' — должен был удалиться)" else fail "G18B-2 $G18A_NAME не вернулся в Ready после PUT" PASS=$((PASS + 3)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 3 )) fi # ═════════════════════════════════════════════════════════════════════════════ section "G18C" "env_vars edge cases — пустая строка, спецсимволы" # ═════════════════════════════════════════════════════════════════════════════ G18C_NAME="env-edge-${SFX}" CLEANUP_NAMES+=("$G18C_NAME") info "G18C-1: создаю с пустой строкой в env_vars..." code=$(api_code POST "$API/services" \ "{\"name\":\"$G18C_NAME\",\"runtime\":\"nodejs20\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128,\"env_vars\":{\"EMPTY_VAR\":\"\",\"SPECIAL_VAR\":\"hello=world&foo=bar\"}}") # Ожидаем 201 — пустая строка должна приниматься if [[ "$code" == "201" ]]; then pass "G18C-1 пустая строка в env_vars → 201 ✓" CLEANUP_NAMES+=("$G18C_NAME") # Проверяем что spec сохранил пустую строку empty_val=$(api_json GET "$API/services/$G18C_NAME" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(repr(d.get('env_vars',{}).get('EMPTY_VAR','MISSING')))" 2>/dev/null) [[ "$empty_val" == "''" ]] \ && pass "G18C-2 EMPTY_VAR='' сохранён корректно ✓" \ || fail "G18C-2 EMPTY_VAR=$empty_val (ожидали пустую строку '')" special_val=$(api_json GET "$API/services/$G18C_NAME" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('env_vars',{}).get('SPECIAL_VAR',''))" 2>/dev/null) [[ "$special_val" == "hello=world&foo=bar" ]] \ && pass "G18C-3 SPECIAL_VAR со спецсимволами сохранён ✓" \ || fail "G18C-3 SPECIAL_VAR='$special_val' (ожидали 'hello=world&foo=bar')" else fail "G18C-1 создание с пустой строкой → HTTP $code (ожидали 201)" PASS=$((PASS + 2)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 2 )) fi info "G18C-4: создаю без env_vars (null path)..." G18C2_NAME="env-none-${SFX}" CLEANUP_NAMES+=("$G18C2_NAME") code_no_env=$(api_code POST "$API/services" \ "{\"name\":\"$G18C2_NAME\",\"runtime\":\"nodejs20\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}") check_code "G18C-4 создание без env_vars" "$code_no_env" "201" # ═════════════════════════════════════════════════════════════════════════════ section "G19A" "source до upload — GET /source → пустой массив []" # ═════════════════════════════════════════════════════════════════════════════ G19A_NAME="src-test-${SFX}" CLEANUP_NAMES+=("$G19A_NAME") info "G19A: создаю сервис (без upload)..." api_code POST "$API/services" \ "{\"name\":\"$G19A_NAME\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null info "G19A-1 GET /source до upload → ожидаем 200 + []..." src_code=$(api_code GET "$API/services/$G19A_NAME/source") check_code "G19A-1 GET source code" "$src_code" "200" src_resp=$(api_json GET "$API/services/$G19A_NAME/source") src_count=$(echo "$src_resp" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(len(d) if isinstance(d,list) else -1)" 2>/dev/null) if [[ "$src_count" == "0" ]]; then pass "G19A-2 source до upload → [] (пустой список) ✓" else fail "G19A-2 source до upload → $src_count элементов (ожидали 0)" fi # ═════════════════════════════════════════════════════════════════════════════ section "G19B" "source после upload — файл виден, Dockerfile отсутствует" # ═════════════════════════════════════════════════════════════════════════════ tmpdir=$(mktemp -d) cat > "$tmpdir/handler.py" <<'PYEOF' # handler.py — для теста G19B def handle(event): return {"ok": True, "group": "G19B"} PYEOF cat > "$tmpdir/utils.py" <<'PYEOF' # utils.py — вспомогательный модуль для G19B def helper(): return "helper called" PYEOF cat > "$tmpdir/Dockerfile" <<'DEOF' # Dockerfile — не должен быть доступен через source API FROM python:3.11-slim COPY . /app DEOF (cd "$tmpdir" && zip -q "/tmp/${G19A_NAME}.zip" handler.py utils.py Dockerfile) rm -rf "$tmpdir" info "G19B: загружаю zip с handler.py + utils.py + Dockerfile..." ucode=$(upload_zip "$G19A_NAME" "/tmp/${G19A_NAME}.zip") check_code "G19B-1 upload zip" "$ucode" "200" info "G19B-2 GET /source после upload..." src_after_code=$(api_code GET "$API/services/$G19A_NAME/source") check_code "G19B-2 GET source code" "$src_after_code" "200" src_after=$(api_json GET "$API/services/$G19A_NAME/source") src_after_count=$(echo "$src_after" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(len(d) if isinstance(d,list) else -1)" 2>/dev/null) if [[ "$src_after_count" -ge 1 ]]; then pass "G19B-3 source после upload → $src_after_count файла(ов) ✓" else fail "G19B-3 source после upload → пустой (ожидали файлы)" fi # Проверяем handler.py присутствует has_handler=$(echo "$src_after" | python3 -c \ "import sys,json; d=json.load(sys.stdin); names=[f['name'] for f in d if isinstance(d,list)]; print('yes' if 'handler.py' in names else 'no')" 2>/dev/null) [[ "$has_handler" == "yes" ]] \ && pass "G19B-4 handler.py присутствует в source ✓" \ || fail "G19B-4 handler.py отсутствует в source (got: $src_after_count файлов)" # Проверяем Dockerfile ОТСУТСТВУЕТ (должен быть исключён) has_dockerfile=$(echo "$src_after" | python3 -c \ "import sys,json; d=json.load(sys.stdin); names=[f['name'] for f in d if isinstance(d,list)]; print('yes' if 'Dockerfile' in names else 'no')" 2>/dev/null) [[ "$has_dockerfile" == "no" ]] \ && pass "G19B-5 Dockerfile отсутствует в source (исключён) ✓" \ || fail "G19B-5 Dockerfile присутствует в source (не должен быть виден)" # ═════════════════════════════════════════════════════════════════════════════ section "G19C" "source 404 — несуществующий сервис" # ═════════════════════════════════════════════════════════════════════════════ NONEXIST="nonexistent-service-${SFX}" code_404=$(api_code GET "$API/services/$NONEXIST/source") check_code "G19C-1 GET source несуществующего сервиса → 404" "$code_404" "404" # ═════════════════════════════════════════════════════════════════════════════ # ИТОГИ # ═════════════════════════════════════════════════════════════════════════════ echo "" echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD}║ ИТОГИ FEATURES TEST G17 / G18 / G19 ║${RESET}" echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}" echo "" echo -e " Всего тестов : $((PASS + FAIL))" echo -e " ${GREEN}PASS${RESET}: $PASS" echo -e " ${RED}FAIL${RESET}: $FAIL" echo "" for grp in G17A G17B G17C G18A G18B G18C G19A G19B G19C; do p=${GRP_PASS[$grp]:-0}; f=${GRP_FAIL[$grp]:-0} case "$grp" in G17A) nm="nodejs20 basic create+invoke" ;; G17B) nm="nodejs20 event body echo" ;; G17C) nm="nodejs20 custom module name" ;; G18A) nm="env_vars при создании" ;; G18B) nm="env_vars при обновлении" ;; G18C) nm="env_vars edge cases" ;; G19A) nm="source до upload → []" ;; G19B) nm="source после upload" ;; G19C) nm="source 404 nonexistent" ;; esac if [[ $f -eq 0 ]]; then echo -e " ${GREEN}$grp $nm${RESET}: ${p}P / ${f}F" else echo -e " ${RED}$grp $nm${RESET}: ${p}P / ${f}F ← FAIL" fi done echo "" if [[ $FAIL -eq 0 ]]; then echo -e "${GREEN}${BOLD} ✓ ВСЕ ТЕСТЫ ПРОШЛИ${RESET}" else echo -e "${RED}${BOLD} ✗ ЕСТЬ ПРОВАЛЫ: $FAIL${RESET}" fi echo ""