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; } 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 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; }; 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 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: [], }] }); 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).standardFields as Array>; const expenseList = standardFields.find((field) => field.key === "expenseList"); assert.deepEqual((expenseList?.itemFields as Array>).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[] = []; 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, 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).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); });