release: 1.1.0
TallyNote release / linux-x64 (push) Successful in 6m24s

This commit is contained in:
Qiufeng
2026-08-31 20:11:34 +08:00
parent cbe0d65320
commit 12495fb6a4
66 changed files with 4315 additions and 77 deletions
+94
View File
@@ -392,4 +392,98 @@ describe("TallyNote API", () => {
expect(deleted.json().expense.invoiceCount).toBe(0);
expect(deleted.json().expense.invoiceMissingReason).toBe("原始发票文件已丢失,无法重新取得");
});
it("组合 multipart 编辑一次提交字段和附件,并只递增一次版本", async () => {
const session = await login();
const initial = multipart([
{ name: "paidAt", value: "2026-08-27T12:00:00.000Z" },
{ name: "amount", value: "12.34" },
{ name: "note", value: "组合编辑前" },
{ name: "invoiceMissingReason", value: "供应商暂未开票" },
{ name: "paymentProofs", filename: "proof-a.png", contentType: "image/png", data: tinyPng },
]);
const created = await app.inject({
method: "POST",
url: "/api/expenses",
headers: { origin: config.publicOrigin, cookie: session.cookies, "x-csrf-token": session.csrf, "content-type": initial.contentType },
payload: initial.body,
});
expect(created.statusCode).toBe(201);
const before = created.json().expense as { id: string; version: number; paymentProofCount: number; invoiceCount: number };
const edit = multipart([
{ name: "paidAt", value: "2026-08-28T13:30:00.000Z" },
{ name: "amount", value: "18.90" },
{ name: "note", value: "组合编辑后" },
{ name: "version", value: String(before.version) },
{ name: "paymentProofs", filename: "proof-b.png", contentType: "image/png", data: tinyPng },
{ name: "invoices", filename: "invoice.xml", contentType: "application/xml", data: Buffer.from("<invoice />") },
]);
const updated = await app.inject({
method: "PATCH",
url: `/api/expenses/${before.id}`,
headers: { origin: config.publicOrigin, cookie: session.cookies, "x-csrf-token": session.csrf, "content-type": edit.contentType },
payload: edit.body,
});
expect(updated.statusCode).toBe(200);
const result = updated.json().expense as { version: number; amountCents: number; note: string; invoiceMissingReason: string | null; paymentProofCount: number; invoiceCount: number; attachments: Array<{ originalName: string }> };
expect(result).toMatchObject({ version: before.version + 1, amountCents: 1890, note: "组合编辑后", invoiceMissingReason: null, paymentProofCount: 2, invoiceCount: 1 });
expect(result.attachments.map((item) => item.originalName)).toEqual(expect.arrayContaining(["proof-a.png", "proof-b.png", "invoice.xml"]));
const auditCount = (database.sqlite.prepare("SELECT COUNT(*) AS count FROM audit_events WHERE target_id=? AND action='expense.updated'").get(before.id) as { count: number }).count;
expect(auditCount).toBe(1);
});
it("组合编辑版本冲突或金额非法时不落附件也不改变账目", async () => {
const session = await login();
const initial = multipart([
{ name: "paidAt", value: "2026-08-27T12:00:00.000Z" },
{ name: "amount", value: "12.34" },
{ name: "invoiceMissingReason", value: "暂时无法取得" },
{ name: "paymentProofs", filename: "proof.png", contentType: "image/png", data: tinyPng },
]);
const created = await app.inject({
method: "POST",
url: "/api/expenses",
headers: { origin: config.publicOrigin, cookie: session.cookies, "x-csrf-token": session.csrf, "content-type": initial.contentType },
payload: initial.body,
});
const before = created.json().expense as { id: string; version: number; amountCents: number; paymentProofCount: number };
const conflictForm = multipart([
{ name: "paidAt", value: "2026-08-29T12:00:00.000Z" },
{ name: "amount", value: "20.00" },
{ name: "note", value: "不应保存" },
{ name: "invoiceMissingReason", value: "暂时无法取得" },
{ name: "version", value: String(before.version + 1) },
{ name: "paymentProofs", filename: "orphan.png", contentType: "image/png", data: tinyPng },
]);
const conflictResponse = await app.inject({
method: "PATCH",
url: `/api/expenses/${before.id}`,
headers: { origin: config.publicOrigin, cookie: session.cookies, "x-csrf-token": session.csrf, "content-type": conflictForm.contentType },
payload: conflictForm.body,
});
expect(conflictResponse.statusCode).toBe(409);
const afterConflict = app.inject({ method: "GET", url: `/api/expenses/${before.id}`, headers: { cookie: session.cookies } });
const current = (await afterConflict).json().expense;
expect(current).toMatchObject({ version: before.version, amountCents: before.amountCents, paymentProofCount: before.paymentProofCount });
expect(current.attachments.some((item: { originalName: string }) => item.originalName === "orphan.png")).toBe(false);
const invalidForm = multipart([
{ name: "paidAt", value: "2026-08-29T12:00:00.000Z" },
{ name: "amount", value: "1000000000000.00" },
{ name: "invoiceMissingReason", value: "暂时无法取得" },
{ name: "version", value: String(before.version) },
{ name: "paymentProofs", filename: "invalid.png", contentType: "image/png", data: tinyPng },
]);
const invalidResponse = await app.inject({
method: "PATCH",
url: `/api/expenses/${before.id}`,
headers: { origin: config.publicOrigin, cookie: session.cookies, "x-csrf-token": session.csrf, "content-type": invalidForm.contentType },
payload: invalidForm.body,
});
expect(invalidResponse.statusCode).toBe(400);
const afterInvalid = (await app.inject({ method: "GET", url: `/api/expenses/${before.id}`, headers: { cookie: session.cookies } })).json().expense;
expect(afterInvalid).toMatchObject({ version: before.version, amountCents: before.amountCents, paymentProofCount: before.paymentProofCount });
expect(afterInvalid.attachments.some((item: { originalName: string }) => item.originalName === "invalid.png")).toBe(false);
});
});