#!/usr/bin/env bash set -euo pipefail BASE="https://fission.kube5s.ru/console/api" ROOT_DIR=$(cd "$(dirname "$0")" && pwd) NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" RUN_ID="ui$(date +%s | tail -c 6)" PASS=0 FAIL=0 LOGIN_SUB="ui-login-${RUN_ID}@test.local" LANG_SUB="ui-lang-${RUN_ID}@test.local" NODE_SUB="ui-node-${RUN_ID}@test.local" STACK_SUB="ui-stack-${RUN_ID}@test.local" NEG_SUB="ui-neg-${RUN_ID}@test.local" pass() { PASS=$((PASS+1)); echo " PASS: $1"; } fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; } json_quote() { python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1" } json_field() { local payload="$1" local key="$2" printf '%s' "$payload" | python3 -c 'import json,sys key=sys.argv[1] try: data=json.load(sys.stdin) value=data.get(key, "") print(value if not isinstance(value, (dict,list)) else json.dumps(value)) except Exception: print("")' "$key" } cleanup_namespaces() { if [ -x "$NS_CLEANUP_SCRIPT" ]; then echo "" echo ">>> Cleanup UI test namespaces..." KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$LOGIN_SUB" "$LANG_SUB" "$NODE_SUB" "$STACK_SUB" "$NEG_SUB" || true fi } trap cleanup_namespaces EXIT auth_call() { local token="$1" local env_name="${2:-test}" curl -s -w "\n%{http_code}" --max-time 45 -X POST "${BASE}/auth" \ -H 'Content-Type: application/json' \ -d "{\"token\":\"${token}\",\"env\":\"${env_name}\"}" } auth_get() { local path="$1" local token="$2" local env_name="${3:-test}" curl -s -w "\n%{http_code}" --max-time 45 "${BASE}${path}" \ -H "X-Auth-Token: ${token}" \ -H "X-Auth-Env: ${env_name}" } create_call() { local sub="$1" local body="$2" curl -s -w "\n%{http_code}" --max-time 60 -X POST "${BASE}/functions" \ -H "X-Test-Sub: ${sub}" -H 'Content-Type: application/json' -d "$body" } invoke_call() { local sub="$1" local name="$2" local body="${3:-{}}" local out_file err_file http_code rc resp err_text out_file=$(mktemp) err_file=$(mktemp) rc=0 http_code=$(curl -sS -o "$out_file" -w "%{http_code}" --max-time 60 -X POST "${BASE}/functions/${name}/invoke" \ -H "X-Test-Sub: ${sub}" -H 'Content-Type: application/json' -d "$body" 2>"$err_file") || rc=$? resp=$(cat "$out_file" 2>/dev/null || true) err_text=$(tr '\n' ' ' <"$err_file" | sed 's/[[:space:]]\+/ /g') rm -f "$out_file" "$err_file" if [ "$rc" -ne 0 ]; then printf '{"error":%s,"status":"","response_raw":"","http_status":"%s"}\n' \ "$(json_quote "curl rc=${rc} http=${http_code}: ${err_text}")" "$http_code" return 0 fi if printf '%s' "$resp" | python3 -c 'import json,sys; json.load(sys.stdin)' >/dev/null 2>&1; then printf '%s' "$resp" return 0 fi printf '{"error":%s,"status":"","response_raw":%s,"http_status":"%s"}\n' \ "$(json_quote "non-json invoke response http=${http_code}")" "$(json_quote "$resp")" "$http_code" } wait_invoke_ok() { local sub="$1" local name="$2" local expected="$3" local tries="$4" local delay="$5" local try status raw err latency http_status for try in $(seq 1 "$tries"); do resp=$(invoke_call "$sub" "$name") status=$(json_field "$resp" status) raw=$(json_field "$resp" response_raw) err=$(json_field "$resp" error) latency=$(json_field "$resp" latency_ms) http_status=$(json_field "$resp" http_status) echo " TRY=${try} fn=${name} status=${status} http=${http_status} latency_ms=${latency} raw=${raw} err=${err}" if [ "$status" = "200" ] && printf '%s' "$raw" | grep -q "$expected"; then return 0 fi if [ "$try" -lt "$tries" ]; then sleep "$delay" fi done return 1 } wait_init_ready() { local token="$1" local env_name="${2:-test}" local tries="$3" local delay="$4" local try debug_raw debug_code debug_body status_raw status_code status_body ready done dbg_ready exec_ready exec_pods router_ready router_pods for try in $(seq 1 "$tries"); do debug_raw=$(auth_get "/ns/debug" "$token" "$env_name") debug_code=$(printf '%s' "$debug_raw" | tail -1) debug_body=$(printf '%s' "$debug_raw" | sed '$d') status_raw=$(auth_get "/ns/status" "$token" "$env_name") status_code=$(printf '%s' "$status_raw" | tail -1) status_body=$(printf '%s' "$status_raw" | sed '$d') ready=$(json_field "$status_body" ready) done=$(printf '%s' "$status_body" | python3 -c 'import json,sys try: data=json.load(sys.stdin) print(sum(1 for s in data.get("stages", []) if s.get("done"))) except Exception: print(0)') dbg_ready=$(printf '%s' "$debug_body" | python3 -c 'import json,sys try: data=json.load(sys.stdin) print(str(data.get("controlPlane", {}).get("ready", ""))) except Exception: print("")') exec_ready=$(printf '%s' "$debug_body" | python3 -c 'import json,sys try: data=json.load(sys.stdin) print(str(data.get("controlPlane", {}).get("executor", {}).get("ready", ""))) except Exception: print("")') exec_pods=$(printf '%s' "$debug_body" | python3 -c 'import json,sys try: data=json.load(sys.stdin) print(data.get("controlPlane", {}).get("executor", {}).get("podCount", "")) except Exception: print("")') router_ready=$(printf '%s' "$debug_body" | python3 -c 'import json,sys try: data=json.load(sys.stdin) print(str(data.get("controlPlane", {}).get("router", {}).get("ready", ""))) except Exception: print("")') router_pods=$(printf '%s' "$debug_body" | python3 -c 'import json,sys try: data=json.load(sys.stdin) print(data.get("controlPlane", {}).get("router", {}).get("podCount", "")) except Exception: print("")') echo " INIT_TRY=${try} status_http=${status_code} debug_http=${debug_code} ready=${ready} done=${done}/3 cp_ready=${dbg_ready} executor_ready=${exec_ready} executor_pods=${exec_pods} router_ready=${router_ready} router_pods=${router_pods}" if [ "$status_code" = "200" ] && [ "$debug_code" = "200" ] && [ "$ready" = "True" -o "$ready" = "true" ]; then return 0 fi if [ "$try" -lt "$tries" ]; then sleep "$delay" fi done return 1 } echo "" echo "========================================" echo " UI-like user flow tests" echo " Run ID: ${RUN_ID}" echo "========================================" echo "" echo ">>> T01: login с невалидным токеном" R=$(auth_call "not-an-email") CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "401" ] && pass "login invalid token -> 401" || fail "login invalid token -> got $CODE" echo "" echo ">>> T02: login c валидным test token" R=$(auth_call "$LOGIN_SUB") CODE=$(printf '%s' "$R" | tail -1) BODY=$(printf '%s' "$R" | sed '$d') NS=$(json_field "$BODY" namespace) [ "$CODE" = "200" ] && pass "login valid token -> 200" || fail "login valid token -> got $CODE" printf '%s' "$NS" | grep -q '^fission-' && pass "login returns namespace" || fail "login namespace malformed: ${NS}" echo "" echo ">>> T02b: первый вход ждёт init overlay до 3/3" if wait_init_ready "$LOGIN_SUB" "test" 12 5; then pass "init overlay path -> namespace ready 3/3" else fail "init overlay path did not reach 3/3" fi echo "" echo ">>> T03: ошибки create, которые типичны для UI" R=$(create_call "$NEG_SUB" '{"name":"","language":"python","code":"def main():\n return 1"}') CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "400" ] && pass "empty name -> 400" || fail "empty name -> got $CODE" R=$(create_call "$NEG_SUB" '{"name":"empty-code","language":"python","code":" "}') CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "400" ] && pass "empty code -> 400" || fail "empty code -> got $CODE" R=$(create_call "$NEG_SUB" '{"name":"bad ttl","language":"python","code":"def main():\n return 1","ttl":"99z"}') CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "400" ] && pass "invalid ttl -> 400" || fail "invalid ttl -> got $CODE" R=$(create_call "$NEG_SUB" '{"name":"bad name fn","language":"python","code":"def main():\n return 1"}') CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" != "201" ] && [ "$CODE" != "200" ] && pass "invalid name with spaces rejected" || fail "invalid name unexpectedly created" R=$(create_call "$NEG_SUB" '{"name":"bad-lang","language":"cobol","code":"x"}') CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "400" ] && pass "invalid language -> 400" || fail "invalid language -> got $CODE" echo "" echo ">>> T04: duplicate create и повторный delete" R=$(create_call "$NEG_SUB" '{"name":"dup-ui","language":"python","entrypoint":"main.main","code":"def main():\n return \"dup-ui\""}') CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "201" ] && pass "first create -> 201" || fail "first create -> got $CODE" R=$(create_call "$NEG_SUB" '{"name":"dup-ui","language":"python","entrypoint":"main.main","code":"def main():\n return \"dup-ui-2\""}') CODE=$(printf '%s' "$R" | tail -1) ([ "$CODE" = "409" ] || [ "$CODE" = "502" ]) && pass "duplicate create rejected" || fail "duplicate create -> got $CODE" DC=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/dup-ui" -H "X-Test-Sub: ${NEG_SUB}") [ "$DC" = "200" ] && pass "first delete -> 200" || fail "first delete -> got $DC" DC2=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/dup-ui" -H "X-Test-Sub: ${NEG_SUB}") [ "$DC2" = "404" ] && pass "second delete -> 404" || fail "second delete -> got $DC2" echo "" echo ">>> T05: Python create/invoke как из UI" R=$(create_call "$LANG_SUB" '{"name":"py-ui","language":"python","entrypoint":"main.main","code":"def main():\n return \"py-ui-ok\""}') CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "201" ] && pass "python create -> 201" || fail "python create -> got $CODE" if wait_invoke_ok "$LANG_SUB" "py-ui" "py-ui-ok" 8 5; then pass "python invoke -> 200" else fail "python invoke did not stabilize" fi PY_PAR1=$(invoke_call "$LANG_SUB" "py-ui") PY_PAR2=$(invoke_call "$LANG_SUB" "py-ui") [ "$(json_field "$PY_PAR1" status)" = "200" ] && [ "$(json_field "$PY_PAR2" status)" = "200" ] && pass "python double invoke -> both 200" || fail "python double invoke did not return two 200s" echo "" echo ">>> T06: NodeJS create/invoke и edit/update" R=$(create_call "$NODE_SUB" '{"name":"node-ui","language":"nodejs","entrypoint":"main","code":"module.exports = async function() { return { status: 200, body: \"node-ui-ok\" }; }"}') CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "201" ] && pass "nodejs create -> 201" || fail "nodejs create -> got $CODE" if wait_invoke_ok "$NODE_SUB" "node-ui" "node-ui-ok" 16 5; then pass "nodejs invoke -> 200" else fail "nodejs invoke did not stabilize" fi UPD=$(curl -s -w "\n%{http_code}" --max-time 60 -X PUT "${BASE}/functions/node-ui/code" \ -H "X-Test-Sub: ${NODE_SUB}" -H 'Content-Type: application/json' \ -d '{"code":"module.exports = async function() { return { status: 200, body: \"node-ui-updated\" }; }"}') CODE=$(printf '%s' "$UPD" | tail -1) [ "$CODE" = "200" ] && pass "nodejs update -> 200" || fail "nodejs update -> got $CODE" if wait_invoke_ok "$NODE_SUB" "node-ui" "node-ui-updated" 16 5; then pass "nodejs invoke after edit -> updated body" else fail "nodejs invoke after edit did not return updated body" fi echo "" echo ">>> T07: PHP и Ruby в одном namespace" PHP_CODE=$(python3 -c 'import json; print(json.dumps("getBody()->write(\"php-ui-ok\");\n}"))') RUBY_CODE=$(python3 -c 'import json; print(json.dumps("def handler\n \"ruby-ui-ok\"\nend"))') R=$(create_call "$STACK_SUB" "{\"name\":\"php-ui\",\"language\":\"php\",\"entrypoint\":\"main.php::handler\",\"code\":${PHP_CODE}}") CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "201" ] && pass "php create -> 201" || fail "php create -> got $CODE" if wait_invoke_ok "$STACK_SUB" "php-ui" "php-ui-ok" 24 10; then pass "php invoke -> 200" else fail "php invoke did not stabilize" fi R=$(create_call "$STACK_SUB" "{\"name\":\"ruby-ui\",\"language\":\"ruby\",\"entrypoint\":\"handler\",\"code\":${RUBY_CODE}}") CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "201" ] && pass "ruby create -> 201" || fail "ruby create -> got $CODE" if wait_invoke_ok "$STACK_SUB" "ruby-ui" "ruby-ui-ok" 24 10; then pass "ruby invoke -> 200" else fail "ruby invoke did not stabilize" fi echo "" echo ">>> T08: плохой entrypoint как пользовательский промах" R=$(create_call "$NEG_SUB" '{"name":"node-bad-entry","language":"nodejs","entrypoint":"missing","code":"module.exports = async function() { return { status: 200, body: \"bad\" }; }"}') CODE=$(printf '%s' "$R" | tail -1) [ "$CODE" = "201" ] && pass "bad entrypoint function created" || fail "bad entrypoint create -> got $CODE" RESP=$(invoke_call "$NEG_SUB" "node-bad-entry") STATUS=$(json_field "$RESP" status) ERR=$(json_field "$RESP" error) [ "$STATUS" != "200" ] && pass "bad entrypoint invoke fails as expected" || fail "bad entrypoint unexpectedly invoked" printf '%s' "$ERR" | grep -Eiq 'timeout|specialization|error|not found' && pass "bad entrypoint error is visible" || fail "bad entrypoint error text weak: ${ERR}" echo "" echo "========================================" echo " RESULT: PASS=${PASS} FAIL=${FAIL}" echo "========================================" [ "$FAIL" -eq 0 ]