forked from baskbuddy/zhangshun
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
333 lines
13 KiB
333 lines
13 KiB
#!/usr/bin/env bash
|
|
|
|
# ── xtrace 防护(S01 第三轮 F4)───────────────────────────────
|
|
# 必须在读取任何环境变量、组装任何请求之前关闭 xtrace。
|
|
# 脚本是独立可执行程序,安全优先:全程不重新 set -x,
|
|
# 避免验证码 / 完整手机号 / LOGIN_BODY / 登录响应 / TOKEN / AUTH 进入 stderr。
|
|
case "$-" in
|
|
*x*) set +x ;;
|
|
esac
|
|
|
|
set -u
|
|
|
|
# MyDreamJob backend RC smoke test.
|
|
# Required tools: curl, grep, sed, head, tail, docker(仅取测试验证码时)。
|
|
# Environment:
|
|
# BASE_URL API base URL (default: http://127.0.0.1:8080/api/v1)
|
|
# STUDENT_PHONE Fresh 11-digit phone; generated when omitted
|
|
# COMPANY_TOKEN Company JWT; company status step is SKIP when omitted
|
|
# SMS_MOCK_ENABLED=true **必需**:声明被测应用以 app.sms.mock.enabled=true 运行
|
|
# ALLOW_TEST_OTP_DB_READ=true **必需**:显式授权本次本地测试从数据库读取验证码
|
|
#
|
|
# 安全说明(S01 定向整改 F1 / 第三轮 F4+F5):
|
|
# * 应用日志**不再**输出验证码,本脚本也**不得**从日志/控制台/stdout 读取验证码。
|
|
# * 仅在 [SMS_MOCK_ENABLED=true] **且** [ALLOW_TEST_OTP_DB_READ=true] 时,
|
|
# 才通过容器内部只读 SQL 取得验证码;任一条件不满足立即安全失败。
|
|
# * 验证码只保存在 shell 局部变量 / REPLY 约定中,不写日志、不写文件、不输出到终端。
|
|
# * **xtrace 永远关闭**:即使以 `bash -x` 调用,脚本开头即 set +x 且全程不恢复。
|
|
# * 数据库凭据通过容器进程环境 MYSQL_PWD 传递,**不进入 mysql 命令行参数**;
|
|
# 使用应用账号 MYSQL_USER(不使用 root,缺失即安全失败)。
|
|
# * 不接入真实短信供应商;生产环境禁止使用该取码方式。
|
|
|
|
BASE_URL="${BASE_URL:-http://127.0.0.1:8080/api/v1}"
|
|
PASS=0
|
|
FAIL=0
|
|
SKIP=0
|
|
|
|
# ── 凭据变量登记(退出时统一清理)─────────────────────────────
|
|
# 覆盖全部敏感变量:手机号、验证码、登录请求体、登录响应、JWT、Authorization 头。
|
|
# 注意:不得在此处给 PHONE / STUDENT_PHONE / COMPANY_TOKEN 赋空值,
|
|
# 它们携带调用方传入值或由后续逻辑生成。
|
|
SMS=""
|
|
OTP=""
|
|
REPLY=""
|
|
LOGIN_BODY=""
|
|
LOGIN=""
|
|
TOKEN=""
|
|
AUTH=""
|
|
cleanup_secrets() {
|
|
unset PHONE SMS OTP REPLY LOGIN_BODY LOGIN TOKEN AUTH 2>/dev/null || true
|
|
unset STUDENT_PHONE COMPANY_TOKEN 2>/dev/null || true
|
|
}
|
|
trap cleanup_secrets EXIT
|
|
|
|
if [ -n "${STUDENT_PHONE:-}" ]; then
|
|
PHONE="$STUDENT_PHONE"
|
|
else
|
|
# 139 + eight digits = 11 digits.
|
|
PHONE="139$(date +%s | tail -c 9)"
|
|
fi
|
|
|
|
say() { printf '%-58s' "$1"; }
|
|
ok() { echo "[PASS] $1"; PASS=$((PASS + 1)); }
|
|
fail() { echo "[FAIL] $1"; FAIL=$((FAIL + 1)); }
|
|
skip() { echo "[SKIP] $1"; SKIP=$((SKIP + 1)); }
|
|
|
|
# 手机号脱敏(控制台输出不展示完整号码)
|
|
mask_phone() {
|
|
local p="${1:-}"
|
|
if [ "${#p}" -lt 7 ]; then
|
|
printf '%s' '***'
|
|
return
|
|
fi
|
|
printf '%s****%s' "${p:0:3}" "${p: -4}"
|
|
}
|
|
|
|
# ── 布尔解析(大小写/空白不敏感,仅 true 为真)────────────────
|
|
is_true() {
|
|
local v
|
|
v="$(printf '%s' "${1:-}" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')"
|
|
[ "$v" = "true" ]
|
|
}
|
|
|
|
# ── 取码双重保护 ─────────────────────────────────────────────
|
|
# 保护 1:被测应用必须显式处于 mock 短信模式
|
|
require_mock_sms_mode() {
|
|
is_true "${SMS_MOCK_ENABLED:-}"
|
|
}
|
|
|
|
# 保护 2:本次执行必须显式授权读取验证码表
|
|
require_test_otp_db_read_permission() {
|
|
is_true "${ALLOW_TEST_OTP_DB_READ:-}"
|
|
}
|
|
|
|
# 从项目实际 compose 文件解析 mysql 容器名(不硬编码、不假设)
|
|
# 使用 BASH_SOURCE 以便脚本被 `source` 时仍能正确定位自身目录
|
|
compose_mysql_container() {
|
|
local self base compose
|
|
self="${BASH_SOURCE[0]:-$0}"
|
|
base="$(cd "$(dirname "$self")" && pwd)"
|
|
for compose in "$base/../backend-infra/compose.yaml" "$base/../backend-infra/docker-compose.yml"; do
|
|
[ -f "$compose" ] || continue
|
|
awk '
|
|
/^[[:space:]]+mysql:[[:space:]]*$/ { in_mysql=1; next }
|
|
/^[[:space:]]+[A-Za-z0-9_-]+:[[:space:]]*$/ { in_mysql=0 }
|
|
in_mysql && /^[[:space:]]+container_name:[[:space:]]*/ {
|
|
sub(/^[[:space:]]*container_name:[[:space:]]*/, ""); print; exit
|
|
}
|
|
' "$compose"
|
|
done
|
|
}
|
|
|
|
# 容器内只读查询(F5:凭据经进程环境 MYSQL_PWD 传递,不进入 mysql argv;SQL 经 stdin)
|
|
# - 使用应用账号 MYSQL_USER(不使用 root;缺失即安全失败,不回退 root)
|
|
# - 仅允许 SELECT;查询失败返回非 0,由 sms_code() 输出固定安全错误
|
|
otp_db_query() {
|
|
local phone="$1" purpose="$2" container
|
|
container="$(compose_mysql_container | head -n 1)"
|
|
[ -n "$container" ] || return 1
|
|
|
|
printf '%s\n' "SELECT code FROM sms_verification_codes" \
|
|
" WHERE phone = '$phone' AND purpose = '$purpose' AND used = 0" \
|
|
" AND expired_at > NOW()" \
|
|
" ORDER BY created_at DESC, id DESC LIMIT 1;" \
|
|
| docker exec -i "$container" sh -c '
|
|
# 应用账号凭据必需,缺失则安全失败(绝不回退 root)
|
|
if [ -z "${MYSQL_USER:-}" ] || [ -z "${MYSQL_PASSWORD:-}" ]; then
|
|
echo "db credentials unavailable" >&2
|
|
exit 3
|
|
fi
|
|
# 密码只经环境变量 MYSQL_PWD 传递,argv 中不出现 -p/--password
|
|
MYSQL_PWD="$MYSQL_PASSWORD" exec mysql \
|
|
-u"$MYSQL_USER" \
|
|
-N -B \
|
|
"${MYSQL_DATABASE:-mydreamjob}"' 2>/dev/null \
|
|
| head -n 1
|
|
}
|
|
|
|
# 取得验证码:成功时写入 REPLY;绝不打印到 stdout/stderr
|
|
sms_code() {
|
|
local disabled_msg="test OTP retrieval is disabled"
|
|
|
|
if ! require_mock_sms_mode || ! require_test_otp_db_read_permission; then
|
|
echo "$disabled_msg" >&2
|
|
return 1
|
|
fi
|
|
|
|
case "${PHONE:-}" in
|
|
*[!0-9]*|'') echo "$disabled_msg" >&2; return 1 ;;
|
|
esac
|
|
[ "${#PHONE}" -eq 11 ] || { echo "$disabled_msg" >&2; return 1; }
|
|
|
|
# xtrace 已在脚本开头全局关闭,这里无需(也不得)保存/恢复其状态
|
|
local code="" qstatus=0
|
|
IFS= read -r code < <(otp_db_query "$PHONE" "login") || qstatus=$?
|
|
|
|
# 校验格式:当前验证码为 6 位数字(app.sms.mock.code-length / CODE_PATTERN)
|
|
local valid=1
|
|
[ "$qstatus" -eq 0 ] || valid=0
|
|
case "$code" in
|
|
[0-9][0-9][0-9][0-9][0-9][0-9]) : ;;
|
|
*) valid=0 ;;
|
|
esac
|
|
|
|
if [ "$valid" -ne 1 ]; then
|
|
unset code
|
|
echo "test OTP retrieval failed" >&2
|
|
return 1
|
|
fi
|
|
|
|
REPLY="$code"
|
|
unset code
|
|
return 0
|
|
}
|
|
|
|
report() {
|
|
local response="$1"
|
|
local http_code body biz_code request_id
|
|
http_code="$(printf '%s\n' "$response" | tail -n 1)"
|
|
body="$(printf '%s\n' "$response" | sed '$d')"
|
|
biz_code="$(printf '%s' "$body" | grep -oE '"code"[[:space:]]*:[[:space:]]*[0-9]+' | head -n 1 || true)"
|
|
request_id="$(printf '%s' "$body" | grep -oE '"requestId"[[:space:]]*:[[:space:]]*"[^"]*"' | head -n 1 || true)"
|
|
echo " http=$http_code ${biz_code:-code=N/A} ${request_id:-requestId=N/A}"
|
|
}
|
|
|
|
# 仅接收可安全打印的三个字段,不接收任何响应体全文(用于登录等敏感响应)
|
|
report_fields() {
|
|
local http_code="$1" biz_code="$2" request_id="$3"
|
|
echo " http=$http_code ${biz_code:-code=N/A} ${request_id:-requestId=N/A}"
|
|
}
|
|
|
|
# 从响应中只提取可安全打印字段(http / business code / requestId),不保留响应体
|
|
extract_status_fields() {
|
|
local response="$1"
|
|
local http_code body biz_code request_id
|
|
http_code="$(printf '%s\n' "$response" | tail -n 1)"
|
|
body="$(printf '%s\n' "$response" | sed '$d')"
|
|
biz_code="$(printf '%s' "$body" | grep -oE '"code"[[:space:]]*:[[:space:]]*[0-9]+' | head -n 1 | grep -oE '[0-9]+' || true)"
|
|
request_id="$(printf '%s' "$body" | grep -oE '"requestId"[[:space:]]*:[[:space:]]*"[^"]*"' | head -n 1 || true)"
|
|
printf '%s\n%s\n%s' "$http_code" "$biz_code" "$request_id"
|
|
}
|
|
|
|
extract_token() {
|
|
printf '%s' "$1" | sed -n 's/.*"accessToken"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1
|
|
}
|
|
|
|
extract_id() {
|
|
printf '%s' "$1" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n 1
|
|
}
|
|
|
|
# ── 可测试性钩子 ───────────────────────────────────────────
|
|
# 供 shell 单元测试加载函数(验证取码保护与泄漏防护)而不执行完整流程。
|
|
# 正常运行(不设该变量)时完全不生效,不影响任何行为。
|
|
if [ "${SMOKE_TEST_LIB_ONLY:-}" = "1" ]; then
|
|
return 0 2>/dev/null || exit 0
|
|
fi
|
|
|
|
echo "=== MyDreamJob backend smoke test ==="
|
|
echo "BASE_URL=$BASE_URL"
|
|
echo "PHONE=$(mask_phone "$PHONE")"
|
|
|
|
say "GET /health"
|
|
HEALTH="$(curl -sS -w '\n%{http_code}' "$BASE_URL/health" || true)"
|
|
report "$HEALTH"
|
|
if [ "$(printf '%s\n' "$HEALTH" | tail -n 1)" = "200" ]; then ok "health"; else fail "health"; fi
|
|
|
|
say "POST /auth/sms/send and POST /auth/login"
|
|
SMS=""
|
|
for attempt in 1 2 3; do
|
|
curl -sS -X POST "$BASE_URL/auth/sms/send?phone=$PHONE&purpose=login" \
|
|
-H "X-Request-Id: smoke-sms-$attempt" >/dev/null 2>&1 || true
|
|
sleep 2
|
|
if sms_code; then
|
|
SMS="$REPLY"
|
|
unset REPLY
|
|
[ -n "$SMS" ] && break
|
|
fi
|
|
done
|
|
if [ -z "$SMS" ]; then
|
|
fail "login skipped"
|
|
unset SMS
|
|
TOKEN=""
|
|
else
|
|
# 请求体经 stdin 传入(--data-binary @-),验证码不出现在进程参数中
|
|
LOGIN_BODY="{\"phone\":\"$PHONE\",\"code\":\"$SMS\",\"purpose\":\"login\"}"
|
|
LOGIN="$(printf '%s' "$LOGIN_BODY" | curl -sS -w '\n%{http_code}' -X POST "$BASE_URL/auth/login" \
|
|
-H 'Content-Type: application/json' \
|
|
-H 'X-Request-Id: smoke-login-001' \
|
|
--data-binary @- || true)"
|
|
unset LOGIN_BODY
|
|
unset SMS
|
|
# 只保留可安全打印的三个字段;登录响应全文绝不交给打印函数,也绝不落盘
|
|
LOGIN_FIELDS="$(extract_status_fields "$LOGIN")"
|
|
TOKEN="$(extract_token "$LOGIN")"
|
|
unset LOGIN
|
|
report_fields "$(printf '%s' "$LOGIN_FIELDS" | sed -n '1p')" \
|
|
"$(printf '%s' "$LOGIN_FIELDS" | sed -n '2p')" \
|
|
"$(printf '%s' "$LOGIN_FIELDS" | sed -n '3p')"
|
|
unset LOGIN_FIELDS
|
|
if [ -n "$TOKEN" ]; then ok "login"; else fail "login"; fi
|
|
fi
|
|
|
|
if [ -z "$TOKEN" ]; then
|
|
echo "PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
|
|
exit 1
|
|
fi
|
|
|
|
AUTH="Authorization: Bearer $TOKEN"
|
|
|
|
say "GET /jobs?size=5"
|
|
JOBS="$(curl -sS -w '\n%{http_code}' "$BASE_URL/jobs?size=5" -H "$AUTH" || true)"
|
|
report "$JOBS"
|
|
JOB_ID="$(extract_id "$JOBS")"
|
|
if printf '%s' "$JOBS" | grep -q 'items' && [ -n "$JOB_ID" ]; then ok "jobs list"; else fail "jobs list"; fi
|
|
|
|
say "GET /jobs/public/$JOB_ID"
|
|
PUBLIC="$(curl -sS -w '\n%{http_code}' "$BASE_URL/jobs/public/$JOB_ID" || true)"
|
|
report "$PUBLIC"
|
|
if printf '%s' "$PUBLIC" | grep -q 'title'; then ok "public job"; else fail "public job"; fi
|
|
|
|
say "GET /jobs/$JOB_ID/match"
|
|
MATCH="$(curl -sS -w '\n%{http_code}' "$BASE_URL/jobs/$JOB_ID/match" -H "$AUTH" || true)"
|
|
report "$MATCH"
|
|
if printf '%s' "$MATCH" | grep -q 'rulesVersion'; then ok "match diagnose"; else fail "match diagnose"; fi
|
|
|
|
say "POST /applications"
|
|
APP="$(curl -sS -w '\n%{http_code}' -X POST "$BASE_URL/applications" \
|
|
-H "$AUTH" -H 'Content-Type: application/json' \
|
|
-H 'X-Request-Id: smoke-apply-001' -d "{\"jobId\":$JOB_ID}" || true)"
|
|
report "$APP"
|
|
APP_ID="$(extract_id "$APP")"
|
|
if [ -n "$APP_ID" ]; then ok "apply application $APP_ID"; else fail "apply application"; fi
|
|
|
|
say "PATCH /applications/$APP_ID/status"
|
|
if [ -z "${COMPANY_TOKEN:-}" ]; then
|
|
skip "COMPANY_TOKEN not provided"
|
|
else
|
|
STATUS="$(curl -sS -w '\n%{http_code}' -X PATCH "$BASE_URL/applications/$APP_ID/status" \
|
|
-H "Authorization: Bearer $COMPANY_TOKEN" -H 'Content-Type: application/json' \
|
|
-H 'X-Request-Id: smoke-status-001' -d '{"status":"VIEWED"}' || true)"
|
|
report "$STATUS"
|
|
if printf '%s' "$STATUS" | grep -q 'VIEWED'; then ok "company status VIEWED"; else fail "company status"; fi
|
|
fi
|
|
|
|
say "GET /notifications"
|
|
NOTIFICATIONS="$(curl -sS -w '\n%{http_code}' "$BASE_URL/notifications?size=5" -H "$AUTH" || true)"
|
|
report "$NOTIFICATIONS"
|
|
if printf '%s' "$NOTIFICATIONS" | grep -q 'items'; then ok "notifications list"; else fail "notifications list"; fi
|
|
|
|
say "GET /notifications/unread-count"
|
|
UNREAD="$(curl -sS -w '\n%{http_code}' "$BASE_URL/notifications/unread-count" -H "$AUTH" || true)"
|
|
report "$UNREAD"
|
|
if printf '%s' "$UNREAD" | grep -q 'unreadCount'; then ok "unread count"; else fail "unread count"; fi
|
|
|
|
NOTIFICATION_ID="$(extract_id "$NOTIFICATIONS")"
|
|
say "POST /notifications/$NOTIFICATION_ID/read"
|
|
if [ -z "$NOTIFICATION_ID" ]; then
|
|
skip "no notification available"
|
|
else
|
|
READ="$(curl -sS -w '\n%{http_code}' -X POST "$BASE_URL/notifications/$NOTIFICATION_ID/read" -H "$AUTH" || true)"
|
|
report "$READ"
|
|
if printf '%s' "$READ" | grep -q 'marked as read'; then ok "mark notification read"; else fail "mark notification read"; fi
|
|
fi
|
|
|
|
say "POST /applications/$APP_ID/withdraw"
|
|
WITHDRAW="$(curl -sS -w '\n%{http_code}' -X POST "$BASE_URL/applications/$APP_ID/withdraw" \
|
|
-H "$AUTH" -H 'X-Request-Id: smoke-withdraw-001' || true)"
|
|
report "$WITHDRAW"
|
|
if printf '%s' "$WITHDRAW" | grep -q 'WITHDRAWN'; then ok "withdraw application"; else fail "withdraw application"; fi
|
|
|
|
echo "============================================"
|
|
echo "PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
|
|
echo "============================================"
|
|
[ "$FAIL" -eq 0 ]
|
|
|