Files
WorthPath/apps/api/test/credit-balance.test.ts
T

195 lines
6.5 KiB
TypeScript

import { fixtureFetch } from './backup-fixture';
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 } from '../src/zip';
const base = process.env.TEST_API_URL || 'http://127.0.0.1:3100/api';
test('liability account credit balances survive revisions, replay, calendar, privacy and ZIP restore', async () => {
const db = new PrismaClient(),
ids: string[] = [];
const origin = process.env.WEB_ORIGIN === '*' ? 'http://localhost:5173' : process.env.WEB_ORIGIN!;
async function user() {
const u = await db.user.create({
data: { username: 'wp_credit_' + randomUUID(), passwordHash: 'unused' },
});
ids.push(u.id);
const token = randomBytes(32).toString('hex');
await db.session.create({
data: {
id: createHash('sha256').update(token).digest('hex'),
userId: u.id,
expiresAt: new Date(Date.now() + 3600000),
},
});
return { id: u.id, cookie: 'wp_session=' + token };
}
async function call(path: string, cookie: string, method = 'GET', body?: unknown) {
const res = await fixtureFetch(base + path, {
method,
headers: {
Cookie: cookie,
Origin: origin,
...(body ? { 'Content-Type': 'application/json' } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
return { status: res.status, data: await res.json() };
}
try {
const a = await user(),
b = await user();
const initial = {
name: 'Card',
kind: 'account',
side: 'liability',
category: 'credit_card',
currency: 'CNY',
amount: '100',
date: '2026-09-01T10:00',
};
const create = await call('/positions', a.cookie, 'POST', initial);
assert.equal(create.status, 201);
const id = create.data.id;
const balance = async (amount: string, date = '2026-09-02T10:00') =>
call('/positions/' + id + '/revisions', a.cookie, 'POST', {
amount,
date,
reason: 'balance',
});
const credit = await balance('-25');
assert.equal(credit.status, 201);
assert.equal((await call('/positions/' + id, a.cookie)).data.amount, '-25');
let totals = (await call('/overview', a.cookie)).data;
assert.equal(totals.assets, '25.00');
assert.equal(totals.liabilities, '0.00');
assert.equal(totals.net, '25.00');
let day = (await call('/calendar/day?date=2026-09-02', a.cookie)).data;
assert.equal(day.items[0].amount, '125');
assert.equal(day.items[0].direction, 'income');
assert.equal(
(
await call('/positions/' + id + '/revisions/' + credit.data.id, b.cookie, 'PUT', {
amount: '-30',
date: '2026-09-02T10:00',
})
).status,
404,
);
assert.equal(
(
await call('/positions/' + id + '/revisions/' + credit.data.id, a.cookie, 'PUT', {
amount: '-30',
date: '2026-09-02T10:00',
})
).status,
200,
);
assert.equal((await call('/overview', a.cookie)).data.assets, '30.00');
assert.equal(
(await call('/calendar/day?date=2026-09-02', a.cookie)).data.items[0].amount,
'130',
);
assert.equal(
(await call('/positions/' + id + '/revisions/' + credit.data.id, a.cookie, 'DELETE')).status,
200,
);
assert.equal((await call('/overview', a.cookie)).data.liabilities, '100.00');
await balance('-20');
await balance('10', '2026-09-03T10:00');
day = (await call('/calendar/day?date=2026-09-03', a.cookie)).data;
assert.equal(day.items[0].direction, 'expense');
assert.equal(day.items[0].amount, '30');
const history = (await call('/positions/' + id + '/history', a.cookie)).data.items;
await call('/positions/' + id + '/revisions/' + history[0].id, a.cookie, 'DELETE');
assert.equal((await call('/positions/' + id, a.cookie)).data.amount, '-20');
assert.equal(
(
await call('/positions', a.cookie, 'POST', {
...initial,
name: 'Initial credit',
amount: '-5.01',
})
).status,
201,
);
assert.equal(
(
await call('/positions', a.cookie, 'POST', {
...initial,
name: 'Debt',
kind: 'debt',
category: 'personal',
amount: '10',
})
).status,
201,
);
assert.equal(
(
await call('/positions', a.cookie, 'POST', {
...initial,
name: 'Invalid debt',
kind: 'debt',
category: 'personal',
amount: '-1',
})
).status,
400,
);
assert.equal(
(
await call('/positions', a.cookie, 'POST', {
...initial,
name: 'Invalid asset',
kind: 'asset',
side: 'asset',
category: 'other',
amount: '-1',
})
).status,
400,
);
totals = (await call('/overview', a.cookie)).data;
assert.equal(totals.assets, '25.01');
assert.equal(totals.liabilities, '10.00');
assert.equal(totals.net, '15.01');
const trend = (await call('/trend?from=2026-09-01&to=2026-09-03', a.cookie)).data.items;
assert.equal(trend[0].assets, '5.01');
assert.equal(trend[0].liabilities, '110.00');
assert.equal(trend[1].assets, '25.01');
assert.equal(trend[1].liabilities, '10.00');
const zip = await fetch(base + '/backup', { headers: { Cookie: a.cookie } });
assert.equal(zip.status, 200);
const backup: any = await readBackupZip(Buffer.from(await zip.arrayBuffer()));
assert.equal(
(await call('/backup/restore-fixture', b.cookie, 'POST', { confirmed: true, backup })).status,
201,
);
const restored = (await call('/overview', b.cookie)).data;
assert.equal(restored.assets, '25.01');
assert.equal(restored.liabilities, '10.00');
assert.equal(restored.net, '15.01');
assert.equal(
(await call('/calendar/day?date=2026-09-02', b.cookie)).data.items[0].amount,
'120',
);
assert.equal(
(
await call('/positions/' + id, a.cookie, 'PATCH', {
name: 'Card',
category: 'credit_card',
hidden: true,
})
).status,
200,
);
assert.equal((await call('/positions/' + id, a.cookie)).status, 404);
assert.equal((await call('/overview', a.cookie)).data.assets, '5.01');
} finally {
await db.user.deleteMany({ where: { id: { in: ids } } });
await db.$disconnect();
}
});