const { buildDetailUrl, buildImDedupeKey, enqueueImNotifications, resolveRecipientRoleCodes, } = require("../../../src/services/notifications/imEnqueue"); const { verifyToken } = require("../../../src/utils/signedUrl"); describe("IM enqueue", () => { it("builds detail url from APP_ORIGIN", () => { const detailUrl = buildDetailUrl({ appOrigin: "https://app.example.com", jwtSecret: "test-secret" }, "/workflow/tasks/1"); expect(detailUrl).toContain( "https://app.example.com/api/auth/deeplink?token=", ); const token = new URL(detailUrl).searchParams.get("token"); const verified = verifyToken(token, "test-secret"); expect(verified.ok).toBe(true); expect(verified.payload).toEqual({ targetPath: "/workflow/tasks/1" }); expect(buildDetailUrl({ appOrigin: "" }, "/workflow/tasks/1")).toBeNull(); }); it("builds dedupe key with target id first", () => { expect( buildImDedupeKey({ notificationType: "workflow.task_assigned", targetType: "workflow_task", targetId: "task-1", targetPath: "/workflow/tasks/task-1", channelId: "channel-1", }), ).toBe("workflow.task_assigned:workflow_task:task-1:channel-1"); }); it("resolves unique recipient role codes", async () => { const roles = await resolveRecipientRoleCodes( { employeeRole: { findMany: vi.fn().mockResolvedValue([ { role_code: "manager" }, { role_code: "manager" }, { role_code: "finance_mgr" }, ]), }, }, ["employee-1", "employee-2"], ); expect(roles).toEqual(["manager", "finance_mgr"]); }); it("enqueues one outbox event per matched channel", async () => { const tx = { employeeRole: { findMany: vi.fn().mockResolvedValue([{ role_code: "manager" }]), }, notificationChannel: { findMany: vi.fn().mockResolvedValue([ { id: "channel-1", channel_type: "wecom", name: "manager-workflow", enabled: true, config_json: { routing: { roleCodes: ["manager"], domainScopes: ["workflow"], allowedTypes: ["workflow.task_assigned"], }, }, template_json: null, }, { id: "channel-2", channel_type: "wechat_work", name: "all-workflow", enabled: true, config_json: { routing: { roleCodes: [], domainScopes: ["workflow"], allowedTypes: [], }, }, template_json: null, }, ]), }, integrationOutbox: { findUnique: vi .fn() .mockResolvedValueOnce(null) .mockResolvedValueOnce({ id: "outbox-2", status: "pending" }), create: vi.fn().mockImplementation(async ({ data }) => ({ id: `${data.dedupe_key}-id`, status: "pending" })), }, }; const result = await enqueueImNotifications(tx, { config: { imNotifierEnabled: true, integrationOutboxMaxAttempts: 5, appOrigin: "https://app.example.com", jwtSecret: "test-secret", }, logger: { warn: vi.fn() }, recipientEmployeeIds: ["employee-1"], type: "workflow.task_assigned", title: "【待办】采购申请待审批", body: "采购申请已流转到你,请及时处理。", targetType: "workflow_task", targetId: "task-1", targetPath: "/workflow/tasks/task-1", entityType: "purchase_request", entityId: "pr-1", actorId: "actor-1", }); expect(result.queued).toBe(1); expect(result.duplicated).toBe(1); expect(result.matchedChannels).toEqual(["channel-1", "channel-2"]); expect(tx.integrationOutbox.create).toHaveBeenCalledTimes(1); expect(tx.integrationOutbox.create.mock.calls[0][0].data.payload.detailUrl).toEqual( expect.stringContaining("https://app.example.com/api/auth/deeplink?token="), ); }); it("redacts project follow-up notifications under public security hardening", async () => { const tx = { employeeRole: { findMany: vi.fn().mockResolvedValue([{ role_code: "manager" }]), }, notificationChannel: { findMany: vi.fn().mockResolvedValue([ { id: "channel-project", channel_type: "wecom", name: "project-workflow", enabled: true, config_json: { routing: { roleCodes: ["manager"], domainScopes: ["workflow"], allowedTypes: ["workflow.task_assigned"], }, }, template_json: null, }, ]), }, integrationOutbox: { findUnique: vi.fn().mockResolvedValue(null), create: vi.fn().mockImplementation(async ({ data }) => ({ id: `${data.dedupe_key}-id`, status: "pending" })), }, }; const result = await enqueueImNotifications(tx, { config: { imNotifierEnabled: true, integrationOutboxMaxAttempts: 5, appOrigin: "https://app.example.com", jwtSecret: "test-secret", publicSecurityHardeningEnabled: true, }, logger: { warn: vi.fn() }, recipientEmployeeIds: ["employee-1"], type: "workflow.task_assigned", title: "项目收口提醒:合同与沟通记录待补齐", body: "请补齐沟通摘要、收口备注与合同要点。", targetType: "workflow_task", targetId: "task-project-1", targetPath: "/projects/11111111-1111-4111-8111-111111111111", entityType: "project", entityId: "11111111-1111-4111-8111-111111111111", actorId: "actor-1", }); expect(result.queued).toBe(1); expect(tx.integrationOutbox.create.mock.calls[0][0].data.payload.title).toBe("有项目待收口"); expect(tx.integrationOutbox.create.mock.calls[0][0].data.payload.body).toBe(""); }); it("safely skips when no channel matches", async () => { const tx = { employeeRole: { findMany: vi.fn().mockResolvedValue([{ role_code: "manager" }]), }, notificationChannel: { findMany: vi.fn().mockResolvedValue([ { id: "channel-3", channel_type: "wecom", name: "finance-only", enabled: true, config_json: { routing: { roleCodes: ["finance_mgr"], domainScopes: ["procurement"], allowedTypes: ["purchase_request.approved"], }, }, template_json: null, }, ]), }, integrationOutbox: { findUnique: vi.fn(), create: vi.fn(), }, }; const result = await enqueueImNotifications(tx, { config: { imNotifierEnabled: true, integrationOutboxMaxAttempts: 5, appOrigin: "https://app.example.com", jwtSecret: "test-secret", }, logger: { warn: vi.fn() }, recipientEmployeeIds: ["employee-1"], type: "workflow.task_assigned", title: "title", body: "body", targetType: "workflow_task", targetId: "task-1", targetPath: "/workflow/tasks/task-1", entityType: "purchase_request", entityId: "pr-1", actorId: "actor-1", }); expect(result.skippedReason).toBe("no_matching_channel"); expect(tx.integrationOutbox.create).not.toHaveBeenCalled(); }); });