Some checks failed
lotterLaravel CI / test (push) Has been cancelled
添加LOTTERY_E2E环境变量来控制E2E测试相关功能, 包括绕过验证码、登录限制和钱包API URL验证, 同时更新composer.json以包含E2E专用的提供者和服务。
130 lines
4.8 KiB
TypeScript
130 lines
4.8 KiB
TypeScript
/**
|
||
* e2e 共用 fixture:玩家 / 超管登录、API 客户端、期号工具。
|
||
*
|
||
* 设计原则:
|
||
* - 走真 HTTP(request.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;
|
||
}
|