571 lines
28 KiB
Bash
Executable File
571 lines
28 KiB
Bash
Executable File
#!/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 ""
|