添加LOTTERY_E2E环境变量来控制E2E测试相关功能, 包括绕过验证码、登录限制和钱包API URL验证, 同时更新composer.json以包含E2E专用的提供者和服务。
This commit is contained in:
172
e2e/tests/api/05.wallet-transfer.spec.ts
Normal file
172
e2e/tests/api/05.wallet-transfer.spec.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* E2E:玩家钱包 transfer-in / transfer-out
|
||||
*
|
||||
* 链路:
|
||||
* 1. transfer-in: 主站扣款 → 彩票钱包加款(e2e 走 stub,秒成功)
|
||||
* 2. transfer-out: 彩票钱包扣款 → 主站加款(同样 stub 秒成功)
|
||||
* 3. 余额一致性:transfer-in 后 balance 增加、available_balance 增加
|
||||
* 4. 幂等:同 idempotent_key 第二次返回同 transfer_no
|
||||
* 5. 余额不足 1001:把 balance 改到 0 → transfer-out → 1001
|
||||
* 6. 幂等冲突 1010:同 idempotent_key 第二次 amount 不同 → 1010
|
||||
*
|
||||
* 不覆盖(需外部主站 mock 才能跑,留 skip + 文档):
|
||||
* - 主站失败 1009(需真主站返 5xx)
|
||||
* - 主站超时 504/408 → 1002 pending_reconcile(需真主站返 timeout)
|
||||
* - 转入关 1004(需 .env 关 transfer_in_enabled,跑前要改)
|
||||
*/
|
||||
|
||||
import { test, expect, request as pwRequest } from '@playwright/test';
|
||||
import { playerLoginViaBypass, resetE2EPlayer, e2e } from './_helper';
|
||||
|
||||
const CURRENCY = (process.env.LOTTERY_DEFAULT_CURRENCY ?? 'NPR').toUpperCase();
|
||||
const IN_AMOUNT = 50_000; // 转入 50 元 NPR
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetE2EPlayer();
|
||||
});
|
||||
|
||||
async function ctxOf(token: string) {
|
||||
return pwRequest.newContext({
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
test('transfer-in: 加款 + 余额增加 + lottery_balance_after 一致', async () => {
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
const bal0 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
|
||||
const before = Number(bal0.data.balance);
|
||||
|
||||
const r = await ctx.post('/api/v1/wallet/transfer-in', {
|
||||
data: {
|
||||
amount: IN_AMOUNT,
|
||||
currency: CURRENCY,
|
||||
idempotent_key: `e2e-tin-${Date.now()}`,
|
||||
},
|
||||
});
|
||||
expect(r.ok(), `transfer-in status=${r.status()}`).toBeTruthy();
|
||||
const body = (await r.json()) as any;
|
||||
expect(body.code).toBe(0);
|
||||
expect(body.data.transfer_no).toMatch(/^T[IO]_[a-z0-9]+$/);
|
||||
expect(Number(body.data.amount)).toBe(IN_AMOUNT);
|
||||
expect(body.data.currency_code).toBe(CURRENCY);
|
||||
// lottery_balance_after = before + IN_AMOUNT
|
||||
expect(Number(body.data.lottery_balance_after)).toBe(before + IN_AMOUNT);
|
||||
|
||||
const bal1 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
|
||||
expect(Number(bal1.data.balance)).toBe(before + IN_AMOUNT);
|
||||
expect(Number(bal1.data.available_balance)).toBe(before + IN_AMOUNT);
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
test('transfer-out: 扣款 + 余额减少', async () => {
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
const bal0 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
|
||||
const before = Number(bal0.data.balance);
|
||||
|
||||
const r = await ctx.post('/api/v1/wallet/transfer-out', {
|
||||
data: {
|
||||
amount: IN_AMOUNT,
|
||||
currency: CURRENCY,
|
||||
idempotent_key: `e2e-tout-${Date.now()}`,
|
||||
},
|
||||
});
|
||||
expect(r.ok(), `transfer-out status=${r.status()}`).toBeTruthy();
|
||||
const body = (await r.json()) as any;
|
||||
expect(body.code).toBe(0);
|
||||
expect(Number(body.data.amount)).toBe(IN_AMOUNT);
|
||||
expect(Number(body.data.lottery_balance_after)).toBe(before - IN_AMOUNT);
|
||||
|
||||
const bal1 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
|
||||
expect(Number(bal1.data.balance)).toBe(before - IN_AMOUNT);
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
test('transfer-in 幂等:同 key 第二次返回同 transfer_no + 不重复加款', async () => {
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
const key = `e2e-tin-idem-${Date.now()}`;
|
||||
const payload = { amount: IN_AMOUNT, currency: CURRENCY, idempotent_key: key };
|
||||
|
||||
const r1 = await ctx.post('/api/v1/wallet/transfer-in', { data: payload });
|
||||
const b1 = (await r1.json()) as any;
|
||||
const t1 = b1.data.transfer_no;
|
||||
const bal1 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
|
||||
const after1 = Number(bal1.data.balance);
|
||||
|
||||
const r2 = await ctx.post('/api/v1/wallet/transfer-in', { data: payload });
|
||||
const b2 = (await r2.json()) as any;
|
||||
expect(b2.data.transfer_no).toBe(t1);
|
||||
|
||||
const bal2 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
|
||||
expect(Number(bal2.data.balance)).toBe(after1); // 余额没再变
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
test('transfer-in 幂等冲突 1010:同 key 第二次 amount 不同', async () => {
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
const key = `e2e-tin-conflict-${Date.now()}`;
|
||||
const r1 = await ctx.post('/api/v1/wallet/transfer-in', {
|
||||
data: { amount: IN_AMOUNT, currency: CURRENCY, idempotent_key: key },
|
||||
});
|
||||
expect(r1.ok()).toBeTruthy();
|
||||
|
||||
const r2 = await ctx.post('/api/v1/wallet/transfer-in', {
|
||||
data: { amount: IN_AMOUNT + 100, currency: CURRENCY, idempotent_key: key },
|
||||
});
|
||||
// 业务失败:HTTP 200,code=1010
|
||||
const b2 = (await r2.json()) as any;
|
||||
expect(Number(b2.code)).toBe(1010);
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
test('transfer-out 余额不足 1001:把余额改到 0 后转出', async () => {
|
||||
// 把 balance 改到 0
|
||||
await e2e('POST', '/player/set-balance', { balance: 0, currency: CURRENCY });
|
||||
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
const r = await ctx.post('/api/v1/wallet/transfer-out', {
|
||||
data: {
|
||||
amount: 100,
|
||||
currency: CURRENCY,
|
||||
idempotent_key: `e2e-tout-empty-${Date.now()}`,
|
||||
},
|
||||
});
|
||||
const body = (await r.json()) as any;
|
||||
expect(Number(body.code)).toBe(1001);
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
test('transfer-in 负数金额 → 422 校验失败', async () => {
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
const r = await ctx.post('/api/v1/wallet/transfer-in', {
|
||||
data: {
|
||||
amount: -100,
|
||||
currency: CURRENCY,
|
||||
idempotent_key: `e2e-tin-neg-${Date.now()}`,
|
||||
},
|
||||
});
|
||||
expect([422, 400]).toContain(r.status());
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
test('transfer-in 缺 idempotent_key → 422 校验失败', async () => {
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
const r = await ctx.post('/api/v1/wallet/transfer-in', {
|
||||
data: { amount: IN_AMOUNT, currency: CURRENCY },
|
||||
});
|
||||
expect([422, 400]).toContain(r.status());
|
||||
await ctx.dispose();
|
||||
});
|
||||
Reference in New Issue
Block a user