651 lines
39 KiB
Bash
651 lines
39 KiB
Bash
#!/usr/bin/env bash
|
||
# 2026-03-21 — bug_hunter.sh: охота за багами во всех POSTGRES-функциях.
|
||
# Цель: найти bugs типа "ложный 200", "должен 500 но 200", неверные данные.
|
||
# Логика принципиально отличается от chaos_marathon — здесь акцент на семантике и data integrity.
|
||
# Запускать ТОЛЬКО на VM через SSH.
|
||
|
||
set -euo pipefail
|
||
|
||
BASE_URL="${BASE_URL:-https://sless.kube5s.ru}"
|
||
NAMESPACE="${NAMESPACE:-sless-ffd1f598c169b0ae}"
|
||
TOKEN_FILE="${TOKEN_FILE:-$HOME/terra/sless/test.token}"
|
||
TOKEN=$(cat "$TOKEN_FILE")
|
||
|
||
RED="\033[0;31m"; GREEN="\033[0;32m"; YELLOW="\033[1;33m"; CYAN="\033[0;36m"; NC="\033[0m"
|
||
|
||
PASS=0; FAIL=0; TOTAL=0
|
||
BUGS_FOUND=()
|
||
|
||
ts() { date '+%H:%M:%S'; }
|
||
|
||
# ── Хелперы ──────────────────────────────────────────────────────────────────
|
||
raw() {
|
||
local svc="$1" payload="$2"
|
||
curl -sf -X POST -H "Content-Type: application/json" \
|
||
-d "$payload" "${BASE_URL}/fn/${NAMESPACE}/${svc}" 2>/dev/null || echo "__CURL_FAIL__"
|
||
}
|
||
|
||
http_code() {
|
||
local svc="$1" payload="$2"
|
||
curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||
-d "$payload" "${BASE_URL}/fn/${NAMESPACE}/${svc}" 2>/dev/null || echo "000"
|
||
}
|
||
|
||
# Проверяем что HTTP-код РАВЕН ожидаемому
|
||
check_http() {
|
||
local label="$1" got="$2" want="$3"
|
||
TOTAL=$((TOTAL+1))
|
||
if [[ "$got" == "$want" ]]; then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got HTTP $got, want HTTP $want)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("$label → HTTP $got ≠ $want")
|
||
fi
|
||
}
|
||
|
||
# Проверяем что JSON содержит строку
|
||
check_has() {
|
||
local label="$1" body="$2" substr="$3"
|
||
TOTAL=$((TOTAL+1))
|
||
if echo "$body" | grep -q "$substr" 2>/dev/null; then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (key/substr '$substr' not found in: ${body:0:120})"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("$label → '$substr' not in response")
|
||
fi
|
||
}
|
||
|
||
# Проверяем что JSON НЕ содержит строку
|
||
check_not() {
|
||
local label="$1" body="$2" substr="$3"
|
||
TOTAL=$((TOTAL+1))
|
||
if echo "$body" | grep -q "$substr" 2>/dev/null; then
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (found '$substr' but should NOT be there: ${body:0:120})"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("$label → '$substr' should NOT be in response")
|
||
else
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
||
PASS=$((PASS+1))
|
||
fi
|
||
}
|
||
|
||
# Проверяем числовое равенство: jq вытаскивает значение и сравниваем
|
||
check_val() {
|
||
local label="$1" body="$2" jq_expr="$3" want="$4"
|
||
local got
|
||
got=$(echo "$body" | python3 -c "
|
||
import json,sys
|
||
try:
|
||
d=json.load(sys.stdin)
|
||
expr='$jq_expr'.lstrip('.')
|
||
parts=expr.split('.')
|
||
val=d
|
||
for p in parts:
|
||
val=val[p] if isinstance(val,dict) else val[int(p)]
|
||
print(val)
|
||
except Exception as e:
|
||
print('__ERR__:'+str(e))
|
||
" 2>/dev/null || echo "__ERR__")
|
||
TOTAL=$((TOTAL+1))
|
||
if [[ "$got" == "$want" ]]; then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got '$got', want '$want')"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("$label → got '$got' want '$want'")
|
||
fi
|
||
}
|
||
|
||
# Проверяем что числовое значение >= порога
|
||
check_gte() {
|
||
local label="$1" got="$2" min_val="$3"
|
||
TOTAL=$((TOTAL+1))
|
||
if [[ "$got" =~ ^[0-9]+$ ]] && (( got >= min_val )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label (got $got >= $min_val)"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got '$got', want >= $min_val)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("$label → $got < $min_val")
|
||
fi
|
||
}
|
||
|
||
section() {
|
||
echo ""
|
||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${CYAN} $1${NC}"
|
||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
||
}
|
||
|
||
echo -e "${YELLOW}"
|
||
echo "╔══════════════════════════════════════════════════════════╗"
|
||
echo "║ BUG HUNTER — $(date '+%Y-%m-%d %H:%M:%S') ║"
|
||
echo "║ Ищем: ложные 200, неверные данные, скрытые баги ║"
|
||
echo "╚══════════════════════════════════════════════════════════╝"
|
||
echo -e "${NC}"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 1 — Тест на Missing Input Validation (должно быть 200, НО БУДЕТ 500 если баг есть)"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
echo " Каждая функция должна СТОЙКО обрабатывать строку вместо числа."
|
||
echo " Если возвращает 500 — это BUG: не хватает try/except."
|
||
|
||
c=$(http_code "chaos-slowquery" '{"sleep_sec": "не_число"}')
|
||
check_http "BUG1: chaos-slowquery sleep_sec=string → должен 200" "$c" "200"
|
||
|
||
c=$(http_code "chaos-bigpayload" '{"size_kb": "не_число"}')
|
||
check_http "BUG2: chaos-bigpayload size_kb=string → должен 200" "$c" "200"
|
||
|
||
c=$(http_code "py-retry-writer" '{"n": "не_число"}')
|
||
check_http "BUG3: py-retry-writer n=string → должен 200" "$c" "200"
|
||
|
||
c=$(http_code "pg-delete-old" '{"older_than_min": "не_число"}')
|
||
check_http "BUG4: pg-delete-old older_than_min=string → должен 200" "$c" "200"
|
||
|
||
c=$(http_code "chaos-slowquery" '{"sleep_sec": null}')
|
||
check_http "BUG5: chaos-slowquery sleep_sec=null → должен 200" "$c" "200"
|
||
|
||
c=$(http_code "chaos-bigpayload" '{"size_kb": null}')
|
||
check_http "BUG6: chaos-bigpayload size_kb=null → должен 200" "$c" "200"
|
||
|
||
c=$(http_code "chaos-bigpayload" '{"size_kb": -999}')
|
||
check_http "BUG7: chaos-bigpayload size_kb=-999 (negative) → должен 200" "$c" "200"
|
||
|
||
c=$(http_code "chaos-slowquery" '{"sleep_sec": -5}')
|
||
check_http "BUG8: chaos-slowquery sleep_sec=-5 → должен 200" "$c" "200"
|
||
|
||
c=$(http_code "chaos-slowquery" '{"sleep_sec": 99999}')
|
||
check_http "BUG9: chaos-slowquery sleep_sec=99999 (huge) → должен 200" "$c" "200"
|
||
|
||
c=$(http_code "py-retry-writer" '{"n": -1}')
|
||
check_http "BUG10: py-retry-writer n=-1 (negative) → должен 200" "$c" "200"
|
||
|
||
c=$(http_code "py-retry-writer" '{"n": 99999}')
|
||
check_http "BUG11: py-retry-writer n=99999 (huge, capped) → должен 200" "$c" "200"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 2 — Data Integrity: реальное значение vs ожидаемое"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# js-pg-batch: n=0 должен вернуть inserted=0, а не 20 (баг: 0||20=20)
|
||
r=$(raw "js-pg-batch" '{"n": 0, "prefix": "bughunt-zero"}')
|
||
check_val "BUG12: js-pg-batch n=0 → inserted должен быть 0" "$r" ".inserted" "0"
|
||
|
||
# js-pg-batch: n=1 → ровно 1 строка
|
||
r=$(raw "js-pg-batch" '{"n": 1, "prefix": "bughunt-one"}')
|
||
check_val "SAFE: js-pg-batch n=1 → inserted=1" "$r" ".inserted" "1"
|
||
|
||
# js-pg-batch: n=200 (cap) → не больше 200
|
||
r=$(raw "js-pg-batch" '{"n": 9999, "prefix": "bughunt-cap"}')
|
||
i=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin)['inserted'])" 2>/dev/null || echo "-1")
|
||
check_gte "SAFE: js-pg-batch n=9999 → capped (inserted > 0)" "$i" 1
|
||
# inserted должно быть <= 200
|
||
TOTAL=$((TOTAL+1))
|
||
if (( i <= 200 )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-pg-batch n=9999 → capped <= 200 (got $i)"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG: js-pg-batch n=9999 → вставил $i строк (ожидали <= 200)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("js-pg-batch n=9999 → inserted=$i > 200")
|
||
fi
|
||
|
||
# pg-bulk-insert: точное соответствие n → inserted
|
||
for n_val in 1 10 100 499 500; do
|
||
r=$(raw "pg-bulk-insert" "{\"n\": $n_val, \"prefix\": \"bughunt-n$n_val\"}")
|
||
check_val "SAFE: pg-bulk-insert n=$n_val → inserted=$n_val" "$r" ".inserted" "$n_val"
|
||
done
|
||
|
||
# py-retry-writer с simulate_error: должен вернуть ok:true (retry работает)
|
||
r=$(raw "py-retry-writer" '{"n": 5, "simulate_error": true, "prefix": "bughunt-retry"}')
|
||
check_has "SAFE: py-retry-writer simulate_error=true → ok:true" "$r" '"ok": true'
|
||
attempts=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('attempts',0))" 2>/dev/null || echo "0")
|
||
check_gte "SAFE: py-retry-writer simulate_error=true → attempts >= 2" "$attempts" 2
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 3 — Semantic / Logic Bugs (ложный 200, неверные данные)"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# BUG: pg-search с "_" в query — _ это LIKE wildcard, не экранируется.
|
||
# Вставляем уникальную строку без подчёркивания, ищем с _ → не должна найтись.
|
||
UNIQUE_NOUNDERSCORE="bughunt-no-underscore-$(date +%s%N)"
|
||
raw "pg-bulk-insert" "{\"n\": 1, \"prefix\": \"$UNIQUE_NOUNDERSCORE\"}" >/dev/null
|
||
# Поиск exact строки — должна найтись
|
||
r=$(raw "pg-search" "{\"query\": \"$UNIQUE_NOUNDERSCORE\", \"limit\": 10}")
|
||
cnt=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "0")
|
||
check_gte "SAFE: pg-search exact match найдена" "$cnt" 1
|
||
|
||
# Теперь создаём строку с дефисом в mid позиции: "aXb" где X — любой char.
|
||
# Ищем с _ (wildcard): "a_b" должно совпадать. Это НЕ баг если мы ищем wildcard.
|
||
# Реальный баг: ESCAPE не поддерживается, пользователь не может искать literal "_".
|
||
# Проверяем: строка "test-literal-underscore_here" ищем по query="literal_underscore_here"
|
||
# Без экраниравания: _ = любой символ → совпадёт AND с "literaXunderscoreYhere" тоже.
|
||
EXACT_TITLE="bughunt-exact-$(date +%s%N)"
|
||
raw "pg-upsert" "{\"title\": \"${EXACT_TITLE}\"}" >/dev/null
|
||
# Поиск с подчёркиванием вместо дефиса в этой строке — совпадать НЕ должно с точным матчем
|
||
# но совпадёт из-за ILIKE wildcard `_`
|
||
UNDER_QUERY="${EXACT_TITLE//-/_}" # заменяем дефисы на подчёркивания
|
||
r=$(raw "pg-search" "{\"query\": \"$UNDER_QUERY\", \"limit\": 100}")
|
||
underscore_count=$(echo "$r" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('count',0))" 2>/dev/null || echo "0")
|
||
# Если count >> 1, значит _ матчит что попало (wildcard)
|
||
echo " pg-search с query='${UNDER_QUERY:0:30}...' вернул $underscore_count строк (если > 1 — это баг wildcard)"
|
||
TOTAL=$((TOTAL+1))
|
||
# Должен найти ТОЛЬКО нашу строку (count=1), но без экранирования найдёт много
|
||
if (( underscore_count == 1 )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search underscore matches only exact row"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG13: pg-search '_' is unescaped LIKE wildcard → matches $underscore_count rows instead of 1"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("pg-search: '_' is unescaped LIKE wildcard (got $underscore_count rows)")
|
||
fi
|
||
|
||
# BUG: pg-delete-old — параметр НАЗЫВАЕТСЯ older_than_min, но в chaos_marathon шлём older_than_minutes.
|
||
# Шлём неправильный ключ и правильный — результаты должны различаться.
|
||
# older_than_minutes=1 → функция игнорирует, использует default (60 мин) → ничего не удалится из только что вставленных
|
||
# older_than_min=0 → capped to 1 → удалит строки старше 1 мин (ну, только что вставленные > 1 мин назад)
|
||
# Проверяем: older_than_minutes=0 → использует default 60 → параметр проигнорирован → bug
|
||
|
||
# Вставляем свежую строку, пытаемся удалить с older_than_minutes=0 (неверный ключ)
|
||
FRESH_TITLE="bughunt-del-$(date +%s%N)"
|
||
ins_r=$(raw "pg-bulk-insert" "{\"n\": 3, \"prefix\": \"$FRESH_TITLE\"}")
|
||
# Ждём немного, затем пытаемся удалить по неправильному ключу
|
||
sleep 1
|
||
r=$(raw "pg-delete-old" "{\"older_than_minutes\": 0}")
|
||
deleted_wrong_key=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deleted',0))" 2>/dev/null || echo "?")
|
||
r2=$(raw "pg-delete-old" "{\"older_than_min\": 99999}")
|
||
deleted_right_key=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deleted',0))" 2>/dev/null || echo "?")
|
||
echo " pg-delete-old older_than_minutes=0: deleted=$deleted_wrong_key (использовался default 60min)"
|
||
echo " pg-delete-old older_than_min=99999: deleted=$deleted_right_key (правильный ключ — удалило всё старое)"
|
||
# Если с неверным ключом deleted > 0 при очень маленьком значении — значит параметр работает.
|
||
# Если с right key deleted больше — это подтверждает что wrong key игнорировался.
|
||
TOTAL=$((TOTAL+1))
|
||
if [[ "$deleted_right_key" =~ ^[0-9]+$ ]] && (( deleted_right_key >= 0 )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] INFO: pg-delete-old right key works (deleted=$deleted_right_key)"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] pg-delete-old right key failed: got $deleted_right_key"
|
||
FAIL=$((FAIL+1))
|
||
fi
|
||
|
||
# BUG: pg-counter prefix="%" → LIKE "%%%" → считает все строки (wildcard leak)
|
||
r_all=$(raw "pg-counter" '{}')
|
||
total_all=$(echo "$r_all" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
||
r_pct=$(raw "pg-counter" '{"prefix": "%"}')
|
||
total_pct=$(echo "$r_pct" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
||
echo " pg-counter prefix='': total=$total_all | pg-counter prefix='%': total=$total_pct"
|
||
TOTAL=$((TOTAL+1))
|
||
# Если оба возвращают одно число — значит % матчит все строки → баг wildcard
|
||
if [[ "$total_all" == "$total_pct" ]] && (( total_all > 0 )); then
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG14: pg-counter prefix='%' counts ALL rows ($total_pct) — % is unescaped LIKE wildcard"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("pg-counter: prefix='%' is unescaped LIKE wildcard (same as no prefix)")
|
||
else
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-counter prefix='%' gives different count than no-prefix"
|
||
PASS=$((PASS+1))
|
||
fi
|
||
|
||
# BUG: pg-search limit=0 — clamp max(1, min(0,100)) = 1, не 0.
|
||
# Юзер просит 0 строк но получает 1.
|
||
r=$(raw "pg-search" '{"query": "", "limit": 0}')
|
||
limit_got=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('limit',0))" 2>/dev/null || echo "-1")
|
||
echo " pg-search limit=0 → вернул limit=$limit_got в ответе"
|
||
TOTAL=$((TOTAL+1))
|
||
if [[ "$limit_got" == "0" ]]; then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search limit=0 → limit=0 in response"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] INFO15: pg-search limit=0 → silently changed to $limit_got (min clamp = 1, not 0)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("pg-search: limit=0 silently becomes $limit_got (user wants 0 rows, gets $limit_got)")
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 4 — pg-upsert: idempotency + action field correctness"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# Первый вызов → action=inserted
|
||
UPSERT_KEY="bughunt-upsert-$(date +%s%N)"
|
||
r1=$(raw "pg-upsert" "{\"title\": \"$UPSERT_KEY\"}")
|
||
action1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?")
|
||
check_val "SAFE: pg-upsert первый вызов → action=inserted" "$r1" ".action" "inserted"
|
||
|
||
# Второй вызов → action=updated
|
||
r2=$(raw "pg-upsert" "{\"title\": \"$UPSERT_KEY\"}")
|
||
action2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?")
|
||
check_val "SAFE: pg-upsert второй вызов (same title) → action=updated" "$r2" ".action" "updated"
|
||
|
||
# ID должен совпадать
|
||
id1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null || echo "?1")
|
||
id2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null || echo "?2")
|
||
TOTAL=$((TOTAL+1))
|
||
if [[ "$id1" == "$id2" ]] && [[ "$id1" != "?" ]]; then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-upsert same title → same id ($id1)"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG16: pg-upsert same title → different ids ($id1 vs $id2)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("pg-upsert: same title yields different ids ($id1 vs $id2)")
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 5 — js-idempotent: concurrent same key"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# 5 параллельных вызовов с одним ключом → должен быть 1 created, 4 existing
|
||
IDEM_KEY="bughunt-idem-concurrent-$(date +%s%N)"
|
||
declare -a IDEM_RESULTS=()
|
||
for i in 1 2 3 4 5; do
|
||
r=$(raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}") &
|
||
IDEM_RESULTS+=($!)
|
||
done
|
||
wait
|
||
# Перезапустим последовательно чтобы собрать результаты
|
||
actions=()
|
||
for i in 1 2 3 4 5; do
|
||
r=$(raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}")
|
||
a=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?")
|
||
actions+=("$a")
|
||
done
|
||
|
||
created_count=0; existing_count=0
|
||
for a in "${actions[@]}"; do
|
||
[[ "$a" == "created" ]] && created_count=$((created_count+1))
|
||
[[ "$a" == "existing" ]] && existing_count=$((existing_count+1))
|
||
done
|
||
echo " js-idempotent key же 5× последовательно: created=$created_count, existing=$existing_count"
|
||
TOTAL=$((TOTAL+1))
|
||
# Первый должен быть created, остальные existing (с учётом что первый вызов в параллельном блоке уже создал)
|
||
# Теперь все 5 последовательных должны быть existing
|
||
if (( existing_count == 5 )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-idempotent 5× same key → все existing"
|
||
PASS=$((PASS+1))
|
||
elif (( created_count == 1 && existing_count == 4 )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-idempotent 5× same key → 1 created + 4 existing"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG17: js-idempotent same key → created=$created_count existing=$existing_count (ожидали 0-1 created, 4-5 existing)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("js-idempotent: 5× same key → created=$created_count, existing=$existing_count")
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 6 — go-pg-race: workers=0 div-by-zero"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
r=$(raw "go-pg-race" '{"workers": 0, "n_per_worker": 5}')
|
||
c=$(http_code "go-pg-race" '{"workers": 0, "n_per_worker": 5}')
|
||
check_http "SAFE: go-pg-race workers=0 → 200 (не div-by-zero)" "$c" "200"
|
||
|
||
# ops_per_sec при workers=0 inserted=0 должен быть 0 или Inf — проверяем что не NaN/invalid JSON
|
||
TOTAL=$((TOTAL+1))
|
||
if echo "$r" | python3 -c "import json,sys; json.load(sys.stdin); print('valid_json')" 2>/dev/null | grep -q valid_json; then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-pg-race workers=0 → valid JSON"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG18: go-pg-race workers=0 → invalid JSON (div-by-zero → Inf/NaN)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("go-pg-race: workers=0 → invalid JSON response")
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 7 — Crash functions: параметры управляют crash-ом"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# stress-go-nil: crash=false → 200 (не должен падать)
|
||
c=$(http_code "stress-go-nil" '{"crash": false}')
|
||
check_http "SAFE: stress-go-nil crash=false → 200" "$c" "200"
|
||
|
||
# stress-go-nil: crash=true (default) → 500
|
||
c=$(http_code "stress-go-nil" '{"crash": true}')
|
||
check_http "SAFE: stress-go-nil crash=true → 500" "$c" "500"
|
||
|
||
# stress-divzero: d=0 → 500
|
||
c=$(http_code "stress-divzero" '{"n": 10, "d": 0}')
|
||
check_http "SAFE: stress-divzero d=0 → 500" "$c" "500"
|
||
|
||
# stress-divzero: d=2 → 200
|
||
c=$(http_code "stress-divzero" '{"n": 10, "d": 2}')
|
||
check_http "SAFE: stress-divzero d=2 → 200" "$c" "200"
|
||
|
||
r=$(raw "stress-divzero" '{"n": 10, "d": 2}')
|
||
check_has "SAFE: stress-divzero d=2 → result in response" "$r" "result"
|
||
|
||
# stress-bigloop: n=1000000 (большое) → должен вернуть 200
|
||
c=$(http_code "stress-bigloop" '{"n": 1000000}')
|
||
check_http "SAFE: stress-bigloop n=1000000 → 200" "$c" "200"
|
||
|
||
# stress-go-fast: n=0 → должен вернуть 200 (factorial(0) = 1)
|
||
c=$(http_code "stress-go-fast" '{"n": 0}')
|
||
check_http "SAFE: stress-go-fast n=0 → 200" "$c" "200"
|
||
|
||
# stress-go-fast: n=20 (cap) → factorial(20) не переполнение?
|
||
r=$(raw "stress-go-fast" '{"n": 20}')
|
||
check_has "SAFE: stress-go-fast n=20 → factorial in response" "$r" "factorial"
|
||
|
||
# stress-go-fast: n=21 → capped to 20 (проверяем что cap работает)
|
||
r=$(raw "stress-go-fast" '{"n": 21}')
|
||
n_got=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('n',0))" 2>/dev/null || echo "-1")
|
||
TOTAL=$((TOTAL+1))
|
||
if (( n_got <= 20 )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: stress-go-fast n=21 → capped to $n_got (<= 20)"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG: stress-go-fast n=21 → not capped (n=$n_got)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("stress-go-fast: n=21 not capped (got n=$n_got)")
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 8 — pg-counter: счёт соответствует реально inserted"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
PREFIX_TEST="bughunt-count-$(date +%s%N)"
|
||
# Получаем текущий счётчик с этим prefix
|
||
r_before=$(raw "pg-counter" "{\"prefix\": \"$PREFIX_TEST\"}")
|
||
cnt_before=$(echo "$r_before" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
||
|
||
# Вставляем ровно 7 строк
|
||
raw "pg-bulk-insert" "{\"n\": 7, \"prefix\": \"$PREFIX_TEST\"}" >/dev/null
|
||
|
||
# Считаем снова
|
||
r_after=$(raw "pg-counter" "{\"prefix\": \"$PREFIX_TEST\"}")
|
||
cnt_after=$(echo "$r_after" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
||
|
||
inserted_delta=$(( cnt_after - cnt_before ))
|
||
echo " pg-counter prefix='$PREFIX_TEST': before=$cnt_before, after=$cnt_after, delta=$inserted_delta (ожидаем 7)"
|
||
TOTAL=$((TOTAL+1))
|
||
if (( inserted_delta == 7 )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-counter правильно считает: delta=$inserted_delta"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG19: pg-counter delta=$inserted_delta ≠ 7 (prefix wildcard или баг в счёте)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("pg-counter: inserted 7, delta=$inserted_delta")
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 9 — pg-search: pagination correctness"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
PREFIX_SEARCH="bughunt-search-$(date +%s%N)"
|
||
# Вставляем ровно 25 строк
|
||
raw "pg-bulk-insert" "{\"n\": 25, \"prefix\": \"$PREFIX_SEARCH\"}" >/dev/null
|
||
sleep 0.5
|
||
|
||
# Page 1: offset=0 limit=10 → count=10
|
||
r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 0}")
|
||
count_p1=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1")
|
||
total_p1=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "-1")
|
||
check_val "SAFE: pg-search page1 count=10" "$r" ".count" "10"
|
||
check_gte "SAFE: pg-search total >= 25" "$total_p1" 25
|
||
|
||
# Page 3: offset=20 limit=10 → count=5 (строк 21-25)
|
||
r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 20}")
|
||
count_p3=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1")
|
||
echo " pg-search page3 (offset=20, limit=10): count=$count_p3 (ожидаем 5)"
|
||
TOTAL=$((TOTAL+1))
|
||
# Учитываем что могут быть другие строки с этим prefix
|
||
if [[ "$count_p3" =~ ^[0-9]+$ ]] && (( count_p3 > 0 && count_p3 <= 10 )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search page3 count=$count_p3 (допустимо)"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG20: pg-search page3 count=$count_p3 (expected 1-10)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("pg-search: page3 count=$count_p3 out of range")
|
||
fi
|
||
|
||
# offset > total → count=0
|
||
r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 999999}")
|
||
count_over=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1")
|
||
check_val "SAFE: pg-search offset>total → count=0" "$r" ".count" "0"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 10 — pg-dedup: idempotency (повторный вызов безопасен)"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# Сначала удаляем все дубли
|
||
raw "pg-dedup" '{"dry_run": false}' >/dev/null
|
||
|
||
# dry_run=true → deleted всегда = 0
|
||
r=$(raw "pg-dedup" '{"dry_run": true}')
|
||
check_val "SAFE: pg-dedup dry_run=true → deleted=0" "$r" ".deleted" "0"
|
||
|
||
# Создаём дублей: вставляем одно и то же через bulk (уникальные title), затем уpsert одно и то же
|
||
DUP_TITLE="bughunt-dup-$(date +%s%N)"
|
||
raw "pg-upsert" "{\"title\": \"${DUP_TITLE}\"}" >/dev/null
|
||
# INSERT прямой дубль через bulk — но у него нет механизма вставки дублей...
|
||
# Используем pg-counter + pg-search чтобы найти дубли что уже есть от других тестов
|
||
r=$(raw "pg-dedup" '{"dry_run": true}')
|
||
dupes=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('duplicates_found',0))" 2>/dev/null || echo "0")
|
||
echo " pg-dedup dry_run: duplicates_found=$dupes"
|
||
|
||
# Запускаем dedup дважды — второй раз должен найти 0 дублей
|
||
raw "pg-dedup" '{"dry_run": false}' >/dev/null
|
||
r2=$(raw "pg-dedup" '{"dry_run": false}')
|
||
dupes2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('duplicates_found',0))" 2>/dev/null || echo "0")
|
||
TOTAL=$((TOTAL+1))
|
||
if [[ "$dupes2" == "0" ]]; then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-dedup повторный вызов → duplicates_found=0 (идемпотентен)"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG21: pg-dedup повторный вызов → duplicates_found=$dupes2 ≠ 0"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("pg-dedup: не идемпотентен — второй вызов нашёл $dupes2 дублей")
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 11 — chaos-echo: крайние случаи"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# Пустой JSON {} → echo должен вернуть echo:{}, keys:[], size_bytes:2
|
||
r=$(raw "chaos-echo" '{}')
|
||
check_has "SAFE: chaos-echo {} → echo in response" "$r" '"echo"'
|
||
check_val "SAFE: chaos-echo {} → keys=[](empty_list len=0)" "$r" ".size_bytes" "2"
|
||
|
||
# Очень глубоко вложенный JSON
|
||
r=$(raw "chaos-echo" '{"a": {"b": {"c": {"d": {"e": "deep"}}}}}')
|
||
c=$(http_code "chaos-echo" '{"a": {"b": {"c": {"d": {"e": "deep"}}}}}')
|
||
check_http "SAFE: chaos-echo deeply nested → 200" "$c" "200"
|
||
|
||
# Массив вместо объекта (некоторые функции падают)
|
||
c=$(http_code "chaos-echo" '[1, 2, 3]')
|
||
check_http "SAFE: chaos-echo array input → 200" "$c" "200"
|
||
|
||
# Булево значение вместо объекта
|
||
c=$(http_code "chaos-echo" 'true')
|
||
check_http "SAFE: chaos-echo true input → 200" "$c" "200"
|
||
|
||
# Число вместо объекта
|
||
c=$(http_code "chaos-echo" '42')
|
||
check_http "SAFE: chaos-echo number input → 200" "$c" "200"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 12 — go-counter-atomic: invocation_n растёт"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
r1=$(raw "go-counter-atomic" '{}')
|
||
n1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('invocation_n','?'))" 2>/dev/null || echo "?")
|
||
r2=$(raw "go-counter-atomic" '{}')
|
||
n2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('invocation_n','?'))" 2>/dev/null || echo "?")
|
||
echo " go-counter-atomic: call1 invocation_n=$n1, call2 invocation_n=$n2"
|
||
TOTAL=$((TOTAL+1))
|
||
if [[ "$n1" =~ ^[0-9]+$ ]] && [[ "$n2" =~ ^[0-9]+$ ]] && (( n2 > n1 )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-counter-atomic invocation_n растёт ($n1 → $n2)"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG22: go-counter-atomic invocation_n НЕ растёт ($n1 → $n2)"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("go-counter-atomic: invocation_n не растёт ($n1 → $n2)")
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 13 — go-pg-race: все inserted = workers × n_per_worker"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
r=$(raw "go-pg-race" '{"workers": 4, "n_per_worker": 10}')
|
||
ins=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('inserted',0))" 2>/dev/null || echo "0")
|
||
errs=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('errors',0))" 2>/dev/null || echo "-1")
|
||
echo " go-pg-race workers=4 n_per_worker=10: inserted=$ins, errors=$errs (ожидаем inserted=40, errors=0)"
|
||
TOTAL=$((TOTAL+1))
|
||
if [[ "$ins" == "40" ]] && [[ "$errs" == "0" ]]; then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-pg-race 4×10 = 40 inserted, 0 errors"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG23: go-pg-race 4×10: inserted=$ins (≠40), errors=$errs"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("go-pg-race: workers=4 n_per_worker=10 → inserted=$ins errors=$errs")
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "БЛОК 14 — pg-bulk-insert: first_id реально существует в БД"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
PREFIX_VER="bughunt-verify-$(date +%s%N)"
|
||
r=$(raw "pg-bulk-insert" "{\"n\": 5, \"prefix\": \"$PREFIX_VER\"}")
|
||
first_id=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('first_id','null'))" 2>/dev/null || echo "null")
|
||
echo " pg-bulk-insert n=5 → first_id=$first_id"
|
||
|
||
# Проверяем что эта строка находится через pg-search
|
||
sleep 0.3
|
||
r_search=$(raw "pg-search" "{\"query\": \"$PREFIX_VER\", \"limit\": 10}")
|
||
found_count=$(echo "$r_search" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "0")
|
||
TOTAL=$((TOTAL+1))
|
||
if (( found_count >= 5 )); then
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-bulk-insert n=5 → pg-search находит $found_count строк"
|
||
PASS=$((PASS+1))
|
||
else
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG24: pg-bulk-insert n=5 → pg-search нашёл только $found_count строк"
|
||
FAIL=$((FAIL+1))
|
||
BUGS_FOUND+=("pg-bulk-insert: inserted 5 but pg-search found only $found_count")
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ИТОГИ BUG HUNTER"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
echo ""
|
||
echo -e "${YELLOW}PASS: $PASS / $TOTAL FAIL: $FAIL${NC}"
|
||
echo ""
|
||
|
||
if (( ${#BUGS_FOUND[@]} > 0 )); then
|
||
echo -e "${RED}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${RED}║ НАЙДЕНО БАГОВ: ${#BUGS_FOUND[@]} ║${NC}"
|
||
echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||
for bug in "${BUGS_FOUND[@]}"; do
|
||
echo -e " ${RED}✗${NC} $bug"
|
||
done
|
||
else
|
||
echo -e "${GREEN}╔══════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ БАГОВ НЕ НАЙДЕНО. Всё чисто. ✓ ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════╝${NC}"
|
||
fi
|
||
echo ""
|
||
|
||
exit $(( FAIL > 0 ? 1 : 0 ))
|