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 { const envelope = JSON.parse(String(init?.body)) as { secretMsg: string }; return JSON.parse(String(sm4.decrypt(envelope.secretMsg, privateKey.slice(0, 32)))) as Record; } function findRecordByKey(value: unknown, key: string): Record | 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; 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> { if (Array.isArray(value)) return value.flatMap((item) => findRecordsByKey(item, key)); if (!value || typeof value !== "object") return []; const record = value as Record; 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; 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 }> = []; 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; 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).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; }; 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[] = []; 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).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[] = []; 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).standardFields as Array>; const minimalExample = (schema.inputContract.values as Record).minimalExample as Record; 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>; 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 | 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; assert.ok(values.minimalExample && typeof values.minimalExample === "object"); assert.match(JSON.stringify(values.minimalExample), / 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)?.exposeInternalValue, false); assert.doesNotMatch(String(field.label), /编码/); } } const travelMode = findRecordByKey(combined, "travelMode"); assert.equal((travelMode?.enumLabels as Record)?.AirPlane, "飞机"); assert.equal((travelMode?.enumLabels as Record)?.RideHailing, "网约车"); assert.equal((findRecordByKey(combined, "roundTrip")?.enumLabels as Record)?.Y, "往返"); assert.equal((findRecordByKey(combined, "beginTimePrecision")?.enumLabels as Record)?.AM, "上午"); assert.equal((findRecordByKey(combined, "currencyCode")?.enumLabels as Record)?.CNY, "人民币"); assert.equal((findRecordByKey(combined, "hotelStar")?.enumLabels as Record)?.["1"], "五星"); for (const key of ["peerEmpNumbers", "participants", "customArchiveOptionNumbers", "aircraftCabin", "trainCabin", "shipCabin", "relatedBillId"]) { const field = findRecordByKey(combined, key); assert.equal((field?.selectionCapability as Record)?.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)?.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[] = []; 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).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).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).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); });