fix: repair XFT expense type diagnostics

This commit is contained in:
2026-08-26 05:05:59 +00:00
parent 2c60c35048
commit 76a4e0592c
4 changed files with 339 additions and 52 deletions
+187 -26
View File
@@ -273,10 +273,37 @@ test("Xft OpenAPI rejects non-exact, missing-identity, and ambiguous user mappin
});
test("Xft OpenAPI surfaces XFT framework business errors", async () => {
const client = new XftOpenApiClient(config(), async () => encrypted({
SYCOMRETZ: [{ ERRCOD: "SYOPA16", ERRMSG: "应用状态错误" }],
}));
await assert.rejects(client.authStatus({ userId: "ding-u1" }), /应用状态错误/);
const client = new XftOpenApiClient(config(), async () => new Response(
String(sm4.encrypt(JSON.stringify({
SYCOMRETZ: [{ ERRCOD: "SYOPA16", ERRMSG: "应用状态错误" }],
}), privateKey.slice(0, 32))),
{ status: 403 },
));
await assert.rejects(
client.authStatus({ userId: "ding-u1" }),
/应用状态错误(操作:用户身份映射;HTTP 403;薪福通错误码 SYOPA16)/,
);
});
test("Xft OpenAPI exposes safe transport diagnostics without leaking the native error message", async () => {
for (const [cause, expected] of [
[Object.assign(new Error("connect ECONNRESET https://user:password@private.invalid/?token=secret"), { code: "ECONNRESET" }),
/用户身份映射请求失败:网络连接被重置或中断(底层错误码 ECONNRESET;超时上限 2000ms)/],
[Object.assign(new Error("getaddrinfo ENOTFOUND private.invalid"), { code: "ENOTFOUND" }),
/用户身份映射请求失败:DNS 解析失败(底层错误码 ENOTFOUND;超时上限 2000ms)/],
[Object.assign(new Error("connect timed out"), { code: "UND_ERR_CONNECT_TIMEOUT", name: "ConnectTimeoutError" }),
/用户身份映射请求失败:连接或等待响应超时(底层错误码 UND_ERR_CONNECT_TIMEOUT;错误类型 ConnectTimeoutError;超时上限 2000ms)/],
] as const) {
const client = new XftOpenApiClient(config(), async () => {
throw new TypeError("fetch failed", { cause });
});
await assert.rejects(client.authStatus({ userId: "ding-u1" }), (error: unknown) => {
assert.ok(error instanceof Error);
assert.match(error.message, expected);
assert.doesNotMatch(error.message, /password|private\.invalid|token=secret|fetch failed/);
return true;
});
}
});
test("Xft OpenAPI preserves official GET endpoint query parameters in the signed target", async () => {
@@ -454,7 +481,8 @@ test("Xft submission options expose only departments, cities, and the matching e
let expenseSourceRequests = 0;
let unrelatedSourceRequests = 0;
const client = new XftOpenApiClient(config(), async (input, init) => {
const path = new URL(String(input)).pathname;
const url = new URL(String(input));
const path = url.pathname;
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
if (path.endsWith("form/query-event-list")) return encrypted({ code: "0", data: { list: [templateRecord] } });
if (path.endsWith("member/get/by-id")) {
@@ -476,27 +504,32 @@ test("Xft submission options expose only departments, cities, and the matching e
}
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [] });
if (path.endsWith("dataSource/findMultilevelSourcePage")) {
assert.deepEqual(decodedBody(init), { start: 0, limit: 100 });
return encrypted({ code: "0", data: { records: [
{ sourceSeqId: "DS-EXP", sourceName: "企业费用类型" },
{ sourceSeqId: "DS-PROJECT", sourceName: "项目档案" },
] } });
assert.equal(init?.method, "POST");
assert.equal(url.searchParams.get("start"), "0");
assert.equal(url.searchParams.get("limit"), "100");
assert.deepEqual(decodedBody(init), {});
return encrypted({ success: true, message: "调用成功!", totalCount: 2, list: [
{ seqId: 150, id: "DS-EXP", selectSourceName: "企业费用类型" },
{ seqId: 151, id: "DS-PROJECT", selectSourceName: "项目档案" },
] });
}
if (path.endsWith("dataSource/getMultiLevelDetail/DS-EXP")) {
if (path.endsWith("dataSource/getMultiLevelDetail/150")) {
expenseSourceRequests += 1;
return encrypted({ code: "0", data: {
sourceSeqId: "DS-EXP",
sourceName: "企业费用类型",
levels: [{ nodes: [
{ optionNumber: "TRAFFIC", optionName: "交通费" },
{ optionNumber: "HOTEL", optionName: "住宿费" },
{ optionNumber: "HIDDEN", optionName: "停用费用", selectable: false },
] }],
return encrypted({ success: true, message: "调用成功!", data: {
id: "DS-EXP",
selectSourceName: "企业费用类型",
optionSaveDtoList: [
{ seqId: 601, selectOptionName: "交通费", optionSaveDtoList: null },
{ seqId: 602, selectOptionName: "住宿费", optionSaveDtoList: null },
{ seqId: 603, selectOptionName: "停用费用", optionSaveDtoList: null, selectable: false },
],
} });
}
if (path.endsWith("dataSource/getMultiLevelDetail/DS-PROJECT")) {
if (path.endsWith("dataSource/getMultiLevelDetail/151")) {
unrelatedSourceRequests += 1;
return encrypted({ code: "0", data: { nodes: [{ optionNumber: "P1", optionName: "项目一" }] } });
return encrypted({ success: true, message: "调用成功!", data: {
optionSaveDtoList: [{ seqId: 701, selectOptionName: "项目一", optionSaveDtoList: null }],
} });
}
throw new Error(`unexpected ${path}`);
});
@@ -513,7 +546,7 @@ test("Xft submission options expose only departments, cities, and the matching e
assert.deepEqual(cachedCities.items, [{ code: "441900", label: "东莞市" }]);
assert.equal(cityRequests, 1);
const expenseTypes = await client.getSubmissionOptions({ userId: "ding-u1" }, template, "expense_type", "住宿", 10);
assert.deepEqual(expenseTypes.items, [{ code: "HOTEL", label: "住宿费" }]);
assert.deepEqual(expenseTypes.items, [{ code: "602", label: "住宿费" }]);
assert.equal(expenseTypes.total, 1);
assert.equal(expenseTypes.hasMore, false);
assert.equal(expenseSourceRequests, 1);
@@ -669,15 +702,143 @@ test("Xft expense types prefer user-readable live template options and fail clos
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
if (path.endsWith("form/query-event-list")) return encrypted({ code: "0", data: { list: [templateRecord] } });
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [] });
if (path.endsWith("dataSource/findMultilevelSourcePage")) return encrypted({ code: "0", data: { records: [
{ sourceSeqId: "DS-PROJECT", sourceName: "项目档案" },
] } });
if (path.endsWith("dataSource/findMultilevelSourcePage")) return encrypted({
success: true, message: "调用成功!", totalCount: 1, list: [
{ seqId: 100, id: "DS-PROJECT", selectSourceName: "项目档案" },
],
});
throw new Error(`unexpected ${path}`);
});
const unavailableTemplate = (await unavailable.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
await assert.rejects(
unavailable.getSubmissionOptions({ userId: "ding-u1" }, unavailableTemplate, "expense_type"),
/暂时无法实时获取可选费用类型/,
/提单实时字段未提供内嵌选项,且企业多级数据源共返回 1 项,但没有费用类型数据源.*正式 OpenAPI 未提供独立的费用类型目录查询/,
);
});
test("Xft expense type lookup explains a successful empty official data-source response", async () => {
const templateRecord = {
eventSeqId: 16, eventNumber: "EXP-16", eventName: "费用报销单", eventStatus: "1", eventType: "COMMON_REIM",
};
const client = new XftOpenApiClient(config(), async (input) => {
const path = new URL(String(input)).pathname;
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
if (path.endsWith("form/query-event-list")) return encrypted({ code: "0", data: { list: [templateRecord] } });
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [] });
if (path.endsWith("dataSource/findMultilevelSourcePage")) return encrypted({
success: true, message: "调用成功!", totalCount: 0, list: [],
});
throw new Error(`unexpected ${path}`);
});
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
await assert.rejects(
client.getSubmissionOptions({ userId: "ding-u1" }, template, "expense_type"),
/提单实时字段未提供内嵌选项,且企业多级数据源列表为空.*请在费用管理页面确认配置或使用薪福通客户端完成/,
);
});
test("Xft expense type lookup follows the official data-source paging and detail contract", async () => {
const templateRecord = {
eventSeqId: 17, eventNumber: "EXP-17", eventName: "费用报销单", eventStatus: "1", eventType: "COMMON_REIM",
};
let listRequests = 0;
let detailRequests = 0;
const client = new XftOpenApiClient(config(), async (input, init) => {
const url = new URL(String(input));
const path = url.pathname;
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
if (path.endsWith("form/query-event-list")) return encrypted({ code: "0", data: { list: [templateRecord] } });
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [] });
if (path.endsWith("dataSource/findMultilevelSourcePage")) {
listRequests += 1;
assert.equal(init?.method, "POST");
assert.equal(url.searchParams.get("start"), "0");
assert.equal(url.searchParams.get("limit"), "100");
assert.deepEqual(decodedBody(init), {});
return encrypted({
success: true, message: "调用成功!", totalCount: 1, list: [
{ seqId: 150, id: "SOUR01", selectSourceName: "企业费用类型" },
],
});
}
if (path.endsWith("dataSource/getMultiLevelDetail/150")) {
detailRequests += 1;
assert.equal(init?.method, "GET");
assert.equal(init?.body, undefined);
return encrypted({
success: true,
message: "调用成功!",
data: {
id: "SOUR01",
selectSourceName: "企业费用类型",
optionSaveDtoList: [{
seqId: 1214876,
selectOptionName: "交通费",
optionSaveDtoList: [{ seqId: 1214877, selectOptionName: "出租车费", optionSaveDtoList: null }],
}],
},
});
}
throw new Error(`unexpected ${path}`);
});
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
const result = await client.getSubmissionOptions({ userId: "ding-u1" }, template, "expense_type");
assert.equal(listRequests, 1);
assert.equal(detailRequests, 1);
assert.deepEqual(result.items, [
{ code: "1214876", label: "交通费" },
{ code: "1214877", label: "出租车费" },
]);
});
test("Xft expense type lookup preserves data-source API errors instead of replacing them with an empty result", async () => {
const templateRecord = {
eventSeqId: 18, eventNumber: "EXP-18", eventName: "费用报销单", eventStatus: "1", eventType: "COMMON_REIM",
};
const client = new XftOpenApiClient(config(), async (input) => {
const path = new URL(String(input)).pathname;
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
if (path.endsWith("form/query-event-list")) return encrypted({ code: "0", data: { list: [templateRecord] } });
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [] });
if (path.endsWith("dataSource/findMultilevelSourcePage")) {
return new Response(String(sm4.encrypt(JSON.stringify({
code: "NO_PERMISSION", message: "未开通多级数据源查询权限",
}), privateKey.slice(0, 32))), { status: 403 });
}
throw new Error(`unexpected ${path}`);
});
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
await assert.rejects(
client.getSubmissionOptions({ userId: "ding-u1" }, template, "expense_type"),
/未开通多级数据源查询权限(操作:费用类型数据源列表;HTTP 403;薪福通错误码 NO_PERMISSION)/,
);
});
test("Xft expense type lookup preserves a failed matching data-source detail", async () => {
const templateRecord = {
eventSeqId: 19, eventNumber: "EXP-19", eventName: "费用报销单", eventStatus: "1", eventType: "COMMON_REIM",
};
const client = new XftOpenApiClient(config(), async (input) => {
const path = new URL(String(input)).pathname;
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
if (path.endsWith("form/query-event-list")) return encrypted({ code: "0", data: { list: [templateRecord] } });
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [] });
if (path.endsWith("dataSource/findMultilevelSourcePage")) return encrypted({
success: true, message: "调用成功!", totalCount: 1, list: [
{ seqId: 151, id: "SOUR02", selectSourceName: "企业费用类型" },
],
});
if (path.endsWith("dataSource/getMultiLevelDetail/151")) {
throw new TypeError("fetch failed", {
cause: Object.assign(new Error("socket closed with internal address"), { code: "UND_ERR_SOCKET" }),
});
}
throw new Error(`unexpected ${path}`);
});
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
await assert.rejects(
client.getSubmissionOptions({ userId: "ding-u1" }, template, "expense_type"),
/费用类型数据源详情请求失败:网络连接被重置或中断(底层错误码 UND_ERR_SOCKET;超时上限 2000ms)/,
);
});