文件
hyb-oa/server/tests/unit/services/paymentOrderXls.test.js
T
wangsli a5b7e18507
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
fix(detail-page-structure-progress-actions): 收敛操作面板定位并补充制单诊断
2026-08-03 16:41:32 +08:00

192 行
8.0 KiB
JavaScript

const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { encryptStringOrThrow } = require("../../../src/services/crypto");
const {
PaymentOrderXlsError,
buildCustomerSerial,
generatePaymentOrderXls,
} = require("../../../src/services/paymentOrderXls");
const FIELD_ENCRYPTION_KEY = Buffer.alloc(32, 7).toString("base64");
const config = { fieldEncryptionKey: FIELD_ENCRYPTION_KEY };
const encrypt = (value) => encryptStringOrThrow(config, value);
function buildBatch(overrides = {}) {
return {
batch_no: "PB-20260802-002",
payment_account_snapshot_json: {
bankInstitutionCode: "CCB",
accountName: "合成测试公司",
bankName: "中国建设银行",
accountNumberEncrypted: encrypt("6217000000000012345"),
},
items: [
{
id: "b",
entity_code: "BX-20260802-0002",
amount: "2.00",
snapshot_json: { payee: { name: "乙", bankName: "中国建设银行", bankInstitutionCode: "CCB", bankAccountEncrypted: encrypt("6222000000000056789") } },
},
{
id: "a",
entity_code: "BX-20260802-0001",
amount: "1.20",
snapshot_json: { payee: { name: "甲", bankName: "中国建设银行", bankInstitutionCode: "CCB", bankAccountEncrypted: encrypt("6222000000000012345") } },
},
],
...overrides,
};
}
describe("payment order BIFF8 generator", () => {
it("generates a BIFF8 workbook with stable rows, totals and customer serials", () => {
const first = generatePaymentOrderXls(config, buildBatch());
const second = generatePaymentOrderXls(config, buildBatch());
expect(first.buffer.subarray(0, 8).toString("hex")).toBe("d0cf11e0a1b11ae1");
expect(first.rowCount).toBe(2);
expect(first.serials).toEqual(second.serials);
expect(first.totalAmount).toBe("3.20");
});
it("fails the whole export and reports every blocking payee issue", () => {
const batch = buildBatch();
batch.items[1].snapshot_json.payee.bankInstitutionCode = "OTHER";
batch.items[0].snapshot_json.payee.bankAccountEncrypted = null;
try {
generatePaymentOrderXls(config, batch);
throw new Error("expected payment order generation to fail");
} catch (error) {
expect(error).toBeInstanceOf(PaymentOrderXlsError);
expect(error.code).toBe("payee_bank_not_supported");
expect(error.details.blockers).toEqual(expect.arrayContaining([
{ code: "payee_bank_not_supported", entityCode: "BX-20260802-0001" },
{ code: "payee_account_unavailable", entityCode: "BX-20260802-0002" },
]));
}
});
it("does not invent a bank display name when the frozen snapshot is incomplete", () => {
const batch = buildBatch();
delete batch.items[0].snapshot_json.payee.bankName;
expect(() => generatePaymentOrderXls(config, batch)).toThrow(expect.objectContaining({
code: "payee_bank_name_missing",
details: {
blockers: expect.arrayContaining([
{ code: "payee_bank_name_missing", entityCode: "BX-20260802-0002" },
]),
},
}));
});
it("validates customer serial length and character set without truncation", () => {
expect(buildCustomerSerial("PB-20260802-002", 1)).toBe("PB-20260802-002-R001");
expect(() => buildCustomerSerial("pb invalid", 1)).toThrow(PaymentOrderXlsError);
expect(() => buildCustomerSerial("PB-123456789012345678901234567890", 1)).toThrow(PaymentOrderXlsError);
});
it("fails closed when the fixed Python runtime is unavailable", () => {
let caught;
try {
generatePaymentOrderXls(config, buildBatch(), {
pythonBin: "/definitely-missing/payment-order-python",
});
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(PaymentOrderXlsError);
expect(caught).toMatchObject({ code: "payment_order_generator_unavailable" });
expect(caught.diagnostic).toContain("server/requirements/payment-order-biff8.txt");
expect(caught.diagnostic).toContain("PAYMENT_ORDER_PYTHON_BIN");
expect(JSON.stringify(caught)).not.toContain("payment-order-biff8.txt");
});
it("fails closed when the fixed Python dependencies are unavailable", () => {
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "payment-order-dependency-test-"));
try {
const fakePython = path.join(temporaryRoot, "python");
fs.writeFileSync(
fakePython,
"#!/bin/sh\nprintf '%s\\n' '[payment-order-template][ERROR] runtime_dependency_unavailable' >&2\nexit 70\n",
{ mode: 0o700 },
);
let caught;
try {
generatePaymentOrderXls(config, buildBatch(), { pythonBin: fakePython });
} catch (error) {
caught = error;
}
expect(caught).toMatchObject({ code: "payment_order_generator_unavailable" });
expect(caught.diagnostic).toContain("server/requirements/payment-order-biff8.txt");
expect(JSON.stringify(caught)).not.toContain("PAYMENT_ORDER_PYTHON_BIN");
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
});
it("fails closed when the controlled template hash drifts", () => {
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "payment-order-template-test-"));
try {
const scriptDirectory = path.join(temporaryRoot, "src", "scripts");
const assetDirectory = path.join(temporaryRoot, "src", "assets", "payment-order");
fs.mkdirSync(scriptDirectory, { recursive: true });
fs.mkdirSync(assetDirectory, { recursive: true });
const scriptPath = path.resolve(__dirname, "../../../src/scripts/fill-payment-order-template.py");
const templatePath = path.resolve(__dirname, "../../../src/assets/payment-order/ccb-payment-order-template.xls");
const copiedScriptPath = path.join(scriptDirectory, "fill-payment-order-template.py");
const copiedTemplatePath = path.join(assetDirectory, "ccb-payment-order-template.xls");
fs.copyFileSync(scriptPath, copiedScriptPath);
const template = fs.readFileSync(templatePath);
template[template.length - 1] ^= 1;
fs.writeFileSync(copiedTemplatePath, template);
expect(() => generatePaymentOrderXls(config, buildBatch(), {
scriptPath: copiedScriptPath,
})).toThrow(expect.objectContaining({ code: "payment_order_template_unavailable" }));
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
});
it("does not copy child stderr or account payload into the exposed error", () => {
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "payment-order-stderr-test-"));
try {
const fakePython = path.join(temporaryRoot, "python");
fs.writeFileSync(fakePython, "#!/bin/sh\ntee /dev/stderr >/dev/null\nexit 70\n", { mode: 0o700 });
let caught;
try {
generatePaymentOrderXls(config, buildBatch(), { pythonBin: fakePython });
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(PaymentOrderXlsError);
expect(caught.code).toBe("payment_order_generation_failed");
expect(JSON.stringify(caught)).not.toContain("6217000000000012345");
expect(JSON.stringify(caught)).not.toContain("6222000000000012345");
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
});
it.each([
["timeout", "#!/bin/sh\nwhile :; do :; done\n", { timeout: 20 }],
[
"oversized output",
"#!/bin/sh\ni=0\nwhile [ \"$i\" -lt 5000 ]; do printf X; i=$((i + 1)); done\n",
{ maxBuffer: 1024 },
],
])("fails closed on child %s", (_label, script, options) => {
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "payment-order-process-test-"));
try {
const fakePython = path.join(temporaryRoot, "python");
fs.writeFileSync(fakePython, script, { mode: 0o700 });
expect(() => generatePaymentOrderXls(config, buildBatch(), { pythonBin: fakePython, ...options }))
.toThrow(expect.objectContaining({ code: "payment_order_generation_failed" }));
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
});
});