Files
lotteryLaravel/e2e/tests/api/06.wallet-logs.spec.ts
kang 6ec9634704
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
feat: 添加E2E测试环境配置支持
添加LOTTERY_E2E环境变量来控制E2E测试相关功能,
包括绕过验证码、登录限制和钱包API URL验证,
同时更新composer.json以包含E2E专用的提供者和服务。
2026-06-18 14:42:22 +08:00

135 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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&currency=${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&currency=${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&currency=${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&currency=${CURRENCY}`);
const r2 = await ctx.get(`/api/v1/wallet/logs?page=2&size=2&type=transfer_in&currency=${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();
});