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

View File

@@ -0,0 +1,247 @@
/**
* E2E超管玩家管理
*
* 链路:
* 1. 创建玩家site_code=demo, username/password
* 2. 用新建玩家登录 → 拿 token
* 3. /admin/players/{id}/freeze → status=1
* 4. 玩家用 frozen 账号登录 → 403 PlayerAccountSuspended
* 5. /admin/players/{id}/unfreeze → status=0
* 6. 玩家又能登录
* 7. /admin/players 列表能找到新建玩家
* 8. /admin/players/{id}/wallets 返回钱包列表
*
* 不测 destroy避免把 e2e 自带的 demo_player 误删player id 不固定)。
*/
import { test, expect, request as pwRequest } from '@playwright/test';
import { adminLoginViaBypass, fetchPlayerCaptcha, e2e } from './_helper';
const SITE_CODE = 'demo';
const CURRENCY = (process.env.LOTTERY_DEFAULT_CURRENCY ?? 'NPR').toUpperCase();
const PWD = '12345678';
function uniqueUsername(): string {
// 玩家 username 规则 native 6-32 位字母数字下划线(参考 nativePlayerUsernameRules
return 'e2e_p_' + Math.random().toString(36).slice(2, 10);
}
async function adminCtxOf(token: string) {
return pwRequest.newContext({
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
});
}
async function playerLoginCtx(siteCode: string, username: string, password: string, captchaKey: string) {
const ctx = await pwRequest.newContext();
const r = await ctx.post('/api/v1/player/auth/login', {
data: {
site_code: siteCode,
username,
password,
captcha_key: captchaKey,
captcha_code: 'LOTTERY_E2E_BYPASS',
},
});
return { ctx, status: r.status(), body: (await r.json().catch(() => ({}))) as any };
}
test('超管创建玩家 + 玩家登录成功', async () => {
const session = await adminLoginViaBypass();
const admin = await adminCtxOf(session.accessToken);
const username = uniqueUsername();
const r = await admin.post('/api/v1/admin/players', {
data: {
site_code: SITE_CODE,
site_player_id: 'e2e-' + username,
username,
password: PWD,
default_currency: CURRENCY,
status: 0,
},
});
expect(r.ok(), `create player status=${r.status()}`).toBeTruthy();
const created = (await r.json()) as any;
expect(created.code).toBe(0);
const playerId = created.data.id;
expect(typeof playerId).toBe('number');
await admin.dispose();
// 玩家登录
const cap = await fetchPlayerCaptcha();
const { ctx, status, body } = await playerLoginCtx(SITE_CODE, username, PWD, cap.captcha_key);
expect(status, `player login status=${status}`).toBe(200);
expect(body.code).toBe(0);
expect(body.data.player.username).toBe(username);
await ctx.dispose();
});
test('超管 freeze → 玩家登录 403unfreeze → 又能登录', async () => {
const session = await adminLoginViaBypass();
const admin = await adminCtxOf(session.accessToken);
const username = uniqueUsername();
// 创建
const r = await admin.post('/api/v1/admin/players', {
data: {
site_code: SITE_CODE,
site_player_id: 'e2e-' + username,
username,
password: PWD,
default_currency: CURRENCY,
status: 0,
},
});
const created = (await r.json()) as any;
const playerId = created.data.id;
// freeze
const fz = await admin.post(`/api/v1/admin/players/${playerId}/freeze`);
expect(fz.ok(), `freeze status=${fz.status()}`).toBeTruthy();
const fzBody = (await fz.json()) as any;
expect(fzBody.data.status).toBe(1);
await admin.dispose();
// frozen 玩家登录应失败player_account_suspended
const cap1 = await fetchPlayerCaptcha();
const { ctx: ctx1, status: st1, body: b1 } = await playerLoginCtx(SITE_CODE, username, PWD, cap1.captcha_key);
expect([200, 403]).toContain(st1);
if (st1 === 200) {
expect(b1.code).not.toBe(0);
}
await ctx1.dispose();
// unfreeze
const admin2 = await adminCtxOf(session.accessToken);
const uf = await admin2.post(`/api/v1/admin/players/${playerId}/unfreeze`);
expect(uf.ok(), `unfreeze status=${uf.status()}`).toBeTruthy();
const ufBody = (await uf.json()) as any;
expect(ufBody.data.status).toBe(0);
await admin2.dispose();
// 玩家又能登录
const cap2 = await fetchPlayerCaptcha();
const { ctx: ctx2, status: st2, body: b2 } = await playerLoginCtx(SITE_CODE, username, PWD, cap2.captcha_key);
expect(st2, `player login after unfreeze status=${st2}`).toBe(200);
expect(b2.code).toBe(0);
await ctx2.dispose();
});
test('/admin/players 列表能找到新建玩家(按 username 搜索)', async () => {
const session = await adminLoginViaBypass();
const admin = await adminCtxOf(session.accessToken);
const username = uniqueUsername();
const r = await admin.post('/api/v1/admin/players', {
data: {
site_code: SITE_CODE,
site_player_id: 'e2e-' + username,
username,
password: PWD,
default_currency: CURRENCY,
status: 0,
},
});
const created = (await r.json()) as any;
const playerId = created.data.id;
// 列表搜索
const list = await admin.get(`/api/v1/admin/players?keyword=${encodeURIComponent(username)}&size=20`);
expect(list.ok(), `list status=${list.status()}`).toBeTruthy();
const body = (await list.json()) as any;
const items: any[] = body.data.items;
expect(items.length).toBeGreaterThanOrEqual(1);
const found = items.find((it) => it.id === playerId);
expect(found).toBeTruthy();
expect(found.username).toBe(username);
await admin.dispose();
});
test('/admin/players/{id}/wallets 信用盘玩家返回空钱包列表', async () => {
const session = await adminLoginViaBypass();
const admin = await adminCtxOf(session.accessToken);
const username = uniqueUsername();
const r = await admin.post('/api/v1/admin/players', {
data: {
site_code: SITE_CODE,
site_player_id: 'e2e-' + username,
username,
password: PWD,
default_currency: CURRENCY,
status: 0,
},
});
const created = (await r.json()) as any;
const playerId = created.data.id;
expect(created.data.funding_mode).toBe('credit');
// 信用盘玩家登录不会开立 player_wallets
const cap = await fetchPlayerCaptcha();
const { ctx, status, body } = await playerLoginCtx(SITE_CODE, username, PWD, cap.captcha_key);
expect(status).toBe(200);
expect(body.code).toBe(0);
await ctx.dispose();
// 查 wallet
const w = await admin.get(`/api/v1/admin/players/${playerId}/wallets`);
expect(w.ok(), `wallets status=${w.status()}`).toBeTruthy();
const wb = (await w.json()) as any;
const wallets: any[] = wb.data.wallets ?? [];
expect(wallets.length).toBe(0);
await admin.dispose();
});
test('创建玩家缺 site_player_id → 422', async () => {
const session = await adminLoginViaBypass();
const admin = await adminCtxOf(session.accessToken);
const username = uniqueUsername();
const r = await admin.post('/api/v1/admin/players', {
data: {
site_code: SITE_CODE,
username,
default_currency: CURRENCY,
},
});
expect([422, 400]).toContain(r.status());
await admin.dispose();
});
test('创建玩家重复 username → 409/422 业务失败', async () => {
const session = await adminLoginViaBypass();
const admin = await adminCtxOf(session.accessToken);
const username = uniqueUsername();
// 第一次
const r1 = await admin.post('/api/v1/admin/players', {
data: {
site_code: SITE_CODE,
site_player_id: 'e2e-dup-' + username,
username,
password: PWD,
default_currency: CURRENCY,
},
});
expect(r1.ok()).toBeTruthy();
// 第二次同 username不同 site_player_id
const r2 = await admin.post('/api/v1/admin/players', {
data: {
site_code: SITE_CODE,
site_player_id: 'e2e-dup2-' + username,
username,
password: PWD,
default_currency: CURRENCY,
},
});
// 业务失败HTTP 200 + code 非 0 是常见;或直接 4xx
const b2 = (await r2.json().catch(() => ({}))) as any;
if (r2.status() === 200) {
expect(b2.code).not.toBe(0);
} else {
expect([409, 422, 400, 500]).toContain(r2.status());
}
await admin.dispose();
});