Files
calendar/packages/features/tasker/tasks/scanWorkflowBody.test.ts
T
Joe Au-YeungandGitHub 8c9eb18463 chore: Add spam checking for workflow bodies (#18822)
* Add akismet package to tasker

* Create scanWorkflowBody task

* Schedule workflow body scan

* Add AKISMET_API_KEY .env

* Auto lock user if spam is detected

* Uncommit key

* Add safe param to workflow step

* Migration for safe field

* Do not process workflow steps is `safe` is false

* Update migration to set previous records to true

* Address comments

* Refactor `scheduleWorkflowNotifications` to accept an object

* If new steps or editing old ones send to tasker

* Call `scheduleWorkflowNotifications` in task

* Fix `IS_SELF_HOSTED`

* Remove unused function

* Make `safe` optional in schema

* Type fix

* Revert "Make `safe` optional in schema"

This reverts commit d0964702affa87c35562300301473d25635c565b.

* Revert "Type fix"

This reverts commit d9a031303269a2994ae46f576ab2a3d31e4d977b.

* Type fixes

* Type fixes

* Address comments

* Fix tests

* Add tests

* Update tests

* Typo fix

* Update `safe` to `verifiedAt`

* feat: Compare workflow reminder bodies to default template (#19060)

* Add `getTemplateForAction` function

* Use `getTemplateForAction` when creating a new step

* Use `getTemplateForAction` when action changes

* Have `emailReminderTemplate` accept an object as a param

* Rename `getTemplateForAction` to  `getTemplateBodyForAction`

* Simplify changing body when changing templates

* Create `compareReminderBodyToTemplate`

* In task, compare if reminderBody is a template

* Linting

* Add tests

* refactor: `emailReminderTemplate` to accept object as param (#19288)

* Add `getTemplateForAction` function

* Use `getTemplateForAction` when creating a new step

* Use `getTemplateForAction` when action changes

* Have `emailReminderTemplate` accept an object as a param

* Rename `getTemplateForAction` to  `getTemplateBodyForAction`

* Simplify changing body when changing templates

* Create `compareReminderBodyToTemplate`

* In task, compare if reminderBody is a template

* Linting

* Add tests

* Refactor `scheduleEmailReminders`

* Refactor `create.handler` for new workflows

* Refactor `emailReminderManager`

* Refactor `getEmailTemplateText`

* Fix typo

* Type fix - whatsapp plain text template imports

* Type fix - no template found

* Type fix - add `isBrandingDisabled` to `emailReminderTemplate`

* Add workflow and user to prisma mock

* Fix imports for akismet dependencies

* Record user lock reason

* Undo linting changes

* Fix tests

* New workflow, at verify created step

* Handle if `SCANNING_WORKFLOW_STEPS` is toggled

* Move `verifiedAt` checks to specific schedule functions
- `scheduleWhatsappReminder`
- `scheduleEmailReminder`
- `scheduleSMSReminder`

* Update logic

* Do not fallback verifiedAt

* Add comment to next.config.js
2025-04-02 08:16:26 -07:00

176 lines
4.8 KiB
TypeScript

import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { lockUser, LockReason } from "@calcom/lib/autoLock";
import { scheduleWorkflowNotifications } from "@calcom/trpc/server/routers/viewer/workflows/util";
import { scanWorkflowBody } from "./scanWorkflowBody";
const mockAkismetCheckSpam = vi.fn();
// Mock the entire module
vi.mock("akismet-api", () => {
return {
AkismetClient: class {
constructor() {
return {
checkSpam: mockAkismetCheckSpam,
};
}
},
};
});
vi.mock("@calcom/lib/autoLock", async (importActual) => {
const actual = await importActual<typeof import("@calcom/lib/autoLock")>();
return {
...actual, // Keep all original exports
lockUser: vi.fn(), // Override just the lockUser function
};
});
vi.mock("@calcom/trpc/server/routers/viewer/workflows/util", () => ({
scheduleWorkflowNotifications: 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", () => {
beforeEach(() => {
vi.resetAllMocks();
process.env.AKISMET_API_KEY = "test-key";
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
});
it("should skip scan if AKISMET_API_KEY is not set", async () => {
process.env.AKISMET_API_KEY = "";
const payload = JSON.stringify({
userId: 1,
workflowStepIds: [1],
});
await scanWorkflowBody(payload);
expect(prismaMock.workflowStep.findMany).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);
mockAkismetCheckSpam.mockResolvedValue(false);
await scanWorkflowBody(payload);
expect(mockAkismetCheckSpam).toHaveBeenCalledWith({
user_ip: "127.0.0.1",
content: mockWorkflowStep.reminderBody,
});
expect(prismaMock.workflowStep.update).toHaveBeenCalledWith({
where: { id: 1 },
data: { verifiedAt: expect.any(Date) },
});
});
it("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]);
mockAkismetCheckSpam.mockResolvedValue(true);
await scanWorkflowBody(payload);
expect(mockAkismetCheckSpam).toHaveBeenCalled();
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);
mockAkismetCheckSpam.mockResolvedValue(false);
await scanWorkflowBody(payload);
expect(scheduleWorkflowNotifications).toHaveBeenCalledWith({
activeOn: [1],
isOrg: false,
workflowSteps: [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);
mockAkismetCheckSpam.mockResolvedValue(false);
await scanWorkflowBody(payload);
expect(scheduleWorkflowNotifications).not.toHaveBeenCalled();
});
});