实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户