#!/usr/bin/env bash # # integration_test.sh — Cocoon 集成测试套件 # # 依赖: curl, pgrep, sleep # 用法: ./tests/integration_test.sh [server_binary] [doc_root] # set -euo pipefail SERVER="${1:-./cocoon}" ROOT="${2:-./tests/fixtures}" HOST="localhost:9999" BASE="http://${HOST}" PASS=0 FAIL=0 TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"; kill_server' EXIT kill_server() { local pids pids=$(pgrep -f "cocoon.*-p 9999" 2>/dev/null || true) if [[ -n "$pids" ]]; then echo "$pids" | xargs kill -9 2>/dev/null || true fi pids=$(pgrep -f "cocoon.*--cert" 2>/dev/null || true) if [[ -n "$pids" ]]; then echo "$pids" | xargs kill -9 2>/dev/null || true fi sleep 0.5 # 确保端口已释放 for i in {1..20}; do if ! ss -tlnp 2>/dev/null | grep -q ':9999'; then break fi sleep 0.1 done } # 启动服务器 start_server() { echo "[test] 启动服务器: $SERVER -r $ROOT -p 9999" $SERVER -r "$ROOT" -p 9999 -l debug > "$TMPDIR/server.log" 2>&1 & local pid=$! for i in {1..30}; do if nc -z localhost 9999 2>/dev/null; then echo "[test] 服务器就绪 (PID $pid)" return 0 fi sleep 0.1 done echo "[test] 服务器启动失败" cat "$TMPDIR/server.log" exit 1 } # 断言辅助函数 pass() { PASS=$((PASS + 1)) } fail() { FAIL=$((FAIL + 1)) } assert_status() { local url="$1" local expect="$2" local desc="${3:-$url}" local status status=$(curl -s -o /dev/null -w "%{http_code}" "$url") if [[ "$status" == "$expect" ]]; then echo " ✓ $desc — HTTP $status" pass else echo " ✗ $desc — 期望 HTTP $expect, 实际 HTTP $status" fail fi } assert_status_with_header() { local header="$1" local url="$2" local expect="$3" local desc="${4:-$url}" local status status=$(curl -s -o /dev/null -w "%{http_code}" -H "$header" "$url") if [[ "$status" == "$expect" ]]; then echo " ✓ $desc — HTTP $status" pass else echo " ✗ $desc — 期望 HTTP $expect, 实际 HTTP $status" fail fi } assert_head_status() { local url="$1" local expect="$2" local desc="${3:-$url}" local status status=$(curl -s -o /dev/null -w "%{http_code}" -I "$url") if [[ "$status" == "$expect" ]]; then echo " ✓ $desc — HTTP $status" pass else echo " ✗ $desc — 期望 HTTP $expect, 实际 HTTP $status" fail fi } assert_head_no_body() { local url="$1" local desc="${2:-$url}" local len len=$(curl -s -o /dev/null -w "%{size_download}" -I "$url") if [[ "$len" == "0" ]]; then echo " ✓ $desc — 无响应体" pass else echo " ✗ $desc — 期望无响应体, 实际 $len 字节" fail fi } assert_header_contains() { local url="$1" local header="$2" local expect="$3" local desc="${4:-$url}" local value value=$(curl -sI "$url" | grep -i "^$header:" | tr -d '\r' || true) if echo "$value" | grep -qi "$expect"; then echo " ✓ $desc — $header 包含 '$expect'" pass else echo " ✗ $desc — $header 期望包含 '$expect', 实际: $value" fail fi } assert_header_contains_with_header() { local req_header="$1" local url="$2" local header="$3" local expect="$4" local desc="${5:-$url}" local value value=$(curl -sI -H "$req_header" "$url" | grep -i "^$header:" | tr -d '\r' || true) if echo "$value" | grep -qi "$expect"; then echo " ✓ $desc — $header 包含 '$expect'" pass else echo " ✗ $desc — $header 期望包含 '$expect', 实际: $value" fail fi } assert_body_contains() { local url="$1" local expect="$2" local desc="${3:-$url}" local body body=$(curl -s -k --compressed "$url") if echo "$body" | grep -q "$expect"; then echo " ✓ $desc — 响应体包含 '$expect'" pass else echo " ✗ $desc — 响应体期望包含 '$expect'" fail fi } assert_brotli() { local url="$1" local desc="${2:-$url}" local encoding encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: br" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true) if echo "$encoding" | grep -qi "br"; then echo " ✓ $desc — 返回 brotli 压缩" pass else echo " ✗ $desc — 期望 brotli 压缩, 实际: $encoding" fail fi } assert_brotli_preferred() { local url="$1" local desc="${2:-$url}" local encoding encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: gzip, br" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true) if echo "$encoding" | grep -qi "br"; then echo " ✓ $desc — 优先返回 brotli 压缩" pass else echo " ✗ $desc — 期望优先 brotli, 实际: $encoding" fail fi } assert_not_brotli() { local url="$1" local desc="${2:-$url}" local encoding encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: br" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true) if [[ -z "$encoding" ]]; then echo " ✓ $desc — 无 Content-Encoding(未压缩)" pass else echo " ✗ $desc — 期望不压缩, 实际: $encoding" fail fi } assert_gzip() { local url="$1" local desc="${2:-$url}" local encoding encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: gzip" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true) if echo "$encoding" | grep -qi "gzip"; then echo " ✓ $desc — 返回 gzip 压缩" pass else echo " ✗ $desc — 期望 gzip 压缩, 实际: $encoding" fail fi } assert_not_gzip() { local url="$1" local desc="${2:-$url}" local encoding encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: gzip" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true) if [[ -z "$encoding" ]]; then echo " ✓ $desc — 无 Content-Encoding(未压缩)" pass else echo " ✗ $desc — 期望不压缩, 实际: $encoding" fail fi } assert_304() { local url="$1" local etag="$2" local desc="${3:-$url}" local status status=$(curl -s -o /dev/null -w "%{http_code}" -H "If-None-Match: $etag" "$url") if [[ "$status" == "304" ]]; then echo " ✓ $desc — 缓存命中 HTTP 304" pass else echo " ✗ $desc — 期望 304, 实际 $status" fail fi } assert_304_modified_since() { local url="$1" local mtime="$2" local desc="${3:-$url}" local status status=$(curl -s -o /dev/null -w "%{http_code}" -H "If-Modified-Since: $mtime" "$url") if [[ "$status" == "304" ]]; then echo " ✓ $desc — If-Modified-Since 命中 HTTP 304" pass else echo " ✗ $desc — 期望 304, 实际 $status" fail fi } assert_post_multipart() { local url="$1" local desc="${2:-$url}" local resp local boundary="----CocoonTestBoundary" local body body=$(printf -- '--%s\r\nContent-Disposition: form-data; name="file"; filename="upload_test.txt"\r\nContent-Type: text/plain\r\n\r\nCocoon multipart upload test content\r\n--%s--\r\n' "$boundary" "$boundary") resp=$(curl -s -X POST -H "Content-Type: multipart/form-data; boundary=${boundary}" --data-binary "$body" "$url") if echo "$resp" | grep -q '"uploaded"'; then echo " ✓ $desc — 响应包含上传结果" pass else echo " ✗ $desc — 响应期望包含上传结果, 实际: $resp" fail fi # 验证文件是否被保存 if [ -f "$ROOT/uploads/upload_test.txt" ]; then echo " ✓ $desc — 文件已保存到 uploads/" pass else echo " ✗ $desc — 文件未保存到 uploads/" fail fi } assert_post_json() { local url="$1" local body="$2" local expect="$3" local desc="${4:-$url}" local resp resp=$(curl -s -X POST -H "Content-Type: application/json" -d "$body" "$url") if echo "$resp" | grep -q "$expect"; then echo " ✓ $desc — 响应包含 '$expect'" pass else echo " ✗ $desc — 响应期望包含 '$expect', 实际: $resp" fail fi } assert_post_form() { local url="$1" local body="$2" local expect="$3" local desc="${4:-$url}" local resp resp=$(curl -s -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "$body" "$url") if echo "$resp" | grep -q "$expect"; then echo " ✓ $desc — 响应包含 '$expect'" pass else echo " ✗ $desc — 响应期望包含 '$expect', 实际: $resp" fail fi } assert_http2_brotli() { local url="$1" local desc="${2:-$url}" local encoding encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true) if echo "$encoding" | grep -qi "br"; then echo " ✓ $desc — HTTP/2 返回 brotli 压缩" pass else echo " ✗ $desc — HTTP/2 期望 brotli 压缩, 实际: $encoding" fail fi } assert_http2_gzip() { local url="$1" local desc="${2:-$url}" local encoding encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true) if echo "$encoding" | grep -qi "gzip"; then echo " ✓ $desc — HTTP/2 返回 gzip 压缩" pass else echo " ✗ $desc — HTTP/2 期望 gzip 压缩, 实际: $encoding" fail fi } assert_http2_not_compressed() { local url="$1" local desc="${2:-$url}" local encoding encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true) if [[ -z "$encoding" ]]; then echo " ✓ $desc — HTTP/2 无压缩" pass else echo " ✗ $desc — HTTP/2 期望无压缩, 实际: $encoding" fail fi } assert_status_405() { local url="$1" local desc="${2:-$url}" local status status=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$url") if [[ "$status" == "405" ]]; then echo " ✓ $desc — PUT 返回 405" pass else echo " ✗ $desc — 期望 405, 实际 $status" fail fi } assert_access_log() { local log_file="$1" local desc="${2:-访问日志}" if [ -f "$log_file" ] && [ -s "$log_file" ]; then local count count=$(grep -c '"GET /' "$log_file" || true) if [ "$count" -ge 1 ]; then echo " ✓ $desc — 日志文件包含请求记录" pass else echo " ✗ $desc — 日志文件无请求记录" fail fi else echo " ✗ $desc — 日志文件不存在或为空" fail fi } # ===== 测试开始 ===== start_server echo "" echo "=== 基础功能测试 ===" assert_status "$BASE/" "200" "首页 GET" assert_status "$BASE/index.html" "200" "index.html GET" assert_status "$BASE/nonexist.html" "404" "404 页面" assert_status "$BASE/../etc/passwd" "404" "路径遍历防护" echo "" echo "=== HEAD 请求测试 ===" assert_head_status "$BASE/" "200" "首页 HEAD" assert_head_no_body "$BASE/index.html" "HEAD 无响应体" echo "" echo "=== 响应头测试 ===" assert_header_contains "$BASE/index.html" "Server" "Cocoon" "Server 标识" assert_header_contains "$BASE/index.html" "Content-Type" "text/html" "Content-Type" assert_header_contains "$BASE/" "Content-Type" "text/html" "目录浏览 Content-Type" echo "" echo "=== 目录浏览测试 ===" assert_status "$BASE/subdir/" "200" "目录浏览" assert_body_contains "$BASE/subdir/" "Index of" "目录列表标题" echo "" echo "=== Range 请求测试 ===" assert_status_with_header "Range: bytes=0-9" "$BASE/index.html" "206" "Range 206" assert_header_contains_with_header "Range: bytes=0-9" "$BASE/index.html" "Content-Range" "bytes 0-9" "Content-Range 头" assert_status_with_header "Range: bytes=99999-100000" "$BASE/index.html" "416" "越界 Range 416" echo "" echo "=== 缓存协商测试 ===" # 先获取 ETag ETAG=$(curl -sI "$BASE/index.html" | grep -i "ETag:" | awk '{print $2}' | tr -d '\r') echo " 首页 ETag: $ETAG" assert_header_contains "$BASE/index.html" "ETag" "\"" "ETag 存在" assert_header_contains "$BASE/index.html" "Last-Modified" "" "Last-Modified 存在" if [[ -n "$ETAG" ]]; then assert_304 "$BASE/index.html" "$ETAG" "If-None-Match 304" fi # If-Modified-Since 测试 LM=$(curl -sI "$BASE/index.html" | grep -i "Last-Modified:" | sed 's/Last-Modified: //i' | tr -d '\r') if [[ -n "$LM" ]]; then assert_304_modified_since "$BASE/index.html" "$LM" "If-Modified-Since 304" fi echo "" echo "=== Gzip 压缩测试 ===" assert_gzip "$BASE/index.html" "HTML gzip" assert_gzip "$BASE/style.css" "CSS gzip" assert_gzip "$BASE/app.js" "JS gzip" assert_gzip "$BASE/api.json" "JSON gzip" # 图片不应压缩 assert_not_gzip "$BASE/image.png" "图片不压缩" echo "" echo "=== Brotli 压缩测试 ===" assert_brotli "$BASE/index.html" "HTML brotli" assert_brotli "$BASE/style.css" "CSS brotli" assert_brotli "$BASE/app.js" "JS brotli" assert_brotli_preferred "$BASE/index.html" "优先 brotli" # 图片不应压缩 assert_not_brotli "$BASE/image.png" "图片不压缩" echo "" echo "=== MIME 类型测试 ===" assert_header_contains "$BASE/style.css" "Content-Type" "text/css" "CSS MIME" assert_header_contains "$BASE/app.js" "Content-Type" "application/javascript" "JS MIME" assert_header_contains "$BASE/api.json" "Content-Type" "application/json" "JSON MIME" assert_header_contains "$BASE/image.png" "Content-Type" "image/" "PNG MIME" echo "" echo "=== POST 请求测试 ===" assert_post_json "$BASE/api/echo" '{"test": "hello"}' '"test"' "POST JSON 回显" assert_post_form "$BASE/api/echo" 'name=cocoon&version=1.0' 'cocoon' "POST 表单回显" assert_status_405 "$BASE/index.html" "PUT 405" echo "" echo "=== 文件上传测试 ===" assert_post_multipart "$BASE/upload" "multipart 文件上传" echo "" echo "=== 访问日志测试 ===" # 带访问日志启动服务器 kill_server sleep 1 LOG_FILE="$TMPDIR/access.log" $SERVER -r "$ROOT" -p 9999 --access-log "$LOG_FILE" > "$TMPDIR/server_access.log" 2>&1 & pid_access=$! for i in {1..30}; do if curl -s -o /dev/null "$BASE/" 2>/dev/null; then break fi sleep 0.1 done # 发送请求并验证日志记录 curl -s -o /dev/null "$BASE/index.html" -H "User-Agent: CocoonTest/1.0" curl -s -o /dev/null "$BASE/nonexist.html" -H "Referer: http://example.com" sleep 0.5 assert_access_log "$LOG_FILE" "访问日志文件生成" # 检查日志格式是否包含关键字段 if grep -q '"GET /index.html HTTP/1.1"' "$LOG_FILE"; then echo " ✓ 日志包含 GET /index.html" pass else echo " ✗ 日志缺少 GET /index.html" fail fi if grep -q 'CocoonTest/1.0' "$LOG_FILE"; then echo " ✓ 日志包含 User-Agent" pass else echo " ✗ 日志缺少 User-Agent" fail fi if grep -q 'example.com' "$LOG_FILE"; then echo " ✓ 日志包含 Referer" pass else echo " ✗ 日志缺少 Referer" fail fi if grep -q '404' "$LOG_FILE"; then echo " ✓ 日志包含 404 状态码" pass else echo " ✗ 日志缺少 404 状态码" fail fi # 恢复普通服务器 kill_server sleep 1 start_server echo "" echo "=== TLS/HTTPS 测试 ===" # 启动 HTTPS 服务器(使用测试证书) kill_server sleep 1 $SERVER -r "$ROOT" -p 9999 --cert tests/server.crt --key tests/server.key > "$TMPDIR/server_tls.log" 2>&1 & pid_tls=$! # 等待服务器就绪(curl -k 忽略证书验证) for i in {1..50}; do if curl -s -o /dev/null -k --max-time 2 "https://$HOST/" 2>/dev/null; then break fi sleep 0.2 done # 使用 openssl s_client 验证 TLS 握手 if command -v openssl >/dev/null 2>&1; then tls_resp=$(echo -e "GET / HTTP/1.1\r\nHost: $HOST\r\nConnection: close\r\n\r\n" | \ timeout 5 openssl s_client -connect "$HOST" -quiet 2>/dev/null | head -20) if echo "$tls_resp" | grep -q "HTTP/1.1"; then echo " ✓ TLS 握手 + HTTP GET — 通过 HTTPS 获取响应" pass else echo " ✗ TLS 握手 + HTTP GET — 未能通过 HTTPS 获取响应" fail fi else echo " ⚠ openssl 未安装,跳过 TLS 握手验证" fi # 使用 curl -k 验证 HTTPS 响应体 assert_body_contains "https://$HOST/" "Cocoon" "HTTPS 首页响应" echo "" echo "=== HTTP/2 测试 ===" # 使用已启动的 TLS 服务器测试 HTTP/2(ALPN 协商) kill_server sleep 1 $SERVER -r "$ROOT" -p 9999 --cert tests/server.crt --key tests/server.key > "$TMPDIR/server_h2.log" 2>&1 & pid_h2=$! # 等待服务器就绪 for i in {1..50}; do if curl -s -o /dev/null -k --max-time 2 "https://$HOST/" 2>/dev/null; then break fi sleep 0.2 done # 验证 HTTP/2 协议版本 h2_version=$(curl --http2 -k -s -o /dev/null -w "%{http_version}" "https://$HOST/") if [[ "$h2_version" == "2" ]]; then echo " ✓ HTTP/2 ALPN 协商 — 协议版本 HTTP/2" pass else echo " ✗ HTTP/2 ALPN 协商 — 期望 HTTP/2, 实际 HTTP/$h2_version" fail fi # HTTP/2 GET 首页 h2_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/") if [[ "$h2_status" == "200" ]]; then echo " ✓ HTTP/2 GET 首页 — HTTP 200" pass else echo " ✗ HTTP/2 GET 首页 — 期望 200, 实际 $h2_status" fail fi # HTTP/2 响应体 h2_body=$(curl --http2 -k -s --compressed "https://$HOST/") if echo "$h2_body" | grep -q "Cocoon"; then echo " ✓ HTTP/2 响应体 — 包含 'Cocoon'" pass else echo " ✗ HTTP/2 响应体 — 未包含 'Cocoon'" fail fi # HTTP/2 HEAD 请求 h2_head_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" -I "https://$HOST/index.html") if [[ "$h2_head_status" == "200" ]]; then echo " ✓ HTTP/2 HEAD 请求 — HTTP 200" pass else echo " ✗ HTTP/2 HEAD 请求 — 期望 200, 实际 $h2_head_status" fail fi # HTTP/2 404 h2_404=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/nonexist.html") if [[ "$h2_404" == "404" ]]; then echo " ✓ HTTP/2 404 请求 — HTTP 404" pass else echo " ✗ HTTP/2 404 请求 — 期望 404, 实际 $h2_404" fail fi # HTTP/2 缓存协商(ETag 304) H2_ETAG=$(curl --http2 -k -sI "https://$HOST/index.html" | grep -i "ETag:" | awk '{print $2}' | tr -d '\r') if [[ -n "$H2_ETAG" ]]; then h2_304=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" -H "If-None-Match: $H2_ETAG" "https://$HOST/index.html") if [[ "$h2_304" == "304" ]]; then echo " ✓ HTTP/2 If-None-Match 304 — 缓存命中" pass else echo " ✗ HTTP/2 If-None-Match 304 — 期望 304, 实际 $h2_304" fail fi fi # HTTP/2 压缩测试 assert_http2_brotli "https://$HOST/index.html" "HTTP/2 HTML brotli" assert_http2_brotli "https://$HOST/style.css" "HTTP/2 CSS brotli" assert_http2_brotli "https://$HOST/app.js" "HTTP/2 JS brotli" # HTTP/2 目录浏览测试 h2_dir_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/subdir/") h2_dir_body=$(curl --http2 -k -s "https://$HOST/subdir/") if [[ "$h2_dir_status" == "200" ]] && echo "$h2_dir_body" | grep -q "page.html"; then echo " ✓ HTTP/2 目录浏览 — HTTP 200,包含 page.html" pass else echo " ✗ HTTP/2 目录浏览 — 期望 200+page.html, 实际 $h2_dir_status" fail fi # 停止 HTTP/2 服务器,恢复 HTTP 服务器 kill_server sleep 1 start_server # h2c 测试(明文 HTTP/2) echo "" echo "=== h2c 测试 ===" # h2c prior knowledge 直接连接 h2c_pk_status=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/") h2c_pk_version=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_version}" "http://$HOST/") if [[ "$h2c_pk_status" == "200" && "$h2c_pk_version" == "2" ]]; then echo " ✓ h2c prior knowledge — HTTP 200, 协议 HTTP/2" pass else echo " ✗ h2c prior knowledge — 期望 200+HTTP/2, 实际 $h2c_pk_status+HTTP/$h2c_pk_version" fail fi # h2c prior knowledge 404 h2c_pk_404=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/nonexist.html") if [[ "$h2c_pk_404" == "404" ]]; then echo " ✓ h2c prior knowledge 404 — HTTP 404" pass else echo " ✗ h2c prior knowledge 404 — 期望 404, 实际 $h2c_pk_404" fail fi # h2c Upgrade 协商 h2c_up_status=$(curl --http2 -s -o /dev/null -w "%{http_code}" "http://$HOST/") h2c_up_version=$(curl --http2 -s -o /dev/null -w "%{http_version}" "http://$HOST/") if [[ "$h2c_up_status" == "200" && "$h2c_up_version" == "2" ]]; then echo " ✓ h2c Upgrade 协商 — HTTP 200, 协议 HTTP/2" pass else echo " ✗ h2c Upgrade 协商 — 期望 200+HTTP/2, 实际 $h2c_up_status+HTTP/$h2c_up_version" fail fi # h2c Upgrade 404 h2c_up_404=$(curl --http2 -s -o /dev/null -w "%{http_code}" "http://$HOST/nonexist.html") if [[ "$h2c_up_404" == "404" ]]; then echo " ✓ h2c Upgrade 404 — HTTP 404" pass else echo " ✗ h2c Upgrade 404 — 期望 404, 实际 $h2c_up_404" fail fi # HTTP/2 目录浏览测试 # subdir 目录没有 index.html,应返回 200 目录列表 h2c_dir_status=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/subdir/") h2c_dir_body=$(curl --http2-prior-knowledge -s "http://$HOST/subdir/") if [[ "$h2c_dir_status" == "200" ]] && echo "$h2c_dir_body" | grep -q "page.html"; then echo " ✓ h2c 目录浏览 — HTTP 200,包含 page.html" pass else echo " ✗ h2c 目录浏览 — 期望 200+page.html, 实际 $h2c_dir_status" fail fi # WebSocket 测试 # 停止 HTTP 服务器,恢复默认服务器 kill_server sleep 1 start_server # 中间件测试 echo "" echo "=== 中间件测试 ===" # CORS 测试 kill_server sleep 1 $SERVER -r "$ROOT" -p 9999 --cors > "$TMPDIR/server_cors.log" 2>&1 & for i in {1..30}; do if nc -z localhost 9999 2>/dev/null; then break; fi sleep 0.1 done cors_status=$(curl -s -o /dev/null -w "%{http_code}" -X OPTIONS "$BASE/" -H "Origin: http://example.com" -H "Access-Control-Request-Method: POST") if [[ "$cors_status" == "204" ]]; then echo " ✓ CORS OPTIONS 预检 — HTTP 204" pass else echo " ✗ CORS OPTIONS 预检 — 期望 204, 实际 $cors_status" fail fi # Basic Auth 测试 kill_server sleep 1 $SERVER -r "$ROOT" -p 9999 --auth-user "admin" --auth-pass "secret" > "$TMPDIR/server_auth.log" 2>&1 & for i in {1..30}; do if nc -z localhost 9999 2>/dev/null; then break; fi sleep 0.1 done auth_no_cred=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/") if [[ "$auth_no_cred" == "401" ]]; then echo " ✓ Basic Auth 无认证 — HTTP 401" pass else echo " ✗ Basic Auth 无认证 — 期望 401, 实际 $auth_no_cred" fail fi auth_with_cred=$(curl -s -o /dev/null -w "%{http_code}" -u "admin:secret" "$BASE/") if [[ "$auth_with_cred" == "200" ]]; then echo " ✓ Basic Auth 正确认证 — HTTP 200" pass else echo " ✗ Basic Auth 正确认证 — 期望 200, 实际 $auth_with_cred" fail fi auth_wrong_pass=$(curl -s -o /dev/null -w "%{http_code}" -u "admin:wrong" "$BASE/") if [[ "$auth_wrong_pass" == "401" ]]; then echo " ✓ Basic Auth 错误密码 — HTTP 401" pass else echo " ✗ Basic Auth 错误密码 — 期望 401, 实际 $auth_wrong_pass" fail fi # Rate Limit 测试 kill_server sleep 1 $SERVER -r "$ROOT" -p 9999 --rate-limit 1 > "$TMPDIR/server_ratelimit.log" 2>&1 & for i in {1..30}; do if nc -z localhost 9999 2>/dev/null; then break; fi sleep 0.1 done rl_first=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/") rl_second=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/") if [[ "$rl_first" == "200" && "$rl_second" == "429" ]]; then echo " ✓ Rate Limit 限流 — 第1次 200, 第2次 429" pass else echo " ✗ Rate Limit 限流 — 期望 200+429, 实际 $rl_first+$rl_second" fail fi # 恢复默认服务器 kill_server sleep 1 start_server echo "" echo "=== WebSocket 测试 ===" if python3 "$ROOT/../websocket_test.py" > "$TMPDIR/ws_test.log" 2>&1; then echo " ✓ WebSocket 握手 + echo — 通过" pass pass else echo " ✗ WebSocket 测试失败" cat "$TMPDIR/ws_test.log" fail fail fi echo "" echo "=== 结果汇总 ===" echo "通过: $PASS" echo "失败: $FAIL" echo "" if [[ "$FAIL" -gt 0 ]]; then echo "[test] 查看服务器日志:" cat "$TMPDIR/server.log" | tail -20 exit 1 fi echo "[test] 全部通过 ✓"