Some checks failed
lotterLaravel CI / test (push) Has been cancelled
添加LOTTERY_E2E环境变量来控制E2E测试相关功能, 包括绕过验证码、登录限制和钱包API URL验证, 同时更新composer.json以包含E2E专用的提供者和服务。
254 lines
8.7 KiB
Bash
Executable File
254 lines
8.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# e2e 一键跑通:
|
||
# 1. 启 docker compose(postgres + redis)
|
||
# 2. 复制 .env.e2e → .env,注入强随机 JWT 密钥
|
||
# 3. migrate --seed,跑 LocalDemoSeeder(admin/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
|
||
|
||
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
|
||
if [[ ! -d node_modules/@playwright/test/.local-browsers ]]; then
|
||
npx playwright install chromium
|
||
fi
|
||
|
||
PLAYWRIGHT_JWT_SECRET=$(grep '^LOTTERY_NATIVE_JWT_SECRET=' "$ENV_FILE" | cut -d= -f2-) \
|
||
PLAYWRIGHT_API_URL="$API_URL" \
|
||
PLAYWRIGHT_ADMIN_URL="http://localhost:3801" \
|
||
PLAYWRIGHT_FRONT_URL="http://localhost:3800" \
|
||
REVERB_APP_KEY=$(grep '^REVERB_APP_KEY=' "$ENV_FILE" | cut -d= -f2-) \
|
||
REVERB_HOST=127.0.0.1 \
|
||
REVERB_PORT=8080 \
|
||
E2E_MOCK_WALLET_PORT=5555 \
|
||
E2E_ADMIN_USERNAME=admin \
|
||
E2E_ADMIN_PASSWORD=12345678 \
|
||
E2E_PLAYER_USERNAME=$(grep '^E2E_PLAYER_USERNAME=' "$ENV_E2E" | cut -d= -f2-) \
|
||
E2E_PLAYER_PASSWORD=$(grep '^E2E_PLAYER_PASSWORD=' "$ENV_E2E" | cut -d= -f2-) \
|
||
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"
|