fix: repair XFT expense type diagnostics
This commit is contained in:
+187
-26
@@ -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)/,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user