Files
sless/operator_features_test.sh

571 lines
28 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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/<module>.js и вызывает <functionName>(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 ""