添加LOTTERY_E2E环境变量来控制E2E测试相关功能, 包括绕过验证码、登录限制和钱包API URL验证, 同时更新composer.json以包含E2E专用的提供者和服务。
This commit is contained in:
82
e2e/scripts/mock-wallet-server.mjs
Normal file
82
e2e/scripts/mock-wallet-server.mjs
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* E2E 主站钱包 mock:供 transfer-in/out 异常场景(504 / 业务拒绝)。
|
||||
*
|
||||
* 控制端点:
|
||||
* POST /_e2e/mode body: { "mode": "success" | "504" | "reject" }
|
||||
*
|
||||
* 业务端点(与 config lottery.main_site 默认路径一致):
|
||||
* POST /wallet/debit-for-lottery
|
||||
* POST /wallet/credit-from-lottery
|
||||
*/
|
||||
|
||||
import http from 'node:http';
|
||||
|
||||
const PORT = Number(process.env.E2E_MOCK_WALLET_PORT ?? 5555);
|
||||
let mode = process.env.E2E_MOCK_WALLET_MODE ?? 'success';
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve) => {
|
||||
const chunks = [];
|
||||
req.on('data', (c) => chunks.push(c));
|
||||
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
||||
});
|
||||
}
|
||||
|
||||
function json(res, status, body) {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(body));
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = req.url ?? '/';
|
||||
const method = req.method ?? 'GET';
|
||||
|
||||
if (method === 'POST' && url === '/_e2e/mode') {
|
||||
try {
|
||||
const raw = await readBody(req);
|
||||
const parsed = JSON.parse(raw || '{}');
|
||||
if (typeof parsed.mode === 'string') {
|
||||
mode = parsed.mode;
|
||||
}
|
||||
json(res, 200, { mode });
|
||||
} catch {
|
||||
json(res, 400, { error: 'invalid_json' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'GET' && url === '/_e2e/health') {
|
||||
json(res, 200, { ok: true, mode });
|
||||
return;
|
||||
}
|
||||
|
||||
const isWallet =
|
||||
method === 'POST' &&
|
||||
(url === '/wallet/debit-for-lottery' || url === '/wallet/credit-from-lottery' || url.startsWith('/wallet/'));
|
||||
|
||||
if (!isWallet) {
|
||||
json(res, 404, { error: 'not_found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === '504') {
|
||||
json(res, 504, { success: false, message: 'gateway_timeout' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'reject') {
|
||||
json(res, 200, { success: false, message: 'credit_denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
json(res, 200, {
|
||||
success: true,
|
||||
external_ref: `mock-${Date.now()}`,
|
||||
message: 'ok',
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`[e2e-mock-wallet] listening on http://127.0.0.1:${PORT} mode=${mode}`);
|
||||
});
|
||||
253
e2e/scripts/run.sh
Executable file
253
e2e/scripts/run.sh
Executable file
@@ -0,0 +1,253 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user