perf: optimize portfolio queries and workflows
This commit is contained in:
1 parent
da98d02661
commit
a7e0a0fe58
45 files changed
+4446
-637
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { history, overview, totals, type Holding, type Rate } from '../src/calculation';
|
||||
import { history, overview, totals, trend, type Holding, type Rate } from '../src/calculation';
|
||||
import { positionInput, date, amount, businessDate, toBusinessDate } from '../src/validation';
|
||||
import { validateBackup } from '../src/backup';
|
||||
import { RatesService } from '../src/rates';
|
||||
@@ -44,6 +44,29 @@ const rate = (value: string, day: string): Rate => ({
|
||||
rate: value,
|
||||
source: 'manual',
|
||||
});
|
||||
test('bounded replay seeds the range, preserves same-time order, FX attribution and missing rates', () => {
|
||||
const a = p('asset', 'USD');
|
||||
a.revisions = [
|
||||
{ ...rev('110', '2026-09-02'), sequence: 4 },
|
||||
{ ...rev('100', '2026-08-31'), sequence: 1 },
|
||||
{ ...rev('105', '2026-09-02'), sequence: 3 },
|
||||
];
|
||||
const rates = [rate('7', '2026-08-31'), rate('8', '2026-09-02')];
|
||||
const rows = trend([a], rates, 'CNY', '2026-09-01', '2026-09-03');
|
||||
assert.equal(rows[0].net, '700.00');
|
||||
assert.equal(rows[1].fxChange, '100.00');
|
||||
assert.equal(rows[1].balanceChange, '80.00');
|
||||
assert.equal(rows[2].net, '880.00');
|
||||
assert.equal('items' in rows[0], false);
|
||||
const sampled = trend([a], rates, 'CNY', '2026-09-01', '2026-09-03', 'month');
|
||||
assert.equal(sampled.length, 1);
|
||||
assert.equal(sampled[0].fxChange, '100.00');
|
||||
assert.equal(sampled[0].balanceChange, '80.00');
|
||||
const missing = trend([a], [rate('8', '2026-09-02')], 'CNY', '2026-09-01', '2026-09-03');
|
||||
assert.equal(missing[0].complete, false);
|
||||
assert.equal(missing[1].fxChange, null);
|
||||
assert.equal(missing[2].balanceChange, '0.00');
|
||||
});
|
||||
test('decimal totals and liability sign', () => {
|
||||
const a = p(),
|
||||
b = p('liability');
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'dotenv/config';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { readBackupZip, packBackup } from '../src/zip';
|
||||
const base = process.env.TEST_API_URL || 'http://127.0.0.1:3100/api';
|
||||
const origin = process.env.WEB_ORIGIN === '*' ? 'http://localhost:5173' : process.env.WEB_ORIGIN!;
|
||||
test('borrow/lend/collect/repay pair balances atomically and survive concurrency and restore', async () => {
|
||||
const db = new PrismaClient(),
|
||||
users: string[] = [];
|
||||
async function user() {
|
||||
const row = await db.user.create({
|
||||
data: { username: 'wp_debt_' + randomUUID(), passwordHash: 'not-a-login-hash' },
|
||||
});
|
||||
users.push(row.id);
|
||||
const token = randomBytes(32).toString('hex');
|
||||
await db.session.create({
|
||||
data: {
|
||||
id: createHash('sha256').update(token).digest('hex'),
|
||||
userId: row.id,
|
||||
expiresAt: new Date(Date.now() + 600000),
|
||||
},
|
||||
});
|
||||
return { id: row.id, token };
|
||||
}
|
||||
async function call(token: string, path: string, method = 'GET', body?: unknown) {
|
||||
const response = await fetch(base + path, {
|
||||
method,
|
||||
headers: {
|
||||
Cookie: 'wp_session=' + token,
|
||||
Origin: origin,
|
||||
...(body ? { 'Content-Type': 'application/json' } : {}),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
return {
|
||||
status: response.status,
|
||||
data: response.headers.get('content-type')?.includes('application/zip')
|
||||
? ((await readBackupZip(Buffer.from(await response.arrayBuffer()))) as any)
|
||||
: await response.json(),
|
||||
};
|
||||
}
|
||||
try {
|
||||
const a = await user(),
|
||||
b = await user();
|
||||
async function position(name: string, kind: string, side: string, amount = '0') {
|
||||
const result = await call(a.token, '/positions', 'POST', {
|
||||
name,
|
||||
kind,
|
||||
side,
|
||||
amount,
|
||||
currency: 'CNY',
|
||||
category: kind === 'debt' ? 'personal' : 'bank',
|
||||
date: '2026-09-01T00:00',
|
||||
});
|
||||
assert.equal(result.status, 201);
|
||||
return result.data.id as string;
|
||||
}
|
||||
const cash = await position('test account', 'account', 'asset', '1000'),
|
||||
borrowed = await position('test borrowed', 'debt', 'liability'),
|
||||
lent = await position('test lent', 'debt', 'asset');
|
||||
const request = (operation: string, targetId: string, amount: string, fee = '0') => ({
|
||||
requestId: randomUUID(),
|
||||
operation,
|
||||
sourceId: cash,
|
||||
targetId,
|
||||
amount,
|
||||
received: amount,
|
||||
fee,
|
||||
date: '2026-09-02T10:00',
|
||||
});
|
||||
const borrow = request('borrow', borrowed, '100', '2');
|
||||
const parallel = await Promise.all([
|
||||
call(a.token, '/transfers', 'POST', borrow),
|
||||
call(a.token, '/transfers', 'POST', borrow),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
parallel.map((r) => r.status),
|
||||
[201, 201],
|
||||
);
|
||||
assert.equal((await call(a.token, '/overview')).data.net, '998.00');
|
||||
assert.equal(
|
||||
(await call(a.token, '/transfers', 'POST', request('lend', lent, '200', '-2'))).status,
|
||||
201,
|
||||
);
|
||||
assert.equal((await call(a.token, '/overview')).data.net, '1000.00');
|
||||
assert.equal(
|
||||
(await call(a.token, '/transfers', 'POST', request('collect', lent, '50'))).status,
|
||||
201,
|
||||
);
|
||||
assert.equal(
|
||||
(await call(a.token, '/transfers', 'POST', request('repay', borrowed, '30', '1'))).status,
|
||||
201,
|
||||
);
|
||||
assert.equal((await call(a.token, '/overview')).data.net, '999.00');
|
||||
const list = (await call(a.token, '/positions')).data;
|
||||
assert.equal(list.find((p: any) => p.id === cash).amount, '919');
|
||||
assert.equal(list.find((p: any) => p.id === borrowed).amount, '70');
|
||||
assert.equal(list.find((p: any) => p.id === lent).amount, '150');
|
||||
assert.equal(list.find((p: any) => p.id === lent).outgoing[0].targetId, cash);
|
||||
const count = await db.revision.count({ where: { position: { userId: a.id } } });
|
||||
assert.equal(
|
||||
(await call(a.token, '/transfers', 'POST', request('repay', borrowed, '71'))).status,
|
||||
400,
|
||||
);
|
||||
assert.equal(
|
||||
(await call(a.token, '/transfers', 'POST', request('lend', borrowed, '10'))).status,
|
||||
400,
|
||||
);
|
||||
assert.equal(
|
||||
(await call(b.token, '/transfers', 'POST', request('repay', borrowed, '10'))).status,
|
||||
400,
|
||||
);
|
||||
assert.equal(await db.revision.count({ where: { position: { userId: a.id } } }), count);
|
||||
const race = await Promise.all(
|
||||
[1, 2].map(() => call(a.token, '/transfers', 'POST', request('collect', lent, '100'))),
|
||||
);
|
||||
assert.equal(race.filter((r) => r.status === 201).length, 1, JSON.stringify(race));
|
||||
assert.equal(race.filter((r) => r.status === 400 || r.status === 409).length, 1);
|
||||
const paired = (await call(a.token, '/positions/' + cash + '/history')).data.items.find(
|
||||
(r: any) => r.reason === 'loan_collect',
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await call(a.token, '/positions/' + cash + '/revisions/' + paired.id, 'PUT', {
|
||||
amount: '1',
|
||||
date: '2026-09-02T10:00',
|
||||
})
|
||||
).status,
|
||||
409,
|
||||
);
|
||||
// A later record at the same business minute can be corrected by sequence.
|
||||
assert.equal(
|
||||
(
|
||||
await call(a.token, '/positions/' + cash + '/revisions', 'POST', {
|
||||
amount: '1020',
|
||||
date: '2026-09-02T10:00',
|
||||
})
|
||||
).status,
|
||||
201,
|
||||
);
|
||||
const latest = (await call(a.token, '/positions/' + cash + '/history?limit=1')).data.items[0];
|
||||
assert.equal(
|
||||
(
|
||||
await call(a.token, '/positions/' + cash + '/revisions/' + latest.id, 'PUT', {
|
||||
amount: '1021',
|
||||
date: '2026-09-02T10:00',
|
||||
})
|
||||
).status,
|
||||
200,
|
||||
);
|
||||
const backup = (await call(a.token, '/backup')).data;
|
||||
assert.equal(JSON.parse(packBackup(backup)['manifest.json']).version, 6);
|
||||
assert.equal(backup.transfers.length, 5);
|
||||
assert.ok(backup.transfers.some((p: any) => p.operation === 'lend' && p.fee === '-2'));
|
||||
assert.equal(
|
||||
(await call(b.token, '/backup/import', 'POST', { confirmed: true, backup })).status,
|
||||
201,
|
||||
);
|
||||
assert.equal(
|
||||
(await call(b.token, '/overview')).data.net,
|
||||
(await call(a.token, '/overview')).data.net,
|
||||
);
|
||||
assert.equal(await db.transfer.count({ where: { userId: b.id } }), 5);
|
||||
const restored = (await call(b.token, '/positions?kind=debt')).data;
|
||||
assert.equal(restored.find((p: any) => p.side === 'asset').amount, '50');
|
||||
assert.equal(restored.find((p: any) => p.side === 'liability').amount, '70');
|
||||
} finally {
|
||||
await db.user.deleteMany({ where: { id: { in: users } } });
|
||||
await db.$disconnect();
|
||||
}
|
||||
});
|
||||
@@ -176,7 +176,11 @@ test('real MySQL: authentication, isolation, history, backup and atomic failures
|
||||
).status,
|
||||
201,
|
||||
);
|
||||
const d = (await call('/positions/' + bank, 'GET', undefined, a.cookie)).data;
|
||||
const d = {
|
||||
history: (
|
||||
await call('/positions/' + bank + '/history', 'GET', undefined, a.cookie)
|
||||
).data.items.reverse(),
|
||||
};
|
||||
assert.equal(
|
||||
(
|
||||
await call(
|
||||
@@ -200,7 +204,8 @@ test('real MySQL: authentication, isolation, history, backup and atomic failures
|
||||
200,
|
||||
);
|
||||
assert.equal(
|
||||
(await call('/positions/' + bank, 'GET', undefined, a.cookie)).data.history[1].delta,
|
||||
(await call('/positions/' + bank + '/history', 'GET', undefined, a.cookie)).data.items[0]
|
||||
.delta,
|
||||
'20',
|
||||
);
|
||||
assert.equal(
|
||||
@@ -249,7 +254,9 @@ test('real MySQL: authentication, isolation, history, backup and atomic failures
|
||||
).status,
|
||||
201,
|
||||
);
|
||||
const sameDay = (await call('/positions/' + bank, 'GET', undefined, a.cookie)).data.history;
|
||||
const sameDay = (
|
||||
await call('/positions/' + bank + '/history', 'GET', undefined, a.cookie)
|
||||
).data.items.reverse();
|
||||
assert.equal(sameDay.length, 3);
|
||||
assert.equal(sameDay[2].after, '120.1');
|
||||
assert.equal(sameDay[2].delta, '10');
|
||||
@@ -305,9 +312,12 @@ test('real MySQL: authentication, isolation, history, backup and atomic failures
|
||||
),
|
||||
);
|
||||
assert.equal(restored.find((p: { kind: string }) => p.kind === 'debt').outgoing.length, 2);
|
||||
const restoredHistory = restored.find(
|
||||
const restoredAccount = restored.find(
|
||||
(p: { kind: string; side: string }) => p.kind === 'account' && p.side === 'asset',
|
||||
).history;
|
||||
);
|
||||
const restoredHistory = (
|
||||
await call('/positions/' + restoredAccount.id + '/history', 'GET', undefined, b.cookie)
|
||||
).data.items.reverse();
|
||||
assert.deepEqual(
|
||||
restoredHistory.map((h: { after: string }) => h.after),
|
||||
sameDay.map((h: { after: string }) => h.after),
|
||||
|
||||
@@ -109,7 +109,9 @@ test('privacy, minute history, backup-gated clear and idle sessions remain user
|
||||
).status,
|
||||
201,
|
||||
);
|
||||
const history = (await call('/positions/' + hidden, 'GET', undefined, a.cookie)).data.history;
|
||||
const history = (
|
||||
await call('/positions/' + hidden + '/history', 'GET', undefined, a.cookie)
|
||||
).data.items.reverse();
|
||||
assert.deepEqual(
|
||||
history.map((h: { time: string }) => h.time),
|
||||
['2026-09-01T09:17', '2026-09-01T09:18'],
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import 'dotenv/config';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
||||
import { toBusinessDate } from '../src/validation';
|
||||
import { readBackupZip } from '../src/zip';
|
||||
const base = process.env.TEST_API_URL || 'http://127.0.0.1:3100/api';
|
||||
test('bounded APIs preserve seeds, predecessor balances, pagination, archive and FX scope', async () => {
|
||||
const db = new PrismaClient();
|
||||
const user = await db.user.create({
|
||||
data: { username: 'wp_query_' + randomUUID(), passwordHash: 'not-a-login-hash' },
|
||||
});
|
||||
const token = randomBytes(32).toString('hex'),
|
||||
sessionId = createHash('sha256').update(token).digest('hex');
|
||||
let restoredUserId: string | undefined;
|
||||
async function call(path: string) {
|
||||
const response = await fetch(base + path, { headers: { Cookie: 'wp_session=' + token } });
|
||||
return { status: response.status, data: await response.json() };
|
||||
}
|
||||
try {
|
||||
await db.session.create({
|
||||
data: { id: sessionId, userId: user.id, expiresAt: new Date(Date.now() + 600000) },
|
||||
});
|
||||
const make = (name: string, currency = 'CNY', hidden = false) =>
|
||||
db.position.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
name,
|
||||
currency,
|
||||
hidden,
|
||||
kind: 'account',
|
||||
side: 'asset',
|
||||
category: 'bank',
|
||||
notes: '',
|
||||
},
|
||||
});
|
||||
const a = await make('archive'),
|
||||
b = await make('foreign', 'USD'),
|
||||
hidden = await make('hidden', 'CNY', true);
|
||||
const revision = (positionId: string, amount: string, date: string) =>
|
||||
db.revision.create({
|
||||
data: {
|
||||
positionId,
|
||||
amount,
|
||||
effectiveDate: toBusinessDate(date),
|
||||
notes: '',
|
||||
reason: 'balance',
|
||||
},
|
||||
});
|
||||
await revision(a.id, '100', '2026-08-31T23:59');
|
||||
await revision(a.id, '119', '2026-09-02T00:01');
|
||||
await revision(a.id, '120', '2026-09-02T00:01');
|
||||
await revision(a.id, '110', '2026-09-01T00:01'); // Late insert does not replace the latest balance.
|
||||
await revision(b.id, '10', '2026-08-31T23:59');
|
||||
await revision(hidden.id, '3', '2026-09-01T00:01');
|
||||
await db.position.update({ where: { id: a.id }, data: { archived: true } });
|
||||
await db.exchangeRate.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
currency: 'USD',
|
||||
baseCurrency: 'CNY',
|
||||
date: new Date('2026-09-02'),
|
||||
rate: '7',
|
||||
source: 'manual',
|
||||
},
|
||||
});
|
||||
const list = (await call('/positions?kind=account')).data;
|
||||
assert.equal(list.length, 2);
|
||||
assert.equal(list.find((p: any) => p.id === a.id).amount, '120');
|
||||
assert.equal('history' in list[0], false);
|
||||
assert.equal('revisions' in list[0], false);
|
||||
const current = (await call('/overview')).data;
|
||||
assert.equal(current.net, '190.00');
|
||||
assert.equal('trend' in current, false);
|
||||
const trend = (await call('/trend?from=2026-09-01&to=2026-09-03')).data.items;
|
||||
assert.equal(trend[0].complete, false);
|
||||
assert.equal(trend[1].net, '190.00');
|
||||
assert.equal(trend[1].balanceChange, null);
|
||||
assert.equal(trend[2].fxChange, '0.00');
|
||||
assert.equal('items' in trend[0], false);
|
||||
const seen = new Set<string>();
|
||||
let cursor: string | null = null;
|
||||
do {
|
||||
const response = await call(
|
||||
'/positions/' + a.id + '/history?limit=1' + (cursor ? '&cursor=' + cursor : ''),
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const row = response.data.items[0];
|
||||
assert.equal(seen.has(row.id), false);
|
||||
seen.add(row.id);
|
||||
if (row.after === '120') {
|
||||
assert.equal(row.before, '119');
|
||||
assert.equal(row.delta, '1');
|
||||
}
|
||||
if (row.after === '119') {
|
||||
assert.equal(row.before, '110');
|
||||
assert.equal(row.delta, '9');
|
||||
}
|
||||
if (row.after === '110') assert.equal(row.before, '100');
|
||||
cursor = response.data.nextCursor;
|
||||
} while (cursor);
|
||||
assert.equal(seen.size, 4);
|
||||
const bounded = (await call('/positions/' + a.id + '/history?from=2026-09-02&to=2026-09-02'))
|
||||
.data.items;
|
||||
assert.equal(bounded[1].before, '110');
|
||||
assert.equal((await call('/history?limit=101')).status, 400);
|
||||
assert.equal((await call('/history?cursor=bad')).status, 400);
|
||||
assert.equal((await call('/trend?from=2020-01-01&to=2026-09-03')).status, 400);
|
||||
assert.equal((await call('/positions/' + hidden.id + '/history')).status, 404);
|
||||
await db.session.update({
|
||||
where: { id: sessionId },
|
||||
data: { revealUntil: new Date(Date.now() + 60000) },
|
||||
});
|
||||
assert.equal((await call('/overview')).data.net, '193.00');
|
||||
await db.user.update({ where: { id: user.id }, data: { baseCurrency: 'USD' } });
|
||||
assert.equal((await call('/overview')).data.complete, false);
|
||||
await db.exchangeRate.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
currency: 'CNY',
|
||||
baseCurrency: 'USD',
|
||||
date: new Date('2026-09-01'),
|
||||
rate: '0.1',
|
||||
source: 'manual',
|
||||
},
|
||||
});
|
||||
assert.equal((await call('/overview')).data.net, '22.30');
|
||||
await db.user.update({ where: { id: user.id }, data: { baseCurrency: 'CNY' } });
|
||||
await db.position.update({ where: { id: a.id }, data: { archived: false } });
|
||||
const target = await make('discount target');
|
||||
await revision(target.id, '0', '2026-09-01T00:00');
|
||||
const origin =
|
||||
process.env.WEB_ORIGIN === '*' ? 'http://localhost:5173' : process.env.WEB_ORIGIN!;
|
||||
const transfer = {
|
||||
requestId: randomUUID(),
|
||||
sourceId: a.id,
|
||||
targetId: target.id,
|
||||
amount: '100',
|
||||
received: '100',
|
||||
fee: '-2',
|
||||
date: '2026-09-03T00:01',
|
||||
};
|
||||
const post = async (path: string, body: unknown, cookie = token) => {
|
||||
const response = await fetch(base + path, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: 'wp_session=' + cookie,
|
||||
Origin: origin,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return { status: response.status, data: await response.json() };
|
||||
};
|
||||
assert.equal((await post('/transfers', transfer)).status, 201);
|
||||
assert.equal((await post('/transfers', transfer)).status, 201);
|
||||
const discounted = (await call('/positions')).data;
|
||||
assert.equal(discounted.find((p: any) => p.id === a.id).amount, '22');
|
||||
assert.equal(discounted.find((p: any) => p.id === target.id).amount, '100');
|
||||
assert.equal(
|
||||
(await post('/transfers', { ...transfer, requestId: randomUUID(), fee: '-101' })).status,
|
||||
400,
|
||||
);
|
||||
const exported = await fetch(base + '/backup', { headers: { Cookie: 'wp_session=' + token } });
|
||||
assert.equal(exported.status, 200);
|
||||
const backup = (await readBackupZip(Buffer.from(await exported.arrayBuffer()))) as any;
|
||||
assert.equal(backup.transfers[0].fee, '-2');
|
||||
const restored = await db.user.create({
|
||||
data: { username: 'wp_query_restore_' + randomUUID(), passwordHash: 'not-a-login-hash' },
|
||||
});
|
||||
restoredUserId = restored.id;
|
||||
const restoredToken = randomBytes(32).toString('hex');
|
||||
await db.session.create({
|
||||
data: {
|
||||
id: createHash('sha256').update(restoredToken).digest('hex'),
|
||||
userId: restored.id,
|
||||
expiresAt: new Date(Date.now() + 600000),
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
(await post('/backup/import', { confirmed: true, backup }, restoredToken)).status,
|
||||
201,
|
||||
);
|
||||
const restoredTransfer = await db.transfer.findFirstOrThrow({ where: { userId: restored.id } });
|
||||
assert.equal(restoredTransfer.fee.toString(), '-2');
|
||||
const restoredBalance = await db.revision.findUniqueOrThrow({
|
||||
where: { id: restoredTransfer.sourceRevisionId },
|
||||
});
|
||||
assert.equal(restoredBalance.amount.toString(), '22');
|
||||
} finally {
|
||||
if (restoredUserId) await db.user.delete({ where: { id: restoredUserId } });
|
||||
await db.user.delete({ where: { id: user.id } });
|
||||
await db.$disconnect();
|
||||
}
|
||||
});
|
||||
@@ -103,8 +103,8 @@ test('transfers are atomic, scoped, retry-safe, decimal exact and included in ba
|
||||
assert.equal(rows.find((p: any) => p.id === sourceId).amount, '74');
|
||||
assert.equal(rows.find((p: any) => p.id === targetId).amount, '35');
|
||||
assert.equal((await call('/overview', a.cookie)).data.net, '109.00');
|
||||
assert.equal((await call('/transfers', b.cookie)).data.length, 0);
|
||||
const revision = rows.find((p: any) => p.id === sourceId).history.at(-1);
|
||||
assert.equal((await call('/transfers', b.cookie)).data.items.length, 0);
|
||||
const revision = (await call('/positions/' + sourceId + '/history', a.cookie)).data.items[0];
|
||||
assert.equal(
|
||||
(
|
||||
await call(`/positions/${sourceId}/revisions/${revision.id}`, a.cookie, 'PUT', {
|
||||
@@ -188,7 +188,11 @@ test('transfers are atomic, scoped, retry-safe, decimal exact and included in ba
|
||||
}),
|
||||
),
|
||||
);
|
||||
assert.equal(race.filter((r) => r.status === 201).length, 1);
|
||||
assert.equal(
|
||||
race.filter((r) => r.status === 201).length,
|
||||
1,
|
||||
JSON.stringify(race.map((r) => ({ status: r.status, data: r.data }))),
|
||||
);
|
||||
assert.equal(race.filter((r) => [400, 409].includes(r.status)).length, 1);
|
||||
assert.equal(
|
||||
(
|
||||
@@ -249,7 +253,7 @@ test('transfers are atomic, scoped, retry-safe, decimal exact and included in ba
|
||||
category: 'bank',
|
||||
hidden: true,
|
||||
});
|
||||
const visibleTransfers = (await call('/transfers', a.cookie)).data;
|
||||
const visibleTransfers = (await call('/transfers', a.cookie)).data.items;
|
||||
assert.equal(visibleTransfers.length, 2);
|
||||
assert.ok(
|
||||
visibleTransfers.every((t: any) => t.sourceId !== sourceId && t.targetId !== sourceId),
|
||||
|
||||
Reference in new issue
Block a user