test: add unit tests for sms sending (#14737)
* add tests for workflow sms * fix type error * improvements * add tests for team workflow * rename test * add emailsToReceive everywhere * fix type errors * code clean up * fix fresh-booking.test.ts * add destination email to emailsToReceive * remove unused webhooks --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Hariom <hariombalhara@gmail.com>
This commit is contained in:
co-authored by
CarinaWolli
Udit Takkar
Hariom
parent
c18407d75a
commit
0391bfe887
Vendored
+11
@@ -2,15 +2,20 @@
|
||||
import { test as base } from "vitest";
|
||||
|
||||
import { getTestEmails } from "@calcom/lib/testEmails";
|
||||
import { getTestSMS } from "@calcom/lib/testSMS";
|
||||
|
||||
export interface Fixtures {
|
||||
emails: ReturnType<typeof getEmailsFixture>;
|
||||
sms: ReturnType<typeof getSMSFixture>;
|
||||
}
|
||||
|
||||
export const test = base.extend<Fixtures>({
|
||||
emails: async ({}, use) => {
|
||||
await use(getEmailsFixture());
|
||||
},
|
||||
sms: async ({}, use) => {
|
||||
await use(getSMSFixture());
|
||||
},
|
||||
});
|
||||
|
||||
function getEmailsFixture() {
|
||||
@@ -18,3 +23,9 @@ function getEmailsFixture() {
|
||||
get: getTestEmails,
|
||||
};
|
||||
}
|
||||
|
||||
function getSMSFixture() {
|
||||
return {
|
||||
get: getTestSMS,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import type { WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@calcom/prisma/client";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { SchedulingType, SMSLockState } from "@calcom/prisma/enums";
|
||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { userMetadataType } from "@calcom/prisma/zod-utils";
|
||||
@@ -952,6 +952,7 @@ export function getOrganizer({
|
||||
teams,
|
||||
organizationId,
|
||||
metadata,
|
||||
smsLockState,
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
@@ -965,6 +966,7 @@ export function getOrganizer({
|
||||
weekStart?: WeekDays;
|
||||
teams?: InputUser["teams"];
|
||||
metadata?: userMetadataType;
|
||||
smsLockState?: SMSLockState;
|
||||
}) {
|
||||
return {
|
||||
...TestData.users.example,
|
||||
@@ -981,6 +983,7 @@ export function getOrganizer({
|
||||
organizationId,
|
||||
profiles: [],
|
||||
metadata,
|
||||
smsLockState,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -307,38 +307,72 @@ export function expectWebhookToHaveBeenCalledWith(
|
||||
|
||||
export function expectWorkflowToBeTriggered({
|
||||
emails,
|
||||
organizer,
|
||||
destinationEmail,
|
||||
emailsToReceive,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: { email: string; name: string; timeZone: string };
|
||||
destinationEmail?: string;
|
||||
emailsToReceive: string[];
|
||||
}) {
|
||||
const subjectPattern = /^Reminder: /i;
|
||||
expect(emails.get()).toEqual(
|
||||
emailsToReceive.forEach((email) => {
|
||||
expect(emails.get()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
subject: expect.stringMatching(subjectPattern),
|
||||
to: email,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function expectWorkflowToBeNotTriggered({
|
||||
emails,
|
||||
emailsToReceive,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
emailsToReceive: string[];
|
||||
}) {
|
||||
const subjectPattern = /^Reminder: /i;
|
||||
|
||||
emailsToReceive.forEach((email) => {
|
||||
expect(emails.get()).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
subject: expect.stringMatching(subjectPattern),
|
||||
to: email,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function expectSMSWorkflowToBeTriggered({
|
||||
sms,
|
||||
toNumber,
|
||||
}: {
|
||||
sms: Fixtures["sms"];
|
||||
toNumber: string;
|
||||
}) {
|
||||
expect(sms.get()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
subject: expect.stringMatching(subjectPattern),
|
||||
to: destinationEmail ?? organizer.email,
|
||||
to: toNumber,
|
||||
}),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
export function expectWorkflowToBeNotTriggered({
|
||||
emails,
|
||||
organizer,
|
||||
export function expectSMSWorkflowToBeNotTriggered({
|
||||
sms,
|
||||
toNumber,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: { email: string; name: string; timeZone: string };
|
||||
sms: Fixtures["sms"];
|
||||
toNumber: string;
|
||||
}) {
|
||||
const subjectPattern = /^Reminder: /i;
|
||||
|
||||
expect(emails.get()).not.toEqual(
|
||||
expect(sms.get()).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
subject: expect.stringMatching(subjectPattern),
|
||||
to: organizer.email,
|
||||
to: toNumber,
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ export function getMockRequestDataForBooking({
|
||||
email: string;
|
||||
name: string;
|
||||
location: { optionValue: ""; value: string };
|
||||
smsReminderNumber?: string;
|
||||
};
|
||||
};
|
||||
}) {
|
||||
|
||||
@@ -15,7 +15,7 @@ const _testWithAndWithoutOrg = (
|
||||
const t = mode === "only" ? test.only : mode === "skip" ? test.skip : test;
|
||||
t(
|
||||
`${description} - With org`,
|
||||
async ({ emails, meta, task, onTestFailed, expect, skip }) => {
|
||||
async ({ emails, sms, meta, task, onTestFailed, expect, skip }) => {
|
||||
const org = await createOrganization({
|
||||
name: "Test Org",
|
||||
slug: "testorg",
|
||||
@@ -27,6 +27,7 @@ const _testWithAndWithoutOrg = (
|
||||
onTestFailed,
|
||||
expect,
|
||||
emails,
|
||||
sms,
|
||||
skip,
|
||||
org: {
|
||||
organization: org,
|
||||
@@ -39,9 +40,10 @@ const _testWithAndWithoutOrg = (
|
||||
|
||||
t(
|
||||
`${description}`,
|
||||
async ({ emails, meta, task, onTestFailed, expect, skip }) => {
|
||||
async ({ emails, sms, meta, task, onTestFailed, expect, skip }) => {
|
||||
await fn({
|
||||
emails,
|
||||
sms,
|
||||
meta,
|
||||
task,
|
||||
onTestFailed,
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
|
||||
import {
|
||||
expectWorkflowToBeTriggered,
|
||||
expectWorkflowToBeNotTriggered,
|
||||
expectSuccessfulBookingCreationEmails,
|
||||
expectBookingToBeInDatabase,
|
||||
expectAwaitingPaymentEmails,
|
||||
@@ -51,7 +52,6 @@ import {
|
||||
expectBookingPaymentIntiatedWebhookToHaveBeenFired,
|
||||
expectBrokenIntegrationEmails,
|
||||
expectSuccessfulCalendarEventCreationInCalendar,
|
||||
expectWorkflowToBeNotTriggered,
|
||||
expectICalUIDAsString,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
@@ -215,9 +215,8 @@ describe("handleNewBooking", () => {
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({
|
||||
organizer,
|
||||
emailsToReceive: [organizerDestinationCalendarEmailOnEventType],
|
||||
emails,
|
||||
destinationEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
});
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
@@ -379,7 +378,7 @@ describe("handleNewBooking", () => {
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ organizer, emails });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
// We won't be sending evt.destinationCalendar in this case.
|
||||
@@ -541,7 +540,7 @@ describe("handleNewBooking", () => {
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ organizer, emails });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
|
||||
calendarId: "organizer@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
@@ -675,7 +674,7 @@ describe("handleNewBooking", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ organizer, emails });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
// FIXME: We should send Broken Integration emails on calendar event creation failure
|
||||
// expectCalendarEventCreationFailureEmails({ booker, organizer, emails });
|
||||
@@ -825,7 +824,7 @@ describe("handleNewBooking", () => {
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ organizer, emails });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
|
||||
calendarId: "organizer@google-calendar.com",
|
||||
@@ -982,7 +981,7 @@ describe("handleNewBooking", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ organizer, emails });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
|
||||
calendarId: "organizer@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
@@ -1511,7 +1510,7 @@ describe("handleNewBooking", () => {
|
||||
status: BookingStatus.PENDING,
|
||||
});
|
||||
|
||||
expectWorkflowToBeNotTriggered({ organizer, emails });
|
||||
expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectBookingRequestedEmails({
|
||||
booker,
|
||||
@@ -1636,7 +1635,7 @@ describe("handleNewBooking", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
expectWorkflowToBeNotTriggered({ organizer, emails });
|
||||
expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectBookingRequestedEmails({
|
||||
booker,
|
||||
@@ -1764,7 +1763,7 @@ describe("handleNewBooking", () => {
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ organizer, emails });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
|
||||
|
||||
@@ -1897,7 +1896,7 @@ describe("handleNewBooking", () => {
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectWorkflowToBeNotTriggered({ organizer, emails });
|
||||
expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectBookingRequestedEmails({ booker, organizer, emails });
|
||||
|
||||
@@ -2076,7 +2075,7 @@ describe("handleNewBooking", () => {
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ organizer, emails });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
|
||||
|
||||
@@ -2220,7 +2219,7 @@ describe("handleNewBooking", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
expectWorkflowToBeNotTriggered({ organizer, emails });
|
||||
expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectAwaitingPaymentEmails({ organizer, booker, emails });
|
||||
|
||||
@@ -2244,7 +2243,7 @@ describe("handleNewBooking", () => {
|
||||
status: BookingStatus.ACCEPTED,
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ organizer, emails });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectBookingCreatedWebhookToHaveBeenFired({
|
||||
booker,
|
||||
@@ -2374,7 +2373,7 @@ describe("handleNewBooking", () => {
|
||||
status: BookingStatus.PENDING,
|
||||
});
|
||||
|
||||
expectWorkflowToBeNotTriggered({ organizer, emails });
|
||||
expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectAwaitingPaymentEmails({ organizer, booker, emails });
|
||||
expectBookingPaymentIntiatedWebhookToHaveBeenFired({
|
||||
|
||||
@@ -240,7 +240,7 @@ describe("handleNewBooking", () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
expectWorkflowToBeTriggered({ emails, organizer });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, {
|
||||
calEvent: {
|
||||
@@ -459,7 +459,7 @@ describe("handleNewBooking", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ emails, organizer });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, {
|
||||
calEvent: {
|
||||
@@ -656,7 +656,7 @@ describe("handleNewBooking", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ emails, organizer });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
// FIXME: We should send Broken Integration emails on calendar event updation failure
|
||||
// expectBrokenIntegrationEmails({ booker, organizer, emails });
|
||||
@@ -850,7 +850,7 @@ describe("handleNewBooking", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ emails, organizer });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectBookingRequestedEmails({
|
||||
booker,
|
||||
@@ -1085,7 +1085,7 @@ describe("handleNewBooking", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ emails, organizer });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, {
|
||||
calEvent: {
|
||||
@@ -1312,7 +1312,7 @@ describe("handleNewBooking", () => {
|
||||
},
|
||||
});
|
||||
|
||||
//expectWorkflowToBeTriggered({emails, organizer});
|
||||
//expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectBookingRequestedEmails({
|
||||
booker,
|
||||
@@ -1559,7 +1559,7 @@ describe("handleNewBooking", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ emails, organizer });
|
||||
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
|
||||
|
||||
expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, {
|
||||
calEvent: {
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
import { describe, beforeEach } from "vitest";
|
||||
|
||||
import { resetTestSMS } from "@calcom/lib/testSMS";
|
||||
import { SMSLockState, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
import {
|
||||
createBookingScenario,
|
||||
getGoogleCalendarCredential,
|
||||
TestData,
|
||||
getOrganizer,
|
||||
getBooker,
|
||||
getScenarioData,
|
||||
mockSuccessfulVideoMeetingCreation,
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
BookingLocations,
|
||||
getDate,
|
||||
Timezones,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
|
||||
import {
|
||||
expectWorkflowToBeTriggered,
|
||||
expectSMSWorkflowToBeTriggered,
|
||||
expectSMSWorkflowToBeNotTriggered,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
// Local test runs sometime gets too slow
|
||||
const timeout = process.env.CI ? 5000 : 20000;
|
||||
|
||||
describe("handleNewBooking", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
beforeEach(() => {
|
||||
resetTestSMS();
|
||||
});
|
||||
|
||||
describe("User Workflows", () => {
|
||||
test(
|
||||
"should send workflow email and sms when booking is created",
|
||||
async ({ emails, sms }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizerOtherEmail = "organizer2@example.com";
|
||||
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "organizer@google-calendar.com",
|
||||
primaryEmail: organizerOtherEmail,
|
||||
},
|
||||
});
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
workflows: [
|
||||
{
|
||||
userId: organizer.id,
|
||||
trigger: "NEW_EVENT",
|
||||
action: "EMAIL_HOST",
|
||||
template: "REMINDER",
|
||||
activeEventTypeId: 1,
|
||||
},
|
||||
{
|
||||
userId: organizer.id,
|
||||
trigger: "NEW_EVENT",
|
||||
action: "SMS_ATTENDEE",
|
||||
template: "REMINDER",
|
||||
activeEventTypeId: 1,
|
||||
},
|
||||
],
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
primaryEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
},
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
videoMeetingData: {
|
||||
id: "MOCK_ID",
|
||||
password: "MOCK_PASS",
|
||||
url: `http://mock-dailyvideo.example.com/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
smsReminderNumber: "000",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
await handleNewBooking(req);
|
||||
|
||||
expectSMSWorkflowToBeTriggered({
|
||||
sms,
|
||||
toNumber: "000",
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({
|
||||
emailsToReceive: [organizerDestinationCalendarEmailOnEventType],
|
||||
emails,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
test(
|
||||
"should not send workflow sms when booking is created if the organizer is locked for sms sending",
|
||||
async ({ sms }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizerOtherEmail = "organizer2@example.com";
|
||||
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "organizer@google-calendar.com",
|
||||
primaryEmail: organizerOtherEmail,
|
||||
},
|
||||
smsLockState: SMSLockState.LOCKED,
|
||||
});
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
workflows: [
|
||||
{
|
||||
userId: organizer.id,
|
||||
trigger: "NEW_EVENT",
|
||||
action: "SMS_ATTENDEE",
|
||||
template: "REMINDER",
|
||||
activeEventTypeId: 1,
|
||||
},
|
||||
],
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
primaryEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
},
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
smsReminderNumber: "000",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
await handleNewBooking(req);
|
||||
|
||||
expectSMSWorkflowToBeNotTriggered({
|
||||
sms,
|
||||
toNumber: "000",
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
describe("Team Workflows", () => {
|
||||
test(
|
||||
"should send workflow email and sms when booking is created",
|
||||
async ({ emails, sms }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "organizer@google-calendar.com",
|
||||
primaryEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
membership: {
|
||||
accepted: true,
|
||||
},
|
||||
team: {
|
||||
id: 1,
|
||||
name: "Team 1",
|
||||
slug: "team-1",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const otherTeamMembers = [
|
||||
{
|
||||
name: "Other Team Member 1",
|
||||
username: "other-team-member-1",
|
||||
defaultScheduleId: null,
|
||||
email: "other-team-member-1@example.com",
|
||||
timeZone: Timezones["+0:00"],
|
||||
id: 102,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
destinationCalendar: {
|
||||
integration: TestData.apps["google-calendar"].type,
|
||||
externalId: "other-team-member-1@google-calendar.com",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
workflows: [
|
||||
{
|
||||
teamId: 1,
|
||||
trigger: "NEW_EVENT",
|
||||
action: "EMAIL_HOST",
|
||||
template: "REMINDER",
|
||||
activeEventTypeId: 1,
|
||||
},
|
||||
{
|
||||
teamId: 1,
|
||||
trigger: "NEW_EVENT",
|
||||
action: "SMS_ATTENDEE",
|
||||
template: "REMINDER",
|
||||
activeEventTypeId: 1,
|
||||
},
|
||||
],
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 15,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
length: 15,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
},
|
||||
],
|
||||
teamId: 1,
|
||||
destinationCalendar: {
|
||||
integration: TestData.apps["google-calendar"].type,
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
usersApartFromOrganizer: otherTeamMembers,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
videoMeetingData: {
|
||||
id: "MOCK_ID",
|
||||
password: "MOCK_PASS",
|
||||
url: `http://mock-dailyvideo.example.com/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `${getDate({ dateIncrement: 1 }).dateString}T09:00:00.000Z`,
|
||||
end: `${getDate({ dateIncrement: 1 }).dateString}T09:15:00.000Z`,
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
smsReminderNumber: "000",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
await handleNewBooking(req);
|
||||
|
||||
expectSMSWorkflowToBeTriggered({
|
||||
sms,
|
||||
toNumber: "000",
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({
|
||||
// emailsToReceive: [organizer.email].concat(otherTeamMembers.map(member => member.email)),
|
||||
emailsToReceive: [organizer.email],
|
||||
emails,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
"should not send workflow sms when booking is created if the team is locked for sms sending",
|
||||
async ({ emails, sms }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "organizer@google-calendar.com",
|
||||
primaryEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
membership: {
|
||||
accepted: true,
|
||||
},
|
||||
team: {
|
||||
id: 1,
|
||||
name: "Team 1",
|
||||
slug: "team-1",
|
||||
smsLockState: SMSLockState.LOCKED,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const otherTeamMembers = [
|
||||
{
|
||||
name: "Other Team Member 1",
|
||||
username: "other-team-member-1",
|
||||
defaultScheduleId: null,
|
||||
email: "other-team-member-1@example.com",
|
||||
timeZone: Timezones["+0:00"],
|
||||
id: 102,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
destinationCalendar: {
|
||||
integration: TestData.apps["google-calendar"].type,
|
||||
externalId: "other-team-member-1@google-calendar.com",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
workflows: [
|
||||
{
|
||||
teamId: 1,
|
||||
trigger: "NEW_EVENT",
|
||||
action: "SMS_ATTENDEE",
|
||||
template: "REMINDER",
|
||||
activeEventTypeId: 1,
|
||||
},
|
||||
],
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 15,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
length: 15,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
},
|
||||
],
|
||||
teamId: 1,
|
||||
destinationCalendar: {
|
||||
integration: TestData.apps["google-calendar"].type,
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
usersApartFromOrganizer: otherTeamMembers,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
videoMeetingData: {
|
||||
id: "MOCK_ID",
|
||||
password: "MOCK_PASS",
|
||||
url: `http://mock-dailyvideo.example.com/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `${getDate({ dateIncrement: 1 }).dateString}T09:00:00.000Z`,
|
||||
end: `${getDate({ dateIncrement: 1 }).dateString}T09:15:00.000Z`,
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
smsReminderNumber: "000",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
await handleNewBooking(req);
|
||||
|
||||
expectSMSWorkflowToBeNotTriggered({
|
||||
sms,
|
||||
toNumber: "000",
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import TwilioClient from "twilio";
|
||||
|
||||
import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { setTestSMS } from "@calcom/lib/testSMS";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SMSLockState } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -31,7 +32,7 @@ function getDefaultSender(whatsapp = false) {
|
||||
if (whatsapp) {
|
||||
defaultSender = `whatsapp:+${process.env.TWILIO_WHATSAPP_PHONE_NUMBER}`;
|
||||
}
|
||||
return defaultSender;
|
||||
return defaultSender || "";
|
||||
}
|
||||
|
||||
function getSMSNumber(phone: string, whatsapp = false) {
|
||||
@@ -46,8 +47,6 @@ export const sendSMS = async (
|
||||
teamId?: number | null,
|
||||
whatsapp = false
|
||||
) => {
|
||||
assertTwilio(twilio);
|
||||
|
||||
const isSMSSendingLocked = await isLockedForSMSSending(userId, teamId);
|
||||
|
||||
if (isSMSSendingLocked) {
|
||||
@@ -55,6 +54,23 @@ export const sendSMS = async (
|
||||
return;
|
||||
}
|
||||
|
||||
const testMode = process.env.NEXT_PUBLIC_IS_E2E || process.env.INTEGRATION_TEST_MODE;
|
||||
|
||||
if (testMode) {
|
||||
setTestSMS({
|
||||
to: getSMSNumber(phoneNumber, whatsapp),
|
||||
from: whatsapp ? getDefaultSender(whatsapp) : sender ? sender : getDefaultSender(),
|
||||
message: body,
|
||||
});
|
||||
console.log(
|
||||
"Skipped sending SMS because process.env.NEXT_PUBLIC_IS_E2E or process.env.INTEGRATION_TEST_MODE is set. SMS are available in globalThis.testSMS"
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
assertTwilio(twilio);
|
||||
|
||||
if (!teamId && userId) {
|
||||
await checkSMSRateLimit({
|
||||
identifier: `sms:user:${userId}`,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var testSMS: {
|
||||
to: string;
|
||||
from: string;
|
||||
message: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const setTestSMS = (sms: (typeof globalThis.testSMS)[number]) => {
|
||||
globalThis.testSMS = globalThis.testSMS || [];
|
||||
globalThis.testSMS.push(sms);
|
||||
};
|
||||
|
||||
export const getTestSMS = () => {
|
||||
return globalThis.testSMS;
|
||||
};
|
||||
|
||||
export const resetTestSMS = () => {
|
||||
globalThis.testSMS = [];
|
||||
};
|
||||
Reference in New Issue
Block a user