Files
dingMCP/test/xft-open-api.test.ts
gitea-admin b7313ab74d
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
feat: support secure XFT submission attachments
2026-09-01 03:44:36 +00:00

1273 lines
71 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import smCrypto from "sm-crypto";
import type { AppConfig } from "../src/config.js";
import { XftOpenApiClient } from "../src/xft-open-api.js";
const privateKey = "1".padStart(64, "0");
const { sm3, sm4 } = smCrypto;
function config(): AppConfig {
return {
clientId: "ding-app",
clientSecret: "ding-secret",
apiBaseUrl: "https://ding.test",
oapiBaseUrl: "https://ding-oapi.test",
publicBaseUrl: "http://localhost:3000",
trustedLocalUser: { userId: "ding-u1" },
port: 3000,
requestTimeoutMs: 2_000,
officialLookbackDays: 120,
officialScanConcurrency: 2,
officialClientOnlyProcessCodes: [],
approvalRefSecret: "fixed-reference-secret-for-tests",
batchLimit: 20,
xftOpenApi: {
appId: "app-id",
authoritySecret: privateKey,
enterpriseId: "TEST0001",
apiBaseUrl: "https://xft-api.test",
encrypted: true,
dingTalkAppCode: "DDingTalk",
},
};
}
function encrypted(value: unknown): Response {
return new Response(String(sm4.encrypt(JSON.stringify(value), privateKey.slice(0, 32))), {
headers: { "content-type": "text/plain" },
});
}
function decodedBody(init?: RequestInit): Record<string, unknown> {
const envelope = JSON.parse(String(init?.body)) as { secretMsg: string };
return JSON.parse(String(sm4.decrypt(envelope.secretMsg, privateKey.slice(0, 32)))) as Record<string, unknown>;
}
function findRecordByKey(value: unknown, key: string): Record<string, unknown> | undefined {
if (Array.isArray(value)) {
for (const item of value) {
const match = findRecordByKey(item, key);
if (match) return match;
}
return undefined;
}
if (!value || typeof value !== "object") return undefined;
const record = value as Record<string, unknown>;
if (record.key === key) return record;
for (const child of Object.values(record)) {
const match = findRecordByKey(child, key);
if (match) return match;
}
return undefined;
}
function findRecordsByKey(value: unknown, key: string): Array<Record<string, unknown>> {
if (Array.isArray(value)) return value.flatMap((item) => findRecordsByKey(item, key));
if (!value || typeof value !== "object") return [];
const record = value as Record<string, unknown>;
return [...(record.key === key ? [record] : []), ...Object.values(record).flatMap((item) => findRecordsByKey(item, key))];
}
test("Xft OpenAPI maps the authenticated DingTalk user and signs encrypted requests", async () => {
const calls: Array<{ path: string; body: Record<string, unknown>; headers: Headers; url: URL }> = [];
const fakeFetch: typeof fetch = async (input, init) => {
const url = new URL(String(input));
const body = decodedBody(init);
const headers = new Headers(init?.headers);
calls.push({ path: url.pathname, body, headers, url });
if (url.pathname.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1", userName: "用户一" }] } });
if (url.pathname.endsWith("todo-task/page/by-user")) return encrypted({ code: "0", data: { records: [{ todoTaskId: "TD-1", groupId: "TRIP_2099010100000001", appCode: "xft-bpm", businessCode: "OA000001" }], totalSize: 1 } });
if (url.pathname.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [{ todoTaskId: "TD-1", businessId: "TRIP_2099010100000001", appCode: "TEST0001_TRIP", businessCode: "TEST0001_TRIP_TRIP_2099010100000002" }] });
if (url.pathname.endsWith("bill-list")) return encrypted({ code: "0", data: { list: [{ billId: "2099010100000001", eventType: "COMMON_REIM", title: "费用报销", applicantName: "测试用户丁", totalAmount: 4812 }] } });
throw new Error(`unexpected ${url.pathname}`);
};
const client = new XftOpenApiClient(config(), fakeFetch);
const result = await client.listMyTodos({ userId: "ding-u1" }, 50);
assert.equal(result.items[0]?.billId, "2099010100000001");
assert.equal(result.items[0]?.amount, 4812);
assert.equal(result.identity.enterpriseUserId, "EMP-1");
assert.deepEqual(calls[0]?.body.thirdStaffIds, ["ding-u1"]);
assert.equal(calls[0]?.body.appCode, "DDingTalk");
for (const call of calls) {
assert.equal(call.url.searchParams.get("CSCAPPUID"), "app-id");
assert.equal(call.url.searchParams.get("OPAUID"), "TEST0001");
assert.equal(call.headers.get("x-alb-verify"), "sm3withsm2");
assert.equal(call.headers.get("apisign")?.length, 128);
assert.equal(call.headers.get("x-alb-digest"), sm3(JSON.stringify({ secretMsg: sm4.encrypt(JSON.stringify(call.body), privateKey.slice(0, 32)) })));
}
const todoCall = calls.find((call) => call.path.endsWith("todo-task/page/by-user"));
assert.equal(todoCall?.url.searchParams.get("CSCUSRNBR"), "EMP-1");
assert.equal(todoCall?.url.searchParams.get("CSCPRJCOD"), "TEST0001");
});
test("Xft OpenAPI reads the official billAmount in todo summaries", async () => {
const fakeFetch: typeof fetch = async (input, init) => {
const path = new URL(String(input)).pathname;
void decodedBody(init);
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
if (path.endsWith("todo-task/page/by-user")) return encrypted({ code: "0", data: { records: [{ todoTaskId: "TD-1", groupId: "TRIP_2099010200000001", appCode: "xft-bpm", businessCode: "OA000001" }] } });
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [{ todoTaskId: "TD-1", businessId: "TRIP_2099010200000001", appCode: "TEST0001_TRIP" }] });
if (path.endsWith("bill-list")) return encrypted({ code: "0", data: { list: [{ billId: "2099010200000001", eventType: "COMMON_REIM", billAmount: 2336 }] } });
throw new Error(`unexpected ${path}`);
};
const client = new XftOpenApiClient(config(), fakeFetch);
const result = await client.listMyTodos({ userId: "ding-u1" }, 50);
assert.equal(result.items[0]?.amount, 2336);
});
test("Xft OpenAPI lists handled tasks and本人 submitted bills as separate read-only history scopes", async () => {
const calls: Array<{ path: string; body: Record<string, unknown> }> = [];
const fakeFetch: typeof fetch = async (input, init) => {
const path = new URL(String(input)).pathname;
const body = decodedBody(init);
calls.push({ path, body });
if (path.includes("user-mapping")) {
return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
}
if (path.endsWith("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
number: "E001", idRelation: { enterpriseUserId: "EMP-1" },
} });
if (path.endsWith("todo-task/page/by-user")) return encrypted({ code: "0", data: { records: [{
todoTaskId: "DONE-1", groupId: "TRIP_2026081400000001", appCode: "xft-bpm", businessCode: "OA000001",
dealTime: "2026-08-14T04:00:00.000Z",
}] } });
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [{
todoTaskId: "DONE-1", businessId: "TRIP_2026081400000001", appCode: "TEST0001_TRIP",
}] });
if (path.endsWith("bills/queryBillByCondition")) return encrypted({ code: "0", data: { pageData: {
records: [
{ billId: "2026081400000002", billSts: "1", eventType: "COMMON_REIM", applyTime: "2026-08-14T05:00:00.000Z" },
{ billId: "2026070100000003", billSts: "2", eventType: "COMMON_REIM", applyTime: "2026-07-01T05:00:00.000Z" },
], total: 2,
} } });
if (path.endsWith("bill-list")) {
const ids = body.billIds as Array<string | number>;
return encrypted({ code: "0", data: { list: ids.map((billId) => ({
billId: String(billId), eventType: "COMMON_REIM",
applyTime: String(billId).endsWith("3") ? "2026-07-01T05:00:00.000Z" : "2026-08-14T03:00:00.000Z",
title: String(billId).endsWith("1") ? "已处理报销" : "本人发起报销",
applicantName: String(billId).endsWith("1") ? "同事甲" : "用户一", billAmount: 88,
})) } });
}
throw new Error(`unexpected ${path}`);
};
const client = new XftOpenApiClient(config(), fakeFetch);
const historyRange = { startTime: Date.parse("2026-08-01T00:00:00+08:00"), endTime: Date.parse("2026-08-31T23:59:59+08:00") };
const handled = await client.listMyHistory({ userId: "ding-u1" }, "handled", 20, "", historyRange);
const submitted = await client.listMyHistory({ userId: "ding-u1" }, "submitted", 20, "", historyRange);
assert.equal(handled.items[0]?.billId, "2026081400000001");
assert.equal(handled.items[0]?.historyScope, "handled");
assert.equal("allowPass" in handled.items[0]!, false);
assert.equal(submitted.items[0]?.billId, "2026081400000002");
assert.equal(submitted.items.length, 1, "submitted history excludes bills outside the requested time range");
assert.equal(submitted.items[0]?.historyScope, "submitted");
assert.equal(calls.find((call) => call.path.endsWith("todo-task/page/by-user"))?.body.dealStatus, "done");
assert.deepEqual(calls.find((call) => call.path.endsWith("bills/queryBillByCondition"))?.body, {
empId: "E001", start: 0, limit: 100,
});
});
test("Xft OpenAPI does not turn missing or blank amounts into zero", async () => {
for (const billAmount of [null, ""]) {
const fakeFetch: typeof fetch = 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("todo-task/page/by-user")) return encrypted({ code: "0", data: { records: [{ todoTaskId: "TD-1", groupId: "TRIP_2099010200000001", appCode: "xft-bpm", businessCode: "OA000001" }] } });
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [{ todoTaskId: "TD-1", businessId: "TRIP_2099010200000001", appCode: "TEST0001_TRIP" }] });
if (path.endsWith("bill-list")) return encrypted({ code: "0", data: { list: [{ billId: "2099010200000001", eventType: "COMMON_REIM", billAmount }] } });
throw new Error(`unexpected ${path}`);
};
const result = await new XftOpenApiClient(config(), fakeFetch).listMyTodos({ userId: "ding-u1" }, 50);
assert.equal(result.items[0]?.amount, undefined);
}
});
test("Xft OpenAPI keeps unadapted personal todos visible instead of filtering them", async () => {
const pages: number[] = [];
const client = new XftOpenApiClient(config(), async (input, init) => {
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("todo-task/page/by-user")) {
const page = Number(decodedBody(init).currentPage);
pages.push(page);
return encrypted({ code: "0", data: { records: [{
todoTaskId: "OTHER-1", groupId: "OTHER-1", appCode: "other-app", title: "其他审批",
}], totalSize: 1 } });
}
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [] });
throw new Error(`unexpected ${path}`);
});
const result = await client.listMyTodos({ userId: "ding-u1" }, 10);
assert.equal(result.items[0]?.title, "其他审批");
assert.equal(result.items[0]?.capability, "list_only");
assert.equal(result.items[0]?.unavailableReason, "missing_bill_identifier");
assert.equal(result.diagnostics.billApiCount, 0);
assert.equal(result.diagnostics.clientOnlyCount, 1);
assert.deepEqual(pages, [1]);
});
test("Xft OpenAPI upgrades an unknown app todo when the official bill API recognizes it", async () => {
const targetBillId = "2026081400000001";
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("todo-task/page/by-user")) return encrypted({ code: "0", data: { records: [{
todoTaskId: "TD-INVITATION", groupId: targetBillId, appCode: "new-approval-app",
}], totalSize: 1 } });
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [] });
if (path.endsWith("bill-list")) return encrypted({ code: "0", data: { list: [{
billId: targetBillId, eventType: "INVITATION_APPLY", title: "招待费用申请单",
}] } });
throw new Error(`unexpected ${path}`);
});
const result = await client.listMyTodos({ userId: "ding-u1" }, 10);
assert.equal(result.items[0]?.billId, targetBillId);
assert.equal(result.items[0]?.title, "招待费用申请单");
assert.equal(result.items[0]?.capability, "bill_api");
assert.equal(result.diagnostics.billApiCount, 1);
assert.equal(result.diagnostics.clientOnlyCount, 0);
});
test("Xft OpenAPI degrades a failed unknown bill probe without hiding known expense todos", async () => {
const knownBillId = "2026081400000001";
const unknownBillId = "2026081400000002";
const client = new XftOpenApiClient(config(), async (input, init) => {
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("todo-task/page/by-user")) return encrypted({ code: "0", data: { records: [{
todoTaskId: "TD-KNOWN", groupId: knownBillId, appCode: "xft-itrip",
}, {
todoTaskId: "TD-UNKNOWN", groupId: unknownBillId, appCode: "new-approval-app", title: "新类型审批",
}], totalSize: 2 } });
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [] });
if (path.endsWith("bill-list")) {
const ids = (decodedBody(init).billIds as Array<string | number>).map(String);
if (ids.includes(unknownBillId)) return encrypted({ code: "BIZ_LOOKUP_FAILED", message: "lookup unavailable" });
return encrypted({ code: "0", data: { list: [{ billId: knownBillId, eventType: "COMMON_REIM", title: "费用报销单" }] } });
}
throw new Error(`unexpected ${path}`);
});
const result = await client.listMyTodos({ userId: "ding-u1" }, 10);
assert.equal(result.items.length, 2);
assert.equal(result.items[0]?.billId, knownBillId);
assert.equal(result.items[0]?.capability, "bill_api");
assert.equal(result.items[1]?.title, "新类型审批");
assert.equal(result.items[1]?.billId, undefined);
assert.equal(result.items[1]?.capability, "list_only");
assert.equal(result.items[1]?.unavailableReason, "bill_lookup_unavailable");
assert.equal(result.diagnostics.billApiCount, 1);
assert.equal(result.diagnostics.clientOnlyCount, 1);
});
test("Xft write transport failures are uncertain while deterministic rejections stay failed", async () => {
for (const [upstream, expected] of [
[{ code: "XFTSYS9999", message: "temporary unavailable", status: 503 }, "outcome_uncertain"],
[{ code: "BIZ001", message: "not allowed", status: 400 }, "failed"],
] as const) {
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("todo-task/page/by-user")) return encrypted({ code: "0", data: { records: [{
todoTaskId: "TD-1", groupId: "2026081400000001", appCode: "xft-itrip",
}] } });
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [] });
if (path.endsWith("bill-list")) return encrypted({ code: "0", data: { list: [{ billId: "2026081400000001", eventType: "COMMON_REIM" }] } });
if (path.endsWith("approve/pass")) {
return new Response(String(sm4.encrypt(JSON.stringify({ code: upstream.code, message: upstream.message }), privateKey.slice(0, 32))), {
status: upstream.status,
});
}
throw new Error(`unexpected ${path}`);
});
const result = await client.processApprovalBatch({ userId: "ding-u1" }, "agree", "同意", [{ billId: "2026081400000001" }]);
assert.equal(result.results[0]?.status, expected);
}
});
test("Xft OpenAPI fails closed when the DingTalk user has no enterprise mapping", async () => {
let calls = 0;
const client = new XftOpenApiClient(config(), async () => {
calls += 1;
return encrypted({ code: "0", data: { records: [] } });
});
await assert.rejects(client.listMyTodos({ userId: "unknown" }, 50), /未映射到薪福通企业成员/);
assert.equal(calls, 1);
});
test("Xft OpenAPI rejects non-exact, missing-identity, and ambiguous user mappings", async () => {
for (const records of [
[{ thirdStaffId: "victim-user", enterpriseUserId: "EMP-VICTIM" }],
[{ enterpriseUserId: "EMP-WITHOUT-DING-ID" }],
[
{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" },
{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-2" },
],
]) {
let mappingCalls = 0;
let downstreamCalls = 0;
const client = new XftOpenApiClient(config(), async (input) => {
if (new URL(String(input)).pathname.includes("user-mapping")) {
mappingCalls += 1;
return encrypted({ code: "0", data: { records } });
}
downstreamCalls += 1;
return encrypted({ code: "0", data: {} });
});
await assert.rejects(client.listMyTodos({ userId: "ding-u1" }, 50), /未映射|多个薪福通成员映射/);
assert.equal(mappingCalls, 1);
assert.equal(downstreamCalls, 0);
}
});
test("Xft OpenAPI surfaces XFT framework business errors", async () => {
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 () => {
let capturedUrl: URL | undefined;
let capturedInit: RequestInit | undefined;
const client = new XftOpenApiClient(config(), async (input, init) => {
capturedUrl = new URL(String(input));
capturedInit = init;
return encrypted({ code: "0", data: { billId: "2099010200000001" } });
});
const request = client as unknown as {
request(path: string, body: undefined, method: "GET", identity: { enterpriseUserId: string }): Promise<unknown>;
};
await request.request(
"/itrip/xft-api/v1/reimburse/bill/common/detail?billId=2099010200000001",
undefined,
"GET",
{ enterpriseUserId: "EMP-1" },
);
assert.equal(capturedUrl?.pathname, "/itrip/xft-api/v1/reimburse/bill/common/detail");
assert.equal(capturedUrl?.searchParams.get("billId"), "2099010200000001");
assert.equal(capturedUrl?.searchParams.get("CSCUSRNBR"), "EMP-1");
assert.equal(capturedInit?.method, "GET");
assert.equal(capturedInit?.body, undefined);
assert.equal(new Headers(capturedInit?.headers).has("x-alb-digest"), false);
});
test("Xft OpenAPI derives empId server-side and validates ownership before and after a write", async () => {
let todoQueries = 0;
const actionBodies: Record<string, unknown>[] = [];
const fakeFetch: typeof fetch = async (input, init) => {
const path = new URL(String(input)).pathname;
const body = decodedBody(init);
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { list: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
if (path.endsWith("todo-task/page/by-user")) {
todoQueries += 1;
return encrypted({ code: "0", data: todoQueries === 1 ? { records: [{ todoTaskId: "TD-1", groupId: "10000000001", appCode: "xft-itrip" }] } : { records: [] } });
}
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [{ todoTaskId: "TD-1", businessId: "10000000001", appCode: "xft-itrip" }] });
if (path.endsWith("bill-list")) return encrypted({ code: "0", data: { list: [{ billId: "10000000001", eventType: "COMMON_REIM" }] } });
if (path.endsWith("approve/pass")) { actionBodies.push(body); return encrypted({ code: "0", data: {} }); }
throw new Error(`unexpected ${path}`);
};
const client = new XftOpenApiClient(config(), fakeFetch);
const result = await client.processApprovalBatch({ userId: "ding-u1" }, "agree", "同意", [{ billId: "10000000001" }]);
assert.equal(result.succeeded, 1);
assert.equal(todoQueries, 2);
assert.equal(actionBodies[0]?.empId, "EMP-1");
});
test("Xft approval batches revalidate each write and share one post-write todo scan", async () => {
const billIds = Array.from({ length: 20 }, (_, index) => String(10_000_000_100 + index));
const pending = new Set(billIds);
let todoQueries = 0;
let actionCalls = 0;
const client = new XftOpenApiClient(config(), async (input, init) => {
const path = new URL(String(input)).pathname;
const body = init?.body ? decodedBody(init) : {};
if (path.includes("user-mapping")) {
return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
}
if (path.endsWith("todo-task/page/by-user")) {
todoQueries += 1;
return encrypted({ code: "0", data: { records: [...pending].map((billId) => ({
todoTaskId: `TD-${billId}`, groupId: billId, appCode: "xft-itrip",
})) } });
}
if (path.endsWith("todo-task/detail/by-ids")) {
const taskIds = Array.isArray(body.todoTaskIds) ? body.todoTaskIds : [];
return encrypted({ code: "0", data: taskIds.map((taskId) => ({
todoTaskId: taskId, businessId: String(taskId).replace(/^TD-/, ""), appCode: "xft-itrip",
})) });
}
if (path.endsWith("bill-list")) {
const ids = Array.isArray(body.billIds) ? body.billIds.map(String) : [];
return encrypted({ code: "0", data: { list: ids.map((billId) => ({ billId, eventType: "COMMON_REIM" })) } });
}
if (path.endsWith("approve/pass")) {
actionCalls += 1;
const businessList = Array.isArray(body.businessList) ? body.businessList : [];
const businessId = businessList[0] && typeof businessList[0] === "object"
? String((businessList[0] as Record<string, unknown>).businessId)
: "";
pending.delete(businessId);
return encrypted({ code: "0", data: {} });
}
throw new Error(`unexpected ${path}`);
});
const result = await client.processApprovalBatch(
{ userId: "ding-u1" },
"agree",
"同意",
billIds.map((billId) => ({ billId })),
);
assert.equal(result.succeeded, 20);
assert.equal(result.failed, 0);
assert.equal(actionCalls, 20);
assert.equal(todoQueries, 21, "each write needs a live ownership scan, followed by one shared post-write scan");
assert.deepEqual(result.results.map((item) => item.billId), billIds);
});
test("Xft submission discovers live templates and derives protected create fields server-side", async () => {
const createBodies: Record<string, unknown>[] = [];
const fakeFetch: typeof fetch = async (input, init) => {
const path = new URL(String(input)).pathname;
const body = init?.body ? decodedBody(init) : {};
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{
thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1",
}] } });
if (path.endsWith("member/get/by-id")) {
assert.deepEqual(body, { id: "EMP-1", extFields: ["historyId", "org"] });
return encrypted({ returnCode: "SUC0000", body: {
memberId: "EMP-1", number: "E001", idRelation: { enterpriseUserId: "EMP-1", staffId: "0000000001" },
} });
}
if (path.endsWith("form/query-event-list")) return encrypted({ code: "0", data: { total: 1, list: [{
eventSeqId: 12345, eventNumber: "FORM-1", eventName: "费用报销单", eventStatus: "1", eventType: "COMMON_REIM",
}] } });
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [{
fieldNumber: "C1", name: "说明", type: "INPUT", required: true, options: [],
}, {
fieldNumber: "C1", name: "说明", type: "INPUT", required: true, options: [],
}, {
fieldNumber: "billDepartmentCode", name: "承担部门", type: "ADV_DEPARTMENT", required: true, options: [],
}, {
fieldNumber: "C-DYNAMIC-DEPT", name: "承担部门", type: "ADV_DEPARTMENT", required: true, options: [],
}, {
fieldNumber: "ACCESSORIES_1", name: "发票附件", type: "ACCESSORIES", required: false, options: [],
}] });
if (path.endsWith("reimburse/bill/create")) {
createBodies.push(body);
return encrypted({ code: "SUC0000", body: 2026081200000001, success: true });
}
throw new Error(`unexpected ${path}`);
};
const client = new XftOpenApiClient(config(), fakeFetch);
const templates = await client.listSubmissionTemplates({ userId: "ding-u1" }, 20);
assert.equal(templates[0]?.kind, "common_reimburse");
const schema = await client.getSubmissionSchema({ userId: "ding-u1" }, templates[0]!);
assert.equal(schema.fields[0]?.fieldNumber, "C1");
assert.equal(schema.fields.length, 2);
assert.deepEqual(schema.fields.find((field) => field.fieldNumber === "ACCESSORIES_1")?.attachmentCapability, {
status: "supported",
input: "local_file",
uploadWhen: "after_final_confirmation",
maxFileBytes: 50 * 1024 * 1024,
maxFilesPerSubmission: 10,
});
const standardFields = (schema.inputContract.values as Record<string, unknown>).standardFields as Array<Record<string, unknown>>;
const minimalExample = (schema.inputContract.values as Record<string, unknown>).minimalExample as Record<string, unknown>;
assert.deepEqual(Object.keys(minimalExample), ["reimAmount", "reimAmountCurrencyCode", "expenseList"]);
assert.match(JSON.stringify(minimalExample), /报销总金额.*费用类型选项.*本笔费用金额/);
const expenseList = standardFields.find((field) => field.key === "expenseList");
const expenseItemFields = expenseList?.itemFields as Array<Record<string, unknown>>;
assert.deepEqual(expenseItemFields.filter((field) => field.required).map((field) => field.key), [
"expTypeCode", "currencyCode", "originalCurrencyReimAmt",
]);
assert.deepEqual(expenseItemFields.find((field) => field.key === "expTypeCode")?.optionSource, {
tool: "approval_get_submission_options", kind: "expense_type",
});
assert.equal(expenseItemFields.find((field) => field.key === "expTypeCode")?.label, "费用类型");
const identityCheck = await client.validateSubmissionIdentity({ userId: "ding-u1" });
assert.equal(identityCheck.validated, true);
assert.doesNotMatch(JSON.stringify(identityCheck), /E001|EMP-1/);
const result = await client.submitApproval({ userId: "ding-u1" }, templates[0]!, {
reimAmount: 10, reimAmountCurrencyCode: "CNY", expenseList: [{ amount: 10 }],
});
assert.equal(result.billId, "2026081200000001");
assert.equal(createBodies[0]?.eventNumber, "FORM-1");
assert.equal(createBodies[0]?.empNumber, "E001");
assert.equal(createBodies[0]?.billStatus, "APPRV_PEND");
assert.equal(createBodies[0]?.billApplyAuthCheckType, "Y");
});
test("Xft attachment upload follows the official multipart signing mode and injects opaque file IDs only at create", async () => {
const file = Buffer.from("%PDF-1.7\nmock invoice", "utf8");
const sha256 = (await import("node:crypto")).createHash("sha256").update(file).digest("hex");
let uploadHeaders: Headers | undefined;
let uploadBody: Buffer | undefined;
let createBody: Record<string, unknown> | undefined;
const fakeFetch: typeof fetch = 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("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
number: "E001", idRelation: { enterpriseUserId: "EMP-1" },
} });
if (path.endsWith("form/query-event-list")) return encrypted({ code: "0", data: { list: [{
eventSeqId: "1", eventNumber: "FORM-1", eventName: "费用报销单", eventStatus: "1", eventType: "COMMON_REIM",
}] } });
if (path.endsWith("accessory/custom-field/upload")) {
uploadHeaders = new Headers(init?.headers);
uploadBody = Buffer.from(init?.body as ArrayBuffer);
assert.equal(url.searchParams.get("uploaderNumber"), "E001");
return encrypted({ code: "SUC0000", body: { fileId: "file-secret-1" } });
}
if (path.endsWith("reimburse/bill/create")) {
createBody = decodedBody(init);
return encrypted({ code: "SUC0000", body: 2026090100000001 });
}
throw new Error(`unexpected ${path}`);
};
const client = new XftOpenApiClient(config(), fakeFetch);
const confirmationId = "confirmation-bound-to-user-preview";
const uploaded = await client.uploadSubmissionAttachment({ userId: "ding-u1" }, {
fieldName: "发票附件", fileName: "invoice.pdf", size: file.length, sha256, content: file, confirmationId,
});
assert.equal(uploadHeaders?.has("x-alb-digest"), false, "official SDK omits body digest for multipart uploads");
assert.equal(uploadHeaders?.get("x-alb-verify"), "sm3withsm2");
assert.equal(uploadHeaders?.get("apisign")?.length, 128);
assert.match(uploadHeaders?.get("content-type") ?? "", /^multipart\/form-data;.*boundary=/);
assert.match(uploadBody?.toString("utf8") ?? "", /name="file"; filename="invoice\.pdf"/);
assert.ok(uploadBody?.includes(file));
assert.doesNotMatch(uploaded.attachmentRef, /file-secret-1/);
const template = {
eventSeqId: "1", eventNumber: "FORM-1", eventName: "费用报销单",
eventType: "COMMON_REIM", kind: "common_reimburse" as const,
};
await client.submitApproval({ userId: "ding-u1" }, template, {
reimAmount: "551.00", reimAmountCurrencyCode: "CNY", expenseList: [{ expTypeCode: "MEAL" }],
}, [{
fieldNumber: "ACCESSORIES_1", fieldName: "发票附件", fileName: "invoice.pdf",
size: file.length, sha256, attachmentRef: uploaded.attachmentRef,
}], confirmationId);
assert.deepEqual(createBody?.customFieldValues, [{
fieldNumber: "ACCESSORIES_1", fieldValue: JSON.stringify(["file-secret-1"]),
}]);
assert.doesNotMatch(JSON.stringify(createBody), /confirmation-bound|attachmentRef|sha256/);
await assert.rejects(client.submitApproval({ userId: "ding-u1" }, template, {}, [{
fieldNumber: "ACCESSORIES_1", fieldName: "发票附件", fileName: "invoice.pdf",
size: file.length, sha256, attachmentRef: uploaded.attachmentRef,
}], "another-confirmation"), /与本次用户确认.*不一致/);
});
test("Xft submission options expose only departments, cities, and the matching expense data source", async () => {
const templateRecord = {
eventSeqId: 1, eventNumber: "TRAVEL-1", eventName: "出差费用申请单", eventStatus: "1", eventType: "TRAVEL_APPLY",
};
let cityRequests = 0;
let expenseSourceRequests = 0;
let unrelatedSourceRequests = 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.endsWith("member/get/by-id")) {
assert.deepEqual(decodedBody(init), { id: "EMP-1", extFields: ["historyId", "org"] });
return encrypted({ returnCode: "SUC0000", body: {
number: "SECRET-EMPLOYEE-NUMBER",
idRelation: { enterpriseUserId: "EMP-1" },
organizationRelations: [
{ organizationCode: "ORG-SZ", organizationName: "深圳总部" },
{ organization: { code: "ORG-RD", name: "研发部" } },
],
} });
}
if (path.endsWith("common/city/getAllCity")) {
cityRequests += 1;
return encrypted({ code: "0", data: {
regions: [{ cityCode: "440300", cityName: "深圳市" }, { cityCode: "441900", cityName: "东莞市" }],
} });
}
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [] });
if (path.endsWith("dataSource/findMultilevelSourcePage")) {
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/150")) {
expenseSourceRequests += 1;
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/151")) {
unrelatedSourceRequests += 1;
return encrypted({ success: true, message: "调用成功!", data: {
optionSaveDtoList: [{ seqId: 701, selectOptionName: "项目一", optionSaveDtoList: null }],
} });
}
throw new Error(`unexpected ${path}`);
});
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
const departments = await client.getSubmissionOptions({ userId: "ding-u1" }, template, "department");
assert.deepEqual(departments.items, [
{ code: "ORG-SZ", label: "深圳总部" },
{ code: "ORG-RD", label: "研发部" },
]);
assert.doesNotMatch(JSON.stringify(departments), /SECRET-EMPLOYEE-NUMBER|EMP-1/);
const cities = await client.getSubmissionOptions({ userId: "ding-u1" }, template, "city", "深圳", 10);
assert.deepEqual(cities.items, [{ code: "440300", label: "深圳市" }]);
const cachedCities = await client.getSubmissionOptions({ userId: "ding-u1" }, template, "city", "东莞", 10);
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: "602", label: "住宿费" }]);
assert.equal(expenseTypes.total, 1);
assert.equal(expenseTypes.hasMore, false);
assert.equal(expenseSourceRequests, 1);
assert.equal(unrelatedSourceRequests, 0);
});
test("all formal Xft expense fields declare a user-readable expense type option source", async () => {
const records = [
{ eventSeqId: 11, eventNumber: "EXP-11", eventName: "费用申请", eventStatus: "1", eventType: "EXPENSE_APPLY" },
{ eventSeqId: 12, eventNumber: "TRIP-12", eventName: "出差费用申请", eventStatus: "1", eventType: "TRAVEL_APPLY" },
{ eventSeqId: 13, eventNumber: "REIM-13", 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: records } });
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [] });
throw new Error(`unexpected ${path}`);
});
const templates = await client.listSubmissionTemplates({ userId: "ding-u1" }, 10);
const expected = ["expenseNumber", "expenseTypeNumber", "expTypeCode"];
for (const [index, fieldKey] of expected.entries()) {
const schema = await client.getSubmissionSchema({ userId: "ding-u1" }, templates[index]!);
const field = findRecordByKey(schema.inputContract, fieldKey);
assert.equal(field?.label, "费用类型");
assert.deepEqual(field?.optionSource, { tool: "approval_get_submission_options", kind: "expense_type" });
assert.deepEqual(field?.selectionCapability, {
status: "supported", userInput: "label", submitValue: "code", exposeInternalValue: false,
});
assert.doesNotMatch(String(field?.description ?? ""), /要求用户.*编码/);
}
});
test("all formal Xft city and enum choices expose readable labels while unresolved references fail closed", async () => {
const records = [
{ eventSeqId: 21, eventNumber: "EXP-21", eventName: "费用申请", eventStatus: "1", eventType: "EXPENSE_APPLY" },
{ eventSeqId: 22, eventNumber: "ENT-22", eventName: "招待申请", eventStatus: "1", eventType: "ENTERTAINMENT_APPLY" },
{ eventSeqId: 23, eventNumber: "TRIP-23", eventName: "差旅申请", eventStatus: "1", eventType: "TRAVEL_APPLY" },
{ eventSeqId: 24, eventNumber: "REIM-24", eventName: "费用报销", eventStatus: "1", eventType: "COMMON_REIM" },
{ eventSeqId: 25, eventNumber: "TR-25", eventName: "差旅报销", eventStatus: "1", eventType: "TRAVEL_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: records } });
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [] });
throw new Error(`unexpected ${path}`);
});
const templates = await client.listSubmissionTemplates({ userId: "ding-u1" }, 20);
const schemas = await Promise.all(templates.map((template) => client.getSubmissionSchema({ userId: "ding-u1" }, template)));
const combined = schemas.map((schema) => schema.inputContract);
for (const schema of schemas) {
const values = schema.inputContract.values as Record<string, unknown>;
assert.ok(values.minimalExample && typeof values.minimalExample === "object");
assert.match(JSON.stringify(values.minimalExample), /</);
}
for (const key of ["payCityCode", "entertainmentCityCode", "departCityCode", "destinationCityCode", "departureCode", "destinationCode"]) {
const matches = findRecordsByKey(combined, key);
assert.ok(matches.length > 0, `${key} should exist`);
for (const field of matches) {
assert.deepEqual(field.optionSource, { tool: "approval_get_submission_options", kind: "city" });
assert.equal((field.selectionCapability as Record<string, unknown>)?.exposeInternalValue, false);
assert.doesNotMatch(String(field.label), /编码/);
}
}
const travelMode = findRecordByKey(combined, "travelMode");
assert.equal((travelMode?.enumLabels as Record<string, string>)?.AirPlane, "飞机");
assert.equal((travelMode?.enumLabels as Record<string, string>)?.RideHailing, "网约车");
assert.equal((findRecordByKey(combined, "roundTrip")?.enumLabels as Record<string, string>)?.Y, "往返");
assert.equal((findRecordByKey(combined, "beginTimePrecision")?.enumLabels as Record<string, string>)?.AM, "上午");
assert.equal((findRecordByKey(combined, "currencyCode")?.enumLabels as Record<string, string>)?.CNY, "人民币");
assert.equal((findRecordByKey(combined, "hotelStar")?.enumLabels as Record<string, string>)?.["1"], "五星");
for (const key of ["peerEmpNumbers", "participants", "customArchiveOptionNumbers", "aircraftCabin", "trainCabin", "shipCabin", "relatedBillId"]) {
const field = findRecordByKey(combined, key);
assert.equal((field?.selectionCapability as Record<string, unknown>)?.status, "client_only", key);
assert.doesNotMatch(String(field?.label ?? ""), /工号|编码| ID/);
}
});
test("live Xft custom choices normalize labels recursively and unsupported selectors are explicit", async () => {
const templateRecord = {
eventSeqId: 31, eventNumber: "CUSTOM-31", eventName: "费用申请", eventStatus: "1", eventType: "EXPENSE_APPLY",
};
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: [
{ fieldNumber: "S1", name: "餐别", type: "SELECT", required: true, options: [
{ optionCode: "LUNCH", optionName: "午餐" }, { optionCode: "DINNER", optionName: "晚餐" },
] },
{ fieldNumber: "S2", name: "无名称映射", type: "SELECT", required: false, options: ["1", "2"] },
{ fieldNumber: "P1", name: "参与人员", type: "PERSON", required: true },
{ fieldNumber: "D1", name: "成本部门", type: "ADV_DEPARTMENT", required: true },
{ fieldNumber: "T1", name: "明细", type: "TABLE", required: true, columns: [
{ titleCode: "TS", name: "用餐类型", type: "SELECT", required: true, options: [{ code: "TEAM", label: "团队餐" }] },
{ titleCode: "TP", name: "同行人", type: "PERSON", required: false },
] },
] });
throw new Error(`unexpected ${path}`);
});
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
const fields = (await client.getSubmissionSchema({ userId: "ding-u1" }, template)).fields;
const select = fields.find((field) => field.fieldNumber === "S1")!;
assert.deepEqual(select.options, [{ code: "LUNCH", label: "午餐" }, { code: "DINNER", label: "晚餐" }]);
assert.equal(select.selectionCapability?.status, "supported");
assert.equal(fields.find((field) => field.fieldNumber === "S2")?.selectionCapability?.status, "client_only");
assert.equal(fields.find((field) => field.fieldNumber === "P1")?.selectionCapability?.status, "client_only");
assert.deepEqual(fields.find((field) => field.fieldNumber === "D1")?.optionSource, {
tool: "approval_get_submission_options", kind: "department",
});
const tableFields = fields.find((field) => field.fieldNumber === "T1")?.itemFields ?? [];
assert.deepEqual(tableFields.find((field) => field.titleCode === "TS")?.options, [{ code: "TEAM", label: "团队餐" }]);
assert.equal((tableFields.find((field) => field.titleCode === "TP")?.selectionCapability as Record<string, unknown>)?.status, "client_only");
});
test("Xft expense types prefer user-readable live template options and fail closed when unavailable", async () => {
const templateRecord = {
eventSeqId: 8, eventNumber: "EXP-8", eventName: "招待费用申请单", eventStatus: "1", eventType: "EXPENSE_APPLY",
};
let dataSourceRequests = 0;
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: [{
fieldNumber: "expenseType", name: "费用类型", type: "SELECT", required: true,
options: [
{ optionCode: "MEAL", optionName: "客户招待费" },
{ optionCode: "CAR", optionName: "市内交通费" },
{ optionCode: "其他费", optionName: "其他费" },
],
}] });
if (path.includes("dataSource/")) {
dataSourceRequests += 1;
throw new Error("data source must not be called");
}
throw new Error(`unexpected ${path}`);
});
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
const schema = await client.getSubmissionSchema({ userId: "ding-u1" }, template);
assert.deepEqual(schema.fields[0]?.optionSource, {
tool: "approval_get_submission_options", kind: "expense_type",
});
const result = await client.getSubmissionOptions({ userId: "ding-u1" }, template, "expense_type", "招待", 10);
assert.deepEqual(result.items, [{ code: "MEAL", label: "客户招待费" }]);
const sameCodeAndLabel = await client.getSubmissionOptions({ userId: "ding-u1" }, template, "expense_type", "其他", 10);
assert.deepEqual(sameCodeAndLabel.items, [{ code: "其他费", label: "其他费" }]);
assert.equal(dataSourceRequests, 0);
const unavailable = 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: 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)/,
);
});
test("Xft recall derives empId from the authenticated user and validates the upstream success key", async () => {
const recallBodies: Record<string, unknown>[] = [];
const client = new XftOpenApiClient(config(), async (input, init) => {
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("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
number: "E001", idRelation: { enterpriseUserId: "EMP-1" },
} });
if (path.endsWith("bills/detail/2026081200000001")) return encrypted({ code: "0", data: {
billId: 2026081200000001, empNumber: "E001", billSts: "1",
} });
if (path.endsWith("approve/recall")) {
recallBodies.push(decodedBody(init));
return encrypted({ code: "0", data: { success: true, error: "", successKey: [2026081200000001] } });
}
throw new Error(`unexpected ${path}`);
});
const result = await client.recallSubmission({ userId: "ding-u1" }, "2026081200000001", "测试撤回");
assert.equal(result.status, "revoked");
assert.deepEqual(recallBodies[0], {
empId: "E001", businessId: 2026081200000001, approveCategory: "BILL", withdrawReason: "测试撤回",
});
});
test("Xft recall reports deterministic failure without retrying", async () => {
let recallCalls = 0;
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("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
number: "E001", idRelation: { enterpriseUserId: "EMP-1" },
} });
if (path.endsWith("bills/detail/2026081200000001")) return encrypted({ code: "0", data: {
billId: 2026081200000001, empNumber: "E001", billSts: "1",
} });
if (path.endsWith("approve/recall")) {
recallCalls += 1;
return encrypted({ code: "0", data: { success: false, error: {
"2026081200000001": { errorMessage: "单据状态不允许撤回", traceId: "must-not-leak" },
}, successKey: [] } });
}
throw new Error(`unexpected ${path}`);
});
const result = await client.recallSubmission({ userId: "ding-u1" }, "2026081200000001");
assert.equal(result.status, "failed");
assert.match(result.message ?? "", /状态不允许/);
assert.doesNotMatch(result.message ?? "", /trace|must-not-leak/i);
assert.equal(recallCalls, 1);
});
test("Xft recall reconciles a framework service error against live bill status", async () => {
let recallCalls = 0;
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("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
number: "E001", idRelation: { enterpriseUserId: "EMP-1" },
} });
if (path.endsWith("approve/recall")) {
recallCalls += 1;
return encrypted({ code: "XFTSYS9999", message: "服务异常" });
}
if (path.endsWith("bills/detail/2026081200000001")) return encrypted({ code: "0", data: {
billId: 2026081200000001, empNumber: "E001", billSts: "1",
} });
throw new Error(`unexpected ${path}`);
});
const result = await client.recallSubmission({ userId: "ding-u1" }, "2026081200000001");
assert.equal(result.status, "failed");
assert.match(result.message ?? "", /服务异常.*待审批/);
assert.equal((result.diagnostics as Record<string, unknown>).postWriteVerified, true);
assert.equal(recallCalls, 1);
});
test("Xft submission status revalidates current-user ownership and returns only compact state", async () => {
const client = new XftOpenApiClient(config(), async (input, init) => {
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("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
number: "E001", idRelation: { enterpriseUserId: "EMP-1" },
} });
if (path.endsWith("bills/detail/2099010300000001")) return encrypted({ code: "0", data: {
billId: 2099010300000001, empNumber: "E001", billSts: "11", billApprSts: "WITHDRAWN", privateFormData: "not returned",
aprHisList: [{ nodeName: "发起", actions: [{ action: "start", empNumber: "must-not-leak" }] }],
} });
throw new Error(`unexpected ${path}`);
});
const result = await client.getMySubmissionStatus({ userId: "ding-u1" }, "2099010300000001");
assert.deepEqual(result, {
billId: "2099010300000001", status: "11", statusName: "已撤回", approvalStatus: "WITHDRAWN",
revoked: true, revokeAllowed: false,
flowHistory: [{ nodeName: "发起", actions: ["start"] }],
diagnostics: {
source: "xft_official_openapi", realtime: true, ownershipRevalidated: true,
applicantIdentity: "server_derived", persistedBusinessData: false,
},
});
assert.doesNotMatch(JSON.stringify(result), /privateFormData|must-not-leak|E001|EMP-1/);
});
test("Xft submission ownership ignores employee numbers from approval history", async () => {
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("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
number: "E001", idRelation: { enterpriseUserId: "EMP-1" },
} });
if (path.endsWith("bills/detail/2026081400000001")) return encrypted({ code: "0", data: {
billId: "2026081400000001", empNumber: "E999", billSts: "1",
aprHisList: [{ actions: [{ empNumber: "E001" }] }],
} });
throw new Error(`unexpected ${path}`);
});
await assert.rejects(client.getMySubmissionStatus({ userId: "ding-u1" }, "2026081400000001"), /不属于当前登录用户/);
});
test("Xft submission ownership accepts object rows from the condition-query fallback", async () => {
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("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
number: "E001", idRelation: { enterpriseUserId: "EMP-1" },
} });
if (path.endsWith("bills/detail/2026081400000001")) return encrypted({ code: "0", data: {
billId: "2026081400000001", billSts: "1",
} });
if (path.endsWith("bills/queryBillByCondition")) return encrypted({ code: "0", data: {
list: [{ billId: "2026081400000001" }],
} });
throw new Error(`unexpected ${path}`);
});
const result = await client.getMySubmissionStatus({ userId: "ding-u1" }, "2026081400000001");
assert.equal((result.diagnostics as Record<string, unknown>).ownershipRevalidated, true);
assert.equal(result.revokeAllowed, true);
});
test("Xft submission routes every supported template kind to its documented create endpoint", async () => {
const templateRecords = [
{ eventSeqId: 1, eventNumber: "T1", eventName: "差旅申请", eventStatus: "1", eventType: "TRAVEL_APPLY" },
{ eventSeqId: 2, eventNumber: "T2", eventName: "费用申请", eventStatus: "1", eventType: "EXPENSE_APPLY" },
{ eventSeqId: 3, eventNumber: "T3", eventName: "招待申请", eventStatus: "1", eventType: "ENTERTAINMENT_APPLY" },
{ eventSeqId: 8, eventNumber: "T8", eventName: "招待费用申请单", eventStatus: "1", eventType: "EXPENSE_APPLY" },
{ eventSeqId: 4, eventNumber: "T4", eventName: "通用申请", eventStatus: "1", eventType: "COMMON_APPLY" },
{ eventSeqId: 5, eventNumber: "T5", eventName: "用车申请", eventStatus: "1", eventType: "USE_CAR_APPLY" },
{ eventSeqId: 6, eventNumber: "T6", eventName: "日常报销", eventStatus: "1", eventType: "COMMON_REIM" },
{ eventSeqId: 7, eventNumber: "T7", eventName: "差旅报销", eventStatus: "1", eventType: "TRAVEL_REIM" },
];
const createPaths: string[] = [];
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: templateRecords } });
if (path.endsWith("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
number: "E001", idRelation: { enterpriseUserId: "EMP-1" },
} });
if (path.endsWith("/create")) {
createPaths.push(path);
return encrypted({ code: "0", data: { billId: String(1000 + createPaths.length) } });
}
throw new Error(`unexpected ${path}`);
});
const templates = await client.listSubmissionTemplates({ userId: "ding-u1" }, 20);
assert.equal(templates.length, 7);
assert.equal(templates.some((item) => item.eventNumber === "T5"), false);
assert.equal(templates.find((item) => item.eventNumber === "T8")?.kind, "expense_apply");
for (const template of templates) await client.submitApproval({ userId: "ding-u1" }, template, {});
assert.deepEqual(createPaths.sort(), [
"/itrip/xft-api/v1/bills/apply/common/create",
"/itrip/xft-api/v1/bills/apply/entertainment/create",
"/itrip/xft-api/v1/bills/apply/expense/create",
"/itrip/xft-api/v1/bills/apply/expense/create",
"/itrip/xft-api/v1/bills/apply/travel/create",
"/itrip/xft-api/v1/reimburse/bill/create",
"/itrip/xft-api/v1/reimburse/bill/travel/create",
]);
});
test("Xft submission fails closed when the authenticated member has no employee number", async () => {
let createCalls = 0;
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: [{
eventSeqId: 1, eventNumber: "FORM-1", eventName: "通用申请", eventStatus: "1", eventType: "COMMON_APPLY",
}] } });
if (path.endsWith("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
memberId: "EMP-1", number: null, idRelation: { enterpriseUserId: "EMP-1", staffId: "0000000001" },
} });
if (path.endsWith("bills/apply/common/create")) createCalls += 1;
return encrypted({ code: "0", data: {} });
});
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
await assert.rejects(client.submitApproval({ userId: "ding-u1" }, template, {}), /未维护员工号/);
assert.equal(createCalls, 0);
});
test("Xft submission rejects a member lookup that does not match the authenticated identity", async () => {
let createCalls = 0;
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: [{
eventSeqId: 1, eventNumber: "FORM-1", eventName: "通用申请", eventStatus: "1", eventType: "COMMON_APPLY",
}] } });
if (path.endsWith("member/get/by-id")) return encrypted({ returnCode: "SUC0000", body: {
memberId: "EMP-OTHER", number: "VICTIM-001", idRelation: { enterpriseUserId: "EMP-OTHER" },
} });
if (path.endsWith("bills/apply/common/create")) createCalls += 1;
return encrypted({ code: "0", data: {} });
});
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
await assert.rejects(client.submitApproval({ userId: "ding-u1" }, template, {}), /登录身份不一致/);
assert.equal(createCalls, 0);
});
test("Xft submission rejects caller-controlled applicant and status fields before create", async () => {
const client = new XftOpenApiClient(config(), async (input, init) => {
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: [{
eventSeqId: 1, eventNumber: "FORM-1", eventName: "通用申请", eventStatus: "1", eventType: "COMMON_APPLY",
}] } });
throw new Error(`unexpected create call ${path} ${String(init?.body)}`);
});
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
await assert.rejects(client.submitApproval({ userId: "ding-u1" }, template, { empNumber: "victim" }), /Agent 不得传入/);
});
test("Xft OpenAPI tolerates eventual consistency after an accepted write", async () => {
let todoQueries = 0;
const client = new XftOpenApiClient(config(), async (input, init) => {
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("todo-task/page/by-user")) {
todoQueries += 1;
return encrypted({ code: "0", data: { records: todoQueries < 4 ? [{ todoTaskId: "TD-1", groupId: "10000000001", appCode: "xft-itrip" }] : [] } });
}
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [] });
if (path.endsWith("bill-list")) return encrypted({ code: "0", data: { list: [{ billId: "10000000001", eventType: "COMMON_REIM" }] } });
if (path.endsWith("approve/pass")) { void decodedBody(init); return encrypted({ code: "0", data: {} }); }
throw new Error(`unexpected ${path}`);
});
const result = await client.processApprovalBatch({ userId: "ding-u1" }, "agree", "同意", [{ billId: "10000000001" }]);
assert.equal(result.succeeded, 1);
assert.equal(todoQueries, 4);
});
test("Xft detail falls back to generic bill detail when a specialized endpoint is unavailable", async () => {
const targetBillId = "2099010400000001";
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("todo-task/page/by-user")) return encrypted({ code: "0", data: { records: [{ todoTaskId: "TD-1", groupId: targetBillId, appCode: "xft-itrip" }] } });
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [] });
if (path.endsWith("bill-list")) return encrypted({ code: "0", data: { list: [{ billId: targetBillId, eventType: "EXPENSE_APPLY" }] } });
if (path.endsWith("bills/apply/expense/detail")) return new Response("Not Found", { status: 404 });
if (path.endsWith(`bills/detail/${targetBillId}`)) return encrypted({ code: "0", data: { billId: targetBillId, applyExpenses: [{ expenseNumber: "MEAL" }] } });
throw new Error(`unexpected ${path}`);
});
const detail = await client.getTodoDetail({ userId: "ding-u1" }, targetBillId);
assert.deepEqual(detail.form, { billId: targetBillId, applyExpenses: [{ expenseNumber: "MEAL" }] });
assert.equal((detail.diagnostics as Record<string, unknown>).detailFallback, "generic_bill_detail_after_specialized_404");
});
test("Xft ownership verification scans beyond the first 1000 pending tasks", async () => {
const targetBillId = "99999999999";
const requestedPages: number[] = [];
const firstPage = Array.from({ length: 1000 }, (_, index) => ({
todoTaskId: `TD-${index}`, groupId: String(10_000_000_000 + index), appCode: "xft-itrip",
}));
const client = new XftOpenApiClient(config(), async (input, init) => {
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("todo-task/page/by-user")) {
const body = decodedBody(init);
requestedPages.push(Number(body.currentPage));
return encrypted({ code: "0", data: { records: body.currentPage === 1 ? firstPage : [{ todoTaskId: "TD-TARGET", groupId: targetBillId, appCode: "xft-itrip" }] } });
}
if (path.endsWith("todo-task/detail/by-ids")) return encrypted({ code: "0", data: [] });
if (path.endsWith("bill-list")) return encrypted({ code: "0", data: { list: [{ billId: targetBillId, eventType: "COMMON_REIM" }] } });
if (path.endsWith("reimburse/bill/common/detail")) return encrypted({ code: "0", data: { billId: targetBillId } });
throw new Error(`unexpected ${path}`);
});
const detail = await client.getTodoDetail({ userId: "ding-u1" }, targetBillId);
assert.equal(detail.todo.billId, targetBillId);
assert.deepEqual(requestedPages, [1, 2]);
});
test("Xft OpenAPI never calls an action endpoint for a bill outside the user's live todos", async () => {
let actionCalls = 0;
const client = new XftOpenApiClient(config(), async (input, init) => {
const path = new URL(String(input)).pathname;
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { list: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
if (path.endsWith("todo-task/page/by-user")) return encrypted({ code: "0", data: { records: [] } });
if (path.includes("/approve/")) actionCalls += 1;
void init;
return encrypted({ code: "0", data: {} });
});
const result = await client.processApprovalBatch({ userId: "ding-u1" }, "agree", "", [{ billId: "other-user-bill" }]);
assert.equal(result.failed, 1);
assert.equal(actionCalls, 0);
});