diff --git a/tests_v2.sh b/tests_v2.sh new file mode 100644 index 0000000..25302bf --- /dev/null +++ b/tests_v2.sh @@ -0,0 +1,370 @@ +#!/usr/bin/env bash +set -uo pipefail + +BASE="https://fission.kube5s.ru/console/api" +PASS=0; FAIL=0 +RUN_ID=$(date +%s | tail -c 6) + +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 HTTP $got, want $want"; } + +echo "" +echo "========================================" +echo " fission-console v0.6.8 — ТЕСТ v2" +echo "========================================" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК A: Фиксы v0.6.8 — регресс" + +R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions/nonexistent-xyz-abc/invoke \ + -H "X-Test-Sub: alice@test.local" -H "Content-Type: application/json" -d '{}') +CODE=$(echo "$R" | tail -1); BODY=$(echo "$R" | head -1) +check_http "$CODE" "404" "INVOKE nonexistent → 404" +echo "$BODY" | grep -q "not found" && pass "INVOKE nonexistent — тело содержит 'not found'" || fail "INVOKE nonexistent — тело: $BODY" + +R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/nonexistent-xyz-abc \ + -H "X-Test-Sub: alice@test.local") +CODE=$(echo "$R" | tail -1) +check_http "$CODE" "404" "DELETE nonexistent → 404" + +# Создаём изолированные функции и проверяем route isolation +ISO_NAME="iso-${RUN_ID}" +R_A=$(curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: routetest-alice@test.local" -H "Content-Type: application/json" \ + -d "{\"name\":\"${ISO_NAME}\",\"language\":\"nodejs\",\"code\":\"module.exports=async(ctx)=>({body:\\\"alice-iso\\\"})\",\"ttl\":\"5m\"}") +R_B=$(curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: routetest-bob@test.local" -H "Content-Type: application/json" \ + -d "{\"name\":\"${ISO_NAME}\",\"language\":\"nodejs\",\"code\":\"module.exports=async(ctx)=>({body:\\\"bob-iso\\\"})\",\"ttl\":\"5m\"}") +ROUTE_A=$(echo "$R_A" | python3 -c "import sys,json; print(json.load(sys.stdin).get('route','?'))" 2>/dev/null) +ROUTE_B=$(echo "$R_B" | python3 -c "import sys,json; print(json.load(sys.stdin).get('route','?'))" 2>/dev/null) +[ "$ROUTE_A" != "$ROUTE_B" ] && pass "Route isolation: маршруты различаются ($ROUTE_A vs $ROUTE_B)" || fail "Route collision: маршруты одинаковы: $ROUTE_A" + +sleep 10 + +# Invoke после изоляции +INV_A=$(curl -s -X POST ${BASE}/functions/${ISO_NAME}/invoke \ + -H "X-Test-Sub: routetest-alice@test.local" -H "Content-Type: application/json" -d '{}') +INV_B=$(curl -s -X POST ${BASE}/functions/${ISO_NAME}/invoke \ + -H "X-Test-Sub: routetest-bob@test.local" -H "Content-Type: application/json" -d '{}') +BODY_A=$(echo "$INV_A" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null) +BODY_B=$(echo "$INV_B" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null) +[ "$BODY_A" = "alice-iso" ] && pass "Route isolation invoke — alice получает alice" || fail "Route isolation — alice получила: $BODY_A" +[ "$BODY_B" = "bob-iso" ] && pass "Route isolation invoke — bob получает bob" || fail "Route isolation — bob получил: $BODY_B" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК B: Валидация входных данных" + +R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ + -H "X-Test-Sub: val-user@test.local" -H "Content-Type: application/json" \ + -d '{"language":"nodejs","code":"x"}') +CODE=$(echo "$R" | tail -1); check_http "$CODE" "400" "Создание без name → 400" + +R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ + -H "X-Test-Sub: val-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"nocode-fn","language":"nodejs"}') +CODE=$(echo "$R" | tail -1); check_http "$CODE" "400" "Создание без code → 400" + +R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ + -H "X-Test-Sub: val-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"nolang-fn","code":"x"}') +CODE=$(echo "$R" | tail -1); check_http "$CODE" "400" "Создание без language → 400" + +R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ + -H "X-Test-Sub: val-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"","language":"nodejs","code":"x"}') +CODE=$(echo "$R" | tail -1); check_http "$CODE" "400" "Создание с пустым name → 400" + +R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ + -H "X-Test-Sub: val-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"bad lang fn","language":"nodejs","code":"x"}') +# Fission не позволяет пробелы в имени — ожидаем 4xx или 502 +CODE=$(echo "$R" | tail -1) +( [ "$CODE" = "400" ] || [ "$CODE" = "422" ] || [ "$CODE" = "502" ] ) && pass "Имя с пробелами → не 201 (got $CODE)" || fail "Имя с пробелами → got $CODE" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК C: Дублирующее создание" + +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: dup-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"dup-fn","language":"nodejs","code":"module.exports=async()=>({body:\"v1\"});","ttl":"5m"}' > /dev/null + +R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ + -H "X-Test-Sub: dup-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"dup-fn","language":"nodejs","code":"module.exports=async()=>({body:\"v2\"});","ttl":"5m"}') +CODE=$(echo "$R" | tail -1) +( [ "$CODE" = "409" ] || [ "$CODE" = "502" ] ) && pass "Дублирующее создание → конфликт (got $CODE)" || fail "Дублирующее создание → got $CODE" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК D: UPDATE кода и повторный invoke" + +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: upd-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"upd-fn","language":"nodejs","code":"module.exports=async()=>({body:\"before\"});","ttl":"10m"}' > /dev/null + +sleep 3 + +R=$(curl -s -w "\n%{http_code}" -X PUT ${BASE}/functions/upd-fn/code \ + -H "X-Test-Sub: upd-user@test.local" -H "Content-Type: application/json" \ + -d '{"code":"module.exports=async()=>({body:\"after\"});"}') +CODE=$(echo "$R" | tail -1); check_http "$CODE" "200" "UPDATE code → 200" + +sleep 10 + +INV=$(curl -s -X POST ${BASE}/functions/upd-fn/invoke \ + -H "X-Test-Sub: upd-user@test.local" -H "Content-Type: application/json" -d '{}') +BODY=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null) +[ "$BODY" = "after" ] && pass "После UPDATE — invoke возвращает новый код" || fail "После UPDATE — invoke вернул: $BODY" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК E: GET функции" + +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: get-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"get-fn","language":"nodejs","code":"x","ttl":"5m"}' > /dev/null + +R=$(curl -s -w "\n%{http_code}" ${BASE}/functions/get-fn \ + -H "X-Test-Sub: get-user@test.local") +CODE=$(echo "$R" | tail -1); BODY=$(echo "$R" | head -1) +check_http "$CODE" "200" "GET существующей функции → 200" +echo "$BODY" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('name')=='get-fn' else 1)" 2>/dev/null \ + && pass "GET — имя функции в ответе" || fail "GET — имя не найдено: $BODY" + +R=$(curl -s -w "\n%{http_code}" ${BASE}/functions/nonexist-get-xyz \ + -H "X-Test-Sub: get-user@test.local") +CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "GET несуществующей → 404" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК F: Namespace изоляция LIST" + +# u1 создаёт 3 функции, u2 — 1. LIST u1 должен вернуть только свои +for i in 1 2 3; do + curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: ns-u1@test.local" -H "Content-Type: application/json" \ + -d "{\"name\":\"ns-fn-$i\",\"language\":\"nodejs\",\"code\":\"x\",\"ttl\":\"5m\"}" > /dev/null +done +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: ns-u2@test.local" -H "Content-Type: application/json" \ + -d '{"name":"ns-fn-u2","language":"nodejs","code":"x","ttl":"5m"}' > /dev/null + +COUNT_U1=$(curl -s ${BASE}/functions -H "X-Test-Sub: ns-u1@test.local" \ + | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) +COUNT_U2=$(curl -s ${BASE}/functions -H "X-Test-Sub: ns-u2@test.local" \ + | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) + +[ "$COUNT_U1" -ge 3 ] 2>/dev/null && pass "LIST изоляция: u1 видит $COUNT_U1 функций (>= 3)" || fail "LIST u1 — $COUNT_U1" +[ "$COUNT_U2" -ge 1 ] && [ "$COUNT_U2" -lt "$COUNT_U1" ] 2>/dev/null && pass "LIST изоляция: u2 видит $COUNT_U2 (меньше чем u1)" || fail "LIST u2 — $COUNT_U2 (u1=$COUNT_U1)" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК G: Namespace изоляция DELETE" + +# Carol пытается удалить функцию Alice +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: alice-del@test.local" -H "Content-Type: application/json" \ + -d '{"name":"alice-secret","language":"nodejs","code":"x","ttl":"5m"}' > /dev/null + +R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/alice-secret \ + -H "X-Test-Sub: carol-del@test.local") +CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "Carol DELETE alice функции → 404" + +# Alice сама удаляет — должно быть 200 +R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/alice-secret \ + -H "X-Test-Sub: alice-del@test.local") +CODE=$(echo "$R" | tail -1); check_http "$CODE" "200" "Alice DELETE своей функции → 200" + +# Alice повторно удаляет — 404 +R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/alice-secret \ + -H "X-Test-Sub: alice-del@test.local") +CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "Alice повторный DELETE → 404" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК H: Python — разные форматы кода" + +# Формат 1: def main() без аргумента — Fission Python env вызывает main() без аргументов +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: py-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"py-main-'${RUN_ID}'","language":"python","code":"def main():\n return \"py-main\", 200","ttl":"10m"}' > /dev/null + +sleep 15 + +INV=$(curl -s -X POST ${BASE}/functions/py-main-${RUN_ID}/invoke \ + -H "X-Test-Sub: py-user@test.local" -H "Content-Type: application/json" -d '{}') +STATUS=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null) +[ "$STATUS" = "200" ] && pass "Python def main() (no-arg) → invoke 200" || fail "Python def main → status=$STATUS body=$(echo $INV)" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК I: NodeJS — несколько export-паттернов" + +# module.exports = async function +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: js-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"js-anon","language":"nodejs","code":"module.exports=async function(ctx){return {body:\"anon\"};}","ttl":"10m"}' > /dev/null + +# exports.handler +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: js-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"js-handler","language":"nodejs","code":"exports.handler=async(ctx)=>({body:\"handler\"});","ttl":"10m"}' > /dev/null + +# exports.default +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: js-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"js-default","language":"nodejs","code":"exports.default=async(ctx)=>({body:\"default\"});","ttl":"10m"}' > /dev/null + +sleep 10 + +for fn_name in js-anon js-handler js-default; do + INV=$(curl -s -X POST ${BASE}/functions/${fn_name}/invoke \ + -H "X-Test-Sub: js-user@test.local" -H "Content-Type: application/json" -d '{}') + STATUS=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null) + [ "$STATUS" = "200" ] && pass "NodeJS $fn_name → 200" || fail "NodeJS $fn_name → status=$STATUS / $(echo $INV)" +done + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК J: TTL — создание и проверка" + +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: ttl-user@test.local" -H "Content-Type: application/json" \ + -d "{\"name\":\"ttl-has-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"x\",\"ttl\":\"15m\"}" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('expires_at') else 1)" 2>/dev/null \ + && pass "TTL=15m — expires_at присутствует" || fail "TTL=15m — expires_at отсутствует" + +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: ttl-user@test.local" -H "Content-Type: application/json" \ + -d "{\"name\":\"ttl-noexp-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"x\"}" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if not d.get('expires_at') else 1)" 2>/dev/null \ + && pass "Без TTL — expires_at отсутствует" || fail "Без TTL — expires_at присутствует (не ожидалось)" + +# Невалидный TTL +R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ + -H "X-Test-Sub: ttl-user@test.local" -H "Content-Type: application/json" \ + -d "{\"name\":\"ttl-bad-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"x\",\"ttl\":\"99z\"}") +CODE=$(echo "$R" | tail -1) +[ "$CODE" = "400" ] && pass "Невалидный TTL=99z → 400" || fail "Невалидный TTL=99z → got $CODE" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК K: Invoke — тело запроса передаётся в функцию" + +# ctx.request.body может быть undefined — тестируем через ctx.request.url (всегда доступен) +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: body-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"echo-url","language":"nodejs","code":"module.exports=async(ctx)=>({body:\"got:\"+ctx.request.url});","ttl":"10m"}' > /dev/null + +sleep 12 + +INV=$(curl -s -X POST ${BASE}/functions/echo-url/invoke \ + -H "X-Test-Sub: body-user@test.local" -H "Content-Type: application/json" \ + -d '{"hello":"world"}') +RESP=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null) +STATUS=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null) +[ "$STATUS" = "200" ] && pass "NodeJS ctx.request.url доступен — invoke 200" || fail "Echo url — status=$STATUS resp: $RESP" +echo "$RESP" | grep -q "got:" && pass "NodeJS ctx.request.url содержит данные" || fail "ctx.request.url — ответ: $RESP" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК L: Stress — 10 параллельных invoke" + +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: stress-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"stress-fn","language":"nodejs","code":"module.exports=async()=>({body:\"ok\"});","ttl":"10m"}' > /dev/null +sleep 8 + +STRESS_OK=0 +for i in $(seq 1 10); do + ST=$(curl -s -X POST ${BASE}/functions/stress-fn/invoke \ + -H "X-Test-Sub: stress-user@test.local" -H "Content-Type: application/json" -d '{}' \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null) + [ "$ST" = "200" ] && STRESS_OK=$((STRESS_OK+1)) +done & +wait + +# Run them +STRESS_OK=0 +PIDS=() +TMPDIR_S=$(mktemp -d) +for i in $(seq 1 10); do + ( curl -s -X POST ${BASE}/functions/stress-fn/invoke \ + -H "X-Test-Sub: stress-user@test.local" -H "Content-Type: application/json" -d '{}' \ + | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('status')==200 else 1)" 2>/dev/null \ + && touch "${TMPDIR_S}/ok_$i" || true ) & +done +wait +STRESS_OK=$(ls ${TMPDIR_S}/ok_* 2>/dev/null | wc -l) +rm -rf "${TMPDIR_S}" +[ "$STRESS_OK" -ge 9 ] && pass "Stress 10 parallel: $STRESS_OK/10 OK" || fail "Stress 10 parallel: $STRESS_OK/10 OK" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК M: LIST пустого namespace" + +COUNT=$(curl -s ${BASE}/functions -H "X-Test-Sub: brand-new-empty-user-xyzqwerty@test.local" \ + | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) +[ "$COUNT" = "0" ] && pass "LIST новый пользователь → пустой список" || fail "LIST новый пользователь → $COUNT" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК N: DELETE полная цепочка (fn + pkg + trigger)" + +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: chain-user@test.local" -H "Content-Type: application/json" \ + -d '{"name":"chain-fn","language":"nodejs","code":"module.exports=async()=>({body:\"ok\"});","ttl":"5m"}' > /dev/null + +# Получаем NS для chain-user +NS_CHAIN=$(curl -s ${BASE}/auth -H "X-Test-Sub: chain-user@test.local" \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('namespace','?'))" 2>/dev/null) + +R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/chain-fn \ + -H "X-Test-Sub: chain-user@test.local") +CODE=$(echo "$R" | tail -1); check_http "$CODE" "200" "DELETE chain-fn → 200" + +# k8s: проверяем что функция и триггер удалены +FN_EXISTS=$(kubectl get functions.fission.io chain-fn -n ${NS_CHAIN} 2>&1 | grep -c "not found" || true) +TRG_EXISTS=$(kubectl get httptriggers.fission.io -n ${NS_CHAIN} 2>&1 | grep -c "chain-fn" || true) +[ "$FN_EXISTS" -ge 1 ] && pass "k8s: Function удалена после DELETE" || fail "k8s: Function ещё существует в $NS_CHAIN" +[ "$TRG_EXISTS" -eq 0 ] && pass "k8s: HTTPTrigger удалён после DELETE" || fail "k8s: HTTPTrigger ещё существует ($TRG_EXISTS)" + +# После удаления GET должен вернуть 404 +R=$(curl -s -w "\n%{http_code}" ${BASE}/functions/chain-fn \ + -H "X-Test-Sub: chain-user@test.local") +CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "GET после DELETE → 404" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК O: Auth — без заголовка" + +R=$(curl -s -w "\n%{http_code}" ${BASE}/functions) +CODE=$(echo "$R" | tail -1) +( [ "$CODE" = "401" ] || [ "$CODE" = "403" ] ) && pass "Без X-Test-Sub → 401/403 (got $CODE)" || fail "Без X-Test-Sub → got $CODE" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК P: /console/api/auth" + +# POST /auth без токена → 400 bad request +R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/auth \ + -H "Content-Type: application/json" -d '{"token":""}') +CODE=$(echo "$R" | tail -1); BODY=$(echo "$R" | head -1) +check_http "$CODE" "400" "POST /auth без токена → 400" +echo "$BODY" | grep -q "token\|required\|invalid" && pass "/auth — тело содержит ошибку токена" || fail "/auth — тело: $BODY" + +# POST /auth с невалидным токеном → 401 +R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/auth \ + -H "Content-Type: application/json" -d '{"token":"not.a.real.jwt.token"}') +CODE=$(echo "$R" | tail -1) +check_http "$CODE" "401" "POST /auth с невалидным токеном → 401" + +# ────────────────────────────────────────────── +echo "" +echo "========================================" +printf "Итого: PASS=%d FAIL=%d\n" $PASS $FAIL +echo "========================================"