diff --git a/ops/redis_idempotency_smoke.sh b/ops/redis_idempotency_smoke.sh new file mode 100755 index 00000000..8051bae0 --- /dev/null +++ b/ops/redis_idempotency_smoke.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# ops/redis_idempotency_smoke.sh — A/B smoke for Redis idempotency +# +# Usage: +# bash ops/redis_idempotency_smoke.sh +# BFF_A=http://127.0.0.1:8002 BFF_B=http://127.0.0.1:8003 bash ops/redis_idempotency_smoke.sh +# +# Optional args (override env): +# $1 = BFF_A +# $2 = BFF_B +# $3 = AGENT_ID +# $4 = NODE_ID +# +# Exit codes: +# 0 = PASS (distributed replay works) +# 1 = FAIL +# 2 = prerequisites missing + +set -euo pipefail + +BFF_A="${BFF_A:-${1:-http://127.0.0.1:8002}}" +BFF_B="${BFF_B:-${2:-http://127.0.0.1:8003}}" +AGENT_ID="${AGENT_ID:-${3:-sofiia}}" +NODE_ID="${NODE_ID:-${4:-NODA2}}" + +# Optional auth header if environment needs it: +# export AUTH_HEADER_VALUE="Bearer " or "ApiKey " +AUTH_HEADER_VALUE="${AUTH_HEADER_VALUE:-}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +_pass() { echo -e "${GREEN}PASS${NC}: $1"; } +_fail() { echo -e "${RED}FAIL${NC}: $1"; } +_warn() { echo -e "${YELLOW}WARN${NC}: $1"; } + +for cmd in curl python3; do + command -v "$cmd" >/dev/null 2>&1 || { echo "Missing prerequisite: $cmd"; exit 2; } +done + +if command -v jq >/dev/null 2>&1; then + HAS_JQ=1 +else + HAS_JQ=0 + _warn "jq not found, using python3 JSON fallback" +fi + +json_get() { + local json="$1" + local path="$2" + if [ "$HAS_JQ" = "1" ]; then + echo "$json" | jq -r "$path // empty" 2>/dev/null || true + else + python3 - "$path" <<'PYEOF' <<<"$json" +import json, sys +path = sys.argv[1].strip() +data = json.load(sys.stdin) +if path.startswith("."): + path = path[1:] +if not path: + print("") + raise SystemExit(0) +cur = data +for part in path.split("."): + if part == "": + continue + if not isinstance(cur, dict): + print("") + raise SystemExit(0) + cur = cur.get(part) + if cur is None: + print("") + raise SystemExit(0) +if isinstance(cur, bool): + print("true" if cur else "false") +elif isinstance(cur, (dict, list)): + print(json.dumps(cur, ensure_ascii=True)) +else: + print(str(cur)) +PYEOF + fi +} + +post_json() { + local url="$1" + local body="$2" + local out + if [ -n "$AUTH_HEADER_VALUE" ]; then + out=$(curl -sS -w $'\n__HTTP__%{http_code}' \ + -X POST "$url" \ + -H "Content-Type: application/json" \ + -H "Authorization: ${AUTH_HEADER_VALUE}" \ + -d "$body" || true) + else + out=$(curl -sS -w $'\n__HTTP__%{http_code}' \ + -X POST "$url" \ + -H "Content-Type: application/json" \ + -d "$body" || true) + fi + HTTP_CODE=$(echo "$out" | awk -F'__HTTP__' 'NF>1{print $2}' | tail -n1 | tr -d '\r') + HTTP_BODY=$(echo "$out" | awk 'BEGIN{p=1} /__HTTP__/{p=0} {if(p)print}') +} + +post_json_with_idem() { + local url="$1" + local body="$2" + local idem_key="$3" + local out + if [ -n "$AUTH_HEADER_VALUE" ]; then + out=$(curl -sS -w $'\n__HTTP__%{http_code}' \ + -X POST "$url" \ + -H "Content-Type: application/json" \ + -H "Authorization: ${AUTH_HEADER_VALUE}" \ + -H "Idempotency-Key: ${idem_key}" \ + -d "$body" || true) + else + out=$(curl -sS -w $'\n__HTTP__%{http_code}' \ + -X POST "$url" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: ${idem_key}" \ + -d "$body" || true) + fi + HTTP_CODE=$(echo "$out" | awk -F'__HTTP__' 'NF>1{print $2}' | tail -n1 | tr -d '\r') + HTTP_BODY=$(echo "$out" | awk 'BEGIN{p=1} /__HTTP__/{p=0} {if(p)print}') +} + +echo "Redis Idempotency A/B Smoke" +echo " BFF_A = $BFF_A" +echo " BFF_B = $BFF_B" +echo " AGENT_ID = $AGENT_ID" +echo " NODE_ID = $NODE_ID" +echo " $(date -u '+%Y-%m-%dT%H:%M:%SZ')" + +EXT_REF="redis-smoke-$(date +%s)-$RANDOM" +IDEMPOTENCY_KEY="redis-smoke-key-$(date +%s)-$RANDOM" + +post_json "$BFF_A/api/chats" "{\"agent_id\":\"${AGENT_ID}\",\"node_id\":\"${NODE_ID}\",\"source\":\"web\",\"external_chat_ref\":\"${EXT_REF}\"}" +if [ "${HTTP_CODE:-000}" != "200" ]; then + _fail "create chat on A failed: HTTP ${HTTP_CODE:-000}" + echo "$HTTP_BODY" + exit 1 +fi +CHAT_ID=$(json_get "$HTTP_BODY" ".chat.chat_id") +if [ -z "$CHAT_ID" ]; then + _fail "chat_id missing in create-chat response" + echo "$HTTP_BODY" + exit 1 +fi +_pass "chat created: $CHAT_ID" + +post_json_with_idem "$BFF_A/api/chats/${CHAT_ID}/send" "{\"text\":\"redis-smoke-a\"}" "$IDEMPOTENCY_KEY" +if [ "${HTTP_CODE:-000}" != "200" ]; then + _fail "first keyed send on A failed: HTTP ${HTTP_CODE:-000}" + echo "$HTTP_BODY" + exit 1 +fi +A_BODY="$HTTP_BODY" +_pass "first keyed send via A completed" + +post_json_with_idem "$BFF_B/api/chats/${CHAT_ID}/send" "{\"text\":\"redis-smoke-b\"}" "$IDEMPOTENCY_KEY" +if [ "${HTTP_CODE:-000}" != "200" ]; then + _fail "replay send on B failed: HTTP ${HTTP_CODE:-000}" + echo "$HTTP_BODY" + exit 1 +fi +B_BODY="$HTTP_BODY" +_pass "replay send via B completed" + +A_MSG_ID=$(json_get "$A_BODY" ".message.message_id") +B_MSG_ID=$(json_get "$B_BODY" ".message.message_id") +B_REPLAYED=$(json_get "$B_BODY" ".idempotency.replayed") + +echo "A.message_id = ${A_MSG_ID:-}" +echo "B.message_id = ${B_MSG_ID:-}" +echo "B.replayed = ${B_REPLAYED:-}" + +if [ -z "$A_MSG_ID" ] || [ -z "$B_MSG_ID" ]; then + _fail "message_id missing in A or B response" + echo "A body: $A_BODY" + echo "B body: $B_BODY" + exit 1 +fi + +if [ "$A_MSG_ID" != "$B_MSG_ID" ]; then + _fail "message_id mismatch (A != B)" + echo "A body: $A_BODY" + echo "B body: $B_BODY" + exit 1 +fi + +if [ "$B_REPLAYED" != "true" ]; then + _fail "B response is not replayed=true" + echo "B body: $B_BODY" + exit 1 +fi + +_pass "distributed replay verified (same message_id + replayed=true on B)" +exit 0