Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Tests\Api;
use MauticPlugin\MauticCrmBundle\Api\ConnectwiseApi;
use MauticPlugin\MauticCrmBundle\Integration\ConnectwiseIntegration;
use MauticPlugin\MauticCrmBundle\Tests\Integration\DataGeneratorTrait;
#[\PHPUnit\Framework\Attributes\CoversClass(ConnectwiseApi::class)]
class ConnectwiseApiTest extends \PHPUnit\Framework\TestCase
{
use DataGeneratorTrait;
/**
* @throws \Mautic\PluginBundle\Exception\ApiErrorException
*/
#[\PHPUnit\Framework\Attributes\TestDox('Tests that fetchAllRecords loops until all records are obtained')]
public function testResultPagination(): void
{
$integration = $this->getMockBuilder(ConnectwiseIntegration::class)
->disableOriginalConstructor()
->onlyMethods(['makeRequest', 'getApiUrl'])
->getMock();
$page = 0;
$integration->expects($this->exactly(3))
->method('makeRequest')
->willReturnCallback(
function ($endpoint, $parameters) use (&$page) {
++$page;
// Page should be incremented 3 times by fetchAllRecords method
$this->assertEquals(['page' => $page, 'pageSize' => ConnectwiseIntegration::PAGESIZE], $parameters);
return $this->generateData(3);
}
);
$api = new ConnectwiseApi($integration);
$records = $api->fetchAllRecords('test');
$this->assertEquals($this->generatedRecords, $records);
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace MauticPlugin\MauticCrmBundle\Tests\Api;
use Mautic\PluginBundle\Exception\ApiErrorException;
use MauticPlugin\MauticCrmBundle\Api\HubspotApi;
use MauticPlugin\MauticCrmBundle\Integration\HubspotIntegration;
use PHPUnit\Framework\TestCase;
class HubspotApiTest extends TestCase
{
#[\PHPUnit\Framework\Attributes\TestDox('Test Hubspot api when the api-key is invalid')]
public function testHubspotWhenKeyIsInvalid(): void
{
$integration = $this->createMock(HubspotIntegration::class);
$message = 'The API key provided is invalid. View or manage your API key here: https://app-eu1.hubspot.com/l/api-key/';
$code = 401;
$response = [
'status' => 'error',
'message' => $message,
'correlationId' => '00000000-0000-0000-0000-000000000000',
'category' => 'INVALID_AUTHENTICATION',
'links' => [
'api key' => 'https://app-eu1.hubspot.com/l/api-key/',
],
];
$integration->expects(self::once())
->method('makeRequest')
->willReturn(
[
'error' => [
'code' => $code,
'message' => json_encode($response),
],
]
);
$integration->expects(self::once())
->method('getAuthenticationType')
->willReturn('crm');
$this->expectException(ApiErrorException::class);
$this->expectExceptionMessage($message);
$this->expectExceptionCode($code);
$api = new HubspotApi($integration);
$api->getLeadFields();
self::fail('ApiErrorException not thrown');
}
public function testHubspotWhenKeyIsInvalidIfOauth(): void
{
$integration = $this->createMock(HubspotIntegration::class);
$message = 'The API key provided is invalid. View or manage your API key here: https://app-eu1.hubspot.com/l/api-key/';
$response = [
'error' => 'error',
'code' => 402,
'message' => $message,
'correlationId' => '00000000-0000-0000-0000-000000000000',
'category' => 'INVALID_AUTHENTICATION',
'links' => [
'api key' => 'https://app-eu1.hubspot.com/l/api-key/',
],
];
$integration->expects(self::once())
->method('makeRequest')
->willReturn(['error' => $response]);
$integration->expects(self::once())
->method('getAuthenticationType')
->willReturn('oauth2');
$this->expectException(ApiErrorException::class);
$this->expectExceptionMessage($message);
$this->expectExceptionCode(0);
$api = new HubspotApi($integration);
$api->getLeadFields();
self::fail('ApiErrorException not thrown');
}
}

View File

@@ -0,0 +1,522 @@
<?php
declare(strict_types=1);
namespace MauticPlugin\MauticCrmBundle\Tests\Api;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Helper\CacheStorageHelper;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Exception\ApiErrorException;
use MauticPlugin\MauticCrmBundle\Api\SalesforceApi;
use MauticPlugin\MauticCrmBundle\Integration\SalesforceIntegration;
use Symfony\Contracts\Translation\TranslatorInterface;
#[\PHPUnit\Framework\Attributes\CoversClass(SalesforceApi::class)]
class SalesforceApiTest extends \PHPUnit\Framework\TestCase
{
#[\PHPUnit\Framework\Attributes\TestDox('Test that a locked record request is retried up to 3 times')]
public function testRecordLockedErrorIsRetriedThreeTimes(): void
{
$integration = $this->createMock(SalesforceIntegration::class);
$message = 'unable to obtain exclusive access to this record or 1 records: 70137000000Ugy3AAC';
$integration->expects($this->exactly(3))
->method('makeRequest')
->willReturn(
[
[
'errorCode' => 'UNABLE_TO_LOCK_ROW',
'message' => $message,
],
]
);
$api = new SalesforceApi($integration);
try {
$api->request('/test');
$this->fail('ApiErrorException not thrown');
} catch (ApiErrorException $exception) {
$this->assertEquals($message, $exception->getMessage());
}
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a locked record request is retried up to 3 times with last one being successful so no exception should be thrown')]
public function testRecordLockedErrorIsRetriedThreeTimesWithLastOneSuccessful(): void
{
$integration = $this->createMock(SalesforceIntegration::class);
$message = 'unable to obtain exclusive access to this record or 1 records: 70137000000Ugy3AAC';
$integration->expects($this->exactly(3))
->method('makeRequest')
->willReturnOnConsecutiveCalls(
[
[
'errorCode' => 'UNABLE_TO_LOCK_ROW',
'message' => $message,
],
],
[
[
'errorCode' => 'UNABLE_TO_LOCK_ROW',
'message' => $message,
],
],
[
[
'success' => true,
],
]
);
$api = new SalesforceApi($integration);
try {
$api->request('/test');
} catch (ApiErrorException) {
$this->fail('ApiErrorException thrown');
}
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a locked record request is retried 2 times with 3rd being successful')]
public function testRecordLockedErrorIsRetriedTwoTimesWithThirdSuccess(): void
{
$integration = $this->createMock(SalesforceIntegration::class);
$message = 'unable to obtain exclusive access to this record or 1 records: 70137000000Ugy3AAC';
$integration->expects($this->exactly(2))
->method('makeRequest')
->willReturnOnConsecutiveCalls(
[
[
'errorCode' => 'UNABLE_TO_LOCK_ROW',
'message' => $message,
],
],
[
[
['success' => true],
],
]
);
$api = new SalesforceApi($integration);
try {
$api->request('/test');
} catch (ApiErrorException) {
$this->fail('ApiErrorException should not have been thrown');
}
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a session expired should attempt a refresh before failing')]
public function testSessionExpiredIsRefreshed(): void
{
$integration = $this->createMock(SalesforceIntegration::class);
$message = '["errorCode":"INVALID_SESSION_ID","body":"Session expired or invalid"]';
$integration->expects($this->exactly(2))
->method('authCallback');
$integration->expects($this->exactly(2))
->method('makeRequest')
->willReturn(
[
[
'message' => $message,
],
]
);
$api = new SalesforceApi($integration);
try {
$api->request('/test');
$this->fail('ApiErrorException not thrown');
} catch (ApiErrorException $exception) {
$this->assertEquals($message, $exception->getMessage());
}
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a session expired should attempt a refresh but not throw an exception if successful on second request')]
public function testSessionExpiredIsRefreshedWithoutThrowingExceptionOnSecondRequestWithSuccess(): void
{
$integration = $this->createMock(SalesforceIntegration::class);
$message = 'Session expired';
$integration->expects($this->once())
->method('authCallback');
// Test again but both attempts should fail resulting in
$integration->expects($this->exactly(2))
->method('makeRequest')
->willReturnOnConsecutiveCalls(
[
[
'errorCode' => 'INVALID_SESSION_ID',
'message' => $message,
],
],
[
['success' => true],
]
);
$api = new SalesforceApi($integration);
try {
$api->request('/test');
} catch (ApiErrorException) {
$this->fail('ApiErrorException thrown');
}
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that an exception is thrown for all other errors')]
public function testErrorDoesNotRetryRequest(): void
{
$integration = $this->createMock(SalesforceIntegration::class);
$message = 'Fatal error';
$integration->expects($this->once())
->method('makeRequest')
->willReturn(
[
[
'errorCode' => 'FATAL_ERROR',
'message' => $message,
],
]
);
$api = new SalesforceApi($integration);
try {
$api->request('/test');
$this->fail('ApiErrorException not thrown');
} catch (ApiErrorException $exception) {
$this->assertEquals($message, $exception->getMessage());
}
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a backslash and a single quote are escaped for SF queries')]
public function testCompanyQueryIsEscapedCorrectly(): void
{
$integration = $this->getMockBuilder(SalesforceIntegration::class)
->disableOriginalConstructor()
->onlyMethods(['mergeConfigToFeatureSettings', 'makeRequest', 'getQueryUrl', 'getIntegrationSettings', 'getFieldsForQuery', 'getApiUrl'])
->getMock();
$integration->expects($this->once())
->method('mergeConfigToFeatureSettings')
->willReturn(
[
'objects' => [
'company',
],
]
);
$integration->expects($this->once())
->method('makeRequest')
->willReturnCallback(
function ($url, $parameters = [], $method = 'GET', $settings = []): void {
$this->assertEquals(
[
'q' => 'select Id from Account where Name = \'Some\\\\thing E\\\'lse\' and BillingCountry = \'Some\\\\Where E\\\'lse\' and BillingCity = \'Some\\\\Where E\\\'lse\' and BillingState = \'Some\\\\Where E\\\'lse\'',
],
$parameters
);
}
);
$api = new SalesforceApi($integration);
$api->getCompany(
[
'company' => [
'BillingCountry' => 'Some\\Where E\'lse',
'BillingCity' => 'Some\\Where E\'lse',
'BillingState' => 'Some\\Where E\'lse',
'Name' => 'Some\\thing E\'lse',
],
]
);
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a backslash and an html entity of single quote are escaped for SF queries')]
public function testCompanyQueryWithHtmlEntitiesIsEscapedCorrectly(): void
{
$integration = $this->getMockBuilder(SalesforceIntegration::class)
->disableOriginalConstructor()
->onlyMethods(['mergeConfigToFeatureSettings', 'makeRequest', 'getQueryUrl', 'getIntegrationSettings', 'getFieldsForQuery', 'getApiUrl'])
->getMock();
$integration->expects($this->once())
->method('mergeConfigToFeatureSettings')
->willReturn(
[
'objects' => [
'company',
],
]
);
$integration->expects($this->once())
->method('makeRequest')
->willReturnCallback(
function ($url, $parameters = [], $method = 'GET', $settings = []): void {
$this->assertEquals(
[
'q' => 'select Id from Account where Name = \'Some\\\\thing\\\' E\\\'lse\' and BillingCountry = \'Some\\\\Where\\\' E\\\'lse\' and BillingCity = \'Some\\\\Where\\\' E\\\'lse\' and BillingState = \'Some\\\\Where\\\' E\\\'lse\'',
],
$parameters
);
}
);
$api = new SalesforceApi($integration);
$api->getCompany(
[
'company' => [
'BillingCountry' => 'Some\\Where&#39; E\'lse',
'BillingCity' => 'Some\\Where&#39; E\'lse',
'BillingState' => 'Some\\Where&#39; E\'lse',
'Name' => 'Some\\thing&#39; E\'lse',
],
]
);
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a backslash and a single quote are escaped for SF queries')]
public function testContactQueryIsEscapedCorrectly(): void
{
$integration = $this->getMockBuilder(SalesforceIntegration::class)
->disableOriginalConstructor()
->onlyMethods(['mergeConfigToFeatureSettings', 'makeRequest', 'getQueryUrl', 'getIntegrationSettings', 'getFieldsForQuery', 'getApiUrl'])
->getMock();
$integration->expects($this->once())
->method('mergeConfigToFeatureSettings')
->willReturn(
[
'objects' => [
'Contact',
],
]
);
$integration->expects($this->once())
->method('getFieldsForQuery')
->willReturn([]);
$integration->expects($this->once())
->method('makeRequest')
->willReturnCallback(
function ($url, $parameters = [], $method = 'GET', $settings = []): void {
$this->assertEquals(
[
'q' => 'select Id from Contact where email = \'con\\\\tact\\\'email@email.com\'',
],
$parameters
);
}
);
$integration->method('getFieldsForQuery')
->willReturn([]);
$api = new SalesforceApi($integration);
$api->getPerson([
'Contact' => [
'Email' => 'con\\tact\'email@email.com',
],
]);
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a backslash and a single quote are escaped for SF queries')]
public function testLeadQueryIsEscapedCorrectly(): void
{
$integration = $this->getMockBuilder(SalesforceIntegration::class)
->disableOriginalConstructor()
->onlyMethods(['mergeConfigToFeatureSettings', 'makeRequest', 'getQueryUrl', 'getIntegrationSettings', 'getFieldsForQuery', 'getApiUrl'])
->getMock();
$integration->expects($this->once())
->method('mergeConfigToFeatureSettings')
->willReturn(
[
'objects' => [
'Lead',
],
]
);
$integration->expects($this->once())
->method('getFieldsForQuery')
->willReturn([]);
$integration->expects($this->once())
->method('makeRequest')
->willReturnCallback(
function ($url, $parameters = [], $method = 'GET', $settings = []): void {
$this->assertEquals(
[
'q' => 'select Id from Lead where email = \'con\\\\tact\\\'email@email.com\' and ConvertedContactId = NULL',
],
$parameters
);
}
);
$integration->method('getFieldsForQuery')
->willReturn([]);
$api = new SalesforceApi($integration);
$api->getPerson([
'Lead' => [
'Email' => 'con\\tact\'email@email.com',
],
]);
}
public function testHandleDeletesGracefullyWithHasOptedOutOfEmailAsMissingField(): void
{
/**
* @phpstan-ignore-next-line
*/
$cache = $this->createMock(CacheStorageHelper::class);
$cache
->method('get')
->withAnyParameters()
->willReturn('2019-05-22 19:36:30');
$integration = $this->getMockBuilder(SalesforceIntegration::class)
->disableOriginalConstructor()
->onlyMethods([
'mergeConfigToFeatureSettings',
'makeRequest',
'getQueryUrl',
'getIntegrationSettings',
'getFieldsForQuery',
'getApiUrl',
'getCache',
'getTranslator',
'upsertUnreadAdminsNotification',
])
->getMock();
$integration
->expects($this->atLeastOnce())
->method('getCache')
->willReturn($cache);
$integration->method('getFieldsForQuery')
->with('Lead')
->willReturn(['firstname', 'lastname', 'HasOptedOutOfEmail']);
$translator = $this->createMock(TranslatorInterface::class);
$integration->method('getTranslator')->willReturn($translator);
$this->expectException(ApiErrorException::class);
$integration->expects($this->atLeastOnce())
->method('makeRequest')
->willReturn(
[
[
'errorCode' => 'FATAL_ERROR',
'message' => "ERROR at Row1\nNo such column 'HasOptedOutOfEmail' on entity 'Lead'",
],
]
);
$params['start'] = '2019-05-22 19:36:30';
$params['end'] = '2030-05-22 19:36:30';
$api = new SalesforceApi($integration);
self::assertEquals('2019-05-22 19:36:30', $api->getOrganizationCreatedDate());
$api->getLeads($params, 'Lead');
}
public function testHandleDeletesGracefully(): void
{
/**
* @phpstan-ignore-next-line
*/
$cache = $this->createMock(CacheStorageHelper::class);
$cache
->method('get')
->withAnyParameters()
->willReturn('2019-05-22 19:36:30');
$integration = $this->getMockBuilder(SalesforceIntegration::class)
->disableOriginalConstructor()
->onlyMethods([
'mergeConfigToFeatureSettings',
'makeRequest',
'getQueryUrl',
'getIntegrationSettings',
'getFieldsForQuery',
'getApiUrl',
'getCache',
'getTranslator',
'upsertUnreadAdminsNotification',
'getEntityManager',
])
->getMock();
$integration
->expects($this->atLeastOnce())
->method('getCache')
->willReturn($cache);
$integration->method('getFieldsForQuery')
->with('Lead')
->willReturn(['firstname', 'lastname', 'extraField']);
$integration->expects($this->never())->method('upsertUnreadAdminsNotification');
$entityManager = $this->createMock(EntityManager::class);
$entity = $this
->getMockBuilder(Integration::class)
->disableOriginalConstructor()
->onlyMethods(['getFeatureSettings', 'setFeatureSettings'])
->getMock();
$integration->method('getEntityManager')->willReturn($entityManager);
$integration->method('getIntegrationSettings')->willReturn($entity);
$entity->method('getFeatureSettings')->willReturn(['leadFields' => ['extraField__Lead' => '']]);
$this->expectException(ApiErrorException::class);
$integration->expects($this->atLeastOnce())
->method('makeRequest')
->willReturn(
[
[
'errorCode' => 'FATAL_ERROR',
'message' => "ERROR at Row1\nNo such column 'extraField' on entity 'Lead'",
],
]
);
$params['start'] = '2019-05-22 19:36:30';
$params['end'] = '2030-05-22 19:36:30';
$api = new SalesforceApi($integration);
self::assertEquals('2019-05-22 19:36:30', $api->getOrganizationCreatedDate());
$api->getLeads($params, 'Lead');
}
}

View File

@@ -0,0 +1,227 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Tests\Api\Zoho;
use MauticPlugin\MauticCrmBundle\Api\Zoho\Exception\MatchingKeyNotFoundException;
use MauticPlugin\MauticCrmBundle\Api\Zoho\Mapper;
#[\PHPUnit\Framework\Attributes\CoversClass(Mapper::class)]
class MapperTest extends \PHPUnit\Framework\TestCase
{
/**
* @var array
*/
protected $availableFields = [
'Leads' => [
'Company' => [
'type' => 'string',
'label' => 'Company',
'api_name' => 'Company',
'required' => true,
],
'FirstName' => [
'type' => 'string',
'label' => 'First Name',
'api_name' => 'First Name',
'required' => false,
],
'LastName' => [
'type' => 'string',
'label' => 'Last Name',
'api_name' => 'Last Name',
'required' => true,
],
'Email' => [
'type' => 'string',
'label' => 'Email',
'api_name' => 'Email',
'required' => false,
],
],
];
/**
* @var array
*/
protected $mappedFields = [
'Company' => 'company',
'Email' => 'email',
'Country' => 'country',
'FirstName' => 'firstname',
'LastName' => 'lastname',
];
/**
* @var array
*/
protected $contacts = [
[
'firstname' => 'FirstName1',
'lastname' => 'LastName1',
'email' => 'zoho1@email.com',
'integration_entity' => 'Leads',
'integration_entity_id' => 'abc',
'internal_entity' => 'lead',
'internal_entity_id' => 1,
],
[
'firstname' => 'FirstName2',
'lastname' => 'LastName2',
'email' => 'zoho2@email.com',
'integration_entity' => 'Leads',
'integration_entity_id' => 'def',
'internal_entity' => 'lead',
'internal_entity_id' => 2,
],
[
'firstname' => 'FirstName3',
'lastname' => 'LastName3',
'email' => 'zoho3@email.com',
'integration_entity' => 'Leads',
'integration_entity_id' => 'ghi',
'internal_entity' => 'lead',
'internal_entity_id' => 3,
],
];
#[\PHPUnit\Framework\Attributes\TestDox('Test that array is generated according to the mapping')]
public function testArrayIsGeneratedBasedOnMapping(): void
{
$mapper = new Mapper($this->availableFields);
$mapper->setObject('Leads');
foreach ($this->contacts as $contact) {
$mapper->setMappedFields($this->mappedFields)
->setContact($contact)
->map($contact['internal_entity_id']);
}
$expected = [
[
'Email' => 'zoho1@email.com',
'First Name' => 'FirstName1',
'Last Name' => 'LastName1',
],
[
'Email' => 'zoho2@email.com',
'First Name' => 'FirstName2',
'Last Name' => 'LastName2',
],
[
'Email' => 'zoho3@email.com',
'First Name' => 'FirstName3',
'Last Name' => 'LastName3',
],
];
$this->assertEquals($expected, $mapper->getArray());
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that contacts do not inherit previous contact information')]
public function testContactDoesNotInheritPreviousContactData(): void
{
$mapper = new Mapper($this->availableFields);
$mapper->setObject('Leads');
$contacts = $this->contacts;
$contacts[1]['firstname'] = null;
foreach ($contacts as $contact) {
$mapper->setMappedFields($this->mappedFields)
->setContact($contact)
->map($contact['internal_entity_id'], $contact['integration_entity_id']);
}
$expected = [
[
'id' => 'abc',
'Email' => 'zoho1@email.com',
'First Name' => 'FirstName1',
'Last Name' => 'LastName1',
],
[
'id' => 'def',
'Email' => 'zoho2@email.com',
'Last Name' => 'LastName2',
],
[
'id' => 'ghi',
'Email' => 'zoho3@email.com',
'First Name' => 'FirstName3',
'Last Name' => 'LastName3',
],
];
$this->assertEquals($expected, $mapper->getArray());
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that array is generated according to the mapping')]
public function testArrayIsGeneratedBasedOnMappingWithId(): void
{
$mapper = new Mapper($this->availableFields);
$mapper->setObject('Leads');
foreach ($this->contacts as $contact) {
$mapper->setMappedFields($this->mappedFields)
->setContact($contact)
->map($contact['internal_entity_id'], $contact['integration_entity_id']);
}
$expected = [
[
'id' => 'abc',
'Email' => 'zoho1@email.com',
'First Name' => 'FirstName1',
'Last Name' => 'LastName1',
],
[
'id' => 'def',
'First Name' => 'FirstName2',
'Email' => 'zoho2@email.com',
'Last Name' => 'LastName2',
],
[
'id' => 'ghi',
'Email' => 'zoho3@email.com',
'First Name' => 'FirstName3',
'Last Name' => 'LastName3',
],
];
$this->assertEquals($expected, $mapper->getArray());
}
#[\PHPUnit\Framework\Attributes\TestDox('Test asking for a key returns the correct contact')]
public function testThatContactIdMatchesGivenKey(): void
{
$mapper = new Mapper($this->availableFields);
$mapper->setObject('Leads');
foreach ($this->contacts as $contact) {
$mapper->setMappedFields($this->mappedFields)
->setContact($contact)
->map($contact['internal_entity_id'], $contact['integration_entity_id']);
}
$this->assertEquals(3, $mapper->getContactIdByKey(2));
$this->assertEquals(2, $mapper->getContactIdByKey(1));
$this->assertEquals(1, $mapper->getContactIdByKey(0));
}
#[\PHPUnit\Framework\Attributes\TestDox("Test asking for a key that doesn't exist throws exception")]
public function testThatExceptionIsThrownIfKeyNotFound(): void
{
$this->expectException(MatchingKeyNotFoundException::class);
$mapper = new Mapper($this->availableFields);
$mapper->setObject('Leads');
foreach ($this->contacts as $contact) {
$mapper->setMappedFields($this->mappedFields)
->setContact($contact)
->map($contact['internal_entity_id'], $contact['integration_entity_id']);
}
$mapper->getContactIdByKey(4);
}
}