This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user