实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-31 18:57:34 +08:00
父节点 e123d50f86
当前提交 410d8cd71b
共修改 23 个文件,包含 1352 行新增和 69 行删除
+115
查看文件
@@ -4,6 +4,8 @@ const fastifyCookie = require("@fastify/cookie");
const { AppError } = require("../../../src/utils/AppError");
const wecomAuthRoutes = require("../../../src/routes/wecomAuth");
const { signPayload, verifyToken } = require("../../../src/utils/signedUrl");
const { clearWecomAppAccessTokenCache } = require("../../../src/services/integrations/wecomAccessToken");
const { clearWecomJsApiTicketCache } = require("../../../src/services/integrations/wecomJsSdk");
const NOW = new Date("2026-03-22T11:00:00.000Z");
const EMPLOYEE_ID = "11111111-1111-4111-8111-111111111111";
@@ -103,6 +105,8 @@ function mockWecomFetchWithPayload(payload) {
afterEach(() => {
global.fetch = originalFetch;
clearWecomAppAccessTokenCache();
clearWecomJsApiTicketCache();
vi.restoreAllMocks();
});
@@ -1069,4 +1073,115 @@ describe("wecom auth routes R27", () => {
await app.close();
});
it("signs a same-origin page URL without exposing WeCom credentials", async () => {
global.fetch = vi.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => ({ errcode: 0, access_token: "synthetic-access-token", expires_in: 7200 }),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({ errcode: 0, ticket: "synthetic-jsapi-ticket", expires_in: 7200 }),
});
const app = await buildApp();
const response = await app.inject({
method: "POST",
url: "/api/auth/wecom/js-sdk-signature",
headers: { authorization: "Bearer test-token", "content-type": "application/json" },
payload: { url: "https://oa.hyinda.com/mobile/reimbursements/test?source=wecom" },
});
expect(response.statusCode).toBe(200);
expect(response.json().data).toEqual({
corpId: "wx-test-corp",
timestamp: expect.any(Number),
nonceStr: expect.any(String),
signature: expect.stringMatching(/^[a-f0-9]{40}$/),
});
const serialized = JSON.stringify(response.json()).toLowerCase();
expect(serialized).not.toContain("synthetic-access-token");
expect(serialized).not.toContain("synthetic-jsapi-ticket");
expect(serialized).not.toContain("wecom-secret");
expect(global.fetch).toHaveBeenCalledTimes(2);
await app.close();
});
it("rejects cross-origin and credential-bearing JS-SDK signing URLs before provider access", async () => {
global.fetch = vi.fn();
const app = await buildApp();
const crossOrigin = await app.inject({
method: "POST",
url: "/api/auth/wecom/js-sdk-signature",
headers: { authorization: "Bearer test-token", "content-type": "application/json" },
payload: { url: "https://evil.example/mobile" },
});
const credentialUrl = await app.inject({
method: "POST",
url: "/api/auth/wecom/js-sdk-signature",
headers: { authorization: "Bearer test-token", "content-type": "application/json" },
payload: { url: "https://user:[email protected]/mobile" },
});
const fragmentUrl = await app.inject({
method: "POST",
url: "/api/auth/wecom/js-sdk-signature",
headers: { authorization: "Bearer test-token", "content-type": "application/json" },
payload: { url: "https://oa.hyinda.com/mobile#detail" },
});
expect(crossOrigin.statusCode).toBe(403);
expect(credentialUrl.statusCode).toBe(400);
expect(fragmentUrl.statusCode).toBe(400);
expect(global.fetch).not.toHaveBeenCalled();
await app.close();
});
it("returns a stable redacted error when the JSAPI ticket provider fails", async () => {
global.fetch = vi.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => ({ errcode: 0, access_token: "synthetic-access-token", expires_in: 7200 }),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({ errcode: 40001, errmsg: "provider-secret-detail" }),
});
const app = await buildApp();
const response = await app.inject({
method: "POST",
url: "/api/auth/wecom/js-sdk-signature",
headers: { authorization: "Bearer test-token", "content-type": "application/json" },
payload: { url: "https://oa.hyinda.com/mobile" },
});
expect(response.statusCode).toBe(503);
expect(response.json().error.details).toEqual({ wecom: "jsapi_ticket_unavailable" });
expect(JSON.stringify(response.json())).not.toContain("provider-secret-detail");
expect(JSON.stringify(response.json())).not.toContain("synthetic-access-token");
await app.close();
});
it("fails closed without provider access when JS-SDK credentials are incomplete", async () => {
global.fetch = vi.fn();
const app = await buildApp({ config: { wecomAgentSecret: "" } });
const response = await app.inject({
method: "POST",
url: "/api/auth/wecom/js-sdk-signature",
headers: { authorization: "Bearer test-token", "content-type": "application/json" },
payload: { url: "https://oa.hyinda.com/mobile" },
});
expect(response.statusCode).toBe(503);
expect(response.json().error.details).toEqual({ wecom: "jsapi_ticket_unavailable" });
expect(global.fetch).not.toHaveBeenCalled();
await app.close();
});
});