文件
hyb-oa/server/tests/unit/services/sensitiveRecoveryContract.test.js
T
wangsli 1f8d5b658a 完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
2026-09-19 13:11:04 +08:00

146 行
7.2 KiB
JavaScript

const { randomBytes, createHash } = require("node:crypto");
const { spawnSync } = require("node:child_process");
const path = require("node:path");
const cryptoService = require("../../../src/services/crypto");
const {
checkSyntheticSample,
runSyntheticContract,
} = require("../../../../scripts/qa/check-sensitive-recovery-contract.cjs");
describe("K0 synthetic sensitive recovery contract", () => {
let config;
let plaintext;
let encrypted;
beforeEach(() => {
config = { fieldEncryptionKey: randomBytes(32).toString("base64") };
plaintext = `synthetic-only-${randomBytes(16).toString("hex")}`;
encrypted = cryptoService.encryptStringOrThrow(config, plaintext);
});
// Boolean assertions keep failed tests from printing fixture values/digests.
function expectNoFixture(output, additional = []) {
const key = Buffer.from(config.fieldEncryptionKey, "base64");
const forbidden = [
plaintext, encrypted, config.fieldEncryptionKey, key.toString("hex"),
createHash("sha256").update(key).digest("hex"),
createHash("sha256").update(config.fieldEncryptionKey).digest("hex"),
...encrypted.split(":").slice(1), ...additional,
];
expect(forbidden.some((value) => output.includes(value))).toBe(false);
}
it("round-trips with the actual production crypto exports and only reports status", () => {
const report = checkSyntheticSample(config, encrypted, plaintext);
expect(report).toEqual({ status: "PASS", reason: "SYNTHETIC_MATCH" });
expectNoFixture(JSON.stringify(report));
});
it.each([
["no config", undefined], ["no key", {}],
["null", { fieldEncryptionKey: null }], ["empty", { fieldEncryptionKey: "" }],
["whitespace", { fieldEncryptionKey: " " }],
["wrong type", { fieldEncryptionKey: 32 }],
["unparseable", { fieldEncryptionKey: "%%%" }],
["short", { fieldEncryptionKey: Buffer.alloc(31).toString("base64") }],
["long", { fieldEncryptionKey: Buffer.alloc(33).toString("base64") }],
])("stops for an unavailable or invalid key: %s", (_label, candidate) => {
expect(checkSyntheticSample(candidate, encrypted, plaintext)).toEqual({
status: "STOP", reason: "KEY_UNAVAILABLE_OR_INVALID",
});
});
it("stops for a different but valid 32-byte key", () => {
const wrongKey = Buffer.from(config.fieldEncryptionKey, "base64");
wrongKey[0] ^= 1;
const report = checkSyntheticSample({ fieldEncryptionKey: wrongKey.toString("base64") }, encrypted, plaintext);
expect(report).toEqual({ status: "STOP", reason: "DECRYPTION_FAILED" });
expectNoFixture(JSON.stringify(report), [wrongKey.toString("base64")]);
});
it.each([["iv", 1], ["tag", 2], ["ciphertext", 3]])("stops after tampering with %s", (_label, index) => {
const parts = encrypted.split(":");
const value = Buffer.from(parts[index], "base64url");
value[0] ^= 1;
parts[index] = value.toString("base64url");
const tampered = parts.join(":");
const report = checkSyntheticSample(config, tampered, plaintext);
expect(report).toEqual({ status: "STOP", reason: "DECRYPTION_FAILED" });
expectNoFixture(JSON.stringify(report), [tampered]);
});
it.each([["empty", ""], ["null", null], ["absent", undefined]])("marks %s fields NOT_TESTED", (_label, value) => {
expect(checkSyntheticSample(config, value, plaintext)).toEqual({ status: "NOT_TESTED", reason: "EMPTY_FIELD" });
});
it("does not let an empty field hide a missing key", () => {
expect(checkSyntheticSample({}, "", plaintext)).toEqual({ status: "STOP", reason: "KEY_UNAVAILABLE_OR_INVALID" });
});
it.each([
["legacy plaintext", "synthetic-unencrypted"], ["unknown version", "v2:a:b:c"],
["missing parts", "v1:a:b"], ["extra parts", "v1:a:b:c:d"],
["empty parts", "v1:::"], ["invalid encoding", "v1:%%:%%:%%"],
["wrong type", 123],
])("stops instead of treating a null decryption as success: %s", (_label, payload) => {
expect(checkSyntheticSample(config, payload, plaintext)).toEqual({ status: "STOP", reason: "INVALID_PAYLOAD" });
});
it("requires the synthetic expectation and detects a plaintext mismatch", () => {
expect(checkSyntheticSample(config, encrypted)).toEqual({ status: "STOP", reason: "SYNTHETIC_EXPECTATION_REQUIRED" });
const report = checkSyntheticSample(config, encrypted, "different-synthetic-value");
expect(report).toEqual({ status: "STOP", reason: "PLAINTEXT_MISMATCH" });
expectNoFixture(JSON.stringify(report));
});
it("drops raw exceptions, does not log, and never uses the logging decrypt branch", () => {
const marker = `synthetic-error-${randomBytes(8).toString("hex")}`;
const spies = ["log", "warn", "error", "info", "debug"].map((name) => vi.spyOn(console, name).mockImplementation(() => {}));
const safe = vi.spyOn(cryptoService, "decryptStringSafe").mockImplementation(() => { throw new Error(marker); });
try {
const throwingConfig = { get fieldEncryptionKey() { throw new Error(marker); } };
const report = checkSyntheticSample(throwingConfig, encrypted, plaintext);
expect(report).toEqual({ status: "STOP", reason: "DECRYPTION_FAILED" });
expectNoFixture(JSON.stringify(report), [marker]);
expect(runSyntheticContract().status).toBe("PASS");
expect(safe).not.toHaveBeenCalled();
expect(spies.every((spy) => spy.mock.calls.length === 0)).toBe(true);
} finally {
safe.mockRestore();
spies.forEach((spy) => spy.mockRestore());
}
});
it("keeps real custody and real-data decryption untested after the synthetic matrix passes", () => {
const report = runSyntheticContract();
expect(report.status).toBe("PASS");
expect(report.checks).toHaveLength(7);
expect(report.checks.every((check) => check.matched)).toBe(true);
expect(report).toMatchObject({ scope: "K0_SYNTHETIC_ONLY", originalKeySource: "UNKNOWN", custody: "NOT_TESTED", realDataDecryption: "NOT_TESTED" });
});
it("CLI ignores ambient key inputs and produces only the fixed aggregate contract", () => {
const script = path.resolve(__dirname, "../../../../scripts/qa/check-sensitive-recovery-contract.cjs");
const child = spawnSync(process.execPath, [script], {
encoding: "utf8",
env: { FIELD_ENCRYPTION_KEY: config.fieldEncryptionKey, DATABASE_URL: "synthetic-unused" },
});
expect(child.status).toBe(0);
expect(child.stderr.length).toBe(0);
expectNoFixture(child.stdout + child.stderr);
const report = JSON.parse(child.stdout);
expect(Object.keys(report).sort()).toEqual(["checks", "custody", "originalKeySource", "realDataDecryption", "scope", "status"]);
expect(report.status).toBe("PASS");
expect(report.checks.every((check) => Object.keys(check).sort().join(",") === "category,matched,reason,status")).toBe(true);
});
it("CLI rejects arbitrary arguments without echoing them", () => {
const script = path.resolve(__dirname, "../../../../scripts/qa/check-sensitive-recovery-contract.cjs");
const child = spawnSync(process.execPath, [script, plaintext], { encoding: "utf8", env: {} });
expect(child.status).toBe(1);
expect(child.stderr.length).toBe(0);
expectNoFixture(child.stdout + child.stderr);
expect(JSON.parse(child.stdout)).toEqual({ scope: "K0_SYNTHETIC_ONLY", status: "FAIL", reason: "ARGUMENTS_NOT_ALLOWED" });
});
});