添加LOTTERY_E2E环境变量来控制E2E测试相关功能, 包括绕过验证码、登录限制和钱包API URL验证, 同时更新composer.json以包含E2E专用的提供者和服务。
This commit is contained in:
218
e2e/tests/api/_helper.ts
Normal file
218
e2e/tests/api/_helper.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 共享步骤:取 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 个 slot:first=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 };
|
||||
Reference in New Issue
Block a user