mautic / plugins /MauticCrmBundle /Tests /Integration /SalesforceIntegrationTest.php
chrisbryan17's picture
Upload folder using huggingface_hub
d2897cd verified
<?php
namespace MauticPlugin\MauticCrmBundle\Tests\Integration;
use Mautic\CoreBundle\Entity\AuditLogRepository;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Entity\IntegrationEntity;
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
use Mautic\PluginBundle\Event\PluginIntegrationKeyEvent;
use Mautic\PluginBundle\Exception\ApiErrorException;
use Mautic\PluginBundle\Model\IntegrationEntityModel;
use Mautic\PluginBundle\Tests\Integration\AbstractIntegrationTestCase;
use MauticPlugin\MauticCrmBundle\Integration\SalesforceIntegration;
use PHPUnit\Framework\MockObject\MockObject;
class SalesforceIntegrationTest extends AbstractIntegrationTestCase
{
public const SC_MULTIPLE_SF_LEADS = 'multiple_sf_leads';
public const SC_MULTIPLE_SF_CONTACTS = 'multiple_sf_contacts';
public const SC_CONVERTED_SF_LEAD = 'converted_sf_lead';
public const SC_EMAIL_WITH_APOSTROPHE = 'email_with_apostrophe';
public const SC_MULTIPLE_MAUTIC_CONTACTS = 'multiple_mautic_contacts';
/**
* @var array
*/
protected $maxInvocations = [];
/**
* @var string|null
*/
protected $specialSfCase;
/**
* @var array
*/
protected $persistedIntegrationEntities = [];
/**
* @var array
*/
protected $returnedSfEntities = [];
/**
* @var array
*/
protected $mauticContacts = [
'Contact' => [],
'Lead' => [],
];
/**
* @var array
*/
protected $sfObjects = [
'Lead',
'Contact',
'company',
];
/**
* @var array
*/
protected $sfMockMethods = [
'makeRequest',
];
/**
* @var array
*/
protected $sfMockResetMethods = [
'makeRequest',
];
/**
* @var array
*/
protected $sfMockResetObjects = [
'Lead',
'Contact',
'company',
];
/**
* @var int
*/
protected $idCounter = 1;
/**
* @var array
*/
protected $leadsUpdatedCounter = [
'Lead' => 0,
'Contact' => 0,
];
/**
* @var int
*/
protected $leadsCreatedCounter = 0;
protected function setUp(): void
{
parent::setUp();
defined('MAUTIC_ENV') or define('MAUTIC_ENV', 'test');
}
/**
* Reset.
*/
public function tearDown(): void
{
$this->returnedSfEntities = [];
$this->persistedIntegrationEntities = [];
$this->sfMockMethods = $this->sfMockResetMethods;
$this->sfObjects = $this->sfMockResetObjects;
$this->specialSfCase = null;
$this->idCounter = 1;
$this->leadsCreatedCounter = 0;
$this->leadsUpdatedCounter = [
'Lead' => 0,
'Contact' => 0,
];
$this->mauticContacts = [];
}
public function testPushLeadsUpdateAndCreateCorrectNumbers(): void
{
$sf = $this->getSalesforceIntegration();
$stats = $sf->pushLeads();
$this->assertCount(400, $this->getPersistedIntegrationEntities());
$this->assertEquals(300, $stats[0], var_export($stats, true)); // update
$this->assertEquals(100, $stats[1], var_export($stats, true)); // create
}
public function testThatMultipleSfLeadsReturnedAreUpdatedButOnlyOneIntegrationRecordIsCreated(): void
{
$this->companyModel->expects($this->any())
->method('fetchCompanyFields')
->willReturn([]);
$this->specialSfCase = self::SC_MULTIPLE_SF_LEADS;
$sf = $this->getSalesforceIntegration(2, 0, 2, 0, 'Lead');
$sf->pushLeads();
// Validate there are only two integration entities (two contacts with same email)
$integrationEntities = $this->getPersistedIntegrationEntities();
$this->assertCount(2, $integrationEntities);
// Validate that there were 4 found entries (two duplicate leads)
$sfEntities = $this->getReturnedSfEntities();
$this->assertCount(4, $sfEntities);
}
public function testThatMultipleSfContactsReturnedAreUpdatedButOnlyOneIntegrationRecordIsCreated(): void
{
$this->companyModel->expects($this->any())
->method('fetchCompanyFields')
->willReturn([]);
$this->specialSfCase = self::SC_MULTIPLE_SF_CONTACTS;
$sf = $this->getSalesforceIntegration(2, 0, 0, 2, 'Contact');
$sf->pushLeads();
// Validate there are only two integration entities (two contacts with same email)
$integrationEntities = $this->getPersistedIntegrationEntities();
$this->assertEquals(2, count($integrationEntities));
// Validate that there were 4 found entries (two duplciate leads)
$sfEntities = $this->getReturnedSfEntities();
$this->assertEquals(4, count($sfEntities));
}
public function testThatLeadsAreOnlyCreatedIfEnabled(): void
{
$this->sfObjects = ['Contact'];
$this->sfMockMethods = ['makeRequest', 'getMauticContactsToCreate'];
$sf = $this->getSalesforceIntegration();
$sf->expects($this->never())
->method('findLeadsToCreate');
$sf->expects($this->never())
->method('getMauticContactsToCreate');
$sf->pushLeads();
}
public function testThatLeadsAreOnlyCreatedIfLimitIsAppropriate(): void
{
$this->sfMockMethods = ['makeRequest', 'getMauticContactsToCreate', 'getMauticContactsToUpdate', 'getSalesforceSyncLimit'];
$sf = $this->getSalesforceIntegration();
$counter = 0;
$sf->expects($this->exactly(2))
->method('getMauticContactsToUpdate')
->will(
$this->returnCallback(
function () use (&$counter) {
++$counter;
return true;
}
)
);
$sf->method('getSalesforceSyncLimit')
->willReturn(50);
$sf->expects($this->exactly(1))
->method('getMauticContactsToCreate');
$sf->pushLeads();
}
public function testThatLeadsAreNotCreatedIfCountIsLessThanLimit(): void
{
$this->sfMockMethods = ['makeRequest', 'getMauticContactsToCreate', 'getMauticContactsToUpdate', 'getSalesforceSyncLimit'];
$sf = $this->getSalesforceIntegration();
$counter = 0;
$sf->expects($this->exactly(2))
->method('getMauticContactsToUpdate')
->will(
$this->returnCallback(
function () use (&$counter) {
++$counter;
return true;
}
)
);
$counter = 0;
$sf->method('getSalesforceSyncLimit')
->will(
$this->returnCallback(
function () use (&$counter) {
++$counter;
return (1 === $counter) ? 100 : 0;
}
)
);
$sf->expects($this->never())
->method('getMauticContactsToCreate');
$sf->pushLeads();
}
public function testLastSyncDate(): void
{
$class = new \ReflectionClass(SalesforceIntegration::class);
$lastSyncMethod = $class->getMethod('getLastSyncDate');
$lastSyncMethod->setAccessible(true);
$sf = $this->getSalesforceIntegration();
// should be a DateTime
$lastSync = $lastSyncMethod->invokeArgs($sf, []);
$this->assertTrue($lastSync instanceof \DateTime);
// Set the override set by fetch command
define('MAUTIC_DATE_MODIFIED_OVERRIDE', time());
/** @var \DateTime $lastSync */
$lastSync = $lastSyncMethod->invokeArgs($sf, []);
// should be teh same as MAUTIC_DATE_MODIFIED_OVERRIDE override
$this->assertTrue($lastSync instanceof \DateTime);
$this->assertEquals(MAUTIC_DATE_MODIFIED_OVERRIDE, $lastSync->format('U'));
$lead = new Lead();
$reflectedLead = new \ReflectionObject($lead);
$reflectedId = $reflectedLead->getProperty('id');
$reflectedId->setAccessible(true);
$reflectedId->setValue($lead, 1);
$modified = new \DateTime('-15 minutes');
$lead->setDateModified($modified);
// Set it twice to get an original and updated datetime
$now = new \DateTime();
$lead->setDateModified($now);
$params = [
'start' => $modified->format('c'),
];
// Should be null due to the contact was updated since last sync
$lastSync = $lastSyncMethod->invokeArgs($sf, [$lead, $params, false]);
$this->assertNull($lastSync);
// Should be a DateTime object
$lead = new Lead();
$lastSync = $lastSyncMethod->invokeArgs($sf, [$lead, $params]);
$this->assertTrue($lastSync instanceof \DateTime);
}
public function testLeadsAreNotCreatedInSfIfFoundToAlreadyExistAsContacts(): void
{
$this->sfObjects = ['Lead', 'Contact'];
$this->sfMockMethods = ['makeRequest', 'getSalesforceObjectsByEmails', 'prepareMauticContactsToCreate'];
$sf = $this->getSalesforceIntegration(0, 2);
/*
* This forces the integration to think the contact exists in SF,
* and removes those emails from the array for creation.
*/
$sf->method('getSalesforceObjectsByEmails')
->willReturnCallback(
function () {
$args = func_get_args();
$emails = array_column($args[1], 'email');
return $this->getSalesforceObjects($emails, 0, 1);
}
);
$sf->expects($this->never())
->method('prepareMauticContactsToCreate');
$sf->pushLeads();
}
public function testLeadsAreNotCreatedInSfIfFoundToAlreadyExistAsLeads(): void
{
$this->sfObjects = ['Lead'];
$this->sfMockMethods = ['makeRequest', 'getSalesforceObjectsByEmails', 'prepareMauticContactsToCreate'];
$sf = $this->getSalesforceIntegration(0, 1);
/*
* This forces the integration to think the contact exists in SF,
* and removes those emails from the array for creation.
*/
$sf->method('getSalesforceObjectsByEmails')
->willReturnCallback(
function () {
$args = func_get_args();
$emails = array_column($args[1], 'email');
return $this->getSalesforceObjects($emails, 0, 1);
}
);
$sf->expects($this->never())
->method('prepareMauticContactsToCreate');
$sf->pushLeads();
}
public function testExceptionIsThrownIfSfReturnsErrorOnEmailLookup(): void
{
$this->sfObjects = ['Lead'];
$this->sfMockMethods = ['makeRequest', 'getSalesforceObjectsByEmails'];
$sf = $this->getSalesforceIntegration();
$sf->method('getSalesforceObjectsByEmails')
->willReturn('Some Error');
$this->expectException(ApiErrorException::class);
$sf->pushLeads();
}
public function testGetCampaigns(): void
{
$this->sfObjects = ['Contact'];
$this->sfMockMethods = ['makeRequest'];
$sf = $this->getSalesforceIntegration();
$sf->expects($this->once())
->method('makeRequest')
->with(
'https://sftest.com/services/data/v34.0/query',
[
'q' => 'Select Id, Name from Campaign where isDeleted = false',
]
);
$sf->getCampaigns();
}
public function testGetCampaignMembers(): void
{
$this->sfObjects = ['Contact'];
$this->sfMockMethods = ['makeRequest'];
$sf = $this->getSalesforceIntegration();
$sf->expects($this->once())
->method('makeRequest')
->with(
'https://sftest.com/services/data/v34.0/query',
[
'q' => "Select CampaignId, ContactId, LeadId, isDeleted from CampaignMember where CampaignId = '1'",
]
);
$sf->getCampaignMembers(1);
}
public function testGetCampaignMemberStatus(): void
{
$this->sfObjects = ['Contact'];
$this->sfMockMethods = ['makeRequest'];
$sf = $this->getSalesforceIntegration();
$sf->expects($this->once())
->method('makeRequest')
->with(
'https://sftest.com/services/data/v34.0/query',
[
'q' => "Select Id, Label from CampaignMemberStatus where isDeleted = false and CampaignId='1'",
]
);
$sf->getCampaignMemberStatus(1);
}
public function testPushToCampaign(): void
{
$this->sfObjects = ['Contact'];
$this->sfMockMethods = ['makeRequest'];
$sf = $this->getSalesforceIntegration();
$lead = new Lead();
$lead->setFirstname('Lead1');
$lead->setEmail('[email protected]');
$lead->setId(1);
$sf->expects($this->any())
->method('makeRequest')
->willReturnCallback(
function () {
$args = func_get_args();
// Checking for campaign members should return empty array for testing purposes
if (str_contains($args[0], '/query') && str_contains($args[1]['q'], 'CampaignMember')) {
return [];
}
if (str_contains($args[0], '/composite')) {
$this->assertSame(
'1-CampaignMemberNew-null-1',
$args[1]['compositeRequest'][0]['referenceId'],
'The composite request when pushing a campaign member should contain the correct referenceId.'
);
return true;
}
}
);
$sf->pushLeadToCampaign($lead, 1, 'Active', ['Lead' => [1]]);
}
public function testPushCompany(): void
{
$this->sfObjects = ['Account'];
$this->sfMockMethods = ['makeRequest'];
$sf = $this->getSalesforceIntegration();
$company = new Company();
$company->setName('MyCompanyName');
$sf->expects($this->any())
->method('makeRequest')
->willReturnCallback(
function () {
$args = func_get_args();
// Checking for campaign members should return empty array for testing purposes
if (str_contains($args[0], '/query') && str_contains($args[1]['q'], 'Account')) {
return [];
}
if (str_contains($args[0], '/composite')) {
$this->assertSame(
'1-Account-null-1',
$args[1]['compositeRequest'][0]['referenceId'],
'The composite request when pushing an account should contain the correct referenceId.'
);
return true;
}
}
);
$this->assertFalse($sf->pushCompany($company));
}
public function testExportingContactActivity(): void
{
$this->sfObjects = ['Contact'];
$this->sfMockMethods = ['makeRequest', 'getSalesforceObjectsByEmails', 'isAuthorized', 'getFetchQuery', 'getLeadData'];
$sf = $this->getSalesforceIntegration(2, 2);
$sf->expects($this->once())
->method('isAuthorized')
->willReturn(true);
$sf->expects($this->once())
->method('getFetchQuery')
->with([])
->willReturnCallback(
fn () => [
'start' => '-1 week',
'end' => 'now',
]
);
$this->setMaxInvocations('getIntegrationsEntityId', 1);
$sf->expects($this->once())
->method('getLeadData')
->willReturnCallback(
function () {
$leadIds = func_get_arg(2);
$data = [];
foreach ($leadIds as $i => $id) {
++$i;
$data[$id] = [
'records' => [
[
'eventType' => 'email',
'name' => 'Email Name',
'description' => 'Email sent',
'dateAdded' => new \DateTime(),
'id' => 'pointChange'.$i,
],
],
];
}
return $data;
}
);
/*
* Ensures that makeRequest is called with the mautic_timeline__c endpoint.
* If it is, then we've successfully exported contact activity.
*/
$sf->expects($this->exactly(1))
->method('makeRequest')
->with('https://sftest.com/services/data/v38.0/composite/')
->willReturnCallback(
fn () => $this->getSalesforceCompositeResponse(func_get_arg(1))
);
$sf->pushLeadActivity();
}
public function testMauticContactTimelineLinkPopulatedsPayload(): void
{
$this->sfObjects = ['Contact'];
$this->sfMockMethods = ['makeRequest', 'getSalesforceObjectsByEmails'];
$sf = $this->getSalesforceIntegration(2, 2);
/*
* When checking if contacts need to be created, the mauticContactTimelineLink
* has been populated at this point. If populated here, the test passes.
* We return the salesforce objects so as not to throw an error.
*/
$sf->method('getSalesforceObjectsByEmails')
->willReturnCallback(
function () {
$args = func_get_args();
$emails = array_column($args[1], 'email');
return $this->getSalesforceObjects($emails, 0, 1);
}
);
/*
* With the given SF integration setup, the `getSalesforceObjectsByEmails` method
* will be called once, and have the given parameters, which contains the contact
* timeline link.
*/
$sf->expects($this->once())
->method('getSalesforceObjectsByEmails')
->with(
'Contact',
[
'[email protected]' => [
'integration_entity_id' => 'SF1',
'integration_entity' => 'Contact',
'id' => 1,
'internal_entity_id' => 1,
'firstname' => 'Contact1',
'lastname' => 'Contact1',
'company' => 'Contact1',
'email' => '[email protected]',
'mauticContactTimelineLink' => 'mautic_plugin_timeline_view',
'mauticContactIsContactableByEmail' => 1,
'mauticContactId' => 1,
],
'[email protected]' => [
'integration_entity_id' => 'SF2',
'integration_entity' => 'Contact',
'id' => 2,
'internal_entity_id' => 2,
'firstname' => 'Contact2',
'lastname' => 'Contact2',
'company' => 'Contact2',
'email' => '[email protected]',
'mauticContactTimelineLink' => 'mautic_plugin_timeline_view',
'mauticContactIsContactableByEmail' => 1,
'mauticContactId' => 2,
],
],
'FirstName,LastName,Email'
);
$sf->pushLeads();
}
public function testUpdateDncBySfDate(): void
{
$this->sfMockMethods = ['makeRequest', 'updateDncByDate', 'getDncHistory'];
$objects = ['Contact', 'Lead'];
foreach ($objects as $object) {
$mappedData = [
'[email protected]' => [
'integration_entity_id' => 'SF1',
'integration_entity' => $object,
'id' => 1,
'internal_entity_id' => 1,
'firstname' => 'Contact1',
'lastname' => 'Contact1',
'company' => 'Contact1',
'email' => '[email protected]',
'mauticContactTimelineLink' => 'mautic_plugin_timeline_view',
'mauticContactIsContactableByEmail' => 1,
],
'[email protected]' => [
'integration_entity_id' => 'SF2',
'integration_entity' => $object,
'id' => 2,
'internal_entity_id' => 2,
'firstname' => 'Contact2',
'lastname' => 'Contact2',
'company' => 'Contact2',
'email' => '[email protected]',
'mauticContactTimelineLink' => 'mautic_plugin_timeline_view',
'mauticContactIsContactableByEmail' => 0,
],
];
$sf = $this->getSalesforceIntegration(2, 2);
$sf->expects($this->any())->method('updateDncByDate')->willReturn(true);
$sf->expects($this->any())
->method('getDncHistory')
->willReturn(
$this->getSalesforceDNCHistory($object, 'SF')
);
$sf->pushLeadDoNotContactByDate('email', $mappedData, $object, ['start' => '2017-10-16 13:00:00.000000']);
foreach ($mappedData as $assertion) {
$this->assertArrayHasKey('mauticContactIsContactableByEmail', $assertion);
}
}
}
public function testUpdateDncByMauticDate(): void
{
$this->sfMockMethods = ['makeRequest', 'updateDncByDate', 'getDoNotContactHistory'];
$objects = ['Contact', 'Lead'];
foreach ($objects as $object) {
$mappedData = [
'[email protected]' => [
'integration_entity_id' => 'SF1',
'integration_entity' => $object,
'id' => 1,
'internal_entity_id' => 1,
'firstname' => 'Contact1',
'lastname' => 'Contact1',
'company' => 'Contact1',
'email' => '[email protected]',
'mauticContactTimelineLink' => 'mautic_plugin_timeline_view',
'mauticContactIsContactableByEmail' => 1,
],
];
$sf = $this->getSalesforceIntegration(2, 2);
$sf->expects($this->any())->method('updateDncByDate')->willReturn(true);
$sf->expects($this->any())->method('getDoNotContactHistory')->willReturn($this->getSalesforceDNCHistory($object, 'Mautic'));
$sf->pushLeadDoNotContactByDate('email', $mappedData, $object, ['start' => '2017-10-15T10:00:00.000000']);
foreach ($mappedData as $assertion) {
$this->assertArrayNotHasKey('mauticContactIsContactableByEmail', $assertion);
}
}
}
public function testAmendLeadDataBeforePush(): void
{
$input = ['first', false, 'first|second', 1];
$output = ['first', false, 'first;second', 1];
$sf = $this->getSalesforceIntegration();
$sf->amendLeadDataBeforePush($input);
self::assertSame($input, $output);
self::assertEquals('string', gettype($output[0]));
self::assertEquals('boolean', gettype($output[1]));
self::assertEquals('string', gettype($output[2]));
self::assertEquals('integer', gettype($output[3]));
}
/**
* @param string $name
* @param int $max
*
* @return $this
*/
protected function setMaxInvocations($name, $max)
{
$this->maxInvocations[$name] = $max;
return $this;
}
/**
* @return int
*/
protected function getMaxInvocations($name)
{
return $this->maxInvocations[$name] ?? 1;
}
protected function setMocks()
{
$integrationEntityRepository = $this->getMockBuilder(IntegrationEntityRepository::class)
->disableOriginalConstructor()
->getMock();
// we need insight into the entities persisted
$integrationEntityRepository->method('saveEntities')
->willReturnCallback(
function (): void {
$this->persistedIntegrationEntities = array_merge($this->persistedIntegrationEntities, func_get_arg(0));
}
);
$integrationEntityRepository
->expects($spy = $this->any())
->method('getIntegrationsEntityId')
->willReturnCallback(
function () use ($spy) {
// WARNING: this is using a PHPUnit undocumented workaround:
// https://github.com/sebastianbergmann/phpunit/issues/3888
$spyParentProperties = self::getParentPrivateProperties($spy);
$invocations = $spyParentProperties['invocations'];
if (count($invocations) > $this->getMaxInvocations('getIntegrationsEntityId')) {
return [];
}
// Just return some bogus entities for testing
return $this->getLeadsToUpdate('Lead', 2, 2, 'Lead')['Lead'];
}
);
$auditLogRepo = $this->getMockBuilder(AuditLogRepository::class)
->disableOriginalConstructor()
->getMock();
$auditLogRepo
->expects($this->any())
->method('getAuditLogsForLeads')
->willReturn(
[
[
'userName' => 'Salesforce',
'userId' => 0,
'bundle' => 'lead',
'object' => 'lead',
'objectId' => 1,
'action' => 'update',
'details' => [
'dnc_channel_status' => [
'email' => [
'reason' => 3,
'comments' => 'Set by Salesforce',
],
],
'dnc_status' => [
'manual',
'Set by Salesforce',
],
],
'dateAdded' => new \DateTime('2017-10-16 15:00:36.000000', new \DateTimeZone('UTC')),
'ipAddress' => '127.0.0.1',
],
]
);
$this->em->method('getRepository')
->willReturnMap(
[
[IntegrationEntity::class, $integrationEntityRepository],
[\Mautic\CoreBundle\Entity\AuditLog::class, $auditLogRepo],
]
);
$this->em->method('getReference')
->willReturnCallback(
function () {
switch (func_get_arg(0)) {
case IntegrationEntity::class:
return new IntegrationEntity();
}
}
);
$this->router->method('generate')
->willReturnArgument(0);
$this->leadModel->method('getEntity')
->willReturn(new Lead());
$this->companyModel->method('getEntity')
->willReturn(new Company());
$this->companyModel->method('getEntities')
->willReturn([]);
$leadFields = [
'Id__Lead' => [
'type' => 'string',
'label' => 'Lead-Lead ID',
'required' => false,
'group' => 'Lead',
'optionLabel' => 'Lead ID',
],
'LastName__Lead' => [
'type' => 'string',
'label' => 'Lead-Last Name',
'required' => true,
'group' => 'Lead',
'optionLabel' => 'Last Name',
],
'FirstName__Lead' => [
'type' => 'string',
'label' => 'Lead-First Name',
'required' => false,
'group' => 'Lead',
'optionLabel' => 'First Name',
],
'Company__Lead' => [
'type' => 'string',
'label' => 'Lead-Company',
'required' => true,
'group' => 'Lead',
'optionLabel' => 'Company',
],
'Email__Lead' => [
'type' => 'string',
'label' => 'Lead-Email',
'required' => false,
'group' => 'Lead',
'optionLabel' => 'Email',
],
];
$contactFields = [
'Id__Contact' => [
'type' => 'string',
'label' => 'Contact-Contact ID',
'required' => false,
'group' => 'Contact',
'optionLabel' => 'Contact ID',
],
'LastName__Contact' => [
'type' => 'string',
'label' => 'Contact-Last Name',
'required' => true,
'group' => 'Contact',
'optionLabel' => 'Last Name',
],
'FirstName__Contact' => [
'type' => 'string',
'label' => 'Contact-First Name',
'required' => false,
'group' => 'Contact',
'optionLabel' => 'First Name',
],
'Email__Contact' => [
'type' => 'string',
'label' => 'Contact-Email',
'required' => false,
'group' => 'Contact',
'optionLabel' => 'Email',
],
];
$this->cache
->method('get')
->willReturnMap(
[
['leadFields.Lead', null, $leadFields],
['leadFields.Contact', null, $contactFields],
]
);
$this->cache->method('getCache')
->willReturn($this->cache);
}
/**
* @param int $maxUpdate
* @param int $maxCreate
* @param int $maxSfLeads
* @param int $maxSfContacts
*
* @return SalesforceIntegration|MockObject
*/
protected function getSalesforceIntegration($maxUpdate = 100, $maxCreate = 200, $maxSfLeads = 25, $maxSfContacts = 25, $updateObject = null)
{
$this->setMocks();
$featureSettings = [
'sandbox' => [
],
'updateOwner' => [
],
'objects' => $this->sfObjects,
'namespace' => null,
'leadFields' => [
'Company__Lead' => 'company',
'FirstName__Lead' => 'firstname',
'LastName__Lead' => 'lastname',
'Email__Lead' => 'email',
'FirstName__Contact' => 'firstname',
'LastName__Contact' => 'lastname',
'Email__Contact' => 'email',
],
'update_mautic' => [
'Company__Lead' => '0',
'FirstName__Lead' => '0',
'LastName__Lead' => '0',
'Email__Lead' => '0',
'FirstName__Contact' => '0',
'LastName__Contact' => '0',
'Email__Contact' => '0',
],
'companyFields' => [
'Name' => 'companyname',
],
'update_mautic_company' => [
'Name' => '0',
],
];
$integration = new Integration();
$integration->setIsPublished(true)
->setName('Salesforce')
->setPlugin('MauticCrmBundle')
->setApiKeys(
[
'access_token' => '123',
'instance_url' => 'https://sftest.com',
]
)
->setFeatureSettings($featureSettings)
->setSupportedFeatures(
[
'get_leads',
'push_lead',
'push_leads',
]
);
$integrationEntityModelMock = $this->getMockBuilder(IntegrationEntityModel::class)
->disableOriginalConstructor()
->getMock();
$integrationEntityModelMock->method('getEntityByIdAndSetSyncDate')
->willReturn(new IntegrationEntity());
$sf = $this->getMockBuilder(SalesforceIntegration::class)
->setConstructorArgs([
$this->dispatcher,
$this->cache,
$this->em,
$this->session,
$this->request,
$this->router,
$this->translator,
$this->logger,
$this->encryptionHelper,
$this->leadModel,
$this->companyModel,
$this->pathsHelper,
$this->notificationModel,
$this->fieldModel,
$integrationEntityModelMock,
$this->doNotContact,
])
->onlyMethods($this->sfMockMethods)
->addMethods(['findLeadsToCreate'])
->getMock();
$sf->method('makeRequest')
->will(
$this->returnCallback(
function () use ($maxSfContacts, $maxSfLeads, $updateObject) {
$args = func_get_args();
// Determine what to return by analyzing the URL and query parameters
switch (true) {
case str_contains($args[0], '/query'):
if (isset($args[1]['q']) && str_contains($args[0], 'from CampaignMember')) {
return [];
} elseif (isset($args[1]['q']) && str_contains($args[1]['q'], 'from Campaign')) {
return [
'totalSize' => 0,
'records' => [],
];
} elseif (isset($args[1]['q']) && str_contains($args[1]['q'], 'from Account')) {
return [
'totalSize' => 0,
'records' => [],
];
} elseif (isset($args[1]['q']) && 'SELECT CreatedDate from Organization' === $args[1]['q']) {
return [
'records' => [
['CreatedDate' => '2012-10-30T17:56:50.000+0000'],
],
];
} elseif (isset($args[1]['q']) && str_contains($args[1]['q'], 'from '.$updateObject.'History')) {
return $this->getSalesforceDNCHistory($updateObject, 'Mautic');
} else {
// Extract emails
$found = preg_match('/Email in \(\'(.*?)\'\)/', $args[1]['q'], $match);
if ($found) {
$emails = explode("','", $match[1]);
return $this->getSalesforceObjects($emails, $maxSfContacts, $maxSfLeads);
} else {
return $this->getSalesforceObjects([], $maxSfContacts, $maxSfLeads);
}
}
// no break
case str_contains($args[0], '/composite'):
return $this->getSalesforceCompositeResponse($args[1]);
}
}
)
);
/* @var \PHPUnit\Framework\MockObject\MockObject $this->>dispatcher */
$this->dispatcher->method('dispatch')
->will(
$this->returnCallback(
function () use ($sf, $integration) {
$args = func_get_args();
return match ($args[0]) {
default => new PluginIntegrationKeyEvent($sf, $integration->getApiKeys()),
};
}
)
);
$sf->setIntegrationSettings($integration);
$repo = $sf->getIntegrationEntityRepository();
$this->setLeadsToUpdate($repo, $maxUpdate, $maxSfContacts, $maxSfLeads, $updateObject);
$this->setLeadsToCreate($repo, $maxCreate);
return $sf;
}
protected function setLeadsToUpdate(MockObject $mockRepository, $max, $maxSfContacts, $maxSfLeads, $specificObject)
{
$mockRepository->method('findLeadsToUpdate')
->willReturnCallback(
function () use ($max, $specificObject) {
$args = func_get_args();
$object = $args[6];
// determine whether to return a count or records
$results = [];
if (false === $args[3]) {
foreach ($object as $object) {
if ($specificObject && $specificObject !== $object) {
continue;
}
// Should be 100 contacts and 100 leads
$results[$object] = $max;
}
return $results;
}
return $this->getLeadsToUpdate($object, $args[3], $max, $specificObject);
}
);
}
/**
* @param int $max
*/
protected function setLeadsToCreate(MockObject $mockRepository, $max = 200)
{
$mockRepository->method('findLeadsToCreate')
->willReturnCallback(
function () use ($max) {
$args = func_get_args();
if (false === $args[2]) {
return $max;
}
$createLeads = $this->getLeadsToCreate($args[2], $max);
// determine whether to return a count or records
if (false === $args[2]) {
return count($createLeads);
}
return $createLeads;
}
);
}
/**
* Simulate looping over Mautic leads to update.
*
* @return array
*/
protected function getLeadsToUpdate($object, $limit, $max, $specificObject)
{
$entities = [
$object => [],
];
// Should be 100 each
if (($this->leadsUpdatedCounter[$object] >= $max) || ($specificObject && $specificObject !== $object)) {
return $entities;
}
if ($limit > $max) {
$limit = $max;
}
$counter = 0;
while ($counter < $limit) {
$entities[$object][$this->idCounter] = [
'integration_entity_id' => 'SF'.$this->idCounter,
'integration_entity' => $object,
'id' => $this->idCounter,
'internal_entity_id' => $this->idCounter,
'firstname' => $object.$this->idCounter,
'lastname' => $object.$this->idCounter,
'company' => $object.$this->idCounter,
'email' => $object.$this->idCounter.'@sftest.com',
];
++$counter;
++$this->idCounter;
++$this->leadsUpdatedCounter[$object];
}
$this->mauticContacts = array_merge($entities[$object], $this->mauticContacts);
return $entities;
}
/**
* Simulate looping over Mautic leads to create.
*
* @return array
*/
protected function getLeadsToCreate($limit, $max = 200)
{
$entities = [];
if ($this->leadsCreatedCounter > $max) {
return $entities;
}
if ($limit > $max) {
$limit = $max;
}
$counter = 0;
while ($counter < $limit) {
// Start after the update
$entities[$this->idCounter] = [
'id' => $this->idCounter,
'internal_entity_id' => $this->idCounter,
'firstname' => 'Lead'.$this->idCounter,
'lastname' => 'Lead'.$this->idCounter,
'company' => 'Lead'.$this->idCounter,
'email' => 'Lead'.$this->idCounter.'@sftest.com',
];
++$this->idCounter;
++$counter;
++$this->leadsCreatedCounter;
}
$this->mauticContacts = array_merge($entities, $this->mauticContacts);
return $entities;
}
/**
* Mock SF response.
*
* @return array
*/
protected function getSalesforceObjects($emails, $maxContacts, $maxLeads)
{
// Let's find around $max records
$records = [];
$contactCount = 0;
$leadCount = 0;
if (!empty($emails)) {
foreach ($emails as $email) {
// Extact ID
preg_match('/(Lead|Contact)([0-9]*)@sftest\.com/', $email, $match);
$object = $match[1];
if ('Lead' === $object) {
if ($leadCount >= $maxLeads) {
continue;
}
++$leadCount;
} else {
if ($contactCount >= $maxContacts) {
continue;
}
++$contactCount;
}
$id = $match[2];
$records[] = [
'attributes' => [
'type' => $object,
'url' => "/services/data/v34.0/sobjects/$object/SF$id",
],
'Id' => 'SF'.$id,
'FirstName' => $object.$id,
'LastName' => $object.$id,
'Email' => $object.$id.'@sftest.com',
];
$this->addSpecialCases($id, $records);
}
}
$this->returnedSfEntities = array_merge($this->returnedSfEntities, $records);
return [
'totalSize' => count($records),
'done' => true,
'records' => $records,
];
}
/**
* Mock SF response.
*
* @return array
*/
protected function getSalesforceDNCHistory($object, $priority)
{
$datePriority = [
'SF' => '2017-10-16T00:43:43.000+0000',
'Mautic' => '2017-10-16T18:43:43.000+0000',
];
return [
'totalSize' => 3,
'done' => 1,
'records' => [
[
'attributes' => [
'type' => 'ContactHistory',
'url' => '/services/data/v34.0/sobjects/'.$object.'History/0170SFH1',
],
'Field' => 'HasOptedOutOfEmail',
$object.'Id' => 'SF1',
'CreatedDate' => $datePriority[$priority],
'IsDeleted' => false,
'NewValue' => true,
],
[
'attributes' => [
'type' => 'ContactHistory',
'url' => '/services/data/v34.0/sobjects/'.$object.'History/0170SFH3',
],
'Field' => 'HasOptedOutOfEmail',
$object.'Id' => 'SF2',
'CreatedDate' => $datePriority[$priority],
'IsDeleted' => false,
'NewValue' => true,
],
],
];
}
protected function addSpecialCases($id, &$records)
{
switch ($this->specialSfCase) {
case self::SC_MULTIPLE_SF_LEADS:
$records[] = [
'attributes' => [
'type' => 'Lead',
'url' => '/services/data/v34.0/sobjects/Lead/SF'.$id.'b',
],
'Id' => 'SF'.$id.'b',
'FirstName' => 'Lead'.$id,
'LastName' => 'Lead'.$id,
'Email' => 'Lead'.$id.'@sftest.com',
'Company' => 'Lead'.$id,
'ConvertedContactId' => null,
];
break;
case self::SC_MULTIPLE_SF_CONTACTS:
$records[] = [
'attributes' => [
'type' => 'Contact',
'url' => '/services/data/v34.0/sobjects/Contact/SF'.$id.'b',
],
'Id' => 'SF'.$id.'b',
'FirstName' => 'Contact'.$id,
'LastName' => 'Contact'.$id,
'Email' => 'Contact'.$id.'@sftest.com',
'Company' => 'Contact'.$id,
'ConvertedContactId' => null,
];
break;
}
}
/**
* Mock SF response.
*
* @return array
*/
protected function getSalesforceCompositeResponse($data)
{
$response = [];
foreach ($data['compositeRequest'] as $subrequest) {
if ('PATCH' === $subrequest['method']) {
$response[] = [
'body' => null,
'httpHeaders' => [],
'httpStatusCode' => 204,
'referenceId' => $subrequest['referenceId'],
];
} else {
$contactId = '';
$parts = explode('-', $subrequest['referenceId']);
if (3 === count($parts)) {
[$contactId, $sfObject, $id] = $parts;
} elseif (2 === count($parts)) {
[$contactId, $sfObject] = $parts;
} elseif (4 === count($parts)) {
[$contactId, $sfObject, $empty, $campaignId] = $parts;
}
$response[] = [
'body' => [
'id' => 'SF'.$contactId,
'success' => true,
'errors' => [],
],
'httpHeaders' => [
'Location' => '/services/data/v38.0/sobjects/'.$sfObject.'/SF'.$contactId,
],
'httpStatusCode' => 201,
'referenceId' => $subrequest['referenceId'],
];
}
}
return ['compositeResponse' => $response];
}
/**
* @return array
*/
protected function getPersistedIntegrationEntities()
{
$entities = $this->persistedIntegrationEntities;
$this->persistedIntegrationEntities = [];
return $entities;
}
protected function getReturnedSfEntities()
{
$entities = $this->returnedSfEntities;
$this->returnedSfEntities = [];
return $entities;
}
protected function getMauticContacts()
{
$contacts = $this->mauticContacts;
$this->mauticContacts = [
'Contact' => [],
'Lead' => [],
];
return $contacts;
}
/**
* This function determines the parent of the class instance provided, and returns all properties of its parent.
* Inspired from https://github.com/sebastianbergmann/phpunit/issues/3888#issuecomment-559513371.
*
* Result structure:
* Array =>[
* 'parentPropertyName1' => 'value1'
* 'parentPropertyName2' => 'value2'
* ...
* ]
*
* @throws \ReflectionException
*/
private static function getParentPrivateProperties($instance): array
{
$reflectionClass = new \ReflectionClass($instance::class);
$parentReflectionClass = $reflectionClass->getParentClass();
$parentProperties = [];
foreach ($parentReflectionClass->getProperties() as $p) {
$p->setAccessible(true);
$parentProperties[$p->getName()] = $p->getValue($instance);
}
return $parentProperties;
}
}