Some checks failed
lotterLaravel CI / test (push) Has been cancelled
添加LOTTERY_E2E环境变量来控制E2E测试相关功能, 包括绕过验证码、登录限制和钱包API URL验证, 同时更新composer.json以包含E2E专用的提供者和服务。
248 lines
7.9 KiB
TypeScript
248 lines
7.9 KiB
TypeScript
/**
|
||
* 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 → 玩家登录 403;unfreeze → 又能登录', 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();
|
||
});
|