* Revert "Revert "feat: Phone based bookings for everyone (#21320)" (#22018)"
This reverts commit 80e2118e68.
* chore: don't use repository
* chore: create getTeamWithOrganizationSettings function
---------
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
256 lines
7.6 KiB
TypeScript
256 lines
7.6 KiB
TypeScript
import type { TFunction } from "i18next";
|
|
import { describe, expect, test, vi, beforeEach } from "vitest";
|
|
|
|
import { sendSmsOrFallbackEmail } from "@calcom/features/ee/workflows/lib/reminders/messageDispatcher";
|
|
import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError";
|
|
import prisma from "@calcom/prisma";
|
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
|
|
|
import SMSManager from "../sms-manager";
|
|
|
|
vi.mock("@calcom/lib/checkRateLimitAndThrowError");
|
|
vi.mock("@calcom/features/ee/workflows/lib/reminders/messageDispatcher");
|
|
vi.mock("@calcom/prisma", () => ({
|
|
default: {
|
|
team: {
|
|
findUnique: vi.fn(),
|
|
},
|
|
},
|
|
}));
|
|
|
|
interface TestAttendee extends Person {
|
|
name: string;
|
|
email: string;
|
|
phoneNumber?: string;
|
|
timeZone: string;
|
|
language: {
|
|
translate: TFunction;
|
|
locale: string;
|
|
};
|
|
}
|
|
|
|
interface TestCalEvent extends CalendarEvent {
|
|
uid: string;
|
|
type: string;
|
|
title: string;
|
|
startTime: string;
|
|
endTime: string;
|
|
attendees: TestAttendee[];
|
|
organizer: {
|
|
id: number;
|
|
name: string;
|
|
email: string;
|
|
timeZone: string;
|
|
language: {
|
|
translate: TFunction;
|
|
locale: string;
|
|
};
|
|
};
|
|
team?: {
|
|
id: number;
|
|
name: string;
|
|
members: unknown[];
|
|
};
|
|
}
|
|
|
|
// Create a concrete implementation of SMSManager for testing
|
|
class TestSMSManager extends SMSManager {
|
|
getMessage(attendee: TestAttendee): string {
|
|
return `Test message for ${attendee.name}`;
|
|
}
|
|
}
|
|
|
|
describe("SMSManager", () => {
|
|
const mockTranslate = ((key: string) => key) as TFunction;
|
|
const mockCalEvent: TestCalEvent = {
|
|
uid: "test-booking-uid",
|
|
type: "test",
|
|
title: "Test Event",
|
|
startTime: "2024-03-20T10:00:00Z",
|
|
endTime: "2024-03-20T11:00:00Z",
|
|
attendees: [
|
|
{
|
|
name: "John Doe",
|
|
email: "john@sms.cal.com",
|
|
phoneNumber: "+1234567890",
|
|
timeZone: "America/New_York",
|
|
language: { translate: mockTranslate, locale: "en" },
|
|
},
|
|
{
|
|
name: "Jane Smith",
|
|
email: "jane@example.com",
|
|
phoneNumber: "+1987654321",
|
|
timeZone: "America/New_York",
|
|
language: { translate: mockTranslate, locale: "en" },
|
|
},
|
|
],
|
|
organizer: {
|
|
id: 1,
|
|
name: "Organizer",
|
|
email: "organizer@example.com",
|
|
timeZone: "America/New_York",
|
|
language: { translate: mockTranslate, locale: "en" },
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("sendSMSToAttendee", () => {
|
|
test("should not send SMS if phone number is missing", async () => {
|
|
const smsManager = new TestSMSManager(mockCalEvent);
|
|
const attendeeWithoutPhone: TestAttendee = {
|
|
...mockCalEvent.attendees[0],
|
|
phoneNumber: undefined,
|
|
timeZone: "America/New_York",
|
|
language: { translate: mockTranslate, locale: "en" },
|
|
};
|
|
|
|
await smsManager.sendSMSToAttendee(attendeeWithoutPhone);
|
|
|
|
expect(sendSmsOrFallbackEmail).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should not send SMS if email is not @sms.cal.com", async () => {
|
|
const smsManager = new TestSMSManager(mockCalEvent);
|
|
const attendeeWithRegularEmail: TestAttendee = {
|
|
...mockCalEvent.attendees[0],
|
|
email: "john@example.com",
|
|
phoneNumber: "+1234567890",
|
|
timeZone: "America/New_York",
|
|
language: { translate: mockTranslate, locale: "en" },
|
|
};
|
|
|
|
await smsManager.sendSMSToAttendee(attendeeWithRegularEmail);
|
|
|
|
expect(sendSmsOrFallbackEmail).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should send SMS only when both phone number and @sms.cal.com email are present", async () => {
|
|
const smsManager = new TestSMSManager(mockCalEvent);
|
|
const mockSmsResponse = { success: true };
|
|
|
|
(sendSmsOrFallbackEmail as jest.Mock).mockResolvedValue(mockSmsResponse);
|
|
(checkSMSRateLimit as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
const result = await smsManager.sendSMSToAttendee(mockCalEvent.attendees[0], "test-booking-uid");
|
|
|
|
expect(checkSMSRateLimit).toHaveBeenCalledWith({
|
|
identifier: "handleSendingSMS:user:1",
|
|
rateLimitingType: "sms",
|
|
});
|
|
|
|
expect(sendSmsOrFallbackEmail).toHaveBeenCalledWith({
|
|
twilioData: {
|
|
phoneNumber: mockCalEvent.attendees[0].phoneNumber,
|
|
body: expect.stringContaining(mockCalEvent.attendees[0].name),
|
|
sender: expect.any(String),
|
|
teamId: undefined,
|
|
userId: 1,
|
|
bookingUid: "test-booking-uid",
|
|
},
|
|
});
|
|
|
|
expect(result).toEqual(mockSmsResponse);
|
|
});
|
|
|
|
test("should not send SMS if SMS notifications are disabled for team", async () => {
|
|
const mockTeamId = 123;
|
|
const mockTeam = {
|
|
id: mockTeamId,
|
|
name: "Test Team",
|
|
members: [],
|
|
};
|
|
const smsManager = new TestSMSManager({
|
|
...mockCalEvent,
|
|
team: mockTeam,
|
|
});
|
|
|
|
// Mock team settings to disable SMS
|
|
(prisma.team.findUnique as jest.Mock).mockResolvedValue({
|
|
parent: {
|
|
isOrganization: true,
|
|
organizationSettings: {
|
|
disablePhoneOnlySMSNotifications: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
await smsManager.sendSMSToAttendee(mockCalEvent.attendees[0]);
|
|
|
|
expect(sendSmsOrFallbackEmail).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should handle SMS sending errors", async () => {
|
|
const smsManager = new TestSMSManager(mockCalEvent);
|
|
const mockError = new Error("SMS sending failed");
|
|
|
|
(sendSmsOrFallbackEmail as jest.Mock).mockRejectedValue(mockError);
|
|
(checkSMSRateLimit as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
await expect(smsManager.sendSMSToAttendee(mockCalEvent.attendees[0])).rejects.toThrow(mockError);
|
|
});
|
|
});
|
|
|
|
describe("sendSMSToAttendees", () => {
|
|
test("should send SMS only to attendees with phone number and @sms.cal.com email", async () => {
|
|
const smsManager = new TestSMSManager(mockCalEvent);
|
|
const mockSmsResponse = { success: true };
|
|
|
|
(sendSmsOrFallbackEmail as jest.Mock).mockResolvedValue(mockSmsResponse);
|
|
(checkSMSRateLimit as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
await smsManager.sendSMSToAttendees();
|
|
|
|
// Only one attendee has both phone number and @sms.cal.com email
|
|
expect(sendSmsOrFallbackEmail).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("should not send SMS if notifications are disabled", async () => {
|
|
const mockTeamId = 123;
|
|
const mockTeam = {
|
|
id: mockTeamId,
|
|
name: "Test Team",
|
|
members: [],
|
|
};
|
|
const smsManager = new TestSMSManager({
|
|
...mockCalEvent,
|
|
team: mockTeam,
|
|
});
|
|
|
|
(prisma.team.findUnique as jest.Mock).mockResolvedValue({
|
|
parent: {
|
|
isOrganization: true,
|
|
organizationSettings: {
|
|
disablePhoneOnlySMSNotifications: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
await smsManager.sendSMSToAttendees();
|
|
|
|
expect(sendSmsOrFallbackEmail).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("getFormattedTime and getFormattedDate", () => {
|
|
test("should format time correctly", () => {
|
|
const smsManager = new TestSMSManager(mockCalEvent);
|
|
const formattedTime = smsManager.getFormattedTime("America/New_York", "en", "2024-03-20T10:00:00Z");
|
|
|
|
expect(formattedTime).toContain("2024");
|
|
expect(formattedTime).toContain("6:00am");
|
|
});
|
|
|
|
test("should format date range correctly", () => {
|
|
const smsManager = new TestSMSManager(mockCalEvent);
|
|
const formattedDate = smsManager.getFormattedDate("America/New_York", "en");
|
|
|
|
expect(formattedDate).toContain("2024");
|
|
expect(formattedDate).toContain("6:00am");
|
|
expect(formattedDate).toContain("7:00am");
|
|
});
|
|
});
|
|
});
|