文件
hyb-oa/server/tests/unit/scripts/ocr-provider-samples.test.js
T
wangsli 5f2788ccac
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
增加铁路OCR金额字段安全诊断(07-28-travel-reimbursement-guide)
2026-07-29 15:07:58 +08:00

229 行
6.9 KiB
JavaScript

const path = require("node:path");
const { pathToFileURL } = require("node:url");
const { spawnSync } = require("node:child_process");
const ROOT_DIR = path.resolve(__dirname, "../../../..");
const SCRIPT_PATH = path.join(ROOT_DIR, "scripts/qa/check-ocr-provider-samples.mjs");
const OCR_PDF_MAX_BYTES = 7 * 1024 * 1024;
function syntheticPdf(body = "synthetic") {
return Buffer.from(`%PDF-1.7\n${body}\n%%EOF\n`, "ascii");
}
function providerConfig() {
return {
ocrProvider: "tencent",
tencentSecretId: "synthetic-id",
tencentSecretKey: "synthetic-key",
tencentOcrRegion: "ap-beijing",
ocrTimeoutMs: 5000,
};
}
function normalizedResult(overrides = {}) {
return {
documentSubtype: "VatElectronicCommonInvoice",
documentCategory: "vat_invoice",
documentCount: 1,
invoiceCode: "synthetic-code",
invoiceNumber: "synthetic-number",
invoiceDate: "2026-01-01",
amount: "1.00",
taxAmount: "0.01",
totalAmount: "1.01",
sellerName: "synthetic-seller",
buyerName: "synthetic-buyer",
transportMode: "pdf_native",
...overrides,
};
}
describe("check-ocr-provider-samples", () => {
let liveRunner;
beforeAll(async () => {
liveRunner = await import(pathToFileURL(SCRIPT_PATH).href);
});
it.each([
["vat_invoice", normalizedResult()],
[
"full_vat_invoice",
normalizedResult({
documentSubtype: "VatElectronicInvoiceFull",
documentCategory: "full_vat_invoice",
invoiceCode: null,
}),
],
[
"electronic_train_ticket",
normalizedResult({
documentSubtype: "ElectronicTrainTicketFull",
documentCategory: "electronic_train_ticket",
invoiceCode: null,
sellerName: null,
}),
],
])("accepts category-aware required fields for %s", async (_category, result) => {
const runOcr = vi.fn().mockResolvedValue(result);
const summary = await liveRunner.runLiveSampleCheck({
alias: "SAMPLE-PDF-TEST",
buffer: syntheticPdf(),
config: providerConfig(),
runOcr,
now: vi.fn().mockReturnValueOnce(100).mockReturnValueOnce(125),
});
expect(summary.status).toBe("PASS");
expect(summary.elapsedMs).toBe(25);
expect(summary.documentCategory).toBe(result.documentCategory);
expect(summary.missingRequiredFields).toEqual([]);
expect(runOcr).toHaveBeenCalledTimes(1);
expect(runOcr.mock.calls[0][0]).toMatchObject({
ocrType: "vat",
sourceMimeType: "application/pdf",
});
});
it("holds a railway ticket when its buyer field is missing", async () => {
const summary = await liveRunner.runLiveSampleCheck({
alias: "SAMPLE-PDF-TRAIN",
buffer: syntheticPdf(),
config: providerConfig(),
runOcr: vi.fn().mockResolvedValue(
normalizedResult({
documentSubtype: "ElectronicTrainTicketFull",
documentCategory: "electronic_train_ticket",
invoiceCode: null,
sellerName: null,
buyerName: null,
}),
),
});
expect(summary.status).toBe("HOLD");
expect(summary.missingRequiredFields).toEqual(["buyerName"]);
});
it("exposes only boolean railway amount diagnostics", async () => {
const summary = await liveRunner.runLiveSampleCheck({
alias: "SAMPLE-PDF-TRAIN-DIAGNOSTIC",
buffer: syntheticPdf(),
config: providerConfig(),
runOcr: vi.fn().mockResolvedValue(
normalizedResult({
documentSubtype: "ElectronicTrainTicketFull",
documentCategory: "electronic_train_ticket",
invoiceCode: null,
totalAmount: null,
sellerName: null,
diagnostics: {
trainAmountFields: {
total: {
present: true,
isString: true,
isNumber: false,
nonEmpty: true,
parseable: false,
value: "forbidden-total-marker",
},
fare: {
present: false,
isString: false,
isNumber: false,
nonEmpty: false,
parseable: false,
value: "forbidden-fare-marker",
},
},
},
}),
),
});
const serialized = JSON.stringify(summary);
expect(summary.status).toBe("HOLD");
expect(summary.trainAmountDiagnostics).toEqual({
totalPresent: true,
totalIsString: true,
totalIsNumber: false,
totalNonEmpty: true,
totalParseable: false,
farePresent: false,
fareIsString: false,
fareIsNumber: false,
fareNonEmpty: false,
fareParseable: false,
});
expect(serialized).not.toContain("forbidden-total-marker");
expect(serialized).not.toContain("forbidden-fare-marker");
});
it("never exposes provider raw fields or normalized business values", async () => {
const summary = await liveRunner.runLiveSampleCheck({
alias: "SAMPLE-PDF-PRIVACY",
buffer: syntheticPdf(),
config: providerConfig(),
runOcr: vi.fn().mockResolvedValue(
normalizedResult({
raw: {
passengerIdentity: "forbidden-identity-marker",
response: "forbidden-response-marker",
},
}),
),
});
const serialized = JSON.stringify(summary);
expect(serialized).not.toContain("forbidden-identity-marker");
expect(serialized).not.toContain("forbidden-response-marker");
expect(serialized).not.toContain("synthetic-number");
expect(serialized).not.toContain("synthetic-seller");
expect(summary.fieldHits.invoiceNumber).toBe(true);
expect(summary.fieldHits.sellerName).toBe(true);
});
it.each([
["non-pdf", Buffer.from("not a pdf", "ascii"), "sample_pdf_invalid"],
["truncated", Buffer.from("%PDF-1.7\nmissing eof", "ascii"), "sample_pdf_invalid"],
["oversized", Buffer.alloc(OCR_PDF_MAX_BYTES + 1, 0x20), "sample_too_large"],
])("rejects %s before provider invocation", async (_caseName, buffer, errorCode) => {
const runOcr = vi.fn();
await expect(
liveRunner.runLiveSampleCheck({
alias: "SAMPLE-PDF-TEST",
buffer,
config: providerConfig(),
runOcr,
}),
).rejects.toMatchObject({ code: errorCode });
expect(runOcr).not.toHaveBeenCalled();
});
it("consumes one stdin PDF and returns only a safe config error without credentials", () => {
const result = spawnSync(process.execPath, [SCRIPT_PATH, "--alias=SAMPLE-PDF-STDIN"], {
cwd: ROOT_DIR,
env: {
...process.env,
TENCENT_SECRET_ID: "",
TENCENT_SECRET_KEY: "",
},
input: syntheticPdf(),
encoding: "utf8",
});
const summary = JSON.parse(result.stdout.trim());
expect(result.status).toBe(1);
expect(result.stderr).toBe("");
expect(summary).toEqual({
alias: "SAMPLE-PDF-STDIN",
provider: "tencent",
status: "HOLD",
transportMode: "pdf_native",
errorCode: "ocr_credentials_missing",
});
});
});