Files
lotteryLaravel/e2e/scripts/run.sh
kang 3f29229499
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled
fix: 部分收付累计释额并扩展结算 E2E 与 CI
账期收付改为按累计已付同步 credit_ledger,修复多笔 partial_paid 与坏账核销重复释额;新增 API E2E(占成、权限、部分收付/坏账)与 GitHub Actions e2e-api job。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 09:48:01 +08:00

281 lines
9.9 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
# e2e 一键跑通:
# 1. 启 docker composepostgres + redis
# 2. 复制 .env.e2e → .env注入强随机 JWT 密钥
# 3. migrate --seed跑 LocalDemoSeederadmin/12345678 + demo_player
# 4. php artisan serve 起应用
# 5. 启动 queue:work异步开奖/广播)
# 6. 启动 reverb:start
# 7. npx playwright install chromium仅缺浏览器缓存时
# 8. npx playwright test
# 9. 失败时打 docker logs + Laravel 日志;结束时清理 compose
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
E2E_DIR="$ROOT_DIR/e2e"
COMPOSE_FILE="$E2E_DIR/docker-compose.yml"
ENV_FILE="$ROOT_DIR/.env"
ENV_E2E="$E2E_DIR/.env.e2e"
ARTIFACT_DIR="$E2E_DIR/artifacts"
LOG_DIR="$E2E_DIR/logs"
mkdir -p "$ARTIFACT_DIR" "$LOG_DIR"
API_URL="http://127.0.0.1:8000"
PUBLIC_URL="$API_URL/api/v1"
PIDS=()
cleanup() {
local code=$?
trap - INT TERM EXIT
echo
echo "==> Cleaning up e2e processes (code=$code)…"
for pid in "${PIDS[@]:-}"; do
[[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true
done
# docker compose 留给用户决定是否 down避免误删 pg volume
exit "$code"
}
trap cleanup INT TERM EXIT
require() {
command -v "$1" >/dev/null 2>&1 || { echo "Missing: $1"; exit 2; }
}
require docker
require php
require node
require npx
playwright_cache_dir() {
case "$(uname -s)" in
Darwin) echo "$HOME/Library/Caches/ms-playwright" ;;
Linux) echo "${XDG_CACHE_HOME:-$HOME/.cache}/ms-playwright" ;;
MINGW*|MSYS*|CYGWIN*) echo "${LOCALAPPDATA:-$HOME/AppData/Local}/ms-playwright" ;;
*) echo "${XDG_CACHE_HOME:-$HOME/.cache}/ms-playwright" ;;
esac
}
playwright_chromium_cached() {
local cache_dir
cache_dir="$(playwright_cache_dir)"
# UI 项目依赖 chromium_headless_shellAPI 仅 request 时可能不触发,但一并检测。
compgen -G "${cache_dir}/chromium-"* >/dev/null 2>&1 \
|| compgen -G "${cache_dir}/chromium_headless_shell-"* >/dev/null 2>&1 \
|| [[ -d "$E2E_DIR/node_modules/playwright-core/.local-browsers" ]]
}
echo "==> [1/8] docker compose up -d"
if ! docker image inspect postgres:16-alpine >/dev/null 2>&1 \
|| ! docker image inspect redis:7-alpine >/dev/null 2>&1; then
echo " pulling postgres/redis images (docker-credential-desktop workaround)"
mkdir -p /tmp/docker-e2e-nocreds
printf '%s\n' '{"auths":{}}' > /tmp/docker-e2e-nocreds/config.json
docker --config /tmp/docker-e2e-nocreds pull postgres:16-alpine
docker --config /tmp/docker-e2e-nocreds pull redis:7-alpine
fi
if ! docker info >/dev/null 2>&1; then
cat <<'EOF' >&2
!! Docker daemon 未运行。
启动 Docker Desktop / OrbStack / Colima 后重试。
macOS Docker Desktop: 打开 Docker.app
macOS OrbStack: open -a OrbStack
macOS Colima: colima start
或者不使用 docker直接本机起 PG端口 15432+ Redis端口 16379
跳过这一步,只跑 [4/8] 起的 .env + 后面步骤。
EOF
exit 5
fi
docker compose -f "$COMPOSE_FILE" up -d
echo "==> [2/8] wait for pg/redis health"
for i in {1..30}; do
if docker compose -f "$COMPOSE_FILE" ps --format json | grep -q '"Health":"healthy"' \
|| docker compose -f "$COMPOSE_FILE" ps | grep -E "(healthy|running)" >/dev/null; then
pg_ok=$(docker compose -f "$COMPOSE_FILE" exec -T postgres pg_isready -U lottery -d lottery_e2e 2>/dev/null || true)
redis_ok=$(docker compose -f "$COMPOSE_FILE" exec -T redis redis-cli ping 2>/dev/null || true)
if [[ "$pg_ok" == *"accepting connections"* && "$redis_ok" == "PONG" ]]; then
echo " pg/redis healthy"
break
fi
fi
sleep 1
if [[ $i -eq 30 ]]; then
echo "!! pg/redis not healthy after 30s; dumping compose logs"
docker compose -f "$COMPOSE_FILE" logs --no-color
exit 3
fi
done
echo "==> [3/8] install composer deps if missing"
if [[ ! -d vendor ]]; then
composer install --no-interaction --prefer-dist
fi
echo "==> [4/8] prepare .env"
if [[ ! -f "$ENV_FILE" ]]; then
cp "$ENV_E2E" "$ENV_FILE"
fi
# 同步关键 e2e 变量(不覆盖已存在的 APP_KEY避免触发额外 key:generate 流程)
python3 - <<PY
import os, re
src = "$ENV_E2E"
dst = "$ENV_FILE"
with open(src) as f: new = f.read()
with open(dst) as f: cur = f.read()
# 用 e2e 模板值覆盖,仅保留 APP_KEY
for line in new.splitlines():
if not line or line.lstrip().startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
if k.strip() == "APP_KEY":
continue
pattern = re.compile(rf"^{re.escape(k.strip())}=.*$", re.M)
if pattern.search(cur):
cur = pattern.sub(f"{k.strip()}={v}", cur)
else:
cur += ("\n" if not cur.endswith("\n") else "") + line
with open(dst, "w") as f: f.write(cur)
PY
# 强制覆盖关键 secret避免与主站/生产共用
php -r '
$env = "'"$ENV_FILE"'";
$c = file_get_contents($env);
$c = preg_replace("/^LOTTERY_NATIVE_JWT_SECRET=.*$/m", "LOTTERY_NATIVE_JWT_SECRET=" . bin2hex(random_bytes(32)), $c);
$c = preg_replace("/^REVERB_APP_SECRET=.*$/m", "REVERB_APP_SECRET=" . bin2hex(random_bytes(16)), $c);
file_put_contents($env, $c);
'
if ! grep -q "^APP_KEY=base64:" "$ENV_FILE"; then
php artisan key:generate --force
fi
echo "==> [5/8] lottery:db-init --fresh (e2e environment only)"
# 注意AGENTS.md 要求 migrate:fresh 须用户确认;本脚本为 e2e 自动化专用,仅作用于
# docker compose 内的 lottery_e2e 库,绝不触及生产。
# composer dump-autoload 让 E2EPlayerSeeder位于 e2e/database/seeders被发现。
composer dump-autoload --quiet
# 用统一入口migrate:fresh + FoundationSeeder + admin-auth-sync + LocalDemoSeeder
# lottery:db-init --fresh 内部已对 migrate 传 --force无需外层加。
DB_DATABASE=lottery_e2e php artisan lottery:db-init --fresh
# e2e 专用 seeder建可登录玩家带 password_hash。LocalDemoSeeder 不会建可登录玩家
DB_DATABASE=lottery_e2e php artisan db:seed --class='E2E\Seeders\E2EPlayerSeeder' --force
echo "==> [6/10] boot backend processes"
php artisan config:clear >/dev/null
export LOTTERY_NATIVE_JWT_SECRET="$(grep '^LOTTERY_NATIVE_JWT_SECRET=' "$ENV_FILE" | cut -d= -f2-)"
if [[ -z "${LOTTERY_NATIVE_JWT_SECRET}" ]]; then
LOTTERY_NATIVE_JWT_SECRET="$(openssl rand -hex 32)"
if grep -q '^LOTTERY_NATIVE_JWT_SECRET=' "$ENV_FILE"; then
perl -i -pe "s/^LOTTERY_NATIVE_JWT_SECRET=.*/LOTTERY_NATIVE_JWT_SECRET=${LOTTERY_NATIVE_JWT_SECRET}/" "$ENV_FILE"
else
echo "LOTTERY_NATIVE_JWT_SECRET=${LOTTERY_NATIVE_JWT_SECRET}" >>"$ENV_FILE"
fi
export LOTTERY_NATIVE_JWT_SECRET
fi
export LOTTERY_E2E=true
php artisan serve --host=127.0.0.1 --port=8000 >"$LOG_DIR/serve.log" 2>&1 &
PIDS+=($!)
php artisan queue:work redis --queue=broadcasts:countdown,broadcasts,default --tries=3 --timeout=120 >"$LOG_DIR/queue.log" 2>&1 &
PIDS+=($!)
php artisan reverb:start --host=127.0.0.1 --hostname=127.0.0.1 --port=8080 >"$LOG_DIR/reverb.log" 2>&1 &
PIDS+=($!)
node "$E2E_DIR/scripts/mock-wallet-server.mjs" >"$LOG_DIR/mock-wallet.log" 2>&1 &
PIDS+=($!)
ADMIN_DIR="$ROOT_DIR/../lotteryadmin"
FRONT_DIR="$ROOT_DIR/../lotteryfront"
if [[ "${E2E_UI:-1}" == "1" && -d "$ADMIN_DIR" ]]; then
echo "==> [7/10] start lotteryadmin (3801)"
(cd "$ADMIN_DIR" && LOTTERY_API_UPSTREAM="$API_URL" ALLOWED_DEV_ORIGINS=127.0.0.1 npm run dev) >"$LOG_DIR/admin-ui.log" 2>&1 &
PIDS+=($!)
fi
if [[ "${E2E_UI:-1}" == "1" && -d "$FRONT_DIR" ]]; then
echo "==> [7/10] start lotteryfront (3800)"
(cd "$FRONT_DIR" && LOTTERY_API_UPSTREAM="$API_URL" NEXT_PUBLIC_PLAYER_SITE_CODE=demo ALLOWED_DEV_ORIGINS=127.0.0.1 npm run dev) >"$LOG_DIR/front-ui.log" 2>&1 &
PIDS+=($!)
fi
echo "==> [8/10] wait for API healthy"
for i in {1..30}; do
if curl -fsS "$PUBLIC_URL/health" >/dev/null 2>&1; then
echo " API up"
break
fi
sleep 1
if [[ $i -eq 30 ]]; then
echo "!! API not healthy; logs:"
tail -n 50 "$LOG_DIR"/*.log
exit 4
fi
done
if [[ "${E2E_UI:-1}" == "1" ]]; then
echo "==> [9/10] wait for admin/front UI"
for i in {1..60}; do
admin_ok=false
front_ok=false
curl -fsS "http://localhost:3801/admin/login" >/dev/null 2>&1 && admin_ok=true
curl -fsS "http://localhost:3800/login" >/dev/null 2>&1 && front_ok=true
if [[ "$admin_ok" == true && "$front_ok" == true ]]; then
echo " UI up"
break
fi
sleep 2
if [[ $i -eq 60 ]]; then
echo "!! UI not ready; see admin-ui.log / front-ui.log"
fi
done
fi
echo "==> [10/10] playwright test"
cd "$E2E_DIR"
if [[ ! -d node_modules ]]; then
npm install
fi
# UI 默认 channel:chrome本机 Google Chrome仅显式要求 bundled 时才下载 ms-playwright Chromium。
if [[ "${PLAYWRIGHT_USE_BUNDLED_CHROMIUM:-0}" == "1" ]]; then
if ! playwright_chromium_cached; then
export PLAYWRIGHT_BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-$(playwright_cache_dir)}"
npx playwright install chromium
else
export PLAYWRIGHT_BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-$(playwright_cache_dir)}"
fi
fi
export PLAYWRIGHT_JWT_SECRET="$(grep '^LOTTERY_NATIVE_JWT_SECRET=' "$ENV_FILE" | cut -d= -f2-)"
export PLAYWRIGHT_API_URL="$API_URL"
export PLAYWRIGHT_ADMIN_URL="http://localhost:3801"
export PLAYWRIGHT_FRONT_URL="http://localhost:3800"
export REVERB_APP_KEY="$(grep '^REVERB_APP_KEY=' "$ENV_FILE" | cut -d= -f2-)"
export REVERB_HOST=127.0.0.1
export REVERB_PORT=8080
export E2E_MOCK_WALLET_PORT=5555
export E2E_ADMIN_USERNAME=admin
export E2E_ADMIN_PASSWORD=12345678
export E2E_PLAYER_USERNAME="$(grep '^E2E_PLAYER_USERNAME=' "$ENV_E2E" | cut -d= -f2-)"
export E2E_PLAYER_PASSWORD="$(grep '^E2E_PLAYER_PASSWORD=' "$ENV_E2E" | cut -d= -f2-)"
if [[ "${E2E_UI:-1}" != "1" ]]; then
set -- --project=api "$@"
fi
npx playwright test "$@"
rc=$?
if [[ $rc -ne 0 ]]; then
echo "==> Playwright failed; saving artifacts"
cp -R "$E2E_DIR/test-results" "$ARTIFACT_DIR/test-results-$(date +%s)" 2>/dev/null || true
cp -R "$E2E_DIR/playwright-report" "$ARTIFACT_DIR/playwright-report-$(date +%s)" 2>/dev/null || true
tail -n 200 "$LOG_DIR"/*.log > "$ARTIFACT_DIR/backend-logs-$(date +%s).log" || true
fi
exit "$rc"