Some checks failed
lotterLaravel CI / test (push) Has been cancelled
添加LOTTERY_E2E环境变量来控制E2E测试相关功能, 包括绕过验证码、登录限制和钱包API URL验证, 同时更新composer.json以包含E2E专用的提供者和服务。
121 lines
4.5 KiB
TypeScript
121 lines
4.5 KiB
TypeScript
/**
|
||
* E2E:钱包 + 下注 + 注单查看
|
||
*
|
||
* 链路:
|
||
* 1. 玩家登录 → 拿钱包余额
|
||
* 2. 取当前 draw_no(公开 draw.current)
|
||
* 3. POST /ticket/preview(不落库,只算钱)
|
||
* 4. POST /ticket/place(落库 + 扣 frozen)
|
||
* 5. GET /ticket/items/{ticket_no} 验真
|
||
* 6. 钱包余额减少(frozen 增加)
|
||
* 7. 同 client_trace_id 第二次 place → 幂等回放
|
||
*
|
||
* 不做结算(结算链路走 settlement service + command,e2e 单独测)。
|
||
*/
|
||
|
||
import { test, expect, request as pwRequest } from '@playwright/test';
|
||
import { playerLoginViaBypass, resetE2EPlayer, fetchCurrentDrawNo } from './_helper';
|
||
|
||
const CURRENCY = (process.env.LOTTERY_DEFAULT_CURRENCY ?? 'NPR').toUpperCase();
|
||
const BET_AMOUNT_MINOR = 1000;
|
||
|
||
test.beforeEach(async () => {
|
||
await resetE2EPlayer();
|
||
});
|
||
|
||
test('玩家登录 → /wallet/balance 返回币种 + 余额', async () => {
|
||
const session = await playerLoginViaBypass();
|
||
const ctx = await pwRequest.newContext({
|
||
extraHTTPHeaders: { Authorization: `Bearer ${session.access_token}` },
|
||
});
|
||
const r = await ctx.get('/api/v1/wallet/balance');
|
||
expect(r.ok(), `balance status=${r.status()}`).toBeTruthy();
|
||
const body = (await r.json()) as any;
|
||
expect(body.data.currency_code).toBe(CURRENCY);
|
||
expect(typeof body.data.balance).toBe('number');
|
||
expect(typeof body.data.available_balance).toBe('number');
|
||
expect(body.data.credit_line_mode).toBe(false);
|
||
await ctx.dispose();
|
||
});
|
||
|
||
test('preview → place → 拿 ticket_no + 余额变动', async () => {
|
||
const draw = await fetchCurrentDrawNo();
|
||
test.skip(draw === null, '当前无开放期号(draw.current 返回 null),跳过下注链路');
|
||
|
||
const session = await playerLoginViaBypass();
|
||
const ctx = await pwRequest.newContext({
|
||
extraHTTPHeaders: { Authorization: `Bearer ${session.access_token}` },
|
||
});
|
||
|
||
const bal0 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
|
||
const before = Number(bal0.data.balance);
|
||
|
||
const preview = await ctx.post('/api/v1/ticket/preview', {
|
||
data: {
|
||
draw_id: draw!.draw_no,
|
||
currency_code: CURRENCY,
|
||
client_trace_id: `e2e-preview-${Date.now()}`,
|
||
lines: [{ number: '1234', play_code: 'straight', amount: BET_AMOUNT_MINOR }],
|
||
},
|
||
});
|
||
expect(preview.ok(), `preview status=${preview.status()}`).toBeTruthy();
|
||
const previewBody = (await preview.json()) as any;
|
||
expect(previewBody.code).toBe(0);
|
||
|
||
const place = await ctx.post('/api/v1/ticket/place', {
|
||
data: {
|
||
draw_id: draw!.draw_no,
|
||
currency_code: CURRENCY,
|
||
client_trace_id: `e2e-place-${Date.now()}`,
|
||
lines: [{ number: '1234', play_code: 'straight', amount: BET_AMOUNT_MINOR }],
|
||
},
|
||
});
|
||
expect(place.ok(), `place status=${place.status()}`).toBeTruthy();
|
||
const placeBody = (await place.json()) as any;
|
||
expect(placeBody.code).toBe(0);
|
||
const ticketNo = placeBody.data.items?.[0]?.ticket_no ?? placeBody.data.ticket_no;
|
||
expect(ticketNo).toMatch(/^TK[0-9]+$/);
|
||
|
||
const show = await ctx.get(`/api/v1/ticket/items/${ticketNo}`);
|
||
expect(show.ok(), `items show status=${show.status()}`).toBeTruthy();
|
||
const showBody = (await show.json()) as any;
|
||
expect(showBody.data.ticket_no).toBe(ticketNo);
|
||
|
||
const bal1 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
|
||
const afterAvailable = Number(bal1.data.available_balance);
|
||
expect(afterAvailable).toBeLessThan(before);
|
||
|
||
await ctx.dispose();
|
||
});
|
||
|
||
test('同 client_trace_id 第二次 place → 幂等回放,不重复扣款', async () => {
|
||
const draw = await fetchCurrentDrawNo();
|
||
test.skip(draw === null, '当前无开放期号,跳过幂等用例');
|
||
|
||
const session = await playerLoginViaBypass();
|
||
const ctx = await pwRequest.newContext({
|
||
extraHTTPHeaders: { Authorization: `Bearer ${session.access_token}` },
|
||
});
|
||
|
||
const trace = `e2e-idem-${Date.now()}`;
|
||
const payload = {
|
||
draw_id: draw!.draw_no,
|
||
currency_code: CURRENCY,
|
||
client_trace_id: trace,
|
||
lines: [{ number: '5678', play_code: 'straight', amount: 500 }],
|
||
};
|
||
|
||
const r1 = await ctx.post('/api/v1/ticket/place', { data: payload });
|
||
expect(r1.ok(), `place1 status=${r1.status()}`).toBeTruthy();
|
||
const body1 = (await r1.json()) as any;
|
||
const ticket1 = body1.data.items?.[0]?.ticket_no ?? body1.data.ticket_no;
|
||
|
||
const r2 = await ctx.post('/api/v1/ticket/place', { data: payload });
|
||
expect(r2.ok(), `place2 status=${r2.status()}`).toBeTruthy();
|
||
const body2 = (await r2.json()) as any;
|
||
const ticket2 = body2.data.items?.[0]?.ticket_no ?? body2.data.ticket_no;
|
||
|
||
expect(ticket1).toBe(ticket2);
|
||
await ctx.dispose();
|
||
});
|