Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Command\CleanupExportedFilesCommand;
|
||||
use Mautic\LeadBundle\Command\ContactScheduledExportCommand;
|
||||
use Mautic\LeadBundle\Entity\ContactExportScheduler;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class CleanupExportedFilesCommandFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['clear_export_files_after_days'] = 0;
|
||||
$this->configParams['contact_export_dir'] = '/tmp';
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testCleanupContactExportFiles(): void
|
||||
{
|
||||
$filePath = $this->exportContactToCsvFile();
|
||||
|
||||
$this->testSymfonyCommand(CleanupExportedFilesCommand::COMMAND_NAME);
|
||||
Assert::assertFileDoesNotExist($filePath);
|
||||
}
|
||||
|
||||
private function exportContactToCsvFile(): string
|
||||
{
|
||||
$this->createContacts();
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
's/contacts/batchExport',
|
||||
['filetype' => 'csv']
|
||||
);
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
$contactExportSchedulerRows = $this->checkContactExportScheduler(1);
|
||||
/** @var ContactExportScheduler $contactExportScheduler */
|
||||
$contactExportScheduler = $contactExportSchedulerRows[0];
|
||||
$this->testSymfonyCommand(ContactScheduledExportCommand::COMMAND_NAME, ['--ids' => $contactExportScheduler->getId()]);
|
||||
|
||||
/** @var CoreParametersHelper $coreParametersHelper */
|
||||
$coreParametersHelper = self::getContainer()->get('mautic.helper.core_parameters');
|
||||
$zipFileName = 'contacts_export_'.$contactExportScheduler->getScheduledDateTime()
|
||||
->format('Y_m_d_H_i_s').'.zip';
|
||||
$filePath = $coreParametersHelper->get('contact_export_dir').'/'.$zipFileName;
|
||||
Assert::assertFileExists($filePath);
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
private function createContacts(): void
|
||||
{
|
||||
$contacts = [];
|
||||
|
||||
for ($i = 1; $i <= 2; ++$i) {
|
||||
$contact = new Lead();
|
||||
$contact
|
||||
->setFirstname('ContactFirst'.$i)
|
||||
->setLastname('ContactLast'.$i)
|
||||
->setEmail('FirstLast'.$i.'@email.com');
|
||||
$contacts[] = $contact;
|
||||
}
|
||||
|
||||
$leadModel = self::getContainer()->get('mautic.lead.model.lead');
|
||||
$leadModel->saveEntities($contacts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
private function checkContactExportScheduler(int $count): array
|
||||
{
|
||||
$repo = $this->em->getRepository(ContactExportScheduler::class);
|
||||
$allRows = $repo->findAll();
|
||||
Assert::assertCount($count, $allRows);
|
||||
|
||||
return $allRows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\ExitCode;
|
||||
use Mautic\CoreBundle\ProcessSignal\Exception\SignalCaughtException;
|
||||
use Mautic\CoreBundle\ProcessSignal\ProcessSignalService;
|
||||
use Mautic\CoreBundle\Twig\Helper\DateHelper;
|
||||
use Mautic\CoreBundle\Twig\Helper\FormatterHelper;
|
||||
use Mautic\LeadBundle\Command\ContactScheduledExportCommand;
|
||||
use Mautic\LeadBundle\Entity\ContactExportScheduler;
|
||||
use Mautic\LeadBundle\Entity\ContactExportSchedulerRepository;
|
||||
use Mautic\LeadBundle\Model\ContactExportSchedulerModel;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ContactScheduledExportCommandTest extends TestCase
|
||||
{
|
||||
public function testForSignalCaughtException(): void
|
||||
{
|
||||
$contactExportScheduledModel = $this->createMock(ContactExportSchedulerModel::class);
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$dateHelper = new DateHelper(
|
||||
'F j, Y g:i a T',
|
||||
'D, M d',
|
||||
'F j, Y',
|
||||
'g:i a',
|
||||
$translator,
|
||||
$coreParametersHelper
|
||||
);
|
||||
|
||||
$formatterHelper = new FormatterHelper($dateHelper, $translator);
|
||||
$processSignalService = $this->createMock(ProcessSignalService::class);
|
||||
|
||||
$contactExportSchedulerRepository = $this->createMock(ContactExportSchedulerRepository::class);
|
||||
$contactExportSchedulerRepository->method('findBy')
|
||||
->willReturn([new ContactExportScheduler()]);
|
||||
|
||||
$contactExportScheduledModel->method('getRepository')
|
||||
->willReturn($contactExportSchedulerRepository);
|
||||
|
||||
$eventDispatcher->expects($this->once())
|
||||
->method('dispatch')
|
||||
->willThrowException(new SignalCaughtException(1));
|
||||
|
||||
$command = new class($contactExportScheduledModel, $eventDispatcher, $formatterHelper, $processSignalService) extends ContactScheduledExportCommand {
|
||||
public function getExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
return $this->execute($input, $output);
|
||||
}
|
||||
};
|
||||
|
||||
$inputInterfaceMock = $this->createMock(InputInterface::class);
|
||||
$outputInterfaceMock = $this->createMock(OutputInterface::class);
|
||||
|
||||
$inputInterfaceMock->method('getOption')
|
||||
->with('ids')
|
||||
->willReturn(1);
|
||||
|
||||
$this->assertSame(ExitCode::TERMINATED, $command->getExecute($inputInterfaceMock, $outputInterfaceMock));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Field\BackgroundService;
|
||||
use Mautic\LeadBundle\Field\Command\CreateCustomFieldCommand;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class CreateCustomFieldCommandTest extends TestCase
|
||||
{
|
||||
private BackgroundService $backgroundServiceMock;
|
||||
|
||||
private TranslatorInterface $translatorMock;
|
||||
|
||||
private LeadFieldRepository $leadFieldRepositoryMock;
|
||||
|
||||
private PathsHelper $pathsHelperMock;
|
||||
|
||||
private CoreParametersHelper $coreParametersHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->backgroundServiceMock = $this->createMock(BackgroundService::class);
|
||||
$this->translatorMock = $this->createMock(TranslatorInterface::class);
|
||||
$this->leadFieldRepositoryMock = $this->createMock(LeadFieldRepository::class);
|
||||
$this->pathsHelperMock = $this->createMock(PathsHelper::class);
|
||||
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('completeRunMethodProvider')]
|
||||
public function testCompleteRunMethodIsCalled(bool $checkRunStatusResult, int $completeRunExpected): void
|
||||
{
|
||||
$command = $this->getMockBuilder(CreateCustomFieldCommand::class)
|
||||
->setConstructorArgs([
|
||||
$this->backgroundServiceMock,
|
||||
$this->translatorMock,
|
||||
$this->leadFieldRepositoryMock,
|
||||
$this->pathsHelperMock,
|
||||
$this->coreParametersHelper,
|
||||
])
|
||||
->onlyMethods(['completeRun', 'checkRunStatus'])
|
||||
->getMock();
|
||||
|
||||
$command->expects($this->once())->method('checkRunStatus')->willReturn($checkRunStatusResult);
|
||||
$command->expects($this->exactly($completeRunExpected))->method('completeRun');
|
||||
|
||||
$input = new ArrayInput([
|
||||
'--id' => '123',
|
||||
]);
|
||||
$output = new BufferedOutput();
|
||||
$command->run($input, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, bool|int>>
|
||||
*/
|
||||
public static function completeRunMethodProvider(): array
|
||||
{
|
||||
return [
|
||||
[true, 1], // `completeRun` should be called once
|
||||
[false, 0], // `completeRun` should never be called
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Command\DeduplicateCommand;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class DeduplicateCommandFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
if ('testDeduplicateCommandWithAnotherUniqueFieldAndAnd' === $this->name()) {
|
||||
$this->configParams['contact_unique_identifiers_operator'] = CompositeExpression::TYPE_AND;
|
||||
}
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testDeduplicateCommandWithUniqueEmail(): void
|
||||
{
|
||||
$contactRepository = $this->em->getRepository(Lead::class);
|
||||
|
||||
$contactDeduper = static::getContainer()->get('mautic.lead.deduper');
|
||||
|
||||
Assert::assertSame(0, $contactRepository->count([]), 'Some contacts were forgotten to remove from other tests');
|
||||
|
||||
$this->saveContact('john@doe.email'); // 1
|
||||
$this->saveContact('john@doe.email'); // 1
|
||||
$this->saveContact('john@doe.email'); // 1
|
||||
$this->saveContact('john@doe.email'); // 1
|
||||
$this->saveContact('anna@munic.email'); // 2
|
||||
$this->saveContact('anna@munic.email'); // 2
|
||||
$this->saveContact('jane@gabriel.email'); // 3
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
Assert::assertSame(7, $contactRepository->count([]));
|
||||
|
||||
Assert::assertSame(
|
||||
2,
|
||||
$contactDeduper->countDuplicatedContacts(array_keys($contactDeduper->getUniqueFields('lead'))),
|
||||
'The deduper should see and process only 2 duplicated contacts. The third is unique.'
|
||||
);
|
||||
|
||||
$output = $this->testSymfonyCommand(DeduplicateCommand::NAME);
|
||||
|
||||
Assert::assertSame(3, $contactRepository->count([]), $output->getDisplay());
|
||||
}
|
||||
|
||||
public function testDeduplicateCommandWithAnotherUniqueFieldAndAnd(): void
|
||||
{
|
||||
$contactRepository = $this->em->getRepository(Lead::class);
|
||||
|
||||
$fieldRepository = $this->em->getRepository(LeadField::class);
|
||||
|
||||
Assert::assertSame(0, $contactRepository->count([]), 'Some contacts were forgotten to remove from other tests');
|
||||
|
||||
$this->saveContact('john@doe.email', '111111111'); // 1
|
||||
$this->saveContact('john@doe.email', '111111111'); // 1
|
||||
$this->saveContact('john@doe.email', '222222222'); // 2
|
||||
$this->saveContact('john@doe.email', '222222222'); // 2
|
||||
$this->saveContact('anna@munic.email', '333333333'); // 3
|
||||
$this->saveContact('anna@munic.email', '333333333'); // 3
|
||||
$this->saveContact('jane@gabriel.email', '4444444444'); // 4
|
||||
$this->saveContact('jane.gabriel@gmail.com', '4444444444'); // 5
|
||||
|
||||
$phoneField = $fieldRepository->findOneBy(['alias' => 'phone']);
|
||||
\assert($phoneField instanceof LeadField);
|
||||
$phoneField->setIsUniqueIdentifer(true);
|
||||
$phoneField->setLabel('Cell phone'); // Testing also field with more words.
|
||||
$this->em->persist($phoneField);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
Assert::assertSame(8, $contactRepository->count([]));
|
||||
|
||||
$output = $this->testSymfonyCommand(DeduplicateCommand::NAME);
|
||||
|
||||
Assert::assertSame(5, $contactRepository->count([]), $output->getDisplay());
|
||||
}
|
||||
|
||||
public function testDeduplicateCommandWithAnotherUniqueFieldAndOr(): void
|
||||
{
|
||||
$contactRepository = $this->em->getRepository(Lead::class);
|
||||
|
||||
$fieldRepository = $this->em->getRepository(LeadField::class);
|
||||
|
||||
Assert::assertSame(0, $contactRepository->count([]), 'Some contacts were forgotten to remove from other tests');
|
||||
|
||||
$this->saveContact('john@doe.email', '111111111'); // 1
|
||||
$this->saveContact('john@doe.email', '111111111'); // 1
|
||||
$this->saveContact('john@doe.email', '222222222'); // 1
|
||||
$this->saveContact('john@doe.email', '222222222'); // 1
|
||||
$this->saveContact('anna@munic.email', '333333333'); // 2
|
||||
$this->saveContact('anna@munic.email', '333333333'); // 2
|
||||
$this->saveContact('jane@gabriel.email', '4444444444'); // 3
|
||||
$this->saveContact('jane.gabriel@gmail.com', '4444444444'); // 3
|
||||
|
||||
$phoneField = $fieldRepository->findOneBy(['alias' => 'phone']);
|
||||
\assert($phoneField instanceof LeadField);
|
||||
$phoneField->setIsUniqueIdentifer(true);
|
||||
$phoneField->setLabel('Cell phone'); // Testing also field with more words.
|
||||
$this->em->persist($phoneField);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
Assert::assertSame(8, $contactRepository->count([]));
|
||||
|
||||
$output = $this->testSymfonyCommand(DeduplicateCommand::NAME);
|
||||
|
||||
Assert::assertSame(3, $contactRepository->count([]), $output->getDisplay());
|
||||
}
|
||||
|
||||
private function saveContact(string $email, ?string $phone = null): Lead
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail($email);
|
||||
$contact->setPhone($phone);
|
||||
$contact->setDateIdentified(new \DateTime());
|
||||
|
||||
$this->em->persist($contact);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Command\DeduplicateIdsCommand;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class DeduplicateIdsCommandFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testDeduplicateCommandWithContactIdsParam(): void
|
||||
{
|
||||
$contactRepository = $this->em->getRepository(Lead::class);
|
||||
|
||||
Assert::assertSame(0, $contactRepository->count([]), 'Some contacts were forgotten to remove from other tests');
|
||||
|
||||
$contact1 = $this->saveContact('john@doe.email');
|
||||
$this->saveContact('john@doe.email');
|
||||
$contact2 = $this->saveContact('jane@doe.email');
|
||||
$this->saveContact('jane@doe.email');
|
||||
$contact3 = $this->saveContact('anna@munic.email');
|
||||
$this->saveContact('anna@munic.email');
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
Assert::assertSame(6, $contactRepository->count([]));
|
||||
|
||||
$this->testSymfonyCommand(DeduplicateIdsCommand::NAME, ['--contact-ids' => "{$contact1->getId()},{$contact2->getId()},{$contact3->getId()}"]);
|
||||
|
||||
Assert::assertSame(3, $contactRepository->count([]));
|
||||
}
|
||||
|
||||
private function saveContact(string $email): Lead
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail($email);
|
||||
$contact->setDateIdentified(new \DateTime());
|
||||
|
||||
$this->em->persist($contact);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Command\DeleteContactSecondaryCompaniesCommand;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\CompanyLead;
|
||||
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
|
||||
final class DeleteContactSecondaryCompaniesCommandTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
public function testDeleteContactSecondaryCompanies(): void
|
||||
{
|
||||
$contact = $this->getContactWithCompanies();
|
||||
/** @var CompanyLeadRepository $companyLeadRepo */
|
||||
$companyLeadRepo = $this->em->getRepository(CompanyLead::class);
|
||||
|
||||
$contactCompanies = $companyLeadRepo->getCompaniesByLeadId($contact->getId());
|
||||
self::assertEquals(2, count($contactCompanies));
|
||||
|
||||
$this->testSymfonyCommand(DeleteContactSecondaryCompaniesCommand::NAME);
|
||||
|
||||
$contactCompanies = $companyLeadRepo->getCompaniesByLeadId($contact->getId());
|
||||
self::assertEquals(2, count($contactCompanies));
|
||||
|
||||
$this->setUpSymfony(['contact_allow_multiple_companies' => 0]);
|
||||
$this->testSymfonyCommand(DeleteContactSecondaryCompaniesCommand::NAME);
|
||||
|
||||
$contactCompanies = $companyLeadRepo->getCompaniesByLeadId($contact->getId());
|
||||
self::assertEquals(1, count($contactCompanies));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
protected function getContactWithCompanies(): Lead
|
||||
{
|
||||
$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 = self::getContainer()->get('mautic.lead.model.lead');
|
||||
$this->assertTrue($leadModel->addToCompany($contact, $company));
|
||||
$this->assertTrue($leadModel->addToCompany($contact, $company2));
|
||||
|
||||
return $contact;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Model\NotificationModel;
|
||||
use Mautic\CoreBundle\ProcessSignal\ProcessSignalService;
|
||||
use Mautic\LeadBundle\Command\ImportCommand;
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Model\ImportModel;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Mautic\UserBundle\Security\UserTokenSetter;
|
||||
use Monolog\Logger;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ImportCommandTest extends TestCase
|
||||
{
|
||||
public function testExecuteFailsIfModifiedByIsNotSet(): void
|
||||
{
|
||||
$translatorMock = $this->createMock(TranslatorInterface::class);
|
||||
$translatorMock->method('trans')->willReturnCallback(fn ($id) => $id);
|
||||
$importMock = $this->createMock(Import::class);
|
||||
$importModelMock = $this->createMock(ImportModel::class);
|
||||
$loggerMock = $this->createMock(Logger::class);
|
||||
$notificationMock = $this->createMock(NotificationModel::class);
|
||||
$userModelMock = $this->createMock(UserModel::class);
|
||||
$tokenStorageMock = $this->createMock(TokenStorage::class);
|
||||
$userTokenSetter = new UserTokenSetter($userModelMock, $tokenStorageMock);
|
||||
|
||||
$importModelMock->expects($this->once())
|
||||
->method('getImportToProcess')
|
||||
->willReturn($importMock);
|
||||
|
||||
$importCommand = new class($translatorMock, $importModelMock, new ProcessSignalService(), $userTokenSetter, $loggerMock, $notificationMock) extends ImportCommand {
|
||||
public function getExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
return $this->execute($input, $output);
|
||||
}
|
||||
};
|
||||
$inputInterfaceMock = $this->createMock(InputInterface::class);
|
||||
$outputInterfaceMock = $this->createMock(OutputInterface::class);
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Import does not have "modifiedBy" property set.');
|
||||
$importCommand->getExecute($inputInterfaceMock, $outputInterfaceMock);
|
||||
}
|
||||
|
||||
public function testExecute(): void
|
||||
{
|
||||
// Translator
|
||||
$translatorMock = $this->createMock(TranslatorInterface::class);
|
||||
$translatorMock->method('trans')->willReturnCallback(fn ($id) => $id);
|
||||
|
||||
// Import entity
|
||||
$importMock = $this->createMock(Import::class);
|
||||
$importMock->expects($this->once())
|
||||
->method('getModifiedBy')
|
||||
->willReturn(42);
|
||||
$importMock->method('getProcessedRows')->willReturn(1);
|
||||
$importMock->method('getInsertedCount')->willReturn(1);
|
||||
$importMock->method('getUpdatedCount')->willReturn(0);
|
||||
$importMock->method('getIgnoredCount')->willReturn(0);
|
||||
|
||||
// Import Model Mock
|
||||
$importModelMock = $this->createMock(ImportModel::class);
|
||||
$importModelMock->expects($this->once())
|
||||
->method('getEntity')
|
||||
->with(42)
|
||||
->willReturn($importMock);
|
||||
|
||||
// User Token Setter
|
||||
$user = new User();
|
||||
$userModelMock = $this->createMock(UserModel::class);
|
||||
$userModelMock->expects($this->once())
|
||||
->method('getEntity')
|
||||
->with(42)
|
||||
->willReturn($user);
|
||||
$tokenStorageMock = $this->createMock(TokenStorage::class);
|
||||
$tokenStorageMock->expects($this->once())
|
||||
->method('setToken');
|
||||
$userTokenSetter = new UserTokenSetter($userModelMock, $tokenStorageMock);
|
||||
|
||||
$loggerMock = $this->createMock(Logger::class);
|
||||
$notificationMock = $this->createMock(NotificationModel::class);
|
||||
// No notification expected for successful imports - they're handled in ImportModel
|
||||
|
||||
$importCommand = new class($translatorMock, $importModelMock, new ProcessSignalService(), $userTokenSetter, $loggerMock, $notificationMock) extends ImportCommand {
|
||||
public function getExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
return $this->execute($input, $output);
|
||||
}
|
||||
};
|
||||
|
||||
// InputInterface
|
||||
$inputInterfaceMock = $this->createMock(InputInterface::class);
|
||||
$matcher = $this->exactly(2);
|
||||
$inputInterfaceMock->expects($matcher)->method('getOption')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('id', $parameters[0]);
|
||||
|
||||
return 42;
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('limit', $parameters[0]);
|
||||
|
||||
return 10;
|
||||
}
|
||||
});
|
||||
|
||||
// OutputInterface
|
||||
$outputInterfaceMock = $this->createMock(OutputInterface::class);
|
||||
// Start test
|
||||
$this->assertSame(0, $importCommand->getExecute($inputInterfaceMock, $outputInterfaceMock));
|
||||
}
|
||||
|
||||
public function testExecuteAddsNotificationOnFailure(): void
|
||||
{
|
||||
$translatorMock = $this->createMock(TranslatorInterface::class);
|
||||
$translatorMock->method('trans')->willReturnCallback(fn ($id) => $id);
|
||||
|
||||
$importMock = $this->createMock(Import::class);
|
||||
$importMock->expects($this->once())
|
||||
->method('getModifiedBy')
|
||||
->willReturn(42);
|
||||
$importMock->method('getStatusInfo')->willReturn('fail');
|
||||
$importMock->method('getProcessedRows')->willReturn(1);
|
||||
$importMock->method('getInsertedCount')->willReturn(0);
|
||||
$importMock->method('getUpdatedCount')->willReturn(0);
|
||||
$importMock->method('getIgnoredCount')->willReturn(1);
|
||||
|
||||
$importModelMock = $this->createMock(ImportModel::class);
|
||||
$importModelMock->expects($this->once())
|
||||
->method('getEntity')
|
||||
->with(42)
|
||||
->willReturn($importMock);
|
||||
$importModelMock->expects($this->once())
|
||||
->method('beginImport')
|
||||
->willThrowException(new \Mautic\LeadBundle\Exception\ImportFailedException('fail'));
|
||||
|
||||
$user = new User();
|
||||
$userModelMock = $this->createMock(UserModel::class);
|
||||
$userModelMock->expects($this->once())
|
||||
->method('getEntity')
|
||||
->with(42)
|
||||
->willReturn($user);
|
||||
$tokenStorageMock = $this->createMock(TokenStorage::class);
|
||||
$tokenStorageMock->expects($this->once())->method('setToken');
|
||||
$userTokenSetter = new UserTokenSetter($userModelMock, $tokenStorageMock);
|
||||
|
||||
$loggerMock = $this->createMock(Logger::class);
|
||||
$notificationMock = $this->createMock(NotificationModel::class);
|
||||
$notificationMock->expects($this->once())->method('addNotification');
|
||||
|
||||
$importCommand = new class($translatorMock, $importModelMock, new ProcessSignalService(), $userTokenSetter, $loggerMock, $notificationMock) extends ImportCommand {
|
||||
public function getExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
return $this->execute($input, $output);
|
||||
}
|
||||
};
|
||||
|
||||
$inputInterfaceMock = $this->createMock(InputInterface::class);
|
||||
$inputInterfaceMock->method('getOption')->willReturnMap([
|
||||
['id', 42],
|
||||
['limit', 10],
|
||||
]);
|
||||
|
||||
$outputInterfaceMock = $this->createMock(OutputInterface::class);
|
||||
|
||||
$this->assertSame(1, $importCommand->getExecute($inputInterfaceMock, $outputInterfaceMock));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Command\SegmentCountCacheCommand;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadListRepository;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SegmentCountCacheCommandFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testSegmentCountCacheCommand(): void
|
||||
{
|
||||
$contacts = $this->saveContacts();
|
||||
$segment = $this->saveSegment();
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
// Run segments update command.
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segmentId]);
|
||||
|
||||
// Run segment count cache command.
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment cached contact count using the SegmentCountCacheHelper directly
|
||||
$segmentCountCacheHelper = static::getContainer()->get('mautic.helper.segment.count.cache');
|
||||
$count = $segmentCountCacheHelper->getSegmentContactCount($segmentId);
|
||||
self::assertEquals(5, $count, "Expected segment $segmentId to have 5 contacts");
|
||||
|
||||
// Delete 1 contact.
|
||||
$contact = $contacts[0];
|
||||
$this->client->request(Request::METHOD_POST, '/s/contacts/delete/'.$contact->getId());
|
||||
$clientResponse = $this->client->getResponse();
|
||||
self::assertSame(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
|
||||
// Run segment count cache command again.
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment cached contact count using the SegmentCountCacheHelper directly
|
||||
$segmentCountCacheHelper = static::getContainer()->get('mautic.helper.segment.count.cache');
|
||||
$count = $segmentCountCacheHelper->getSegmentContactCount($segmentId);
|
||||
self::assertEquals(4, $count, "Expected segment $segmentId to have 4 contacts");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Lead>
|
||||
*/
|
||||
private function saveContacts(): array
|
||||
{
|
||||
// Add 5 contacts
|
||||
/** @var LeadRepository $contactRepo */
|
||||
$contactRepo = $this->em->getRepository(Lead::class);
|
||||
$contacts = [];
|
||||
|
||||
for ($i = 1; $i <= 5; ++$i) {
|
||||
$contact = new Lead();
|
||||
$contact->setFirstname('Contact '.$i);
|
||||
$contacts[] = $contact;
|
||||
}
|
||||
|
||||
$contactRepo->saveEntities($contacts);
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
private function saveSegment(): LeadList
|
||||
{
|
||||
// Add 1 segment
|
||||
/** @var LeadListRepository $segmentRepo */
|
||||
$segmentRepo = $this->em->getRepository(LeadList::class);
|
||||
$segment = new LeadList();
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'firstname',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'operator' => 'like',
|
||||
'properties' => ['filter' => 'Contact'],
|
||||
],
|
||||
];
|
||||
$segment->setName('Segment A')
|
||||
->setPublicName('Segment A')
|
||||
->setFilters($filters)
|
||||
->setAlias('segment-a');
|
||||
$segmentRepo->saveEntity($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
|
||||
final class SegmentFilterOnUpdateCommandFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testSegmentFilterOnUpdateCommand(): void
|
||||
{
|
||||
$this->saveContacts();
|
||||
$segmentA = $this->saveSegmentA();
|
||||
$segmentAId = $segmentA->getId();
|
||||
|
||||
// Run segments update command.
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segmentAId]);
|
||||
self::assertCount(5, $this->em->getRepository(ListLead::class)->findBy(['list' => $segmentAId]));
|
||||
|
||||
$segmentB = $this->saveSegmentB($segmentAId);
|
||||
$segmentBId = $segmentB->getId();
|
||||
// Run segments update command.
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segmentBId]);
|
||||
self::assertCount(3, $this->em->getRepository(ListLead::class)->findBy(['list' => $segmentBId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead[]
|
||||
*/
|
||||
private function saveContacts(): array
|
||||
{
|
||||
// Add 10 contacts
|
||||
/** @var LeadRepository $contactRepo */
|
||||
$contactRepo = $this->em->getRepository(Lead::class);
|
||||
$contacts = [];
|
||||
|
||||
for ($i = 0; $i <= 10; ++$i) {
|
||||
$contact = new Lead();
|
||||
$contact->setFirstname('fn'.$i);
|
||||
$contact->setLastname('ln'.$i);
|
||||
$contacts[] = $contact;
|
||||
}
|
||||
|
||||
$contactRepo->saveEntities($contacts);
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
private function saveSegmentA(): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$filters = [
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'address1',
|
||||
'type' => 'text',
|
||||
'operator' => '!empty',
|
||||
'properties' => ['filter' => null],
|
||||
// The filter key is deprecated but sometimes it contains rubbish values including a string.
|
||||
'filter' => 'somestring',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'address1',
|
||||
'type' => 'text',
|
||||
'operator' => '!=',
|
||||
'properties' => ['filter' => null],
|
||||
// The filter key is deprecated but sometimes it contains rubbish values including an array.
|
||||
'filter' => ['option A', 'option B'],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'firstname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'fn1'],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'lastname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'ln1'],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'firstname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'fn2'],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'firstname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'fn3'],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'lastname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'ln3'],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'firstname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'fn4'],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'lastname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'ln5'],
|
||||
],
|
||||
];
|
||||
|
||||
$segment->setName('Segment A')
|
||||
->setPublicName('Segment A')
|
||||
->setFilters($filters)
|
||||
->setAlias('segment-a');
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
private function saveSegmentB(int $segmentAId): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$filters = [
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'firstname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'fn6'],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'firstname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'fn2'],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'firstname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'fn3'],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'lastname',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'ln3'],
|
||||
],
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'object' => 'lead',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentAId]],
|
||||
],
|
||||
];
|
||||
|
||||
$segment->setName('Segment B')
|
||||
->setPublicName('Segment B')
|
||||
->setFilters($filters)
|
||||
->setAlias('segment-b');
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
return $segment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadListRepository;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Segment\OperatorOptions;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
class SegmentFiltersFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private const FIELD_NAME = 'car';
|
||||
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
private string $testIdentifier;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->testIdentifier = 'test_'.uniqid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $fieldDetails
|
||||
* @param array<string, mixed> $segmentData
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
#[DataProvider('filtersSegmentsContacts')]
|
||||
public function testFiltersHasCorrectContactsIncludedInSegment(
|
||||
array $fieldDetails,
|
||||
array $segmentData,
|
||||
callable $checkValidContact,
|
||||
): void {
|
||||
$this->saveCustomField($fieldDetails);
|
||||
$contacts = $this->saveContacts();
|
||||
$segment = $this->saveSegment($segmentData);
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
/** @var array<int> $contactIds */
|
||||
$contactIds = [];
|
||||
/** @var array<int> $leadListLeadsIds */
|
||||
$leadListLeadsIds = [];
|
||||
|
||||
// get contacts with valid filter
|
||||
foreach ($contacts as $contact) {
|
||||
if ($checkValidContact($contact)) {
|
||||
$contactIds[] = (int) $contact->getId();
|
||||
}
|
||||
}
|
||||
|
||||
// update the segment
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segmentId]);
|
||||
|
||||
// get the lead list leads stored in db after the segment update
|
||||
$leadListLeads = $this->em->getRepository(ListLead::class)->findBy(['list' => $segment]);
|
||||
foreach ($leadListLeads as $listLead) {
|
||||
$leadListLeadsIds[] = (int) $listLead->getLead()->getId();
|
||||
}
|
||||
|
||||
sort($leadListLeadsIds);
|
||||
sort($contactIds);
|
||||
|
||||
// assert filter lead ids are the same as contact ids saved in db
|
||||
$this->assertSame($leadListLeadsIds, $contactIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $fieldDetails
|
||||
*/
|
||||
private function saveCustomField(array $fieldDetails = []): void
|
||||
{
|
||||
// Create a field and add it to the lead object.
|
||||
$field = new LeadField();
|
||||
$field->setLabel($fieldDetails['label']);
|
||||
$field->setType($fieldDetails['type']);
|
||||
$field->setObject('lead');
|
||||
$field->setGroup('core');
|
||||
$field->setAlias($fieldDetails['alias']);
|
||||
$field->setProperties($fieldDetails['properties']);
|
||||
|
||||
/** @var FieldModel $fieldModel */
|
||||
$fieldModel = static::getContainer()->get('mautic.lead.model.field');
|
||||
$fieldModel->saveEntity($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<object>
|
||||
*/
|
||||
private function saveContacts(): array
|
||||
{
|
||||
$numberOfContacts = 8;
|
||||
$numberOfContactsWithBlankValue = 2;
|
||||
/** @var LeadRepository $contactRepo */
|
||||
$contactRepo = $this->em->getRepository(Lead::class);
|
||||
$contacts = [];
|
||||
$cars = [
|
||||
'value1', 'value2', 'value3',
|
||||
];
|
||||
for ($i = 1; $i <= $numberOfContacts; ++$i) {
|
||||
$contact = new Lead();
|
||||
$contact->setFirstname('Contact '.$i);
|
||||
$contact->setLastname($this->testIdentifier); // Use lastname to identify test contacts
|
||||
|
||||
if ($i > $numberOfContactsWithBlankValue) {
|
||||
$contact->setFields([
|
||||
'core' => [
|
||||
self::FIELD_NAME => [
|
||||
'value' => '',
|
||||
'type' => 'multiselect',
|
||||
'alias' => self::FIELD_NAME,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
|
||||
$leadModel->setFieldValues($contact, [self::FIELD_NAME => [$cars[$i % 3]]]);
|
||||
}
|
||||
$contacts[] = $contact;
|
||||
}
|
||||
$contactRepo->saveEntities($contacts);
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $segmentData
|
||||
*/
|
||||
private function saveSegment(array $segmentData = []): LeadList
|
||||
{
|
||||
/** @var LeadListRepository $segmentRepo */
|
||||
$segmentRepo = $this->em->getRepository(LeadList::class);
|
||||
$segment = new LeadList();
|
||||
|
||||
$filterToSave = $segmentData['filterToSave'];
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => $filterToSave['field'],
|
||||
'object' => 'lead',
|
||||
'type' => 'multiselect',
|
||||
'filter' => $filterToSave['filter'],
|
||||
'display' => null,
|
||||
'operator' => $filterToSave['operator'],
|
||||
],
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'lastname',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'filter' => $this->testIdentifier,
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
];
|
||||
|
||||
$segment->setName($segmentData['name'])
|
||||
->setFilters($filters)
|
||||
->setAlias($segmentData['alias'])
|
||||
->setPublicName($segmentData['name']);
|
||||
$segmentRepo->saveEntity($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<int, mixed>
|
||||
*/
|
||||
public static function filtersSegmentsContacts(): iterable
|
||||
{
|
||||
$customField = [
|
||||
'label' => 'Cars',
|
||||
'alias' => self::FIELD_NAME,
|
||||
'type' => 'multiselect',
|
||||
'properties' => [
|
||||
'list' => [
|
||||
['label' => 'car1', 'value' => 'value1'],
|
||||
['label' => 'car2', 'value' => 'value2'],
|
||||
['label' => 'car3', 'value' => 'value3'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$segmentData = [
|
||||
'alias' => 'segment-a',
|
||||
'name' => 'Segment A',
|
||||
'filterToSave' => [
|
||||
'field' => self::FIELD_NAME,
|
||||
'filter' => [
|
||||
'value1',
|
||||
],
|
||||
'operator' => OperatorOptions::EXCLUDING_ANY,
|
||||
],
|
||||
];
|
||||
// to test excluding filter, should contain blank values as well
|
||||
yield [
|
||||
// custom field
|
||||
$customField,
|
||||
$segmentData,
|
||||
function ($contact): bool {
|
||||
return empty($contact->getFields()) || 'value1' !== $contact->getField(self::FIELD_NAME)['value'];
|
||||
},
|
||||
];
|
||||
|
||||
// to test multiple excluding values
|
||||
$segmentData['filterToSave']['filter'] = ['value1', 'value2'];
|
||||
yield [
|
||||
// custom field
|
||||
$customField,
|
||||
$segmentData,
|
||||
function ($contact): bool {
|
||||
return
|
||||
empty($contact->getFields())
|
||||
|| !in_array($contact->getField(self::FIELD_NAME)['value'], ['value1', 'value2']);
|
||||
},
|
||||
];
|
||||
|
||||
// to test including filter, should NOT contain blank values
|
||||
$segmentData['filterToSave']['operator'] = OperatorOptions::INCLUDING_ANY;
|
||||
$segmentData['filterToSave']['filter'] = ['value1'];
|
||||
yield [
|
||||
// custom field
|
||||
$customField,
|
||||
$segmentData,
|
||||
function ($contact): bool {
|
||||
return !empty($contact->getFields()) && 'value1' === $contact->getField(self::FIELD_NAME)['value'];
|
||||
},
|
||||
];
|
||||
|
||||
// to test multiple including values
|
||||
$segmentData['filterToSave']['filter'] = ['value1', 'value2'];
|
||||
yield [
|
||||
// custom field
|
||||
$customField,
|
||||
$segmentData,
|
||||
function ($contact): bool {
|
||||
return
|
||||
!empty($contact->getFields())
|
||||
&& in_array($contact->getField(self::FIELD_NAME)['value'], ['value1', 'value2']);
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
|
||||
final class SegmentNumberFilterWithOrsCommandFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testSegmentNuberFilterWithOrsCommand(): void
|
||||
{
|
||||
$contact1 = new Lead();
|
||||
$contact1->setPoints(1);
|
||||
$contact2 = new Lead();
|
||||
$contact2->setPoints(2);
|
||||
$contact3 = new Lead();
|
||||
$contact3->setPoints(3);
|
||||
$contact4 = new Lead();
|
||||
$contact4->setPoints(4);
|
||||
|
||||
$this->em->persist($contact1);
|
||||
$this->em->persist($contact2);
|
||||
$this->em->persist($contact3);
|
||||
$this->em->persist($contact4);
|
||||
|
||||
$segment = new LeadList();
|
||||
$segment->setName('Segment A');
|
||||
$segment->setPublicName('Segment A');
|
||||
$segment->setAlias('segment-a');
|
||||
$segment->setFilters([
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'points',
|
||||
'type' => 'number',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 1],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'points',
|
||||
'type' => 'number',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 2],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'points',
|
||||
'type' => 'number',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 3],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segment->getId()]);
|
||||
self::assertCount(3, $this->em->getRepository(ListLead::class)->findBy(['list' => $segment]));
|
||||
}
|
||||
|
||||
public function testSegmentNuberFilterWithRegexCommand(): void
|
||||
{
|
||||
$contact1 = new Lead();
|
||||
$contact1->setPoints(1);
|
||||
$contact2 = new Lead();
|
||||
$contact2->setPoints(2);
|
||||
$contact3 = new Lead();
|
||||
$contact3->setPoints(3);
|
||||
|
||||
$this->em->persist($contact1);
|
||||
$this->em->persist($contact2);
|
||||
$this->em->persist($contact3);
|
||||
|
||||
$segment = new LeadList();
|
||||
$segment->setName('Segment A');
|
||||
$segment->setPublicName('Segment A');
|
||||
$segment->setAlias('segment-a');
|
||||
$segment->setFilters([
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'points',
|
||||
'type' => 'number',
|
||||
'operator' => 'regexp',
|
||||
'properties' => ['filter' => '^(1|3)$'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segment->getId()]);
|
||||
self::assertCount(2, $this->em->getRepository(ListLead::class)->findBy(['list' => $segment]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
|
||||
final class SegmentStatCommandTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testSegmentStatCommandWithOutSegment(): void
|
||||
{
|
||||
$output = $this->testSymfonyCommand('mautic:segments:stat');
|
||||
|
||||
$this->assertStringContainsString('There is no segment to show!!', $output->getDisplay());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function testSegmentStatCommandWithSegment(): void
|
||||
{
|
||||
$segmentName = 'Segment For Campaign';
|
||||
$segment = new LeadList();
|
||||
$segment->setName($segmentName);
|
||||
$segment->setPublicName($segmentName);
|
||||
$segment->setAlias(mb_strtolower($segmentName));
|
||||
$segment->setIsPublished(true);
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Campaign With LeadList');
|
||||
$campaign->addList($segment);
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$output = $this->testSymfonyCommand('mautic:segments:stat');
|
||||
|
||||
// test table header
|
||||
$this->assertMatchesRegularExpression('/Title\s+Id\s+IsPublished\s+IsUsed/i', $output->getDisplay());
|
||||
// test table content
|
||||
$this->assertMatchesRegularExpression("/Segment For Campaign\s+{$segment->getId()}\s+{$segment->getIsPublished()}\s+1/i", $output->getDisplay());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\LeadBundle\Command\UpdateLeadListsCommand;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadListRepository;
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Segment\OperatorOptions;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
|
||||
final class UpdateLeadListCommandFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
|
||||
protected $useCleanupRollback = false; // This should be here, because test is changing DDL of the leads table.
|
||||
|
||||
public function testFailWhenSegmentDoesNotExist(): void
|
||||
{
|
||||
$output = $this->testSymfonyCommand(UpdateLeadListsCommand::NAME, ['--list-id' => 999999]);
|
||||
|
||||
Assert::assertSame(1, $output->getStatusCode());
|
||||
Assert::assertStringContainsString('Segment #999999 does not exist', $output->getDisplay());
|
||||
}
|
||||
|
||||
#[DataProvider('provider')]
|
||||
public function testCommandRebuildingAllSegments(callable $getCommandParams, callable $assert): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('halusky@bramborak.makovec');
|
||||
|
||||
$segment = new LeadList();
|
||||
$segment->setName('Test segment');
|
||||
$segment->setPublicName('Test segment');
|
||||
$segment->setAlias('test-segment');
|
||||
$segment->setFilters([
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'filter' => 'halusky@bramborak.makovec',
|
||||
'display' => null,
|
||||
'operator' => 'eq',
|
||||
],
|
||||
]);
|
||||
|
||||
$longTimeAgo = new \DateTime('2000-01-01 00:00:00');
|
||||
|
||||
// The last built date is set on pre persist to 2000-01-01 00:00:00.
|
||||
// Setting it 1 year ago so we could assert that it is updated after the command runs.
|
||||
$segment->setLastBuiltDate($longTimeAgo);
|
||||
|
||||
$this->em->persist($contact);
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
Assert::assertEquals($longTimeAgo, $segment->getLastBuiltDate());
|
||||
|
||||
$output = $this->testSymfonyCommand(UpdateLeadListsCommand::NAME, $getCommandParams($segment));
|
||||
|
||||
/** @var LeadList $segment */
|
||||
$segment = $this->em->find(LeadList::class, $segment->getId());
|
||||
$assert($segment, $output->getDisplay());
|
||||
|
||||
/** @var LeadListRepository $leadListRepository */
|
||||
$leadListRepository = $this->em->getRepository(LeadList::class);
|
||||
|
||||
Assert::assertSame(
|
||||
1,
|
||||
$leadListRepository->getLeadCount([$segment->getId()])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array<callable>>
|
||||
*/
|
||||
public static function Provider(): iterable
|
||||
{
|
||||
// Test that all segments will be rebuilt with no params set.
|
||||
yield [
|
||||
fn (): array => [],
|
||||
function (LeadList $segment): void {
|
||||
Assert::assertGreaterThan(
|
||||
new \DateTime('2000-01-01 00:00:00'),
|
||||
$segment->getLastBuiltDate()
|
||||
);
|
||||
Assert::assertNotNull($segment->getLastBuiltTime());
|
||||
},
|
||||
];
|
||||
|
||||
// Test that it will work when we select a specific segment too.
|
||||
// Also testing the timing option = 0.
|
||||
yield [
|
||||
fn (LeadList $segment): array => ['--list-id' => $segment->getId()],
|
||||
function (LeadList $segment, string $output): void {
|
||||
Assert::assertGreaterThan(
|
||||
new \DateTime('2000-01-01 00:00:00'),
|
||||
$segment->getLastBuiltDate()
|
||||
);
|
||||
Assert::assertNotNull($segment->getLastBuiltTime());
|
||||
Assert::assertStringNotContainsString('Total time:', $output);
|
||||
},
|
||||
];
|
||||
|
||||
// But the last built date will not update if we limit how many contacts to process.
|
||||
// Also testing the timing option = 1.
|
||||
yield [
|
||||
fn (): array => ['--max-contacts' => 1, '--timing' => 1],
|
||||
function (LeadList $segment, string $output): void {
|
||||
Assert::assertEquals(
|
||||
new \DateTime('2000-01-01 00:00:00'),
|
||||
$segment->getLastBuiltDate()
|
||||
);
|
||||
Assert::assertNull($segment->getLastBuiltTime());
|
||||
Assert::assertStringContainsString('Total time:', $output);
|
||||
Assert::assertStringContainsString('seconds', $output);
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $expected
|
||||
* @param array<int> $addTagsToContact
|
||||
* @param array<int> $addTagsToSegment
|
||||
*/
|
||||
#[DataProvider('provideIncludeExclude')]
|
||||
public function testTagIncludeExclude(string $filter, $expected, array $addTagsToContact, array $addTagsToSegment): void
|
||||
{
|
||||
$tag1 = new Tag('tag1');
|
||||
$tag2 = new Tag('tag2');
|
||||
$tag3 = new Tag('tag3');
|
||||
|
||||
$this->em->persist($tag1);
|
||||
$this->em->persist($tag2);
|
||||
$this->em->persist($tag3);
|
||||
$this->em->flush();
|
||||
|
||||
$contact = $this->createLead('First name', emailId: 'halusky@bramborak.makovec');
|
||||
|
||||
if (in_array(1, $addTagsToContact, true)) {
|
||||
$contact->addTag($tag1);
|
||||
}
|
||||
|
||||
if (in_array(2, $addTagsToContact, true)) {
|
||||
$contact->addTag($tag2);
|
||||
}
|
||||
|
||||
if (in_array(3, $addTagsToContact, true)) {
|
||||
$contact->addTag($tag3);
|
||||
}
|
||||
|
||||
$tagSegment = [];
|
||||
|
||||
if (in_array(1, $addTagsToSegment, true)) {
|
||||
$tagSegment[] = $tag1->getId();
|
||||
}
|
||||
|
||||
if (in_array(2, $addTagsToSegment, true)) {
|
||||
$tagSegment[] = $tag2->getId();
|
||||
}
|
||||
|
||||
if (in_array(3, $addTagsToSegment, true)) {
|
||||
$tagSegment[] = $tag3->getId();
|
||||
}
|
||||
|
||||
$segment = $this->createSegment(
|
||||
'test-segment',
|
||||
[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'tags',
|
||||
'object' => 'lead',
|
||||
'type' => 'tags',
|
||||
'filter' => $tagSegment,
|
||||
'display' => null,
|
||||
'operator' => $filter,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$longTimeAgo = new \DateTime('2000-01-01 00:00:00');
|
||||
|
||||
$segment->setLastBuiltDate($longTimeAgo);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
Assert::assertEquals($longTimeAgo, $segment->getLastBuiltDate());
|
||||
|
||||
$output = $this->testSymfonyCommand(UpdateLeadListsCommand::NAME);
|
||||
|
||||
Assert::assertSame(Command::SUCCESS, $output->getStatusCode());
|
||||
|
||||
/** @var LeadListRepository $leadListRepository */
|
||||
$leadListRepository = $this->em->getRepository(LeadList::class);
|
||||
|
||||
Assert::assertSame(
|
||||
$expected,
|
||||
$leadListRepository->getLeadCount([$segment->getId()])
|
||||
);
|
||||
}
|
||||
|
||||
public static function provideIncludeExclude(): \Generator
|
||||
{
|
||||
yield 'include any with match' => [OperatorOptions::INCLUDING_ANY, 1, [1, 2], [1, 2, 3]];
|
||||
yield 'include any no match' => [OperatorOptions::INCLUDING_ANY, 0, [1, 2], [3]];
|
||||
yield 'exclude any with match' => [OperatorOptions::EXCLUDING_ANY, 0, [1, 2], [1, 2, 3]];
|
||||
yield 'exclude any no match' => [OperatorOptions::EXCLUDING_ANY, 1, [2], [1, 3]];
|
||||
yield 'include all no match' => [OperatorOptions::INCLUDING_ALL, 0, [1, 2], [1, 2, 3]];
|
||||
yield 'include all with match' => [OperatorOptions::INCLUDING_ALL, 1, [1, 3], [1, 3]];
|
||||
yield 'exclude all no match' => [OperatorOptions::EXCLUDING_ALL, 1, [1, 2], [1, 2, 3]];
|
||||
yield 'exclude all with match' => [OperatorOptions::EXCLUDING_ALL, 0, [1, 3], [1, 3]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $expected
|
||||
* @param array<int> $addFieldsToContact
|
||||
* @param array<int> $addFieldsToSegment
|
||||
*/
|
||||
#[DataProvider('provideIncludeExclude')]
|
||||
public function testCustomFieldIncludeExclude(string $filter, $expected, array $addFieldsToContact, array $addFieldsToSegment): void
|
||||
{
|
||||
$fieldAlias = 'test_inc_ex_field';
|
||||
|
||||
/** @var FieldModel $fieldModel */
|
||||
$fieldModel = $this->getContainer()->get(FieldModel::class);
|
||||
|
||||
$fields = $fieldModel->getLeadFieldCustomFields();
|
||||
Assert::assertEmpty($fields, 'There are no Custom Fields.');
|
||||
|
||||
// Add field.
|
||||
$leadField = new LeadField();
|
||||
$leadField->setName('Test Field')
|
||||
->setAlias($fieldAlias)
|
||||
->setType('multiselect')
|
||||
->setObject('lead')
|
||||
->setProperties([
|
||||
'list' => [
|
||||
[
|
||||
'label' => 'Halusky',
|
||||
'value' => 'halusky',
|
||||
],
|
||||
[
|
||||
'label' => 'Bramborak',
|
||||
'value' => 'bramborak',
|
||||
],
|
||||
[
|
||||
'label' => 'Makovec',
|
||||
'value' => 'makovec',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$fieldModel->saveEntity($leadField);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$contact = $this->createLead('First name', emailId: 'halusky@bramborak.makovec');
|
||||
|
||||
$contactValue = [];
|
||||
if (in_array(1, $addFieldsToContact, true)) {
|
||||
$contactValue[] = 'halusky';
|
||||
}
|
||||
|
||||
if (in_array(2, $addFieldsToContact, true)) {
|
||||
$contactValue[] = 'bramborak';
|
||||
}
|
||||
|
||||
if (in_array(3, $addFieldsToContact, true)) {
|
||||
$contactValue[] = 'makovec';
|
||||
}
|
||||
|
||||
$contact->addUpdatedField($fieldAlias, $contactValue);
|
||||
$contactModel = self::getContainer()->get(LeadModel::class);
|
||||
\assert($contactModel instanceof LeadModel);
|
||||
$contactModel->saveEntity($contact);
|
||||
|
||||
$segmentValue = [];
|
||||
|
||||
if (in_array(1, $addFieldsToSegment, true)) {
|
||||
$segmentValue[] = 'halusky';
|
||||
}
|
||||
|
||||
if (in_array(2, $addFieldsToSegment, true)) {
|
||||
$segmentValue[] = 'bramborak';
|
||||
}
|
||||
|
||||
if (in_array(3, $addFieldsToSegment, true)) {
|
||||
$segmentValue[] = 'makovec';
|
||||
}
|
||||
|
||||
$segment = $this->createSegment(
|
||||
'test-segment',
|
||||
[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => $fieldAlias,
|
||||
'object' => 'lead',
|
||||
'type' => 'multiselect',
|
||||
'filter' => $segmentValue,
|
||||
'display' => null,
|
||||
'operator' => $filter,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$longTimeAgo = new \DateTime('2000-01-01 00:00:00');
|
||||
|
||||
$segment->setLastBuiltDate($longTimeAgo);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
Assert::assertEquals($longTimeAgo, $segment->getLastBuiltDate());
|
||||
|
||||
$output = $this->testSymfonyCommand(UpdateLeadListsCommand::NAME);
|
||||
|
||||
Assert::assertSame(Command::SUCCESS, $output->getStatusCode());
|
||||
|
||||
/** @var LeadListRepository $leadListRepository */
|
||||
$leadListRepository = $this->em->getRepository(LeadList::class);
|
||||
|
||||
Assert::assertSame(
|
||||
$expected,
|
||||
$leadListRepository->getLeadCount([$segment->getId()])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $expected
|
||||
* @param array<int> $addSegmentsToContact
|
||||
* @param array<int> $addSegmentsToSegment
|
||||
*/
|
||||
#[DataProvider('provideIncludeExclude')]
|
||||
public function testSegmentIncludeExclude(string $filter, $expected, array $addSegmentsToContact, array $addSegmentsToSegment): void
|
||||
{
|
||||
$contact = $this->createLead('First name', emailId: 'halusky@bramborak.makovec');
|
||||
|
||||
$segmentA = $this->createSegment('A', []);
|
||||
$segmentB = $this->createSegment('B', []);
|
||||
$segmentC = $this->createSegment('C', []);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
if (in_array(1, $addSegmentsToContact, true)) {
|
||||
$this->createListLead($segmentA, $contact);
|
||||
}
|
||||
|
||||
if (in_array(2, $addSegmentsToContact, true)) {
|
||||
$this->createListLead($segmentB, $contact);
|
||||
}
|
||||
|
||||
if (in_array(3, $addSegmentsToContact, true)) {
|
||||
$this->createListLead($segmentC, $contact);
|
||||
}
|
||||
|
||||
$filteredSegments = [];
|
||||
|
||||
if (in_array(1, $addSegmentsToSegment, true)) {
|
||||
$filteredSegments[] = $segmentA->getId();
|
||||
}
|
||||
|
||||
if (in_array(2, $addSegmentsToSegment, true)) {
|
||||
$filteredSegments[] = $segmentB->getId();
|
||||
}
|
||||
|
||||
if (in_array(3, $addSegmentsToSegment, true)) {
|
||||
$filteredSegments[] = $segmentC->getId();
|
||||
}
|
||||
|
||||
$segmentD = $this->createSegment(
|
||||
'D',
|
||||
[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'object' => 'lead',
|
||||
'type' => 'leadlist',
|
||||
'filter' => $filteredSegments,
|
||||
'display' => null,
|
||||
'operator' => $filter,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$longTimeAgo = new \DateTime('2000-01-01 00:00:00');
|
||||
|
||||
$segmentD->setLastBuiltDate($longTimeAgo);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
Assert::assertEquals($longTimeAgo, $segmentD->getLastBuiltDate());
|
||||
|
||||
$output = $this->testSymfonyCommand(UpdateLeadListsCommand::NAME);
|
||||
|
||||
Assert::assertSame(Command::SUCCESS, $output->getStatusCode());
|
||||
|
||||
/** @var LeadListRepository $leadListRepository */
|
||||
$leadListRepository = $this->em->getRepository(LeadList::class);
|
||||
|
||||
Assert::assertSame(
|
||||
$expected,
|
||||
$leadListRepository->getLeadCount([$segmentD->getId()])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Command\UpdateLeadListsCommand;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
|
||||
class UpdateLeadListsCommandCircularDependencyTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var LeadList[]
|
||||
*/
|
||||
private array $segments = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->createCircularDependencySegments();
|
||||
}
|
||||
|
||||
public function testCircularDependencyDetection(): void
|
||||
{
|
||||
$segmentA = $this->segments['Segment A'];
|
||||
|
||||
$this->expectException(\Mautic\LeadBundle\Segment\Exception\SegmentQueryException::class);
|
||||
$this->expectExceptionMessage('Circular reference detected.');
|
||||
|
||||
$this->testSymfonyCommand(
|
||||
UpdateLeadListsCommand::NAME,
|
||||
[
|
||||
'-i' => $segmentA->getId(),
|
||||
'--env' => 'test',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates segments with circular dependencies:
|
||||
* Segment A includes Segment B
|
||||
* Segment B includes Segment C
|
||||
* Segment C includes Segment A
|
||||
*/
|
||||
private function createCircularDependencySegments(): void
|
||||
{
|
||||
// Create three test segments
|
||||
$segmentA = $this->createSegment('Segment A');
|
||||
$segmentB = $this->createSegment('Segment B');
|
||||
$segmentC = $this->createSegment('Segment C');
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
// Add filters to create circular dependencies
|
||||
// Segment A includes Segment B
|
||||
$this->addSegmentDependency($segmentA, $segmentB);
|
||||
|
||||
// Segment B includes Segment C
|
||||
$this->addSegmentDependency($segmentB, $segmentC);
|
||||
|
||||
// Segment C includes Segment A (creating the circular dependency)
|
||||
$this->addSegmentDependency($segmentC, $segmentA);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
// Store segments for later use
|
||||
$this->segments = [
|
||||
'Segment A' => $segmentA,
|
||||
'Segment B' => $segmentB,
|
||||
'Segment C' => $segmentC,
|
||||
];
|
||||
}
|
||||
|
||||
private function createSegment(string $name): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName($name);
|
||||
$segment->setPublicName($name);
|
||||
$segment->setAlias(strtolower(str_replace(' ', '-', $name)));
|
||||
$segment->setIsGlobal(true);
|
||||
$segment->setIsPublished(true);
|
||||
|
||||
$this->em->persist($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
private function addSegmentDependency(LeadList $segment, LeadList $includeSegment): void
|
||||
{
|
||||
$filters = $segment->getFilters();
|
||||
$filters[] = [
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'object' => 'lead',
|
||||
'type' => 'leadlist',
|
||||
'filter' => [$includeSegment->getId()],
|
||||
'display' => null,
|
||||
'operator' => 'in',
|
||||
];
|
||||
|
||||
$segment->setFilters($filters);
|
||||
$this->em->persist($segment);
|
||||
}
|
||||
|
||||
public function testSkippingNonExistentDependentSegment(): void
|
||||
{
|
||||
// Create two segments (A and B)
|
||||
$segmentA = $this->createSegment('Segment A');
|
||||
$segmentB = $this->createSegment('Segment B');
|
||||
$this->em->flush();
|
||||
|
||||
// Add a filter to segment A that includes segment B
|
||||
$this->addSegmentDependency($segmentA, $segmentB);
|
||||
|
||||
// Add a non-existent segment ID as a dependency for segment A
|
||||
$nonExistentSegmentId = 9999; // An ID that doesn't exist
|
||||
$filters = $segmentA->getFilters();
|
||||
$filters[] = [
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'object' => 'lead',
|
||||
'type' => 'leadlist',
|
||||
'filter' => [$nonExistentSegmentId],
|
||||
'display' => null,
|
||||
'operator' => 'in',
|
||||
];
|
||||
$segmentA->setFilters($filters);
|
||||
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->flush();
|
||||
|
||||
$this->segments = [
|
||||
'Segment A' => $segmentA,
|
||||
'Segment B' => $segmentB,
|
||||
];
|
||||
|
||||
// The command should complete without errors despite the non-existent segment dependency
|
||||
$output = $this->testSymfonyCommand(
|
||||
UpdateLeadListsCommand::NAME,
|
||||
[
|
||||
'-i' => $segmentA->getId(),
|
||||
'--env' => 'test',
|
||||
]
|
||||
);
|
||||
|
||||
// Verify that segment B was processed
|
||||
$this->assertStringContainsString(
|
||||
sprintf('Rebuilding contacts for segment %d', $segmentB->getId()),
|
||||
$output->getDisplay()
|
||||
);
|
||||
|
||||
// Verify that segment A was processed after its dependencies
|
||||
$this->assertStringContainsString(
|
||||
sprintf('Rebuilding contacts for segment %d', $segmentA->getId()),
|
||||
$output->getDisplay()
|
||||
);
|
||||
|
||||
// Verify that the command completed successfully
|
||||
$this->assertStringNotContainsString('error', strtolower($output->getDisplay()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user