test: Salesforce CRM - Add user facing tests and more logs (#19531)

* Added user facing tests

* fix tests

* Fix ts error
This commit is contained in:
Hariom Balhara
2025-02-26 22:21:36 +05:30
committed by GitHub
parent 3850b519fd
commit 381e22ae7e
4 changed files with 655 additions and 12 deletions
+15 -11
View File
@@ -391,7 +391,7 @@ export default class SalesforceCRMService implements CRM {
if (recordToSearch === SalesforceRecordEnum.ACCOUNT) {
// For an account let's assume that the first email is the one we should be querying against
const attendeeEmail = emailArray[0];
log.info("Searching account for email", safeStringify({ attendeeEmail }));
log.info("[recordToSearch=ACCOUNT] Searching contact for email", safeStringify({ attendeeEmail }));
soql = `SELECT Id, Email, OwnerId, AccountId, Account.Owner.Email, Account.Website FROM ${SalesforceRecordEnum.CONTACT} WHERE Email = '${attendeeEmail}' AND AccountId != null`;
} else {
// Handle Contact/Lead record types
@@ -454,25 +454,26 @@ export default class SalesforceCRMService implements CRM {
!(await this.shouldSkipAttendeeIfFreeEmailDomain(emailArray[0]));
const includeAccountRecordType = forRoundRobinSkip && recordToSearch === SalesforceRecordEnum.ACCOUNT;
return records.map((record) => {
// Handle if Account is nested
const ownerEmail =
recordToSearch === SalesforceRecordEnum.ACCOUNT &&
record?.attributes?.type !== SalesforceRecordEnum.ACCOUNT
? record?.Account?.Owner?.Email
: record?.Owner?.Email;
return {
id: includeAccountRecordType ? record?.AccountId : record?.Id || "",
email: record?.Email || "",
recordType: includeAccountRecordType ? SalesforceRecordEnum.ACCOUNT : record?.attributes?.type,
...(includeOwnerData && {
ownerId: record?.OwnerId,
// Handle if Account is nested
ownerEmail:
recordToSearch === SalesforceRecordEnum.ACCOUNT &&
record?.attributes?.type !== SalesforceRecordEnum.ACCOUNT
? record?.Account?.Owner?.Email
: record?.Owner?.Email,
ownerEmail: ownerEmail,
}),
};
});
} catch (error) {
log.error("Error in getContacts", safeStringify({ error }));
log.error("Error in getContacts", safeStringify(error));
return [];
}
}
@@ -482,7 +483,7 @@ export default class SalesforceCRMService implements CRM {
organizerEmail?: string,
calEventResponses?: CalEventResponses | null
) {
const log = logger.getSubLogger({ prefix: [`[getContacts]`] });
const log = logger.getSubLogger({ prefix: [`[createContacts]`] });
const conn = await this.conn;
const appOptions = this.getAppOptions();
const createEventOn = appOptions.createEventOn ?? SalesforceRecordEnum.CONTACT;
@@ -901,13 +902,15 @@ export default class SalesforceCRMService implements CRM {
private async getAccountBasedOnEmailDomainOfContacts(email: string) {
const conn = await this.conn;
const emailDomain = email.split("@")[1];
const log = logger.getSubLogger({ prefix: [`[getAccountBasedOnEmailDomainOfContacts]:${email}`] });
log.info("Querying first account matching email domain", safeStringify({ emailDomain }));
// First check if an account has the same website as the email domain of the attendee
const accountQuery = await conn.query(
`SELECT Id, OwnerId, Owner.Email FROM Account WHERE Website LIKE '%${emailDomain}%' LIMIT 1`
);
if (accountQuery.records.length > 0) {
log.info("Found account matching email domain", safeStringify({ emailDomain }));
return {
...(accountQuery.records[0] as { Id?: string; OwnerId?: string; Owner?: { Email?: string } }),
Email: undefined,
@@ -928,6 +931,7 @@ export default class SalesforceCRMService implements CRM {
const dominantAccountId = this.getDominantAccountId(contacts);
const contactUnderAccount = contacts.find((contact) => contact.AccountId === dominantAccountId);
log.info("Using dominant account's owner", safeStringify({ dominantAccountId }));
return {
Id: dominantAccountId,
@@ -0,0 +1,340 @@
/**
* Purpose of these tests is to ensure that we test from CrmService and salesforce interactions perspective purely
* It allows us to document the behaviour more from User perspective and also verify it in parallel
*
* It will also give us confidence to refactor as it doesn't care about internal functions.
*
* CrmService.test.ts could still focus on testing detailed edge cases as needed.
*/
import "../../../../../tests/libs/__mocks__/prisma";
import jsforce from "@jsforce/jsforce-node";
import { describe, it, expect, vi, beforeEach } from "vitest";
import SalesforceCRMService from "../CrmService";
import { SalesforceRecordEnum } from "../enums";
import { createSalesforceMock } from "./salesforceMock";
vi.mock("@calcom/lib/constants", () => {
return {
WEBAPP_URL: "https://app.cal.com",
APP_CREDENTIAL_SHARING_ENABLED: false,
IS_PRODUCTION: false,
};
});
// Mock dependencies
vi.mock("@jsforce/jsforce-node", () => {
return {
default: {
Connection: vi.fn().mockImplementation(() => ({})),
},
};
});
vi.mock("@calcom/lib/freeEmailDomainCheck/checkIfFreeEmailDomain", () => ({
checkIfFreeEmailDomain: vi.fn().mockResolvedValue(false),
}));
vi.mock("../getSalesforceAppKeys", () => ({
getSalesforceAppKeys: vi.fn().mockResolvedValue({
consumer_key: "test_consumer_key",
consumer_secret: "test_consumer_secret",
}),
}));
// Helper to create mock credential
const createMockCredential = () => {
return {
id: 1,
key: {
access_token: "test_access_token",
refresh_token: "test_refresh_token",
instance_url: "https://test.salesforce.com",
},
type: "salesforce_other_calendar",
user: {
email: "test@example.com",
},
userId: 1,
teamId: 1,
appId: "test_app_id",
invalid: false,
};
};
const salesforceSettingScenario = {
createOnLeadAndSearchOnAccount: () => {
return {
appOptions: {
createEventOn: SalesforceRecordEnum.LEAD,
createEventOnLeadCheckForContact: true,
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.ACCOUNT,
},
credential: createMockCredential(),
};
},
};
describe("SalesforceCRMService", () => {
let salesforceMock: ReturnType<typeof createSalesforceMock>;
beforeEach(() => {
salesforceMock = createSalesforceMock();
fetchMock.mockReturnValueOnce(
Promise.resolve(
new Response(
JSON.stringify({
id: "abc",
issued_at: "123",
instance_url: "https://test.salesforce.com",
signature: "123",
access_token: "123",
scope: "123",
token_type: "123",
}),
{
status: 200,
}
)
)
);
// Override jsforce mock with our custom mock
// @ts-expect-error - Not full implementation of jsforce.Connection
vi.mocked(jsforce.Connection).mockImplementation(() => ({
...salesforceMock.mockConnection,
version: "1.0",
loginUrl: "https://test.salesforce.com",
instanceUrl: "https://test.salesforce.com",
accessToken: "123",
}));
});
describe("createOnLeadAndSearchOnAccount", () => {
const { appOptions, credential } = salesforceSettingScenario.createOnLeadAndSearchOnAccount();
describe("getContacts: forRoundRobinSkip=true", () => {
const forRoundRobinSkip = true;
it("(Lookup-1) should return contact's account owner's email as ownerEmail if the contact is found directly by email", async () => {
const crmService = new SalesforceCRMService(credential, appOptions);
const contactAccountOwnerEmail = "contact-account-owner@acme.com";
const contactOwnerEmail = "contact-owner@acme.com";
const lookingForEmail = "test@example.com";
const account = salesforceMock.addAccount({
Id: "test-account-id",
Owner: {
Email: contactAccountOwnerEmail,
},
Website: "https://anything",
});
salesforceMock.addContact({
Email: lookingForEmail,
FirstName: "Test",
LastName: "User",
AccountId: account.Id,
Owner: {
Email: contactOwnerEmail,
},
});
const contacts = await crmService.getContacts({
emails: lookingForEmail,
forRoundRobinSkip,
});
expect(contacts).toHaveLength(1);
expect(contacts[0].ownerEmail).toBe(contactAccountOwnerEmail);
expect(contacts[0].email).toBe(lookingForEmail);
expect(contacts[0].recordType).toBe(SalesforceRecordEnum.ACCOUNT);
expect(contacts[0].id).toBe(account.Id);
});
it("(Lookup-2) should fallback to account(and use the account owner email as ownerEmail) matched by emailDomain if the contact is not found(i.e Lookup-1 fails)", async () => {
const crmService = new SalesforceCRMService(credential, appOptions);
const contactAccountOwnerEmail = "contact-account-owner@example.com";
const emailDomain = "example.com";
const lookingForEmail = `test@${emailDomain}`;
const account = salesforceMock.addAccount({
Id: "test-account-id",
Owner: {
Email: contactAccountOwnerEmail,
},
Website: `https://${emailDomain}`,
});
const contacts = await crmService.getContacts({
emails: lookingForEmail,
forRoundRobinSkip,
});
expect(contacts).toHaveLength(1);
expect(contacts[0].ownerEmail).toBe(contactAccountOwnerEmail);
// Account doesn't have email
expect(contacts[0].email).toBe("");
expect(contacts[0].recordType).toBe(SalesforceRecordEnum.ACCOUNT);
// FIXME: Seems like a bug as this should have been accountId but it is undefined
// expect(contacts[0].id).toBe(account.Id);
});
it("(Lookup-3) should fallback to account having most number of contacts matched by emailDomain when Lookup-1 and Lookup-2 fails", async () => {
const crmService = new SalesforceCRMService(credential, appOptions);
const account1OwnerEmail = "contact-account-owner1@example.com";
const account2OwnerEmail = "contact-account-owner2@example.com";
const emailDomain = "example.com";
const lookingForEmail = `test@${emailDomain}`;
const account1 = salesforceMock.addAccount({
Id: "test-account-id1",
Owner: {
Email: account1OwnerEmail,
},
Website: `https://anything`,
});
const dominatingAccount = salesforceMock.addAccount({
Id: "test-account-id2",
Owner: {
Email: account2OwnerEmail,
},
Website: `https://anything`,
});
salesforceMock.addContact({
Email: `some-other-contact_acc2_1@${emailDomain}`,
FirstName: "Test",
LastName: "User",
AccountId: dominatingAccount.Id,
});
salesforceMock.addContact({
Email: `some-other-contact_acc2_2@${emailDomain}`,
FirstName: "Test",
LastName: "User",
AccountId: dominatingAccount.Id,
});
salesforceMock.addContact({
Email: `some-other-contact_acc1_1@${emailDomain}`,
FirstName: "Test",
LastName: "User",
AccountId: account1.Id,
});
const contacts = await crmService.getContacts({
emails: lookingForEmail,
forRoundRobinSkip,
});
expect(contacts).toHaveLength(1);
expect(contacts[0].ownerEmail).toBe(account2OwnerEmail);
// Account doesn't have email
expect(contacts[0].email).toBe("");
expect(contacts[0].recordType).toBe(SalesforceRecordEnum.ACCOUNT);
// FIXME: Seems like a bug as this should have been accountId but it is undefined
// expect(contacts[0].id).toBe(dominatingAccount.Id);
});
});
describe("getContacts: forRoundRobinSkip=false i.e. how it is called when creating event", () => {
const forRoundRobinSkip = false;
it("should return lead object if the lead is found directly by email and contact is not found by that email", async () => {
const crmService = new SalesforceCRMService(credential, appOptions);
const contactAccountOwnerEmail = "contact-account-owner@acme.com";
const contactOwnerEmail = "contact-owner@acme.com";
const leadOwnerEmail = "lead-owner@acme.com";
const lookingForEmail = "test1@example.com";
const contactEmail = "test2@example.com";
const account = salesforceMock.addAccount({
Id: "test-account-id",
Owner: {
Email: contactAccountOwnerEmail,
},
Website: "https://anything",
});
const contact = salesforceMock.addContact({
// Contact doesn't have the matching email
Email: contactEmail,
FirstName: "Test",
LastName: "User",
AccountId: account.Id,
Owner: {
Email: contactOwnerEmail,
},
});
const lead = salesforceMock.addLead({
// Lead has the matching email
Email: lookingForEmail,
FirstName: "Test",
LastName: "User",
Owner: {
Email: leadOwnerEmail,
},
});
const contactsOrLeadsOrAccounts = await crmService.getContacts({
emails: lookingForEmail,
forRoundRobinSkip,
});
expect(contactsOrLeadsOrAccounts).toHaveLength(1);
// Because we intentionally don't include owner data in this case
expect(contactsOrLeadsOrAccounts[0].ownerEmail).toBe(undefined);
expect(contactsOrLeadsOrAccounts[0].email).toBe(lookingForEmail);
expect(contactsOrLeadsOrAccounts[0].recordType).toBe(SalesforceRecordEnum.LEAD);
expect(contactsOrLeadsOrAccounts[0].id).toBe(lead.Id);
});
it("should return contact object(if there is one by that email) even if lead is found by that email", async () => {
const crmService = new SalesforceCRMService(credential, appOptions);
const contactAccountOwnerEmail = "contact-account-owner@acme.com";
const contactOwnerEmail = "contact-owner@acme.com";
const leadOwnerEmail = "lead-owner@acme.com";
const lookingForEmail = "test1@example.com";
const contactEmail = "test2@example.com";
const account = salesforceMock.addAccount({
Id: "test-account-id",
Owner: {
Email: contactAccountOwnerEmail,
},
Website: "https://anything",
});
const contact = salesforceMock.addContact({
// Contact doesn't have the matching email
Email: lookingForEmail,
FirstName: "Test",
LastName: "User",
AccountId: account.Id,
Owner: {
Email: contactOwnerEmail,
},
});
const lead = salesforceMock.addLead({
// Lead has the matching email
Email: lookingForEmail,
FirstName: "Test",
LastName: "User",
Owner: {
Email: leadOwnerEmail,
},
});
const contactsOrLeadsOrAccounts = await crmService.getContacts({
emails: lookingForEmail,
forRoundRobinSkip,
});
expect(contactsOrLeadsOrAccounts).toHaveLength(1);
// Because we intentionally don't include owner data in this case
expect(contactsOrLeadsOrAccounts[0].ownerEmail).toBe(undefined);
expect(contactsOrLeadsOrAccounts[0].id).toBe(contact.Id);
expect(contactsOrLeadsOrAccounts[0].email).toBe(lookingForEmail);
expect(contactsOrLeadsOrAccounts[0].recordType).toBe(SalesforceRecordEnum.CONTACT);
});
});
});
});
@@ -0,0 +1,294 @@
import { vi } from "vitest";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
type Contact = {
Id?: string;
FirstName?: string;
LastName?: string;
Email?: string;
AccountId?: string;
Account?: Account;
Owner?: {
Email?: string;
};
OwnerId?: string;
};
type Lead = {
Id?: string;
FirstName?: string;
LastName?: string;
Email?: string;
AccountId?: string;
Account?: Account;
Owner?: {
Email?: string;
};
OwnerId?: string;
};
type Account = { Id?: string; OwnerId?: string; Owner?: { Email?: string }; Website?: string };
type User = { Id?: string; IsActive?: boolean; Email?: string };
// Salesforce mock factory
export const createSalesforceMock = () => {
// Store for mock records
const records = {
contacts: [] as Contact[],
leads: [] as Lead[],
accounts: [] as Account[],
users: [] as User[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
events: [] as any[],
};
// Query parser and responder
const handleQuery = (query: string) => {
// Simple SOQL parser
const fromMatch = query.match(/FROM\s+(\w+)/i);
const whereMatch = query.match(/WHERE\s+(.+?)(?:\s+LIMIT|\s+ORDER|\s*$)/i);
const limitMatch = query.match(/LIMIT\s+(\d+)/i);
if (!fromMatch) return { records: [] };
const objectType = fromMatch[1];
const whereClause = whereMatch ? whereMatch[1] : null;
const limit = limitMatch ? parseInt(limitMatch[1]) : Infinity;
let result = [];
switch (objectType) {
case "Contact":
result = records.contacts;
break;
case "Lead":
result = records.leads;
break;
case "Account":
result = records.accounts;
break;
case "User":
result = records.users;
break;
case "Event":
result = records.events;
break;
default:
return { records: [] };
}
// Apply where clause filtering (basic implementation)
if (whereClause) {
if (whereClause.includes("Email =")) {
const emailMatch = whereClause.match(/Email\s*=\s*'([^']+)'/i);
if (emailMatch) {
const email = emailMatch[1];
result = result.filter((r) => r.Email === email);
}
}
if (whereClause.includes("Email IN")) {
const emailsMatch = whereClause.match(/Email IN \('([^']+)(?:','([^']+))*'\)/i);
if (emailsMatch) {
const emails = whereClause
.match(/Email IN \((.+)\)/i)?.[1]
?.split("','")
?.map((e) => e.replace(/'/g, ""));
result = result.filter((r) => emails?.includes(r.Email));
}
}
if (whereClause.includes("Email LIKE")) {
const emailLikeMatch = whereClause.match(/Email LIKE '%@([^']+)'/i);
if (emailLikeMatch) {
const domain = emailLikeMatch[1];
result = result.filter((r) => r.Email && r.Email.endsWith(`@${domain}`));
}
}
if (whereClause.includes("Id =")) {
const idMatch = whereClause.match(/Id\s*=\s*'([^']+)'/i);
if (idMatch) {
const id = idMatch[1];
result = result.filter((r) => r.Id === id);
}
}
if (whereClause.includes("AccountId =")) {
const accountIdMatch = whereClause.match(/AccountId\s*=\s*'([^']+)'/i);
if (accountIdMatch) {
const accountId = accountIdMatch[1];
result = result.filter((r) => r.AccountId === accountId);
}
}
if (whereClause.includes("AccountId != null")) {
result = result.filter((r) => r.AccountId);
}
if (whereClause.includes("Website LIKE")) {
const websiteLikeMatch = whereClause.match(/Website LIKE '%([^']+)%'/i);
if (websiteLikeMatch) {
const websitePart = websiteLikeMatch[1];
result = result.filter((r) => r.Website && r.Website.includes(websitePart));
}
}
if (whereClause.includes("IsActive = true")) {
result = result.filter((r) => r.IsActive === true);
}
}
// Apply limit
result = result.slice(0, limit);
// Handle special cases with nested property selection
if (query.includes("Account.Owner.Email")) {
result = result.map((r) => {
if (r.AccountId) {
const account = records.accounts.find((a) => a.Id === r.AccountId);
if (account && account.OwnerId) {
const owner = records.users.find((u) => u.Id === account.OwnerId);
r.Account = {
Owner: {
Email: owner ? owner.Email : null,
},
OwnerId: account.OwnerId,
};
}
}
return r;
});
}
if (query.includes("Owner.Email")) {
result = result.map((r) => {
if (r.OwnerId) {
const owner = records.users.find((u) => u.Id === r.OwnerId);
r.Owner = {
Email: owner ? owner.Email : null,
};
}
return r;
});
}
return { records: result };
};
const getAccountById = (id: string) => {
return records.accounts.find((a) => a.Id === id);
};
// Add record helpers
const addContact = (contact: Contact) => {
const newContact = {
Id: contact.Id || `003${Math.random().toString().slice(2, 10)}`,
attributes: { type: "Contact" },
...contact,
};
if (newContact.AccountId) {
const account = getAccountById(newContact.AccountId);
if (account) {
newContact.Account = {
...account,
};
} else {
throw new Error(`Account with id ${newContact.AccountId} not found`);
}
}
logger.debug("[SalesforceMock]Adding contact", safeStringify(newContact));
records.contacts.push(newContact);
return newContact;
};
const addLead = (lead: Lead) => {
const newLead = {
Id: lead.Id || `00Q${Math.random().toString().slice(2, 10)}`,
attributes: { type: "Lead" },
...lead,
};
if (newLead.AccountId) {
const account = getAccountById(newLead.AccountId);
if (account) {
newLead.Account = {
...account,
};
} else {
throw new Error(`Account with id ${newLead.AccountId} not found`);
}
}
logger.debug("[SalesforceMock] Adding lead", safeStringify(newLead));
records.leads.push(newLead);
return newLead;
};
const addAccount = (account: Account) => {
const newAccount = {
Id: account.Id || `001${Math.random().toString().slice(2, 10)}`,
...account,
attributes: { type: "Account" },
};
records.accounts.push(newAccount);
logger.debug("[SalesforceMock] Adding account", safeStringify(newAccount));
return newAccount;
};
const addUser = (user: User) => {
const newUser = {
Id: user.Id || `005${Math.random().toString().slice(2, 10)}`,
IsActive: user.IsActive !== undefined ? user.IsActive : true,
attributes: { type: "User" },
...user,
};
logger.debug("[SalesforceMock] Adding user", safeStringify(newUser));
records.users.push(newUser);
return newUser;
};
// Mock connection interface
const mockConnection = {
query: vi.fn().mockImplementation(handleQuery),
sobject: vi.fn().mockReturnValue({
create: vi.fn().mockImplementation(() => ({ success: true, id: `id_${Math.random()}` })),
update: vi.fn().mockImplementation(() => ({ success: true })),
delete: vi.fn().mockImplementation(() => ({ success: true })),
}),
describe: vi.fn().mockImplementation(() => {
return {
fields: [
{ name: "Id", type: "id" },
{ name: "Email", type: "email" },
{ name: "FirstName", type: "string" },
{ name: "LastName", type: "string" },
{ name: "Company", type: "string" },
{ name: "Website", type: "url" },
{ name: "OwnerId", type: "reference" },
{ name: "NoShow__c", type: "boolean", length: 0 },
{ name: "LastMeetingDate__c", type: "date" },
],
};
}),
clear: () => {
records.contacts = [];
records.leads = [];
records.accounts = [];
records.users = [];
records.events = [];
mockConnection.query.mockClear();
mockConnection.sobject.mockClear();
mockConnection.describe.mockClear();
},
};
return {
mockConnection,
addContact,
addLead,
addAccount,
addUser,
getRecords: () => records,
};
};
@@ -123,7 +123,12 @@ async function getOwnerEmailFromCrm(
if (!crmContactOwner?.email) return returnNullValue;
log.info(
"[getOwnerEmailFromCrm] crmContactOwner",
safeStringify({ contactOwnerEmail: crmContactOwner.email, email, eventTypeId: eventData.id })
safeStringify({
contactOwnerEmail: crmContactOwner.email,
recordType: crmContactOwner.recordType,
email,
eventTypeId: eventData.id,
})
);
// Determine if the contactOwner is a part of the event type
const contactOwnerQuery = await findUserByEmailWhoIsAHostOfEventType({