Files
calendar/packages/features/tasker/tasks/scanWorkflowBody.test.ts
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f98acb7301 refactor: migrate workflows utilities from trpc to features layer (#25534)
* refactor: migrate workflows utilities from trpc to features layer

Move workflow utility functions from packages/trpc/server/routers/viewer/workflows/util.ts
to packages/features/ee/workflows/lib/workflowUtils.ts to break circular dependencies.

Functions migrated:
- isAuthorized
- getAllWorkflowsFromEventType
- scheduleWorkflowNotifications
- scheduleBookingReminders

Changes:
- Created new workflowUtils.ts in features layer with migrated functions
- Added getBookingsForWorkflowReminders to WorkflowRepository (no prisma outside repositories)
- Updated all imports in features layer to use new location
- Updated trpc util.ts to re-export from features for backward compatibility
- Fixed pre-existing lint warning (any -> unknown) in handleMarkNoShow.ts

This is part of the effort to remove circular dependencies between
packages/features and packages/trpc.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove re-exports from trpc util.ts, update imports to use features layer

Per user request, removed the backward compatibility re-exports from
packages/trpc/server/routers/viewer/workflows/util.ts and updated all
imports in the trpc package to import directly from the features layer.

Files updated to import from @calcom/features/ee/workflows/lib/workflowUtils:
- confirm.handler.ts (getAllWorkflowsFromEventType)
- delete.handler.ts (isAuthorized)
- update.handler.ts (isAuthorized, scheduleWorkflowNotifications)
- getAllActiveWorkflows.handler.ts (getAllWorkflowsFromEventType)
- get.handler.ts (isAuthorized)
- util.test.ts (isAuthorized)
- delete.handler.test.ts (isAuthorized)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update test imports to use features layer for workflow utilities

Updated test files to import scheduleBookingReminders and
scheduleWorkflowNotifications from @calcom/features/ee/workflows/lib/workflowUtils
instead of @calcom/trpc/server/routers/viewer/workflows/util.

Files updated:
- packages/features/ee/workflows/lib/test/workflows.test.ts
- packages/features/tasker/tasks/scanWorkflowBody.test.ts

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: split workflowUtils.ts into individual files

Split the monolithic workflowUtils.ts into separate files for each function:
- isAuthorized.ts - Authorization check for workflow access
- getAllWorkflowsFromEventType.ts - Get workflows for an event type
- scheduleWorkflowNotifications.ts - Schedule workflow notifications
- scheduleBookingReminders.ts - Schedule booking reminders

The workflowUtils.ts now re-exports from these individual files for
backward compatibility.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix import

* fix more

* wip

* wip

* remove workflowUtils

* wip

* refactor deleteRemindersOfActiveOnIds

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-16 10:17:46 +00:00

233 lines
6.7 KiB
TypeScript

import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { lockUser, LockReason } from "@calcom/features/ee/api-keys/lib/autoLock";
import { scheduleWorkflowNotifications } from "@calcom/features/ee/workflows/lib/scheduleWorkflowNotifications";
import { scanWorkflowBody, iffyScanBody } from "./scanWorkflowBody";
vi.mock("@calcom/features/ee/api-keys/lib/autoLock", async (importActual) => {
const actual = await importActual<typeof import("@calcom/features/ee/api-keys/lib/autoLock")>();
return {
...actual, // Keep all original exports
lockUser: vi.fn(), // Override just the lockUser function
};
});
vi.mock("@calcom/features/ee/workflows/lib/scheduleWorkflowNotifications", () => ({
scheduleWorkflowNotifications: vi.fn(),
}));
vi.mock("./scanWorkflowBody", async (importActual) => {
const actual = await importActual<typeof import("./scanWorkflowBody")>();
return {
...actual,
iffyScanBody: vi.fn(),
};
});
const mockWorkflowStep = {
id: 1,
reminderBody: "Test reminder body",
workflow: {
user: {
timeFormat: 24,
},
},
};
const mockWorkflow = {
id: 1,
time: 24,
timeUnit: "hour",
trigger: "BEFORE",
activeOn: [{ eventTypeId: 1 }],
team: null,
};
describe("scanWorkflowBody", () => {
const mockFetch = vi.fn();
beforeEach(() => {
vi.resetAllMocks();
vi.stubGlobal("fetch", mockFetch);
process.env.IFFY_API_KEY = "test-key";
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
});
it("should skip scan if IFFY_API_KEY is not set", async () => {
process.env.IFFY_API_KEY = "";
const payload = JSON.stringify({
userId: 1,
workflowStepIds: [1],
});
await scanWorkflowBody(payload);
expect(iffyScanBody).not.toHaveBeenCalled();
});
it("should mark workflow step as safe if no reminder body", async () => {
const payload = JSON.stringify({
userId: 1,
workflowStepIds: [1],
});
prismaMock.workflowStep.findMany.mockResolvedValue([{ ...mockWorkflowStep, reminderBody: null }]);
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
await scanWorkflowBody(payload);
expect(prismaMock.workflowStep.update).toHaveBeenCalledWith({
where: { id: 1 },
data: { verifiedAt: expect.any(Date) },
});
});
it("should mark workflow step as safe if content is not spam", async () => {
const payload = JSON.stringify({
userId: 1,
workflowStepIds: [1],
});
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
mockFetch.mockResolvedValue({
json: () => Promise.resolve({ flagged: false }),
});
await scanWorkflowBody(payload);
expect(mockFetch).toHaveBeenCalledWith("https://api.iffy.com/api/v1/moderate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer test-key`,
},
body: JSON.stringify({
clientId: "Workflow step - 1",
name: "Workflow",
entity: "WorkflowBody",
content: "Test reminder body",
passthrough: true,
}),
});
expect(prismaMock.workflowStep.update).toHaveBeenCalledWith({
where: { id: 1 },
data: { verifiedAt: expect.any(Date) },
});
});
it.skip("should lock user and not update step if content is spam", async () => {
const payload = JSON.stringify({
userId: 1,
workflowStepIds: [1],
});
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
mockFetch.mockResolvedValue({
json: () => Promise.resolve({ flagged: true }),
});
await scanWorkflowBody(payload);
expect(mockFetch).toHaveBeenCalledWith("https://api.iffy.com/api/v1/moderate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer test-key`,
},
body: JSON.stringify({
clientId: "Workflow step - 1",
name: "Workflow",
entity: "WorkflowBody",
content: "Test reminder body",
passthrough: true,
}),
});
expect(prismaMock.workflowStep.update).not.toHaveBeenCalled();
expect(lockUser).toHaveBeenCalledWith("userId", "1", LockReason.SPAM_WORKFLOW_BODY);
});
it("should schedule workflow notifications after successful scan", async () => {
const payload = JSON.stringify({
userId: 1,
workflowStepIds: [1],
});
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
await scanWorkflowBody(payload);
expect(scheduleWorkflowNotifications).toHaveBeenCalledWith({
activeOn: [1],
isOrg: false,
workflowSteps: [expect.objectContaining(mockWorkflowStep)],
time: mockWorkflow.time,
timeUnit: mockWorkflow.timeUnit,
trigger: mockWorkflow.trigger,
userId: 1,
teamId: null,
});
});
it("should handle invalid payload", async () => {
const payload = "invalid-json";
await expect(scanWorkflowBody(payload)).rejects.toThrow();
});
it("should handle workflow not found", async () => {
const payload = JSON.stringify({
userId: 1,
workflowStepIds: [1],
});
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
prismaMock.workflow.findFirst.mockResolvedValue(null);
await scanWorkflowBody(payload);
expect(scheduleWorkflowNotifications).not.toHaveBeenCalled();
});
it("should handle whitelisted user being flagged as spam", async () => {
const payload = JSON.stringify({
userId: 1,
workflowStepIds: [1],
});
prismaMock.workflowStep.findMany.mockResolvedValue([
{ ...mockWorkflowStep, workflow: { user: { whitelistWorkflows: true } } },
]);
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
mockFetch.mockResolvedValue({
json: () => Promise.resolve({ flagged: true }),
});
await scanWorkflowBody(payload);
expect(mockFetch).toHaveBeenCalledWith("https://api.iffy.com/api/v1/moderate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer test-key`,
},
body: JSON.stringify({
clientId: "Workflow step - 1",
name: "Workflow",
entity: "WorkflowBody",
content: "Test reminder body",
passthrough: true,
}),
});
expect(prismaMock.workflowStep.update).toHaveBeenCalled();
expect(scheduleWorkflowNotifications).toHaveBeenCalled();
expect(lockUser).not.toHaveBeenCalled();
});
});