/** * 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(resp: APIResponse, hint = ''): Promise { 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; } export async function playerLogin(password?: string): Promise { 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 { 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 { return pwRequest.newContext({ baseURL: API, extraHTTPHeaders: { Authorization: `Bearer ${token}` }, }); } export async function adminCtx(token: string): Promise { 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 { 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( method: 'GET' | 'POST', path: string, body?: Record, ): Promise { 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(resp, `e2e ${method} ${path}`); await ctx.dispose(); return data; }