Files
dingMCP/test/xft-open-api.test.ts
T
gitea-admin 504145aec6
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: initial public release
2026-08-17 03:04:09 +00:00

707 lines
40 KiB
TypeScript

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>;
}
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 continues past an irrelevant full first todo page", async () => {
const pages: number[] = [];
const irrelevant = Array.from({ length: 100 }, (_, index) => ({
todoTaskId: `OTHER-${index}`, groupId: `OTHER-${index}`, appCode: "other-app",
}));
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: page === 1 ? irrelevant : [{
todoTaskId: "TD-TARGET", 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", title: "目标费用单",
}] } });
throw new Error(`unexpected ${path}`);
});
const result = await client.listMyTodos({ userId: "ding-u1" }, 1);
assert.equal(result.items[0]?.billId, "2026081400000001");
assert.deepEqual(pages, [1, 2]);
});
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 () => encrypted({
SYCOMRETZ: [{ ERRCOD: "SYOPA16", ERRMSG: "应用状态错误" }],
}));
await assert.rejects(client.authStatus({ userId: "ding-u1" }), /应用状态错误/);
});
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 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: [],
}] });
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, 1);
const standardFields = (schema.inputContract.values as Record<string, unknown>).standardFields as Array<Record<string, unknown>>;
const expenseList = standardFields.find((field) => field.key === "expenseList");
assert.deepEqual((expenseList?.itemFields as Array<Record<string, unknown>>).filter((field) => field.required).map((field) => field.key), [
"expTypeCode", "currencyCode", "originalCurrencyReimAmt",
]);
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 submission options expose only the current user's departments and searchable city codes", async () => {
const templateRecord = {
eventSeqId: 1, eventNumber: "TRAVEL-1", eventName: "出差费用申请单", eventStatus: "1", eventType: "TRAVEL_APPLY",
};
let cityRequests = 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("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: "东莞市" }],
} });
}
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);
});
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, 8);
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/bills/apply/use-car/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);
});