添加LOTTERY_E2E环境变量来控制E2E测试相关功能, 包括绕过验证码、登录限制和钱包API URL验证, 同时更新composer.json以包含E2E专用的提供者和服务。
This commit is contained in:
134
e2e/tests/api/06.wallet-logs.spec.ts
Normal file
134
e2e/tests/api/06.wallet-logs.spec.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* E2E:钱包流水一致性
|
||||
*
|
||||
* 链路:
|
||||
* 1. reset → 余额 baseline
|
||||
* 2. transfer-in N → 看 /wallet/logs 出现 type=transfer_in 且 amount=N
|
||||
* 3. transfer-out M → 出现 type=transfer_out 且 amount=M
|
||||
* 4. 流水 total 增量为 in - out
|
||||
* 5. type 过滤:只查 transfer_in 不出现 transfer_out
|
||||
*
|
||||
* 与 05 不同:05 测单次 transfer 成功;06 测多笔后的流水呈现 + 过滤。
|
||||
*/
|
||||
|
||||
import { test, expect, request as pwRequest } from '@playwright/test';
|
||||
import { playerLoginViaBypass, resetE2EPlayer } from './_helper';
|
||||
|
||||
const CURRENCY = (process.env.LOTTERY_DEFAULT_CURRENCY ?? 'NPR').toUpperCase();
|
||||
const IN_1 = 12_000;
|
||||
const IN_2 = 8_000;
|
||||
const OUT_1 = 5_000;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetE2EPlayer();
|
||||
});
|
||||
|
||||
async function ctxOf(token: string) {
|
||||
return pwRequest.newContext({
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
async function transferIn(ctx: any, amount: number, key: string) {
|
||||
const r = await ctx.post('/api/v1/wallet/transfer-in', {
|
||||
data: { amount, currency: CURRENCY, idempotent_key: key },
|
||||
});
|
||||
expect(r.ok(), `transfer-in status=${r.status()}`).toBeTruthy();
|
||||
}
|
||||
|
||||
async function transferOut(ctx: any, amount: number, key: string) {
|
||||
const r = await ctx.post('/api/v1/wallet/transfer-out', {
|
||||
data: { amount, currency: CURRENCY, idempotent_key: key },
|
||||
});
|
||||
expect(r.ok(), `transfer-out status=${r.status()}`).toBeTruthy();
|
||||
}
|
||||
|
||||
test('transfer-in/out 后 /wallet/logs 流水一致性', async () => {
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
const keyIn1 = `e2e-log-in1-${Date.now()}`;
|
||||
const keyIn2 = `e2e-log-in2-${Date.now()}`;
|
||||
const keyOut1 = `e2e-log-out1-${Date.now()}`;
|
||||
await transferIn(ctx, IN_1, keyIn1);
|
||||
await transferIn(ctx, IN_2, keyIn2);
|
||||
await transferOut(ctx, OUT_1, keyOut1);
|
||||
|
||||
const r = await ctx.get(`/api/v1/wallet/logs?size=100¤cy=${CURRENCY}`);
|
||||
expect(r.ok(), `logs status=${r.status()}`).toBeTruthy();
|
||||
const body = (await r.json()) as any;
|
||||
|
||||
expect(body.code).toBe(0);
|
||||
expect(body.data.funding_mode).toBe('wallet');
|
||||
expect(body.data.ledger_source).toBeTruthy();
|
||||
|
||||
const items: any[] = body.data.items;
|
||||
expect(items.length).toBeGreaterThanOrEqual(3);
|
||||
|
||||
const ourKeys = new Set([keyIn1, keyIn2, keyOut1]);
|
||||
const ourTxns = items.filter((it) => ourKeys.has(it.idempotent_key));
|
||||
expect(ourTxns.length).toBe(3);
|
||||
|
||||
const ins = ourTxns.filter((it) => it.type === 'transfer_in');
|
||||
const outs = ourTxns.filter((it) => it.type === 'transfer_out');
|
||||
expect(ins.length).toBe(2);
|
||||
expect(outs.length).toBe(1);
|
||||
expect(ins.reduce((s, x) => s + Math.abs(x.amount_abs ?? x.amount), 0)).toBe(IN_1 + IN_2);
|
||||
expect(Math.abs(outs[0].amount_abs ?? outs[0].amount)).toBe(OUT_1);
|
||||
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
test('type=transfer_in 过滤:结果中不应出现 transfer_out', async () => {
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
await transferIn(ctx, IN_1, `e2e-logf-in1-${Date.now()}`);
|
||||
await transferOut(ctx, OUT_1, `e2e-logf-out1-${Date.now()}`);
|
||||
|
||||
const r = await ctx.get(`/api/v1/wallet/logs?type=transfer_in&size=100¤cy=${CURRENCY}`);
|
||||
const body = (await r.json()) as any;
|
||||
const items: any[] = body.data.items;
|
||||
expect(items.length).toBeGreaterThanOrEqual(1);
|
||||
for (const it of items) {
|
||||
expect(it.type).toBe('transfer_in');
|
||||
}
|
||||
// 不能有 transfer_out
|
||||
expect(items.some((it) => it.type === 'transfer_out')).toBe(false);
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
test('pending_reconcile 字段:本地 stub 走秒成功,列表应为空', async () => {
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
await transferIn(ctx, IN_1, `e2e-logp-in1-${Date.now()}`);
|
||||
|
||||
const r = await ctx.get(`/api/v1/wallet/logs?size=20¤cy=${CURRENCY}`);
|
||||
const body = (await r.json()) as any;
|
||||
expect(Array.isArray(body.data.pending_reconcile)).toBeTruthy();
|
||||
expect(body.data.pending_reconcile.length).toBe(0);
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
test('分页:page=1 size=2 + page=2 size=2 不重叠且能拼回完整列表', async () => {
|
||||
const session = await playerLoginViaBypass();
|
||||
const ctx = await ctxOf(session.access_token);
|
||||
|
||||
await transferIn(ctx, 100, `e2e-page-in1-${Date.now()}`);
|
||||
await transferIn(ctx, 200, `e2e-page-in2-${Date.now()}`);
|
||||
await transferIn(ctx, 300, `e2e-page-in3-${Date.now()}`);
|
||||
|
||||
const r1 = await ctx.get(`/api/v1/wallet/logs?page=1&size=2&type=transfer_in¤cy=${CURRENCY}`);
|
||||
const r2 = await ctx.get(`/api/v1/wallet/logs?page=2&size=2&type=transfer_in¤cy=${CURRENCY}`);
|
||||
const b1 = (await r1.json()) as any;
|
||||
const b2 = (await r2.json()) as any;
|
||||
expect(b1.data.items.length).toBeLessThanOrEqual(2);
|
||||
expect(b2.data.items.length).toBeLessThanOrEqual(2);
|
||||
|
||||
const ids1 = new Set(b1.data.items.map((x: any) => x.log_id ?? x.id));
|
||||
for (const it of b2.data.items) {
|
||||
expect(ids1.has(it.log_id ?? it.id)).toBe(false); // 不重叠
|
||||
}
|
||||
await ctx.dispose();
|
||||
});
|
||||
Reference in New Issue
Block a user