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,247 @@
<?php
namespace Mautic\LeadBundle\Tests\Model;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\EmailBundle\Helper\EmailValidator;
use Mautic\LeadBundle\Deduplicate\CompanyDeduper;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Model\CompanyModel;
use Mautic\LeadBundle\Model\FieldModel;
use Symfony\Component\HttpFoundation\Session\Session;
#[\PHPUnit\Framework\Attributes\CoversClass(\Mautic\CoreBundle\Helper\AbstractFormFieldHelper::class)]
class CompanyModelTest extends \PHPUnit\Framework\TestCase
{
/**
* @var FieldModel|\PHPUnit\Framework\MockObject\MockObject
*/
private \PHPUnit\Framework\MockObject\MockObject $leadFieldModel;
/**
* @var \PHPUnit\Framework\MockObject\MockObject|Session
*/
private \PHPUnit\Framework\MockObject\MockObject $session;
/**
* @var EmailValidator|\PHPUnit\Framework\MockObject\MockObject
*/
private \PHPUnit\Framework\MockObject\MockObject $emailValidator;
/**
* @var CompanyDeduper|\PHPUnit\Framework\MockObject\MockObject
*/
private \PHPUnit\Framework\MockObject\MockObject $companyDeduper;
public function setUp(): void
{
$this->leadFieldModel = $this->createMock(FieldModel::class);
$this->session = $this->createMock(Session::class);
$this->emailValidator = $this->createMock(EmailValidator::class);
$this->companyDeduper = $this->createMock(CompanyDeduper::class);
}
#[\PHPUnit\Framework\Attributes\TestDox('Ensure that an array value is flattened before saving')]
public function testArrayValueIsFlattenedBeforeSave(): void
{
/** @var CompanyModel $companyModel */
$companyModel = $this->getMockBuilder(CompanyModel::class)
->disableOriginalConstructor()
->onlyMethods([])
->getMock();
$company = new Company();
$company->setFields(
[
'core' => [
'multiselect' => [
'type' => 'multiselect',
'alias' => 'multiselect',
'value' => 'abc|123',
],
],
]
);
$companyModel->setFieldValues($company, ['multiselect' => ['abc', 'def']]);
$updatedFields = $company->getUpdatedFields();
$this->assertEquals(
[
'multiselect' => 'abc|def',
],
$updatedFields
);
}
public function testImportCompanySkipIfExistsTrue(): void
{
$companyModel = $this->getCompanyModelForImport();
$duplicatedCompany = $this->createMock(Company::class);
$duplicatedCompany->method('getProfileFields')->willReturn(['companyfield'=> 'xxx']);
$companyDeduper = $this->getCompanyDeduperForImport($duplicatedCompany);
$this->setProperty($companyModel, CompanyModel::class, 'companyDeduper', $companyDeduper);
$duplicatedCompany->expects($this->exactly(0))->method('addUpdatedField');
$companyModel->importCompany([], [], null, false, true);
}
public function testImportCompanySkipIfExistsFalse(): void
{
$companyModel = $this->getCompanyModelForImport();
$duplicatedCompany = $this->createMock(Company::class);
$duplicatedCompany->method('getProfileFields')->willReturn(['companyfield'=> 'xxx']);
$companyDeduper = $this->getCompanyDeduperForImport($duplicatedCompany);
$this->setProperty($companyModel, CompanyModel::class, 'companyDeduper', $companyDeduper);
$duplicatedCompany->expects($this->once())->method('addUpdatedField');
$companyModel->importCompany([], [], null, false, false);
}
public function testImportHtmlFieldsForCompany(): void
{
$companyModel = $this->getMockBuilder(CompanyModel::class)
->disableOriginalConstructor()
->onlyMethods(['fetchCompanyFields', 'getFieldData'])
->getMock();
$companyModel->method('fetchCompanyFields')->willReturn(
[
[
'alias' => 'companyfield',
'defaultValue' => '',
'type' => 'text',
],
[
'alias' => 'custom_html_field',
'defaultValue' => '',
'type' => 'html',
],
]
);
$data = ['companyfield' => 'test', 'custom_html_field' => '<p>html content</p>'];
$companyModel->method('getFieldData')
->willReturn($data);
$this->setSecurity($companyModel);
$companyModel->method('getFieldData')->willReturn($data);
$duplicatedCompany = $this->createMock(Company::class);
$duplicatedCompany->method('getProfileFields')->willReturn($data);
$companyDeduper = $this->getCompanyDeduperForImport($duplicatedCompany);
$this->setProperty($companyModel, CompanyModel::class, 'companyDeduper', $companyDeduper);
$duplicatedCompany->expects($this->exactly(2))->method('addUpdatedField');
$companyModel->importCompany([], [], null, false, false);
}
private function getCompanyModelForImport()
{
$companyModel = $this->getMockBuilder(CompanyModel::class)
->disableOriginalConstructor()
->onlyMethods(['fetchCompanyFields', 'getFieldData'])
->getMock();
$companyModel->method('fetchCompanyFields')->willReturn(
[
[
'alias' => 'companyfield',
'defaultValue' => '',
'type' => 'text',
],
]
);
$companyModel->method('getFieldData')->willReturn(['companyfield' => 'xxx']);
$this->setSecurity($companyModel);
return $companyModel;
}
private function getCompanyDeduperForImport(Company $duplicatedCompany)
{
$companyDeduper = $this->createMock(CompanyDeduper::class);
$companyDeduper->method('checkForDuplicateCompanies')->willReturn([$duplicatedCompany]);
return $companyDeduper;
}
/**
* Set protected property to an object.
*
* @param object $object
* @param string $class
* @param string $property
* @param mixed $value
*/
private function setProperty($object, $class, $property, $value): void
{
$reflectedProp = new \ReflectionProperty($class, $property);
$reflectedProp->setAccessible(true);
$reflectedProp->setValue($object, $value);
}
public function testExtractCompanyDataFromImport(): void
{
/** @var CompanyModel $companyModel */
$companyModel = $this->getMockBuilder(CompanyModel::class)
->disableOriginalConstructor()
->onlyMethods(['fetchCompanyFields'])
->getMock();
$companyModel->method('fetchCompanyFields')
->willReturn([
['alias' => 'companyname'],
['alias' => 'companyemail'],
['alias' => 'companyindustry'],
]);
$fields = [
'email' => 'i_contact_email',
'companyemail' => 'i_company_email',
'company' => 'i_company_name',
'companyindustry' => 'i_company_industry',
];
$data= [
'i_contact_email' => 'PennyKMoore@dayrep.com',
'i_company_email' => 'turbochicken@dayrep.com',
'i_company_name' => 'Turbo chicken',
'i_company_industry' => 'Biotechnology',
];
[$companyFields, $companyData] = $companyModel->extractCompanyDataFromImport($fields, $data);
$expectedCompanyFields = [
'companyemail' => 'i_company_email',
'companyindustry' => 'i_company_industry',
'companyname' => 'i_company_name',
];
$expectedCompanyData = [
'i_company_email' => 'turbochicken@dayrep.com',
'i_company_industry' => 'Biotechnology',
'i_company_name' => 'Turbo chicken',
];
$this->assertSame($expectedCompanyFields, $companyFields);
$this->assertSame($expectedCompanyData, $companyData);
}
private function setSecurity(CompanyModel $companyModel): void
{
$security = $this->createMock(CorePermissions::class);
$security->method('hasEntityAccess')
->willReturn(true);
$security->method('isGranted')
->willReturn(true);
$reflection = new \ReflectionClass($companyModel);
$property = $reflection->getProperty('security');
$property->setAccessible(true);
$property->setValue($companyModel, $security);
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace Mautic\LeadBundle\Tests\Model;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\FormBundle\Entity\Field;
use Mautic\LeadBundle\Model\CompanyReportData;
use Mautic\LeadBundle\Model\FieldModel;
use Mautic\ReportBundle\Event\ReportGeneratorEvent;
use Symfony\Contracts\Translation\TranslatorInterface;
#[\PHPUnit\Framework\Attributes\CoversClass(CompanyReportData::class)]
class CompanyReportDataTest extends \PHPUnit\Framework\TestCase
{
/**
* @var TranslatorInterface
*/
private \PHPUnit\Framework\MockObject\MockObject $translator;
protected function setUp(): void
{
$this->translator = $this->createMock(Translator::class);
$this->translator->method('trans')
->willReturnCallback(
fn ($key) => $key
);
}
public function testGetCompanyData(): void
{
$fieldModelMock = $this->createMock(FieldModel::class);
$field1 = new Field();
$field1->setType('boolean');
$field1->setAlias('boolField');
$field1->setLabel('boolFieldLabel');
$field2 = new Field();
$field2->setType('email');
$field2->setAlias('emailField');
$field2->setLabel('emailFieldLabel');
$fields = [
$field1,
$field2,
];
$fieldModelMock->expects($this->once())
->method('getEntities')
->willReturn($fields);
$companyReportData = new CompanyReportData($fieldModelMock, $this->translator);
$result = $companyReportData->getCompanyData();
$expected = [
'comp.id' => [
'alias' => 'comp_id',
'label' => 'mautic.lead.report.company.company_id',
'type' => 'int',
'link' => 'mautic_company_action',
],
'companies_lead.is_primary' => [
'label' => 'mautic.lead.report.company.is_primary',
'type' => 'bool',
],
'companies_lead.date_added' => [
'label' => 'mautic.lead.report.company.date_added',
'type' => 'datetime',
],
'comp.boolField' => [
'label' => 'mautic.report.field.company.label',
'type' => 'bool',
],
'comp.emailField' => [
'label' => 'mautic.report.field.company.label',
'type' => 'email',
],
];
$this->assertSame($expected, $result);
}
public function testEventHasCompanyColumns(): void
{
$fieldModelMock = $this->createMock(FieldModel::class);
$eventMock = $this->createMock(ReportGeneratorEvent::class);
$field = new Field();
$field->setType('email');
$field->setAlias('email');
$field->setLabel('Email');
$fieldModelMock->expects($this->once())
->method('getEntities')
->willReturn([$field]);
$eventMock->expects($this->once())
->method('hasColumn')
->with('comp.id')
->willReturn(true);
$companyReportData = new CompanyReportData($fieldModelMock, $this->translator);
$result = $companyReportData->eventHasCompanyColumns($eventMock);
$this->assertTrue($result);
}
public function testEventDoesNotHaveCompanyColumns(): void
{
$fieldModelMock = $this->createMock(FieldModel::class);
$eventMock = $this->createMock(ReportGeneratorEvent::class);
$field = new Field();
$field->setType('email');
$field->setAlias('email');
$field->setLabel('Email');
$fieldModelMock->expects($this->once())
->method('getEntities')
->willReturn([$field]);
$eventMock->expects($this->any())
->method('hasColumn')
->willReturn(false);
$companyReportData = new CompanyReportData($fieldModelMock, $this->translator);
$result = $companyReportData->eventHasCompanyColumns($eventMock);
$this->assertFalse($result);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Mautic\LeadBundle\Tests\Model;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Model\FieldModel;
final class FieldModelCustomFieldsFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testGetLeadFields(): void
{
/** @var FieldModel $fieldModel */
$fieldModel = $this->getContainer()->get('mautic.lead.model.field');
$fields = $fieldModel->getLeadFields();
$expected = count(FieldModel::$coreFields);
$this->assertGreaterThanOrEqual($expected, count($fields));
}
public function testLeadFieldCustomFields(): void
{
/** @var FieldModel $fieldModel */
$fieldModel = $this->getContainer()->get('mautic.lead.model.field');
$fields = $fieldModel->getLeadFieldCustomFields();
$this->assertEmpty($fields, 'There are no Custom Fields.');
// Add field.
$leadField = new LeadField();
$leadField->setName('Test Field')
->setAlias('test_field')
->setType('string')
->setObject('lead');
$fieldModel->saveEntity($leadField);
$fields = $fieldModel->getLeadFieldCustomFields();
$this->assertEquals(1, count($fields));
}
public function testGetLeadCustomFieldsSchemaDetails(): void
{
/** @var FieldModel $fieldModel */
$fieldModel = $this->getContainer()->get('mautic.lead.model.field');
// Add field.
$leadField = new LeadField();
$leadField->setName('Test Field')
->setAlias('test_field')
->setType('string')
->setObject('lead');
$fieldModel->saveEntity($leadField);
$schemas = $fieldModel->getLeadFieldCustomFieldSchemaDetails();
$this->assertEquals(1, count($schemas));
}
}

View File

@@ -0,0 +1,393 @@
<?php
namespace Mautic\LeadBundle\Tests\Model;
use Doctrine\DBAL\Logging\SQLLogger;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CoreBundle\Doctrine\Helper\ColumnSchemaHelper;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Entity\LeadFieldRepository;
use Mautic\LeadBundle\Field\CustomFieldColumn;
use Mautic\LeadBundle\Field\Dispatcher\FieldSaveDispatcher;
use Mautic\LeadBundle\Field\FieldList;
use Mautic\LeadBundle\Field\LeadFieldDeleter;
use Mautic\LeadBundle\Field\LeadFieldSaver;
use Mautic\LeadBundle\Model\FieldModel;
use Mautic\LeadBundle\Model\ListModel;
use PHPUnit\Framework\Assert;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class FieldModelTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
/**
* @param array<string, mixed[]> $filters
*/
#[\PHPUnit\Framework\Attributes\DataProvider('dataForGetFieldsProperties')]
public function testGetFieldsProperties(array $filters, int $expectedCount): void
{
/** @var FieldModel $fieldModel */
$fieldModel = self::getContainer()->get('mautic.lead.model.field');
// Create an unpublished lead field.
$field = new LeadField();
$field->setName('Test Unpublished Field')
->setAlias('test_unpublished_field')
->setType('string')
->setObject('lead')
->setIsPublished(false);
$fieldModel->saveEntity($field);
$fields = $fieldModel->getFieldsProperties($filters);
$this->assertCount($expectedCount, $fields);
}
/**
* @return iterable<string, mixed[]>
*/
public static function dataForGetFieldsProperties(): iterable
{
// When mautic is installed the total number of fields are 42.
yield 'All fields' => [
// Filters
[],
// Expected count
44,
];
yield 'Contact fields' => [
// Filters
['object' => 'lead'],
// Expected count
29,
];
yield 'Company fields' => [
// Filters
['object' => 'company'],
// Expected count
15,
];
yield 'Text fields' => [
// Filters
['type' => 'text'],
// Expected count
20,
];
yield 'Unpublished fields' => [
// Filters
['isPublished' => false],
// Expected count
1,
];
}
public function testSingleContactFieldIsCreatedAndDeleted(): void
{
$fieldModel = static::getContainer()->get('mautic.lead.model.field');
$field = new LeadField();
$field->setName('Test Field')
->setAlias('test_field')
->setType('string')
->setObject('lead');
$fieldModel->saveEntity($field);
$fieldModel->deleteEntity($field);
$this->assertCount(0, $this->getColumns('leads', $field->getAlias()));
}
public function testSingleCompanyFieldIsCreatedAndDeleted(): void
{
$fieldModel = static::getContainer()->get('mautic.lead.model.field');
$field = new LeadField();
$field->setName('Test Field')
->setAlias('test_field')
->setType('string')
->setObject('company');
$fieldModel->saveEntity($field);
$fieldModel->deleteEntity($field);
$this->assertCount(0, $this->getColumns('companies', $field->getAlias()));
}
public function testMultipleFieldsAreCreatedAndDeleted(): void
{
$fieldModel = static::getContainer()->get('mautic.lead.model.field');
$leadField = new LeadField();
$leadField->setName('Test Field')
->setAlias('test_field')
->setType('string')
->setObject('lead');
$leadField2 = new LeadField();
$leadField2->setName('Test Field')
->setAlias('test_field2')
->setType('string')
->setObject('lead');
$companyField = new LeadField();
$companyField->setName('Test Field')
->setAlias('test_field')
->setType('string')
->setObject('company');
$companyField2 = new LeadField();
$companyField2->setName('Test Field')
->setAlias('test_field2')
->setType('string')
->setObject('company');
$fieldModel->saveEntities([$leadField, $leadField2, $companyField, $companyField2]);
$this->assertCount(1, $this->getColumns('leads', $leadField->getAlias()));
$this->assertCount(1, $this->getColumns('leads', $leadField2->getAlias()));
$this->assertCount(1, $this->getColumns('companies', $companyField->getAlias()));
$this->assertCount(1, $this->getColumns('companies', $companyField2->getAlias()));
$fieldModel->deleteEntities([$leadField->getId(), $leadField2->getId(), $companyField->getId(), $companyField2->getId()]);
$this->assertCount(0, $this->getColumns('leads', $leadField->getAlias()));
$this->assertCount(0, $this->getColumns('leads', $leadField2->getAlias()));
$this->assertCount(0, $this->getColumns('companies', $companyField->getAlias()));
$this->assertCount(0, $this->getColumns('companies', $companyField2->getAlias()));
}
public function testGenerateUniqueFieldAlias(): void
{
$repoMock = $this->getMockBuilder(LeadFieldRepository::class)
->disableOriginalConstructor()
->onlyMethods(['__call']) // only __call can intercept dynamic methods
->getMock();
$repoMock->method('__call')
->with('findOneByAlias', $this->anything())
->willReturnCallback(function ($method, $args) {
$alias = $args[0];
// Simulate alias and alias_1 are taken, alias_2 is available
if (in_array($alias, ['alias', 'alias_1'], true)) {
return new LeadField();
}
return null;
});
// Anonymous subclass that overrides getRepository
$fieldModel = new FieldModel(
$this->createMock(ColumnSchemaHelper::class),
$this->createMock(ListModel::class),
$this->createMock(CustomFieldColumn::class),
$this->createMock(FieldSaveDispatcher::class),
$repoMock,
$this->createMock(FieldList::class),
$this->createMock(LeadFieldSaver::class),
$this->createMock(LeadFieldDeleter::class),
$this->createMock(EntityManagerInterface::class),
$this->createMock(CorePermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->createMock(UrlGeneratorInterface::class),
$this->createMock(Translator::class),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class),
);
$result = $fieldModel->generateUniqueFieldAlias('alias');
$this->assertEquals('alias_2', $result);
}
public function testIsUsedField(): void
{
$leadField = new LeadField();
$columnSchemaHelper = $this->createMock(ColumnSchemaHelper::class);
$leadListModel = $this->createMock(ListModel::class);
$customFieldColumn = $this->createMock(CustomFieldColumn::class);
$fieldSaveDispatcher = $this->createMock(FieldSaveDispatcher::class);
$leadFieldRepository = $this->createMock(LeadFieldRepository::class);
$fieldList = $this->createMock(FieldList::class);
$leadFieldSaver = $this->createMock(LeadFieldSaver::class);
$leadFieldDeleter = $this->createMock(LeadFieldDeleter::class);
$leadListModel->expects($this->once())
->method('isFieldUsed')
->with($leadField)
->willReturn(true);
$model = new FieldModel(
$columnSchemaHelper,
$leadListModel,
$customFieldColumn,
$fieldSaveDispatcher,
$leadFieldRepository,
$fieldList,
$leadFieldSaver,
$leadFieldDeleter,
$this->createMock(EntityManagerInterface::class),
$this->createMock(CorePermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->createMock(UrlGeneratorInterface::class),
$this->createMock(Translator::class),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class),
);
$this->assertTrue($model->isUsedField($leadField));
}
public function testUniqueIdentifierIndexToggleForContacts(): void
{
// Log queries so we can detect if alter queries were executed
/** $stack */
$stack = new class implements SQLLogger { /** @phpstan-ignore-line SQLLogger is deprecated */
/** @var array<mixed> */
private array $indexQueries = [];
public function startQuery($sql, ?array $params = null, ?array $types = null)
{
if (false !== stripos($sql, 'create index')) {
$this->indexQueries[] = $sql;
}
if (false !== stripos($sql, 'drop index')) {
$this->indexQueries[] = $sql;
}
}
public function stopQuery()
{
// not used
}
/**
* @return array<mixed>
*/
public function getIndexQueries(): array
{
return $this->indexQueries;
}
public function resetQueries(): void
{
$this->indexQueries = [];
}
};
$this->connection->getConfiguration()->setSQLLogger($stack); /** @phpstan-ignore-line SQLLogger is deprecated */
$fieldModel = $this->getContainer()->get('mautic.lead.model.field');
// Ensure the index exists
$emailField = $fieldModel->getEntityByAlias('email');
$fieldModel->saveEntity($emailField);
$columns = $this->getUniqueIdentifierIndexColumns('leads');
Assert::assertCount(1, $columns);
Assert::assertEquals('email', $columns[0]['COLUMN_NAME']);
$stack->resetQueries();
// Test updating the index
$ui1Field = new LeadField();
$ui1Field->setName('UI1')
->setAlias('ui1')
->setType('string')
->setObject('lead')
->setIsUniqueIdentifier(true);
$fieldModel->saveEntity($ui1Field);
$columns = $this->getUniqueIdentifierIndexColumns('leads');
Assert::assertCount(2, $columns);
Assert::assertEquals('email', $columns[0]['COLUMN_NAME']);
Assert::assertEquals('ui1', $columns[1]['COLUMN_NAME']);
$alteredIndexes = $stack->getIndexQueries();
Assert::assertCount(3, $alteredIndexes);
Assert::assertEquals(sprintf('DROP INDEX %1$sunique_identifier_search ON %1$sleads', MAUTIC_TABLE_PREFIX), $alteredIndexes[0]);
Assert::assertEquals(sprintf('CREATE INDEX %1$sunique_identifier_search ON %1$sleads (email, ui1)', MAUTIC_TABLE_PREFIX), $alteredIndexes[1]);
Assert::assertEquals(sprintf('CREATE INDEX %1$sui1_search ON %1$sleads (ui1)', MAUTIC_TABLE_PREFIX), $alteredIndexes[2]);
$stack->resetQueries();
// Test only the first 3 columns are used for the index
$ui2Field = new LeadField();
$ui2Field->setName('UI2')
->setAlias('ui2')
->setType('string')
->setObject('lead')
->setIsUniqueIdentifier(true);
$ui3Field = new LeadField();
$ui3Field->setName('UI3')
->setAlias('ui3')
->setType('string')
->setObject('lead')
->setIsUniqueIdentifier(true);
$fieldModel->saveEntities([$ui2Field, $ui3Field]);
$columns = $this->getUniqueIdentifierIndexColumns('leads');
Assert::assertCount(3, $columns);
Assert::assertEquals('email', $columns[0]['COLUMN_NAME']);
Assert::assertEquals('ui1', $columns[1]['COLUMN_NAME']);
Assert::assertEquals('ui2', $columns[2]['COLUMN_NAME']);
$alteredIndexes = $stack->getIndexQueries();
Assert::assertCount(4, $alteredIndexes);
Assert::assertEquals(sprintf('DROP INDEX %1$sunique_identifier_search ON %1$sleads', MAUTIC_TABLE_PREFIX), $alteredIndexes[0]);
Assert::assertEquals(
sprintf('CREATE INDEX %1$sunique_identifier_search ON %1$sleads (email, ui1, ui2)', MAUTIC_TABLE_PREFIX),
$alteredIndexes[1]
);
Assert::assertEquals(sprintf('CREATE INDEX %1$sui2_search ON %1$sleads (ui2)', MAUTIC_TABLE_PREFIX), $alteredIndexes[2]);
Assert::assertEquals(sprintf('CREATE INDEX %1$sui3_search ON %1$sleads (ui3)', MAUTIC_TABLE_PREFIX), $alteredIndexes[3]);
$stack->resetQueries();
// Test that the index was not touched if only the label was updated
$ui1Field->setLabel('UI1 Patched Again');
$fieldModel->saveEntity($ui1Field);
$columns = $this->getUniqueIdentifierIndexColumns('leads');
Assert::assertCount(3, $columns);
Assert::assertCount(0, $stack->getIndexQueries());
// Cleanup
$fieldModel->deleteEntities([$ui1Field->getId(), $ui2Field->getId(), $ui3Field->getId()]);
}
/**
* @return array<mixed>
*/
private function getColumns(string $table, string $column): array
{
$stmt = $this->connection->executeQuery(
"SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '{$this->connection->getDatabase()}' AND TABLE_NAME = '"
.MAUTIC_TABLE_PREFIX
."$table' AND COLUMN_NAME = '$column'"
);
return $stmt->fetchAllAssociative();
}
/**
* @return array<mixed>
*/
private function getUniqueIdentifierIndexColumns(string $table): array
{
$stmt = $this->connection->executeQuery(
sprintf(
"SELECT * FROM information_schema.statistics where table_schema = '%s' and table_name = '%s' and index_name = '%sunique_identifier_search'",
$this->connection->getDatabase(),
MAUTIC_TABLE_PREFIX.$table,
MAUTIC_TABLE_PREFIX
)
);
return $stmt->fetchAllAssociative();
}
}

View File

@@ -0,0 +1,533 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Tests\Model;
use Doctrine\ORM\Exception\ORMException;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Model\NotificationModel;
use Mautic\CoreBundle\ProcessSignal\ProcessSignalService;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\LeadBundle\Entity\Import;
use Mautic\LeadBundle\Entity\ImportRepository;
use Mautic\LeadBundle\Entity\LeadEventLog;
use Mautic\LeadBundle\Entity\LeadEventLogRepository;
use Mautic\LeadBundle\Event\ImportProcessEvent;
use Mautic\LeadBundle\Exception\ImportDelayedException;
use Mautic\LeadBundle\Exception\ImportFailedException;
use Mautic\LeadBundle\Helper\Progress;
use Mautic\LeadBundle\LeadEvents;
use Mautic\LeadBundle\Model\CompanyModel;
use Mautic\LeadBundle\Model\ImportModel;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Tests\StandardImportTestHelper;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class ImportModelTest extends StandardImportTestHelper
{
public function testInitEventLog(): void
{
$userId = 4;
$userName = 'John Doe';
$fileName = 'import.csv';
$line = 104;
$model = $this->initImportModel();
$entity = $this->initImportEntity();
$entity->setCreatedBy($userId)
->setCreatedByUser($userName)
->setModifiedBy($userId)
->setModifiedByUser($userName)
->setOriginalFile($fileName);
$log = $model->initEventLog($entity, $line);
Assert::assertSame($userId, $log->getUserId());
Assert::assertSame($userName, $log->getUserName());
Assert::assertSame('lead', $log->getBundle());
Assert::assertSame('import', $log->getObject());
Assert::assertSame(['line' => $line, 'file' => $fileName], $log->getProperties());
}
public function testProcess(): void
{
$model = $this->initImportModel();
$entity = $this->initImportEntity();
$this->dispatcher->expects($this->exactly(4))
->method('dispatch')
->with(
$this->callback(function (ImportProcessEvent $event) {
// Emulate a subscriber.
$event->setWasMerged(false);
return true;
}),
LeadEvents::IMPORT_ON_PROCESS
);
$entity->start();
$model->process($entity, new Progress());
$entity->end();
Assert::assertEquals(100, $entity->getProgressPercentage());
Assert::assertSame(4, $entity->getInsertedCount());
Assert::assertSame(2, $entity->getIgnoredCount());
Assert::assertSame(Import::IMPORTED, $entity->getStatus());
}
public function testCheckParallelImportLimitWhenMore(): void
{
$model = $this->getMockBuilder(ImportModel::class)
->onlyMethods(['getParallelImportLimit', 'getRepository'])
->disableOriginalConstructor()
->getMock();
$model->expects($this->once())
->method('getParallelImportLimit')
->willReturn(4);
$repository = $this->getMockBuilder(ImportRepository::class)
->onlyMethods(['countImportsWithStatuses'])
->disableOriginalConstructor()
->getMock();
$repository->expects($this->once())
->method('countImportsWithStatuses')
->willReturn(5);
$model->expects($this->once())
->method('getRepository')
->willReturn($repository);
$result = $model->checkParallelImportLimit();
Assert::assertFalse($result);
}
public function testCheckParallelImportLimitWhenEqual(): void
{
$model = $this->getMockBuilder(ImportModel::class)
->onlyMethods(['getParallelImportLimit', 'getRepository'])
->disableOriginalConstructor()
->getMock();
$model->expects($this->once())
->method('getParallelImportLimit')
->willReturn(4);
$repository = $this->getMockBuilder(ImportRepository::class)
->onlyMethods(['countImportsWithStatuses'])
->disableOriginalConstructor()
->getMock();
$repository->expects($this->once())
->method('countImportsWithStatuses')
->willReturn(4);
$model->expects($this->once())
->method('getRepository')
->willReturn($repository);
$result = $model->checkParallelImportLimit();
Assert::assertFalse($result);
}
public function testCheckParallelImportLimitWhenLess(): void
{
$model = $this->getMockBuilder(ImportModel::class)
->onlyMethods(['getParallelImportLimit', 'getRepository'])
->disableOriginalConstructor()
->getMock();
$model->expects($this->once())
->method('getParallelImportLimit')
->willReturn(6);
$repository = $this->getMockBuilder(ImportRepository::class)
->onlyMethods(['countImportsWithStatuses'])
->disableOriginalConstructor()
->getMock();
$repository->expects($this->once())
->method('countImportsWithStatuses')
->willReturn(5);
$model->expects($this->once())
->method('getRepository')
->willReturn($repository);
$result = $model->checkParallelImportLimit();
Assert::assertTrue($result);
}
public function testBeginImportWhenParallelLimitHit(): void
{
$model = $this->getMockBuilder(\Mautic\LeadBundle\Tests\Fixtures\Model\ImportModel::class)
->onlyMethods(['checkParallelImportLimit', 'setGhostImportsAsFailed', 'saveEntity', 'getParallelImportLimit'])
->disableOriginalConstructor()
->getMock();
$model->setTranslator($this->getTranslatorMock());
$model->method('checkParallelImportLimit')
->willReturn(false);
$model->expects($this->once())
->method('getParallelImportLimit')
->willReturn(1);
$entity = $this->initImportEntity(['canProceed']);
$entity->method('canProceed')
->willReturn(true);
try {
$model->beginImport($entity, new Progress());
$this->fail();
} catch (ImportDelayedException) {
// This is expected
}
Assert::assertEquals(0, $entity->getProgressPercentage());
Assert::assertSame(0, $entity->getInsertedCount());
Assert::assertSame(0, $entity->getIgnoredCount());
Assert::assertSame(Import::DELAYED, $entity->getStatus());
$model->expects($this->never())->method('saveEntity');
}
public function testBeginImportWhenDatabaseException(): void
{
$model = $this->getMockBuilder(\Mautic\LeadBundle\Tests\Fixtures\Model\ImportModel::class)
->onlyMethods(['checkParallelImportLimit', 'setGhostImportsAsFailed', 'saveEntity', 'logDebug', 'process'])
->disableOriginalConstructor()
->getMock();
$model->setTranslator($this->getTranslatorMock());
$model->expects($this->once())
->method('checkParallelImportLimit')
->willReturn(true);
$model->expects($this->once())
->method('process')
->will($this->throwException(new ORMException()));
$entity = $this->initImportEntity(['canProceed']);
$entity->method('canProceed')
->willReturn(true);
try {
$model->beginImport($entity, new Progress());
$this->fail();
} catch (ImportFailedException) {
// This is expected
}
Assert::assertEquals(0, $entity->getProgressPercentage());
Assert::assertSame(0, $entity->getInsertedCount());
Assert::assertSame(0, $entity->getIgnoredCount());
Assert::assertSame(Import::DELAYED, $entity->getStatus());
$model->expects($this->never())->method('saveEntity');
}
public function testIsEmptyCsvRow(): void
{
$model = $this->initImportModel();
$testData = [
[
'row' => '',
'res' => true,
],
[
'row' => [],
'res' => true,
],
[
'row' => [null],
'res' => true,
],
[
'row' => [''],
'res' => true,
],
[
'row' => ['John'],
'res' => false,
],
[
'row' => ['John', 'Doe'],
'res' => false,
],
];
foreach ($testData as $test) {
Assert::assertSame(
$test['res'],
$model->isEmptyCsvRow($test['row']),
'Failed on row '.var_export($test['row'], true)
);
}
}
public function testTrimArrayValues(): void
{
$model = $this->initImportModel();
$testData = [
[
'row' => ['John '],
'res' => ['John'],
],
[
'row' => [' John ', ' Do e '],
'res' => ['John', 'Do e'],
],
[
'row' => ['key' => ' John ', 2 => ' Do e '],
'res' => ['key' => 'John', 2 => 'Do e'],
],
];
foreach ($testData as $test) {
Assert::assertSame(
$test['res'],
$model->trimArrayValues($test['row']),
'Failed on row '.var_export($test['row'], true)
);
}
}
public function testHasMoreValuesThanColumns(): void
{
$model = $this->initImportModel();
$columns = 3;
$testData = [
[
'row' => ['John'],
'mod' => ['John', '', ''],
'res' => false,
],
[
'row' => ['John', 'Doe'],
'mod' => ['John', 'Doe', ''],
'res' => false,
],
[
'row' => ['key' => 'John', 2 => 'Doe', 'stuff'],
'mod' => ['key' => 'John', 2 => 'Doe', 'stuff'],
'res' => false,
],
[
'row' => ['key' => 'John', 2 => 'Doe', 'stuff', 'this is too much'],
'mod' => ['key' => 'John', 2 => 'Doe', 'stuff', 'this is too much'],
'res' => true,
],
];
foreach ($testData as $test) {
$res = $model->hasMoreValuesThanColumns($test['row'], $columns);
Assert::assertSame(
$test['res'],
$res,
'Failed on row '.var_export($test['row'], true)
);
Assert::assertSame($test['mod'], $test['row']);
}
}
public function testLimit(): void
{
$model = $this->initImportModel();
$import = new Import();
$import->setFilePath(self::$largeCsvPath)
->setLineCount(511)
->setHeaders(self::$initialList[0])
->setParserConfig(
[
'batchlimit' => 10,
'delimiter' => ',',
'enclosure' => '"',
'escape' => '/',
]
);
$import->start();
$progress = new Progress();
// Each batch should have the last line imported recorded as limit + 1
$model->process($import, $progress, 100);
Assert::assertEquals(101, $import->getLastLineImported());
$model->process($import, $progress, 100);
Assert::assertEquals(201, $import->getLastLineImported());
$model->process($import, $progress, 100);
Assert::assertEquals(301, $import->getLastLineImported());
$model->process($import, $progress, 100);
Assert::assertEquals(401, $import->getLastLineImported());
$model->process($import, $progress, 100);
Assert::assertEquals(501, $import->getLastLineImported());
$model->process($import, $progress, 100);
// 512 is an empty line in the CSV
Assert::assertEquals(512, $import->getLastLineImported());
// Excluding the header but including the empty row in 512, there are 511 rows
Assert::assertEquals(511, $import->getProcessedRows());
$import->end();
}
public function testItLogsDBErrorIfTheEntityManagerIsClosed(): void
{
$this->generateSmallCSV();
$importModel = $this->initImportModel(false);
$import = $this->initImportEntity();
$this->expectException(ORMException::class);
$this->dispatcher->expects($this->once())
->method('dispatch')
->willThrowException(new ORMException('Some DB error'));
$import->start();
$importModel->process($import, new Progress());
$import->end();
Assert::assertSame(Import::FAILED, $import->getStatus());
}
public function testWhenWarningsAvailableInProcessEventLog(): void
{
$model = $this->initImportModel();
$entity = $this->initImportEntity();
$this->dispatcher->expects($this->exactly(4))
->method('dispatch')
->with(
$this->callback(function (ImportProcessEvent $event) {
// Emulate a subscriber.
$event->setWasMerged(false);
$event->addWarning('test warning message');
return true;
}),
LeadEvents::IMPORT_ON_PROCESS
);
$entity->start();
$model->process($entity, new Progress());
$entity->end();
Assert::assertEquals(100, $entity->getProgressPercentage());
Assert::assertSame(Import::IMPORTED, $entity->getStatus());
}
public function testWhenImportUnpublishedInBetweenImportProcess(): void
{
$translator = $this->getTranslatorMock();
$pathsHelper = $this->getPathsHelperMock();
$this->entityManager = $this->getEntityManagerMock();
$coreParametersHelper = $this->getCoreParametersHelperMock();
/** @var MockObject&UserHelper */
$userHelper = $this->createMock(UserHelper::class);
/** @var MockObject&LeadEventLogRepository */
$logRepository = $this->createMock(LeadEventLogRepository::class);
/** @var MockObject&ImportRepository */
$importRepository = $this->createMock(ImportRepository::class);
$importRepository->expects($this->exactly(3))->method('getValue')
->willReturnOnConsecutiveCalls(true, false, false);
$this->entityManager->expects($this->any())
->method('getRepository')
->willReturnMap(
[
[LeadEventLog::class, $logRepository],
[Import::class, $importRepository],
]
);
$this->entityManager->expects($this->any())
->method('isOpen')
->willReturn(true);
/** @var MockObject&LeadModel $leadModel */
$leadModel = $this->getMockBuilder(LeadModel::class)
->disableOriginalConstructor()
->setConstructorArgs([16 => $this->entityManager])
->getMock();
$leadModel->expects($this->any())
->method('getEventLogRepository')
->willReturn($logRepository);
/** @var MockObject&CompanyModel $companyModel */
$companyModel = $this->getMockBuilder(CompanyModel::class)
->disableOriginalConstructor()
->setConstructorArgs([3 => $this->entityManager])
->getMock();
/** @var MockObject&NotificationModel $notificationModel */
$notificationModel = $this->getMockBuilder(NotificationModel::class)
->disableOriginalConstructor()
->setConstructorArgs([3 => $this->entityManager])
->getMock();
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
$this->dispatcher->expects($this->exactly(4))
->method('dispatch')
->with(
$this->callback(function (ImportProcessEvent $event) {
// Emulate a subscriber.
$event->setWasMerged(false);
return true;
}),
LeadEvents::IMPORT_ON_PROCESS,
);
$importModel = new ImportModel(
$pathsHelper,
$leadModel,
$notificationModel,
$coreParametersHelper,
$companyModel,
$this->entityManager,
$this->createMock(CorePermissions::class),
$this->dispatcher,
$this->createMock(UrlGeneratorInterface::class),
$translator,
$userHelper,
$this->createMock(LoggerInterface::class),
new ProcessSignalService()
);
$this->setUpBeforeClass();
$entity = $this->initImportEntity();
$entity->setParserConfig([
'batchlimit' => 3,
'delimiter' => ',',
'enclosure' => '"',
'escape' => '/',
]);
$entity->start();
$importModel->process($entity, new Progress());
$entity->end();
Assert::assertSame(4, $entity->getInsertedCount());
Assert::assertSame(Import::STOPPED, $entity->getStatus());
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace Mautic\LeadBundle\Tests\Model;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\IpAddressModel;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class IpAddressModelTest extends TestCase
{
/**
* @var EntityManager|MockObject
*/
private MockObject $entityManager;
/**
* @var MockObject|LoggerInterface
*/
private MockObject $logger;
private IpAddressModel $ipAddressModel;
protected function setUp(): void
{
parent::setUp();
$this->entityManager = $this->createMock(EntityManager::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->ipAddressModel = new IpAddressModel($this->entityManager, $this->logger);
}
/**
* This test ensures it won't fail if there are no IP addresses.
*/
public function testSaveIpAddressReferencesForContactWhenNoIps(): void
{
$this->entityManager->expects($this->never())
->method('getConnection');
$this->ipAddressModel->saveIpAddressesReferencesForContact(new Lead());
}
public function testSaveIpAddressReferencesForContactThatHasIpsButNoChanges(): void
{
$contact = $this->createMock(Lead::class);
$ipAddress = $this->createMock(IpAddress::class);
$ipAddresses = new ArrayCollection(['1.2.3.4' => $ipAddress]);
$connection = $this->createMock(Connection::class);
$queryBuilder = $this->createMock(QueryBuilder::class);
$contact->expects($this->exactly(1))
->method('getIpAddresses')
->willReturn($ipAddresses);
$this->entityManager->expects($this->never())
->method('getConnection');
$this->ipAddressModel->saveIpAddressesReferencesForContact($contact);
}
public function testSaveIpAddressReferencesForContactThatHasIpsWithSomeAdded(): void
{
$contact = $this->createMock(Lead::class);
$ipAddressAdded = $this->createMock(IpAddress::class);
$ipAddressOld = $this->createMock(IpAddress::class);
$ipAddresses = new ArrayCollection(['1.2.3.999' => $ipAddressOld, '1.2.3.4' => $ipAddressAdded]);
$connection = $this->createMock(Connection::class);
$queryBuilder = $this->createMock(QueryBuilder::class);
$contact->expects($this->exactly(2))
->method('getIpAddresses')
->willReturn($ipAddresses);
$contact->expects($this->exactly(2))
->method('getId')
->willReturn(55);
$contact->expects($this->exactly(2))
->method('getChanges')
->willReturn(['ipAddressList' => ['1.2.3.4' => $ipAddressAdded]]);
$ipAddressAdded->expects($this->exactly(2))
->method('getId')
->willReturn(44);
$ipAddressAdded->expects($this->once())
->method('getIpAddress')
->willReturn('1.2.3.4');
$ipAddressOld->expects($this->never())
->method('getId');
$ipAddressOld->expects($this->once())
->method('getIpAddress')
->willReturn('1.2.3.999');
$queryBuilder->expects($this->once())
->method('executeStatement');
$connection->expects($this->once())
->method('createQueryBuilder')
->willReturn($queryBuilder);
$this->entityManager->expects($this->once())
->method('getConnection')
->willReturn($connection);
$this->ipAddressModel->saveIpAddressesReferencesForContact($contact);
$this->assertCount(2, $contact->getIpAddresses());
}
public function testSaveIpAddressReferencesForContactWhenSomeIpsIfTheReferenceExistsAlready(): void
{
$contact = $this->createMock(Lead::class);
$ipAddress = $this->createMock(IpAddress::class);
$ipAddresses = new ArrayCollection(['1.2.3.4' => $ipAddress]);
$connection = $this->createMock(Connection::class);
$queryBuilder = $this->createMock(QueryBuilder::class);
$contact->expects($this->exactly(2))
->method('getIpAddresses')
->willReturn($ipAddresses);
$contact->expects($this->exactly(3))
->method('getId')
->willReturn(55);
$contact->expects($this->once())
->method('getChanges')
->willReturn(['ipAddressList' => ['1.2.3.4' => $ipAddress]]);
$ipAddress->expects($this->exactly(3))
->method('getId')
->willReturn(44);
$ipAddress->expects($this->once())
->method('getIpAddress')
->willReturn('1.2.3.4');
$queryBuilder->expects($this->once())
->method('executeStatement')
->willThrowException(new UniqueConstraintViolationException($this->createMock(DriverException::class), null));
$connection->expects($this->once())
->method('createQueryBuilder')
->willReturn($queryBuilder);
$this->entityManager->expects($this->once())
->method('getConnection')
->willReturn($connection);
$this->ipAddressModel->saveIpAddressesReferencesForContact($contact);
$this->assertCount(1, $contact->getIpAddresses());
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Mautic\LeadBundle\Tests\Model;
use Mautic\CoreBundle\Helper\Serializer;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\LeadBundle\Model\ListModel;
class LeadListModelTest extends \PHPUnit\Framework\TestCase
{
protected $fixture;
protected function setUp(): void
{
$mockListModel = $this->getMockBuilder(ListModel::class)
->disableOriginalConstructor()
->onlyMethods(['getEntities', 'getEntity'])
->getMock();
$mockListModel->expects($this->any())
->method('getEntity')
->willReturnCallback(function ($id) {
$mockEntity = $this->getMockBuilder(LeadList::class)
->disableOriginalConstructor()
->onlyMethods(['getName'])
->getMock();
$mockEntity->expects($this->once())
->method('getName')
->willReturn((string) $id);
return $mockEntity;
});
$filters = 'a:1:{i:0;a:7:{s:4:"glue";s:3:"and";s:5:"field";s:8:"leadlist";s:6:"object";s:4:"lead";s:4:"type";s:8:"leadlist";s:6:"filter";a:2:{i:0;i:1;i:1;i:3;}s:7:"display";N;s:8:"operator";s:2:"in";}}';
$filters4 = 'a:1:{i:0;a:7:{s:4:"glue";s:3:"and";s:5:"field";s:8:"leadlist";s:6:"object";s:4:"lead";s:4:"type";s:8:"leadlist";s:6:"filter";a:1:{i:0;i:3;}s:7:"display";N;s:8:"operator";s:2:"in";}}';
$mockEntity = $this->createMock(LeadList::class);
$mockEntity1 = clone $mockEntity;
$mockEntity1->expects($this->once())
->method('getFilters')
->willReturn([]);
$mockEntity1->expects($this->any())
->method('getId')
->willReturn(1);
$mockEntity2 = clone $mockEntity;
$mockEntity2->expects($this->once())
->method('getFilters')
->willReturn(Serializer::decode($filters));
$mockEntity2->expects($this->any())
->method('getId')
->willReturn(2);
$mockEntity3 = clone $mockEntity;
$mockEntity3->expects($this->once())
->method('getFilters')
->willReturn([]);
$mockEntity3->expects($this->any())
->method('getId')
->willReturn(3);
$mockEntity4 = clone $mockEntity;
$mockEntity4->expects($this->once())
->method('getFilters')
->willReturn(Serializer::decode($filters4));
$mockEntity4->expects($this->any())
->method('getId')
->willReturn(4);
$mockListModel->expects($this->once())
->method('getEntities')
->willReturn([
1 => $mockEntity1,
2 => $mockEntity2,
3 => $mockEntity3,
4 => $mockEntity4,
]);
$this->fixture = $mockListModel;
}
#[\PHPUnit\Framework\Attributes\DataProvider('segmentTestDataProvider')]
public function testSegmentsCanBeDeletedCorrecty(array $arg, array $expected, $message): void
{
$result = $this->fixture->canNotBeDeleted($arg);
$this->assertEquals($expected, $result, $message);
}
public static function segmentTestDataProvider()
{
return [
[
[1],
[1 => '1'],
'2 is dependent on 1, so 1 cannot be deleted.',
],
[
[1, 3],
[1 => '1', 3 => '3'],
'2 is dependent on 1 & 3, so 1 & 3 cannot be deleted.',
],
[
[1, 2, 3, 4],
[],
'Since we are deleting all segments, it should not prevent any from being deleted.',
],
[
[2],
[],
'Segments without any other segment dependent on them should always be able to be deleted.',
],
];
}
}

View File

@@ -0,0 +1,259 @@
<?php
namespace Mautic\LeadBundle\Tests\Model;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\ORM\EntityManager;
use Doctrine\Persistence\Mapping\MappingException;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Entity\CompanyLead;
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Event\LeadEvent;
use Mautic\LeadBundle\LeadEvents;
use Mautic\LeadBundle\Model\LeadModel;
use Symfony\Component\EventDispatcher\EventDispatcher;
class LeadModelFunctionalTest extends MauticMysqlTestCase
{
private $pointsAdded = false;
protected $useCleanupRollback = false;
public function testSavingPrimaryCompanyAfterPointsAreSetByListenerAreNotResetToDefaultOf0BecauseOfPointsFieldDefaultIs0(): void
{
/** @var EventDispatcher $eventDispatcher */
$eventDispatcher = static::getContainer()->get('event_dispatcher');
$eventDispatcher->addListener(LeadEvents::LEAD_POST_SAVE, [$this, 'addPointsListener']);
/** @var LeadModel $model */
$model = static::getContainer()->get('mautic.lead.model.lead');
/** @var EntityManager $em */
$em = static::getContainer()->get('doctrine.orm.entity_manager');
// Set company to trigger setPrimaryCompany()
$lead = new Lead();
$data = ['email' => 'pointtest@test.com', 'company' => 'PointTest'];
$model->setFieldValues($lead, $data, false, true, true);
// Save to trigger points listener and setting primary company
$model->saveEntity($lead);
// Clear from doctrine memory so we get a fresh entity to ensure the points are definitely saved
$em->detach($lead);
$lead = $model->getEntity($lead->getId());
$this->assertEquals(10, $lead->getPoints());
}
/**
* Simulate a PointModel::triggerAction.
*/
public function addPointsListener(LeadEvent $event): void
{
// Prevent a loop
if ($this->pointsAdded) {
return;
}
$this->pointsAdded = true;
$lead = $event->getLead();
$lead->adjustPoints(10);
/** @var LeadModel $model */
$model = static::getContainer()->get('mautic.lead.model.lead');
$model->saveEntity($lead);
}
public function testMultipleAssignedCompany(): void
{
self::assertEquals(2, count($this->getContactWithAssignTwoCompanies()));
}
public function testSignleAssignedCompany(): void
{
$this->setUpSymfony(array_merge($this->configParams, ['contact_allow_multiple_companies' => 0]));
self::assertEquals(1, count($this->getContactWithAssignTwoCompanies()));
}
/**
* @return array<int,array<int|string>>
*
* @throws DBALException
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
protected function getContactWithAssignTwoCompanies(): array
{
$company = new Company();
$company->setName('Doe Corp');
$this->em->persist($company);
$company2 = new Company();
$company2->setName('Doe Corp 2');
$this->em->persist($company2);
$contact = new Lead();
$contact->setEmail('test@test.com');
$this->em->persist($contact);
$this->em->flush();
/** @var LeadModel $leadModel */
$leadModel = $this->getContainer()->get('mautic.lead.model.lead');
$leadModel->addToCompany($contact, $company);
$leadModel->addToCompany($contact, $company2);
/** @var CompanyLeadRepository $companyLeadRepo */
$companyLeadRepo = $this->em->getRepository(CompanyLead::class);
$contactCompanies = $companyLeadRepo->getCompaniesByLeadId($contact->getId());
return $contactCompanies;
}
public function testGetCustomLeadFieldLength(): void
{
$leadModel = $this->getContainer()->get('mautic.lead.model.lead');
$fieldModel = $this->getContainer()->get('mautic.lead.model.field');
// Create a lead field.
$leadField = new LeadField();
$leadField->setName('Test Field')
->setAlias('custom_field_len_test')
->setType('string')
->setObject('lead')
->setCharLengthLimit(150);
$fieldModel->saveEntity($leadField);
// Create leads without adding value to the 'Test field'.
$bob = new Lead();
$bob->setFirstname('Bob')
->setLastname('Smith')
->setEmail('bob.smith@test.com');
$leadModel->saveEntity($bob);
$jane = new Lead();
$jane->setFirstname('Jane')
->setLastname('Smith')
->setEmail('jane.smith@test.com');
$leadModel->saveEntity($jane);
$this->em->clear();
// Custom field is empty, and will return null.
$length = $leadModel->getCustomLeadFieldLength([$leadField->getAlias()]);
$this->assertNull($length[$leadField->getAlias()]);
// Update lead Bob with 'Test field' value.
$hashStringBob = hash('sha256', __METHOD__);
$bob->addUpdatedField($leadField->getAlias(), $hashStringBob);
$leadModel->saveEntity($bob);
// Update lead Jane with 'Test field' value.
$hashStringJane = hash('sha1', __METHOD__);
$jane->addUpdatedField($leadField->getAlias(), $hashStringJane);
$leadModel->saveEntity($jane);
$this->em->clear();
$length = $leadModel->getCustomLeadFieldLength([$leadField->getAlias()]);
$this->assertEquals(strlen($hashStringBob), $length[$leadField->getAlias()]);
}
public function testGettingUnknownCustomFieldLength(): void
{
$this->expectException(DBALException::class);
$leadModel = $this->getContainer()->get('mautic.lead.model.lead');
$leadModel->getCustomLeadFieldLength(['unknown_field']);
}
/**
* @throws MappingException
*/
#[\PHPUnit\Framework\Attributes\DataProvider('fieldValueProvider')]
public function testSelectFieldSavesOnlyAllowedValuesInDB(string $selectFieldValue, ?string $expectedValue): void
{
$fieldModel = self::getContainer()->get('mautic.lead.model.field');
// Create a lead field.
$selectField = new LeadField();
$selectField->setName('Select Field')
->setAlias('select_field')
->setType('select')
->setObject('lead')
->setProperties(['list' => [
['label' => 'Male', 'value' => 'male'],
['label' => 'Female', 'value' => 'female'],
['label' => 'Other\'s', 'value' => 'other\'s'],
]]);
$fieldModel->saveEntity($selectField);
$this->em->clear();
$leadModel = self::getContainer()->get('mautic.lead.model.lead');
$fields = [
'core' => [
'First Name' => [
'alias' => 'firstname',
'type' => 'string',
'value' => 'FirstName',
],
'Last Name' => [
'alias' => 'lastname',
'type' => 'string',
'value' => 'LastName',
],
'Email' => [
'alias' => 'email',
'type' => 'email',
'value' => 'firstname.lastname@test.com',
],
'Select Field' => [
'alias' => $selectField->getAlias(),
'type' => $selectField->getType(),
'value' => $selectFieldValue,
'properties' => ['list' => [
['label' => 'Male', 'value' => 'male'],
['label' => 'Female', 'value' => 'female'],
// As it stores HTML encoded value.
['label' => 'Other&#39;s', 'value' => 'other&#39;s'],
]],
],
],
];
// Create lead with multiple fields
$lead = new Lead();
$lead->setFields($fields);
$lead->setFirstname('FirstName')
->setLastname('LastName')
->setEmail('firstname.lastname@test.com')
->addUpdatedField($selectField->getAlias(), $selectFieldValue);
$leadModel->saveEntity($lead);
$this->em->clear();
$lead = $leadModel->getEntity($lead->getId());
$this->assertSame($expectedValue, $lead->getFieldValue($selectField->getAlias()));
}
/**
* @return array<mixed>
*/
public static function fieldValueProvider(): array
{
return [
'allowed_value' => ['female', 'female'],
'disallowed_value' => ['gibberish', null],
'with_quotes' => ['other\'s', 'other\'s'],
];
}
}

View File

@@ -0,0 +1,877 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Tests\Model;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Mautic\CategoryBundle\Model\CategoryModel;
use Mautic\ChannelBundle\Helper\ChannelListHelper;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\EmailBundle\Helper\EmailValidator;
use Mautic\LeadBundle\DataObject\LeadManipulator;
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadEventLog;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Entity\LeadRepository;
use Mautic\LeadBundle\Entity\StagesChangeLog;
use Mautic\LeadBundle\Entity\StagesChangeLogRepository;
use Mautic\LeadBundle\Event\LeadEvent;
use Mautic\LeadBundle\Event\SaveBatchLeadsEvent;
use Mautic\LeadBundle\Exception\ImportFailedException;
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
use Mautic\LeadBundle\LeadEvents;
use Mautic\LeadBundle\Model\CompanyModel;
use Mautic\LeadBundle\Model\FieldModel;
use Mautic\LeadBundle\Model\IpAddressModel;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Model\ListModel;
use Mautic\LeadBundle\Tests\Fixtures\Model\LeadModelStub;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Mautic\LeadBundle\Tracker\DeviceTracker;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\StageBundle\Entity\Stage;
use Mautic\StageBundle\Entity\StageRepository;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Security\Provider\UserProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactory;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class LeadModelTest extends \PHPUnit\Framework\TestCase
{
private MockObject|RequestStack $requestStack;
/**
* @var MockObject|IpLookupHelper
*/
private MockObject $ipLookupHelperMock;
/**
* @var MockObject|PathsHelper
*/
private MockObject $pathsHelperMock;
/**
* @var MockObject|IntegrationHelper
*/
private MockObject $integrationHelperkMock;
/**
* @var MockObject|FieldModel
*/
private MockObject $fieldModelMock;
/**
* @var MockObject&FieldsWithUniqueIdentifier
*/
private MockObject $fieldsWithUniqueIdentifier;
/**
* @var MockObject|ListModel
*/
private MockObject $listModelMock;
/**
* @var MockObject|FormFactory
*/
private MockObject $formFactoryMock;
/**
* @var MockObject|CompanyModel
*/
private MockObject $companyModelMock;
/**
* @var MockObject|CategoryModel
*/
private MockObject $categoryModelMock;
private ChannelListHelper $channelListHelperMock;
/**
* @var MockObject|CoreParametersHelper
*/
private MockObject $coreParametersHelperMock;
/**
* @var MockObject|EmailValidator
*/
private MockObject $emailValidatorMock;
/**
* @var MockObject|UserProvider
*/
private MockObject $userProviderMock;
/**
* @var MockObject|ContactTracker
*/
private MockObject $contactTrackerMock;
/**
* @var MockObject|DeviceTracker
*/
private MockObject $deviceTrackerMock;
/**
* @var MockObject|IpAddressModel
*/
private MockObject $ipAddressModelMock;
/**
* @var MockObject|LeadRepository
*/
private MockObject $leadRepositoryMock;
/**
* @var MockObject|CompanyLeadRepository
*/
private MockObject $companyLeadRepositoryMock;
/**
* @var MockObject|UserHelper
*/
private MockObject $userHelperMock;
/**
* @var MockObject|EventDispatcherInterface
*/
private MockObject $dispatcherMock;
/**
* @var MockObject|EntityManager
*/
private MockObject $entityManagerMock;
private LeadModel $leadModel;
/**
* @var MockObject&Translator
*/
private MockObject $translator;
/**
* @var MockObject&UrlGeneratorInterface
*/
private MockObject $urlGeneratorInterfaceMock;
/**
* @var MockObject&LoggerInterface
*/
private MockObject $logger;
/**
* @var MockObject&CorePermissions
*/
private MockObject $corePermissionsMock;
protected function setUp(): void
{
parent::setUp();
$this->requestStack = new RequestStack();
$this->requestStack->push(new Request());
$this->ipLookupHelperMock = $this->createMock(IpLookupHelper::class);
$this->pathsHelperMock = $this->createMock(PathsHelper::class);
$this->integrationHelperkMock = $this->createMock(IntegrationHelper::class);
$this->fieldModelMock = $this->createMock(FieldModel::class);
$this->fieldsWithUniqueIdentifier = $this->createMock(FieldsWithUniqueIdentifier::class);
$this->listModelMock = $this->createMock(ListModel::class);
$this->formFactoryMock = $this->createMock(FormFactory::class);
$this->companyModelMock = $this->createMock(CompanyModel::class);
$this->categoryModelMock = $this->createMock(CategoryModel::class);
$this->channelListHelperMock = new ChannelListHelper($this->createMock(EventDispatcherInterface::class), $this->createMock(Translator::class));
$this->coreParametersHelperMock = $this->createMock(CoreParametersHelper::class);
$this->emailValidatorMock = $this->createMock(EmailValidator::class);
$this->userProviderMock = $this->createMock(UserProvider::class);
$this->contactTrackerMock = $this->createMock(ContactTracker::class);
$this->deviceTrackerMock = $this->createMock(DeviceTracker::class);
$this->ipAddressModelMock = $this->createMock(IpAddressModel::class);
$this->leadRepositoryMock = $this->createMock(LeadRepository::class);
$this->companyLeadRepositoryMock = $this->createMock(CompanyLeadRepository::class);
$this->userHelperMock = $this->createMock(UserHelper::class);
$this->dispatcherMock = $this->createMock(EventDispatcherInterface::class);
$this->entityManagerMock = $this->createMock(EntityManager::class);
$this->corePermissionsMock = $this->createMock(CorePermissions::class);
$this->translator = $this->createMock(Translator::class);
$this->urlGeneratorInterfaceMock = $this->createMock(UrlGeneratorInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->leadModel = new LeadModel(
$this->requestStack,
$this->ipLookupHelperMock,
$this->pathsHelperMock,
$this->integrationHelperkMock,
$this->fieldModelMock,
$this->fieldsWithUniqueIdentifier,
$this->listModelMock,
$this->formFactoryMock,
$this->companyModelMock,
$this->categoryModelMock,
$this->channelListHelperMock,
$this->coreParametersHelperMock,
$this->emailValidatorMock,
$this->userProviderMock,
$this->contactTrackerMock,
$this->deviceTrackerMock,
$this->ipAddressModelMock,
$this->entityManagerMock,
$this->corePermissionsMock,
$this->dispatcherMock,
$this->urlGeneratorInterfaceMock,
$this->translator,
$this->userHelperMock,
$this->logger,
);
$this->setSecurity($this->leadModel);
$this->companyModelMock->method('getCompanyLeadRepository')->willReturn($this->companyLeadRepositoryMock);
}
public function testIpLookupDoesNotAddCompanyIfConfiguredSo(): void
{
$this->mockGetLeadRepository();
$entity = new Lead();
$ipAddress = new IpAddress('some.ip');
$ipAddress->setIpDetails(['organization' => 'Doctors Without Borders']);
$entity->addIpAddress($ipAddress);
$this->coreParametersHelperMock->method('get')
->willReturnMap([
['anonymize_ip', false, false],
['ip_lookup_create_organization', false, false],
]);
$this->setupFieldModelForIpLookupTest();
$this->companyLeadRepositoryMock->expects($this->never())->method('getEntitiesByLead');
$this->companyModelMock->expects($this->never())->method('getEntities');
$this->leadModel->saveEntity($entity);
$this->assertNull($entity->getCompany());
$this->assertArrayNotHasKey('company', $entity->getUpdatedFields());
}
public function testIpLookupAddsCompanyIfDoesNotExistInEntity(): void
{
$this->mockGetLeadRepository();
$companyFromIpLookup = 'Doctors Without Borders';
$entity = new Lead();
$ipAddress = new IpAddress('some.ip');
$ipAddress->setIpDetails(['organization' => $companyFromIpLookup]);
$entity->addIpAddress($ipAddress);
$this->coreParametersHelperMock->method('get')
->willReturnMap([
['anonymize_ip', false, false],
['ip_lookup_create_organization', false, true],
]);
$this->setupFieldModelForIpLookupTest();
$this->companyLeadRepositoryMock->method('getEntitiesByLead')->willReturn([]);
$this->companyModelMock->expects($this->any())
->method('fetchCompanyFields')
->willReturn([]);
$this->leadModel->saveEntity($entity);
$this->assertSame($companyFromIpLookup, $entity->getCompany());
$this->assertSame($companyFromIpLookup, $entity->getUpdatedFields()['company']);
}
public function testIpLookupAddsCompanyIfExistsInEntity(): void
{
$this->mockGetLeadRepository();
$companyFromIpLookup = 'Doctors Without Borders';
$companyFromEntity = 'Red Cross';
$entity = new Lead();
$ipAddress = new IpAddress('some.ip');
$entity->setCompany($companyFromEntity);
$ipAddress->setIpDetails(['organization' => $companyFromIpLookup]);
$entity->addIpAddress($ipAddress);
$this->coreParametersHelperMock->expects($this->once())->method('get')->with('anonymize_ip', false)->willReturn(false);
$this->setupFieldModelForIpLookupTest();
$this->companyLeadRepositoryMock->method('getEntitiesByLead')->willReturn([]);
$this->leadModel->saveEntity($entity);
$this->assertSame($companyFromEntity, $entity->getCompany());
$this->assertFalse(isset($entity->getUpdatedFields()['company']));
}
public function testCheckForDuplicateContact(): void
{
$this->fieldModelMock->expects($this->once())
->method('getFieldList')
->with(false, false, ['isPublished' => true, 'object' => 'lead'])
->willReturn(['email' => 'Email', 'firstname' => 'First Name']);
$this->fieldsWithUniqueIdentifier->expects($this->once())
->method('getFieldsWithUniqueIdentifier')
->willReturn(['email' => 'Email']);
$this->fieldModelMock->expects($this->once())
->method('getEntities')
->willReturn($this->getFieldPaginatorFake());
$mockLeadModel = $this->createMockLeadModelForDuplicateTest();
$this->leadRepositoryMock->expects($this->once())
->method('getLeadsByUniqueFields')
->with(['email' => 'john@doe.com'], null)
->willReturn([]);
// The availableLeadFields property should start empty.
$this->assertEquals([], $mockLeadModel->getAvailableLeadFields());
$contact = $mockLeadModel->checkForDuplicateContact(['email' => 'john@doe.com', 'firstname' => 'John']);
$this->assertEquals(['email' => 'Email', 'firstname' => 'First Name'], $mockLeadModel->getAvailableLeadFields());
$this->assertEquals('john@doe.com', $contact->getEmail());
$this->assertEquals('John', $contact->getFirstname());
}
public function testCheckForDuplicateContactForOnlyPubliclyUpdatable(): void
{
$this->fieldModelMock->expects($this->once())
->method('getFieldList')
->with(false, false, ['isPublished' => true, 'object' => 'lead', 'isPubliclyUpdatable' => true])
->willReturn(['email' => 'Email']);
$this->fieldsWithUniqueIdentifier->expects($this->once())
->method('getFieldsWithUniqueIdentifier')
->willReturn(['email' => 'Email']);
$this->fieldModelMock->expects($this->once())
->method('getEntities')
->willReturn($this->getFieldPaginatorFake());
/** @var LeadModel&MockObject $mockLeadModel */
$mockLeadModel = $this->createMockLeadModelForDuplicateTest();
$this->leadRepositoryMock->expects($this->once())
->method('getLeadsByUniqueFields')
->with(['email' => 'john@doe.com'], null)
->willReturn([]);
// The availableLeadFields property should start empty.
$this->assertEquals([], $mockLeadModel->getAvailableLeadFields());
[$contact, $fields] = $mockLeadModel->checkForDuplicateContact(['email' => 'john@doe.com', 'firstname' => 'John'], true, true);
$this->assertEquals(['email' => 'Email'], $mockLeadModel->getAvailableLeadFields());
$this->assertEquals('john@doe.com', $contact->getEmail());
$this->assertNull($contact->getFirstname());
$this->assertEquals(['email' => 'john@doe.com'], $fields);
}
/**
* Test that the Lead won't be set to the LeadEventLog if the Lead save fails.
*/
public function testImportWillNotSetLeadToLeadEventLogWhenLeadSaveFails(): void
{
$leadEventLog = new LeadEventLog();
$mockLeadModel = $this->createMockLeadModelStub();
$this->setupMockLeadModelForImport($mockLeadModel);
$mockLeadModel->expects($this->once())->method('saveEntity')->willThrowException(new \Exception());
$mockLeadModel->expects($this->once())->method('checkForDuplicateContact')->willReturn(new Lead());
try {
$mockLeadModel->import([], [], null, null, null, true, $leadEventLog);
} catch (\Exception) {
$this->assertNull($leadEventLog->getLead());
}
}
/**
* Test that the Lead will be set to the LeadEventLog if the Lead save succeed.
*/
public function testImportWillSetLeadToLeadEventLogWhenLeadSaveSucceed(): void
{
$leadEventLog = new LeadEventLog();
$lead = new Lead();
$mockLeadModel = $this->createMockLeadModelStub();
$this->setupMockLeadModelForImport($mockLeadModel);
$mockLeadModel->expects($this->once())->method('checkForDuplicateContact')->willReturn($lead);
try {
$mockLeadModel->import([], [], null, null, null, true, $leadEventLog);
} catch (\Exception) {
$this->assertEquals($lead, $leadEventLog->getLead());
}
}
/**
* Test that the tags will be added to the lead from the csv file.
*/
public function testImportWithTagsInCsvFile(): void
{
$mockLeadModel = $this->createMockLeadModelStub(['saveEntity', 'checkForDuplicateContact', 'modifyTags']);
$this->setProperty($mockLeadModel, LeadModel::class, 'leadFieldModel', $this->fieldModelMock);
$this->setupMockLeadModelForImport($mockLeadModel);
$mockLeadModel->expects($this->once())->method('checkForDuplicateContact')->willReturn(new Lead());
$mockLeadModel->expects($this->once())->method('modifyTags')->willReturn(true);
$mockLeadModel->import(['tag' => 'tags'], ['tag' => 'Test 1|Test 2|Test 3']);
}
/**
* Test lead matching by ID.
*/
public function testImportMatchLeadById(): void
{
$leadEventLog = new LeadEventLog();
$lead = new Lead();
$lead->setId(21);
$mockLeadModel = $this->createMockLeadModelStub(['saveEntity', 'getEntity']);
$this->setProperty($mockLeadModel, LeadModel::class, 'leadFieldModel', $this->fieldModelMock);
$this->setupMockLeadModelForImport($mockLeadModel);
$mockLeadModel->expects($this->once())->method('getEntity')->willReturn($lead);
$merged = $mockLeadModel->import(['identifier' => 'id'], ['identifier' => '21'], null, null, null, true, $leadEventLog);
$this->assertTrue($merged);
}
public function testSetFieldValuesWithStage(): void
{
$lead = new Lead();
$lead->setId(1);
$lead->setFields(['all' => 'sth']);
$stageMock = $this->createMock(Stage::class);
$stageMock->expects($this->any())
->method('getId')
->willReturn(1);
$data = ['stage' => $stageMock];
$stagesChangeLogRepo = $this->createMock(StagesChangeLogRepository::class);
$stagesChangeLogRepo->expects($this->once())
->method('getCurrentLeadStage')
->with($lead->getId())
->willReturn(null);
$stageRepositoryMock = $this->createMock(StageRepository::class);
$stageRepositoryMock->expects($this->once())
->method('findByIdOrName')
->with(1)
->willReturn($stageMock);
$matcher = $this->exactly(2);
$this->entityManagerMock->expects($matcher)
->method('getRepository')->willReturnCallback(function (...$parameters) use ($matcher, $stagesChangeLogRepo, $stageRepositoryMock) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame(StagesChangeLog::class, $parameters[0]);
return $stagesChangeLogRepo;
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame(Stage::class, $parameters[0]);
return $stageRepositoryMock;
}
});
$this->translator->expects($this->once())
->method('trans')
->with('mautic.stage.event.changed');
$this->leadModel->setFieldValues($lead, $data, false, false);
}
public function testImportIsIgnoringContactWithNotFoundStage(): void
{
$lead = new Lead();
$lead->setId(1);
$data = ['stage' => 'not found'];
$stagesChangeLogRepo = $this->createMock(StagesChangeLogRepository::class);
$stagesChangeLogRepo->expects($this->once())
->method('getCurrentLeadStage')
->with($lead->getId())
->willReturn(null);
$stageRepositoryMock = $this->createMock(StageRepository::class);
$stageRepositoryMock->expects($this->once())
->method('findByIdOrName')
->with($data['stage'])
->willReturn(null);
$matcher = $this->exactly(2);
$this->entityManagerMock->expects($matcher)
->method('getRepository')->willReturnCallback(function (...$parameters) use ($matcher, $stagesChangeLogRepo, $stageRepositoryMock) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame(StagesChangeLog::class, $parameters[0]);
return $stagesChangeLogRepo;
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame(Stage::class, $parameters[0]);
return $stageRepositoryMock;
}
});
$this->translator->expects($this->once())
->method('trans')
->with('mautic.lead.import.stage.not.exists', ['id' => $data['stage']])
->willReturn('Stage not found');
$this->expectException(ImportFailedException::class);
$this->leadModel->setFieldValues($lead, $data, false, false);
}
public function testManipulatorIsLoggedOnlyOnce(): void
{
$this->mockGetLeadRepository();
$contact = $this->createMock(Lead::class);
$manipulator = new LeadManipulator('lead', 'import', 333);
$contact->expects($this->exactly(2))
->method('getIpAddresses')
->willReturn([]);
$contact->expects($this->exactly(2))
->method('isNewlyCreated')
->willReturn(true);
$contact->expects($this->exactly(2))
->method('getManipulator')
->willReturn($manipulator);
$contact->expects($this->exactly(2))
->method('getUpdatedFields')
->willReturn([]);
$contact->expects($this->once())
->method('addEventLog')
->with($this->callback(function (LeadEventLog $leadEventLog) use ($contact) {
$this->assertSame($contact, $leadEventLog->getLead());
$this->assertSame('identified_contact', $leadEventLog->getAction());
$this->assertSame('lead', $leadEventLog->getBundle());
$this->assertSame('import', $leadEventLog->getObject());
$this->assertSame(333, $leadEventLog->getObjectId());
return true;
}));
$this->fieldModelMock->expects($this->exactly(2))
->method('getFieldListWithProperties')
->willReturn([]);
$this->fieldModelMock->expects($this->once())
->method('getFieldList')
->willReturn([]);
$this->leadModel->saveEntity($contact);
$this->leadModel->saveEntity($contact);
}
public function testImportHtmlFields(): void
{
$this->mockGetLeadRepository();
$fieldEntity = new LeadField();
$fieldEntity->setAlias('custom_html_field');
$fieldEntity->setLabel('Custom HTML Field');
$fieldEntity->setType('html');
$fieldEntity->setGroup('core');
$fieldEntity->setObject('lead');
$fields = ['custom_html_field' => 'custom_html_field'];
$data = ['custom_html_field' => '<html><head></head><body>Test</body></html>'];
$this->userHelperMock->method('getUser')
->willReturn(new User());
$this->fieldModelMock->method('getFieldList')
->willReturn([$fields]);
$this->fieldModelMock->expects($this->atLeastOnce())
->method('getEntities')
->willReturn($this->getFieldPaginatorFake());
$this->fieldModelMock->expects($this->once())
->method('getEntityByAlias')
->with('custom_html_field')
->willReturn($fieldEntity);
$this->companyModelMock->expects($this->once())
->method('extractCompanyDataFromImport')
->willReturn([[], []]);
$this->leadModel->import($fields, $data);
}
/**
* Set protected property to an object.
*
* @param object $object
* @param string $class
* @param string $property
* @param mixed $value
*/
private function setProperty($object, $class, $property, $value): void
{
$reflectedProp = new \ReflectionProperty($class, $property);
$reflectedProp->setAccessible(true);
$reflectedProp->setValue($object, $value);
}
private function mockGetLeadRepository(): void
{
$this->entityManagerMock->expects($this->any())
->method('getRepository')
->willReturnMap(
[
[Lead::class, $this->leadRepositoryMock],
]
);
}
public function testModifiedCompanies(): void
{
$lead = $this->getLead(1);
$companies = [];
$leadCompanies = [];
for ($i = 1; $i <= 4; ++$i) {
$companies[] = $i;
}
// Imitate that companies with id 3 and 4 are already added to the lead
for ($i = 3; $i <= 4; ++$i) {
// Taking only company_id into consideration as only this is required in this case
$leadCompanies[] = ['company_id' => $i];
}
$this->companyModelMock->expects($this->once())
->method('getCompanyLeadRepository')
->willReturn($this->companyLeadRepositoryMock);
$this->companyLeadRepositoryMock->expects($this->once())
->method('getCompaniesByLeadId')
->with($lead->getId())
->willReturn($leadCompanies);
$this->companyModelMock->expects($this->once())
->method('addLeadToCompany')
->with([$companies[0], $companies[1]], $lead);
$this->leadModel->modifyCompanies($lead, $companies);
}
private function getLead(int $id): Lead
{
return new class($id) extends Lead {
public function __construct(
private int $id,
) {
parent::__construct();
}
public function getId(): int
{
return $this->id;
}
};
}
/**
* @return Paginator<mixed[]>
*/
private function getFieldPaginatorFake(): Paginator
{
return new class extends Paginator {
public function __construct()
{
}
/**
* @return \ArrayIterator<int,array{label: string, alias: string, isPublished: bool, id: int, object: string, group: string, type: string}>
*/
public function getIterator()
{
return new \ArrayIterator([
4 => ['label' => 'Email', 'alias' => 'email', 'isPublished' => true, 'id' => 4, 'object' => 'lead', 'group' => 'basic', 'type' => 'email'],
5 => ['label' => 'First Name', 'alias' => 'firstname', 'isPublished' => true, 'id' => 5, 'object' => 'lead', 'group' => 'basic', 'type' => 'text'],
]);
}
};
}
public function testDispatchBatchEvent(): void
{
$leadsParams = [];
for ($x = 0; $x < 2; ++$x) {
$lead = new Lead();
$lead->setEmail(sprintf('test%s@test.cz', $x));
$leadsParams[] = ['entity' => $lead, 'isNew'=> true, 'event'=> null];
}
$action = 'post_batch_save';
// Lead Model that provides access to dispatchBatchEvent
$leadModel = new class($this->requestStack, $this->ipLookupHelperMock, $this->pathsHelperMock, $this->integrationHelperkMock, $this->fieldModelMock, $this->fieldsWithUniqueIdentifier, $this->listModelMock, $this->formFactoryMock, $this->companyModelMock, $this->categoryModelMock, $this->channelListHelperMock, $this->coreParametersHelperMock, $this->emailValidatorMock, $this->userProviderMock, $this->contactTrackerMock, $this->deviceTrackerMock, $this->ipAddressModelMock, $this->entityManagerMock, $this->corePermissionsMock, $this->dispatcherMock, $this->urlGeneratorInterfaceMock, $this->translator, $this->userHelperMock, $this->logger) extends LeadModel {
/**
* @param array<mixed> $leads
*/
public function dispatchBatchEventForTest(string $action, array $leads): ?\Symfony\Contracts\EventDispatcher\Event
{
return $this->dispatchBatchEvent($action, $leads);
}
};
$leadEvent1 = new LeadEvent($leadsParams[0]['entity'], $leadsParams[0]['isNew']);
$leadEvent1->setEntityManager($this->entityManagerMock);
$leadEvent2 = new LeadEvent($leadsParams[1]['entity'], $leadsParams[1]['isNew']);
$leadEvent2->setEntityManager($this->entityManagerMock);
$event = new SaveBatchLeadsEvent([
$leadEvent1,
$leadEvent2,
]);
$this->dispatcherMock->expects($this->once())
->method('hasListeners')
->with(LeadEvents::LEAD_POST_BATCH_SAVE)
->willReturn(true);
$this->dispatcherMock->expects($this->once())
->method('dispatch')
->with($event, LeadEvents::LEAD_POST_BATCH_SAVE)
->willReturn($event);
$leadModel->dispatchBatchEventForTest($action, $leadsParams);
}
private function setSecurity(LeadModel $companyModel): void
{
$security = $this->createMock(CorePermissions::class);
$security->method('hasEntityAccess')
->willReturn(true);
$security->method('isGranted')
->willReturn(true);
$reflection = new \ReflectionClass($companyModel);
$property = $reflection->getProperty('security');
$property->setAccessible(true);
$property->setValue($companyModel, $security);
}
/**
* Creates and configures a mock UserHelper with a User.
*/
private function createMockUserHelper(): UserHelper&MockObject
{
/** @var UserHelper&MockObject $mockUserModel */
$mockUserModel = $this->createMock(UserHelper::class);
$mockUserModel->method('getUser')->willReturn(new User());
return $mockUserModel;
}
/**
* Creates and configures a mock LeadModelStub with common setup.
*
* @param array<string> $methods
*/
private function createMockLeadModelStub(array $methods = ['saveEntity', 'checkForDuplicateContact']): MockObject
{
$mockLeadModel = $this->getMockBuilder(LeadModelStub::class)
->disableOriginalConstructor()
->onlyMethods($methods)
->getMock();
$mockLeadModel->setUserHelper($this->createMockUserHelper());
$this->setSecurity($mockLeadModel);
return $mockLeadModel;
}
/**
* Creates and configures a mock CompanyModel for import tests.
*/
private function createMockCompanyModelForImport(): MockObject
{
$mockCompanyModel = $this->getMockBuilder(CompanyModel::class)
->disableOriginalConstructor()
->onlyMethods(['extractCompanyDataFromImport'])
->getMock();
$mockCompanyModel->expects($this->once())
->method('extractCompanyDataFromImport')
->willReturn([[], []]);
return $mockCompanyModel;
}
/**
* Sets up common properties for a mock LeadModel used in import tests.
*/
private function setupMockLeadModelForImport(MockObject $mockLeadModel): void
{
$mockCompanyModel = $this->createMockCompanyModelForImport();
$this->setProperty($mockLeadModel, LeadModel::class, 'companyModel', $mockCompanyModel);
$this->setProperty($mockLeadModel, LeadModel::class, 'leadFields', [
['alias' => 'email', 'type' => 'email', 'defaultValue' => ''],
]);
}
/**
* Creates a mock LeadModel for duplicate contact tests.
*/
private function createMockLeadModelForDuplicateTest(): MockObject
{
$mockLeadModel = $this->getMockBuilder(LeadModel::class)
->disableOriginalConstructor()
->onlyMethods(['getRepository'])
->getMock();
$mockLeadModel->expects($this->once())
->method('getRepository')
->willReturn($this->leadRepositoryMock);
$this->setProperty($mockLeadModel, LeadModel::class, 'leadFieldModel', $this->fieldModelMock);
$this->setProperty($mockLeadModel, LeadModel::class,
'fieldsWithUniqueIdentifier', $this->fieldsWithUniqueIdentifier);
return $mockLeadModel;
}
/**
* Sets up common field model mocks for IP lookup tests.
*/
private function setupFieldModelForIpLookupTest(): void
{
$this->fieldModelMock->method('getFieldListWithProperties')->willReturn([]);
$this->fieldModelMock->method('getFieldList')->willReturn([]);
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Mautic\LeadBundle\Tests\Model;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\LeadBundle\Entity\LeadListRepository;
use Mautic\LeadBundle\Entity\LeadRepository;
use Mautic\LeadBundle\Model\ListModel;
use Mautic\UserBundle\Entity\User;
use PHPUnit\Framework\Assert;
class ListModelFunctionalTest extends MauticMysqlTestCase
{
public function testPublicSegmentsInContactPreferences(): void
{
$user = $this->em->getRepository(User::class)->findBy([], [], 1)[0];
$firstLeadList = $this->createLeadList($user, 'First', true);
$secondLeadList = $this->createLeadList($user, 'Second', false);
$thirdLeadList = $this->createLeadList($user, 'Third', true);
$this->em->flush();
/** @var LeadListRepository $repo */
$repo = $this->em->getRepository(LeadList::class);
$lists = $repo->getGlobalLists();
Assert::assertCount(2, $lists);
Assert::assertArrayHasKey($firstLeadList->getId(), $lists);
Assert::assertArrayHasKey($thirdLeadList->getId(), $lists);
Assert::assertArrayNotHasKey(
$secondLeadList->getId(),
$lists,
'Non-global lists should not be returned by the `getGlobalLists()` method.'
);
}
public function testSegmentLineChartData(): void
{
/** @var ListModel $segmentModel */
$segmentModel = static::getContainer()->get('mautic.lead.model.list');
/** @var LeadRepository $contactRepository */
$contactRepository = $this->em->getRepository(Lead::class);
$segment = new LeadList();
$segment->setName('Segment A');
$segmentModel->saveEntity($segment);
$contacts = [new Lead(), new Lead(), new Lead(), new Lead()];
$contactRepository->saveEntities($contacts);
$segmentModel->addLead($contacts[0], $segment); // Emulating adding by a filter.
$segmentModel->addLead($contacts[1], $segment); // Emulating adding by a filter.
$segmentModel->addLead($contacts[2], $segment, true); // Manually added.
$segmentModel->addLead($contacts[3], $segment, true); // Manually added.
$data = $segmentModel->getSegmentContactsLineChartData(
'd',
new \DateTime('1 month ago', new \DateTimeZone('UTC')),
new \DateTime('now', new \DateTimeZone('UTC')),
null,
['leadlist_id' => ['value' => $segment->getId(), 'list_column_name' => 't.lead_id']]
);
Assert::assertSame('added', strtolower($data['datasets'][0]['label']));
Assert::assertSame('removed', strtolower($data['datasets'][1]['label']));
Assert::assertSame('total', strtolower($data['datasets'][2]['label']));
Assert::assertSame(4, (int) end($data['datasets'][0]['data'])); // Added for today.
Assert::assertSame(0, (int) end($data['datasets'][1]['data'])); // Removed for today.
Assert::assertSame(4, (int) end($data['datasets'][2]['data'])); // Total for today.
// To make this interesting, lets' remove some contacts to see what happens.
$segmentModel->removeLead($contacts[1], $segment); // Emulating removing by a filter.
$segmentModel->removeLead($contacts[2], $segment, true); // Manually removed.
$data = $segmentModel->getSegmentContactsLineChartData(
'd',
new \DateTime('1 month ago', new \DateTimeZone('UTC')),
new \DateTime('now', new \DateTimeZone('UTC')),
null,
['leadlist_id' => ['value' => $segment->getId(), 'list_column_name' => 't.lead_id']]
);
Assert::assertSame(4, (int) end($data['datasets'][0]['data'])); // Added for today.
Assert::assertSame(2, (int) end($data['datasets'][1]['data'])); // Removed for today.
Assert::assertSame(2, (int) end($data['datasets'][2]['data'])); // Total for today.
}
public function testSegmentLineChartDataWithoutFetchDataFromLeadListTable(): void
{
/** @var ListModel $segmentModel */
$segmentModel = static::getContainer()->get('mautic.lead.model.list');
/** @var LeadRepository $contactRepository */
$contactRepository = $this->em->getRepository(Lead::class);
$segment = new LeadList();
$segment->setName('Segment A');
$segmentModel->saveEntity($segment);
$contacts = [new Lead()];
$contactRepository->saveEntities($contacts);
// Adding record in mautic_lead_lists_leads before 11 second from mautic_lead_event_log
// using old code there should be double records means 2 but now it will show only 1 contact
$segmentModel->addLead($contacts[0], $segment, true, false, 1, new \DateTime('-11 seconds', new \DateTimeZone('UTC'))); // Emulating adding by a filter.
$data = $segmentModel->getSegmentContactsLineChartData(
'd',
new \DateTime('-2 days', new \DateTimeZone('UTC')),
new \DateTime('now', new \DateTimeZone('UTC')),
null,
['leadlist_id' => ['value' => $segment->getId(), 'list_column_name' => 't.lead_id']]
);
// using old code there should be only 1 label added but now there should be all 3 labels
Assert::assertSame('added', strtolower($data['datasets'][0]['label']));
Assert::assertSame('removed', strtolower($data['datasets'][1]['label']));
Assert::assertSame('total', strtolower($data['datasets'][2]['label']));
Assert::assertSame(1, (int) end($data['datasets'][0]['data'])); // Added for today.
Assert::assertSame(0, (int) end($data['datasets'][1]['data'])); // Removed for today.
Assert::assertSame(1, (int) end($data['datasets'][2]['data'])); // Total for today.
// To make this interesting, lets' remove some contacts to see what happens.
$segmentModel->removeLead($contacts[0], $segment, true);
$data = $segmentModel->getSegmentContactsLineChartData(
'd',
new \DateTime('-2 days', new \DateTimeZone('UTC')),
new \DateTime('now', new \DateTimeZone('UTC')),
null,
['leadlist_id' => ['value' => $segment->getId(), 'list_column_name' => 't.lead_id']]
);
Assert::assertSame(1, (int) end($data['datasets'][0]['data'])); // Added for today.
Assert::assertSame(1, (int) end($data['datasets'][1]['data'])); // Removed for today.
Assert::assertSame(0, (int) end($data['datasets'][2]['data'])); // Total for today.
}
private function createLeadList(User $user, string $name, bool $isGlobal): LeadList
{
$leadList = new LeadList();
$leadList->setName($name);
$leadList->setPublicName('Public'.$name);
$leadList->setAlias(mb_strtolower($name));
$leadList->setCreatedBy($user);
$leadList->setIsGlobal($isGlobal);
$this->em->persist($leadList);
return $leadList;
}
}

View File

@@ -0,0 +1,402 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Tests\Model;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CategoryBundle\Model\CategoryModel;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\LeadBundle\Entity\LeadListRepository;
use Mautic\LeadBundle\Helper\SegmentCountCacheHelper;
use Mautic\LeadBundle\Model\ListModel;
use Mautic\LeadBundle\Segment\ContactSegmentService;
use Mautic\LeadBundle\Segment\Stat\SegmentChartQueryFactory;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class ListModelTest extends TestCase
{
/**
* @var MockObject
*/
protected $fixture;
private ListModel $model;
/**
* @var LeadListRepository|MockObject
*/
private MockObject $leadListRepositoryMock;
/**
* @var SegmentCountCacheHelper|MockObject
*/
private MockObject $segmentCountCacheHelper;
/**
* @var ContactSegmentService|MockObject
*/
private MockObject $contactSegmentServiceMock;
protected function setUp(): void
{
$eventDispatcherInterfaceMock = $this->createMock(EventDispatcherInterface::class);
$eventDispatcherInterfaceMock->method('dispatch');
$loggerMock = $this->createMock(LoggerInterface::class);
$translatorMock = $this->createMock(Translator::class);
$this->leadListRepositoryMock = $this->createMock(LeadListRepository::class);
$entityManagerMock = $this->createMock(EntityManager::class);
$entityManagerMock->method('getRepository')
->willReturn($this->leadListRepositoryMock);
$coreParametersHelperMock = $this->createMock(CoreParametersHelper::class);
$this->contactSegmentServiceMock = $this->createMock(ContactSegmentService::class);
$segmentChartQueryFactoryMock = $this->createMock(SegmentChartQueryFactory::class);
$this->segmentCountCacheHelper = $this->createMock(SegmentCountCacheHelper::class);
$requestStackMock = $this->createMock(RequestStack::class);
$categoryModelMock = $this->createMock(CategoryModel::class);
$doNotContactRepositoryMock = $this->createMock(\Mautic\LeadBundle\Entity\DoNotContactRepository::class);
$this->model = new ListModel(
$categoryModelMock,
$coreParametersHelperMock,
$this->contactSegmentServiceMock,
$segmentChartQueryFactoryMock,
$requestStackMock,
$this->segmentCountCacheHelper,
$doNotContactRepositoryMock,
$entityManagerMock,
$this->createMock(CorePermissions::class),
$eventDispatcherInterfaceMock,
$this->createMock(UrlGeneratorInterface::class),
$translatorMock,
$this->createMock(UserHelper::class),
$loggerMock
);
}
/**
* @param string|null $sourceType
*/
#[\PHPUnit\Framework\Attributes\DataProvider('sourceTypeTestDataProvider')]
public function testGetSourceLists(array $getLookupResultsReturn, $sourceType, array $expected): void
{
$this->prepareMockForTestGetSourcesLists($getLookupResultsReturn);
$result = $this->fixture->getSourceLists($sourceType);
$this->assertEquals($expected, $result);
}
private function prepareMockForTestGetSourcesLists(array $getLookupResultsReturn): void
{
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$leadSegment = $this->createMock(ContactSegmentService::class);
$segmentChartQueryFactory = $this->createMock(SegmentChartQueryFactory::class);
$requestStack = $this->createMock(RequestStack::class);
$categoryModel = $this->createMock(CategoryModel::class);
$categoryModel->expects($this->once())->method('getLookupResults')->willReturn($getLookupResultsReturn);
$segmentCountCacheHelperMock = $this->createMock(SegmentCountCacheHelper::class);
$doNotContactRepositoryMock = $this->createMock(\Mautic\LeadBundle\Entity\DoNotContactRepository::class);
$mockListModel = $this->getMockBuilder(ListModel::class)
->setConstructorArgs([
$categoryModel,
$coreParametersHelper,
$leadSegment,
$segmentChartQueryFactory,
$requestStack,
$segmentCountCacheHelperMock,
$doNotContactRepositoryMock,
$this->createMock(EntityManagerInterface::class),
$this->createMock(CorePermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->createMock(UrlGeneratorInterface::class),
$this->createMock(Translator::class),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class)])
->onlyMethods([])
->getMock();
$this->fixture = $mockListModel;
}
public static function sourceTypeTestDataProvider(): array
{
return [
[
[],
'categories',
[],
],
[
[
0 => ['id' => 1, 'title' => 'Segment Test Category 1', 'bundle' => 'segment'],
1 => ['id' => 2, 'title' => 'Segment Test Category 2', 'bundle' => 'segment'],
],
null,
[
'categories' => [
1 => 'Segment Test Category 1',
2 => 'Segment Test Category 2',
],
],
],
[
[
0 => ['id' => 1, 'title' => 'Segment Test Category 1', 'bundle' => 'segment'],
1 => ['id' => 2, 'title' => 'Segment Test Category 2', 'bundle' => 'segment'],
],
'categories',
[
1 => 'Segment Test Category 1',
2 => 'Segment Test Category 2',
],
],
[
[],
null,
[
'categories' => [],
],
],
];
}
public function testSegmentRebuildCountCacheGetsUpdated(): void
{
$leadList = $this->mockLeadList(765);
$segmentId = $leadList->getId();
$leadCount = 433;
$this->leadListRepositoryMock
->expects(self::once())
->method('getLeadCount')
->with($segmentId)
->willReturn($leadCount);
$this->segmentCountCacheHelper
->expects(self::once())
->method('setSegmentContactCount')
->with($segmentId, $leadCount);
$newLeadsCount[$segmentId] = [
'maxId' => 0,
'count' => 0,
];
$this->contactSegmentServiceMock
->expects(self::once())
->method('getNewLeadListLeadsCount')
->with($leadList)
->willReturn($newLeadsCount);
$orphanLeadsCount[$segmentId] = [
'maxId' => 0,
'count' => 0,
];
$this->contactSegmentServiceMock
->expects(self::once())
->method('getOrphanedLeadListLeadsCount')
->with($leadList)
->willReturn($orphanLeadsCount);
self::assertSame(0, $this->model->rebuildListLeads($leadList));
$this->segmentCountCacheHelper
->expects(self::once())
->method('getSegmentContactCount')
->with($segmentId)
->willReturn($leadCount);
$leadCounts = $this->model->getSegmentContactCountFromCache([$segmentId]);
self::assertSame([$segmentId => $leadCount], $leadCounts);
}
public function testRemoveLeadWillDecrementCacheCounter(): void
{
$leadList = $this->mockLeadList(765);
$segmentId = $leadList->getId();
$lead = $this->mockLead(100);
$currentLeadCount = 100;
$this->model->removeLead($lead, $leadList);
$this->segmentCountCacheHelper
->expects(self::once())
->method('getSegmentContactCount')
->with($segmentId)
->willReturn($currentLeadCount - 1);
$leadCounts = $this->model->getSegmentContactCountFromCache([$segmentId]);
self::assertSame([$segmentId => $currentLeadCount - 1], $leadCounts);
}
public function testGetSegmentContactCountFromCache(): void
{
$leadList = $this->mockLeadList(765);
$segmentId = $leadList->getId();
$leadCount = 100;
$this->segmentCountCacheHelper
->expects(self::once())
->method('getSegmentContactCount')
->with($segmentId)
->willReturn($leadCount);
$leadCounts = $this->model->getSegmentContactCountFromCache([$segmentId]);
self::assertSame([$segmentId => $leadCount], $leadCounts);
}
public function testAddLeadWillIncrementCacheCounter(): void
{
$leadList = $this->mockLeadList(765);
$segmentId = $leadList->getId();
$lead = $this->mockLead(100);
$currentLeadCount = 100;
$this->model->addLead($lead, $leadList);
$this->segmentCountCacheHelper
->expects(self::once())
->method('getSegmentContactCount')
->with($segmentId)
->willReturn($currentLeadCount + 1);
$leadCounts = $this->model->getSegmentContactCountFromCache([$segmentId]);
self::assertSame([$segmentId => $currentLeadCount + 1], $leadCounts);
}
public function testGetSegmentContactCountFromDatabaseHavingCache(): void
{
$leadList = $this->mockLeadList(765);
$segmentId = $leadList->getId();
$leadCount = 100;
$this->segmentCountCacheHelper
->expects(self::once())
->method('hasSegmentContactCount')
->with($segmentId)
->willReturn(true);
$this->segmentCountCacheHelper
->expects(self::once())
->method('getSegmentContactCount')
->with($segmentId)
->willReturn($leadCount);
$leadCounts = $this->model->getSegmentContactCount([$segmentId]);
self::assertSame([$segmentId => $leadCount], $leadCounts);
}
public function testGetSegmentContactCountFromDatabase(): void
{
$leadList = $this->mockLeadList(765);
$segmentId = $leadList->getId();
$leadCount = 100;
$this->segmentCountCacheHelper
->expects(self::once())
->method('hasSegmentContactCount')
->with($segmentId)
->willReturn(false);
$this->leadListRepositoryMock
->expects(self::once())
->method('getLeadCount')
->with($segmentId)
->willReturn($leadCount);
$leadCounts = $this->model->getSegmentContactCount([$segmentId]);
self::assertSame([$segmentId => $leadCount], $leadCounts);
}
public function testGetActiveSegmentContactCount(): void
{
$segmentId = 123;
$total = 10;
$dnc = 3;
$this->leadListRepositoryMock
->expects(self::once())
->method('getLeadCount')
->with($segmentId)
->willReturn($total);
$doNotContactRepository = $this->createMock(\Mautic\LeadBundle\Entity\DoNotContactRepository::class);
$doNotContactRepository
->expects(self::once())
->method('getCount')
->with(null, null, null, $segmentId)
->willReturn($dnc);
$reflection = new \ReflectionClass($this->model);
$property = $reflection->getProperty('doNotContactRepository');
$property->setAccessible(true);
$property->setValue($this->model, $doNotContactRepository);
$active = $this->model->getActiveSegmentContactCount($segmentId);
self::assertSame($total - $dnc, $active);
}
public function testLeadListExists(): void
{
$leadList = $this->mockLeadList(765);
$segmentId = $leadList->getId();
$this->leadListRepositoryMock->expects(self::once())
->method('leadListExists')
->with($segmentId)
->willReturn(true);
self::assertTrue($this->model->leadListExists($segmentId));
}
private function mockLeadList(int $id): LeadList
{
return new class($id) extends LeadList {
public function __construct(
private int $id,
) {
parent::__construct();
}
public function getId(): int
{
return $this->id;
}
};
}
private function mockLead(int $id): Lead
{
return new class($id) extends Lead {
public function __construct(
private int $id,
) {
parent::__construct();
}
public function getId(): int
{
return $this->id;
}
};
}
}

View File

@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Tests\Model;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Model\SegmentActionModel;
class SegmentActionModelTest extends \PHPUnit\Framework\TestCase
{
private \PHPUnit\Framework\MockObject\MockObject $contactMock5;
private \PHPUnit\Framework\MockObject\MockObject $contactMock6;
private \PHPUnit\Framework\MockObject\MockObject $contactModelMock;
private SegmentActionModel $actionModel;
protected function setUp(): void
{
$this->contactMock5 = $this->createMock(Lead::class);
$this->contactMock6 = $this->createMock(Lead::class);
$this->contactModelMock = $this->createMock(LeadModel::class);
$this->actionModel = new SegmentActionModel($this->contactModelMock);
}
public function testAddContactsToSegmentsEntityAccess(): void
{
$contacts = [5, 6];
$segments = [4, 5];
$this->contactModelMock->expects($this->once())
->method('getLeadsByIds')
->with($contacts)
->willReturn([$this->contactMock5, $this->contactMock6]);
$matcher = $this->exactly(2);
$this->contactModelMock->expects($matcher)
->method('canEditContact')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock5, $parameters[0]);
return false;
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock6, $parameters[0]);
return true;
}
});
$this->contactModelMock->expects($this->once())
->method('addToLists')
->with($this->contactMock6, $segments);
$this->contactModelMock->expects($this->once())
->method('saveEntities')
->with([$this->contactMock5, $this->contactMock6]);
$this->actionModel->addContacts($contacts, $segments);
}
public function testRemoveContactsFromSementsEntityAccess(): void
{
$contacts = [5, 6];
$segments = [1, 2];
$this->contactModelMock->expects($this->once())
->method('getLeadsByIds')
->with($contacts)
->willReturn([$this->contactMock5, $this->contactMock6]);
$matcher = $this->exactly(2);
$this->contactModelMock->expects($matcher)
->method('canEditContact')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock5, $parameters[0]);
return false;
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock6, $parameters[0]);
return true;
}
});
$this->contactModelMock->expects($this->once())
->method('removeFromLists')
->with($this->contactMock6, $segments);
$this->contactModelMock->expects($this->once())
->method('saveEntities')
->with([$this->contactMock5, $this->contactMock6]);
$this->actionModel->removeContacts($contacts, $segments);
}
public function testAddContactsToSegments(): void
{
$contacts = [5, 6];
$segments = [1, 2];
$this->contactModelMock->expects($this->once())
->method('getLeadsByIds')
->with($contacts)
->willReturn([$this->contactMock5, $this->contactMock6]);
$matcher = $this->exactly(2);
$this->contactModelMock->expects($matcher)
->method('canEditContact')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock5, $parameters[0]);
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock6, $parameters[0]);
}
return true;
});
$matcher = $this->exactly(2);
$this->contactModelMock->expects($matcher)
->method('addToLists')->willReturnCallback(function (...$parameters) use ($matcher, $segments) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock5, $parameters[0]);
$this->assertSame($segments, $parameters[1]);
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock6, $parameters[0]);
$this->assertSame($segments, $parameters[1]);
}
});
$this->contactModelMock->expects($this->once())
->method('saveEntities')
->with([$this->contactMock5, $this->contactMock6]);
$this->actionModel->addContacts($contacts, $segments);
}
public function testRemoveContactsFromCategories(): void
{
$contacts = [5, 6];
$segments = [1, 2];
$this->contactModelMock->expects($this->once())
->method('getLeadsByIds')
->with($contacts)
->willReturn([$this->contactMock5, $this->contactMock6]);
$matcher = $this->exactly(2);
$this->contactModelMock->expects($matcher)
->method('canEditContact')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock5, $parameters[0]);
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock6, $parameters[0]);
}
return true;
});
$matcher = $this->exactly(2);
$this->contactModelMock->expects($matcher)
->method('removeFromLists')->willReturnCallback(function (...$parameters) use ($matcher, $segments) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock5, $parameters[0]);
$this->assertSame($segments, $parameters[1]);
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame($this->contactMock6, $parameters[0]);
}
});
$this->contactModelMock->expects($this->once())
->method('saveEntities')
->with([$this->contactMock5, $this->contactMock6]);
$this->actionModel->removeContacts($contacts, $segments);
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Tests\Model;
use Mautic\CategoryBundle\Entity\Category;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
use Mautic\LeadBundle\Model\LeadModel;
final class SetFrequencyRulesFunctionalTest extends MauticMysqlTestCase
{
use CreateTestEntitiesTrait;
public function testSetFrequencyRulesForCategorySubscriptionUnsubscription(): void
{
$categoriesFlags = [
'one' => true,
'two' => false,
'three' => true,
'four' => true,
'five' => false,
];
$categories = $this->createCategories($categoriesFlags);
$lead = $this->createLead('John', 'Doe', 'some@test.com');
$this->em->flush();
// Subscribe categories.
$categoriesToSubscribe = [];
$categoriesToUnsubscribe = [];
foreach ($categories as $category) {
$categoriesToSubscribe[$category->getId()] = $category->getId();
if (!$categoriesFlags[$category->getTitle()]) {
$categoriesToUnsubscribe[$category->getId()] = $category->getId();
}
}
$data = [
'global_categories' => array_keys($categoriesToSubscribe),
'lead_lists' => [],
];
/** @var LeadModel $model */
$model = static::getContainer()->get('mautic.lead.model.lead');
$model->setFrequencyRules($lead, $data, [], []);
$subscribedCategories = $model->getLeadCategories($lead);
$this->assertEmpty(array_diff($subscribedCategories, array_keys($categoriesToSubscribe)));
// Unsubscribe categories.
$data['global_categories'] = array_keys($categoriesToUnsubscribe);
$model->setFrequencyRules($lead, $data, [], []);
$unsubscribedCategories = $model->getUnsubscribedLeadCategoriesIds($lead);
$this->assertEmpty(array_diff($unsubscribedCategories, array_keys($categoriesToSubscribe)));
}
/**
* @param mixed[] $cats
*
* @return Category[]
*/
private function createCategories(array $cats): array
{
$categories = [];
foreach ($cats as $suffix => $flag) {
$categories[$suffix] = $this->createCategory($suffix, $suffix);
}
$this->em->flush();
return $categories;
}
}