Files
Joe Au-YeungGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
654a865007 refactor: Salesforce getContacts (#24235)
* lint fixes

* Create getContactOrLeadFromEmail method

* Use getContactOrLeadFromEmail in getContacts

* Move normal getContact query

* fix: correct return type in getContactOrLeadFromEmail and add search mock

- Fix getContactOrLeadFromEmail to return single ContactRecord instead of array
- Add conn.search() implementation to salesforceMock for integration tests
- Ensure id field is always string with proper fallbacks for AccountId and Id
- Fix Owner.Email to use undefined instead of null in mock

Co-Authored-By: [email protected] <[email protected]>

* test: add comprehensive tests for getContactOrLeadFromEmail method

- Add 6 new unit tests covering all key scenarios
- Test contact lookup, lead fallback, and preference logic
- Test both roundRobinSkipFallbackToLeadOwner and createEventOnLeadCheckForContact code paths
- Add search mock support to mockConnection in beforeEach
- All 34 tests passing, type-check clean

Co-Authored-By: [email protected] <[email protected]>

* Implement change to error from #24090

* Address feedback

* Type fix

* test: update SOQL query expectations to include Account.OwnerId

Co-Authored-By: [email protected] <[email protected]>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-03 14:53:00 -04:00

808 lines
23 KiB
TypeScript

import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
import { describe, expect, it, beforeEach, vi } from "vitest";
import type { z } from "zod";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { appDataSchema } from "../zod";
import SalesforceCRMService from "./CrmService";
import { SalesforceRecordEnum } from "./enums";
type AppOptions = z.infer<typeof appDataSchema>;
const contactQueryResponse = {
records: [
{
Id: "001",
Email: "[email protected]",
OwnerId: "owner001",
attributes: { type: "Contact" },
Owner: {
Email: "[email protected]",
},
},
],
};
const contactUnderAccountQueryResponse = {
records: [
{
Id: "001",
Email: "[email protected]",
OwnerId: "owner001",
AccountId: "acc001",
attributes: { type: "Contact" },
Account: {
attributes: { type: "Account" },
Owner: {
Email: "[email protected]",
},
},
},
],
};
const leadQueryResponse = {
records: [
{
Id: "001",
Email: "[email protected]",
OwnerId: "owner001",
attributes: { type: "Lead" },
Owner: {
Email: "[email protected]",
},
},
],
};
const _ownerQueryResponse = {
records: [
{
Id: "owner001",
Email: "[email protected]",
Name: "Test Owner",
},
],
};
const accountQueryResponse = {
records: [
{
Id: "acc001",
OwnerId: "owner001",
Email: "[email protected]",
attributes: { type: "Account" },
},
],
};
const mockSalesforceGraphQLClientQuery = vi.fn();
vi.mock("./graphql/SalesforceGraphQLClient", () => ({
SalesforceGraphQLClient: class {
GetAccountRecordsForRRSkip = mockSalesforceGraphQLClientQuery;
},
}));
describe("SalesforceCRMService", () => {
let service: SalesforceCRMService;
let mockConnection: {
query: ReturnType<typeof vi.fn>;
sobject: ReturnType<typeof vi.fn>;
search: ReturnType<typeof vi.fn>;
};
setupAndTeardown();
beforeEach(() => {
mockConnection = {
query: vi.fn(),
sobject: vi.fn(),
search: vi.fn(),
};
const mockCredential: CredentialPayload = {
id: 1,
type: "salesforce_crm",
key: {
access_token: "mock_token",
refresh_token: "mock_refresh",
instance_url: "https://mock.salesforce.com",
},
userId: 1,
appId: "salesforce",
teamId: null,
invalid: false,
user: {
email: "[email protected]",
},
delegationCredentialId: null,
};
service = new SalesforceCRMService(mockCredential, {}, true);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
service.conn = Promise.resolve(mockConnection);
});
const mockAppOptions = (appOptions: AppOptions) => {
const appOptionsSpy = vi.spyOn(service, "getAppOptions");
appOptionsSpy.mockReturnValue(appOptions);
};
describe("getContacts", () => {
it("should return empty array when no records found", async () => {
// Setup mock response
mockConnection.query.mockResolvedValueOnce({ records: [] });
const result = await service.getContacts({ emails: "[email protected]" });
expect(result).toEqual([]);
expect(mockConnection.query).toHaveBeenCalledWith(
"SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('[email protected]')"
);
});
it("should handle single email string input", async () => {
// Setup mock response with descriptive name
const contactQueryResponse = {
records: [
{
Id: "001",
Email: "[email protected]",
OwnerId: "owner001",
attributes: { type: "Contact" },
},
],
};
// Setup query spy
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(contactQueryResponse);
const result = await service.getContacts({ emails: "[email protected]" });
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
recordType: "Contact",
},
]);
// Verify the exact query made
expect(querySpy).toHaveBeenCalledWith(
"SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('[email protected]')"
);
});
it("should handle array of emails input", async () => {
const contactsQueryResponse = {
records: [
{
Id: "001",
Email: "[email protected]",
OwnerId: "owner001",
attributes: { type: "Contact" },
},
{
Id: "002",
Email: "[email protected]",
OwnerId: "owner002",
attributes: { type: "Contact" },
},
],
};
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(contactsQueryResponse);
const result = await service.getContacts({
emails: ["[email protected]", "[email protected]"],
});
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
recordType: "Contact",
},
{
id: "002",
email: "[email protected]",
recordType: "Contact",
},
]);
expect(querySpy).toHaveBeenCalledWith(
"SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('[email protected]','[email protected]')"
);
});
it("should return contacts when includeOwner is true but no owner is found", async () => {
mockAppOptions({
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.CONTACT,
createEventOn: SalesforceRecordEnum.CONTACT,
});
const contactQueryResponse = {
records: [
{
Id: "001",
Email: "[email protected]",
// OwnerId: "owner001",
attributes: { type: "Contact" },
Owner: {
Email: "[email protected]",
},
},
],
};
// Setup query spy with specific responses
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(contactQueryResponse); // Contact query
const result = await service.getContacts({
emails: "[email protected]",
includeOwner: true,
});
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
ownerId: undefined,
ownerEmail: "[email protected]",
recordType: "Contact",
},
]);
expect(querySpy).toHaveBeenNthCalledWith(
1,
"SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('[email protected]')"
);
});
describe("no additional params are passed", () => {
it("when createEventOn is contact", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.CONTACT,
});
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(contactQueryResponse);
const result = await service.getContacts({
emails: "[email protected]",
});
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
recordType: "Contact",
},
]);
expect(querySpy).toHaveBeenNthCalledWith(
1,
"SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('[email protected]')"
);
});
it("when createEventOn is lead", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.LEAD,
});
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(leadQueryResponse);
const result = await service.getContacts({
emails: "[email protected]",
});
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
recordType: "Lead",
},
]);
expect(querySpy).toHaveBeenNthCalledWith(
1,
"SELECT Id, Email, OwnerId, Owner.Email FROM Lead WHERE Email IN ('[email protected]')"
);
});
it("when createEventOn is contact under an account", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.ACCOUNT,
});
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(contactUnderAccountQueryResponse);
const result = await service.getContacts({
emails: "[email protected]",
});
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
recordType: "Contact",
},
]);
expect(querySpy).toHaveBeenNthCalledWith(
1,
"SELECT Id, Email, OwnerId, AccountId, Account.OwnerId, Account.Owner.Email, Account.Website FROM Contact WHERE Email = '[email protected]' AND AccountId != null"
);
});
});
describe("includeOwner param is passed", () => {
it("when createEventOn is contact", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.CONTACT,
});
// Setup query spy with specific responses
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(contactQueryResponse); // Contact query
const result = await service.getContacts({
emails: "[email protected]",
includeOwner: true,
});
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
ownerId: "owner001",
ownerEmail: "[email protected]",
recordType: "Contact",
},
]);
expect(querySpy).toHaveBeenNthCalledWith(
1,
"SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('[email protected]')"
);
});
it("when createEventOn is lead", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.LEAD,
});
// Setup query spy with specific responses
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(leadQueryResponse);
const result = await service.getContacts({
emails: "[email protected]",
includeOwner: true,
});
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
ownerId: "owner001",
ownerEmail: "[email protected]",
recordType: "Lead",
},
]);
expect(querySpy).toHaveBeenNthCalledWith(
1,
"SELECT Id, Email, OwnerId, Owner.Email FROM Lead WHERE Email IN ('[email protected]')"
);
});
it("when createEventOn is contact under an account", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.ACCOUNT,
});
// Setup query spy with specific responses
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(contactUnderAccountQueryResponse);
const result = await service.getContacts({
emails: "[email protected]",
includeOwner: true,
});
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
ownerId: "owner001",
ownerEmail: "[email protected]",
recordType: "Contact",
},
]);
expect(querySpy).toHaveBeenNthCalledWith(
1,
"SELECT Id, Email, OwnerId, AccountId, Account.OwnerId, Account.Owner.Email, Account.Website FROM Contact WHERE Email = '[email protected]' AND AccountId != null"
);
});
});
describe("forRoundRobinSkip param is passed", () => {
it("checking against contacts", async () => {
mockAppOptions({
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.CONTACT,
createEventOn: SalesforceRecordEnum.CONTACT,
});
// Setup query spy with specific responses
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(contactQueryResponse); // Contact query
const result = await service.getContacts({
emails: "[email protected]",
forRoundRobinSkip: true,
});
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
ownerId: "owner001",
ownerEmail: "[email protected]",
recordType: "Contact",
},
]);
expect(querySpy).toHaveBeenNthCalledWith(
1,
"SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('[email protected]')"
);
});
it("checking against leads", async () => {
mockAppOptions({
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.LEAD,
});
// Setup query spy with specific responses
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(leadQueryResponse);
const result = await service.getContacts({
emails: "[email protected]",
forRoundRobinSkip: true,
});
expect(result).toEqual([
{
id: "001",
email: "[email protected]",
ownerId: "owner001",
ownerEmail: "[email protected]",
recordType: "Lead",
},
]);
expect(querySpy).toHaveBeenNthCalledWith(
1,
"SELECT Id, Email, OwnerId, Owner.Email FROM Lead WHERE Email IN ('[email protected]')"
);
});
it("checking against contacts under an account", async () => {
mockAppOptions({
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.ACCOUNT,
});
mockSalesforceGraphQLClientQuery.mockResolvedValueOnce([
{
id: "acc001",
email: "[email protected]",
ownerId: "owner001",
ownerEmail: "[email protected]",
recordType: SalesforceRecordEnum.ACCOUNT,
},
]);
const result = await service.getContacts({
emails: "[email protected]",
forRoundRobinSkip: true,
});
expect(result).toEqual([
{
id: "acc001",
email: "[email protected]",
ownerId: "owner001",
ownerEmail: "[email protected]",
recordType: "Account",
},
]);
});
});
});
describe("getContactOrLeadFromEmail via getContacts", () => {
describe("with roundRobinSkipFallbackToLeadOwner enabled", () => {
it("should return contact when contact is found", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.CONTACT,
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.CONTACT,
roundRobinSkipFallbackToLeadOwner: true,
});
const searchSpy = vi.spyOn(mockConnection, "search");
searchSpy.mockResolvedValueOnce({
searchRecords: [
{
Id: "contact001",
Email: "[email protected]",
OwnerId: "owner001",
attributes: { type: "Contact" },
Owner: { Email: "[email protected]" },
},
],
});
const result = await service.getContacts({
emails: "[email protected]",
forRoundRobinSkip: true,
});
expect(result).toEqual([
{
id: "contact001",
email: "[email protected]",
ownerId: "owner001",
ownerEmail: "[email protected]",
recordType: "Contact",
},
]);
expect(searchSpy).toHaveBeenCalledWith(
"FIND {[email protected]} IN EMAIL FIELDS RETURNING Lead(Id, Email, OwnerId, Owner.Email), Contact(Id, Email, OwnerId, Owner.Email)"
);
});
it("should return lead when no contact exists but lead is found", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.CONTACT,
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.CONTACT,
roundRobinSkipFallbackToLeadOwner: true,
});
const searchSpy = vi.spyOn(mockConnection, "search");
searchSpy.mockResolvedValueOnce({
searchRecords: [
{
Id: "lead001",
Email: "[email protected]",
OwnerId: "owner001",
attributes: { type: "Lead" },
Owner: { Email: "[email protected]" },
},
],
});
const result = await service.getContacts({
emails: "[email protected]",
forRoundRobinSkip: true,
});
expect(result).toEqual([
{
id: "lead001",
email: "[email protected]",
ownerId: "owner001",
ownerEmail: "[email protected]",
recordType: "Lead",
},
]);
expect(searchSpy).toHaveBeenCalled();
});
it("should prefer contact over lead when both exist", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.CONTACT,
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.CONTACT,
roundRobinSkipFallbackToLeadOwner: true,
});
const searchSpy = vi.spyOn(mockConnection, "search");
searchSpy.mockResolvedValueOnce({
searchRecords: [
{
Id: "lead001",
Email: "[email protected]",
OwnerId: "owner002",
attributes: { type: "Lead" },
Owner: { Email: "[email protected]" },
},
{
Id: "contact001",
Email: "[email protected]",
OwnerId: "owner001",
attributes: { type: "Contact" },
Owner: { Email: "[email protected]" },
},
],
});
const result = await service.getContacts({
emails: "[email protected]",
forRoundRobinSkip: true,
});
expect(result).toEqual([
{
id: "contact001",
email: "[email protected]",
ownerId: "owner001",
ownerEmail: "[email protected]",
recordType: "Contact",
},
]);
expect(searchSpy).toHaveBeenCalled();
});
it("should return empty array when no records found", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.CONTACT,
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.CONTACT,
roundRobinSkipFallbackToLeadOwner: true,
});
const searchSpy = vi.spyOn(mockConnection, "search");
searchSpy.mockResolvedValueOnce({
searchRecords: [],
});
const result = await service.getContacts({
emails: "[email protected]",
forRoundRobinSkip: true,
});
expect(result).toEqual([]);
expect(searchSpy).toHaveBeenCalled();
});
});
describe("with createEventOnLeadCheckForContact enabled", () => {
it("should find and return contact when it exists", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.LEAD,
createEventOnLeadCheckForContact: true,
});
const searchSpy = vi.spyOn(mockConnection, "search");
searchSpy.mockResolvedValueOnce({
searchRecords: [
{
Id: "contact001",
Email: "[email protected]",
OwnerId: "owner001",
attributes: { type: "Contact" },
Owner: { Email: "[email protected]" },
},
],
});
const result = await service.getContacts({
emails: "[email protected]",
});
expect(result).toEqual([
{
id: "contact001",
email: "[email protected]",
recordType: "Contact",
},
]);
expect(searchSpy).toHaveBeenCalled();
});
it("should fallback to lead when contact not found", async () => {
mockAppOptions({
createEventOn: SalesforceRecordEnum.LEAD,
createEventOnLeadCheckForContact: true,
});
const searchSpy = vi.spyOn(mockConnection, "search");
searchSpy.mockResolvedValueOnce({
searchRecords: [
{
Id: "lead001",
Email: "[email protected]",
OwnerId: "owner001",
attributes: { type: "Lead" },
Owner: { Email: "[email protected]" },
},
],
});
const result = await service.getContacts({
emails: "[email protected]",
});
expect(result).toEqual([
{
id: "lead001",
email: "[email protected]",
recordType: "Lead",
},
]);
expect(searchSpy).toHaveBeenCalled();
});
});
});
describe("createContacts", () => {
describe("createEventOn lead", () => {
describe("createNewContactUnderAccount enabled", () => {
it("when attendee has an account", async () => {
mockAppOptions({
createNewContactUnderAccount: true,
createEventOn: SalesforceRecordEnum.LEAD,
});
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(accountQueryResponse);
querySpy.mockResolvedValueOnce({ records: [] });
mockConnection.sobject.mockReturnValue({
create: vi.fn().mockResolvedValue({
success: true,
id: "newContactId",
name: "New Contact",
email: "[email protected]",
}),
});
const result = await service.createContacts([{ name: "New Contact", email: "[email protected]" }]);
expect(result).toEqual([{ id: "newContactId", email: "[email protected]" }]);
});
it("attendee has no account", async () => {
mockAppOptions({
createNewContactUnderAccount: true,
createEventOn: SalesforceRecordEnum.LEAD,
});
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce({ records: [] });
querySpy.mockResolvedValueOnce({ records: [] });
mockConnection.sobject.mockReturnValue({
create: vi.fn().mockResolvedValue({
success: true,
id: "newLeadId",
name: "New Lead",
email: "[email protected]",
}),
});
const result = await service.createContacts([{ name: "New Lead", email: "[email protected]" }]);
expect(result).toEqual([{ id: "newLeadId", email: "[email protected]" }]);
});
});
});
});
describe("getAllPossibleAccountWebsiteFromEmailDomain", () => {
it("should return all possible account websites from email domain", () => {
const result = service.getAllPossibleAccountWebsiteFromEmailDomain("example.com");
expect(result).toEqual(
"'example.com', 'www.example.com', 'http://www.example.com', 'http://example.com', 'https://www.example.com', 'https://example.com'"
);
});
});
});