feat: 添加E2E测试环境配置支持
Some checks failed
lotterLaravel CI / test (push) Has been cancelled

添加LOTTERY_E2E环境变量来控制E2E测试相关功能,
包括绕过验证码、登录限制和钱包API URL验证,
同时更新composer.json以包含E2E专用的提供者和服务。
This commit is contained in:
2026-06-18 14:42:22 +08:00
parent 6b2ea39ea1
commit 6ec9634704
45 changed files with 3702 additions and 6 deletions

129
e2e/tests/fixtures.ts Normal file
View File

@@ -0,0 +1,129 @@
/**
* e2e 共用 fixture玩家 / 超管登录、API 客户端、期号工具。
*
* 设计原则:
* - 走真 HTTPrequest.newContext不绕开鉴权
* - token 由各用例按需获取(每个 spec 自带 reset不跨测试串味
* - 任何 SQL 写操作走真 PG不允许 Http::fake / Bus::fake
*/
import { request as pwRequest, type APIRequestContext, type APIResponse } from '@playwright/test';
export interface PlayerSession {
accessToken: string;
expiresIn: number;
player: { id: number; site_code: string; username: string; funding_mode: string; auth_source: string };
}
export interface AdminSession {
accessToken: string;
tokenType: string;
expiresIn: number;
admin: { id: number; username: string; is_super_admin: boolean };
}
const API = process.env.PLAYWRIGHT_API_URL ?? 'http://127.0.0.1:8000';
export function apiUrl(path: string): string {
return new URL(path, API + '/').toString();
}
export async function expectOk<T = any>(resp: APIResponse, hint = ''): Promise<T> {
if (!resp.ok()) {
const body = await resp.text().catch(() => '');
throw new Error(`HTTP ${resp.status()} ${hint}\nURL: ${resp.url()}\nBody: ${body.slice(0, 800)}`);
}
return resp.json() as Promise<T>;
}
export async function playerLogin(password?: string): Promise<PlayerSession> {
const ctx = await pwRequest.newContext({ baseURL: API });
const resp = await ctx.post('/api/v1/player/auth/login', {
data: {
site_code: process.env.E2E_PLAYER_SITE_CODE ?? 'demo',
username: process.env.E2E_PLAYER_USERNAME ?? 'demo_player',
password: password ?? process.env.E2E_PLAYER_PASSWORD ?? '12345678',
},
});
const data = await expectOk<{ code: number; data: PlayerSession; msg?: string }>(resp, 'player login');
await ctx.dispose();
if (!data.data?.access_token) throw new Error('login ok but no access_token: ' + JSON.stringify(data));
return data.data;
}
export async function adminLogin(): Promise<AdminSession> {
const ctx = await pwRequest.newContext({ baseURL: API });
const captchaResp = await ctx.get('/api/v1/admin/auth/captcha');
const captchaBody = (await captchaResp.json()) as { data: { captcha_key: string } };
const resp = await ctx.post('/api/v1/admin/auth/login', {
data: {
account: process.env.E2E_ADMIN_USERNAME ?? 'admin',
password: process.env.E2E_ADMIN_PASSWORD ?? '12345678',
captcha_key: captchaBody.data.captcha_key,
captcha_code: 'LOTTERY_E2E_BYPASS',
},
});
const data = await expectOk<{ code: number; data: AdminSession; msg?: string }>(resp, 'admin login');
await ctx.dispose();
if (!data.data?.access_token) throw new Error('admin login ok but no access_token: ' + JSON.stringify(data));
return data.data;
}
export async function playerCtx(token: string): Promise<APIRequestContext> {
return pwRequest.newContext({
baseURL: API,
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
});
}
export async function adminCtx(token: string): Promise<APIRequestContext> {
return pwRequest.newContext({
baseURL: API,
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
});
}
/** 取当前开放、最近尚未开奖的期号(公开接口,无需登录)。
* 返回 null 表示当前没有可下注期号(大厅空),调用方应跳过下注用例。
*/
export async function fetchCurrentDrawNo(): Promise<{ draw_no: string; status: string | number; close_time?: string } | null> {
const ctx = await pwRequest.newContext({ baseURL: API });
const resp = await ctx.get('/api/v1/draw/current');
const body = await expectOk<{
code: number;
data: {
server_now_ms?: number;
data: { draw_no?: string; status?: string | number; close_time?: string } | null;
};
}>(resp, 'draw/current');
await ctx.dispose();
const snapshot = body.data?.data;
if (!snapshot?.draw_no) return null;
return snapshot as { draw_no: string; status: string | number; close_time?: string };
}
/** 取玩法/赔率/位元e2e 验证玩家下注时拼 bet payload 正确) */
export async function fetchPlayEffective(): Promise<any> {
const ctx = await pwRequest.newContext({ baseURL: API });
const resp = await ctx.get('/api/v1/play/effective');
const data = await expectOk<{ code: number; data: any }>(resp, 'play/effective');
await ctx.dispose();
return data.data;
}
/** 通过 /api/v1/_e2e/* 调一个 e2e 辅助端点POST/GET 通用) */
export async function e2e<T = any>(
method: 'GET' | 'POST',
path: string,
body?: Record<string, any>,
): Promise<T> {
const ctx = await pwRequest.newContext({ baseURL: API });
const resp = await ctx.fetch(`/api/v1/_e2e${path}`, {
method,
data: body ?? {},
headers: { 'Content-Type': 'application/json' },
});
const data = await expectOk<T>(resp, `e2e ${method} ${path}`);
await ctx.dispose();
return data;
}