feat: support secure XFT submission attachments
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
This commit is contained in:
@@ -1079,6 +1079,68 @@ test("Xft travel reimbursement preparation validates nested and custom required
|
||||
}), /第 1 段行程缺少/);
|
||||
});
|
||||
|
||||
test("Xft local attachment metadata satisfies required fields, is preview-bound, and never exposes a path", async () => {
|
||||
const template = {
|
||||
eventSeqId: "1", eventNumber: "XFT-A", eventName: "费用报销单",
|
||||
eventType: "COMMON_REIM", kind: "common_reimburse" as const,
|
||||
};
|
||||
const submittedAttachments: unknown[] = [];
|
||||
const xft = {
|
||||
async listSubmissionTemplates() { return [template]; },
|
||||
async getSubmissionSchema() { return {
|
||||
template,
|
||||
fields: [{
|
||||
fieldNumber: "ACCESSORIES_1", name: "发票附件", type: "ACCESSORIES", required: true,
|
||||
attachmentCapability: {
|
||||
status: "supported", input: "local_file", uploadWhen: "after_final_confirmation",
|
||||
maxFileBytes: 50 * 1024 * 1024, maxFilesPerSubmission: 10,
|
||||
},
|
||||
}],
|
||||
inputContract: {}, diagnostics: {},
|
||||
}; },
|
||||
async getSubmissionOptions() { return { kind: "expense_type", items: [{ code: "MEAL", label: "业务招待费" }], diagnostics: {} }; },
|
||||
async validateSubmissionIdentity() { return { validated: true, diagnostics: {} }; },
|
||||
async submitApproval(_user: unknown, _template: unknown, _values: unknown, attachments: unknown[]) {
|
||||
submittedAttachments.push(...attachments);
|
||||
return { billId: "2026090100000001", status: "submitted", diagnostics: {} };
|
||||
},
|
||||
async getMySubmissionStatus() { return {
|
||||
billId: "2026090100000001", status: "1", statusName: "待审批", revoked: false, revokeAllowed: true,
|
||||
}; },
|
||||
};
|
||||
const service = new UnifiedApprovalService(config(), {} as ApprovalService, xft as never);
|
||||
const user = { userId: "user-1" };
|
||||
const templateId = String((await service.listSubmissionTemplates(user, "xft", 10)).items[0]?.templateId);
|
||||
const values = {
|
||||
reimAmount: "551.00", reimAmountCurrencyCode: "CNY",
|
||||
expenseList: [{ expTypeCode: "MEAL", currencyCode: "CNY", originalCurrencyReimAmt: "551.00" }],
|
||||
};
|
||||
await assert.rejects(service.prepareSubmission(user, { templateId, values }), /发票附件/);
|
||||
const metadata = {
|
||||
fieldName: "发票附件", fileName: "invoice.pdf", size: 21,
|
||||
sha256: "a".repeat(64),
|
||||
};
|
||||
const prepared = await service.prepareSubmission(user, { templateId, values, attachments: [metadata] });
|
||||
assert.equal(prepared.status, "confirmation_required");
|
||||
assert.match(JSON.stringify(prepared.preview), /invoice\.pdf/);
|
||||
assert.doesNotMatch(JSON.stringify(prepared.preview), /a{32}|localPath|attachmentRef/);
|
||||
|
||||
const submitted = await service.submitApproval(user, prepared.confirmationId!, {
|
||||
templateId,
|
||||
values,
|
||||
attachments: [{ ...metadata, attachmentRef: "opaque-upload-reference-12345" }],
|
||||
});
|
||||
assert.equal(submitted.status, "submitted");
|
||||
assert.deepEqual(submittedAttachments, [{
|
||||
...metadata, fieldNumber: "ACCESSORIES_1", attachmentRef: "opaque-upload-reference-12345",
|
||||
}]);
|
||||
await assert.rejects(service.prepareSubmission(user, {
|
||||
templateId,
|
||||
values: { ...values, customFieldValues: [{ fieldNumber: "ACCESSORIES_1", fieldValue: "invented-file-id" }] },
|
||||
attachments: [metadata],
|
||||
}), /必须通过 attachments.*不能直接提供附件编号/);
|
||||
});
|
||||
|
||||
test("DingTalk submission rejects required client-only generated fields", async () => {
|
||||
const ding = {
|
||||
async listSubmissionTemplates() { return [{ processCode: "PROC-1", name: "外出_北极大", dirName: "北极大_人事假勤" }]; },
|
||||
|
||||
Reference in New Issue
Block a user