Files
lotteryLaravel/e2e/tests/api/_helper.ts
kang 6ec9634704
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
feat: 添加E2E测试环境配置支持
添加LOTTERY_E2E环境变量来控制E2E测试相关功能,
包括绕过验证码、登录限制和钱包API URL验证,
同时更新composer.json以包含E2E专用的提供者和服务。
2026-06-18 14:42:22 +08:00

219 lines
8.0 KiB
TypeScript
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.
/**
* 共享步骤:取 captcha、登录玩家、登录超管、获取/重置玩家状态。
*/
import { test, request as pwRequest, type APIRequestContext, expect } from '@playwright/test';
import { adminLogin, playerLogin, playerCtx, adminCtx, fetchCurrentDrawNo, e2e } from '../fixtures';
export const E2E_TAG = '@e2e';
export async function fetchPlayerCaptcha(): Promise<{ captcha_key: string; image_svg: string }> {
const ctx = await pwRequest.newContext();
const resp = await ctx.get('/api/v1/player/auth/captcha');
expect(resp.ok(), `GET /player/auth/captcha failed: ${resp.status()}`).toBeTruthy();
const body = (await resp.json()) as { data: { captcha_key: string; image_svg: string; image_base64: string } };
await ctx.dispose();
return { captcha_key: body.data.captcha_key, image_svg: body.data.image_svg };
}
export async function fetchAdminCaptcha(): Promise<{ captcha_key: string }> {
const ctx = await pwRequest.newContext();
const resp = await ctx.get('/api/v1/admin/auth/captcha');
expect(resp.ok(), `GET /admin/auth/captcha failed: ${resp.status()}`).toBeTruthy();
const body = (await resp.json()) as { data: { captcha_key: string } };
await ctx.dispose();
return { captcha_key: body.data.captcha_key };
}
/** 用 LOTTERY_E2E_BYPASS 登录(管理端 captcha 旁路) */
export async function adminLoginViaBypass(): Promise<ReturnType<typeof adminLogin>> {
const captcha = await fetchAdminCaptcha();
const ctx = await pwRequest.newContext();
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: captcha.captcha_key,
captcha_code: 'LOTTERY_E2E_BYPASS',
},
});
expect(resp.ok(), `admin login failed: ${resp.status()}`).toBeTruthy();
const body = (await resp.json()) as { data: any };
await ctx.dispose();
if (!body.data?.token) throw new Error('admin login returned no token: ' + JSON.stringify(body));
return {
accessToken: body.data.token,
tokenType: body.data.token_type ?? 'Bearer',
expiresIn: 0,
admin: body.data.admin,
};
}
/** 用 LOTTERY_E2E_BYPASS 登录(玩家端 captcha 旁路) */
export async function playerLoginViaBypass(creds?: {
site_code?: string;
username?: string;
password?: string;
}): Promise<ReturnType<typeof playerLogin>> {
const captcha = await fetchPlayerCaptcha();
const ctx = await pwRequest.newContext();
const resp = await ctx.post('/api/v1/player/auth/login', {
data: {
site_code: creds?.site_code ?? process.env.E2E_PLAYER_SITE_CODE ?? 'demo',
username: creds?.username ?? process.env.E2E_PLAYER_USERNAME ?? 'demo_player',
password: creds?.password ?? process.env.E2E_PLAYER_PASSWORD ?? '12345678',
captcha_key: captcha.captcha_key,
captcha_code: 'LOTTERY_E2E_BYPASS',
},
});
expect(resp.ok(), `player login failed: ${resp.status()}`).toBeTruthy();
const body = (await resp.json()) as { data: any };
await ctx.dispose();
if (!body.data?.access_token) throw new Error('player login returned no token: ' + JSON.stringify(body));
return body.data;
}
/** 重置 e2e 玩家(清失败计数、解除锁定、恢复初始余额) */
export async function resetE2EPlayer(): Promise<void> {
await e2e('POST', '/player/reset', {});
}
/** 把期号 close_time 改到过去status=closed */
export async function forceCloseDraw(drawNo: string): Promise<any> {
return e2e('POST', `/draw/${encodeURIComponent(drawNo)}/close-now`, {});
}
export async function finishDrawCooldown(drawNo: string): Promise<any> {
return e2e('POST', `/draw/${encodeURIComponent(drawNo)}/finish-cooldown`, {});
}
export async function tickDraws(): Promise<any> {
return e2e('POST', '/draw/tick', {});
}
export async function inspectDraw(drawNo: string): Promise<any> {
return e2e('GET', `/draw/${encodeURIComponent(drawNo)}/inspect`, {});
}
export async function waitForDrawStatus(
drawNo: string,
targets: string[],
maxAttempts = 20,
pauseMs = 500,
): Promise<any> {
for (let i = 0; i < maxAttempts; i++) {
const insp = await inspectDraw(drawNo);
const status = String(insp.data?.status ?? insp.status ?? '');
if (targets.includes(status)) {
return insp;
}
await tickDraws();
await sleep(pauseMs);
}
const last = await inspectDraw(drawNo);
throw new Error(
`draw ${drawNo} not reached ${targets.join('|')} after ${maxAttempts} ticks; last=${JSON.stringify(last).slice(0, 400)}`,
);
}
export function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
export async function adminCtxOf(token: string): Promise<APIRequestContext> {
return pwRequest.newContext({
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
});
}
export async function playerCtxOf(token: string): Promise<APIRequestContext> {
return pwRequest.newContext({
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
});
}
export async function setupCreditPlayer(opts?: { credit_limit?: number; username?: string }): Promise<{
player_id: number;
username: string;
password: string;
site_code: string;
admin_site_id: number;
agent_node_id: number;
}> {
const data = await e2e<any>('POST', '/credit-player/setup', opts ?? {});
return data.data;
}
export async function mintSsoJwt(sitePlayerId?: string): Promise<{ jwt: string; site_player_id: string }> {
const data = await e2e<any>('POST', '/sso/mint-jwt', {
site_player_id: sitePlayerId ?? `e2e-sso-${Date.now()}`,
});
return data.data;
}
export async function configureWalletMock(baseUrl: string): Promise<void> {
await e2e('POST', '/site/wallet-api', { base_url: baseUrl, wallet_api_key: 'e2e-mock-key' });
}
export async function resetWalletMock(): Promise<void> {
await e2e('POST', '/site/wallet-api/reset', {});
}
export async function setMockWalletMode(mode: 'success' | '504' | 'reject'): Promise<void> {
const port = process.env.E2E_MOCK_WALLET_PORT ?? '5555';
const ctx = await pwRequest.newContext({ baseURL: `http://127.0.0.1:${port}` });
const resp = await ctx.post('/_e2e/mode', { data: { mode } });
expect(resp.ok(), `mock wallet mode=${mode} failed`).toBeTruthy();
await ctx.dispose();
}
/** 造 23 个 slotfirst=winNumber其余填占位号 */
export function buildAllResultItems(winNumber: string): any[] {
const items: any[] = [];
items.push({ prize_type: 'first', prize_index: 0, number_4d: winNumber });
items.push({ prize_type: 'second', prize_index: 0, number_4d: '5678' });
items.push({ prize_type: 'third', prize_index: 0, number_4d: '0123' });
for (let i = 0; i < 10; i++) {
items.push({
prize_type: 'starter',
prize_index: i,
number_4d: String(1000 + i).padStart(4, '0'),
});
}
for (let i = 0; i < 10; i++) {
items.push({
prize_type: 'consolation',
prize_index: i,
number_4d: String(2000 + i).padStart(4, '0'),
});
}
return items;
}
export async function resolveDrawId(admin: APIRequestContext, drawNo: string): Promise<number> {
const drawsList = await admin.get(`/api/v1/admin/draws?keyword=${encodeURIComponent(drawNo)}&size=10`);
const dl = (await drawsList.json()) as any;
const drawId =
dl.data.items.find((it: any) => it.draw_no === drawNo)?.id ?? dl.data.items[0]?.id;
expect(drawId, `draw id for ${drawNo}`).toBeTruthy();
return Number(drawId);
}
export async function fetchOpenDrawNo(): Promise<string> {
const current = await fetchCurrentDrawNo();
if (!current?.draw_no) {
throw new Error('draw/current returned no draw_no');
}
const drawNo = current.draw_no;
const insp = await e2e<any>('GET', `/draw/${encodeURIComponent(drawNo)}/inspect`);
const status = String(insp.data?.status ?? '');
if (status !== 'open') {
await e2e('POST', `/draw/${encodeURIComponent(drawNo)}/reopen`, {});
const after = await e2e<any>('GET', `/draw/${encodeURIComponent(drawNo)}/inspect`);
expect(String(after.data?.status), `draw ${drawNo} reopen`).toBe('open');
}
return drawNo;
}
export { playerLogin, playerCtx, adminLogin, adminCtx, fetchCurrentDrawNo, e2e };