hi-server/script/family_scenarios_regression.sh

395 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
EMAIL_A="${EMAIL_A:-family_a_$(date +%s)@example.com}"
DEVICE_A="${DEVICE_A:-qa-device-a-$(date +%s)}"
DEVICE_B="${DEVICE_B:-qa-device-b-$(date +%s)}"
DEVICE_C="${DEVICE_C:-qa-device-c-$(date +%s)}"
EMAIL_LOGIN_CODE="${EMAIL_LOGIN_CODE:-202511}"
SEND_CODE_RETRY_WAIT="${SEND_CODE_RETRY_WAIT:-61}"
SEND_CODE_MAX_RETRY="${SEND_CODE_MAX_RETRY:-8}"
VERIFY_CODE_PROVIDER="${VERIFY_CODE_PROVIDER:-}"
VERIFY_CODE_OVERRIDE="${VERIFY_CODE_OVERRIDE:-}"
REGISTER_CODE_OVERRIDE="${REGISTER_CODE_OVERRIDE:-}"
SECURITY_CODE_OVERRIDE="${SECURITY_CODE_OVERRIDE:-}"
ALLOW_INTERACTIVE_INPUT="${ALLOW_INTERACTIVE_INPUT:-auto}"
ENABLE_REDIS_CODE_FALLBACK="${ENABLE_REDIS_CODE_FALLBACK:-1}"
REDIS_CLI_BIN="${REDIS_CLI_BIN:-redis-cli}"
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
REDIS_PORT="${REDIS_PORT:-6379}"
REDIS_DB="${REDIS_DB:-0}"
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
BIND_VERIFY_RETRY="${BIND_VERIFY_RETRY:-1}"
bold() { printf "\033[1m%s\033[0m\n" "$*"; }
info() { printf "[INFO] %s\n" "$*" >&2; }
warn() { printf "[WARN] %s\n" "$*" >&2; }
fail() { printf "[FAIL] %s\n" "$*" >&2; exit 1; }
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "缺少命令: $1"
}
json_code() { jq -r '.code // ""' <<<"$1"; }
json_msg() { jq -r '.msg // ""' <<<"$1"; }
can_prompt_input() {
if [[ "$ALLOW_INTERACTIVE_INPUT" == "1" ]]; then
return 0
fi
if [[ "$ALLOW_INTERACTIVE_INPUT" == "0" ]]; then
return 1
fi
[[ -t 0 ]]
}
redis_get_key_raw() {
local key="$1"
command -v "$REDIS_CLI_BIN" >/dev/null 2>&1 || return 1
local -a args
args=("$REDIS_CLI_BIN" -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$REDIS_DB" --raw)
if [[ -n "$REDIS_PASSWORD" ]]; then
args+=(-a "$REDIS_PASSWORD")
fi
"${args[@]}" GET "$key" 2>/dev/null || return 1
}
get_verify_code_from_redis() {
local email="$1"
local typ="$2"
local scene key raw code
if [[ "$ENABLE_REDIS_CODE_FALLBACK" != "1" ]]; then
return 1
fi
case "$typ" in
1) scene="register" ;;
2) scene="security" ;;
*) scene="unknown" ;;
esac
key="auth:verify:email:${scene}:${email}"
raw="$(redis_get_key_raw "$key" || true)"
[[ -n "$raw" ]] || return 1
code="$(jq -r '.code // ""' <<<"$raw" 2>/dev/null || true)"
[[ -n "$code" && "$code" != "null" ]] || return 1
printf "%s" "$code"
return 0
}
api_request() {
local method="$1"
local path="$2"
local body="${3:-}"
local token="${4:-}"
local -a args
args=(-sS -X "$method" "${BASE_URL}${path}" -H "Content-Type: application/json")
if [[ -n "$token" ]]; then
args+=(-H "Authorization: ${token}")
fi
if [[ "$method" != "GET" ]]; then
args+=(-d "$body")
fi
curl "${args[@]}"
}
assert_success() {
local resp="$1"
local step="$2"
local code
code="$(json_code "$resp")"
if [[ "$code" != "200" ]]; then
fail "${step} 失败, code=${code}, msg=$(json_msg "$resp"), resp=${resp}"
fi
}
token_or_keep() {
local resp="$1"
local fallback="$2"
local token
token="$(jq -r '.data.token // ""' <<<"$resp")"
if [[ -n "$token" && "$token" != "null" ]]; then
printf "%s" "$token"
else
printf "%s" "$fallback"
fi
}
device_login() {
local identifier="$1"
local user_agent="$2"
local payload resp token
payload="$(jq -nc --arg identifier "$identifier" --arg user_agent "$user_agent" '{identifier:$identifier,user_agent:$user_agent}')"
resp="$(api_request "POST" "/v1/auth/login/device" "$payload")"
assert_success "$resp" "设备登录(${identifier})"
token="$(jq -r '.data.token // ""' <<<"$resp")"
[[ -n "$token" && "$token" != "null" ]] || fail "设备登录(${identifier})返回空 token: ${resp}"
printf "%s" "$token"
}
email_login() {
local email="$1"
local code="$2"
local payload resp token
payload="$(jq -nc --arg email "$email" --arg code "$code" --arg identifier "web-login-$(date +%s)" --arg user_agent "qa-web" '{email:$email,code:$code,identifier:$identifier,user_agent:$user_agent}')"
resp="$(api_request "POST" "/v1/auth/login/email" "$payload")"
assert_success "$resp" "邮箱登录(${email})"
token="$(jq -r '.data.token // ""' <<<"$resp")"
[[ -n "$token" && "$token" != "null" ]] || fail "邮箱登录(${email})返回空 token: ${resp}"
printf "%s" "$token"
}
send_code_with_retry() {
local email="$1"
local typ="$2"
local payload resp code retry
payload="$(jq -nc --arg email "$email" --argjson type "$typ" '{email:$email,type:$type}')"
retry=0
while true; do
resp="$(api_request "POST" "/v1/common/send_code" "$payload")"
code="$(json_code "$resp")"
if [[ "$code" == "200" ]]; then
printf "%s" "$resp"
return 0
fi
if [[ "$code" == "401" ]]; then
retry=$((retry + 1))
if (( retry > SEND_CODE_MAX_RETRY )); then
fail "发送验证码达到重试上限, email=${email}, type=${typ}, resp=${resp}"
fi
warn "验证码发送频率限制,等待 ${SEND_CODE_RETRY_WAIT}s 后重试(${retry}/${SEND_CODE_MAX_RETRY})..."
sleep "${SEND_CODE_RETRY_WAIT}"
continue
fi
fail "发送验证码失败, email=${email}, type=${typ}, code=${code}, msg=$(json_msg "$resp"), resp=${resp}"
done
}
get_verify_code() {
local email="$1"
local typ="$2"
local send_resp code
send_resp="$(send_code_with_retry "$email" "$typ")"
code="$(jq -r '.data.code // ""' <<<"$send_resp")"
if [[ -n "$code" && "$code" != "null" ]]; then
info "验证码已从接口返回(type=${typ})"
printf "%s" "$code"
return 0
fi
code="$(get_verify_code_from_redis "$email" "$typ" || true)"
if [[ -n "$code" ]]; then
info "验证码已从 Redis 回读(type=${typ})"
printf "%s" "$code"
return 0
fi
if [[ -n "$VERIFY_CODE_PROVIDER" ]]; then
code="$("$VERIFY_CODE_PROVIDER" "$email" "$typ" || true)"
if [[ -n "$code" ]]; then
info "验证码由 VERIFY_CODE_PROVIDER 提供(type=${typ})"
printf "%s" "$code"
return 0
fi
fi
if [[ "$typ" == "1" && -n "$REGISTER_CODE_OVERRIDE" ]]; then
info "使用 REGISTER_CODE_OVERRIDE 作为验证码(type=${typ})"
printf "%s" "$REGISTER_CODE_OVERRIDE"
return 0
fi
if [[ "$typ" == "2" && -n "$SECURITY_CODE_OVERRIDE" ]]; then
info "使用 SECURITY_CODE_OVERRIDE 作为验证码(type=${typ})"
printf "%s" "$SECURITY_CODE_OVERRIDE"
return 0
fi
if [[ -n "$VERIFY_CODE_OVERRIDE" ]]; then
info "使用 VERIFY_CODE_OVERRIDE 作为验证码(type=${typ})"
printf "%s" "$VERIFY_CODE_OVERRIDE"
return 0
fi
if ! can_prompt_input; then
fail "验证码不可自动获取(type=${typ}, email=${email})。请设置 VERIFY_CODE_PROVIDER 或 REGISTER_CODE_OVERRIDE/SECURITY_CODE_OVERRIDE或开启交互输入(ALLOW_INTERACTIVE_INPUT=1)"
fi
read -r -p "请输入邮箱 ${email} 的验证码(type=${typ})" code
[[ -n "$code" ]] || fail "验证码为空"
printf "%s" "$code"
}
bind_email_with_verification() {
local token="$1"
local email="$2"
local code="$3"
local payload resp
payload="$(jq -nc --arg email "$email" --arg code "$code" '{email:$email,code:$code}')"
resp="$(api_request "POST" "/v1/public/user/bind_email_with_verification" "$payload" "$token")"
printf "%s" "$resp"
}
bind_email_with_verification_auto() {
local token="$1"
local email="$2"
local typ="$3"
local max_retry="${4:-$BIND_VERIFY_RETRY}"
local attempt=0 code resp api_code
while true; do
code="$(get_verify_code "$email" "$typ")"
resp="$(bind_email_with_verification "$token" "$email" "$code")"
api_code="$(json_code "$resp")"
if [[ "$api_code" == "200" ]]; then
printf "%s" "$resp"
return 0
fi
if [[ "$api_code" == "70001" && "$attempt" -lt "$max_retry" ]]; then
warn "绑定邮箱验证码校验失败,重新获取验证码重试($((attempt+1))/${max_retry})..."
attempt=$((attempt + 1))
continue
fi
fail "绑定邮箱(${email}) 失败, code=${api_code}, msg=$(json_msg "$resp"), resp=${resp}"
done
}
get_devices() {
local token="$1"
local resp
resp="$(api_request "GET" "/v1/public/user/devices" "" "$token")"
assert_success "$resp" "获取设备列表"
printf "%s" "$resp"
}
find_device_id() {
local devices_resp="$1"
local identifier="$2"
jq -r --arg identifier "$identifier" '.data.list[]? | select(.identifier == $identifier) | .id' <<<"$devices_resp" | head -n1
}
assert_device_exists() {
local devices_resp="$1"
local identifier="$2"
jq -e --arg identifier "$identifier" '.data.list | any(.identifier == $identifier)' <<<"$devices_resp" >/dev/null \
|| fail "设备列表中未找到 identifier=${identifier}, resp=${devices_resp}"
}
unbind_device() {
local token="$1"
local device_id="$2"
local payload resp
payload="$(jq -nc --arg id "$device_id" '{id:($id|tonumber)}')"
resp="$(api_request "PUT" "/v1/public/user/unbind_device" "$payload" "$token")"
assert_success "$resp" "解绑设备(id=${device_id})"
}
assert_token_invalid() {
local token="$1"
local who="$2"
local resp code
resp="$(api_request "GET" "/v1/public/user/info" "" "$token")"
code="$(json_code "$resp")"
if [[ "$code" == "200" ]]; then
fail "${who} 预期已下线/失效,但 token 仍可访问: ${resp}"
fi
info "${who} token 已失效,符合预期(code=${code}, msg=$(json_msg "$resp"))"
}
main() {
require_cmd curl
require_cmd jq
bold "家庭组场景回归脚本"
info "BASE_URL=${BASE_URL}"
info "EMAIL_A=${EMAIL_A}"
info "DEVICE_A=${DEVICE_A}"
info "DEVICE_B=${DEVICE_B}"
info "DEVICE_C=${DEVICE_C} (本脚本当前未使用)"
info "EMAIL_LOGIN_CODE=${EMAIL_LOGIN_CODE}"
info "ALLOW_INTERACTIVE_INPUT=${ALLOW_INTERACTIVE_INPUT}"
info "ENABLE_REDIS_CODE_FALLBACK=${ENABLE_REDIS_CODE_FALLBACK}"
info "BIND_VERIFY_RETRY=${BIND_VERIFY_RETRY}"
local token_a token_b token_a2 token_b2 token_web
local bind_resp devices_resp
local device_id_a device_id_b device_id_b2
bold "场景1: 设备A登录并绑定邮箱A"
token_a="$(device_login "$DEVICE_A" "qa-device-a")"
bind_resp="$(bind_email_with_verification_auto "$token_a" "$EMAIL_A" 1)"
token_a="$(token_or_keep "$bind_resp" "$token_a")"
devices_resp="$(get_devices "$token_a")"
assert_device_exists "$devices_resp" "$DEVICE_A"
device_id_a="$(find_device_id "$devices_resp" "$DEVICE_A")"
[[ -n "$device_id_a" ]] || fail "场景1 无法找到设备A ID"
info "场景1完成: deviceA.id=${device_id_a}"
bold "场景2: 邮箱A网页登录"
token_web="$(email_login "$EMAIL_A" "$EMAIL_LOGIN_CODE")"
[[ -n "$token_web" ]] || fail "场景2 邮箱登录 token 为空"
info "场景2完成: 邮箱登录成功"
bold "场景3: 设备B登录并绑定邮箱A然后B踢A"
token_b="$(device_login "$DEVICE_B" "qa-device-b")"
bind_resp="$(bind_email_with_verification_auto "$token_b" "$EMAIL_A" 2)"
token_b="$(token_or_keep "$bind_resp" "$token_b")"
jq -e '.data.family_joined == true' <<<"$bind_resp" >/dev/null || warn "场景3: family_joined 非 trueresp=${bind_resp}"
devices_resp="$(get_devices "$token_b")"
assert_device_exists "$devices_resp" "$DEVICE_A"
assert_device_exists "$devices_resp" "$DEVICE_B"
device_id_a="$(find_device_id "$devices_resp" "$DEVICE_A")"
[[ -n "$device_id_a" ]] || fail "场景3 无法从B视角找到A设备ID"
unbind_device "$token_b" "$device_id_a"
assert_token_invalid "$token_a" "设备A(被B踢后)"
info "场景3完成: B已踢A"
bold "场景4: 设备A重新登录并绑定邮箱A然后A踢B"
token_a2="$(device_login "$DEVICE_A" "qa-device-a-relogin")"
bind_resp="$(bind_email_with_verification_auto "$token_a2" "$EMAIL_A" 2)"
token_a2="$(token_or_keep "$bind_resp" "$token_a2")"
devices_resp="$(get_devices "$token_a2")"
assert_device_exists "$devices_resp" "$DEVICE_A"
assert_device_exists "$devices_resp" "$DEVICE_B"
device_id_b="$(find_device_id "$devices_resp" "$DEVICE_B")"
[[ -n "$device_id_b" ]] || fail "场景4 无法从A视角找到B设备ID"
unbind_device "$token_a2" "$device_id_b"
assert_token_invalid "$token_b" "设备B(被A踢后)"
info "场景4完成: A已踢B"
bold "场景5: 设备B登录后执行退出(删除当前设备)"
token_b2="$(device_login "$DEVICE_B" "qa-device-b-relogin")"
bind_resp="$(bind_email_with_verification_auto "$token_b2" "$EMAIL_A" 2)"
token_b2="$(token_or_keep "$bind_resp" "$token_b2")"
devices_resp="$(get_devices "$token_b2")"
device_id_b2="$(find_device_id "$devices_resp" "$DEVICE_B")"
[[ -n "$device_id_b2" ]] || fail "场景5 无法找到当前设备B ID"
unbind_device "$token_b2" "$device_id_b2"
assert_token_invalid "$token_b2" "设备B(主动退出后)"
info "场景5完成: B退出后 token 失效"
bold "全部场景执行完成 ✅"
}
main "$@"