/** * 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(); });