From bf9ada20a2ba19b49b6f687daf9317ae57583662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 3 May 2026 16:39:35 +0400 Subject: [PATCH] =?UTF-8?q?tests:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D1=81=D0=B5=20=D1=82=D0=B5=D1=81=D1=82-?= =?UTF-8?q?=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D1=8B=20=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/bench_heavy.py | 1106 ------------------------------- scripts/lib.sh | 91 +++ scripts/pre_cleanup.sh | 38 ++ scripts/smoke_live_multiuser.sh | 134 ---- scripts/smoke_user_journeys.sh | 119 ---- scripts/test59.sh | 21 - scripts/test_auth.sh | 107 +++ scripts/test_functions_crud.sh | 207 ++++++ scripts/test_invoke.sh | 90 +++ scripts/test_isolation.sh | 162 +++++ scripts/test_layer1.sh | 158 ----- scripts/test_linters.sh | 0 scripts/test_multitenant_ns.sh | 214 ------ scripts/test_simple_function.sh | 812 ----------------------- scripts/test_stress.sh | 158 +++++ scripts/test_ttl.sh | 93 +++ scripts/test_ui_user_flows.sh | 326 --------- scripts/test_update.sh | 105 +++ scripts/tests_v2.sh | 783 ---------------------- 19 files changed, 1051 insertions(+), 3673 deletions(-) delete mode 100644 scripts/bench_heavy.py create mode 100755 scripts/lib.sh create mode 100755 scripts/pre_cleanup.sh delete mode 100755 scripts/smoke_live_multiuser.sh delete mode 100755 scripts/smoke_user_journeys.sh delete mode 100644 scripts/test59.sh create mode 100755 scripts/test_auth.sh create mode 100755 scripts/test_functions_crud.sh create mode 100755 scripts/test_invoke.sh create mode 100755 scripts/test_isolation.sh delete mode 100644 scripts/test_layer1.sh mode change 100644 => 100755 scripts/test_linters.sh delete mode 100755 scripts/test_multitenant_ns.sh delete mode 100755 scripts/test_simple_function.sh create mode 100755 scripts/test_stress.sh create mode 100755 scripts/test_ttl.sh delete mode 100755 scripts/test_ui_user_flows.sh create mode 100755 scripts/test_update.sh delete mode 100644 scripts/tests_v2.sh diff --git a/scripts/bench_heavy.py b/scripts/bench_heavy.py deleted file mode 100644 index bf072d9..0000000 --- a/scripts/bench_heavy.py +++ /dev/null @@ -1,1106 +0,0 @@ -#!/usr/bin/env python3 -""" -Создаёт/обновляет 6 benchmark-функций с ~10KB кода и тяжёлой вычислительной нагрузкой. -Случайные задержки, матрицы, сортировки, хэши — настоящая нагрузка. -""" -import json, subprocess, sys - -BASE = "https://fission.kube5s.ru/console/api" -SUB = "livetest@test.local" - -# ── Node.js ────────────────────────────────────────────────────────────────── -NODE_CODE = r"""module.exports = async (ctx) => { - // ~10KB of real CPU work + async delays with random timing - - function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } - - function millerRabin(n) { - if (n < 2) return false; if (n===2||n===3||n===5||n===7) return true; - if (n%2===0) return false; - let d=n-1, r=0; while(d%2===0){d/=2;r++;} - const witnesses=[2,3,5,7,11,13,17,19,23,29,31,37]; - for(const a of witnesses){ - if(a>=n)continue; let x=modPow(a,d,n); - if(x===1||x===n-1)continue; let cont=false; - for(let i=0;i0n){if(e%2n===1n)result=result*b%m;e>>=1n;b=b*b%m;} - return Number(result); - } - - function gcd(a,b){while(b){[a,b]=[b,a%b];}return a;} - function lcm(a,b){return a/gcd(a,b)*b;} - - function lucas(n){ - if(n===0)return 2; if(n===1)return 1; - let a=2,b=1; for(let i=2;i<=n;i++){let t=a+b;a=b;b=t;} return b; - } - - function fnv1a(str){ - let h=2166136261>>>0; - for(let i=0;i>>0;} - return h; - } - - function collatz(n){let s=0;while(n!==1){n=n%2===0?n/2:3*n+1;s++;}return s;} - - function hornerEval(coeffs,x){ - let r=0; for(let i=coeffs.length-1;i>=0;i--)r=r*x+coeffs[i]; return r; - } - - function digitSum(n){let s=0;while(n>0){s+=n%10;n=Math.floor(n/10);}return s;} - - function sundaramSieve(n){ - const lim=Math.floor((n-2)/2); - const s=new Array(lim+1).fill(false); - for(let i=1;i<=lim;i++) for(let j=i;i+j+2*i*j<=lim;j++) s[i+j+2*i*j]=true; - const primes=[2]; - for(let i=1;i<=lim;i++) if(!s[i]) primes.push(2*i+1); - return primes; - } - - function rleEncode(arr){ - if(!arr.length)return[]; - const out=[];let cur=arr[0],cnt=1; - for(let i=1;ia[m])m=l;if(ra[m])m=r;if(m!==i){[a[i],a[m]]=[a[m],a[i]];heapify(m,size);}} - for(let i=Math.floor(n/2)-1;i>=0;i--)heapify(i,n); - for(let i=n-1;i>0;i--){[a[0],a[i]]=[a[i],a[0]];heapify(0,i);} - return a; - } - - function fib(n) { - if (n <= 1) return n; - let a = 0, b = 1; - for (let i = 2; i <= n; i++) { let t = a + b; a = b; b = t; } - return b; - } - - function isPrime(n) { - if (n < 2) return false; - if (n === 2) return true; - if (n % 2 === 0) return false; - for (let i = 3; i * i <= n; i += 2) if (n % i === 0) return false; - return true; - } - - function sieveOfEratosthenes(limit) { - const sieve = new Array(limit + 1).fill(true); - sieve[0] = sieve[1] = false; - for (let i = 2; i * i <= limit; i++) { - if (sieve[i]) for (let j = i * i; j <= limit; j += i) sieve[j] = false; - } - return sieve.map((v, i) => v ? i : -1).filter(v => v > 0); - } - - function matMul(A, B, n) { - const C = Array.from({length: n}, () => new Array(n).fill(0)); - for (let i = 0; i < n; i++) - for (let k = 0; k < n; k++) - for (let j = 0; j < n; j++) - C[i][j] += A[i][k] * B[k][j]; - return C; - } - - function makeMatrix(n, seed) { - const M = []; - for (let i = 0; i < n; i++) { - const row = []; - for (let j = 0; j < n; j++) row.push(((i * n + j + seed) % 17) * 0.1 + 1); - M.push(row); - } - return M; - } - - function bubbleSort(arr) { - const a = [...arr]; - for (let i = 0; i < a.length; i++) - for (let j = 0; j < a.length - i - 1; j++) - if (a[j] > a[j+1]) { let t = a[j]; a[j] = a[j+1]; a[j+1] = t; } - return a; - } - - function mergeSort(arr) { - if (arr.length <= 1) return arr; - const mid = Math.floor(arr.length / 2); - const L = mergeSort(arr.slice(0, mid)); - const R = mergeSort(arr.slice(mid)); - const res = []; - let i = 0, j = 0; - while (i < L.length && j < R.length) - res.push(L[i] <= R[j] ? L[i++] : R[j++]); - return res.concat(L.slice(i)).concat(R.slice(j)); - } - - function lcg(seed) { - // Linear Congruential Generator for pseudo-random - return ((seed * 1664525 + 1013904223) & 0xffffffff) >>> 0; - } - - function generateArray(size, seed) { - const arr = []; - let s = seed; - for (let i = 0; i < size; i++) { s = lcg(s); arr.push(s % 10000); } - return arr; - } - - function polyEval(coeffs, x) { - let result = 0, power = 1; - for (const c of coeffs) { result += c * power; power *= x; } - return result; - } - - function computeHash(str) { - let hash = 5381; - for (let i = 0; i < str.length; i++) hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff; - return hash >>> 0; - } - - function runRLE(data) { - // Run-length encoding - if (!data.length) return []; - const out = []; - let cur = data[0], cnt = 1; - for (let i = 1; i < data.length; i++) { - if (data[i] === cur) cnt++; - else { out.push([cur, cnt]); cur = data[i]; cnt = 1; } - } - out.push([cur, cnt]); - return out; - } - - // Random delay 200ms–1500ms - const delay = 200 + (lcg(Date.now() & 0xffff) % 1300); - await sleep(delay); - - // Phase 1: Sieve primes up to 2000 - const primes = sieveOfEratosthenes(2000); - await sleep(50 + (lcg(primes.length) % 200)); - - // Phase 2: Fibonacci batch - const fibs = []; - for (let i = 20; i <= 50; i += 5) fibs.push({ n: i, val: fib(i) }); - await sleep(30 + (lcg(fibs.length * 7) % 150)); - - // Phase 3: Matrix multiply 20×20 - const A = makeMatrix(20, 3); - const B = makeMatrix(20, 7); - const C = matMul(A, B, 20); - const matSum = C.reduce((s, row) => s + row.reduce((a, b) => a + b, 0), 0); - await sleep(20 + (lcg(Math.floor(matSum) & 0xffff) % 100)); - - // Phase 4: Sort 800 elements two ways - const arr800 = generateArray(800, 42); - const bSorted = bubbleSort(arr800.slice(0, 200)); - const mSorted = mergeSort(arr800); - await sleep(10 + (lcg(bSorted[0]) % 80)); - - // Phase 5: Polynomial evaluation - const coeffs = [1, -3, 2, 5, -1, 4, -2, 1, 3, -1, 2, 1, -3, 2, 5, -1]; - const polyResults = []; - for (let x = -5; x <= 5; x += 0.5) polyResults.push(polyEval(coeffs, x)); - await sleep(10 + (lcg(polyResults.length * 3) % 60)); - - // Phase 6: Hash chain - let h = computeHash("bench-node-start"); - const hashChain = [h]; - for (let i = 0; i < 500; i++) { h = computeHash(String(h)); hashChain.push(h); } - - // Phase 7: RLE on prime indicators - const indicators = Array.from({length: 500}, (_, i) => isPrime(i) ? 1 : 0); - const rle = runRLE(indicators); - - await sleep(10 + (lcg(hashChain[hashChain.length - 1] & 0xffff) % 50)); - - return { - body: JSON.stringify({ - language: "nodejs", - delay_ms: delay, - prime_count: primes.length, - prime_sum: primes.reduce((a, b) => a + b, 0), - fibs: fibs, - mat_sum: matSum.toFixed(4), - sort_check: mSorted[0] <= mSorted[mSorted.length - 1], - poly_max: Math.max(...polyResults).toFixed(4), - hash_final: hashChain[hashChain.length - 1], - rle_segments: rle.length, - }) - }; -}; -""" - -# ── PHP ─────────────────────────────────────────────────────────────────────── -PHP_CODE = r"""= 0 && $arr[$j] > $key) { $arr[$j + 1] = $arr[$j]; $j--; } - $arr[$j + 1] = $key; - } - return $arr; - } - - function poly_eval($coeffs, $x) { - $result = 0.0; $power = 1.0; - foreach ($coeffs as $c) { $result += $c * $power; $power *= $x; } - return $result; - } - - function djb2($str) { - $hash = 5381; - for ($i = 0; $i < strlen($str); $i++) $hash = (($hash << 5) + $hash + ord($str[$i])) & 0x7fffffff; - return $hash; - } - - // Random delay 300ms-1200ms via usleep - $seed = (int)(microtime(true) * 1000) & 0xffff; - $delay_us = (300 + lcg_rand($seed) % 900) * 1000; - usleep($delay_us); - - // Phase 1: Sieve primes up to 2000 - $primes = sieve(2000); - $prime_sum = array_sum($primes); - - // Phase 2: Fibonacci batch - $fibs = []; - for ($i = 20; $i <= 50; $i += 5) $fibs[$i] = fib($i); - usleep(30000 + lcg_rand(count($fibs)*7) % 100000); - - // Phase 3: Matrix 20x20 - $A = make_matrix(20, 5); - $B = make_matrix(20, 9); - $C = mat_mul($A, $B, 20); - $mat_sum = 0.0; - foreach ($C as $row) foreach ($row as $v) $mat_sum += $v; - usleep(20000 + lcg_rand((int)$mat_sum & 0xffff) % 80000); - - // Phase 4: Insertion sort 300 elements - $arr = []; - $s2 = 99; - for ($i = 0; $i < 300; $i++) { $s2 = lcg_rand($s2); $arr[] = $s2 % 10000; } - $sorted = insertion_sort($arr); - usleep(10000 + lcg_rand($sorted[0]) % 60000); - - // Phase 5: Polynomial evaluation - $coeffs = [1, -3, 2, 5, -1, 4, -2, 1, 3, -1, 2, 1, -3, 2, 5, -1]; - $poly_max = -1e18; - for ($x = -5.0; $x <= 5.0; $x += 0.5) { - $v = poly_eval($coeffs, $x); - if ($v > $poly_max) $poly_max = $v; - } - usleep(10000 + lcg_rand(count($primes)) % 50000); - - // Phase 6: Hash chain 500 iterations - $h = djb2("bench-php-start"); - $chain = [$h]; - for ($i = 0; $i < 500; $i++) { $h = djb2((string)$h); $chain[] = $h; } - - // Phase 7: Count twin primes - $twins = 0; - for ($i = 0; $i < count($primes) - 1; $i++) - if ($primes[$i + 1] - $primes[$i] === 2) $twins++; - - usleep(10000 + lcg_rand(end($chain)) % 40000); - - $elapsed = round((microtime(true) - $startTime) * 1000); - - $context["response"]->getBody()->write(json_encode([ - "language" => "php", - "delay_ms" => (int)($delay_us / 1000), - "elapsed_ms" => $elapsed, - "prime_count" => count($primes), - "prime_sum" => $prime_sum, - "fib45" => $fibs[45], - "mat_sum" => round($mat_sum, 4), - "sort_ok" => $sorted[0] <= $sorted[count($sorted)-1], - "poly_max" => round($poly_max, 4), - "hash_final" => end($chain), - "twin_primes" => $twins, - ])); -} -""" - -# ── Ruby ────────────────────────────────────────────────────────────────────── -RUBY_CODE = r"""def handler - require 'json' - - start_time = Time.now - - # LCG pseudo-random - lcg = ->(s) { ((s * 1664525 + 1013904223) & 0x7fffffff) } - - is_prime = ->(n) { - return false if n < 2 - return true if n == 2 - return false if n.even? - d = 3; while d * d <= n; return false if n % d == 0; d += 2; end - true - } - - sieve = ->(limit) { - s = Array.new(limit + 1, true); s[0] = s[1] = false - i = 2; while i * i <= limit - if s[i]; j = i * i; while j <= limit; s[j] = false; j += i; end; end - i += 1 - end - (2..limit).select { |k| s[k] } - } - - mat_mul = ->(a, b, n) { - c = Array.new(n) { Array.new(n, 0.0) } - n.times { |i| n.times { |k| n.times { |j| c[i][j] += a[i][k] * b[k][j] } } } - c - } - - make_matrix = ->(n, seed) { - Array.new(n) { |i| Array.new(n) { |j| ((i * n + j + seed) % 17) * 0.1 + 1.0 } } - } - - fib = ->(n) { - return n if n <= 1 - a, b = 0, 1; (n - 1).times { a, b = b, a + b }; b - } - - shell_sort = ->(arr) { - a = arr.dup; gap = a.length / 2 - while gap > 0 - (gap...a.length).each { |i| tmp = a[i]; j = i; while j >= gap && a[j-gap] > tmp; a[j] = a[j-gap]; j -= gap; end; a[j] = tmp } - gap /= 2 - end - a - } - - poly_eval = ->(coeffs, x) { - result = 0.0; power = 1.0 - coeffs.each { |c| result += c * power; power *= x } - result - } - - djb2 = ->(str) { - h = 5381; str.each_byte { |b| h = ((h << 5) + h + b) & 0x7fffffff }; h - } - - collatz = ->(n) { steps = 0; while n != 1; n = n.even? ? n / 2 : 3 * n + 1; steps += 1; end; steps } - - # Random delay 250ms-1600ms - seed = (Time.now.to_f * 1000).to_i & 0xffff - delay_ms = 250 + lcg.(seed) % 1350 - sleep(delay_ms / 1000.0) - - # Phase 1: Sieve - primes = sieve.(2000) - prime_sum = primes.sum - sleep(0.05 + (lcg.(primes.length) % 200) / 1000.0) - - # Phase 2: Fibonacci batch - fibs = (20..50).step(5).map { |n| [n, fib.(n)] }.to_h - sleep(0.03 + (lcg.(fibs.length * 7) % 150) / 1000.0) - - # Phase 3: Matrix 20x20 - a_mat = make_matrix.(20, 4) - b_mat = make_matrix.(20, 8) - c_mat = mat_mul.(a_mat, b_mat, 20) - mat_sum = c_mat.flatten.sum - sleep(0.02 + (lcg.(mat_sum.to_i & 0xffff) % 100) / 1000.0) - - # Phase 4: Shell sort 500 elements - s2 = 77 - arr = Array.new(500) { s2 = lcg.(s2); s2 % 10000 } - sorted = shell_sort.(arr) - sleep(0.01 + (lcg.(sorted.first) % 80) / 1000.0) - - # Phase 5: Polynomial - coeffs = [1, -3, 2, 5, -1, 4, -2, 1, 3, -1, 2, 1, -3, 2, 5, -1] - poly_max = (-5.0..5.0).step(0.5).map { |x| poly_eval.(coeffs, x) }.max - sleep(0.01 + (lcg.(primes.length) % 60) / 1000.0) - - # Phase 6: Hash chain - h = djb2.("bench-ruby-start") - chain = [h] - 500.times { h = djb2.(h.to_s); chain << h } - - # Phase 7: Collatz max steps for 1..200 - collatz_max = (1..200).map { |n| collatz.(n) }.max - sleep(0.01 + (lcg.(chain.last & 0xffff) % 50) / 1000.0) - - elapsed_ms = ((Time.now - start_time) * 1000).round - - { - language: "ruby", - delay_ms: delay_ms, - elapsed_ms: elapsed_ms, - prime_count: primes.size, - prime_sum: prime_sum, - fib45: fibs[45], - mat_sum: mat_sum.round(4), - sort_ok: sorted.first <= sorted.last, - poly_max: poly_max.round(4), - hash_final: chain.last, - collatz_max: collatz_max, - }.to_json -end -""" - -# ── Perl ────────────────────────────────────────────────────────────────────── -PERL_CODE = r"""sub { - use Time::HiRes qw(time usleep); - use POSIX qw(floor); - - my $start = time(); - - sub lcg_r { return (($_[0] * 1664525 + 1013904223) & 0x7fffffff); } - - sub is_prime_p { - my $n = shift; return 0 if $n < 2; return 1 if $n == 2; - return 0 if $n % 2 == 0; - my $i = 3; while ($i * $i <= $n) { return 0 if $n % $i == 0; $i += 2; } return 1; - } - - sub sieve_p { - my $lim = shift; - my @s = (1) x ($lim + 1); $s[0] = $s[1] = 0; - for (my $i = 2; $i * $i <= $lim; $i++) { - if ($s[$i]) { for (my $j = $i*$i; $j <= $lim; $j += $i) { $s[$j] = 0; } } - } - return grep { $s[$_] } 2..$lim; - } - - sub fib_p { - my $n = shift; return $n if $n <= 1; - my ($a, $b) = (0, 1); - for (my $i = 2; $i <= $n; $i++) { ($a, $b) = ($b, $a + $b); } - return $b; - } - - sub mat_mul_p { - my ($A, $B, $n) = @_; - my @C = map { [(0) x $n] } 0..$n-1; - for my $i (0..$n-1) { for my $k (0..$n-1) { for my $j (0..$n-1) { - $C[$i][$j] += $A->[$i][$k] * $B->[$k][$j]; - }}} - return @C; - } - - sub make_mat_p { - my ($n, $seed) = @_; - return map { my $i = $_; [ map { (($i*$n+$_+$seed)%17)*0.1+1.0 } 0..$n-1 ] } 0..$n-1; - } - - sub djb2_p { - my $str = shift; my $h = 5381; - for my $c (split //, $str) { $h = (($h<<5)+$h+ord($c)) & 0x7fffffff; } - return $h; - } - - sub selection_sort_p { - my @a = @_; my $n = scalar @a; - for my $i (0..$n-2) { - my $min = $i; - for my $j ($i+1..$n-1) { $min = $j if $a[$j] < $a[$min]; } - @a[$i, $min] = @a[$min, $i] if $min != $i; - } - return @a; - } - - sub poly_eval_p { - my ($coeffs, $x) = @_; my ($r, $p) = (0.0, 1.0); - for my $c (@$coeffs) { $r += $c * $p; $p *= $x; } - return $r; - } - - sub collatz_p { - my $n = shift; my $steps = 0; - while ($n != 1) { $n = ($n % 2 == 0) ? $n/2 : 3*$n+1; $steps++; } - return $steps; - } - - # Random delay 300ms-1500ms - my $seed = int(time() * 1000) & 0xffff; - my $delay_us = (300 + lcg_r($seed) % 1200) * 1000; - usleep($delay_us); - - # Phase 1: Sieve - my @primes = sieve_p(2000); - my $prime_sum = 0; $prime_sum += $_ for @primes; - usleep(50000 + lcg_r(scalar @primes) % 200000); - - # Phase 2: Fibonacci - my %fibs = map { $_ => fib_p($_) } grep { $_ % 5 == 0 } 20..50; - usleep(30000 + lcg_r(scalar keys %fibs) % 150000); - - # Phase 3: Matrix 15x15 - my @A = make_mat_p(15, 6); - my @B = make_mat_p(15, 11); - my @C = mat_mul_p(\@A, \@B, 15); - my $mat_sum = 0.0; - for my $row (@C) { $mat_sum += $_ for @$row; } - usleep(20000 + lcg_r(int($mat_sum) & 0xffff) % 100000); - - # Phase 4: Selection sort 250 elements - my $s2 = 55; my @arr; - for (1..250) { $s2 = lcg_r($s2); push @arr, $s2 % 10000; } - my @sorted = selection_sort_p(@arr); - usleep(10000 + lcg_r($sorted[0]) % 80000); - - # Phase 5: Polynomial - my @coeffs = (1, -3, 2, 5, -1, 4, -2, 1, 3, -1, 2, 1, -3, 2, 5, -1); - my $poly_max = -1e18; - for (my $x = -5.0; $x <= 5.0; $x += 0.5) { - my $v = poly_eval_p(\@coeffs, $x); $poly_max = $v if $v > $poly_max; - } - usleep(10000 + lcg_r(scalar @primes) % 60000); - - # Phase 6: Hash chain - my $h = djb2_p("bench-perl-start"); - my @chain = ($h); - for (1..500) { $h = djb2_p("$h"); push @chain, $h; } - - # Phase 7: Collatz - my $cmax = 0; - for my $n (1..200) { my $s = collatz_p($n); $cmax = $s if $s > $cmax; } - usleep(10000 + lcg_r($chain[-1] & 0xffff) % 50000); - - my $elapsed_ms = int((time() - $start) * 1000); - - return sprintf('{"language":"perl","delay_ms":%d,"elapsed_ms":%d,"prime_count":%d,"prime_sum":%d,"fib45":%d,"mat_sum":%.4f,"sort_ok":%s,"poly_max":%.4f,"hash_final":%d,"collatz_max":%d}', - int($delay_us/1000), $elapsed_ms, scalar @primes, $prime_sum, $fibs{45}, - $mat_sum, ($sorted[0] <= $sorted[-1] ? "true" : "false"), - $poly_max, $chain[-1], $cmax); -} -""" - -# ── Go ──────────────────────────────────────────────────────────────────────── -GO_CODE = r"""package main - -import ( - "encoding/json" - "fmt" - "math" - "math/rand" - "net/http" - "sort" - "strings" - "time" -) - -func sieveGo(limit int) []int { - s := make([]bool, limit+1) - for i := range s { s[i] = true } - s[0], s[1] = false, false - for i := 2; i*i <= limit; i++ { - if s[i] { for j := i * i; j <= limit; j += i { s[j] = false } } - } - primes := []int{} - for i := 2; i <= limit; i++ { if s[i] { primes = append(primes, i) } } - return primes -} - -func fibGo(n int) int64 { - if n <= 1 { return int64(n) } - a, b := int64(0), int64(1) - for i := 2; i <= n; i++ { a, b = b, a+b } - return b -} - -func matMulGo(a, b [20][20]float64) [20][20]float64 { - var c [20][20]float64 - for i := 0; i < 20; i++ { - for k := 0; k < 20; k++ { - for j := 0; j < 20; j++ { c[i][j] += a[i][k] * b[k][j] } - } - } - return c -} - -func makeMatGo(seed float64) [20][20]float64 { - var m [20][20]float64 - for i := 0; i < 20; i++ { - for j := 0; j < 20; j++ { - m[i][j] = math.Mod(float64(i*20+j)+seed, 17)*0.1 + 1.0 - } - } - return m -} - -func shellSortGo(arr []int) []int { - a := make([]int, len(arr)); copy(a, arr) - for gap := len(a) / 2; gap > 0; gap /= 2 { - for i := gap; i < len(a); i++ { - tmp := a[i]; j := i - for j >= gap && a[j-gap] > tmp { a[j] = a[j-gap]; j -= gap } - a[j] = tmp - } - } - return a -} - -func quickSortGo(arr []int) []int { - a := make([]int, len(arr)); copy(a, arr) - sort.Ints(a) - return a -} - -func polyEvalGo(coeffs []float64, x float64) float64 { - result, power := 0.0, 1.0 - for _, c := range coeffs { result += c * power; power *= x } - return result -} - -func djb2Go(s string) uint32 { - h := uint32(5381) - for _, c := range s { h = ((h << 5) + h + uint32(c)) & 0x7fffffff } - return h -} - -func fnv1aGo(s string) uint32 { - h := uint32(2166136261) - for _, c := range []byte(s) { h ^= uint32(c); h *= 16777619 } - return h -} - -func collatzGo(n int) int { - steps := 0 - for n != 1 { - if n%2 == 0 { n /= 2 } else { n = 3*n + 1 } - steps++ - } - return steps -} - -func isPrimeGo(n int) bool { - if n < 2 { return false } - if n == 2 { return true } - if n%2 == 0 { return false } - for d := 3; d*d <= n; d += 2 { if n%d == 0 { return false } } - return true -} - -func gcdGo(a, b int) int { - for b != 0 { a, b = b, a%b } - return a -} - -func lcmGo(a, b int) int { return a / gcdGo(a, b) * b } - -// Горнер для вычисления полинома -func hornerEval(coeffs []float64, x float64) float64 { - result := 0.0 - for i := len(coeffs) - 1; i >= 0; i-- { result = result*x + coeffs[i] } - return result -} - -// Решето Сундарама -func sundaramSieve(n int) []int { - limit := (n - 2) / 2 - s := make([]bool, limit+1) - for i := 1; i <= limit; i++ { - for j := i; i+j+2*i*j <= limit; j++ { s[i+j+2*i*j] = true } - } - primes := []int{2} - for i := 1; i <= limit; i++ { if !s[i] { primes = append(primes, 2*i+1) } } - return primes -} - -// Простые числа через Miller-Rabin (детерминированный для малых n) -func millerRabinGo(n int) bool { - if n < 2 { return false } - if n == 2 || n == 3 || n == 5 || n == 7 { return true } - if n%2 == 0 { return false } - d, r := n-1, 0 - for d%2 == 0 { d /= 2; r++ } - witnesses := []int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37} - for _, a := range witnesses { - if a >= n { continue } - x := modPowGo(a, d, n) - if x == 1 || x == n-1 { continue } - cont := false - for _ = range make([]struct{}, r-1) { - x = modPowGo(x, 2, n) - if x == n-1 { cont = true; break } - } - if !cont { return false } - } - return true -} - -func modPowGo(base, exp, mod int) int { - result := 1; base %= mod - for exp > 0 { - if exp%2 == 1 { result = result * base % mod } - exp /= 2; base = base * base % mod - } - return result -} - -// Числа Люка -func lucasGo(n int) int64 { - if n == 0 { return 2 } - if n == 1 { return 1 } - a, b := int64(2), int64(1) - for i := 2; i <= n; i++ { a, b = b, a+b } - return b -} - -// Цифровая сумма -func digitSumGo(n int) int { - s := 0 - for n > 0 { s += n % 10; n /= 10 } - return s -} - -func Handler(w http.ResponseWriter, r *http.Request) { - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - - // Random delay 300ms-1800ms - delayMs := 300 + rng.Intn(1500) - time.Sleep(time.Duration(delayMs) * time.Millisecond) - - // Phase 1: Sieve of Eratosthenes up to 3000 - primes := sieveGo(3000) - primeSum := 0 - for _, p := range primes { primeSum += p } - time.Sleep(time.Duration(50+rng.Intn(200)) * time.Millisecond) - - // Phase 2: Sundaram sieve cross-check - primes2 := sundaramSieve(3000) - _ = primes2 - time.Sleep(time.Duration(30+rng.Intn(150)) * time.Millisecond) - - // Phase 3: Miller-Rabin for 1000..1200 - mrCount := 0 - for n := 1000; n <= 1200; n++ { if millerRabinGo(n) { mrCount++ } } - time.Sleep(time.Duration(20+rng.Intn(100)) * time.Millisecond) - - // Phase 4: Fibonacci + Lucas + digit sums - type entry struct{ N int; Fib, Lucas int64; DigSum int } - entries := []entry{} - for n := 20; n <= 60; n += 5 { - f := fibGo(n); l := lucasGo(n) - entries = append(entries, entry{n, f, l, digitSumGo(int(f % 1000000))}) - } - time.Sleep(time.Duration(30+rng.Intn(150)) * time.Millisecond) - - // Phase 5: Matrix multiply 20x20 - A := makeMatGo(3.0); B := makeMatGo(7.0) - C := matMulGo(A, B) - matSum := 0.0 - for _, row := range C { for _, v := range row { matSum += v } } - time.Sleep(time.Duration(20+rng.Intn(100)) * time.Millisecond) - - // Phase 6: Shell sort + stdlib sort 800 elements, compare - arr := make([]int, 800) - for i := range arr { arr[i] = rng.Intn(10000) } - shellSorted := shellSortGo(arr) - quickSorted := quickSortGo(arr) - sortMatch := true - for i := range shellSorted { if shellSorted[i] != quickSorted[i] { sortMatch = false; break } } - time.Sleep(time.Duration(10+rng.Intn(80)) * time.Millisecond) - - // Phase 7: Polynomial evaluation (Horner vs direct) - coeffs := []float64{1, -3, 2, 5, -1, 4, -2, 1, 3, -1, 2, 1, -3, 2, 5, -1, 3, -2, 1, 4} - polyMax, hornerMax := math.Inf(-1), math.Inf(-1) - for i := -80; i <= 80; i++ { - x := float64(i) * 0.1 - if v := polyEvalGo(coeffs, x); v > polyMax { polyMax = v } - if v := hornerEval(coeffs, x); v > hornerMax { hornerMax = v } - } - time.Sleep(time.Duration(10+rng.Intn(60)) * time.Millisecond) - - // Phase 8: DJB2 + FNV1a hash chains 500 iterations - hDJB := djb2Go("bench-go-start") - hFNV := fnv1aGo("bench-go-start") - for i := 0; i < 500; i++ { - hDJB = djb2Go(fmt.Sprintf("%d", hDJB)) - hFNV = fnv1aGo(fmt.Sprintf("%d", hFNV)) - } - time.Sleep(time.Duration(10+rng.Intn(50)) * time.Millisecond) - - // Phase 9: Collatz max steps for 1..500 - collatzMax, collatzN := 0, 0 - for n := 1; n <= 500; n++ { - if s := collatzGo(n); s > collatzMax { collatzMax = s; collatzN = n } - } - - // Phase 10: GCD/LCM table for first 20 primes - gcdSum, lcmMod := 0, 1 - for i := 0; i < 20 && i < len(primes)-1; i++ { - gcdSum += gcdGo(primes[i], primes[i+1]) - lcmMod = lcmGo(lcmMod, primes[i]) % 1000000007 - } - time.Sleep(time.Duration(10+rng.Intn(40)) * time.Millisecond) - - // Phase 11: Twin primes and cousin primes - twins, cousins := 0, 0 - for i := 0; i < len(primes)-1; i++ { - diff := primes[i+1] - primes[i] - if diff == 2 { twins++ } - if diff == 4 { cousins++ } - } - - // Phase 12: String operations — build prime list string and count digits - var sb strings.Builder - for i, p := range primes { if i >= 100 { break }; fmt.Fprintf(&sb, "%d,", p) } - primeStr := sb.String() - digitCount := 0 - for _, c := range primeStr { if c >= '0' && c <= '9' { digitCount++ } } - - result := map[string]interface{}{ - "language": "go", - "delay_ms": delayMs, - "prime_count": len(primes), - "prime_sum": primeSum, - "mr_count": mrCount, - "fib55": entries[7].Fib, - "lucas55": entries[7].Lucas, - "mat_sum": math.Round(matSum*10000) / 10000, - "sort_match": sortMatch, - "poly_max": math.Round(polyMax*10000) / 10000, - "horner_max": math.Round(hornerMax*10000) / 10000, - "djb2_final": hDJB, - "fnv1a_final": hFNV, - "collatz_max": collatzMax, - "collatz_n": collatzN, - "twin_primes": twins, - "cousin_primes": cousins, - "gcd_sum": gcdSum, - "lcm_mod": lcmMod, - "digit_count": digitCount, - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(result) -} -""" - -# ── Python ──────────────────────────────────────────────────────────────────── -PYTHON_CODE = r"""import time -import math -import random - -def sieve(limit): - s = [True] * (limit + 1); s[0] = s[1] = False - i = 2 - while i * i <= limit: - if s[i]: - j = i * i - while j <= limit: s[j] = False; j += i - i += 1 - return [i for i in range(2, limit+1) if s[i]] - -def fib(n): - if n <= 1: return n - a, b = 0, 1 - for _ in range(n - 1): a, b = b, a + b - return b - -def mat_mul(A, B, n): - C = [[0.0]*n for _ in range(n)] - for i in range(n): - for k in range(n): - for j in range(n): - C[i][j] += A[i][k] * B[k][j] - return C - -def make_matrix(n, seed): - return [[(((i*n+j+seed)%17)*0.1+1.0) for j in range(n)] for i in range(n)] - -def shell_sort(arr): - a = arr[:] - gap = len(a) // 2 - while gap > 0: - for i in range(gap, len(a)): - tmp = a[i]; j = i - while j >= gap and a[j-gap] > tmp: a[j] = a[j-gap]; j -= gap - a[j] = tmp - gap //= 2 - return a - -def poly_eval(coeffs, x): - result, power = 0.0, 1.0 - for c in coeffs: result += c * power; power *= x - return result - -def djb2(s): - h = 5381 - for c in s: h = ((h << 5) + h + ord(c)) & 0x7fffffff - return h - -def collatz(n): - steps = 0 - while n != 1: n = n // 2 if n % 2 == 0 else 3 * n + 1; steps += 1 - return steps - -def lcg(s): return ((s * 1664525 + 1013904223) & 0x7fffffff) - -def main(): - start = time.time() - - # Random delay 250ms-1500ms - seed = int(start * 1000) & 0xffff - delay_ms = 250 + lcg(seed) % 1250 - time.sleep(delay_ms / 1000.0) - - # Phase 1: Sieve primes up to 2000 - primes = sieve(2000) - prime_sum = sum(primes) - time.sleep(0.05 + (lcg(len(primes)) % 200) / 1000.0) - - # Phase 2: Fibonacci batch - fibs = {n: fib(n) for n in range(20, 51, 5)} - time.sleep(0.03 + (lcg(len(fibs) * 7) % 150) / 1000.0) - - # Phase 3: Matrix 20x20 - A = make_matrix(20, 2) - B = make_matrix(20, 6) - C = mat_mul(A, B, 20) - mat_sum = sum(v for row in C for v in row) - time.sleep(0.02 + (lcg(int(mat_sum) & 0xffff) % 100) / 1000.0) - - # Phase 4: Shell sort 500 elements - s2 = 33 - arr = [] - for _ in range(500): s2 = lcg(s2); arr.append(s2 % 10000) - sorted_arr = shell_sort(arr) - time.sleep(0.01 + (lcg(sorted_arr[0]) % 80) / 1000.0) - - # Phase 5: Polynomial evaluation - coeffs = [1, -3, 2, 5, -1, 4, -2, 1, 3, -1, 2, 1, -3, 2, 5, -1] - poly_vals = [poly_eval(coeffs, x * 0.25) for x in range(-20, 21)] - poly_max = max(poly_vals) - time.sleep(0.01 + (lcg(len(primes)) % 60) / 1000.0) - - # Phase 6: Hash chain 500 iterations - h = djb2("bench-python-start") - chain = [h] - for _ in range(500): h = djb2(str(h)); chain.append(h) - - # Phase 7: Collatz max steps for 1..300 - collatz_max = max(collatz(n) for n in range(1, 301)) - time.sleep(0.01 + (lcg(chain[-1] & 0xffff) % 50) / 1000.0) - - # Phase 8: Sum of squares (1..200) - sq_sum = sum(i * i for i in range(1, 201)) - - # Phase 9: GCD / LCM chain on primes - def gcd(a, b): - while b: a, b = b, a % b - return a - gcds = [gcd(primes[i], primes[i+1]) for i in range(min(50, len(primes)-1))] - - elapsed_ms = round((time.time() - start) * 1000) - - return { - "language": "python", - "delay_ms": delay_ms, - "elapsed_ms": elapsed_ms, - "prime_count": len(primes), - "prime_sum": prime_sum, - "fib45": fibs[45], - "mat_sum": round(mat_sum, 4), - "sort_ok": sorted_arr[0] <= sorted_arr[-1], - "poly_max": round(poly_max, 4), - "hash_final": chain[-1], - "collatz_max": collatz_max, - "sq_sum": sq_sum, - "gcd_sum": sum(gcds), - } -""" - -functions = [ - ("bench-node", "nodejs", NODE_CODE), - ("bench-php", "php", PHP_CODE), - ("bench-ruby", "ruby", RUBY_CODE), - ("bench-perl", "perl", PERL_CODE), - ("bench-go", "go", GO_CODE), - ("bench-python", "python", PYTHON_CODE), -] - -print("=== Удаляем старые bench-функции ===") -for name, _, _ in functions: - result = subprocess.run( - ["curl", "-s", "-X", "DELETE", f"{BASE}/functions/{name}", - "-H", f"X-Auth-Token: {SUB}"], - capture_output=True, text=True - ) - print(f" DELETE {name}: {result.stdout.strip()[:80]}") - -print("\n=== Создаём тяжёлые bench-функции (~10KB) ===") -for name, lang, code in functions: - payload = json.dumps({"name": name, "language": lang, "code": code, "ttl": "2h"}) - size_kb = len(code) / 1024 - result = subprocess.run( - ["curl", "-s", "-X", "POST", f"{BASE}/functions", - "-H", f"X-Auth-Token: {SUB}", "-H", "Content-Type: application/json", - "-d", payload], - capture_output=True, text=True - ) - try: - resp = json.loads(result.stdout) - route = resp.get("route", "?") - print(f" {name} ({size_kb:.1f}KB): {route}") - except Exception as e: - print(f" {name}: PARSE ERROR {result.stdout[:150]}") diff --git a/scripts/lib.sh b/scripts/lib.sh new file mode 100755 index 0000000..1f547a8 --- /dev/null +++ b/scripts/lib.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# lib.sh — общие утилиты для тест-скриптов fission-console. +# Подключается через: source "$(dirname "$0")/lib.sh" + +BASE="${BASE:-https://fission.kube5s.ru/console/api}" +PASS=0 +FAIL=0 + +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 $got, want $want" +} + +# json_field PAYLOAD KEY — извлекает строковое значение ключа из JSON +json_field() { + local payload="$1" key="$2" + printf '%s' "$payload" | python3 -c " +import json, sys +key = sys.argv[1] +try: + d = json.load(sys.stdin) + v = d.get(key) + if v is None: + print('') + elif isinstance(v, (dict, list)): + print(json.dumps(v)) + else: + print(v) +except Exception: + print('') +" "$key" 2>/dev/null +} + +# json_escape — читает stdin, возвращает JSON-строку +json_escape() { + python3 -c 'import json, sys; print(json.dumps(sys.stdin.read()))' +} + +# json_len PAYLOAD — длина JSON-массива +json_len() { + printf '%s' "$1" | python3 -c "import json,sys +try: + print(len(json.load(sys.stdin))) +except Exception: + print(0)" 2>/dev/null +} + +# wait_invoke SUB FN EXPECTED [TRIES=12] [DELAY=5] +# Ждёт пока invoke вернёт status=200 и тело содержит EXPECTED. +# Возвращает 0 при успехе, 1 при таймауте. +wait_invoke() { + local sub="$1" fn="$2" expected="$3" + local tries="${4:-12}" delay="${5:-5}" + local try resp status raw err + for try in $(seq 1 "$tries"); do + resp=$(curl -s --max-time 50 -X POST "${BASE}/functions/${fn}/invoke" \ + -H "X-Auth-Token: ${sub}" -H "Content-Type: application/json" -d '{}') + status=$(json_field "$resp" status) + raw=$(json_field "$resp" response_raw) + err=$(json_field "$resp" error) + echo " TRY=$try fn=$fn status=$status raw=$(printf '%s' "$raw" | head -c 80) err=$(printf '%s' "$err" | head -c 80)" + if [ "$status" = "200" ] && printf '%s' "$raw" | grep -qF "$expected"; then + return 0 + fi + [ "$try" -lt "$tries" ] && sleep "$delay" + done + return 1 +} + +# mk_run_id — возвращает уникальный 8-символьный hex ID +mk_run_id() { + (date +%s%N 2>/dev/null; od -An -N4 -tu4 /dev/urandom 2>/dev/null) \ + | sha256sum | head -c 8 +} + +# setup_cleanup CLEANUP_FN — регистрирует trap EXIT +setup_cleanup() { + trap "$1" EXIT +} + +# print_summary — итог тестов +print_summary() { + echo "" + echo "========================================" + printf " ИТОГ: PASS=%d FAIL=%d\n" "$PASS" "$FAIL" + echo "========================================" + [ "$FAIL" -eq 0 ] +} diff --git a/scripts/pre_cleanup.sh b/scripts/pre_cleanup.sh new file mode 100755 index 0000000..15f03e0 --- /dev/null +++ b/scripts/pre_cleanup.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# pre_cleanup.sh — удаляет ВСЕ тест-namespace-ы fission от прошлых прогонов. +# Паттерны: fission- для namespace-ов созданных тест-субами вида +# crud-*/ttl-*/invoke-*/update-*/isolation-*/stress-*/alice-*/bob-*/ +# linter-*@test.local +# Вызывается из run_all.sh с KUBECTL_DIRECT=1. +set -uo pipefail + +if [ "${KUBECTL_DIRECT:-0}" != "1" ]; then + echo "pre_cleanup.sh: нужен KUBECTL_DIRECT=1 (должен запускаться на VM)" >&2 + exit 1 +fi + +CLEANUP="${BASH_SOURCE%/*}/cleanup_test_namespaces.sh" +if [ ! -x "$CLEANUP" ]; then + echo "pre_cleanup.sh: cleanup_test_namespaces.sh не найден" >&2 + exit 1 +fi + +# Список тест-субов создаёт namespace с именем fission-. +# Получить все fission-namespace-ы кроме системного fission: +NS_LIST=$(kubectl get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | \ + grep -E '^fission-[0-9a-f]{16}$' || true) + +if [ -z "$NS_LIST" ]; then + echo "pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем." + exit 0 +fi + +COUNT=$(echo "$NS_LIST" | wc -l) +echo "pre_cleanup.sh: найдено $COUNT тест-namespace-ов, удаляем..." + +# cleanup_test_namespaces.sh принимает email-субы, а не namespace-ы. +# Но мы можем вызвать kubectl delete напрямую для namespace-ов. +echo "$NS_LIST" | xargs -r kubectl delete namespace --ignore-not-found 2>&1 | \ + grep -v "^$" || true + +echo "pre_cleanup.sh: очистка завершена." diff --git a/scripts/smoke_live_multiuser.sh b/scripts/smoke_live_multiuser.sh deleted file mode 100755 index 7f83937..0000000 --- a/scripts/smoke_live_multiuser.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BASE="https://fission.kube5s.ru/console/api" -COUNT=${1:-10} -RUN_ID="mt$(date +%s | tail -c 6)" -ROOT_DIR=$(cd "$(dirname "$0")" && pwd) -NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" -TMP=$(mktemp -d) - -declare -a USERS=() - -cleanup() { - rm -rf "$TMP" 2>/dev/null || true - if [ -x "$NS_CLEANUP_SCRIPT" ] && [ ${#USERS[@]} -gt 0 ]; then - KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "${USERS[@]}" >/dev/null 2>&1 || true - fi -} -trap cleanup EXIT - -json_escape() { - python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' -} - -init_call() { - local user="$1" - local code="" - local try - for try in 1 2 3; do - code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 -X GET "${BASE}/functions" -H "X-Auth-Token: ${user}" 2>/dev/null || echo 000) - echo "INIT_TRY user=${user} try=${try} http=${code}" >&2 - if [ "$code" = "200" ]; then - printf '%s' "$code" - return 0 - fi - [ "$try" -lt 3 ] && sleep 2 - done - printf '%s' "$code" -} - -invoke_call() { - local user="$1" - local fn="$2" - local code="" - local try - for try in 1 2 3 4; do - code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 40 -X POST "${BASE}/functions/${fn}/invoke" -H "X-Auth-Token: ${user}" -H "Content-Type: application/json" -d '{}' 2>/dev/null || echo 000) - echo "INVOKE_TRY user=${user} fn=${fn} try=${try} http=${code}" >&2 - if [ "$code" = "200" ]; then - printf '%s' "$code" - return 0 - fi - [ "$try" -lt 4 ] && sleep 4 - done - printf '%s' "$code" -} - -echo "=== MULTIUSER TEST START run=$RUN_ID count=$COUNT ===" - -for i in $(seq 1 "$COUNT"); do - USERS+=("${RUN_ID}-u${i}@test.local") - ( - U="${RUN_ID}-u${i}@test.local" - CODE=$(init_call "${U}") - echo "$i:$CODE:$U" > "${TMP}/init-$i" - ) & -done -wait - -INIT_OK=0 -for i in $(seq 1 "$COUNT"); do - R=$(cat "${TMP}/init-$i" 2>/dev/null || echo "$i:000:") - C=$(echo "$R" | cut -d: -f2) - if [ "$C" = "200" ]; then - INIT_OK=$((INIT_OK+1)) - else - echo "INIT_FAIL user$i http=$C" - fi -done -echo "INIT_OK=$INIT_OK/$COUNT" - -for i in $(seq 1 "$COUNT"); do - ( - U="${RUN_ID}-u${i}@test.local" - FN="mtfn-${RUN_ID}-${i}" - CODE_TEXT=$(printf 'def main():\n return "ok-%s-%s"\n' "$RUN_ID" "$i") - CODE_JSON=$(printf '%s' "$CODE_TEXT" | json_escape) - BODY=$(printf '{"name":"%s","language":"python","code":%s}' "$FN" "$CODE_JSON") - C=$(curl -s -o /dev/null -w "%{http_code}" --max-time 60 -X POST "${BASE}/functions" -H "X-Auth-Token: ${U}" -H "Content-Type: application/json" -d "$BODY" 2>/dev/null || echo 000) - echo "$i:$C:$FN:$U" > "${TMP}/create-$i" - ) & -done -wait - -CREATE_OK=0 -for i in $(seq 1 "$COUNT"); do - R=$(cat "${TMP}/create-$i" 2>/dev/null || echo "$i:000::") - C=$(echo "$R" | cut -d: -f2) - if [ "$C" = "200" ] || [ "$C" = "201" ]; then - CREATE_OK=$((CREATE_OK+1)) - else - echo "CREATE_FAIL user$i http=$C" - fi -done -echo "CREATE_OK=$CREATE_OK/$COUNT" - -sleep 8 - -for i in $(seq 1 "$COUNT"); do - ( - U="${RUN_ID}-u${i}@test.local" - FN="mtfn-${RUN_ID}-${i}" - C=$(invoke_call "${U}" "${FN}") - echo "$i:$C" > "${TMP}/invoke-$i" - ) & -done -wait - -INVOKE_OK=0 -for i in $(seq 1 "$COUNT"); do - R=$(cat "${TMP}/invoke-$i" 2>/dev/null || echo "$i:000") - C=$(echo "$R" | cut -d: -f2) - if [ "$C" = "200" ]; then - INVOKE_OK=$((INVOKE_OK+1)) - else - echo "INVOKE_FAIL user$i http=$C" - fi -done -echo "INVOKE_OK=$INVOKE_OK/$COUNT" -echo "=== MULTIUSER TEST END ===" - -[ "$INIT_OK" = "$COUNT" ] -[ "$CREATE_OK" = "$COUNT" ] -[ "$INVOKE_OK" = "$COUNT" ] diff --git a/scripts/smoke_user_journeys.sh b/scripts/smoke_user_journeys.sh deleted file mode 100755 index b981a24..0000000 --- a/scripts/smoke_user_journeys.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BASE="https://fission.kube5s.ru/console/api" -COUNT=${1:-5} -RUN_ID="journey$(date +%s | tail -c 6)" -ROOT_DIR=$(cd "$(dirname "$0")" && pwd) -NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" -TMP=$(mktemp -d) - -declare -a USERS=() - -cleanup() { - rm -rf "$TMP" 2>/dev/null || true - if [ -x "$NS_CLEANUP_SCRIPT" ] && [ ${#USERS[@]} -gt 0 ]; then - KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "${USERS[@]}" >/dev/null 2>&1 || true - fi -} -trap cleanup EXIT - -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" -} - -json_escape() { - python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' -} - -create_call() { - local sub="$1" - local body="$2" - curl -s -w "\n%{http_code}" --max-time 60 -X POST "${BASE}/functions" \ - -H "X-Auth-Token: ${sub}" -H 'Content-Type: application/json' -d "$body" -} - -invoke_call() { - local sub="$1" - local name="$2" - curl -s --max-time 45 -X POST "${BASE}/functions/${name}/invoke" \ - -H "X-Auth-Token: ${sub}" -H 'Content-Type: application/json' -d '{}' || true -} - -echo "=== USER JOURNEYS START run=$RUN_ID count=$COUNT ===" - -for i in $(seq 1 "$COUNT"); do - USERS+=("${RUN_ID}-u${i}@test.local") - ( - U="${RUN_ID}-u${i}@test.local" - FN="journey-${RUN_ID}-${i}" - RESULT="PASS" - - AUTH=$(curl -s -w "\n%{http_code}" --max-time 45 -X POST "${BASE}/auth" -H 'Content-Type: application/json' -d "{\"token\":\"${U}\",\"env\":\"test\"}") - AUTH_CODE=$(printf '%s' "$AUTH" | tail -1) - [ "$AUTH_CODE" = "200" ] || RESULT="FAIL auth=${AUTH_CODE}" - - BAD=$(create_call "$U" '{"name":"bad-lang","language":"cobol","code":"x"}') - BAD_CODE=$(printf '%s' "$BAD" | tail -1) - [ "$BAD_CODE" = "400" ] || RESULT="FAIL bad_create=${BAD_CODE}" - - CODE_TEXT=$(printf 'def main():\n return "journey-ok-%s-%s"\n' "$RUN_ID" "$i") - CODE_JSON=$(printf '%s' "$CODE_TEXT" | json_escape) - GOOD=$(create_call "$U" "{\"name\":\"${FN}\",\"language\":\"python\",\"code\":${CODE_JSON}}") - GOOD_CODE=$(printf '%s' "$GOOD" | tail -1) - [ "$GOOD_CODE" = "201" ] || RESULT="FAIL create=${GOOD_CODE}" - - DUP=$(create_call "$U" "{\"name\":\"${FN}\",\"language\":\"python\",\"code\":${CODE_JSON}}") - DUP_CODE=$(printf '%s' "$DUP" | tail -1) - { [ "$DUP_CODE" = "409" ] || [ "$DUP_CODE" = "502" ]; } || RESULT="FAIL dup=${DUP_CODE}" - - INV_STATUS="" - INV_RAW="" - for try in 1 2 3 4 5 6; do - INV=$(invoke_call "$U" "$FN") - INV_STATUS=$(json_field "$INV" status) - INV_RAW=$(json_field "$INV" response_raw) - echo "USER=$i TRY=$try status=$INV_STATUS raw=$INV_RAW" - if [ "$INV_STATUS" = "200" ] && printf '%s' "$INV_RAW" | grep -q "journey-ok-${RUN_ID}-${i}"; then - break - fi - [ "$try" -lt 6 ] && sleep 5 - done - [ "$INV_STATUS" = "200" ] || RESULT="FAIL invoke=${INV_STATUS}" - - DEL1=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/${FN}" -H "X-Auth-Token: ${U}") - [ "$DEL1" = "200" ] || RESULT="FAIL delete1=${DEL1}" - DEL2=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/${FN}" -H "X-Auth-Token: ${U}") - [ "$DEL2" = "404" ] || RESULT="FAIL delete2=${DEL2}" - - echo "$i:$RESULT" > "${TMP}/user-$i" - ) & -done -wait - -PASS=0 -FAIL=0 -for i in $(seq 1 "$COUNT"); do - R=$(cat "${TMP}/user-$i" 2>/dev/null || echo "$i:FAIL missing_result") - STATUS=$(printf '%s' "$R" | cut -d: -f2-) - if [ "$STATUS" = "PASS" ]; then - PASS=$((PASS+1)) - echo "USER_PASS $i" - else - FAIL=$((FAIL+1)) - echo "USER_FAIL $i $STATUS" - fi -done - -echo "USER_JOURNEYS_PASS=$PASS/$COUNT" -echo "=== USER JOURNEYS END ===" -[ "$FAIL" = 0 ] \ No newline at end of file diff --git a/scripts/test59.sh b/scripts/test59.sh deleted file mode 100644 index 380828e..0000000 --- a/scripts/test59.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -BASE="https://fission.kube5s.ru/console/api" -ROOT_DIR=$(cd "$(dirname "$0")" && pwd) -NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" -TEST_SUB="len59direct@test.local" - -cleanup() { - if [ -x "$NS_CLEANUP_SCRIPT" ]; then - "$NS_CLEANUP_SCRIPT" "$TEST_SUB" >/dev/null 2>&1 || true - fi -} -trap cleanup EXIT - -NAME59=$(python3 -c "print('a'*59)") -echo "Name len: ${#NAME59}" -echo "Pkg name would be: ${NAME59}-pkg (len=$(python3 -c "print(59+4)"))" -curl -s -w "\nHTTP:%{http_code}" -X POST "${BASE}/functions" \ - -H "X-Auth-Token: ${TEST_SUB}" \ - -H "Content-Type: application/json" \ - --data-raw "{\"name\":\"${NAME59}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>42\"}" -echo diff --git a/scripts/test_auth.sh b/scripts/test_auth.sh new file mode 100755 index 0000000..3aea09c --- /dev/null +++ b/scripts/test_auth.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# test_auth.sh — тестирует аутентификацию: +# - POST /console/api/auth (login endpoint) +# - X-Auth-Token header (authMiddleware) +# - Authorization: Bearer header +# - DemoAuth: любая строка ≥6 символов +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" +RUN_ID=$(mk_run_id) +SUB="auth-test-${RUN_ID}@test.local" + +cleanup() { + if [ -x "$NS_CLEANUP_SCRIPT" ]; then + KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$SUB" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_auth.sh run=$RUN_ID" +echo "========================================" + +# ── POST /auth ────────────────────────────────────────────────────────────── + +echo "" +echo ">>> POST /auth: пустой токен → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \ + -H "Content-Type: application/json" -d '{"token":""}') +check_http "$(echo "$R" | tail -1)" "400" "POST /auth пустой token → 400" + +echo "" +echo ">>> POST /auth: токен < 6 символов → 401" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \ + -H "Content-Type: application/json" -d '{"token":"ab"}') +check_http "$(echo "$R" | tail -1)" "401" "POST /auth токен 2 символа → 401" + +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \ + -H "Content-Type: application/json" -d '{"token":"abcde"}') +check_http "$(echo "$R" | tail -1)" "401" "POST /auth токен 5 символов → 401" + +echo "" +echo ">>> POST /auth: токен ≥6 символов → 200" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \ + -H "Content-Type: application/json" -d "{\"token\":\"${SUB}\"}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "POST /auth валидный токен → 200" +NS=$(json_field "$BODY" namespace) +printf '%s' "$NS" | grep -q '^fission-' && pass "POST /auth namespace=fission-..." || fail "POST /auth namespace format: $NS" +OK=$(json_field "$BODY" ok) +[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "POST /auth ok=true" || fail "POST /auth ok=$OK" + +echo "" +echo ">>> POST /auth: невалидный JSON → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \ + -H "Content-Type: application/json" -d 'not-json') +CODE=$(echo "$R" | tail -1) +[ "$CODE" = "400" ] && pass "POST /auth невалидный JSON → 400" || fail "POST /auth невалидный JSON → $CODE" + +echo "" +echo ">>> POST /auth: GET метод → 405" +R=$(curl -s -w "\n%{http_code}" "${BASE}/auth") +CODE=$(echo "$R" | tail -1) +[ "$CODE" = "405" ] && pass "GET /auth → 405" || fail "GET /auth → $CODE" + +# ── X-Auth-Token header ────────────────────────────────────────────────────── + +echo "" +echo ">>> X-Auth-Token: отсутствует → 401" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions") +check_http "$(echo "$R" | tail -1)" "401" "GET /functions без токена → 401" + +echo "" +echo ">>> X-Auth-Token: токен < 6 символов → 401" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ab") +check_http "$(echo "$R" | tail -1)" "401" "GET /functions токен 2 символа → 401" + +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: abcde") +check_http "$(echo "$R" | tail -1)" "401" "GET /functions токен 5 символов → 401" + +echo "" +echo ">>> X-Auth-Token: токен ≥6 символов → 200" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ${SUB}") +check_http "$(echo "$R" | tail -1)" "200" "GET /functions валидный X-Auth-Token → 200" + +echo "" +echo ">>> Authorization: Bearer → 200" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" \ + -H "Authorization: Bearer ${SUB}") +check_http "$(echo "$R" | tail -1)" "200" "GET /functions Authorization: Bearer → 200" + +# ── Namespace детерминирован ───────────────────────────────────────────────── + +echo "" +echo ">>> Namespace детерминирован (одинаковый токен → одинаковый NS)" +R1=$(curl -s -X POST "${BASE}/auth" -H "Content-Type: application/json" -d "{\"token\":\"${SUB}\"}") +R2=$(curl -s -X POST "${BASE}/auth" -H "Content-Type: application/json" -d "{\"token\":\"${SUB}\"}") +NS1=$(json_field "$R1" namespace) +NS2=$(json_field "$R2" namespace) +[ "$NS1" = "$NS2" ] && [ -n "$NS1" ] && pass "Namespace детерминирован: $NS1" || fail "Namespace не детерминирован: '$NS1' vs '$NS2'" + +print_summary diff --git a/scripts/test_functions_crud.sh b/scripts/test_functions_crud.sh new file mode 100755 index 0000000..b561bfa --- /dev/null +++ b/scripts/test_functions_crud.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# test_functions_crud.sh — тестирует CRUD операции с функциями: +# POST /functions, GET /functions, GET /functions/{name}, +# DELETE /functions/{name}, валидация имён, дубликаты, размер кода. +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" +RUN_ID=$(mk_run_id) +SUB="crud-${RUN_ID}@test.local" + +cleanup() { + if [ -x "$NS_CLEANUP_SCRIPT" ]; then + KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$SUB" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_functions_crud.sh run=$RUN_ID" +echo "========================================" + +JS_CODE='module.exports = async function(ctx) { return { status: 200, body: "crud-ok" }; }' + +# ── CREATE ─────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE: базовый nodejs → 201" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"crud-fn-${RUN_ID}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "201" "CREATE nodejs → 201" +FN_NAME=$(json_field "$BODY" name) +FN_ROUTE=$(json_field "$BODY" route) +FN_PKG=$(json_field "$BODY" package) +[ "$FN_NAME" = "crud-fn-${RUN_ID}" ] && pass "CREATE: name в ответе" || fail "CREATE: name='$FN_NAME'" +printf '%s' "$FN_ROUTE" | grep -q '/' && pass "CREATE: route непустой" || fail "CREATE: route='$FN_ROUTE'" +[ -n "$FN_PKG" ] && pass "CREATE: package непустой" || fail "CREATE: package пустой" + +echo "" +echo ">>> CREATE: дубликат → 409" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"crud-fn-${RUN_ID}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "409" "CREATE дубликат → 409" + +echo "" +echo ">>> CREATE: без name → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"language":"nodejs","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE без name → 400" + +echo "" +echo ">>> CREATE: без language → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"no-lang","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE без language → 400" + +echo "" +echo ">>> CREATE: без code → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"no-code","language":"nodejs"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE без code → 400" + +echo "" +echo ">>> CREATE: невалидный язык → 400" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"bad-lang","language":"cobol","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE язык=cobol → 400" + +echo "" +echo ">>> CREATE: валидация имени" + +# Заглавная буква +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"BadName","language":"nodejs","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с заглавной → 400" + +# Начинается с дефиса +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"-bad","language":"nodejs","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с ведущим дефисом → 400" + +# Заканчивается дефисом +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"bad-","language":"nodejs","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с завершающим дефисом → 400" + +# Пробел в имени +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"name":"bad name","language":"nodejs","code":"x"}') +check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с пробелом → 400" + +# 58 символов — должно быть 400 (лимит 57) +NAME58=$(python3 -c "print('a'*58)") +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${NAME58}\",\"language\":\"nodejs\",\"code\":\"x\"}") +check_http "$(echo "$R" | tail -1)" "400" "CREATE имя 58 символов → 400" + +# 57 символов — должно быть 201 +NAME57=$(python3 -c "print('a'*57)") +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${NAME57}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "201" "CREATE имя 57 символов → 201" + +echo "" +echo ">>> CREATE: код > 1MB → 400 или 413" +# Используем pipe чтобы избежать Argument list too long +R=$(python3 -c " +import json, sys +payload = json.dumps({'name':'big-code','language':'nodejs','code':'x'*(1024*1024+1)}) +print(payload) +" | curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + --data-binary @- --max-time 30) +CODE=$(echo "$R" | tail -1) +( [ "$CODE" = "400" ] || [ "$CODE" = "413" ] || [ "$CODE" = "000" ] ) \ + && pass "CREATE код > 1MB → отклонён ($CODE)" \ + || fail "CREATE код > 1MB → got $CODE" + +# ── LIST ───────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> LIST: GET /functions → 200, массив" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ${SUB}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "LIST GET /functions → 200" +COUNT=$(json_len "$BODY") +[ "${COUNT:-0}" -ge 1 ] && pass "LIST: содержит ≥1 функцию (создали выше)" || fail "LIST: пустой массив count=$COUNT" + +echo "" +echo ">>> LIST: новый пользователь → пустой массив" +EMPTY_USER="empty-${RUN_ID}@test.local" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ${EMPTY_USER}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "LIST новый пользователь → 200" +COUNT=$(json_len "$BODY") +[ "${COUNT:-99}" -eq 0 ] && pass "LIST новый пользователь → []" || fail "LIST новый пользователь → count=$COUNT" + +# ── GET ────────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> GET: /functions/{name} → 200" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/crud-fn-${RUN_ID}" \ + -H "X-Auth-Token: ${SUB}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "GET /functions/crud-fn-${RUN_ID} → 200" +GOT_NAME=$(json_field "$BODY" name) +[ "$GOT_NAME" = "crud-fn-${RUN_ID}" ] && pass "GET: name совпадает" || fail "GET: name='$GOT_NAME'" +GOT_ENV=$(json_field "$BODY" environment) +[ -n "$GOT_ENV" ] && pass "GET: environment непустой ($GOT_ENV)" || fail "GET: environment пустой" + +echo "" +echo ">>> GET: несуществующая функция → 404" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/does-not-exist-xyz" \ + -H "X-Auth-Token: ${SUB}") +check_http "$(echo "$R" | tail -1)" "404" "GET несуществующая → 404" + +# ── DELETE ─────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> DELETE: /functions/{name} → 200" +R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/crud-fn-${RUN_ID}" \ + -H "X-Auth-Token: ${SUB}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "DELETE crud-fn → 200" +DELETED=$(json_field "$BODY" deleted) +[ "$DELETED" = "True" ] || [ "$DELETED" = "true" ] && pass "DELETE: deleted=true" || fail "DELETE: deleted='$DELETED'" + +echo "" +echo ">>> DELETE: повторное удаление → 404" +R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/crud-fn-${RUN_ID}" \ + -H "X-Auth-Token: ${SUB}") +check_http "$(echo "$R" | tail -1)" "404" "DELETE повторное → 404" + +echo "" +echo ">>> GET: после DELETE → 404" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/crud-fn-${RUN_ID}" \ + -H "X-Auth-Token: ${SUB}") +check_http "$(echo "$R" | tail -1)" "404" "GET после DELETE → 404" + +echo "" +echo ">>> DELETE: несуществующая функция → 404" +R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/does-not-exist-xyz" \ + -H "X-Auth-Token: ${SUB}") +check_http "$(echo "$R" | tail -1)" "404" "DELETE несуществующая → 404" + +print_summary diff --git a/scripts/test_invoke.sh b/scripts/test_invoke.sh new file mode 100755 index 0000000..95b1462 --- /dev/null +++ b/scripts/test_invoke.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# test_invoke.sh — тестирует вызов функций: +# NodeJS и Python: create → wait cold start → invoke → проверка тела. +# Invoke несуществующей функции → 404. +# ВАЖНО: использует livetest@test.local — прогретый namespace в kube кластере. +# Для новых namespace fission router требует перезапуска (watchedNamespaces). +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +RUN_ID=$(mk_run_id) +# Используем прогретый namespace — fission router его знает. +SUB="livetest@test.local" +FN_NODE="node-fn-${RUN_ID}" +FN_PY="py-fn-${RUN_ID}" + +cleanup() { + # Удаляем только конкретные функции через API, не весь namespace. + for fn in "$FN_NODE" "$FN_PY"; do + curl -s -X DELETE "${BASE}/functions/${fn}" -H "X-Auth-Token: ${SUB}" > /dev/null 2>&1 || true + done +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_invoke.sh run=$RUN_ID" +echo "========================================" + +# ── Invoke несуществующей ───────────────────────────────────────────────── + +echo "" +echo ">>> INVOKE несуществующей функции → 404" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions/no-such-fn-xyz-${RUN_ID}/invoke" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') +check_http "$(echo "$R" | tail -1)" "404" "INVOKE несуществующей → 404" + +# ── NodeJS ──────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> NodeJS: create + invoke" +JS_CODE='module.exports = async function(ctx) { return { status: 200, body: "node-invoke-ok" }; }' +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN_NODE}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "201" "NodeJS CREATE → 201" + +if wait_invoke "$SUB" "$FN_NODE" "node-invoke-ok" 12 5; then + pass "NodeJS invoke → 200, тело содержит node-invoke-ok" +else + fail "NodeJS invoke не стабилизировался за 60s" +fi + +# Второй вызов — проверяем что работает warm path +R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_NODE}/invoke" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') +STATUS=$(json_field "$R" status) +[ "$STATUS" = "200" ] && pass "NodeJS второй invoke → 200 (warm)" || fail "NodeJS второй invoke → status=$STATUS" + +# ── Python ─────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> Python: create + invoke" +PY_CODE=$(printf 'def main():\n return "py-invoke-ok", 200' | json_escape) +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN_PY}\",\"language\":\"python\",\"code\":${PY_CODE}}") +check_http "$(echo "$R" | tail -1)" "201" "Python CREATE → 201" + +if wait_invoke "$SUB" "$FN_PY" "py-invoke-ok" 15 5; then + pass "Python invoke → 200, тело содержит py-invoke-ok" +else + fail "Python invoke не стабилизировался за 75s" +fi + +# ── Invoke возвращает корректные поля ───────────────────────────────────────── + +echo "" +echo ">>> Invoke response fields" +R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_NODE}/invoke" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') +STATUS=$(json_field "$R" status) +LATENCY=$(json_field "$R" latency_ms) +RAW=$(json_field "$R" response_raw) +[ "$STATUS" = "200" ] && pass "invoke.status = 200" || fail "invoke.status = $STATUS" +printf '%s' "$LATENCY" | grep -qE '^[0-9]+$' && pass "invoke.latency_ms = $LATENCY ms" || fail "invoke.latency_ms не число: $LATENCY" +[ -n "$RAW" ] && pass "invoke.response_raw непустой" || fail "invoke.response_raw пустой" + +print_summary diff --git a/scripts/test_isolation.sh b/scripts/test_isolation.sh new file mode 100755 index 0000000..202fe23 --- /dev/null +++ b/scripts/test_isolation.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# test_isolation.sh — тестирует namespace изоляцию: +# Пользователь A не видит и не может изменить функции пользователя B. +# LIST каждого пользователя возвращает только его функции. +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" +RUN_ID=$(mk_run_id) +ALICE="alice-${RUN_ID}@test.local" +BOB="bob-${RUN_ID}@test.local" + +cleanup() { + if [ -x "$NS_CLEANUP_SCRIPT" ]; then + KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$ALICE" "$BOB" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_isolation.sh run=$RUN_ID" +echo "========================================" + +JS_CODE='module.exports = async function() { return { status: 200, body: "ok" }; }' + +# ── Alice создаёт функцию ───────────────────────────────────────────────────── + +echo "" +echo ">>> Alice создаёт функцию" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${ALICE}" -H "Content-Type: application/json" \ + -d "{\"name\":\"alice-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "201" "Alice CREATE alice-fn → 201" + +# ── Bob не может получить функцию Alice ────────────────────────────────────── + +echo "" +echo ">>> Bob GET alice-fn → 404" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/alice-fn" \ + -H "X-Auth-Token: ${BOB}") +check_http "$(echo "$R" | tail -1)" "404" "Bob GET alice-fn → 404" + +echo "" +echo ">>> Bob DELETE alice-fn → 404" +R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/alice-fn" \ + -H "X-Auth-Token: ${BOB}") +check_http "$(echo "$R" | tail -1)" "404" "Bob DELETE alice-fn → 404" + +echo "" +echo ">>> Bob PUT alice-fn/code → 404" +R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/alice-fn/code" \ + -H "X-Auth-Token: ${BOB}" -H "Content-Type: application/json" \ + -d '{"code":"module.exports = async () => ({ body: \"hacked\" });"}') +check_http "$(echo "$R" | tail -1)" "404" "Bob PUT alice-fn/code → 404" + +echo "" +echo ">>> Bob INVOKE alice-fn → 404" +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions/alice-fn/invoke" \ + -H "X-Auth-Token: ${BOB}" -H "Content-Type: application/json" -d '{}') +check_http "$(echo "$R" | tail -1)" "404" "Bob INVOKE alice-fn → 404" + +# ── Alice убеждается что её функция цела ──────────────────────────────────── + +echo "" +echo ">>> Alice GET alice-fn → 200 (не повреждена)" +R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/alice-fn" \ + -H "X-Auth-Token: ${ALICE}") +check_http "$(echo "$R" | tail -1)" "200" "Alice GET alice-fn → 200" + +# ── LIST изоляция ──────────────────────────────────────────────────────────── + +echo "" +echo ">>> LIST изоляция: Alice и Bob видят только свои функции" + +# Bob создаёт свою функцию +curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${BOB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"bob-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}" > /dev/null + +sleep 1 + +ALICE_LIST=$(curl -s "${BASE}/functions" -H "X-Auth-Token: ${ALICE}") +BOB_LIST=$(curl -s "${BASE}/functions" -H "X-Auth-Token: ${BOB}") + +# Alice видит alice-fn но не bob-fn +ALICE_COUNT=$(json_len "$ALICE_LIST") +ALICE_HAS_OWN=$(printf '%s' "$ALICE_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 'alice-fn')) +except: + print(0)" 2>/dev/null) +ALICE_HAS_BOB=$(printf '%s' "$ALICE_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 'bob-fn')) +except: + print(0)" 2>/dev/null) + +[ "${ALICE_HAS_OWN:-0}" -ge 1 ] && pass "Alice LIST содержит alice-fn" || fail "Alice LIST не содержит alice-fn (count=$ALICE_COUNT)" +[ "${ALICE_HAS_BOB:-0}" -eq 0 ] && pass "Alice LIST не содержит bob-fn" || fail "Alice LIST содержит bob-fn — УТЕЧКА ДАННЫХ!" + +# Bob видит bob-fn но не alice-fn +BOB_HAS_OWN=$(printf '%s' "$BOB_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 'bob-fn')) +except: + print(0)" 2>/dev/null) +BOB_HAS_ALICE=$(printf '%s' "$BOB_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 'alice-fn')) +except: + print(0)" 2>/dev/null) + +[ "${BOB_HAS_OWN:-0}" -ge 1 ] && pass "Bob LIST содержит bob-fn" || fail "Bob LIST не содержит bob-fn" +[ "${BOB_HAS_ALICE:-0}" -eq 0 ] && pass "Bob LIST не содержит alice-fn" || fail "Bob LIST содержит alice-fn — УТЕЧКА ДАННЫХ!" + +# ── Route изоляция ─────────────────────────────────────────────────────────── + +echo "" +echo ">>> Route изоляция: одинаковое имя → разные routes" +ROUTE_A=$(printf '%s' "$ALICE_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + for f in fns: + if f.get('metadata',{}).get('name','') == 'alice-fn': + print(f.get('spec',{}).get('relativeurl','')) + break +except: + print('')" 2>/dev/null) + +ROUTE_B=$(printf '%s' "$BOB_LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + for f in fns: + if f.get('metadata',{}).get('name','') == 'bob-fn': + print(f.get('spec',{}).get('relativeurl','')) + break +except: + print('')" 2>/dev/null) + +# Проверяем через GET (возвращает route) +ALICE_GET=$(curl -s "${BASE}/functions/alice-fn" -H "X-Auth-Token: ${ALICE}") +BOB_GET=$(curl -s "${BASE}/functions/bob-fn" -H "X-Auth-Token: ${BOB}") +ROUTE_A=$(json_field "$ALICE_GET" route) +ROUTE_B=$(json_field "$BOB_GET" route) +[ "$ROUTE_A" != "$ROUTE_B" ] && [ -n "$ROUTE_A" ] && [ -n "$ROUTE_B" ] \ + && pass "Route изоляция: $ROUTE_A ≠ $ROUTE_B" \ + || fail "Route коллизия: alice=$ROUTE_A bob=$ROUTE_B" + +print_summary diff --git a/scripts/test_layer1.sh b/scripts/test_layer1.sh deleted file mode 100644 index 12606c8..0000000 --- a/scripts/test_layer1.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env bash -# test_layer1.sh — простой тест пропатченного Fission NSWatcher (Layer 1 only). -# Без консоли. Только kubectl + fission CLI. -# -# Что проверяем: -# 1. Создаём NS с меткой fission.io/managed=true -# 2. Executor регистрирует его через NSWatcher (лог) -# 3. Создаём Python env + function в новом NS -# 4. Вызываем функцию — должна ответить 200 -# -# Запуск: bash ~/terra/fission/test_layer1.sh - -set -uo pipefail - -NS="l1-test-$(date +%s | tail -c 6)" -PASS=0; FAIL=0 - -green() { echo -e "\033[32m PASS: $1\033[0m"; PASS=$((PASS+1)); } -red() { echo -e "\033[31m FAIL: $1\033[0m"; FAIL=$((FAIL+1)); } - -echo "" -echo "========================================" -echo " Layer 1 NSWatcher — простой тест" -echo " NS: $NS" -echo "========================================" - -# ── 1. Создаём NS с меткой ──────────────────────────────────────────────────── -echo "" -echo ">>> [1/5] Создаём NS $NS с меткой fission.io/managed=true..." -kubectl create ns "$NS" -kubectl label ns "$NS" fission.io/managed=true --overwrite - -kubectl get ns "$NS" --show-labels | grep "fission.io/managed" -if [ $? -eq 0 ]; then - green "NS создан с меткой" -else - red "Метка не применилась" - exit 1 -fi - -# ── 2. Ждём регистрации в executor ──────────────────────────────────────────── -echo "" -echo ">>> [2/5] Ждём регистрации NS в executor (до 15 сек)..." - -DEADLINE=$((SECONDS + 15)) -REGISTERED=false -while [ $SECONDS -lt $DEADLINE ]; do - if kubectl logs -n fission deployment/executor 2>/dev/null | grep "registered namespace" | grep -q "$NS"; then - REGISTERED=true - break - fi - sleep 2 -done - -if [ "$REGISTERED" = "true" ]; then - green "Executor зарегистрировал NS $NS" -else - red "Executor НЕ зарегистрировал NS за 15 сек" - echo " Лог executor (последние строки про namespace):" - kubectl logs -n fission deployment/executor 2>/dev/null | grep "namespace" | tail -5 -fi - -# ── 3. Создаём environment в новом NS ───────────────────────────────────────── -echo "" -echo ">>> [3/5] Создаём Python environment в NS $NS..." - -fission env create \ - --name py-env \ - --namespace "$NS" \ - --image ghcr.io/fission/python-env:latest \ - --poolsize 1 2>&1 - -if kubectl get environment py-env -n "$NS" &>/dev/null; then - green "Environment py-env создан в $NS" -else - red "Environment не создался" -fi - -# ── 4. Создаём функцию ──────────────────────────────────────────────────────── -echo "" -echo ">>> [4/5] Создаём функцию hello..." - -cat > /tmp/hello.py << 'EOF' -def main(): - return "hello from layer1" -EOF - -fission fn create \ - --name hello \ - --namespace "$NS" \ - --env py-env \ - --code /tmp/hello.py 2>&1 - -fission httptrigger create \ - --name hello-route \ - --namespace "$NS" \ - --function hello \ - --url "/${NS}/hello" \ - --method GET 2>&1 - -if kubectl get httptrigger hello-route -n "$NS" &>/dev/null; then - green "Function + HTTPTrigger созданы" -else - red "HTTPTrigger не создался" -fi - -# ── 5. Вызываем функцию через внутренний router с JWT ──────────────────────── -echo "" -echo ">>> [5/5] Вызываем функцию через internal router + JWT (cold start до 180 сек)..." - -ROUTER_INT="http://router.fission.svc.cluster.local" -DEADLINE=$((SECONDS + 180)) -FN_OK=false - -while [ $SECONDS -lt $DEADLINE ]; do - RESULT=$(kubectl run fn-probe-$$ --rm -i --restart=Never \ - --image=curlimages/curl:8.7.1 \ - --namespace=fission \ - -- sh -c " - TOKEN=\$(curl -sf --max-time 15 -X POST ${ROUTER_INT}/auth/login \ - -H 'Content-Type: application/json' \ - -d '{\"username\":\"admin\",\"password\":\"7XG1lSg0EFqPLE4pf3He\"}' \ - | grep -o '\"accesstoken\":\"[^\"]*' | cut -d'\"' -f4) - if [ -z \"\$TOKEN\" ]; then echo 'LOGIN_FAIL|||000'; exit 0; fi - curl -s -w '|||%{http_code}' --max-time 90 \ - -H \"Authorization: Bearer \$TOKEN\" \ - ${ROUTER_INT}/${NS}/hello - " 2>/dev/null | grep "|||" || echo "|||000") - CODE=$(echo "$RESULT" | grep -o '|||[0-9]*' | tr -d '|||') - BODY=$(echo "$RESULT" | sed 's/|||[0-9]*$//') - echo " HTTP $CODE — $(echo "$BODY" | head -c 80)" - [ "$CODE" = "200" ] && FN_OK=true && break - sleep 5 -done - -if [ "$FN_OK" = "true" ]; then - green "Функция ответила 200: $BODY" -else - red "Функция не ответила 200 (timeout)" - echo " Логи router (last 3):" - kubectl logs -n fission deployment/router 2>/dev/null | tail -3 -fi - -# ── Cleanup ─────────────────────────────────────────────────────────────────── -echo "" -echo ">>> Cleanup..." -fission httptrigger delete --name hello-route --namespace "$NS" 2>/dev/null || true -fission fn delete --name hello --namespace "$NS" 2>/dev/null || true -fission env delete --name py-env --namespace "$NS" 2>/dev/null || true -kubectl delete ns "$NS" --wait=false 2>/dev/null || true -echo " Готово." - -# ── Итог ───────────────────────────────────────────────────────────────────── -echo "" -echo "========================================" -echo " ИТОГ: PASS=$PASS FAIL=$FAIL" -echo "========================================" -[ $FAIL -eq 0 ] && exit 0 || exit 1 diff --git a/scripts/test_linters.sh b/scripts/test_linters.sh old mode 100644 new mode 100755 diff --git a/scripts/test_multitenant_ns.sh b/scripts/test_multitenant_ns.sh deleted file mode 100755 index 0dc5c1e..0000000 --- a/scripts/test_multitenant_ns.sh +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env bash -# test_multitenant_ns.sh — стресс-тест Layer 1 (патченный Fission NSWatcher). -# -# Что проверяем: -# 1. N пользователей создаются параллельно (каждый → свой NS через консоль API) -# 2. Executor регистрирует каждый NS через NSWatcher (лог: registered namespace) -# 3. В каждом NS создаётся функция через консоль API -# 4. Каждая функция вызывается и отвечает правильно -# 5. Параллельные вызовы всех функций одновременно — нет деградации -# -# Запуск: bash test_multitenant_ns.sh [COUNT] -# COUNT — кол-во параллельных пользователей (default: 10) - -set -uo pipefail - -if [ "${KUBECTL_DIRECT:-0}" = "1" ]; then - EXECUTOR_LOGS_CMD() { - kubectl logs -n fission deployment/executor 2>/dev/null - } -else - SSH="ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213" - EXECUTOR_LOGS_CMD() { - $SSH "kubectl logs -n fission deployment/executor 2>/dev/null" - } -fi - -BASE="https://fission.kube5s.ru/console/api" -COUNT=${1:-10} -RUN_ID="mt$(date +%s | tail -c 6)" -ROOT_DIR=$(cd "$(dirname "$0")" && pwd) -NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" -PASS=0; FAIL=0 - -declare -a CREATED_USERS=() - -cleanup_namespaces() { - rm -rf "${TMPDIR_RESULTS:-}" 2>/dev/null || true - if [ -x "$NS_CLEANUP_SCRIPT" ] && [ "${#CREATED_USERS[@]}" -gt 0 ]; then - echo "" - echo ">>> Cleanup test namespaces..." - "$NS_CLEANUP_SCRIPT" "${CREATED_USERS[@]}" || true - fi -} -trap cleanup_namespaces EXIT - -green() { echo -e "\033[32m PASS: $1\033[0m"; PASS=$((PASS+1)); } -red() { echo -e "\033[31m FAIL: $1\033[0m"; FAIL=$((FAIL+1)); } - -echo "" -echo "========================================" -echo " Layer 1 NSWatcher — стресс-тест" -echo " Пользователей: $COUNT run: $RUN_ID" -echo "========================================" - -# Каждый "пользователь" — уникальный X-Auth-Token → уникальный NS -# Консоль вызывает SetupFissionNamespace → NS получает fission.io/managed=true -# Executor NSWatcher подхватывает → регистрирует без рестарта - -# ── Шаг 1: создаём NS для всех пользователей параллельно ───────────────────── -echo "" -echo ">>> [1/4] Создаём NS для $COUNT пользователей параллельно..." - -TMPDIR_RESULTS=$(mktemp -d) - -for i in $(seq 1 $COUNT); do - USER="${RUN_ID}-user${i}@test.local" - CREATED_USERS+=("$USER") - ( - CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 \ - -X GET "${BASE}/functions" \ - -H "X-Auth-Token: ${USER}" 2>/dev/null || echo 000) - echo "$i:$CODE:$USER" > "${TMPDIR_RESULTS}/init-${i}" - ) & -done -wait - -NS_OK=0; NS_FAIL=0 -declare -A USERS -for i in $(seq 1 $COUNT); do - R=$(cat "${TMPDIR_RESULTS}/init-${i}" 2>/dev/null || echo "$i:000:") - CODE=$(echo "$R" | cut -d: -f2) - USER=$(echo "$R" | cut -d: -f3) - USERS[$i]="$USER" - if [ "$CODE" = "200" ]; then - NS_OK=$((NS_OK+1)) - else - NS_FAIL=$((NS_FAIL+1)) - echo " user${i}: HTTP $CODE" - fi -done - -if [ $NS_FAIL -eq 0 ]; then - green "Все $COUNT NS созданы через консоль API (HTTP 200)" -else - red "$NS_FAIL / $COUNT NS не создались" -fi - -# ── Шаг 2: проверяем что executor зарегистрировал NS ───────────────────────── -echo "" -echo ">>> [2/4] Проверяем регистрацию NS в executor..." - -# Ждём немного — informer почти мгновенный, но логи могут задержаться -sleep 5 - -EXEC_LOGS=$(EXECUTOR_LOGS_CMD | grep 'registered namespace' 2>/dev/null || echo "") -REGISTERED=0 -for i in $(seq 1 $COUNT); do - USER="${USERS[$i]:-}" - # NS имя формируется консолью как hash от email - # Ищем любые новые registered namespace в логах после старта теста - REGISTERED=$(EXECUTOR_LOGS_CMD | grep 'registered namespace' | wc -l 2>/dev/null || echo 0) -done - -echo " Всего registered namespace событий в executor: $REGISTERED" -if [ "$REGISTERED" -ge "$COUNT" ]; then - green "Executor зарегистрировал минимум $COUNT NS" -else - # Не обязательно FAIL — NS могли быть уже зарегистрированы ранее - echo " INFO: зарегистрировано $REGISTERED (некоторые NS могли существовать до теста)" - PASS=$((PASS+1)) -fi - -# ── Шаг 3: создаём Python функцию для каждого пользователя ─────────────────── -echo "" -echo ">>> [3/4] Создаём функции для каждого пользователя параллельно..." - -# Простой Python код — не требует компиляции, деплоится мгновенно -PYTHON_CODE='def main(): - return "ok-'"${RUN_ID}"'"' - -json_escape() { - python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' -} - -for i in $(seq 1 $COUNT); do - USER="${USERS[$i]:-${RUN_ID}-user${i}@test.local}" - FN_NAME="testfn-${RUN_ID}" - ( - # Создаём функцию - CREATE=$(curl -s -w "\n%{http_code}" --max-time 60 \ - -X POST "${BASE}/functions" \ - -H "X-Auth-Token: ${USER}" \ - -H "Content-Type: application/json" \ - -d "{\"name\":\"${FN_NAME}\",\"language\":\"python\",\"code\":$(printf '%s' \"${PYTHON_CODE}\" | json_escape)}" \ - 2>/dev/null) - CODE=$(echo "$CREATE" | tail -1) - echo "$i:$CODE" > "${TMPDIR_RESULTS}/create-${i}" - ) & -done -wait - -CREATE_OK=0; CREATE_FAIL=0 -for i in $(seq 1 $COUNT); do - R=$(cat "${TMPDIR_RESULTS}/create-${i}" 2>/dev/null || echo "$i:000") - CODE=$(echo "$R" | cut -d: -f2) - if [ "$CODE" = "200" ] || [ "$CODE" = "201" ]; then - CREATE_OK=$((CREATE_OK+1)) - else - CREATE_FAIL=$((CREATE_FAIL+1)) - echo " user${i}: create HTTP $CODE" - fi -done - -if [ $CREATE_FAIL -eq 0 ]; then - green "Все $COUNT функции созданы" -else - red "$CREATE_FAIL / $COUNT функций не создались" -fi - -# ── Шаг 4: вызываем все функции параллельно ────────────────────────────────── -echo "" -echo ">>> [4/4] Параллельные вызовы всех $COUNT функций..." - -# Ждём прогрева (первый вызов — cold start) -sleep 5 - -for i in $(seq 1 $COUNT); do - USER="${USERS[$i]:-${RUN_ID}-user${i}@test.local}" - FN_NAME="testfn-${RUN_ID}" - ( - CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 \ - -X POST "${BASE}/functions/${FN_NAME}/invoke" \ - -H "X-Auth-Token: ${USER}" \ - -H "Content-Type: application/json" \ - -d '{}' 2>/dev/null || echo 000) - echo "$i:$CODE" > "${TMPDIR_RESULTS}/invoke-${i}" - ) & -done -wait - -INVOKE_OK=0; INVOKE_FAIL=0 -for i in $(seq 1 $COUNT); do - R=$(cat "${TMPDIR_RESULTS}/invoke-${i}" 2>/dev/null || echo "$i:000") - CODE=$(echo "$R" | cut -d: -f2) - if [ "$CODE" = "200" ]; then - INVOKE_OK=$((INVOKE_OK+1)) - else - INVOKE_FAIL=$((INVOKE_FAIL+1)) - echo " user${i}: invoke HTTP $CODE" - fi -done - -if [ $INVOKE_FAIL -eq 0 ]; then - green "Все $COUNT функции отвечают HTTP 200 параллельно" -else - red "$INVOKE_FAIL / $COUNT функций не ответили (возможно cold start — повторить тест)" -fi - -# ── Итог ───────────────────────────────────────────────────────────────────── -echo "" -echo "========================================" -echo " ИТОГ: PASS=$PASS FAIL=$FAIL" -echo "========================================" -[ $FAIL -eq 0 ] && exit 0 || exit 1 diff --git a/scripts/test_simple_function.sh b/scripts/test_simple_function.sh deleted file mode 100755 index 8e51ec4..0000000 --- a/scripts/test_simple_function.sh +++ /dev/null @@ -1,812 +0,0 @@ -#!/usr/bin/env bash -# test_simple_function.sh — интеграционные тесты fission_simple_function -# Запуск: bash test_simple_function.sh -# Требования: terraform + kubectl в PATH, kubeconfig в ~/.kube/config -set -uo pipefail - -PASS=0; FAIL=0; SKIP=0 -NAMESPACE="default" -TESTDIR="/tmp/sless-sf-tests" -KUBECONFIG_PATH="/home/naeel/.kube/config" -PROVIDER_NS="default" - -# уникальный префикс — не пересекаемся с prod -RUN_ID="sf$(date +%s | tail -c 6)" - -TF_PROVIDER=' -terraform { - required_providers { - fission = { source = "nail/fission" } - } -} -provider "fission" { - kubeconfig_path = "/home/naeel/.kube/config" - namespace = "default" -} -' - -green() { echo -e "\033[32m$*\033[0m"; } -red() { echo -e "\033[31m$*\033[0m"; } -yellow(){ echo -e "\033[33m$*\033[0m"; } - -pass() { PASS=$((PASS+1)); green " PASS: $1"; } -fail() { FAIL=$((FAIL+1)); red " FAIL: $1"; } -skip() { SKIP=$((SKIP+1)); yellow " SKIP: $1"; } - -# ── helpers ────────────────────────────────────────────────────────────────── - -kube_exists() { - local kind=$1 name=$2 - kubectl get "${kind}" -n "${NAMESPACE}" "${name}" &>/dev/null -} - -kube_gone() { - local kind=$1 name=$2 - ! kubectl get "${kind}" -n "${NAMESPACE}" "${name}" &>/dev/null -} - -tf_apply() { - local dir=$1 - terraform -chdir="${dir}" apply -auto-approve -no-color 2>&1 -} - -tf_destroy() { - local dir=$1 - terraform -chdir="${dir}" destroy -auto-approve -no-color 2>&1 -} - -tf_plan_exitcode() { - # 0=no-changes, 1=error, 2=changes - local dir=$1 - terraform -chdir="${dir}" plan -detailed-exitcode -no-color 2>&1 - echo $? -} - -wait_package_ready() { - local name=$1 retries=30 - for ((i=0; i/dev/null || echo "") - # для literal packages buildstatus пустой — это нормально - [[ "$status" == "succeeded" || "$status" == "" ]] && return 0 - sleep 2 - done - return 1 -} - -# ── setup / teardown ───────────────────────────────────────────────────────── - -setup_workdir() { - local name=$1 - local dir="${TESTDIR}/${name}" - rm -rf "${dir}" - mkdir -p "${dir}" - echo "${dir}" -} - -write_provider() { - local dir=$1 - cat > "${dir}/main.tf" < "${dir}/main.js" <<'EOF' -exports.handler = async (ctx) => ({ - body: JSON.stringify({ msg: "hello-v1", ts: Date.now() }), - headers: { "Content-Type": "application/json" }, -}); -EOF -} - -write_js_v2() { - local dir=$1 - mkdir -p "${dir}" - cat > "${dir}/main.js" <<'EOF' -exports.handler = async (ctx) => ({ - body: JSON.stringify({ msg: "hello-v2-updated", ts: Date.now() }), - headers: { "Content-Type": "application/json" }, -}); -EOF -} - -write_python() { - local dir=$1 - mkdir -p "${dir}" - cat > "${dir}/main.py" <<'EOF' -def main(): - return {"status": 200, "body": '{"msg":"hello-python"}'} -EOF -} - -write_ruby() { - local dir=$1 - mkdir -p "${dir}" - cat > "${dir}/main.rb" <<'EOF' -def main(ctx) - { body: "hello-ruby", status_code: 200 } -end -EOF -} - -write_php() { - local dir=$1 - mkdir -p "${dir}" - cat > "${dir}/main.php" <<'EOF' - "hello-php"]; -} -EOF -} - -write_perl() { - local dir=$1 - mkdir -p "${dir}" - cat > "${dir}/main.pl" <<'EOF' -sub main { - my ($context) = @_; - return { body => "hello-perl" }; -} -1; -EOF -} - -EOF - cat > "${dir}/go.mod" <<'EOF' -module fn - -go 1.21 -EOF -} - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo "================================================================" -echo " fission_simple_function — интеграционные тесты" -echo " Run ID: ${RUN_ID}" -echo "================================================================" -mkdir -p "${TESTDIR}" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T01: nodejs — create с HTTPTrigger, verify k8s, destroy" -# ════════════════════════════════════════════════════════════════════════ -T01="${RUN_ID}-node-basic" -D=$(setup_workdir "t01") -write_provider "${D}" -write_js_v1 "${D}/code" -cat >> "${D}/main.tf" <>> T02: python — create БЕЗ url (нет HTTPTrigger)" -# ════════════════════════════════════════════════════════════════════════ -T02="${RUN_ID}-py-notrigger" -D=$(setup_workdir "t02") -write_provider "${D}" -write_python "${D}/code" -cat >> "${D}/main.tf" <>> T04: ruby — create с URL" -# ════════════════════════════════════════════════════════════════════════ -T04="${RUN_ID}-ruby" -D=$(setup_workdir "t04") -write_provider "${D}" -write_ruby "${D}/code" -cat >> "${D}/main.tf" <>> T05: php — create с URL" -# ════════════════════════════════════════════════════════════════════════ -T05="${RUN_ID}-php" -D=$(setup_workdir "t05") -write_provider "${D}" -write_php "${D}/code" -cat >> "${D}/main.tf" <>> T06: perl — create с URL" -# ════════════════════════════════════════════════════════════════════════ -T06="${RUN_ID}-perl" -D=$(setup_workdir "t06") -write_provider "${D}" -write_perl "${D}/code" -cat >> "${D}/main.tf" <>> T07: code_hash drift detection — update пересчитывает хеш и обновляет Package" -# ════════════════════════════════════════════════════════════════════════ -T07="${RUN_ID}-drift" -D=$(setup_workdir "t07") -write_provider "${D}" -write_js_v1 "${D}/code" -cat >> "${D}/main.tf" </dev/null \ - | python3 -c "import sys,json; r=json.load(sys.stdin)['values']['root_module']['resources']; print(r[0]['values']['code_hash'])" 2>/dev/null || echo "?") - -# Обновляем код (v2) -write_js_v2 "${D}/code" - -OUT=$(tf_apply "${D}") -echo "${OUT}" | grep -q "Apply complete" && pass "T07: apply v2 OK" || fail "T07: apply v2 FAILED" - -HASH2=$(terraform -chdir="${D}" show -json 2>/dev/null \ - | python3 -c "import sys,json; r=json.load(sys.stdin)['values']['root_module']['resources']; print(r[0]['values']['code_hash'])" 2>/dev/null || echo "?") - -[ "${HASH1}" != "${HASH2}" ] && [ "${HASH1}" != "?" ] && [ "${HASH2}" != "?" ] \ - && pass "T07: code_hash изменился (${HASH1:0:8}… → ${HASH2:0:8}…)" \ - || fail "T07: code_hash не изменился или не читается (h1=${HASH1:0:8}, h2=${HASH2:0:8})" - -echo "${OUT}" | grep -qE "fission_simple_function.fn.*updated|updated.*fission_simple_function.fn" \ - && pass "T07: Package был обновлён (update in apply output)" \ - || { echo "${OUT}" | grep -q "1 updated" && pass "T07: 1 ресурс обновлён" || fail "T07: Обновление не обнаружено в output"; } - -tf_destroy "${D}" | grep -q "Destroy complete" && pass "T07: destroy OK" || fail "T07: destroy FAILED" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T08: добавление url (update — создание Trigger)" -# ════════════════════════════════════════════════════════════════════════ -T08="${RUN_ID}-addurl" -D=$(setup_workdir "t08") -write_provider "${D}" -write_js_v1 "${D}/code" - -# Apply без url -cat > "${D}/main.tf" < "${D}/main.tf" <>> T09: удаление url (update — удаление Trigger)" -# ════════════════════════════════════════════════════════════════════════ -T09="${RUN_ID}-rmurl" -D=$(setup_workdir "t09") -write_provider "${D}" -write_js_v1 "${D}/code" - -# Apply с url -cat > "${D}/main.tf" < "${D}/main.tf" <>> T10: изменение url (update Trigger)" -# ════════════════════════════════════════════════════════════════════════ -T10="${RUN_ID}-churl" -D=$(setup_workdir "t10") -write_provider "${D}" -write_js_v1 "${D}/code" - -cat > "${D}/main.tf" </dev/null || echo "?") - -cat > "${D}/main.tf" </dev/null || echo "?") - -[ "${URL_BEFORE}" = "/test/${T10}/v1" ] && [ "${URL_AFTER}" = "/test/${T10}/v2" ] \ - && pass "T10: URL обновлён (/v1 → /v2)" \ - || fail "T10: URL не обновлён (before='${URL_BEFORE}', after='${URL_AFTER}')" - -tf_destroy "${D}" | grep -q "Destroy complete" && pass "T10: destroy OK" || fail "T10: destroy FAILED" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T11: plan без изменений → 0 changes" -# ════════════════════════════════════════════════════════════════════════ -T11="${RUN_ID}-nochange" -D=$(setup_workdir "t11") -write_provider "${D}" -write_js_v1 "${D}/code" -cat >> "${D}/main.tf" <&1) -echo "${PLAN_OUT}" | grep -qE "No changes|0 to add, 0 to change, 0 to destroy" \ - && pass "T11: повторный plan показывает 0 changes" \ - || fail "T11: повторный plan показывает изменения (не idempotent)" - -tf_destroy "${D}" | grep -q "Destroy complete" && pass "T11: destroy OK" || fail "T11: destroy FAILED" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T12: auto-env idempotency — два ресурса делят один environment" -# ════════════════════════════════════════════════════════════════════════ -T12A="${RUN_ID}-shared-a" -T12B="${RUN_ID}-shared-b" -D=$(setup_workdir "t12") -write_provider "${D}" -write_js_v1 "${D}/code-a" -write_js_v1 "${D}/code-b" -cat >> "${D}/main.tf" </dev/null \ - | grep -c "simple-nodejs-env" || echo "0") -[ "${ENV_COUNT}" -eq 1 ] \ - && pass "T12: ровно 1 экземпляр simple-nodejs-env (shared)" \ - || fail "T12: ENV_COUNT=${ENV_COUNT} (ожидалось 1)" - -tf_destroy "${D}" | grep -q "Destroy complete" && pass "T12: destroy OK" || fail "T12: destroy FAILED" -kube_gone "functions.fission.io" "${T12A}" && pass "T12: fn_a удалена" || fail "T12: fn_a осталась" -kube_gone "functions.fission.io" "${T12B}" && pass "T12: fn_b удалена" || fail "T12: fn_b осталась" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T13: невалидный runtime → apply завершается ошибкой" -# ════════════════════════════════════════════════════════════════════════ -T13="${RUN_ID}-badruntime" -D=$(setup_workdir "t13") -write_provider "${D}" -write_js_v1 "${D}/code" -cat >> "${D}/main.tf" <&1 || true) -echo "${OUT}" | grep -qiE "Error|error|cobol|неподдерживаемый|unsupported" \ - && pass "T13: apply с runtime=cobol выдал ошибку" \ - || fail "T13: apply не выдал ошибку для runtime=cobol" -kube_gone "functions.fission.io" "${T13}" \ - && pass "T13: Function не создана" || fail "T13: Function создана для плохого runtime" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T14: terraform import (namespace/name)" -# ════════════════════════════════════════════════════════════════════════ -T14="${RUN_ID}-import" -D=$(setup_workdir "t14") -write_provider "${D}" -write_js_v1 "${D}/code" -cat >> "${D}/main.tf" </dev/null || true -IMP_OUT=$(terraform -chdir="${D}" import -no-color \ - fission_simple_function.fn "default/${T14}" 2>&1) -echo "${IMP_OUT}" | grep -qiE "Import successful|import" \ - && pass "T14: import выполнен" || fail "T14: import не прошёл: $(echo "${IMP_OUT}" | tail -5)" - -# Plan после import должен показать изменения (code_dir/hash не в k8s state) -# — это нормально, просто убеждаемся что plan не падает с ошибкой -PLAN_OUT=$(terraform -chdir="${D}" plan -no-color 2>&1) -echo $? | grep -qE "^[012]$" && pass "T14: plan после import не падает" || fail "T14: plan после import упал" - -tf_destroy "${D}" | grep -q "Destroy complete" && pass "T14: destroy OK" || fail "T14: destroy FAILED" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T15: nodejs — методы по умолчанию [GET]" -# ════════════════════════════════════════════════════════════════════════ -T15="${RUN_ID}-defmethods" -D=$(setup_workdir "t15") -write_provider "${D}" -write_js_v1 "${D}/code" -cat >> "${D}/main.tf" </dev/null || echo "?") -echo "${METHODS}" | grep -q "GET" \ - && pass "T15: default method=GET в k8s: '${METHODS}'" \ - || fail "T15: методы не GET: '${METHODS}'" - -tf_destroy "${D}" | grep -q "Destroy complete" && pass "T15: destroy OK" || fail "T15: destroy FAILED" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T16: Package.spec для nodejs — deployment (не source)" -# ════════════════════════════════════════════════════════════════════════ -T16="${RUN_ID}-nodepkg" -D=$(setup_workdir "t16") -write_provider "${D}" -write_js_v1 "${D}/code" -cat >> "${D}/main.tf" </dev/null || echo "") -DEP=$(kubectl get packages.fission.io -n "${NAMESPACE}" "${T16}-pkg" \ - -o jsonpath='{.spec.deployment.type}' 2>/dev/null || echo "") -[ -z "${SRC}" ] && [ "${DEP}" = "literal" ] \ - && pass "T16: nodejs Package использует deployment.literal (не source)" \ - || fail "T16: pkg spec — source.type='${SRC}', deployment.type='${DEP}'" - -tf_destroy "${D}" | grep -q "Destroy complete" && pass "T16: destroy OK" || fail "T16: destroy FAILED" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T17: namespace вычисляется из провайдера" -# ════════════════════════════════════════════════════════════════════════ -T17="${RUN_ID}-ns" -D=$(setup_workdir "t17") -write_js_v1 "${D}/code" -# Провайдер без явного namespace в ресурсе -cat > "${D}/main.tf" </dev/null \ - | python3 -c "import sys,json; r=json.load(sys.stdin)['values']['root_module']['resources']; print(r[0]['values']['namespace'])" 2>/dev/null || echo "?") -[ "${NS}" = "default" ] \ - && pass "T17: namespace=default из провайдера (state: ${NS})" \ - || fail "T17: namespace='${NS}' (ожидалось 'default')" - -tf_destroy "${D}" | grep -q "Destroy complete" && pass "T17: destroy OK" || fail "T17: destroy FAILED" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T18: computed поля environment_name/package_name/trigger_name в state" -# ════════════════════════════════════════════════════════════════════════ -T18="${RUN_ID}-computed" -D=$(setup_workdir "t18") -write_provider "${D}" -write_js_v1 "${D}/code" -cat >> "${D}/main.tf" </dev/null \ - | python3 -c " -import sys, json -r = json.load(sys.stdin)['values']['root_module']['resources'][0]['values'] -print(r.get('environment_name','?')) -print(r.get('package_name','?')) -print(r.get('trigger_name','?')) -print(r.get('code_hash','?')) -" 2>/dev/null || echo "? -? -? -?") - -ENV_NAME=$(echo "${SHOW}" | sed -n '1p') -PKG_NAME=$(echo "${SHOW}" | sed -n '2p') -TRG_NAME=$(echo "${SHOW}" | sed -n '3p') -HASH_VAL=$(echo "${SHOW}" | sed -n '4p') - -[ "${ENV_NAME}" = "simple-nodejs-env" ] && pass "T18: environment_name='${ENV_NAME}'" || fail "T18: environment_name='${ENV_NAME}'" -[ "${PKG_NAME}" = "${T18}-pkg" ] && pass "T18: package_name='${PKG_NAME}'" || fail "T18: package_name='${PKG_NAME}'" -[ "${TRG_NAME}" = "${T18}-trigger" ] && pass "T18: trigger_name='${TRG_NAME}'" || fail "T18: trigger_name='${TRG_NAME}'" -[[ "${HASH_VAL}" =~ ^[0-9a-f]{64}$ ]] && pass "T18: code_hash выглядит как sha256" || fail "T18: code_hash='${HASH_VAL}'" - -tf_destroy "${D}" | grep -q "Destroy complete" && pass "T18: destroy OK" || fail "T18: destroy FAILED" - -# ════════════════════════════════════════════════════════════════════════ -echo "" -echo ">>> T19: повторный apply одного ресурса — idempotency (no drift)" -# ════════════════════════════════════════════════════════════════════════ -T19="${RUN_ID}-idem" -D=$(setup_workdir "t19") -write_provider "${D}" -write_js_v1 "${D}/code" -cat >> "${D}/main.tf" <>> T20: destroy без trigger (url не задан) — нет паники" -# ════════════════════════════════════════════════════════════════════════ -T20="${RUN_ID}-notrig-del" -D=$(setup_workdir "t20") -write_provider "${D}" -write_python "${D}/code" -cat >> "${D}/main.tf" </dev/null || true + if [ -x "$NS_CLEANUP_SCRIPT" ] && [ "${#SUBS[@]}" -gt 0 ]; then + KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "${SUBS[@]}" >/dev/null 2>&1 || true + fi + for fn in "${INVOKE_FNS[@]}"; do + curl -s -X DELETE "${BASE}/functions/${fn}" -H "X-Auth-Token: ${INVOKE_SUB}" > /dev/null 2>&1 || true + done +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_stress.sh run=$RUN_ID users=$COUNT invoke_parallel=$INVOKE_COUNT" +echo "========================================" + +JS_CODE='module.exports = async function() { return { status: 200, body: "stress-ok" }; }' + +# ── Шаг 1: параллельное создание ───────────────────────────────────────────── + +echo "" +echo ">>> [1/3] $COUNT пользователей создают функции параллельно..." + +for i in $(seq 1 "$COUNT"); do + ( + SUB="${SUBS[$((i-1))]}" + FN="stress-fn-${RUN_ID}" + R=$(curl -s -w "\n%{http_code}" --max-time 60 -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}" \ + 2>/dev/null) + CODE=$(echo "$R" | tail -1) + echo "$i:$CODE" > "${TMP}/create-$i" + ) & +done +wait + +CREATE_OK=0 +CREATE_FAIL=0 +for i in $(seq 1 "$COUNT"); do + CODE=$(cut -d: -f2 "${TMP}/create-$i" 2>/dev/null || echo "000") + if [ "$CODE" = "201" ] || [ "$CODE" = "200" ]; then + CREATE_OK=$((CREATE_OK+1)) + else + CREATE_FAIL=$((CREATE_FAIL+1)) + echo " user$i: CREATE HTTP $CODE" + fi +done + +[ "$CREATE_FAIL" -eq 0 ] \ + && pass "Параллельный CREATE: все $COUNT пользователей → 201" \ + || fail "Параллельный CREATE: $CREATE_FAIL/$COUNT упали" + +# ── Шаг 2: изоляция LIST ───────────────────────────────────────────────────── + +echo "" +echo ">>> [2/3] Изоляция: каждый видит только свою функцию" + +ISO_OK=0 +for i in $(seq 1 "$COUNT"); do + SUB="${SUBS[$((i-1))]}" + FN="stress-fn-${RUN_ID}" + LIST=$(curl -s --max-time 20 "${BASE}/functions" -H "X-Auth-Token: ${SUB}") + OWN_COUNT=$(printf '%s' "$LIST" | python3 -c " +import json, sys +try: + fns = json.load(sys.stdin) + print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == '${FN}')) +except: + print(0)" 2>/dev/null) + TOTAL_COUNT=$(json_len "$LIST") + if [ "${OWN_COUNT:-0}" -eq 1 ] && [ "${TOTAL_COUNT:-0}" -eq 1 ]; then + ISO_OK=$((ISO_OK+1)) + else + echo " user$i: own=$OWN_COUNT total=$TOTAL_COUNT" + fi +done + +[ "$ISO_OK" -eq "$COUNT" ] \ + && pass "Изоляция: все $COUNT пользователей видят только свои функции" \ + || fail "Изоляция: только $ISO_OK/$COUNT OK" + +# ── Шаг 3: параллельный invoke (livetest — watched namespace) ───────────────── + +echo "" +echo ">>> [3/3] $INVOKE_COUNT параллельных invoke для livetest@test.local" + +INV_JS='module.exports = async function() { return { status: 200, body: "sinv-ok" }; }' +for i in $(seq 1 "$INVOKE_COUNT"); do + FN_I="${INVOKE_FNS[$((i-1))]}" + curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${INVOKE_SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN_I}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$INV_JS" | json_escape)}" > /dev/null +done + +echo "Ждём прогрева $INVOKE_COUNT функций..." +for i in $(seq 1 "$INVOKE_COUNT"); do + FN_I="${INVOKE_FNS[$((i-1))]}" + for try in $(seq 1 12); do + R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_I}/invoke" \ + -H "X-Auth-Token: ${INVOKE_SUB}" -H "Content-Type: application/json" -d '{}') + ST=$(json_field "$R" status) + [ "$ST" = "200" ] && break + sleep 5 + done +done + +for i in $(seq 1 "$INVOKE_COUNT"); do + ( + FN_I="${INVOKE_FNS[$((i-1))]}" + R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_I}/invoke" \ + -H "X-Auth-Token: ${INVOKE_SUB}" -H "Content-Type: application/json" -d '{}') + ST=$(json_field "$R" status) + echo "$i:$ST" > "${TMP}/sinvoke-$i" + ) & +done +wait + +SINV_OK=0 +SINV_FAIL=0 +for i in $(seq 1 "$INVOKE_COUNT"); do + CODE=$(cut -d: -f2 "${TMP}/sinvoke-$i" 2>/dev/null || echo "MISS") + [ "$CODE" = "200" ] && SINV_OK=$((SINV_OK+1)) || SINV_FAIL=$((SINV_FAIL+1)) +done + +[ "$SINV_FAIL" -le 1 ] \ + && pass "Параллельный INVOKE (livetest): $SINV_OK/$INVOKE_COUNT OK" \ + || fail "Параллельный INVOKE (livetest): $SINV_FAIL/$INVOKE_COUNT упали" + +print_summary diff --git a/scripts/test_ttl.sh b/scripts/test_ttl.sh new file mode 100755 index 0000000..18e0ac0 --- /dev/null +++ b/scripts/test_ttl.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# test_ttl.sh — тестирует TTL функций: +# CREATE с TTL → expires_at присутствует в ответе +# CREATE без TTL → expires_at отсутствует +# Невалидный TTL → 400 до создания объектов в k8s +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" +RUN_ID=$(mk_run_id) +SUB="ttl-${RUN_ID}@test.local" + +cleanup() { + if [ -x "$NS_CLEANUP_SCRIPT" ]; then + KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$SUB" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_ttl.sh run=$RUN_ID" +echo "========================================" + +JS_CODE='module.exports = async function() { return { status: 200, body: "ok" }; }' + +# ── Без TTL ────────────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE без TTL → expires_at отсутствует" +R=$(curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"nttl-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}") +CODE_HTTP=$(printf '%s' "$R" | python3 -c "import sys; lines=sys.stdin.read().split('\n'); print(lines[-1] if lines else '')" 2>/dev/null || echo "?") +EXPIRES=$(json_field "$R" expires_at) +[ -z "$EXPIRES" ] && pass "CREATE без TTL: expires_at отсутствует" || fail "CREATE без TTL: expires_at='$EXPIRES'" + +# ── С TTL=15m ──────────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE с TTL=15m → expires_at присутствует" +R=$(curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"ttl15m-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape),\"ttl\":\"15m\"}") +EXPIRES=$(json_field "$R" expires_at) +[ -n "$EXPIRES" ] && pass "CREATE TTL=15m: expires_at=$EXPIRES" || fail "CREATE TTL=15m: expires_at пустой" +# expires_at должен быть RFC3339 +printf '%s' "$EXPIRES" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}T' && pass "expires_at формат RFC3339" || fail "expires_at не RFC3339: $EXPIRES" + +# ── С TTL=1h ───────────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE с TTL=1h → expires_at присутствует" +R=$(curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"ttl1h-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape),\"ttl\":\"1h\"}") +EXPIRES=$(json_field "$R" expires_at) +[ -n "$EXPIRES" ] && pass "CREATE TTL=1h: expires_at=$EXPIRES" || fail "CREATE TTL=1h: expires_at пустой" + +# ── С TTL=2d ───────────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE с TTL=2d → expires_at присутствует" +R=$(curl -s -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"ttl2d-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape),\"ttl\":\"2d\"}") +EXPIRES=$(json_field "$R" expires_at) +[ -n "$EXPIRES" ] && pass "CREATE TTL=2d: expires_at=$EXPIRES" || fail "CREATE TTL=2d: expires_at пустой" + +# ── Невалидный TTL ─────────────────────────────────────────────────────────── + +echo "" +echo ">>> CREATE TTL невалидный → 400 (без создания объектов)" +for badttl in "99z" "abc" "0d" "-1h" ""; do + if [ -z "$badttl" ]; then continue; fi + R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"badttl-fn\",\"language\":\"nodejs\",\"code\":\"x\",\"ttl\":\"${badttl}\"}") + CODE=$(echo "$R" | tail -1) + [ "$CODE" = "400" ] && pass "TTL='$badttl' → 400" || fail "TTL='$badttl' → got $CODE" +done + +# ── expires_at в LIST response ─────────────────────────────────────────────── + +echo "" +echo ">>> LIST: функция с TTL видна в списке" +LIST=$(curl -s "${BASE}/functions" -H "X-Auth-Token: ${SUB}") +COUNT=$(json_len "$LIST") +[ "${COUNT:-0}" -ge 3 ] && pass "LIST: видно ≥3 функции (2 с TTL, 1 без)" || fail "LIST: count=$COUNT" + +print_summary diff --git a/scripts/test_ui_user_flows.sh b/scripts/test_ui_user_flows.sh deleted file mode 100755 index de816a1..0000000 --- a/scripts/test_ui_user_flows.sh +++ /dev/null @@ -1,326 +0,0 @@ -#!/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-Auth-Token: ${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-Auth-Token: ${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-Auth-Token: ${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-Auth-Token: ${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-Auth-Token: ${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 ] \ No newline at end of file diff --git a/scripts/test_update.sh b/scripts/test_update.sh new file mode 100755 index 0000000..d25b7e2 --- /dev/null +++ b/scripts/test_update.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# test_update.sh — тестирует обновление кода функции: +# PUT /functions/{name}/code → invoke возвращает новый код +# PUT несуществующей → 404 +# PUT с пустым кодом → 400 +# ВАЖНО: использует livetest@test.local — прогретый namespace в kube кластере. +# Для новых namespace fission router требует перезапуска (watchedNamespaces). +set -uo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${ROOT_DIR}/lib.sh" + +RUN_ID=$(mk_run_id) +SUB="livetest@test.local" +FN="upd-fn-${RUN_ID}" + +cleanup() { + curl -s -X DELETE "${BASE}/functions/${FN}" -H "X-Auth-Token: ${SUB}" > /dev/null 2>&1 || true +} +trap cleanup EXIT + +echo "" +echo "========================================" +echo " test_update.sh run=$RUN_ID" +echo "========================================" + +# ── Создаём функцию с начальным кодом ──────────────────────────────────────── + +echo "" +echo ">>> CREATE: версия v1" +CODE_V1='module.exports = async function() { return { status: 200, body: "version-one" }; }' +R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$CODE_V1" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "201" "CREATE v1 → 201" + +echo "Ждём прогрева..." +if wait_invoke "$SUB" "$FN" "version-one" 12 5; then + pass "Invoke v1 → version-one" +else + fail "Invoke v1 не стабилизировался" +fi + +# ── PUT /code → 200 ────────────────────────────────────────────────────────── + +echo "" +echo ">>> PUT /code: версия v2" +CODE_V2='module.exports = async function() { return { status: 200, body: "version-two" }; }' +R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/${FN}/code" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"code\":$(printf '%s' "$CODE_V2" | json_escape)}") +CODE=$(echo "$R" | tail -1) +BODY=$(echo "$R" | sed '$d') +check_http "$CODE" "200" "PUT /code v2 → 200" +UPDATED=$(json_field "$BODY" updated) +[ "$UPDATED" = "True" ] || [ "$UPDATED" = "true" ] && pass "PUT /code: updated=true" || fail "PUT /code: updated='$UPDATED'" +NEW_PKG=$(json_field "$BODY" package) +[ -n "$NEW_PKG" ] && pass "PUT /code: новый package=$NEW_PKG" || fail "PUT /code: package пустой" + +echo "Ждём пока executor подхватит новый код..." +if wait_invoke "$SUB" "$FN" "version-two" 15 5; then + pass "Invoke после update → version-two" +else + fail "Invoke после update не вернул version-two" +fi + +# ── PUT с пустым кодом → 400 ────────────────────────────────────────────────── + +echo "" +echo ">>> PUT /code: пустой код → 400" +R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/${FN}/code" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"code":""}') +check_http "$(echo "$R" | tail -1)" "400" "PUT /code пустой код → 400" + +R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/${FN}/code" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d '{"code":" "}') +check_http "$(echo "$R" | tail -1)" "400" "PUT /code пробельный код → 400" + +# ── PUT несуществующей → 404 ────────────────────────────────────────────────── + +echo "" +echo ">>> PUT /code: несуществующая функция → 404" +R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/no-such-fn-xyz/code" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"code\":$(printf '%s' "$CODE_V1" | json_escape)}") +check_http "$(echo "$R" | tail -1)" "404" "PUT /code несуществующая → 404" + +# ── Ещё одно обновление (v3) — убеждаемся что можно делать цепочку ──────────── + +echo "" +echo ">>> PUT /code: версия v3 (цепочка)" +CODE_V3='module.exports = async function() { return { status: 200, body: "version-three" }; }' +curl -s -X PUT "${BASE}/functions/${FN}/code" \ + -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ + -d "{\"code\":$(printf '%s' "$CODE_V3" | json_escape)}" > /dev/null + +if wait_invoke "$SUB" "$FN" "version-three" 15 5; then + pass "Invoke после v3 update → version-three" +else + fail "Invoke после v3 update не стабилизировался" +fi + +print_summary diff --git a/scripts/tests_v2.sh b/scripts/tests_v2.sh deleted file mode 100644 index 4bd32b4..0000000 --- a/scripts/tests_v2.sh +++ /dev/null @@ -1,783 +0,0 @@ -#!/usr/bin/env bash -set -uo pipefail - -BASE="https://fission.kube5s.ru/console/api" -ROOT_DIR=$(cd "$(dirname "$0")" && pwd) -NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh" -PASS=0; FAIL=0 -RUN_ID=$(date +%s%N 2>/dev/null || date +%s)$(od -An -N2 -tu2 /dev/urandom 2>/dev/null | tr -d ' ' || echo "$$") -RUN_ID=$(echo "$RUN_ID" | sha256sum 2>/dev/null | head -c 8 || echo "${RUN_ID}" | cut -c1-8) - -cleanup_namespaces() { - local subs=() - subs+=("alice@test.local" "routetest-alice@test.local" "routetest-bob@test.local") - subs+=("val-user@test.local" "dup-user@test.local" "upd-user@test.local" "get-user@test.local") - subs+=("ns-u1@test.local" "ns-u2@test.local" "alice-del@test.local" "carol-del@test.local") - subs+=("py-user@test.local" "js-user@test.local" "ttl-user@test.local" "body-user@test.local") - subs+=("stress-user@test.local" "brand-new-empty-user-xyzqwerty@test.local" "chain-user@test.local") - subs+=("dup2-user-${RUN_ID}@test.local" "name-user-${RUN_ID}@test.local") - subs+=("len57-user-${RUN_ID}@test.local" "big-user-${RUN_ID}@test.local" "size-user-${RUN_ID}@test.local") - subs+=("ttl-exp-${RUN_ID}@test.local" "race-${RUN_ID}@test.local") - subs+=("mfunc-${RUN_ID}@test.local" "victim-${RUN_ID}@test.local" "attacker-${RUN_ID}@test.local") - subs+=("lang-multi-${RUN_ID}@test.local" "lang-inv-${RUN_ID}@test.local") - - local uid - for uid in $(seq 1 10); do - subs+=("u${uid}-${RUN_ID}@test.local") - done - for uid in $(seq 1 3); do - subs+=("fullcycle-u${uid}-${RUN_ID}@test.local") - done - - if [ -x "$NS_CLEANUP_SCRIPT" ]; then - echo "" - echo ">>> Cleanup test namespaces..." - "$NS_CLEANUP_SCRIPT" "${subs[@]}" || true - fi -} -trap cleanup_namespaces EXIT - -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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: routetest-alice@test.local" -H "Content-Type: application/json" -d '{}') -INV_B=$(curl -s -X POST ${BASE}/functions/${ISO_NAME}/invoke \ - -H "X-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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-Auth-Token: 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 из LIST (до DELETE) -NS_CHAIN=$(curl -s ${BASE}/functions -H "X-Auth-Token: chain-user@test.local" \ - | python3 -c "import sys,json; items=json.load(sys.stdin); print(items[0]['metadata']['namespace'] if items else '')" 2>/dev/null) - -R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/chain-fn \ - -H "X-Auth-Token: chain-user@test.local") -CODE=$(echo "$R" | tail -1); check_http "$CODE" "200" "DELETE chain-fn → 200" - -# Ждём propagation удаления в k8s -sleep 3 -# k8s: проверяем через kubectl (или через API если kubectl недоступен) -FN_RAW=$(kubectl get functions.fission.io chain-fn -n ${NS_CHAIN} 2>&1) -if echo "$FN_RAW" | grep -q "not found"; then - pass "k8s: Function удалена после DELETE" -elif echo "$FN_RAW" | grep -q "credentials\|logged in\|Unauthorized"; then - # kubectl недоступен — проверяем через API (GET должен давать 404) - FN_API=$(curl -s -o /dev/null -w "%{http_code}" ${BASE}/functions/chain-fn \ - -H "X-Auth-Token: chain-user@test.local") - [ "$FN_API" = "404" ] && pass "k8s: Function удалена (подтверждено API, kubectl без прав)" || fail "k8s: Function не удалена, API=$FN_API" -else - fail "k8s: Function ещё существует в $NS_CHAIN" -fi -TRG_EXISTS=$(kubectl get httptriggers.fission.io -n ${NS_CHAIN} 2>&1 | grep -c "chain-fn" || true) - -# После удаления GET должен вернуть 404 -R=$(curl -s -w "\n%{http_code}" ${BASE}/functions/chain-fn \ - -H "X-Auth-Token: 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-Auth-Token → 401/403 (got $CODE)" || fail "Без X-Auth-Token → 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 ">>> БЛОК Q: Дубликат → 409, невалидное имя → 400" - -# Создаём функцию, потом снова с тем же именем → 409 -curl -s -X POST ${BASE}/functions \ - -H "X-Auth-Token: dup2-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"dup-q-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:'x'})\"}" > /dev/null -sleep 3 -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: dup2-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"dup-q-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:'x'})\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "409" "Дубликат → 409 Conflict" - -# Имя с заглавной буквой → 400 -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"BadName\",\"language\":\"nodejs\",\"code\":\"x\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "400" "Имя с заглавной буквой → 400" - -# Имя начинается с дефиса → 400 -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"-badstart\",\"language\":\"nodejs\",\"code\":\"x\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "400" "Имя с ведущим дефисом → 400" - -# Имя заканчивается дефисом → 400 -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"badend-\",\"language\":\"nodejs\",\"code\":\"x\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "400" "Имя с завершающим дефисом → 400" - -# Имя с пробелами → 400 (было 502) -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"bad name\",\"language\":\"nodejs\",\"code\":\"x\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "400" "Имя с пробелом → 400 (не 502)" - -# Имя 64 символа → 400 -LONGNAME=$(python3 -c "print('a'*64)") -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"${LONGNAME}\",\"language\":\"nodejs\",\"code\":\"x\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "400" "Имя 64 символа → 400" - -# Имя 58 символов → 400 (route = 64 > 63, Fission webhook отклонит) -NAME58=$(python3 -c "print('a'*58)") -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"${NAME58}\",\"language\":\"nodejs\",\"code\":\"x\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "400" "Имя 58 символов → 400 (route будет 64 символа)" - -# Имя 57 символов → принято (route = 63 символа, ровно лимит Fission) -NAME57=$(python3 -c "print('a'*57)") -# Первый прогрев namespace -curl -s -X POST ${BASE}/functions \ - -H "X-Auth-Token: len57-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d '{"name":"warmup","language":"nodejs","code":"module.exports=async()=>({body:\"w\"})"}' > /dev/null -sleep 8 -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: len57-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"${NAME57}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:'ok'})\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "201" "Имя 57 символов → принято (201)" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК R: Лимит размера кода 1MB" - -# Код > 1MB → 400 -BIG_TMP=$(mktemp) -python3 -c " -import json, sys -code = 'x' * (1024*1024 + 1) -body = json.dumps({'name':'big-fn','language':'nodejs','code':code}) -sys.stdout.write(body) -" > "$BIG_TMP" -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: big-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - --data @"$BIG_TMP") -rm -f "$BIG_TMP" -CODE=$(echo "$R" | tail -1) -# 400 = наш код, 413 = nginx body limit, 000 = connection reset nginx — всё означает "отклонён" -( [ "$CODE" = "400" ] || [ "$CODE" = "413" ] || [ "$CODE" = "000" ] ) \ - && pass "Код > 1MB → отклонён (HTTP $CODE)" \ - || fail "Код > 1MB → got HTTP $CODE (ожидался 400/413)" - -# Код ровно 1000 символов → не 400 -CODE1K=$(python3 -c "print('module.exports=async()=>({body:' + '\"x\"' + '});' + ' '*(1000-38))") -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: size-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"size-fn-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:'ok'})\"}") -CODE=$(echo "$R" | tail -1) -[ "$CODE" = "201" ] && pass "Код 1000 байт → принят (201)" || fail "Код 1000 байт → got $CODE" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК S: TTL expire + reaper очистка" - -# Создаём функцию с TTL=1m -R=$(curl -s -X POST ${BASE}/functions \ - -H "X-Auth-Token: ttl-exp-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d "{\"name\":\"expire-fn-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:'bye'})\",\"ttl\":\"1m\"}") -CREATED=$(echo "$R" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('name',''))" 2>/dev/null) -[ "$CREATED" = "expire-fn-${RUN_ID}" ] && pass "TTL=1m функция создана" || fail "TTL=1m создание: $R" - -# Сразу должна быть видна в LIST -COUNT=$(curl -s ${BASE}/functions \ - -H "X-Auth-Token: ttl-exp-${RUN_ID}@test.local" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) -[ "$COUNT" -ge 1 ] 2>/dev/null && pass "TTL=1m функция видна в LIST" || fail "TTL=1m LIST count=$COUNT" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК T: Параллельное создание одного namespace (race condition)" - -# 5 одновременных запросов от одного пользователя (первый раз) -NS_USER="race-${RUN_ID}@test.local" -RACE_RESULTS="" -for i in $(seq 1 5); do - RACE_CODE="module.exports=async()=>({body:'r${i}'})" - curl -s -X POST ${BASE}/functions \ - -H "X-Auth-Token: ${NS_USER}" -H "Content-Type: application/json" \ - -d "{\"name\":\"race-fn-${i}\",\"language\":\"nodejs\",\"code\":\"${RACE_CODE}\"}" & -done -wait -# Проверяем что namespace создался и функции (хоть часть) прошли -sleep 5 -COUNT=$(curl -s ${BASE}/functions \ - -H "X-Auth-Token: ${NS_USER}" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) -[ "$COUNT" -ge 1 ] 2>/dev/null && pass "Race condition: namespace создан, функций: $COUNT" || fail "Race condition: functions=$COUNT" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК U: 10 разных пользователей создают функции параллельно" - -for uid in $(seq 1 10); do - ( - U="u${uid}-${RUN_ID}@test.local" - CODE="module.exports=async()=>({body:'user${uid}'})" - curl -s -X POST ${BASE}/functions \ - -H "X-Auth-Token: ${U}" -H "Content-Type: application/json" \ - -d "{\"name\":\"fn-${uid}\",\"language\":\"nodejs\",\"code\":\"${CODE}\"}" > /dev/null - ) & -done -wait -sleep 3 - -OK_USERS=0 -for uid in $(seq 1 10); do - U="u${uid}-${RUN_ID}@test.local" - C=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${U}" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) - [ "$C" -ge 1 ] 2>/dev/null && OK_USERS=$((OK_USERS+1)) -done -[ "$OK_USERS" -ge 8 ] && pass "10 юзеров параллельно: ${OK_USERS}/10 создали функции" || fail "10 юзеров параллельно: только ${OK_USERS}/10" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК V: Изоляция — 10 юзеров не видят чужих функций" - -ISOLATION_OK=0 -for uid in $(seq 1 10); do - U="u${uid}-${RUN_ID}@test.local" - LIST=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${U}") - # Проверяем что НЕТ чужих функций (fn-X от других пользователей не попадают) - # Каждый видит ровно 1 свою (fn-{uid}) и ни одной fn-{other} - MY_COUNT=$(echo "$LIST" | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='fn-${uid}'))" 2>/dev/null) - OTHER_COUNT=$(echo "$LIST" | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name'].startswith('fn-') and f['metadata']['name']!='fn-${uid}'))" 2>/dev/null) - if [ "${MY_COUNT:-0}" -ge 1 ] && [ "${OTHER_COUNT:-0}" -eq 0 ]; then - ISOLATION_OK=$((ISOLATION_OK+1)) - fi -done -[ "$ISOLATION_OK" -ge 8 ] && pass "Изоляция 10 юзеров: ${ISOLATION_OK}/10 видят только своё" || fail "Изоляция 10 юзеров: только ${ISOLATION_OK}/10" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК W: 5 юзеров одновременно вызывают свои функции" - -for uid in $(seq 1 5); do - ( - U="u${uid}-${RUN_ID}@test.local" - NS12=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${U}" \ - | python3 -c "import sys,json; fns=json.load(sys.stdin); print(fns[0]['route'].split('/')[1] if fns else '')" 2>/dev/null) - if [ -n "$NS12" ]; then - INVOKE_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ - "https://fission.kube5s.ru/console/proxy/${NS12}/fn-${uid}") - echo "U${uid}:${INVOKE_CODE}" - else - echo "U${uid}:NO_ROUTE" - fi - ) & -done -INVOKE_RESULTS=$(wait; sleep 2) -# Просто проверяем что invoke не упал совсем (ждём фоновые процессы) -sleep 3 -pass "5 юзеров параллельный invoke — завершён без паники" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК X: 3 юзера — полный цикл CREATE→LIST→INVOKE→DELETE" - -FULL_CYCLE_OK=0 -for uid in $(seq 1 3); do - U="fullcycle-u${uid}-${RUN_ID}@test.local" - FN="fc-fn-${uid}" - CODE="module.exports=async()=>({body:'cycle${uid}'})" - - # CREATE - CR=$(curl -s -o /dev/null -w "%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: ${U}" -H "Content-Type: application/json" \ - -d "{\"name\":\"${FN}\",\"language\":\"nodejs\",\"code\":\"${CODE}\"}") - - # LIST — функция есть (sleep чтобы k8s успел зарегистрировать) - sleep 1 - LC=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${U}" \ - | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='${FN}'))" 2>/dev/null) - - # DELETE - DC=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ - "${BASE}/functions/${FN}" -H "X-Auth-Token: ${U}") - - # LIST после DELETE — функции нет - LC2=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${U}" \ - | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='${FN}'))" 2>/dev/null) - - if [ "$CR" = "201" ] && [ "${LC:-0}" -ge 1 ] && [ "$DC" = "200" ] && [ "${LC2:-1}" -eq 0 ]; then - FULL_CYCLE_OK=$((FULL_CYCLE_OK+1)) - else - echo " [U${uid}] CR=$CR LC=$LC DC=$DC LC2=$LC2" - fi -done -[ "$FULL_CYCLE_OK" -eq 3 ] && pass "3 юзера полный цикл CREATE→LIST→DELETE: ${FULL_CYCLE_OK}/3" || fail "3 юзера полный цикл: только ${FULL_CYCLE_OK}/3" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК Y: Один юзер создаёт 10 функций, проверяем LIST и DELETE all" - -MFUNC_USER="mfunc-${RUN_ID}@test.local" -for i in $(seq 1 10); do - curl -s -X POST ${BASE}/functions \ - -H "X-Auth-Token: ${MFUNC_USER}" -H "Content-Type: application/json" \ - -d "{\"name\":\"mf-${i}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:${i}})\"}" > /dev/null & -done -wait -sleep 3 - -MFUNC_COUNT=$(curl -s ${BASE}/functions \ - -H "X-Auth-Token: ${MFUNC_USER}" \ - | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) -[ "${MFUNC_COUNT:-0}" -ge 8 ] && pass "1 юзер создал 10 функций: видно ${MFUNC_COUNT}" || fail "1 юзер: создано только ${MFUNC_COUNT:-0}" - -# Удаляем все -for i in $(seq 1 10); do - curl -s -X DELETE "${BASE}/functions/mf-${i}" \ - -H "X-Auth-Token: ${MFUNC_USER}" > /dev/null & -done -wait -sleep 3 -MFUNC_AFTER=$(curl -s ${BASE}/functions \ - -H "X-Auth-Token: ${MFUNC_USER}" \ - | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) -[ "${MFUNC_AFTER:-1}" -eq 0 ] && pass "DELETE all: список пуст после удаления 10 функций" || fail "DELETE all: осталось ${MFUNC_AFTER:-?} функций" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК Z: Cross-user attack — юзер пытается DELETE/UPDATE/GET чужую функцию" - -VICTIM_USER="victim-${RUN_ID}@test.local" -ATTACKER_USER="attacker-${RUN_ID}@test.local" - -# Жертва создаёт функцию -curl -s -X POST ${BASE}/functions \ - -H "X-Auth-Token: ${VICTIM_USER}" -H "Content-Type: application/json" \ - -d '{"name":"secret-fn","language":"nodejs","code":"module.exports=async()=>({body:42})"}' > /dev/null -sleep 2 - -# Атакующий пытается GET чужой функции -Z_GET=$(curl -s -o /dev/null -w "%{http_code}" \ - "${BASE}/functions/secret-fn" -H "X-Auth-Token: ${ATTACKER_USER}") -[ "$Z_GET" = "404" ] && pass "Cross-user GET чужой функции → 404" || fail "Cross-user GET → ожидали 404, got $Z_GET" - -# Атакующий пытается UPDATE чужой функции (PUT /functions/{name}/code) -Z_PUT=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \ - "${BASE}/functions/secret-fn/code" \ - -H "X-Auth-Token: ${ATTACKER_USER}" -H "Content-Type: application/json" \ - -d '{"code":"module.exports=async()=>({body:\"hacked\"})"}') -[ "$Z_PUT" = "404" ] && pass "Cross-user UPDATE чужой функции → 404" || fail "Cross-user UPDATE → ожидали 404, got $Z_PUT" - -# Атакующий пытается DELETE чужой функции -Z_DEL=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ - "${BASE}/functions/secret-fn" -H "X-Auth-Token: ${ATTACKER_USER}") -[ "$Z_DEL" = "404" ] && pass "Cross-user DELETE чужой функции → 404" || fail "Cross-user DELETE → ожидали 404, got $Z_DEL" - -# Жертва проверяет что функция цела -Z_VICTIM_LIST=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${VICTIM_USER}" \ - | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='secret-fn'))" 2>/dev/null) -[ "${Z_VICTIM_LIST:-0}" -eq 1 ] && pass "Cross-user: функция жертвы цела после атаки" || fail "Cross-user: функция жертвы исчезла!" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК AA: PHP — создание и вызов" - -# Оба блока (AA/AB) используют ОДИН namespace, чтобы избежать повторных executor rollouts. -# Первая функция (PHP) вызывает addNSToFission → executor rollout (~60s). -# Ruby создаётся в том же namespace → rollout не нужен → пул стартует сразу. -LANG_NS="lang-multi-${RUN_ID}@test.local" - -PHP_CODE=$(python3 -c "import json; print(json.dumps('getBody()->write(\"php-ok\");\n}'))") -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: ${LANG_NS}" -H "Content-Type: application/json" \ - -d "{\"name\":\"php-fn\",\"language\":\"php\",\"code\":${PHP_CODE},\"ttl\":\"10m\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "201" "PHP: create → 201" - -# Ждём пока poolmgr поднимет pod для нового env (retry до 200s). -# После executor rollout (~60s) + startup pod (~30s) = ~90s до первого invoke. -STATUS=""; RAW="" -for _try in $(seq 1 20); do - sleep 10 - INV=$(curl -s -X POST ${BASE}/functions/php-fn/invoke \ - -H "X-Auth-Token: ${LANG_NS}") - STATUS=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null) - RAW=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null) - [ "$STATUS" = "200" ] && break -done -[ "$STATUS" = "200" ] && pass "PHP: invoke → 200" || fail "PHP: invoke → got $STATUS (body: $RAW)" -echo "$RAW" | grep -q "php-ok" && pass "PHP: тело содержит php-ok" || fail "PHP: тело не содержит php-ok (got: $RAW)" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК AB: Ruby — создание и вызов" - -RUBY_CODE=$(python3 -c "import json; print(json.dumps('def handler\n \"ruby-ok\"\nend'))") -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: ${LANG_NS}" -H "Content-Type: application/json" \ - -d "{\"name\":\"ruby-fn\",\"language\":\"ruby\",\"code\":${RUBY_CODE},\"ttl\":\"10m\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "201" "Ruby: create → 201" - -# Ждём пока poolmgr поднимет pod (retry до 200s). -# Namespace уже известен executor после PHP-блока → rollout не нужен → пул стартует быстро. -STATUS=""; RAW="" -for _try in $(seq 1 20); do - sleep 10 - INV=$(curl -s -X POST ${BASE}/functions/ruby-fn/invoke \ - -H "X-Auth-Token: ${LANG_NS}") - STATUS=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null) - RAW=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null) - [ "$STATUS" = "200" ] && break -done -[ "$STATUS" = "200" ] && pass "Ruby: invoke → 200" || fail "Ruby: invoke → got $STATUS (body: $RAW)" -echo "$RAW" | grep -q "ruby-ok" && pass "Ruby: тело содержит ruby-ok" || fail "Ruby: тело не содержит ruby-ok (got: $RAW)" - -# ────────────────────────────────────────────── -echo "" -echo ">>> БЛОК AD: Невалидный язык → 400" - -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Auth-Token: lang-inv-${RUN_ID}@test.local" -H "Content-Type: application/json" \ - -d '{"name":"bad-lang-fn","language":"cobol","code":"x"}') -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "400" "Невалидный язык cobol → 400" - -# ────────────────────────────────────────────── -echo "" -# ────────────────────────────────────────────── -echo "" -echo "========================================" -printf "Итого: PASS=%d FAIL=%d\n" $PASS $FAIL -echo "========================================"