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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\RoleRepository;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Entity\UserRepository;
|
||||
use MauticPlugin\MauticTagManagerBundle\Entity\Tag;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
class AjaxControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function beforeBeginTransaction(): void
|
||||
{
|
||||
$this->resetAutoincrement([
|
||||
'leads',
|
||||
'campaigns',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testToggleLeadCampaignAction(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$contact = $this->createContact('blabla@contact.email');
|
||||
|
||||
// Ensure there is no member for campaign 1 yet.
|
||||
$this->assertSame([], $this->getMembersForCampaign($campaign->getId()));
|
||||
|
||||
// Create the member now.
|
||||
$payload = [
|
||||
'action' => 'lead:toggleLeadCampaign',
|
||||
'leadId' => $contact->getId(),
|
||||
'campaignId' => $campaign->getId(),
|
||||
'campaignAction' => 'add',
|
||||
];
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
$this->assertResponseIsSuccessful();
|
||||
$response = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
// Ensure the contact 1 is a campaign 1 member now.
|
||||
$this->assertSame(
|
||||
[['lead_id' => (string) $contact->getId(), 'manually_added' => '1', 'manually_removed' => '0']],
|
||||
$this->getMembersForCampaign($campaign->getId()),
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
|
||||
$this->assertTrue(isset($response['success']), 'The response does not contain the `success` param.');
|
||||
$this->assertSame(1, $response['success']);
|
||||
|
||||
// Let's remove the member now.
|
||||
$payload = [
|
||||
'action' => 'lead:toggleLeadCampaign',
|
||||
'leadId' => $contact->getId(),
|
||||
'campaignId' => $campaign->getId(),
|
||||
'campaignAction' => 'remove',
|
||||
];
|
||||
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Ensure the contact 1 was removed as a member of campaign 1 member now.
|
||||
$this->assertSame([['lead_id' => (string) $contact->getId(), 'manually_added' => '0', 'manually_removed' => '1']], $this->getMembersForCampaign($campaign->getId()));
|
||||
|
||||
$this->assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
$this->assertTrue(isset($response['success']), 'The response does not contain the `success` param.');
|
||||
$this->assertSame(1, $response['success']);
|
||||
}
|
||||
|
||||
public function testSegmentDependencyTreeWithNotExistingSegment(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:getSegmentDependencyTree&id=9999');
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(404, $response->getStatusCode());
|
||||
Assert::assertSame('{"message":"Segment 9999 could not be found."}', $response->getContent());
|
||||
}
|
||||
|
||||
public function testCompanyLookupWithNoCompanySelected(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:getLookupChoiceList&searchKey=lead.company&lead.company=unicorn');
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
Assert::assertSame('[]', $response->getContent());
|
||||
}
|
||||
|
||||
public function testCompanyLookupWithCompanySelected(): void
|
||||
{
|
||||
$company = new Company();
|
||||
$company->setName('SaaS Company');
|
||||
$this->em->persist($company);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:getLookupChoiceList&searchKey=lead.company&lead.company=sa');
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
Assert::assertSame('[{"text":"SaaS Company","value":"'.$company->getId().'"}]', $response->getContent());
|
||||
}
|
||||
|
||||
public function testCompanyLookupWithNoModelSet(): void
|
||||
{
|
||||
$this->client->xmlHttpRequest(Request::METHOD_GET, '/s/ajax?action=lead:getLookupChoiceList&lead.company=unicorn');
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(400, $response->getStatusCode());
|
||||
Assert::assertStringContainsString('Bad Request - The searchKey parameter is required', $response->getContent());
|
||||
}
|
||||
|
||||
public function testCompanyLookupWithLimit(): void
|
||||
{
|
||||
$company1 = new Company();
|
||||
$company1->setName('Company 1');
|
||||
$this->em->persist($company1);
|
||||
|
||||
$company2 = new Company();
|
||||
$company2->setName('Company 2');
|
||||
$this->em->persist($company2);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:getLookupChoiceList&searchKey=lead.company&lead.company=Company&limit=1');
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$content = json_decode($response->getContent(), true);
|
||||
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
Assert::assertIsArray($content);
|
||||
Assert::assertCount(1, $content, 'The result should contain only one element');
|
||||
Assert::assertSame('Company 1', $content[0]['text']);
|
||||
Assert::assertSame($company1->getId(), (int) $content[0]['value']);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:getLookupChoiceList&searchKey=lead.company&lead.company=Company&limit=1&start=1');
|
||||
$response = $this->client->getResponse();
|
||||
$content = json_decode($response->getContent(), true);
|
||||
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
Assert::assertIsArray($content);
|
||||
Assert::assertCount(1, $content, 'The result should contain only one element');
|
||||
Assert::assertSame('Company 2', $content[0]['text']);
|
||||
Assert::assertSame($company2->getId(), (int) $content[0]['value']);
|
||||
}
|
||||
|
||||
public function testSegmentDependencyTree(): void
|
||||
{
|
||||
$segmentA = new LeadList();
|
||||
$segmentA->setName('Segment A');
|
||||
$segmentA->setPublicName('Segment A');
|
||||
$segmentA->setAlias('segment-a');
|
||||
|
||||
$segmentB = new LeadList();
|
||||
$segmentB->setName('Segment B');
|
||||
$segmentB->setPublicName('Segment B');
|
||||
$segmentB->setAlias('segment-b');
|
||||
|
||||
$segmentC = new LeadList();
|
||||
$segmentC->setName('Segment C');
|
||||
$segmentC->setPublicName('Segment C');
|
||||
$segmentC->setAlias('segment-c');
|
||||
|
||||
$segmentD = new LeadList();
|
||||
$segmentD->setName('Segment D');
|
||||
$segmentD->setPublicName('Segment D');
|
||||
$segmentD->setAlias('segment-d');
|
||||
|
||||
$segmentE = new LeadList();
|
||||
$segmentE->setName('Segment E');
|
||||
$segmentE->setPublicName('Segment E');
|
||||
$segmentE->setAlias('segment-e');
|
||||
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->persist($segmentB);
|
||||
$this->em->persist($segmentC);
|
||||
$this->em->persist($segmentD);
|
||||
$this->em->persist($segmentE);
|
||||
$this->em->flush();
|
||||
|
||||
$segmentA->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentB->getId()]],
|
||||
], [
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => '!in',
|
||||
'properties' => ['filter' => [$segmentC->getId(), $segmentD->getId()]],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$segmentC->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentE->getId()]],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->persist($segmentC);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/ajax?action=lead:getSegmentDependencyTree&id={$segmentA->getId()}");
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk(), $response->getContent());
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'levels' => [
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "0-{$segmentA->getId()}", 'name' => $segmentA->getName(), 'link' => "/s/segments/view/{$segmentA->getId()}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "{$segmentA->getId()}-{$segmentB->getId()}", 'name' => $segmentB->getName(), 'link' => "/s/segments/view/{$segmentB->getId()}"],
|
||||
['id' => "{$segmentA->getId()}-{$segmentC->getId()}", 'name' => $segmentC->getName(), 'link' => "/s/segments/view/{$segmentC->getId()}"],
|
||||
['id' => "{$segmentA->getId()}-{$segmentD->getId()}", 'name' => $segmentD->getName(), 'link' => "/s/segments/view/{$segmentD->getId()}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "{$segmentC->getId()}-{$segmentE->getId()}", 'name' => $segmentE->getName(), 'link' => "/s/segments/view/{$segmentE->getId()}"],
|
||||
],
|
||||
],
|
||||
],
|
||||
'edges' => [
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentB->getId()}"],
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentC->getId()}"],
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentD->getId()}"],
|
||||
['source' => "{$segmentA->getId()}-{$segmentC->getId()}", 'target' => "{$segmentC->getId()}-{$segmentE->getId()}"],
|
||||
],
|
||||
],
|
||||
json_decode($response->getContent(), true)
|
||||
);
|
||||
}
|
||||
|
||||
public function testSegmentDependencyTreeWithLoop(): void
|
||||
{
|
||||
$segmentA = new LeadList();
|
||||
$segmentA->setName('Segment A');
|
||||
$segmentA->setPublicName('Segment A');
|
||||
$segmentA->setAlias('segment-a');
|
||||
|
||||
$segmentB = new LeadList();
|
||||
$segmentB->setName('Segment B');
|
||||
$segmentB->setPublicName('Segment B');
|
||||
$segmentB->setAlias('segment-b');
|
||||
|
||||
$segmentC = new LeadList();
|
||||
$segmentC->setName('Segment C');
|
||||
$segmentC->setPublicName('Segment C');
|
||||
$segmentC->setAlias('segment-c');
|
||||
|
||||
$segmentD = new LeadList();
|
||||
$segmentD->setName('Segment D');
|
||||
$segmentD->setPublicName('Segment D');
|
||||
$segmentD->setAlias('segment-d');
|
||||
|
||||
$segmentE = new LeadList();
|
||||
$segmentE->setName('Segment E');
|
||||
$segmentE->setPublicName('Segment E');
|
||||
$segmentE->setAlias('segment-e');
|
||||
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->persist($segmentB);
|
||||
$this->em->persist($segmentC);
|
||||
$this->em->persist($segmentD);
|
||||
$this->em->persist($segmentE);
|
||||
$this->em->flush();
|
||||
|
||||
$segmentA->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentB->getId()]],
|
||||
], [
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => '!in',
|
||||
'properties' => ['filter' => [$segmentC->getId(), $segmentD->getId()]],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$segmentC->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentE->getId()]],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$segmentE->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentA->getId()]],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->persist($segmentC);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/ajax?action=lead:getSegmentDependencyTree&id={$segmentA->getId()}");
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk(), $response->getContent());
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'levels' => [
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "0-{$segmentA->getId()}", 'name' => $segmentA->getName(), 'link' => "/s/segments/view/{$segmentA->getId()}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "{$segmentA->getId()}-{$segmentB->getId()}", 'name' => $segmentB->getName(), 'link' => "/s/segments/view/{$segmentB->getId()}"],
|
||||
['id' => "{$segmentA->getId()}-{$segmentC->getId()}", 'name' => $segmentC->getName(), 'link' => "/s/segments/view/{$segmentC->getId()}"],
|
||||
['id' => "{$segmentA->getId()}-{$segmentD->getId()}", 'name' => $segmentD->getName(), 'link' => "/s/segments/view/{$segmentD->getId()}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "{$segmentC->getId()}-{$segmentE->getId()}", 'name' => $segmentE->getName(), 'link' => "/s/segments/view/{$segmentE->getId()}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => "{$segmentE->getId()}-{$segmentA->getId()}",
|
||||
'name' => $segmentA->getName(),
|
||||
'link' => "/s/segments/view/{$segmentA->getId()}",
|
||||
'message' => 'This segment already exists in the segment dependency tree',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'levels' => $responseData['levels'],
|
||||
]
|
||||
);
|
||||
|
||||
$expectedEdges = [
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentB->getId()}"],
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentC->getId()}"],
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentD->getId()}"],
|
||||
['source' => "{$segmentA->getId()}-{$segmentC->getId()}", 'target' => "{$segmentC->getId()}-{$segmentE->getId()}"],
|
||||
['source' => "{$segmentC->getId()}-{$segmentE->getId()}", 'target' => "{$segmentE->getId()}-{$segmentA->getId()}"],
|
||||
];
|
||||
|
||||
$actualEdges = $responseData['edges'];
|
||||
Assert::assertCount(count($expectedEdges), $actualEdges, 'Should have the correct number of edges');
|
||||
|
||||
foreach ($expectedEdges as $expectedEdge) {
|
||||
$edgeFound = false;
|
||||
foreach ($actualEdges as $actualEdge) {
|
||||
if ($actualEdge['source'] === $expectedEdge['source'] && $actualEdge['target'] === $expectedEdge['target']) {
|
||||
$edgeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert::assertTrue($edgeFound, "Expected edge {$expectedEdge['source']} -> {$expectedEdge['target']} not found");
|
||||
}
|
||||
}
|
||||
|
||||
public function testRemoveTagFromLeadAction(): void
|
||||
{
|
||||
// Create a lead
|
||||
$lead = $this->createContact('test@email.com');
|
||||
// ... set other properties as needed
|
||||
|
||||
// Create a tag
|
||||
$tag = new Tag();
|
||||
$tag->setTag('Test Tag');
|
||||
// ... set other properties as needed
|
||||
|
||||
// Link the lead and tag
|
||||
$lead->addTag($tag);
|
||||
|
||||
// Persist the lead and tag
|
||||
$this->em->persist($lead);
|
||||
$this->em->persist($tag);
|
||||
$this->em->flush();
|
||||
|
||||
// Call the removeTagFromLeadAction
|
||||
$this->client->request(Request::METHOD_POST, '/s/ajax?action=lead:removeTagFromLead', [
|
||||
'leadId' => $lead->getId(),
|
||||
'tagId' => $tag->getId(),
|
||||
]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
|
||||
// Assert the tag is removed from the lead
|
||||
$updatedLead = $this->em->getRepository(Lead::class)->find($lead->getId());
|
||||
$this->assertFalse(in_array($tag, $updatedLead->getTags()->toArray()));
|
||||
}
|
||||
|
||||
public function testContactListActionSuggestionsByAdminUser(): void
|
||||
{
|
||||
/** @var UserRepository $userRepository */
|
||||
$userRepository = $this->em->getRepository(User::class);
|
||||
|
||||
/** @var User $adminUser */
|
||||
$adminUser = $userRepository->findOneBy(['username' => 'admin']);
|
||||
self::assertInstanceOf(User::class, $adminUser);
|
||||
|
||||
$salesUser = $userRepository->findOneBy(['username' => 'sales']);
|
||||
self::assertInstanceOf(User::class, $salesUser);
|
||||
|
||||
$leads = [];
|
||||
|
||||
// Create 4 leads with two owned by admin and sales users respectively.
|
||||
for ($i = 1; $i <= 4; ++$i) {
|
||||
$owner = $adminUser;
|
||||
|
||||
if ($i > 2) {
|
||||
$owner = $salesUser;
|
||||
}
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname("User $i");
|
||||
$lead->setOwner($owner);
|
||||
$leads[] = $lead;
|
||||
}
|
||||
|
||||
/** @var LeadRepository $leadRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
$leadRepository->saveEntities($leads);
|
||||
$this->em->clear();
|
||||
|
||||
// Check suggestions for admin user.
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:contactList&field=undefined&filter=user');
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk());
|
||||
|
||||
$data = json_decode($response->getContent(), true);
|
||||
$foundNames = array_column($data, 'value');
|
||||
|
||||
self::assertCount(4, $foundNames);
|
||||
|
||||
foreach ($foundNames as $key => $name) {
|
||||
self::assertSame('User '.($key + 1), $name);
|
||||
}
|
||||
}
|
||||
|
||||
public function testContactListActionSuggestionsByNonAdminUser(): void
|
||||
{
|
||||
/** @var UserRepository $userRepository */
|
||||
$userRepository = $this->em->getRepository(User::class);
|
||||
|
||||
$adminUser = $userRepository->findOneBy(['username' => 'admin']);
|
||||
self::assertInstanceOf(User::class, $adminUser);
|
||||
|
||||
$leads = [];
|
||||
|
||||
// Create 2 leads with owned by admin user.
|
||||
for ($i = 1; $i <= 2; ++$i) {
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname("User $i");
|
||||
$lead->setOwner($adminUser);
|
||||
$leads[] = $lead;
|
||||
}
|
||||
|
||||
/** @var LeadRepository $leadRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
$leadRepository->saveEntities($leads);
|
||||
$this->em->clear();
|
||||
|
||||
$role = new Role();
|
||||
$role->setName('Role');
|
||||
$role->setIsAdmin(false);
|
||||
$role->setRawPermissions(['lead:leads' => ['viewown']]);
|
||||
|
||||
/** @var RoleRepository $roleRepository */
|
||||
$roleRepository = $this->em->getRepository(Role::class);
|
||||
$roleRepository->saveEntity($role);
|
||||
|
||||
// Create a non admin user with view own contacts permission.
|
||||
$user = new User();
|
||||
$user->setFirstName('Non');
|
||||
$user->setLastName('Admin');
|
||||
$user->setEmail('non-admin-user@test.com');
|
||||
$user->setUsername('non-admin-user');
|
||||
$user->setRole($role);
|
||||
|
||||
/** @var PasswordHasherInterface $hasher */
|
||||
$hasher = static::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
|
||||
|
||||
$passwordNonAdmin = 'Maut1cR0cks!';
|
||||
$user->setPassword($hasher->hash($passwordNonAdmin));
|
||||
$userRepository->saveEntity($user);
|
||||
|
||||
/** @var User $nonAdminUser */
|
||||
$nonAdminUser = $userRepository->findOneBy(['email' => 'non-admin-user@test.com']);
|
||||
|
||||
$nonAdminLeads = [];
|
||||
|
||||
// Create 2 leads with owned by non-admin user.
|
||||
for ($i = 3; $i <= 4; ++$i) {
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname("User $i");
|
||||
$lead->setOwner($nonAdminUser);
|
||||
$nonAdminLeads[] = $lead;
|
||||
}
|
||||
|
||||
$leadRepository->saveEntities($nonAdminLeads);
|
||||
$this->em->clear();
|
||||
|
||||
$this->logoutUser();
|
||||
|
||||
// Check suggestions for a non admin user.
|
||||
$this->client->loginUser($nonAdminUser, 'mautic');
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', 'non-admin-user');
|
||||
// Set the new password, because new authenticator system checks for it.
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', $passwordNonAdmin);
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:contactList&field=undefined&filter=user');
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk());
|
||||
|
||||
$data = json_decode($response->getContent(), true);
|
||||
$foundNames = array_column($data, 'value');
|
||||
|
||||
self::assertCount(2, $foundNames);
|
||||
self::assertSame('User 3', $foundNames[0]);
|
||||
self::assertSame('User 4', $foundNames[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $expectedOptions
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('leadFieldOrderChoiceListProvider')]
|
||||
public function testUpdateLeadFieldOrderChoiceListAction(string $object, string $group, array $expectedOptions): void
|
||||
{
|
||||
$payload = [
|
||||
'action' => 'lead:updateLeadFieldOrderChoiceList',
|
||||
'object' => $object,
|
||||
'group' => $group,
|
||||
];
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
|
||||
// Get the response HTML
|
||||
$response = $this->client->getResponse();
|
||||
$htmlContent = $response->getContent();
|
||||
|
||||
// Assert the response is successful
|
||||
$this->assertTrue($response->isOk(), "Response was not OK for object: $object, group: $group");
|
||||
$this->assertStringNotContainsString('<form', $htmlContent, 'Response contains a form instead of just field order.');
|
||||
$this->assertStringContainsString('<select', $htmlContent, 'Response contains select tag.');
|
||||
|
||||
// Parse the HTML content using DOMDocument
|
||||
$dom = new \DOMDocument();
|
||||
@$dom->loadHTML($htmlContent);
|
||||
$select = $dom->getElementsByTagName('select')->item(0);
|
||||
$options = $select->getElementsByTagName('option');
|
||||
|
||||
$actualOptions = [];
|
||||
foreach ($options as $option) {
|
||||
if ($option->textContent) {
|
||||
// Get the text content of each <option>
|
||||
$actualOptions[] = trim($option->textContent);
|
||||
}
|
||||
}
|
||||
// Assert that the actual options match the expected options
|
||||
if (empty($expectedOptions)) {
|
||||
$this->assertEmpty($actualOptions);
|
||||
}
|
||||
foreach ($expectedOptions as $expectedValue) {
|
||||
$this->assertContains($expectedValue, $actualOptions, "Missing expected option '$expectedValue' for object: $object, group: $group");
|
||||
}
|
||||
}
|
||||
|
||||
public static function leadFieldOrderChoiceListProvider(): \Generator
|
||||
{
|
||||
yield ['lead', 'core', ['Fax', 'Website']];
|
||||
yield ['lead', 'social', ['Facebook', 'Foursquare', 'Instagram']];
|
||||
yield ['company', 'core', []];
|
||||
}
|
||||
|
||||
public function testTogglePreferredLeadChannelActionWithFlashMessage(): void
|
||||
{
|
||||
$contact = $this->createContact('test@example.com');
|
||||
|
||||
$payload = [
|
||||
'action' => 'lead:togglePreferredLeadChannel',
|
||||
'leadId' => $contact->getId(),
|
||||
'channel' => 'email',
|
||||
'channelAction' => 'remove',
|
||||
];
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$payload = [
|
||||
'action' => 'lead:togglePreferredLeadChannel',
|
||||
'leadId' => $contact->getId(),
|
||||
'channel' => 'email',
|
||||
'channelAction' => 'add',
|
||||
];
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$response = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertTrue(isset($response['success']), 'The response does not contain the `success` param.');
|
||||
$this->assertSame(1, $response['success']);
|
||||
$this->assertTrue(isset($response['flashes']), 'The response should contain flashes');
|
||||
$this->assertNotEmpty($response['flashes'], 'The flashes should not be empty');
|
||||
|
||||
$this->assertStringContainsString('Contact is now contactable on the email channel', $response['flashes'], 'Flash message about channel being contactable should be present');
|
||||
}
|
||||
|
||||
public function testRemoveBounceStatusActionWithFlashMessage(): void
|
||||
{
|
||||
$contact = $this->createContact('bounce@example.com');
|
||||
|
||||
$dnc = new DoNotContact();
|
||||
$dnc->setLead($contact);
|
||||
$dnc->setChannel('email');
|
||||
$dnc->setReason(DoNotContact::BOUNCED);
|
||||
$dnc->setDateAdded(new \DateTime());
|
||||
|
||||
$this->em->persist($dnc);
|
||||
$this->em->flush();
|
||||
|
||||
$payload = [
|
||||
'action' => 'lead:removeBounceStatus',
|
||||
'id' => $dnc->getId(),
|
||||
'channel' => 'email',
|
||||
];
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$response = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertTrue(isset($response['success']), 'The response does not contain the `success` param.');
|
||||
$this->assertSame(1, $response['success']);
|
||||
$this->assertTrue(isset($response['flashes']), 'The response should contain flashes');
|
||||
$this->assertNotEmpty($response['flashes'], 'The flashes should not be empty');
|
||||
|
||||
$this->assertStringContainsString('Contact is now contactable on the email channel', $response['flashes'], 'Flash message about channel being contactable should be present');
|
||||
}
|
||||
|
||||
public function testFieldListAction(): void
|
||||
{
|
||||
// Check if alias is null
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:fieldList',
|
||||
'field' => '',
|
||||
'filter' => '',
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame('Alias cannot be empty', $response['error']);
|
||||
|
||||
// User search for filter
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:fieldList',
|
||||
'field' => 'owner_id',
|
||||
'filter' => 'Admin',
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame('Admin User', $response[0]['value']);
|
||||
|
||||
// Check if filed type is not lookup
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:fieldList',
|
||||
'field' => 'city',
|
||||
'filter' => '',
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertEmpty($response);
|
||||
|
||||
// Check if filed type is lookup
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:fieldList',
|
||||
'field' => 'title',
|
||||
'filter' => '',
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertArrayHasKey(0, $response);
|
||||
}
|
||||
|
||||
public function testGetLookupChoiceListForGlobalCategory(): void
|
||||
{
|
||||
$category1 = $this->createGlobalCategory('GC11');
|
||||
$category2 = $this->createGlobalCategory('GC12');
|
||||
|
||||
// Search global category with string filter
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:getLookupChoiceList',
|
||||
'for_lookup' => 1,
|
||||
'searchKey' => 'category.category',
|
||||
'category_category' => 'GC11',
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
Assert::assertSame($category1->getTitle(), $response[0]['text']);
|
||||
|
||||
// Search global category with array of ids
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:getLookupChoiceList',
|
||||
'for_lookup' => 1,
|
||||
'searchKey' => 'category.category',
|
||||
'category_category' => [$category1->getId(), $category2->getId()],
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
Assert::assertCount(2, $response);
|
||||
}
|
||||
|
||||
public function testLoadSegmentFilterFormForSubscribedCategory(): void
|
||||
{
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:loadSegmentFilterForm',
|
||||
'fieldAlias' => 'globalcategory',
|
||||
'fieldObject' => 'lead',
|
||||
'operator' => 'in',
|
||||
'filterNum' => 2,
|
||||
]
|
||||
);
|
||||
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertArrayHasKey('viewParameters', $response);
|
||||
Assert::assertStringContainsString('data-model="category.category"', $response['viewParameters']['form']);
|
||||
}
|
||||
|
||||
private function getMembersForCampaign(int $campaignId): array
|
||||
{
|
||||
return $this->connection->createQueryBuilder()
|
||||
->select('cl.lead_id, cl.manually_added, cl.manually_removed')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where("cl.campaign_id = {$campaignId}")
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
}
|
||||
|
||||
private function createContact(string $email): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setEmail($email);
|
||||
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
|
||||
$campaign->setName('Campaign A');
|
||||
$campaign->setCanvasSettings(
|
||||
[
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => '148',
|
||||
'positionX' => '760',
|
||||
'positionY' => '155',
|
||||
],
|
||||
[
|
||||
'id' => 'lists',
|
||||
'positionX' => '860',
|
||||
'positionY' => '50',
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => '148',
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createGlobalCategory(string $title): Category
|
||||
{
|
||||
$category = new Category();
|
||||
$category->setIsPublished(true)
|
||||
->setTitle($title)
|
||||
->setAlias(strtolower($title))
|
||||
->setBundle('global');
|
||||
|
||||
$this->em->persist($category);
|
||||
$this->em->flush();
|
||||
|
||||
return $category;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CompanyApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @throws \Doctrine\ORM\Exception\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
protected function markCompanyEmailAsUnique(): void
|
||||
{
|
||||
$fieldRepository = $this->em->getRepository(LeadField::class);
|
||||
$companyEmailField = $fieldRepository->findOneBy(['alias' => 'companyemail']);
|
||||
\assert($companyEmailField instanceof LeadField);
|
||||
$companyEmailField->setIsUniqueIdentifer(true);
|
||||
$this->em->persist($companyEmailField);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// Disable API just for specific test.
|
||||
$this->configParams['api_enabled'] = 'testDisabledApi' !== $this->name();
|
||||
$this->configParams['company_unique_identifiers_operator'] = 'AND';
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testBatchNewEndpoint(): void
|
||||
{
|
||||
$this->markCompanyEmailAsUnique();
|
||||
|
||||
$payload = [
|
||||
[
|
||||
'companyname' => 'BatchUpdate',
|
||||
],
|
||||
[
|
||||
'companyname' => 'BatchUpdate2',
|
||||
],
|
||||
[
|
||||
'companyname' => 'BatchUpdate3',
|
||||
],
|
||||
];
|
||||
|
||||
// create 3 new companies
|
||||
$this->client->request('POST', '/api/companies/batch/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
Assert::assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Assert status codes
|
||||
$this->assertEquals(Response::HTTP_CREATED, $response['statusCodes'][0]);
|
||||
$companyId1 = $response['companies'][0]['id'];
|
||||
$this->assertEquals(Response::HTTP_CREATED, $response['statusCodes'][1]);
|
||||
$this->assertEquals(Response::HTTP_CREATED, $response['statusCodes'][2]);
|
||||
|
||||
// Assert email
|
||||
$this->assertEquals($payload[0]['companyname'], $response['companies'][0]['fields']['all']['companyname']);
|
||||
$this->assertEquals($payload[1]['companyname'], $response['companies'][1]['fields']['all']['companyname']);
|
||||
$this->assertEquals($payload[2]['companyname'], $response['companies'][2]['fields']['all']['companyname']);
|
||||
|
||||
$payload = [
|
||||
[
|
||||
'companyname' => 'BatchUpdate',
|
||||
],
|
||||
];
|
||||
|
||||
// use unique field to not create new company
|
||||
$this->client->request('POST', '/api/companies/batch/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
Assert::assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response['statusCodes'][0]);
|
||||
$this->assertEquals($companyId1, $response['companies'][0]['id']);
|
||||
|
||||
// Assert email
|
||||
$this->assertEquals('BatchUpdate', $response['companies'][0]['fields']['all']['companyname']);
|
||||
|
||||
$payload = [
|
||||
[
|
||||
'companyname' => 'BatchUpdate',
|
||||
'companyemail' => 'BatchUpdate@update.com',
|
||||
],
|
||||
];
|
||||
|
||||
// use both unique fields and create new, because use AND operator between unique fields
|
||||
$this->client->request('POST', '/api/companies/batch/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
Assert::assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertEquals(Response::HTTP_CREATED, $response['statusCodes'][0]);
|
||||
$this->assertNotEquals($companyId1, $response['companies'][0]['id']);
|
||||
}
|
||||
|
||||
public function testSingleNewEndpoint(): void
|
||||
{
|
||||
$this->markCompanyEmailAsUnique();
|
||||
|
||||
$payload = [
|
||||
'companyname' => 'API',
|
||||
];
|
||||
|
||||
$this->client->request('POST', '/api/companies/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$companyId = $response['company']['id'];
|
||||
|
||||
$this->assertEquals($payload['companyname'], $response['company']['fields']['all']['companyname']);
|
||||
|
||||
// Lets try to create the same company
|
||||
$this->client->request('POST', '/api/companies/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertEquals($companyId, $response['company']['id']);
|
||||
|
||||
$payload = [
|
||||
'companyname' => 'API',
|
||||
'companyemail' => 'api@api.com',
|
||||
];
|
||||
|
||||
// Lets try to create the new company because use unique fields with AND operator
|
||||
$this->client->request('POST', '/api/companies/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertNotEquals($companyId, $response['company']['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating a company via API Platform v2 endpoint.
|
||||
*
|
||||
* @param array<string, mixed> $companyData
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('companyCreateDataProvider')]
|
||||
public function testCreateCompanyViaApiPlatform(array $companyData, int $expectedStatusCode): void
|
||||
{
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/v2/companies',
|
||||
[],
|
||||
[],
|
||||
[
|
||||
'CONTENT_TYPE' => 'application/ld+json',
|
||||
'HTTP_ACCEPT' => 'application/ld+json',
|
||||
],
|
||||
json_encode($companyData)
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertSame($expectedStatusCode, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
if (Response::HTTP_CREATED === $expectedStatusCode) {
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
|
||||
$this->assertIsArray($responseData);
|
||||
$this->assertArrayHasKey('id', $responseData);
|
||||
$this->assertArrayHasKey('score', $responseData);
|
||||
|
||||
// Verify the company was actually created in the database
|
||||
$companyRepository = $this->em->getRepository(\Mautic\LeadBundle\Entity\Company::class);
|
||||
$company = $companyRepository->find($responseData['id']);
|
||||
|
||||
$this->assertInstanceOf(\Mautic\LeadBundle\Entity\Company::class, $company);
|
||||
$this->assertSame($companyData['name'] ?? null, $company->getName());
|
||||
$this->assertSame($companyData['score'] ?? 0, $company->getScore());
|
||||
$this->assertSame($companyData['city'] ?? null, $company->getCity());
|
||||
$this->assertSame($companyData['state'] ?? null, $company->getState());
|
||||
$this->assertSame($companyData['country'] ?? null, $company->getCountry());
|
||||
$this->assertSame($companyData['industry'] ?? null, $company->getIndustry());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{companyData: array<string, mixed>, expectedStatusCode: int}>
|
||||
*/
|
||||
public static function companyCreateDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'valid company with all fields' => [
|
||||
'companyData' => [
|
||||
'score' => 0,
|
||||
'socialCache' => [],
|
||||
'city' => 'Boston',
|
||||
'state' => 'Massachusetts',
|
||||
'country' => 'United States',
|
||||
'name' => 'Mautic',
|
||||
'industry' => 'Software',
|
||||
],
|
||||
'expectedStatusCode' => Response::HTTP_CREATED,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\LeadBundle\Controller\Api\CustomFieldsApiControllerTrait;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class CustomFieldsApiControllerTraitTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testGetEntityFormOptions(): void
|
||||
{
|
||||
$result = [
|
||||
'field_1' => [
|
||||
'label' => 'Field 1',
|
||||
'type' => 'text',
|
||||
],
|
||||
'field_2' => [
|
||||
'label' => 'Field 2',
|
||||
'type' => 'text',
|
||||
],
|
||||
];
|
||||
|
||||
$paginator = $this->createMock(Paginator::class);
|
||||
$paginator->method('getIterator')
|
||||
->willReturn($result);
|
||||
|
||||
$modelFake = $this->createMock(FieldModel::class);
|
||||
$modelFake->expects(self::once())
|
||||
->method('getEntities')
|
||||
->willReturn($paginator);
|
||||
|
||||
$controller = new class($modelFake) {
|
||||
use CustomFieldsApiControllerTrait;
|
||||
|
||||
private object $model;
|
||||
private string $entityNameOne = 'lead';
|
||||
|
||||
public function __construct(object $modelFake)
|
||||
{
|
||||
$this->model = $modelFake;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getEntityFormOptionsPublic(): array
|
||||
{
|
||||
return $this->getEntityFormOptions();
|
||||
}
|
||||
|
||||
public function getModel(?string $name): object
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
};
|
||||
|
||||
Assert::assertSame($result, (array) $controller->getEntityFormOptionsPublic()['fields']); // Calling once, should be live
|
||||
Assert::assertSame($result, (array) $controller->getEntityFormOptionsPublic()['fields']); // Calling twice, should be cached
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DeviceApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testPutEditWithInexistingIdSoItShouldCreate(): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_PUT, '/api/devices/99999/edit', [
|
||||
'device' => 'desktop',
|
||||
'deviceOsName' => 'Ubuntu',
|
||||
'deviceOsShortName' => 'UBT',
|
||||
'deviceOsPlatform' => 'x64',
|
||||
'lead' => $contact->getId(),
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(Response::HTTP_CREATED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\IgnoreAnnotation;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @IgnoreAnnotation("covers")
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(\Mautic\LeadBundle\Controller\Api\FieldApiController::class)]
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(\Mautic\LeadBundle\Field\Command\CreateCustomFieldCommand::class)]
|
||||
final class FieldApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['create_custom_field_in_background'] = 'testFieldApiEndpointsWithBackgroundProcessingEnabled' === $this->name();
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testCreatingMultiselectField(): void
|
||||
{
|
||||
$payload = [
|
||||
'label' => 'Shops (TB)',
|
||||
'alias' => 'shops',
|
||||
'type' => 'multiselect',
|
||||
'isPubliclyUpdatable' => true,
|
||||
'isUniqueIdentifier' => false,
|
||||
'properties' => [
|
||||
'list' => [
|
||||
['label' => 'label1', 'value' => 'value1'],
|
||||
['label' => 'label2', 'value' => 'value2'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$typeSafePayload = $this->generateTypeSafePayload($payload);
|
||||
$this->client->request(Request::METHOD_POST, '/api/fields/contact/new', $typeSafePayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$fieldResponse = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
self::assertResponseStatusCodeSame(Response::HTTP_CREATED, $clientResponse->getContent());
|
||||
Assert::assertTrue($fieldResponse['field']['isPublished']);
|
||||
Assert::assertGreaterThan(0, $fieldResponse['field']['id']);
|
||||
Assert::assertSame($payload['label'], $fieldResponse['field']['label']);
|
||||
Assert::assertSame($payload['alias'], $fieldResponse['field']['alias']);
|
||||
Assert::assertSame($payload['type'], $fieldResponse['field']['type']);
|
||||
Assert::assertSame($payload['isPubliclyUpdatable'], $fieldResponse['field']['isPubliclyUpdatable']);
|
||||
Assert::assertSame($payload['isUniqueIdentifier'], $fieldResponse['field']['isUniqueIdentifier']);
|
||||
Assert::assertSame($payload['properties'], $fieldResponse['field']['properties']);
|
||||
|
||||
// Cleanup
|
||||
$this->client->request(Request::METHOD_DELETE, '/api/fields/contact/'.$fieldResponse['field']['id'].'/delete', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful($clientResponse->getContent());
|
||||
}
|
||||
|
||||
public function testFieldApiEndpointsWithBackgroundProcessingEnabled(): void
|
||||
{
|
||||
$alias = uniqid('field');
|
||||
$payload = $this->getCreatePayload($alias);
|
||||
$id = $this->assertCreateResponse($payload, Response::HTTP_ACCEPTED);
|
||||
|
||||
// Test that the command will create the field
|
||||
$commandTester = $this->testSymfonyCommand('mautic:custom-field:create-column', ['--id' => $id]);
|
||||
|
||||
$this->assertEquals(0, $commandTester->getStatusCode());
|
||||
|
||||
// Test fetching
|
||||
$this->assertGetResponse($payload, $id);
|
||||
|
||||
// Test editing
|
||||
$payload = $this->getEditPayload($id);
|
||||
$this->assertPatchResponse($payload, $id, $alias);
|
||||
|
||||
// Test deleting
|
||||
$this->assertDeleteResponse($payload, $id, $alias, true);
|
||||
}
|
||||
|
||||
public function testFieldApiEndpointsWithBackgroundProcessingDisabled(): void
|
||||
{
|
||||
$alias = uniqid('field');
|
||||
$payload = $this->getCreatePayload($alias);
|
||||
$id = $this->assertCreateResponse($payload, Response::HTTP_CREATED);
|
||||
|
||||
// Test fetching
|
||||
$this->assertGetResponse($payload, $id);
|
||||
|
||||
// Test editing
|
||||
$payload = $this->getEditPayload($id);
|
||||
$this->assertPatchResponse($payload, $id, $alias);
|
||||
|
||||
// Test deleting
|
||||
$this->assertDeleteResponse($payload, $id, $alias, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, string>> $properties
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataForCreatingNewBooleanFieldApiEndpoint')]
|
||||
public function testCreatingNewBooleanFieldApiEndpoint(array $properties, string $expectedMessage): void
|
||||
{
|
||||
$payload = [
|
||||
'label' => 'Request a meeting',
|
||||
'alias' => 'meeting',
|
||||
'type' => 'boolean',
|
||||
'isPubliclyUpdatable' => true,
|
||||
'isUniqueIdentifier' => false,
|
||||
];
|
||||
|
||||
$payload += $properties;
|
||||
|
||||
$typeSafePayload = $this->generateTypeSafePayload($payload);
|
||||
$this->client->request(Request::METHOD_POST, '/api/fields/contact/new', $typeSafePayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$errorResponse = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
Assert::assertArrayHasKey('errors', $errorResponse);
|
||||
Assert::assertSame($errorResponse['errors'][0]['code'], $clientResponse->getStatusCode());
|
||||
Assert::assertSame($expectedMessage, $errorResponse['errors'][0]['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array<int, string|array<string, array<string, string>>>>
|
||||
*/
|
||||
public static function dataForCreatingNewBooleanFieldApiEndpoint(): iterable
|
||||
{
|
||||
yield 'No properties' => [
|
||||
[
|
||||
],
|
||||
'A \'positive\' label is required.',
|
||||
];
|
||||
|
||||
yield 'Only Yes' => [
|
||||
[
|
||||
'properties'=> [
|
||||
'yes' => 'Yes',
|
||||
],
|
||||
],
|
||||
'A \'negative\' label is required.',
|
||||
];
|
||||
|
||||
yield 'Only No' => [
|
||||
[
|
||||
'properties'=> [
|
||||
'no' => 'No',
|
||||
],
|
||||
],
|
||||
'A \'positive\' label is required.',
|
||||
];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('provideEmptyMultiSelectValue')]
|
||||
public function testMultiselectSetDefaultValue(mixed $defaultFieldValue): void
|
||||
{
|
||||
$fieldAlias = 'test_multi';
|
||||
|
||||
$fieldModel = $this->getContainer()->get(FieldModel::class);
|
||||
\assert($fieldModel instanceof FieldModel);
|
||||
|
||||
$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 = new Lead();
|
||||
$contact->setEmail('email@acquia.cz');
|
||||
|
||||
$contact->addUpdatedField($fieldAlias, ['bramborak', 'makovec']);
|
||||
$contactModel = self::getContainer()->get(LeadModel::class);
|
||||
\assert($contactModel instanceof LeadModel);
|
||||
$contactModel->saveEntity($contact);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Call endpoint
|
||||
$this->client->request('GET', '/api/contacts/'.(string) $contact->getId());
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('bramborak|makovec', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
|
||||
// Test patch and values should be updated
|
||||
$updatedValues = [
|
||||
$fieldAlias => ['halusky'],
|
||||
];
|
||||
|
||||
$this->client->request(
|
||||
'PATCH',
|
||||
sprintf('/api/contacts/%d/edit', $contact->getId()),
|
||||
$updatedValues
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('halusky', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
|
||||
// Test empty patch and values should be updated
|
||||
$updatedValues = [
|
||||
$fieldAlias => $defaultFieldValue,
|
||||
'overwriteWithBlank' => true,
|
||||
];
|
||||
|
||||
$this->client->request(
|
||||
'PATCH',
|
||||
sprintf('/api/contacts/%d/edit', $contact->getId()),
|
||||
$updatedValues
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Iterator<string, array<string|array<string|null>|null>>
|
||||
*/
|
||||
public static function provideEmptyMultiSelectValue(): \Iterator
|
||||
{
|
||||
yield 'null' => [null];
|
||||
yield 'empty string value' => [''];
|
||||
yield 'empty array with empty string value' => [['']];
|
||||
yield 'empty array with null value' => [[null]];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('provideEmptySelectValue')]
|
||||
public function testSelectSetDefaultValue(mixed $defaultFieldValue): void
|
||||
{
|
||||
$fieldAlias = 'test_single';
|
||||
|
||||
$fieldModel = $this->getContainer()->get(FieldModel::class);
|
||||
\assert($fieldModel instanceof FieldModel);
|
||||
|
||||
$fields = $fieldModel->getLeadFieldCustomFields();
|
||||
Assert::assertEmpty($fields, 'There are no Custom Fields.');
|
||||
|
||||
// Add field.
|
||||
$leadField = new LeadField();
|
||||
$leadField->setName('Test Field')
|
||||
->setAlias($fieldAlias)
|
||||
->setType('select')
|
||||
->setObject('lead')
|
||||
->setProperties([
|
||||
'list' => [
|
||||
[
|
||||
'label' => 'Halusky',
|
||||
'value' => 'halusky',
|
||||
],
|
||||
[
|
||||
'label' => 'Bramborak',
|
||||
'value' => 'bramborak',
|
||||
],
|
||||
[
|
||||
'label' => 'Makovec',
|
||||
'value' => 'makovec',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$fieldModel->saveEntity($leadField);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('email@acquia.cz');
|
||||
|
||||
$contact->addUpdatedField($fieldAlias, ['makovec']);
|
||||
$contactModel = self::getContainer()->get(LeadModel::class);
|
||||
\assert($contactModel instanceof LeadModel);
|
||||
$contactModel->saveEntity($contact);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Call endpoint
|
||||
$this->client->request('GET', '/api/contacts/'.(string) $contact->getId());
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('makovec', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
|
||||
// Test patch and values should be updated
|
||||
$updatedValues = [
|
||||
$fieldAlias => 'halusky',
|
||||
];
|
||||
|
||||
$this->client->request(
|
||||
'PATCH',
|
||||
sprintf('/api/contacts/%d/edit', $contact->getId()),
|
||||
$updatedValues
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('halusky', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
|
||||
// Test empty patch and values should be updated
|
||||
$updatedValues = [
|
||||
$fieldAlias => $defaultFieldValue,
|
||||
'overwriteWithBlank' => true,
|
||||
];
|
||||
|
||||
$this->client->request(
|
||||
'PATCH',
|
||||
sprintf('/api/contacts/%d/edit', $contact->getId()),
|
||||
$updatedValues
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Iterator<string, array<string|null>>
|
||||
*/
|
||||
public static function provideEmptySelectValue(): \Iterator
|
||||
{
|
||||
yield 'null' => [null];
|
||||
yield 'empty string value' => [''];
|
||||
}
|
||||
|
||||
private function assertCreateResponse(array $payload, int $expectedStatusCode): int
|
||||
{
|
||||
// Test creating a new field
|
||||
|
||||
$typeSafePayload = $this->generateTypeSafePayload($payload);
|
||||
$this->client->request('POST', '/api/fields/contact/new', $typeSafePayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// should be "accepted" if pushed to the background; otherwise "created"
|
||||
$this->assertEquals($expectedStatusCode, $clientResponse->getStatusCode());
|
||||
|
||||
// Assert that the fields returned are what is expected
|
||||
foreach ($payload as $key => $value) {
|
||||
$this->assertTrue(isset($response['field'][$key]));
|
||||
|
||||
if (Response::HTTP_ACCEPTED === $expectedStatusCode && 'isPublished' === $key) {
|
||||
// This should be false because the background job publishes once ready
|
||||
$this->assertEquals(false, $response['field'][$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertEquals($value, $response['field'][$key]);
|
||||
}
|
||||
|
||||
return $response['field']['id'];
|
||||
}
|
||||
|
||||
private function assertGetResponse(array $payload, int $id): void
|
||||
{
|
||||
// Test get and that the field was published
|
||||
$this->client->request('GET', sprintf('/api/fields/contact/%s', $id));
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
|
||||
// Assert that the fields returned are what is expected and the field is now published
|
||||
foreach ($payload as $key => $value) {
|
||||
$this->assertTrue(isset($response['field'][$key]));
|
||||
$this->assertEquals($value, $response['field'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
private function assertPatchResponse(array $payload, int $id, string $alias): void
|
||||
{
|
||||
$typeSafePayload = $this->generateTypeSafePayload($payload);
|
||||
$this->client->request('PATCH', sprintf('/api/fields/contact/%s/edit', $id), $typeSafePayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Assert that the fields returned are what is expected noting that certain fields should not be editable
|
||||
foreach ($payload as $key => $value) {
|
||||
$this->assertTrue(isset($response['field'][$key]));
|
||||
|
||||
match ($key) {
|
||||
'alias' => $this->assertEquals($alias, $response['field'][$key]),
|
||||
'object' => $this->assertEquals('lead', $response['field'][$key]),
|
||||
'type' => $this->assertEquals('text', $response['field'][$key]),
|
||||
default => $this->assertEquals($value, $response['field'][$key]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private function assertDeleteResponse(array $payload, int $id, string $alias, bool $isBackground): void
|
||||
{
|
||||
// Test the field is deleted
|
||||
$this->client->request('DELETE', sprintf('/api/fields/contact/%s/delete', $id));
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Assert that the fields returned are what is expected
|
||||
foreach ($payload as $key => $value) {
|
||||
// use array has key because ID will now be null
|
||||
$this->assertArrayHasKey($key, $response['field']);
|
||||
|
||||
match ($key) {
|
||||
'id' => $isBackground ? $this->assertEquals($value, $response['field'][$key]) : $this->assertNull($response['field'][$key]),
|
||||
'alias' => $this->assertEquals($alias, $response['field'][$key]),
|
||||
'object' => $this->assertEquals('lead', $response['field'][$key]),
|
||||
'type' => $this->assertEquals('text', $response['field'][$key]),
|
||||
default => $this->assertEquals($value, $response['field'][$key]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private function getCreatePayload(string $alias): array
|
||||
{
|
||||
return [
|
||||
'isPublished' => true,
|
||||
'label' => 'New Field',
|
||||
'alias' => $alias,
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'defaultValue' => 'foobar',
|
||||
'isRequired' => true,
|
||||
'isPubliclyUpdatable' => true,
|
||||
'isUniqueIdentifier' => true,
|
||||
'isVisible' => false,
|
||||
'isListable' => false,
|
||||
'isIndex' => true, // Must be true, because if isUniqueIdentifier field is true the contact field *must* be indexed.
|
||||
'charLengthLimit' => 25,
|
||||
'properties' => [],
|
||||
];
|
||||
}
|
||||
|
||||
private function getEditPayload(int $id): array
|
||||
{
|
||||
return [
|
||||
'id' => $id,
|
||||
'label' => 'Foo Bar',
|
||||
'alias' => 'should_not_change',
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'company',
|
||||
'defaultValue' => 'foobar',
|
||||
'isRequired' => false,
|
||||
'isPubliclyUpdatable' => false,
|
||||
'isUniqueIdentifier' => false,
|
||||
'isVisible' => true,
|
||||
'isListable' => true,
|
||||
'isIndex' => false, // Can be false, if isUniqueIdentifier the field is *not* indexed.
|
||||
'charLengthLimit' => 50,
|
||||
'properties' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Controller\Api\FieldApiController;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\Router;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class FieldApiControllerTest extends TestCase
|
||||
{
|
||||
private $defaultWhere = [
|
||||
[
|
||||
'col' => 'object',
|
||||
'expr' => 'eq',
|
||||
'val' => null,
|
||||
],
|
||||
];
|
||||
|
||||
public function testgetWhereFromRequestWithNoWhere(): void
|
||||
{
|
||||
$request = new Request();
|
||||
$result = $this->getResultFromProtectedMethod('getWhereFromRequest', [$request], $request);
|
||||
|
||||
$this->assertEquals($this->defaultWhere, $result);
|
||||
}
|
||||
|
||||
public function testgetWhereFromRequestWithSomeWhere(): void
|
||||
{
|
||||
$where = [
|
||||
[
|
||||
'col' => 'id',
|
||||
'expr' => 'eq',
|
||||
'val' => 5,
|
||||
],
|
||||
];
|
||||
|
||||
$request = new Request(['where' => $where]);
|
||||
$result = $this->getResultFromProtectedMethod('getWhereFromRequest', [$request], $request);
|
||||
|
||||
$this->assertEquals(array_merge($where, $this->defaultWhere), $result);
|
||||
}
|
||||
|
||||
protected function getResultFromProtectedMethod($method, array $args, Request $request)
|
||||
{
|
||||
$requestStack = $this->createMock(RequestStack::class);
|
||||
$requestStack->method('getCurrentRequest')
|
||||
->willReturn($request);
|
||||
|
||||
$fieldRepository = $this->createMock(LeadFieldRepository::class);
|
||||
$fieldModel = $this->createMock(FieldModel::class);
|
||||
$fieldModel->method('getRepository')
|
||||
->willReturn($fieldRepository);
|
||||
$modelFactory = $this->createMock(ModelFactory::class);
|
||||
$controller = new FieldApiController(
|
||||
$this->createMock(CorePermissions::class),
|
||||
$this->createMock(Translator::class),
|
||||
$this->createMock(EntityResultHelper::class),
|
||||
$this->createMock(Router::class),
|
||||
$this->createMock(FormFactoryInterface::class),
|
||||
$this->createMock(AppVersion::class),
|
||||
$requestStack,
|
||||
$this->createMock(ManagerRegistry::class),
|
||||
$modelFactory,
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(CoreParametersHelper::class),
|
||||
$fieldModel,
|
||||
);
|
||||
|
||||
$controllerReflection = new \ReflectionClass(FieldApiController::class);
|
||||
$method = $controllerReflection->getMethod($method);
|
||||
$method->setAccessible(true);
|
||||
|
||||
return $method->invokeArgs($controller, $args);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Doctrine\Bundle\DoctrineBundle\DataCollector\DoctrineDataCollector;
|
||||
use Doctrine\Common\Cache\CacheProvider;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Tests that enable debug and profiler to test performance optimizations.
|
||||
* These tests are slower as debug and profiler are enabled. Add tests here only if you need profiler.
|
||||
*/
|
||||
final class LeadApiControllerProfilerTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
protected array $clientOptions = ['debug' => true];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// Disable API just for specific test.
|
||||
$this->configParams['api_enabled'] = true;
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testGetContacts(): void
|
||||
{
|
||||
// reset result cache if any
|
||||
$cache = $this->em->getConfiguration()->getResultCache();
|
||||
|
||||
if ($cache instanceof CacheProvider) {
|
||||
$cache = clone $cache;
|
||||
$cache->setNamespace('leadCount');
|
||||
$cache->deleteAll();
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 11; ++$i) {
|
||||
$contact = new Lead();
|
||||
$contact->setEmail("contact{$i}@email.com");
|
||||
$contact->setPoints($i);
|
||||
$this->em->persist($contact);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->enableProfiler();
|
||||
|
||||
// Make 4 requests to see how many count queries we'll get.
|
||||
$this->getContacts(11);
|
||||
$this->getContacts(11);
|
||||
$this->getContacts(5, ['where' => [['col' => 'l.points', 'expr' => 'lt', 'val' => 5]]]);
|
||||
$this->getContacts(5, ['where' => [['col' => 'l.points', 'expr' => 'lt', 'val' => 5]]]);
|
||||
|
||||
// Without the cache, there would be 4 COUNT queries. With the cache, there is just one.
|
||||
Assert::assertCount(2, $this->findCountQueries());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $queryParams
|
||||
*/
|
||||
private function getContacts(int $expectedCount, array $queryParams = []): void
|
||||
{
|
||||
// We have to reset the param counter to emulate 2 requests otherwise the counter will cause the queries to be different.
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
\assert($leadRepository instanceof LeadRepository);
|
||||
$reflection = new \ReflectionClass($leadRepository);
|
||||
$counter = $reflection->getProperty('lastUsedParameterId');
|
||||
$counter->setAccessible(true);
|
||||
$counter->setValue($leadRepository, 0);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/api/contacts', $queryParams);
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$response = json_decode($this->client->getResponse()->getContent(), true);
|
||||
Assert::assertSame($expectedCount, (int) $response['total']);
|
||||
Assert::assertCount($expectedCount, $response['contacts']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed[]>
|
||||
*/
|
||||
private function findCountQueries(): array
|
||||
{
|
||||
/** @var DoctrineDataCollector $dbCollector */
|
||||
$dbCollector = $this->client->getProfile()->getCollector('db');
|
||||
$allQueries = $dbCollector->getQueries()['default'];
|
||||
|
||||
return array_filter(
|
||||
$allQueries,
|
||||
fn (array $query) => str_starts_with($query['sql'], 'SELECT COUNT(l.id) as count FROM '.MAUTIC_TABLE_PREFIX.'leads l')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ListApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected ListModel $listModel;
|
||||
|
||||
private string $prefix;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->listModel = static::getContainer()->get('mautic.lead.model.list');
|
||||
$this->prefix = static::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
$this->translator = static::getContainer()->get('translator');
|
||||
}
|
||||
|
||||
protected function beforeBeginTransaction(): void
|
||||
{
|
||||
$this->resetAutoincrement(['categories']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array<string|int|null>>
|
||||
*/
|
||||
public static function regexOperatorProvider(): iterable
|
||||
{
|
||||
yield [
|
||||
'regexp',
|
||||
'^{Test|Test string)', // invalid regex: the first parantheses should not be curly
|
||||
Response::HTTP_BAD_REQUEST,
|
||||
'error',
|
||||
];
|
||||
|
||||
yield [
|
||||
'!regexp',
|
||||
'^(Test|Test string))', // invalid regex: 2 ending parantheses
|
||||
Response::HTTP_BAD_REQUEST,
|
||||
'error',
|
||||
];
|
||||
|
||||
yield [
|
||||
'regexp',
|
||||
'^(Test|Test string)', // valid regex
|
||||
Response::HTTP_CREATED,
|
||||
null,
|
||||
];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('regexOperatorProvider')]
|
||||
public function testRegexOperatorValidation(string $operator, string $regex, int $expectedResponseCode, ?string $expectedErrorMessage): void
|
||||
{
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/api/segments/new',
|
||||
[
|
||||
'name' => 'Regex test',
|
||||
'filters' => [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'operator' => $operator,
|
||||
'properties' => ['filter' => $regex],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
Assert::assertSame($expectedResponseCode, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
if ($expectedErrorMessage) {
|
||||
Assert::assertStringContainsString(
|
||||
$expectedErrorMessage,
|
||||
json_decode($this->client->getResponse()->getContent(), true)['errors'][0]['message'],
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testSingleSegmentWorkflow(): void
|
||||
{
|
||||
$payload = [
|
||||
'name' => 'API segment',
|
||||
'description' => 'Segment created via API test',
|
||||
'filters' => [
|
||||
// Legacy structure.
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
// Current structure.
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'operator' => '=',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'display' => 'outdated name',
|
||||
'filter' => 'outdated_id',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Create:
|
||||
$this->client->request('POST', '/api/segments/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
if (!empty($response['errors'][0])) {
|
||||
$this->fail($response['errors'][0]['code'].': '.$response['errors'][0]['message']);
|
||||
}
|
||||
|
||||
$segmentId = $response['list']['id'];
|
||||
|
||||
$this->assertSame(201, $clientResponse->getStatusCode());
|
||||
$this->assertGreaterThan(0, $segmentId);
|
||||
$this->assertEquals($payload['name'], $response['list']['name']);
|
||||
$this->assertEquals($payload['description'], $response['list']['description']);
|
||||
$this->assertEquals([
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'operator' => '=',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'operator' => '=',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'properties' => [
|
||||
'filter' => null,
|
||||
],
|
||||
],
|
||||
],
|
||||
$response['list']['filters']
|
||||
);
|
||||
|
||||
// Edit:
|
||||
$this->client->request('PATCH', "/api/segments/{$segmentId}/edit", ['name' => 'API segment renamed']);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode());
|
||||
$this->assertSame($segmentId, $response['list']['id'], 'ID of the created segment does not match with the edited one.');
|
||||
$this->assertEquals('API segment renamed', $response['list']['name']);
|
||||
$this->assertEquals($payload['description'], $response['list']['description']);
|
||||
|
||||
// Get:
|
||||
$this->client->request('GET', "/api/segments/{$segmentId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode());
|
||||
$this->assertSame($segmentId, $response['list']['id'], 'ID of the created segment does not match with the fetched one.');
|
||||
$this->assertEquals('API segment renamed', $response['list']['name']);
|
||||
$this->assertEquals($payload['description'], $response['list']['description']);
|
||||
|
||||
// Delete:
|
||||
$this->client->request('DELETE', "/api/segments/{$segmentId}/delete");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode());
|
||||
$this->assertNull($response['list']['id']);
|
||||
$this->assertEquals('API segment renamed', $response['list']['name']);
|
||||
$this->assertEquals($payload['description'], $response['list']['description']);
|
||||
|
||||
// Get (ensure it's deleted):
|
||||
$this->client->request('GET', "/api/segments/{$segmentId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(404, $clientResponse->getStatusCode());
|
||||
$this->assertSame(404, $response['errors'][0]['code']);
|
||||
}
|
||||
|
||||
public function testBatchSegmentWorkflow(): void
|
||||
{
|
||||
$payload = [
|
||||
[
|
||||
'name' => 'API batch segment 1',
|
||||
'description' => 'Segment created via API test',
|
||||
'filters' => [
|
||||
// Legacy structure.
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
// Current structure.
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'operator' => '=',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'API batch segment 2',
|
||||
'description' => 'Segment created via API test',
|
||||
],
|
||||
];
|
||||
|
||||
$this->client->request('POST', '/api/segments/batch/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response1 = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
if (!empty($response1['errors'][0])) {
|
||||
$this->fail($response1['errors'][0]['code'].': '.$response1['errors'][0]['message']);
|
||||
}
|
||||
|
||||
foreach ($response1['statusCodes'] as $statusCode) {
|
||||
$this->assertSame(201, $statusCode);
|
||||
}
|
||||
|
||||
foreach ($response1['lists'] as $key => $segment) {
|
||||
$this->assertGreaterThan(0, $segment['id']);
|
||||
$this->assertTrue($segment['isPublished']);
|
||||
$this->assertTrue($segment['isGlobal']);
|
||||
$this->assertFalse($segment['isPreferenceCenter']);
|
||||
$this->assertSame($payload[$key]['name'], $segment['name']);
|
||||
$this->assertSame($payload[$key]['description'], $segment['description']);
|
||||
$this->assertIsArray($segment['filters']);
|
||||
|
||||
// Make a change for the edit request:
|
||||
$response1['lists'][$key]['isPublished'] = false;
|
||||
}
|
||||
|
||||
// Lets try to create the same segment to see that the values are not re-setted
|
||||
$this->client->request('PATCH', '/api/segments/batch/edit', $response1['lists']);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response2 = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
if (!empty($response2['errors'][0])) {
|
||||
$this->fail($response2['errors'][0]['code'].': '.$response2['errors'][0]['message']);
|
||||
}
|
||||
|
||||
foreach ($response2['statusCodes'] as $statusCode) {
|
||||
$this->assertSame(200, $statusCode);
|
||||
}
|
||||
|
||||
foreach ($response2['lists'] as $key => $segment) {
|
||||
$this->assertGreaterThan(0, $segment['id']);
|
||||
$this->assertFalse($segment['isPublished']);
|
||||
$this->assertTrue($segment['isGlobal']);
|
||||
$this->assertFalse($segment['isPreferenceCenter']);
|
||||
$this->assertSame($payload[$key]['name'], $segment['name']);
|
||||
$this->assertSame($payload[$key]['description'], $segment['description']);
|
||||
}
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
],
|
||||
],
|
||||
$response2['lists'][0]['filters']
|
||||
);
|
||||
|
||||
$this->assertSame([], $response2['lists'][1]['filters']);
|
||||
}
|
||||
|
||||
public function testWeGet422ResponseCodeIfSegmentIsBeingUsedInSomeCampaignAndWeUnpublishIt(): void
|
||||
{
|
||||
$segmentName = 'Segment1';
|
||||
$segment = new LeadList();
|
||||
$segment->setName($segmentName);
|
||||
$segment->setPublicName($segmentName);
|
||||
$segment->setAlias(mb_strtolower($segmentName));
|
||||
$segment->setIsPublished(true);
|
||||
$this->em->persist($segment);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaignName = 'Campaign1';
|
||||
$campaign->setName($campaignName);
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
// insert unpublished record
|
||||
$this->connection->insert($this->prefix.'campaign_leadlist_xref', [
|
||||
'campaign_id' => $campaign->getId(),
|
||||
'leadlist_id' => $segment->getId(),
|
||||
]);
|
||||
|
||||
$this->client->request('PATCH', "/api/segments/{$segment->getId()}/edit", ['isPublished' => 0]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $clientResponse->getStatusCode());
|
||||
Assert::assertArrayHasKey('errors', $response);
|
||||
$errorMessage = $this->translator->trans(
|
||||
'mautic.lead.lists.used_in_campaigns',
|
||||
[
|
||||
'%count%' => '1',
|
||||
'%campaignNames%' => '"'.$campaignName.'"',
|
||||
],
|
||||
'validators'
|
||||
);
|
||||
Assert::assertStringContainsString($errorMessage, $response['errors'][0]['message']);
|
||||
}
|
||||
|
||||
public function testWeGet200ResponseCodeIfSegmentIsNotUsedInCampaignsAndWeUnpublishIt(): void
|
||||
{
|
||||
$segmentName = 'Segment1';
|
||||
$segment = new LeadList();
|
||||
$segment->setName($segmentName);
|
||||
$segment->setPublicName($segmentName);
|
||||
$segment->setAlias(mb_strtolower($segmentName));
|
||||
$segment->setIsPublished(true);
|
||||
$this->em->persist($segment);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('campaign1');
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('PATCH', "/api/segments/{$segment->getId()}/edit", ['isPublished' => 0]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
Assert::assertArrayNotHasKey('errors', $response);
|
||||
}
|
||||
|
||||
public function testUnpublishUsedSingleSegment(): void
|
||||
{
|
||||
$filter = [[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
]];
|
||||
$list1 = $this->saveSegment('s1', 's1', $filter);
|
||||
$filter = [[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => [
|
||||
'filter' => [$list1->getId()],
|
||||
],
|
||||
'display' => '',
|
||||
]];
|
||||
$list2 = $this->saveSegment('s2', 's2', $filter);
|
||||
$this->em->clear();
|
||||
$expectedErrorMessage = sprintf('leadlist: This segment is used in %s, please go back and check segments before unpublishing', $list2->getName());
|
||||
|
||||
$this->client->request('PATCH', "/api/segments/{$list1->getId()}/edit", ['name' => 'API segment renamed', 'isPublished' => false]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $clientResponse->getStatusCode());
|
||||
$this->assertSame($response['errors'][0]['message'], $expectedErrorMessage);
|
||||
}
|
||||
|
||||
public function testUnpublishUsedBatchSegment(): void
|
||||
{
|
||||
$filter = [[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
]];
|
||||
$list1 = $this->saveSegment('s1', 's1', $filter);
|
||||
$filter = [[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => [
|
||||
'filter' => [$list1->getId()],
|
||||
],
|
||||
'display' => '',
|
||||
]];
|
||||
$list2 = $this->saveSegment('s2', 's2', $filter);
|
||||
$this->em->clear();
|
||||
$expectedErrorMessage = sprintf('leadlist: This segment is used in %s, please go back and check segments before unpublishing', $list2->getName());
|
||||
|
||||
$segments = [
|
||||
['id' => $list1->getId(), 'isPublished' => false],
|
||||
['id' => $list2->getId(), 'isPublished' => false],
|
||||
];
|
||||
|
||||
$this->client->request('PATCH', '/api/segments/batch/edit', $segments);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response['statusCodes'][0]);
|
||||
$this->assertSame($response['errors'][0]['message'], $expectedErrorMessage);
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $response['statusCodes'][1]);
|
||||
}
|
||||
|
||||
public function testSegmentWithCategory(): void
|
||||
{
|
||||
$categoryPayload = [
|
||||
'title' => 'API Cat',
|
||||
'alias' => 'kitty',
|
||||
'bundle' => 'segment',
|
||||
];
|
||||
$this->client->request('POST', '/api/categories/new', $categoryPayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$categoryId = $response['category']['id'];
|
||||
|
||||
$segmentPayload = [
|
||||
'name' => 'API segment',
|
||||
'description' => 'Segment created via API test',
|
||||
'category' => $categoryId,
|
||||
];
|
||||
|
||||
// Create:
|
||||
$this->client->request('POST', '/api/segments/new', $segmentPayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
if (!empty($response['errors'][0])) {
|
||||
$this->fail($response['errors'][0]['code'].': '.$response['errors'][0]['message']);
|
||||
}
|
||||
|
||||
$segmentId = $response['list']['id'];
|
||||
|
||||
// Get segment with category by id:
|
||||
$this->client->request('GET', "/api/segments/{$segmentId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertTrue($clientResponse->isOk());
|
||||
$this->assertEquals($segmentPayload['category'], $response['list']['category']['id']);
|
||||
|
||||
// Search segments by category:
|
||||
$this->client->request('GET', '/api/segments?search=category:kitty');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertTrue($clientResponse->isOk());
|
||||
$this->assertCount(1, $response['lists']);
|
||||
}
|
||||
|
||||
private function saveSegment(string $name, string $alias, array $filters = [], ?LeadList $segment = null): LeadList
|
||||
{
|
||||
$segment ??= new LeadList();
|
||||
$segment->setName($name)->setPublicName($name)->setAlias($alias)->setFilters($filters);
|
||||
$this->listModel->saveEntity($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class TagApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testTagWorkflow(): void
|
||||
{
|
||||
$tag1Payload = ['tag' => 'test_tag'];
|
||||
|
||||
// Create new tag
|
||||
$this->client->request('POST', '/api/tags/new', $tag1Payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$tagId = $response['tag']['id'];
|
||||
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_CREATED, 'Return code must be 201.');
|
||||
$this->assertGreaterThan(0, $tagId);
|
||||
$this->assertEquals($tag1Payload['tag'], $response['tag']['tag']);
|
||||
|
||||
// Try to create tag with same name
|
||||
$this->client->request('POST', '/api/tags/new', $tag1Payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertResponseIsSuccessful('Return code must be 200.');
|
||||
// The same tag id should be returned
|
||||
$this->assertEquals($tagId, $response['tag']['id'], 'ID of created tag with the same name does not match. Possible duplicates.');
|
||||
$this->assertEquals($tag1Payload['tag'], $response['tag']['tag']);
|
||||
|
||||
// Edit tag name
|
||||
$tag1RenamePayload = ['tag' => 'tag_renamed'];
|
||||
$this->client->request('PATCH', "/api/tags/{$tagId}/edit", $tag1RenamePayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertResponseIsSuccessful('Return code must be 200.');
|
||||
$this->assertSame($tagId, $response['tag']['id'], 'ID of the created tag does not match with the edited one.');
|
||||
$this->assertEquals($tag1RenamePayload['tag'], $response['tag']['tag']);
|
||||
|
||||
// Get tag
|
||||
$this->client->request('GET', "/api/tags/{$tagId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertSame($tagId, $response['tag']['id'], 'ID of the created tag does not match with the fetched one.');
|
||||
$this->assertEquals($tag1RenamePayload['tag'], $response['tag']['tag']);
|
||||
|
||||
// Delete:
|
||||
$this->client->request('DELETE', "/api/tags/{$tagId}/delete");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful($clientResponse->getContent());
|
||||
|
||||
// Get (ensure it's deleted):
|
||||
$this->client->request('GET', "/api/tags/{$tagId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
$this->assertSame(404, $response['errors'][0]['code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if whitespace before or after tag name is removed and duplicates are not created.
|
||||
*/
|
||||
public function testWhitespaceBeforeAndAfterNameNotCreatingDuplicates(): void
|
||||
{
|
||||
$tagName = 'test';
|
||||
$whitespaceTestPayload = ['test', 'test ', ' test', "test\t", "\ttest"];
|
||||
$tagId = null;
|
||||
foreach ($whitespaceTestPayload as $payload) {
|
||||
$this->client->request('POST', '/api/tags/new', ['tag' => $payload]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// whitespace before and after tag name should be removed, name should be the same for each tag
|
||||
$this->assertEquals($tagName, $response['tag']['tag']);
|
||||
if (null === $tagId) {
|
||||
$tagId = $response['tag']['id'];
|
||||
} else {
|
||||
$this->assertSame($tagId, $response['tag']['id'], 'ID of created tag does not match. Possible duplicates.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if special characters in tag name are encoded and duplicates are not created.
|
||||
*/
|
||||
public function testEncodedCharactersNotCreatingDuplicates(): void
|
||||
{
|
||||
$tagInputName = 'hello" world';
|
||||
|
||||
$this->client->request('POST', '/api/tags/new', ['tag' => $tagInputName]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$tagId = $response['tag']['id'];
|
||||
$this->assertGreaterThan(0, $tagId);
|
||||
$this->assertEquals($tagInputName, $response['tag']['tag']);
|
||||
|
||||
// Try to create duplicate
|
||||
$this->client->request('POST', '/api/tags/new', ['tag' => $tagInputName]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertSame($tagId, $response['tag']['id'], 'ID of created tag does not match. Possible duplicates.');
|
||||
}
|
||||
|
||||
public function testTagCreationWithoutRequiredData(): void
|
||||
{
|
||||
// Sending an empty payload should return a 500 server error
|
||||
// TODO ensure that the server sends back a 400 status code instead
|
||||
$this->client->request('POST', '/api/tags/new', []);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertResponseStatusCodeSame(500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class AuditLogControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
private const SALES_USER = 'sales';
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testBatchExportActionAsAdmin(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', '/s/contacts/auditlog/batchExport/'.$contact->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testBatchExportActionAsUserNotPermission(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => self::SALES_USER]);
|
||||
$this->loginUser($user);
|
||||
$this->client->request('GET', '/s/contacts/auditlog/batchExport/'.$contact->getId());
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CompanyControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
private int $company1Id;
|
||||
|
||||
private int $company2Id;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$companiesData = [
|
||||
1 => [
|
||||
'name' => 'Amazon',
|
||||
'state' => 'Washington',
|
||||
'city' => 'Seattle',
|
||||
'country' => 'United States',
|
||||
'industry' => 'Goods',
|
||||
],
|
||||
2 => [
|
||||
'name' => 'Google',
|
||||
'state' => 'Washington',
|
||||
'city' => 'Seattle',
|
||||
'country' => 'United States',
|
||||
'industry' => 'Services',
|
||||
],
|
||||
];
|
||||
|
||||
/** @var \Mautic\LeadBundle\Model\CompanyModel $model */
|
||||
$model = self::getContainer()->get('mautic.lead.model.company');
|
||||
|
||||
foreach ($companiesData as $i => $companyData) {
|
||||
$company = new Company();
|
||||
$company->setIsPublished(true)
|
||||
->setName($companyData['name'])
|
||||
->setState($companyData['state'])
|
||||
->setCity($companyData['city'])
|
||||
->setCountry($companyData['country'])
|
||||
->setIndustry($companyData['industry']);
|
||||
$model->saveEntity($company);
|
||||
|
||||
$this->{'company'.$i.'Id'} = $company->getId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company's view page.
|
||||
*/
|
||||
public function testViewActionCompany(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/companies/view/'.$this->company1Id);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$clientResponseContent = $clientResponse->getContent();
|
||||
$model = self::getContainer()->get('mautic.lead.model.company');
|
||||
$company = $model->getEntity($this->company1Id);
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$this->assertStringContainsString($company->getName(), $clientResponseContent, 'The return must contain the name of company');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company's edit page.
|
||||
*/
|
||||
public function testEditActionCompany(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/companies/edit/'.$this->company1Id);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$clientResponseContent = $clientResponse->getContent();
|
||||
$model = self::getContainer()->get('mautic.lead.model.company');
|
||||
$company = $model->getEntity($this->company1Id);
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$this->assertStringContainsString('Edit Company '.$company->getName(), $clientResponseContent, 'The return must contain \'Edit Company\' text');
|
||||
}
|
||||
|
||||
/* Get company contacts list */
|
||||
public function testListCompanyContacts(): void
|
||||
{
|
||||
/** @var \Mautic\LeadBundle\Model\CompanyModel $companyModel */
|
||||
$companyModel = self::getContainer()->get('mautic.lead.model.company');
|
||||
$leadModel = self::getContainer()->get('mautic.lead.model.lead');
|
||||
$company1 = $companyModel->getEntity($this->company1Id);
|
||||
|
||||
// Create a lead linked to the first company
|
||||
$lead1 = new Lead();
|
||||
$lead1
|
||||
->setFirstname('lead')
|
||||
->setLastname('for '.$company1->getName());
|
||||
$leadModel->saveEntity($lead1);
|
||||
|
||||
$companyModel->addLeadToCompany($company1, $lead1);
|
||||
|
||||
// Create a lead not linked to a company
|
||||
$lead2 = new Lead();
|
||||
$lead2
|
||||
->setFirstname('lead')
|
||||
->setLastname('without company');
|
||||
$leadModel->saveEntity($lead2);
|
||||
|
||||
// Create a lead not linked to a company, but with `ids` in it's name (see https://github.com/mautic/mautic/issues/12415)
|
||||
$lead3 = new Lead();
|
||||
$lead3
|
||||
->setFirstname('lead')
|
||||
->setLastname('without company')
|
||||
->setEmail('example@idstart.com');
|
||||
$leadModel->saveEntity($lead3);
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/company/'.$this->company1Id.'/contacts/');
|
||||
$leadsTableRows = $crawler->filterXPath("//table[@id='leadTable']//tbody//tr");
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertEquals(1, $leadsTableRows->count(), $crawler->html());
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/company/'.$this->company2Id.'/contacts/');
|
||||
$leadsTableRows = $crawler->filterXPath("//table[@id='leadTable']//tbody//tr");
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertEquals(0, $leadsTableRows->count(), $crawler->html());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company's create page.
|
||||
*/
|
||||
public function testNewActionCompany(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/companies/new/');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$clientResponseContent = $clientResponse->getContent();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
}
|
||||
|
||||
public function testNonExitingCompanyIsRedirected(): void
|
||||
{
|
||||
$this->client->followRedirects(false);
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
's/companies/view/1000',
|
||||
);
|
||||
$this->assertEquals(true, $this->client->getResponse()->isRedirect('/s/companies'));
|
||||
}
|
||||
|
||||
public function testNewCompanyMergeButtonVisible(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/companies/new/');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$clientResponseContent = $clientResponse->getContent();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
|
||||
// Use the Crawler to parse the HTML content
|
||||
$crawler = new \Symfony\Component\DomCrawler\Crawler($clientResponseContent);
|
||||
|
||||
// Check for specific buttons by their IDs
|
||||
$applyButton = $crawler->filter('#company_buttons_apply');
|
||||
$saveButton = $crawler->filter('#company_buttons_save');
|
||||
$cancelButton = $crawler->filter('#company_buttons_cancel');
|
||||
$mergeButton = $crawler->filter('#company_buttons_merge');
|
||||
|
||||
$this->assertCount(1, $applyButton, 'Apply button not found');
|
||||
$this->assertCount(1, $saveButton, 'Save button not found');
|
||||
$this->assertCount(1, $cancelButton, 'Cancel button not found');
|
||||
$this->assertCount(0, $mergeButton, 'Merge button found');
|
||||
}
|
||||
|
||||
public function testCompanyWithProject(): void
|
||||
{
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
$this->em->persist($project);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/companies/edit/'.$this->company1Id);
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['company[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedCompany = $this->em->find(Company::class, $this->company1Id);
|
||||
$this->assertSame($project->getId(), $savedCompany->getProjects()->first()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class CompanyProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
|
||||
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
|
||||
{
|
||||
$projectOne = $this->createProject('Project One');
|
||||
$projectTwo = $this->createProject('Project Two');
|
||||
$projectThree = $this->createProject('Project Three');
|
||||
|
||||
$companyAlpha = $this->createCompany('Company Alpha');
|
||||
$companyBeta = $this->createCompany('Company Beta');
|
||||
$this->createCompany('Company Gamma');
|
||||
$this->createCompany('Company Delta');
|
||||
|
||||
$companyAlpha->addProject($projectOne);
|
||||
$companyAlpha->addProject($projectTwo);
|
||||
$companyBeta->addProject($projectTwo);
|
||||
$companyBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/companies', '/s/companies']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
|
||||
*/
|
||||
public static function searchDataProvider(): \Generator
|
||||
{
|
||||
yield 'search by one project' => [
|
||||
'searchTerm' => 'project:"Project Two"',
|
||||
'expectedEntities' => ['Company Alpha', 'Company Beta'],
|
||||
'unexpectedEntities' => ['Company Gamma', 'Company Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND company name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Company Beta'],
|
||||
'unexpectedEntities' => ['Company Alpha', 'Company Gamma', 'Company Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR company name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Company Alpha', 'Company Beta', 'Company Gamma'],
|
||||
'unexpectedEntities' => ['Company Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Company Gamma', 'Company Delta'],
|
||||
'unexpectedEntities' => ['Company Alpha', 'Company Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Company Beta'],
|
||||
'unexpectedEntities' => ['Company Alpha', 'Company Gamma', 'Company Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Company Gamma', 'Company Delta'],
|
||||
'unexpectedEntities' => ['Company Alpha', 'Company Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Company Alpha', 'Company Beta'],
|
||||
'unexpectedEntities' => ['Company Gamma', 'Company Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Company Alpha', 'Company Gamma', 'Company Delta'],
|
||||
'unexpectedEntities' => ['Company Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createCompany(string $name): Company
|
||||
{
|
||||
$company = new Company();
|
||||
$company->setName($name);
|
||||
$this->em->persist($company);
|
||||
|
||||
return $company;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Mautic\CoreBundle\Doctrine\Helper\ColumnSchemaHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class FieldControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
public function testLengthValidationOnLabelFieldWhenAddingCustomFieldFailure(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/new');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$label = 'The leading Drupal Cloud platform to securely develop, deliver, and run websites, applications, and content. Top-of-the-line hosting options are paired with automated testing and development tools. Documentation is also included for the following components';
|
||||
$form['leadfield[label]']->setValue($label);
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$labelErrorMessage = trim($crawler->filter('#leadfield_label')->nextAll()->text());
|
||||
$maxLengthErrorMessageTemplate = 'Label value cannot be longer than 191 characters';
|
||||
|
||||
$this->assertEquals($maxLengthErrorMessageTemplate, $labelErrorMessage);
|
||||
}
|
||||
|
||||
public function testLengthValidationOnLabelFieldWhenAddingCustomFieldSuccess(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/new');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$label = 'Test value for custom field 4';
|
||||
$form['leadfield[label]']->setValue($label);
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$field = $this->em->getRepository(LeadField::class)->findOneBy(['label' => $label]);
|
||||
$this->assertNotNull($field);
|
||||
}
|
||||
|
||||
public function testCloneFieldSubmission(): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$field->setLabel('Field to be cloned');
|
||||
$field->setAlias('field_to_be_cloned');
|
||||
$field->setType('text');
|
||||
|
||||
$this->em->getRepository(LeadField::class)->saveEntity($field);
|
||||
$this->em->clear();
|
||||
|
||||
$field = $this->em->getRepository(LeadField::class)->findOneBy(['alias' => 'field_to_be_cloned']);
|
||||
$this->assertNotNull($field);
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/clone/'.$field->getId());
|
||||
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
$this->assertSelectorTextContains('h1', 'New Custom Field');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$form['leadfield[label]']->setValue('Cloned Field');
|
||||
|
||||
$this->client->submit($form);
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
|
||||
$clonedField = $this->em->getRepository(LeadField::class)->findOneBy(['label' => 'Cloned Field']);
|
||||
$this->assertNotNull($clonedField);
|
||||
$this->assertNotEquals($field->getId(), $clonedField->getId());
|
||||
}
|
||||
|
||||
public function testCloneNonExistentField(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/contacts/fields/clone/9999');
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('getStringTypeFieldsArray')]
|
||||
public function testMaxCharLengthFieldValidationOnStringTypeWhenAddingCustomFieldFailure(string $label, string $type): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/new');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$form['leadfield[label]']->setValue($label);
|
||||
$form['leadfield[object]']->setValue('lead');
|
||||
$form['leadfield[type]']->setValue($type);
|
||||
$form['leadfield[charLengthLimit]']->setValue('260');
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$errorMessage = trim($crawler->filter('#leadfield_charLengthLimit')->nextAll()->text());
|
||||
$maxCharLimitErrorMessage = 'This value should be between 1 and 191.';
|
||||
|
||||
$this->assertEquals($maxCharLimitErrorMessage, $errorMessage);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('getStringTypeFieldsArray')]
|
||||
public function testMaxCharLengthFieldValidationOnStringTypeWhenAddingCustomFieldSuccess(string $label, string $type): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/new');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$form['leadfield[label]']->setValue($label);
|
||||
$form['leadfield[object]']->setValue('lead');
|
||||
$form['leadfield[type]']->setValue($type);
|
||||
$form['leadfield[charLengthLimit]']->setValue('191');
|
||||
$this->client->submit($form);
|
||||
|
||||
$field = $this->em->getRepository(LeadField::class)->findOneBy(['label' => $label]);
|
||||
$this->assertNotNull($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed, mixed>
|
||||
*/
|
||||
public static function getStringTypeFieldsArray(): iterable
|
||||
{
|
||||
yield ['test_email', 'email'];
|
||||
yield ['test_text', 'text'];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('getCustomFields')]
|
||||
public function testCustomFieldCharacterLengthLimit(string $label, string $type): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/new');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$form['leadfield[label]']->setValue($label);
|
||||
$form['leadfield[object]']->setValue('lead');
|
||||
$form['leadfield[type]']->setValue($type);
|
||||
$this->client->submit($form);
|
||||
|
||||
$field = $this->em->getRepository(LeadField::class)->findOneBy(['label' => $label]);
|
||||
$this->assertNotNull($field);
|
||||
|
||||
/** @var ColumnSchemaHelper $helper */
|
||||
$helper = $this->getContainer()->get('mautic.schema.helper.column');
|
||||
|
||||
// Table name to check the fields.
|
||||
$name = 'leads';
|
||||
$schemaHelper = $helper->setName($name);
|
||||
|
||||
/** @var Column $fieldsDescription */
|
||||
$fieldsDescription = $schemaHelper->getColumns()[$label];
|
||||
|
||||
$this->assertSame(191, $fieldsDescription->getLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed, mixed>
|
||||
*/
|
||||
public static function getCustomFields(): iterable
|
||||
{
|
||||
yield ['test_timezone', 'timezone'];
|
||||
yield ['test_locale', 'locale'];
|
||||
yield ['test_country', 'country'];
|
||||
yield ['test_phone', 'tel'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Field\InputFormField;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class FieldFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('provideFieldLength')]
|
||||
public function testNewFieldVarcharFieldLength(int $expectedLength, ?int $inputLength = null): void
|
||||
{
|
||||
$fieldModel = static::getContainer()->get('mautic.lead.model.field');
|
||||
$field = $this->createField('a', 'text', [], $inputLength);
|
||||
$fieldModel->saveEntity($field);
|
||||
|
||||
$tablePrefix = static::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
$columns = $this->connection->createSchemaManager()->listTableColumns("{$tablePrefix}leads");
|
||||
$this->assertEquals($expectedLength, $columns[$field->getAlias()]->getLength());
|
||||
}
|
||||
|
||||
public function testNewMultiSelectField(): void
|
||||
{
|
||||
$fieldModel = static::getContainer()->get('mautic.lead.model.field');
|
||||
$field = $this->createField('s', 'select', ['properties' => ['list' => ['choice_a' => 'Choice A']]]);
|
||||
$fieldModel->saveEntity($field);
|
||||
|
||||
$tablePrefix = static::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
$columns = $this->connection->createSchemaManager()->listTableColumns("{$tablePrefix}leads");
|
||||
$this->assertArrayHasKey('field_s', $columns);
|
||||
}
|
||||
|
||||
public function testNewDateField(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, 's/contacts/fields/new');
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
|
||||
$form['leadfield[label]']->setValue('Best Date Ever');
|
||||
$form['leadfield[type]']->setValue('date');
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$text = strip_tags($this->client->getResponse()->getContent());
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $text);
|
||||
Assert::assertStringNotContainsString('New Custom Field', $text);
|
||||
Assert::assertStringNotContainsString('This form should not contain extra fields.', $text);
|
||||
Assert::assertStringContainsString('Edit Custom Field - Best Date Ever', $text);
|
||||
}
|
||||
|
||||
public function testNewSelectField(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, 's/contacts/fields/new');
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$domDocument = $crawler->getNode(0)->ownerDocument;
|
||||
$inputLabel = $domDocument->createElement('input');
|
||||
$inputLabel->setAttribute('type', 'text');
|
||||
|
||||
$inputLabel->setAttribute('name', 'leadfield[properties][list][0][label]');
|
||||
$inputValue = $domDocument->createElement('input');
|
||||
$inputValue->setAttribute('type', 'text');
|
||||
$inputValue->setAttribute('name', 'leadfield[properties][list][0][value]');
|
||||
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form->set(new InputFormField($inputLabel));
|
||||
$form->set(new InputFormField($inputValue));
|
||||
|
||||
$form['leadfield[label]']->setValue('Test select field');
|
||||
$form['leadfield[type]']->setValue('select');
|
||||
$form['leadfield[properties][list][0][label]']->setValue('Label 1');
|
||||
$form['leadfield[properties][list][0][value]']->setValue('Value 1');
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$text = strip_tags($this->client->getResponse()->getContent());
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $text);
|
||||
Assert::assertStringNotContainsString('New Custom Field', $text);
|
||||
Assert::assertStringNotContainsString('This form should not contain extra fields.', $text);
|
||||
Assert::assertStringContainsString('Edit Custom Field - Test select field', $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $properties
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataForCreatingNewBooleanField')]
|
||||
public function testCreatingNewBooleanField(array $properties, string $expectedMessage): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, 's/contacts/fields/new');
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$domDocument = $crawler->getNode(0)->ownerDocument;
|
||||
$yesLabel = $domDocument->createElement('input');
|
||||
$yesLabel->setAttribute('type', 'text');
|
||||
$yesLabel->setAttribute('name', 'leadfield[properties][yes]');
|
||||
|
||||
$noLabel = $domDocument->createElement('input');
|
||||
$noLabel->setAttribute('type', 'text');
|
||||
$noLabel->setAttribute('name', 'leadfield[properties][no]');
|
||||
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form->set(new InputFormField($yesLabel));
|
||||
$form->set(new InputFormField($noLabel));
|
||||
|
||||
$form['leadfield[label]']->setValue('Request a meeting');
|
||||
$form['leadfield[type]']->setValue('boolean');
|
||||
$form['leadfield[object]']->setValue('lead');
|
||||
$form['leadfield[group]']->setValue('core');
|
||||
|
||||
$form['leadfield[properties][yes]']->setValue($properties['yes'] ?? '');
|
||||
$form['leadfield[properties][no]']->setValue($properties['no'] ?? '');
|
||||
|
||||
$this->client->submit($form);
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$text = strip_tags($this->client->getResponse()->getContent());
|
||||
Assert::assertStringNotContainsString($expectedMessage, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array<int, string|array<string, string>>>
|
||||
*/
|
||||
public static function dataForCreatingNewBooleanField(): iterable
|
||||
{
|
||||
yield 'No properties' => [
|
||||
[],
|
||||
'A \'positive\' label is required.',
|
||||
];
|
||||
|
||||
yield 'Only Yes' => [
|
||||
[
|
||||
'yes' => 'Yes',
|
||||
],
|
||||
'A \'negative\' label is required.',
|
||||
];
|
||||
|
||||
yield 'Only No' => [
|
||||
[
|
||||
'no' => 'No',
|
||||
],
|
||||
'A \'positive\' label is required.',
|
||||
];
|
||||
}
|
||||
|
||||
public function testCheckDefaultBooleanFieldSetting(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, 's/contacts/fields/new');
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
// Check if the radio button with value 0 is checked and value 1 is not
|
||||
Assert::assertNotNull(
|
||||
$crawler->filter('#leadfield_default_template_boolean_0')->attr('checked')
|
||||
);
|
||||
Assert::assertNull(
|
||||
$crawler->filter('#leadfield_default_template_boolean_1')->attr('checked')
|
||||
);
|
||||
}
|
||||
|
||||
public function testFieldsSearchByIds(): void
|
||||
{
|
||||
$urlEncodedSearch = urlencode('ids:2,3');
|
||||
$this->client->request(Request::METHOD_GET, "/s/contacts/fields?search={$urlEncodedSearch}");
|
||||
$this->assertResponseIsSuccessful();
|
||||
Assert::assertStringContainsString('First Name', $this->client->getResponse()->getContent());
|
||||
Assert::assertStringContainsString('Last Name', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
private function createField(string $suffix, string $type = 'text', array $parameters = [], ?int $charLength = null): LeadField
|
||||
{
|
||||
$field = new LeadField();
|
||||
$field->setName("Field $suffix");
|
||||
$field->setAlias("field_$suffix");
|
||||
$field->setDateAdded(new \DateTime());
|
||||
$field->setDateAdded(new \DateTime());
|
||||
$field->setDateModified(new \DateTime());
|
||||
$field->setType($type);
|
||||
if (!empty($charLength)) {
|
||||
$field->setCharLengthLimit($charLength);
|
||||
}
|
||||
$field->setObject('lead');
|
||||
isset($parameters['properties']) && $field->setProperties($parameters['properties']);
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array<mixed>>
|
||||
*/
|
||||
public static function provideFieldLength(): iterable
|
||||
{
|
||||
yield [ClassMetadataBuilder::MAX_VARCHAR_INDEXED_LENGTH, ClassMetadataBuilder::MAX_VARCHAR_INDEXED_LENGTH];
|
||||
yield [64, null];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Command\ImportCommand;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\CompanyLead;
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Entity\ImportRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLog;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\UserBundle\Entity\Permission;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\DomCrawler\Form;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
final class ImportControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
public function testImportWithoutFile(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$form = $crawler->selectButton('Upload')->form();
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
Assert::assertStringContainsString('Please select a CSV file to upload', $crawler->html(), $crawler->html());
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting the phone field as required to test the validation.
|
||||
* Phone is not part of the csv fixture so it won't be auto-mapped.
|
||||
*/
|
||||
public function testImportMappingRequiredFieldValidation(): void
|
||||
{
|
||||
$this->setPhoneFieldIsRequired(true);
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(__DIR__.'/../Fixtures/contacts.csv', 'contacs.csv', 'text/csv');
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString('Some required fields are missing. You must map the field "Phone."', $crawler->html());
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('validateDataProvider')]
|
||||
public function testImportMappingAndImport(string $skipIfExist, string $expectedName): void
|
||||
{
|
||||
$this->createLead('john@doe.email', 'Johny');
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(__DIR__.'/../Fixtures/contacts.csv', 'contacs.csv', 'text/csv');
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$mappingForm['lead_field_import[skip_if_exists]']->setValue($skipIfExist);
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString('Import process was successfully created. You will be notified when finished.', $crawler->html());
|
||||
|
||||
/** @var ImportRepository $importRepository */
|
||||
$importRepository = $this->em->getRepository(Import::class);
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts.csv']);
|
||||
|
||||
$fields = ['email' => 'email', 'firstname' => 'firstname', 'lastname' => 'lastname'];
|
||||
|
||||
Assert::assertNotNull($importEntity);
|
||||
Assert::assertSame(2, $importEntity->getLineCount());
|
||||
Assert::assertSame(Import::QUEUED, $importEntity->getStatus());
|
||||
Assert::assertSame('lead', $importEntity->getObject());
|
||||
Assert::assertSame($fields, $importEntity->getProperties()['fields']);
|
||||
Assert::assertSame(array_values($fields), $importEntity->getProperties()['headers']);
|
||||
|
||||
$this->testSymfonyCommand(ImportCommand::COMMAND_NAME);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts.csv']);
|
||||
|
||||
Assert::assertNotNull($importEntity);
|
||||
Assert::assertSame(2, $importEntity->getLineCount());
|
||||
Assert::assertSame(1, $importEntity->getInsertedCount());
|
||||
Assert::assertSame(1, $importEntity->getUpdatedCount());
|
||||
Assert::assertSame(Import::IMPORTED, $importEntity->getStatus());
|
||||
|
||||
/** @var LeadRepository $importRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
|
||||
/** @var Lead[] $contacts */
|
||||
$contacts = $leadRepository->findBy(['email' => ['john@doe.email', 'ferda@mravenec.email']], ['email' => 'desc']);
|
||||
Assert::assertSame($expectedName, $contacts[0]->getFirstname());
|
||||
Assert::assertCount(2, $contacts);
|
||||
}
|
||||
|
||||
public function testContactPermissionsAreFollowedDuringImport(): void
|
||||
{
|
||||
$filename = 'import-contact-permissions.csv';
|
||||
$permission = [
|
||||
'lead:leads' => ['viewown', 'viewother', 'editown'],
|
||||
'lead:imports' => ['view', 'create', 'edit'],
|
||||
];
|
||||
$role = $this->createRole(false, $permission);
|
||||
$this->createPermission('lead:imports', $role, 1024);
|
||||
$this->createPermission('lead:leads', $role, 14);
|
||||
$user = $this->createUser($role);
|
||||
|
||||
$this->createLead('existing-other@email.tld', 'Existing-other-before');
|
||||
$lead = $this->createLead('existing-owned@email.tld', 'Existing-owned-before');
|
||||
$lead->setOwner($user);
|
||||
$this->em->persist($lead);
|
||||
$this->createCompanyForLead($lead, 'Company One');
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Login newly created non-admin user
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(dirname(__FILE__).'/../Fixtures/'.$filename, 'contacts.csv', 'text/csv');
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$this->selectCompanyMapping($crawler, $mappingForm);
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString('Import process was successfully created.', $crawler->html());
|
||||
|
||||
$importRepository = $this->em->getRepository(Import::class);
|
||||
\assert($importRepository instanceof ImportRepository);
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => $filename]);
|
||||
|
||||
Assert::assertInstanceOf(Import::class, $importEntity);
|
||||
Assert::assertSame($user->getId(), $importEntity->getCreatedBy());
|
||||
Assert::assertSame($user->getId(), $importEntity->getModifiedBy());
|
||||
Assert::assertSame(Import::QUEUED, $importEntity->getStatus());
|
||||
|
||||
$this->testSymfonyCommand(ImportCommand::COMMAND_NAME);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => $filename]);
|
||||
|
||||
Assert::assertInstanceOf(Import::class, $importEntity);
|
||||
Assert::assertSame(3, $importEntity->getLineCount(), '3 rows should be processed as the CSV file contains 3 rows.');
|
||||
Assert::assertSame(0, $importEntity->getInsertedCount(), 'No row should be inserter as the user does not have permission to create contacts.');
|
||||
Assert::assertSame(1, $importEntity->getUpdatedCount(), 'There should be one update as the user has the permission to edit his own contacts.');
|
||||
Assert::assertSame(Import::IMPORTED, $importEntity->getStatus());
|
||||
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
\assert($leadRepository instanceof LeadRepository);
|
||||
|
||||
/** @var Lead[] $contacts */
|
||||
$contacts = $leadRepository->findBy([], ['id' => 'asc']);
|
||||
Assert::assertCount(2, $contacts, 'There should not be any contact inserted as the user does not have permission to create contacts.');
|
||||
Assert::assertSame('Existing-other-before', $contacts[0]->getFirstname(), 'This contact should not be updated as the user does not have permission to edit others.');
|
||||
Assert::assertSame('Existing-owned-after', $contacts[1]->getFirstname(), 'This contact should be updated as the user has permission to edit own.');
|
||||
|
||||
$eventLogRepository = $this->em->getRepository(LeadEventLog::class);
|
||||
\assert($eventLogRepository instanceof LeadEventLogRepository);
|
||||
|
||||
/** @var LeadEventLog[] $logs */
|
||||
$logs = $eventLogRepository->findBy(['bundle' => 'lead', 'object' => 'import'], ['id' => 'asc']);
|
||||
Assert::assertCount(3, $logs, 'There should be 3 logs connected with the import.');
|
||||
$this->assertInsufficientPermissionError($logs[0], $user);
|
||||
$this->assertInsufficientPermissionError($logs[1], $user);
|
||||
Assert::assertSame('updated', $logs[2]->getAction());
|
||||
Assert::assertArrayNotHasKey('error', $logs[2]->getProperties());
|
||||
}
|
||||
|
||||
public function testImportPublishAndUnpublish(): void
|
||||
{
|
||||
$permission = [
|
||||
'lead:imports' => ['view', 'create', 'edit'],
|
||||
];
|
||||
$role = $this->createRole(false, $permission);
|
||||
$this->createPermission('lead:imports', $role, 36);
|
||||
$user = $this->createUser($role);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Login newly created non-admin user
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUsername());
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(dirname(__FILE__).'/../Fixtures/contacts.csv', 'contacs.csv', 'itext/csv');
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString(
|
||||
'Import process was successfully created. But it will not be processed as you do not have permission to publish.',
|
||||
$crawler->html()
|
||||
);
|
||||
|
||||
/** @var ImportRepository $importRepository */
|
||||
$importRepository = $this->em->getRepository(Import::class);
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts.csv']);
|
||||
|
||||
Assert::assertNotNull($importEntity);
|
||||
Assert::assertFalse($importEntity->getIsPublished());
|
||||
Assert::assertSame(Import::STOPPED, $importEntity->getStatus());
|
||||
}
|
||||
|
||||
public function testImportFailedWithImportFailedException(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(
|
||||
dirname(__FILE__).'/../Fixtures/contacts.csv',
|
||||
'contacs.csv',
|
||||
'itext/csv'
|
||||
);
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString(
|
||||
'Import process was successfully created. You will be notified when finished.',
|
||||
$crawler->html(),
|
||||
$crawler->html()
|
||||
);
|
||||
|
||||
/** @var ImportRepository $importRepository */
|
||||
$importRepository = $this->em->getRepository(Import::class);
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts.csv']);
|
||||
|
||||
$importEntity->setStatus(4);
|
||||
$importRepository->saveEntity($importEntity);
|
||||
|
||||
$applicationTester = $this->testSymfonyCommand(ImportCommand::COMMAND_NAME, ['--id' => $importEntity->getId()]);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$expectedString = 'Reason: Import could not be triggered since it is not queued nor delayed';
|
||||
|
||||
Assert::assertStringContainsString($expectedString, $applicationTester->getDisplay());
|
||||
}
|
||||
|
||||
public function testImportWithSkipIfExistsFlagTrue(): void
|
||||
{
|
||||
$this->createBooleanField();
|
||||
$lead = $this->createLead('john@doe.email', 'Johny');
|
||||
$this->createCompanyForLead($lead, 'Company A');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(dirname(__FILE__).'/../Fixtures/contacts-with-custom-field.csv', 'contacs.csv', 'itext/csv');
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$mappingForm['lead_field_import[skip_if_exists]']->setValue('1');
|
||||
|
||||
// fetch company name mapping value
|
||||
$primaryCompanyOptions = $crawler->filter("#lead_field_import_company > optgroup[label='Primary company']")->filter('option');
|
||||
$optionValues = $primaryCompanyOptions->each(function ($node) {
|
||||
if ('Company Name' === $node->text()) {
|
||||
return $node->attr('value');
|
||||
}
|
||||
});
|
||||
$companyFieldMapping = array_filter($optionValues);
|
||||
$mappingForm['lead_field_import[company]']->setValue(end($companyFieldMapping));
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString('Import process was successfully created. You will be notified when finished.', $crawler->html(), $crawler->html());
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
/** @var ImportRepository $importRepository */
|
||||
$importRepository = $this->em->getRepository(Import::class);
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts-with-custom-field.csv']);
|
||||
|
||||
$fields = ['email' => 'email', 'firstname' => 'firstname', 'lastname' => 'lastname', 'company' => 'companyname', 'custom_boolean_field' => 'custom_boolean_field'];
|
||||
|
||||
Assert::assertNotNull($importEntity);
|
||||
Assert::assertSame(2, $importEntity->getLineCount());
|
||||
Assert::assertSame('lead', $importEntity->getObject());
|
||||
Assert::assertEquals($fields, $importEntity->getProperties()['fields']);
|
||||
Assert::assertEquals(array_keys($fields), $importEntity->getProperties()['headers']);
|
||||
|
||||
$this->testSymfonyCommand(ImportCommand::COMMAND_NAME);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts-with-custom-field.csv']);
|
||||
|
||||
Assert::assertNotNull($importEntity);
|
||||
Assert::assertSame(2, $importEntity->getLineCount());
|
||||
Assert::assertSame(1, $importEntity->getInsertedCount());
|
||||
Assert::assertSame(1, $importEntity->getUpdatedCount());
|
||||
Assert::assertSame(Import::IMPORTED, $importEntity->getStatus());
|
||||
|
||||
/** @var LeadRepository $importRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
|
||||
/** @var Lead[] $contacts */
|
||||
$contacts = $leadRepository->findBy(['email' => ['john@doe.email', 'ferda@mravenec.email']], ['email' => 'desc']);
|
||||
Assert::assertSame('Johny', $contacts[0]->getFirstname());
|
||||
Assert::assertSame('Company A', $contacts[0]->getCompany());
|
||||
Assert::assertSame('Company B', $contacts[1]->getCompany());
|
||||
Assert::assertCount(2, $contacts);
|
||||
}
|
||||
|
||||
private function setPhoneFieldIsRequired(bool $required): void
|
||||
{
|
||||
/** @var LeadFieldRepository $fieldRepository */
|
||||
$fieldRepository = $this->em->getRepository(LeadField::class);
|
||||
|
||||
/** @var LeadField $phoneField */
|
||||
$phoneField = $fieldRepository->findOneBy(['alias' => 'phone']);
|
||||
|
||||
$phoneField->setIsRequired($required);
|
||||
$fieldRepository->saveEntity($phoneField);
|
||||
}
|
||||
|
||||
private function createLead(?string $email = null, string $firstName = ''): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
if (!empty($email)) {
|
||||
$lead->setEmail($email);
|
||||
}
|
||||
$lead->setFirstname($firstName);
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $permission
|
||||
*/
|
||||
private function createRole(bool $isAdmin = false, array $permission = []): Role
|
||||
{
|
||||
$role = new Role();
|
||||
$role->setName('Role');
|
||||
$role->setIsAdmin($isAdmin);
|
||||
$role->setRawPermissions($permission);
|
||||
$this->em->persist($role);
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
private function createPermission(string $rawPermission, Role $role, int $bitwise): void
|
||||
{
|
||||
$parts = explode(':', $rawPermission);
|
||||
$permission = new Permission();
|
||||
$permission->setBundle($parts[0]);
|
||||
$permission->setName($parts[1]);
|
||||
$permission->setRole($role);
|
||||
$permission->setBitwise($bitwise);
|
||||
$this->em->persist($permission);
|
||||
}
|
||||
|
||||
private function createUser(Role $role): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setFirstName('John');
|
||||
$user->setLastName('Doe');
|
||||
$user->setUsername('john.doe');
|
||||
$user->setEmail('john.doe@email.com');
|
||||
$hasher = self::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
|
||||
\assert($hasher instanceof PasswordHasherInterface);
|
||||
$user->setPassword($hasher->hash('Maut1cR0cks!'));
|
||||
$user->setRole($role);
|
||||
$this->em->persist($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function createBooleanField(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)
|
||||
->findOneBy(['username' => $this->clientServer['PHP_AUTH_USER'] ?? 'admin']);
|
||||
$this->loginUser($user);
|
||||
$field = new LeadField();
|
||||
$field->setType('boolean');
|
||||
$field->setObject('lead');
|
||||
$field->setGroup('core');
|
||||
$field->setLabel('Test boolean field');
|
||||
$field->setAlias('custom_boolean_field');
|
||||
$field->setDefaultValue('0');
|
||||
$field->setProperties(['no' => 'No', 'yes' => 'Yes']);
|
||||
|
||||
/** @var FieldModel $fieldModel */
|
||||
$fieldModel = $this->getContainer()->get('mautic.lead.model.field');
|
||||
$fieldModel->saveEntity($field);
|
||||
}
|
||||
|
||||
private function createCompanyForLead(Lead $lead, string $companyName): void
|
||||
{
|
||||
$company = new Company();
|
||||
$company->setName($companyName);
|
||||
$this->em->persist($company);
|
||||
|
||||
// add company to lead
|
||||
$lead->setCompany($companyName);
|
||||
$this->em->persist($lead);
|
||||
|
||||
// set primary company for lead
|
||||
$companyLead = new CompanyLead();
|
||||
$companyLead->setCompany($company);
|
||||
$companyLead->setLead($lead);
|
||||
$companyLead->setDateAdded(new \DateTime());
|
||||
$companyLead->setPrimary(true);
|
||||
$this->em->persist($companyLead);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function validateDataProvider(): iterable
|
||||
{
|
||||
yield ['0', 'John'];
|
||||
yield ['1', 'Johny'];
|
||||
}
|
||||
|
||||
private function assertInsufficientPermissionError(LeadEventLog $log, User $user): void
|
||||
{
|
||||
Assert::assertSame('failed', $log->getAction(), 'The insertion should fail as the user does not have permission to create contacts.');
|
||||
Assert::assertSame(sprintf('User \'%s\' has insufficient permissions', $user->getUserIdentifier()), $log->getProperties()['error'], 'There should be an insufficient permission error.');
|
||||
}
|
||||
|
||||
private function selectCompanyMapping(Crawler $crawler, Form $mappingForm): void
|
||||
{
|
||||
$options = $crawler->filter("#lead_field_import_company > optgroup[label='Primary company']")->filter('option');
|
||||
$values = array_filter($options->each(function ($node) {
|
||||
if ('Company Name' === $node->text()) {
|
||||
return $node->attr('value');
|
||||
}
|
||||
}));
|
||||
$mappingForm['lead_field_import[company]']->setValue(end($values));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
|
||||
class LeadCompanyControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['contact_allow_multiple_companies'] = 0;
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testSimpleCompanyFeature(): void
|
||||
{
|
||||
$crawler = $this->client->request('GET', 's/contacts/new/');
|
||||
$multiple = $crawler->filterXPath('//*[@id="lead_companies"]')->attr('multiple');
|
||||
self::assertNull($multiple);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class LeadControllerListingPageTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['contact_columns'] = ['name', 'location', 'email'];
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $location
|
||||
*
|
||||
* @throws ORMException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataForContactListing')]
|
||||
public function testContactListingForLocation(array $location, string $expected): void
|
||||
{
|
||||
$this->createContact($location);
|
||||
|
||||
$crawler = $this->client->request('GET', 's/contacts');
|
||||
$rowContent = $crawler->filterXPath("//table[@id='leadTable']//tbody//tr");
|
||||
|
||||
Assert::assertStringEndsWith($expected, $rowContent->text());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array<int, string|string[]>>
|
||||
*/
|
||||
public static function dataForContactListing(): iterable
|
||||
{
|
||||
yield 'With no location' => [
|
||||
// Location Details
|
||||
[
|
||||
'setCity' => '',
|
||||
'setState' => '',
|
||||
'setCountry' => '',
|
||||
],
|
||||
// Expected suffice
|
||||
'John Doe john@doe.example.com',
|
||||
];
|
||||
|
||||
yield 'With whole location details' => [
|
||||
// Location Details
|
||||
[
|
||||
'setCity' => 'Pune',
|
||||
'setState' => 'MH',
|
||||
'setCountry' => 'India',
|
||||
],
|
||||
// Expected suffice
|
||||
'John Doe Pune, MH john@doe.example.com',
|
||||
];
|
||||
|
||||
yield 'With only City for location' => [
|
||||
// Location Details
|
||||
[
|
||||
'setCity' => 'Pune',
|
||||
'setState' => '',
|
||||
'setCountry' => '',
|
||||
],
|
||||
// Expected suffice
|
||||
'John Doe Pune john@doe.example.com',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $location
|
||||
*
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createContact(array $location = []): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setFirstname('John');
|
||||
$contact->setLastname('Doe');
|
||||
$contact->setEmail('john@doe.example.com');
|
||||
|
||||
foreach ($location as $name => $value) {
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
$contact->$name($value);
|
||||
}
|
||||
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class LeadDetailFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testCustomFieldOrderIsRespected(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('John');
|
||||
$lead->setLastname('Doe');
|
||||
$lead->setEmail('john@his-site.com');
|
||||
$this->em->persist($lead);
|
||||
|
||||
$fieldRepository = $this->em->getRepository(LeadField::class);
|
||||
|
||||
/** @var LeadField[] $fields */
|
||||
$fields = $fieldRepository->findBy(['object' => 'lead', 'group' => 'core'], [
|
||||
'label' => 'desc',
|
||||
'id' => 'desc',
|
||||
]);
|
||||
$order = 0;
|
||||
|
||||
// re-order fields by the label
|
||||
foreach ($fields as $field) {
|
||||
$field->setOrder(++$order);
|
||||
$this->em->persist($field);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// initialize lead fields to adjust the expected core labels
|
||||
$lead->setFields([
|
||||
'core' => [
|
||||
'First Name' => [
|
||||
'value' => 'John',
|
||||
],
|
||||
'Last Name' => [
|
||||
'value' => 'Doe',
|
||||
],
|
||||
'Email' => [
|
||||
'value' => 'john@his-site.com',
|
||||
],
|
||||
'Primary company' => [
|
||||
'value' => null,
|
||||
],
|
||||
'Points' => [
|
||||
'value' => 0,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$leadFields = array_filter($lead->getFields(true), fn ($value) => isset($value['value']));
|
||||
$leadFields = array_keys($leadFields);
|
||||
|
||||
// get expected core labels
|
||||
$expectedLabels = $this->connection->createQueryBuilder()
|
||||
->select('label')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_fields')
|
||||
->where('object = "lead"')
|
||||
->andWhere('field_group = "core"')
|
||||
->andWhere('label IN (:leadFields)')
|
||||
->orderBy('field_order')
|
||||
->setParameter(
|
||||
'leadFields',
|
||||
$leadFields,
|
||||
ArrayParameterType::STRING
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchFirstColumn();
|
||||
|
||||
$expectedLabels = array_merge(['Created on', 'ID'], $expectedLabels);
|
||||
|
||||
$crawler = $this->client->request('GET', sprintf('/s/contacts/view/%d', $lead->getId()));
|
||||
|
||||
// get actual core labels
|
||||
$actualLabels = $crawler->filter('#lead-details table')
|
||||
->first()
|
||||
->filter('td:first-child')
|
||||
->extract(['_text']);
|
||||
$actualLabels = array_map('trim', $actualLabels);
|
||||
|
||||
Assert::assertSame($expectedLabels, $actualLabels);
|
||||
}
|
||||
|
||||
public function testLeadViewPreventsXSS(): void
|
||||
{
|
||||
$firstName = 'aaa" onmouseover=alert(1) a="';
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname($firstName);
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', sprintf('/s/contacts/view/%d', $lead->getId()));
|
||||
|
||||
$anchorTag = $crawler->filter('#toolbar ul.dropdown-menu-right li')->first()->filter('a');
|
||||
$mouseOver = $anchorTag->attr('onmouseover');
|
||||
$dataHeader = $anchorTag->attr('data-header');
|
||||
|
||||
Assert::assertNull($mouseOver);
|
||||
Assert::assertSame(sprintf('Campaigns for %s', $firstName), $dataHeader);
|
||||
$response = $this->client->getResponse();
|
||||
// Make sure the data-target-url is not an absolute URL
|
||||
Assert::assertStringContainsString(sprintf('data-target-url="/s/contacts/view/%s/stats"', $lead->getId()), $response->getContent());
|
||||
}
|
||||
|
||||
public function testLeadDetailPageForSocialTabInDetailsCollapsibleForNoData(): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('john@doe.corp');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', 's/contacts/view/'.$contact->getId());
|
||||
$data = $crawler->filterXPath('//div[@id="social"]//td');
|
||||
$this->assertCount(1, $data);
|
||||
|
||||
$translator = static::getContainer()->get('translator');
|
||||
$this->assertStringContainsString($translator->trans('mautic.lead.field.group.no_data'), $data->text());
|
||||
}
|
||||
|
||||
public function testLeadDetailPageForSocialTabInDetailsCollapsible(): void
|
||||
{
|
||||
$crawler = $this->client->request('GET', 's/contacts/new/');
|
||||
|
||||
$fbLink = 'https://fb.com/john_doe_test';
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'lead[firstname]' => 'John',
|
||||
'lead[lastname]' => 'Doe',
|
||||
'lead[email]' => 'john@doe.corp',
|
||||
'lead[facebook]' => $fbLink,
|
||||
]
|
||||
);
|
||||
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$fbProfile = $crawler->filterXPath('//div[@id="social"]//td[2]');
|
||||
|
||||
$this->assertCount(1, $fbProfile);
|
||||
$this->assertStringContainsString($fbLink, $fbProfile->text());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class LeadListProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
|
||||
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
|
||||
{
|
||||
$projectOne = $this->createProject('Project One');
|
||||
$projectTwo = $this->createProject('Project Two');
|
||||
$projectThree = $this->createProject('Project Three');
|
||||
|
||||
$segmentAlpha = $this->createSegment('Segment Alpha');
|
||||
$segmentBeta = $this->createSegment('Segment Beta');
|
||||
$this->createSegment('Segment Gamma');
|
||||
$this->createSegment('Segment Delta');
|
||||
|
||||
$segmentAlpha->addProject($projectOne);
|
||||
$segmentAlpha->addProject($projectTwo);
|
||||
$segmentBeta->addProject($projectTwo);
|
||||
$segmentBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/segments', '/s/segments']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
|
||||
*/
|
||||
public static function searchDataProvider(): \Generator
|
||||
{
|
||||
yield 'search by one project' => [
|
||||
'searchTerm' => 'project:"Project Two"',
|
||||
'expectedEntities' => ['Segment Alpha', 'Segment Beta'],
|
||||
'unexpectedEntities' => ['Segment Gamma', 'Segment Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND segment name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Segment Beta'],
|
||||
'unexpectedEntities' => ['Segment Alpha', 'Segment Gamma', 'Segment Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR segment name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Segment Alpha', 'Segment Beta', 'Segment Gamma'],
|
||||
'unexpectedEntities' => ['Segment Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Segment Gamma', 'Segment Delta'],
|
||||
'unexpectedEntities' => ['Segment Alpha', 'Segment Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Segment Beta'],
|
||||
'unexpectedEntities' => ['Segment Alpha', 'Segment Gamma', 'Segment Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Segment Gamma', 'Segment Delta'],
|
||||
'unexpectedEntities' => ['Segment Alpha', 'Segment Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Segment Alpha', 'Segment Beta'],
|
||||
'unexpectedEntities' => ['Segment Gamma', 'Segment Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Segment Alpha', 'Segment Gamma', 'Segment Delta'],
|
||||
'unexpectedEntities' => ['Segment Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createSegment(string $name): LeadList
|
||||
{
|
||||
$leadList = new LeadList();
|
||||
$leadList->setName($name);
|
||||
$leadList->setPublicName($name);
|
||||
$leadList->setAlias(str_replace(' ', '-', mb_strtolower($name)));
|
||||
$this->em->persist($leadList);
|
||||
|
||||
return $leadList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\Bundle\DoctrineBundle\DataCollector\DoctrineDataCollector;
|
||||
use Doctrine\Bundle\DoctrineBundle\Twig\DoctrineExtension;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class LeadListSearchFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
protected array $clientOptions = ['debug' => true];
|
||||
|
||||
/** @noinspection SqlResolve */
|
||||
public function testSegmentSearch(): void
|
||||
{
|
||||
// create some leads
|
||||
$leadOne = $this->createLead('one');
|
||||
$leadTwo = $this->createLead('two');
|
||||
$leadThree = $this->createLead('three');
|
||||
$leadFour = $this->createLead('four');
|
||||
$leadFive = $this->createLead('five');
|
||||
$leadSix = $this->createLead('six');
|
||||
|
||||
// add some leads in lists
|
||||
$listOne = $this->createLeadList('first-list', $leadOne, $leadTwo, $leadThree);
|
||||
$listTwo = $this->createLeadList('second-list', $leadOne, $leadFour, $leadFive, $leadSix);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->client->enableProfiler();
|
||||
$prefix = static::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
$previousQueries = [];
|
||||
|
||||
// non-existent segment search
|
||||
$this->assertSearchResult('segment%3AnonExistent', [], [$leadOne, $leadTwo, $leadThree, $leadFour, $leadFive, $leadSix]);
|
||||
$this->assertQueries([
|
||||
"SELECT list.id FROM {$prefix}lead_lists list WHERE list.alias = 'nonexistent'",
|
||||
"SELECT COUNT(l.id) as count FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN (0)))) AND (l.date_identified IS NOT NULL)",
|
||||
], $previousQueries);
|
||||
|
||||
// first-list segment search
|
||||
$this->assertSearchResult('segment%3A'.$listOne->getAlias(), [$leadOne, $leadTwo, $leadThree], [$leadFour, $leadFive, $leadSix]);
|
||||
$this->assertQueries([
|
||||
"SELECT list.id FROM {$prefix}lead_lists list WHERE list.alias = '{$listOne->getAlias()}'",
|
||||
"SELECT COUNT(l.id) as count FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listOne->getId()}')))) AND (l.date_identified IS NOT NULL)",
|
||||
"SELECT l.* FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listOne->getId()}')))) AND (l.date_identified IS NOT NULL) ORDER BY l.last_active DESC, l.id DESC LIMIT 30",
|
||||
], $previousQueries);
|
||||
$this->assertSearchResult('!segment%3A'.$listOne->getAlias(), [$leadFour, $leadFive, $leadSix], [$leadOne, $leadTwo, $leadThree]);
|
||||
$this->assertQueries([
|
||||
"SELECT list.id FROM {$prefix}lead_lists list WHERE list.alias = '{$listOne->getAlias()}'",
|
||||
"SELECT COUNT(l.id) as count FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (NOT EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listOne->getId()}')))) AND (l.date_identified IS NOT NULL)",
|
||||
"SELECT l.* FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (NOT EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listOne->getId()}')))) AND (l.date_identified IS NOT NULL) ORDER BY l.last_active DESC, l.id DESC LIMIT 30",
|
||||
], $previousQueries);
|
||||
|
||||
// second-list segment search
|
||||
$this->assertSearchResult('segment%3A'.$listTwo->getAlias(), [$leadOne, $leadFour, $leadFive, $leadSix], [$leadTwo, $leadThree]);
|
||||
$this->assertQueries([
|
||||
"SELECT list.id FROM {$prefix}lead_lists list WHERE list.alias = '{$listTwo->getAlias()}'",
|
||||
"SELECT COUNT(l.id) as count FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listTwo->getId()}')))) AND (l.date_identified IS NOT NULL)",
|
||||
"SELECT l.* FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listTwo->getId()}')))) AND (l.date_identified IS NOT NULL) ORDER BY l.last_active DESC, l.id DESC LIMIT 30",
|
||||
], $previousQueries);
|
||||
$this->assertSearchResult('!segment%3A'.$listTwo->getAlias(), [$leadTwo, $leadThree], [$leadOne, $leadFour, $leadFive, $leadSix]);
|
||||
$this->assertQueries([
|
||||
"SELECT list.id FROM {$prefix}lead_lists list WHERE list.alias = '{$listTwo->getAlias()}'",
|
||||
"SELECT COUNT(l.id) as count FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (NOT EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listTwo->getId()}')))) AND (l.date_identified IS NOT NULL)",
|
||||
"SELECT l.* FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (NOT EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listTwo->getId()}')))) AND (l.date_identified IS NOT NULL) ORDER BY l.last_active DESC, l.id DESC LIMIT 30",
|
||||
], $previousQueries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead[] $expectedLeads
|
||||
* @param Lead[] $notExpectedLeads
|
||||
*/
|
||||
private function assertSearchResult(string $search, array $expectedLeads, array $notExpectedLeads): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search='.$search);
|
||||
self::assertResponseIsSuccessful();
|
||||
$responseText = $crawler->text();
|
||||
|
||||
foreach ($expectedLeads as $expectedLead) {
|
||||
Assert::assertStringContainsString($expectedLead->getEmail(), $responseText, sprintf('Lead with the email "%s" should be in the result.', $expectedLead->getEmail()));
|
||||
}
|
||||
|
||||
foreach ($notExpectedLeads as $notExpectedLead) {
|
||||
Assert::assertStringNotContainsString($notExpectedLead->getEmail(), $responseText, sprintf('Lead with the email "%s" should not be in the result.', $notExpectedLead->getEmail()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $expectedQueries
|
||||
* @param string[] $previousQueries
|
||||
*/
|
||||
private function assertQueries(array $expectedQueries, array &$previousQueries): void
|
||||
{
|
||||
/** @var DoctrineDataCollector $dbCollector */
|
||||
$dbCollector = $this->client->getProfile()->getCollector('db');
|
||||
$allQueries = $dbCollector->getQueries()['default'];
|
||||
$queries = array_diff_key($allQueries, $previousQueries);
|
||||
$previousQueries = $allQueries;
|
||||
$doctrineExtension = new DoctrineExtension();
|
||||
|
||||
$queries = array_map(function (array $query) use ($doctrineExtension) {
|
||||
return $doctrineExtension->replaceQueryParameters($query['sql'], $query['params']);
|
||||
}, $queries);
|
||||
|
||||
foreach ($expectedQueries as $expectedQuery) {
|
||||
$matchedQueries = array_filter($queries, function (string $query) use ($expectedQuery) {
|
||||
return $expectedQuery === $query;
|
||||
});
|
||||
Assert::assertCount(1, $matchedQueries, sprintf('The query "%s" was expected to be executed once.', $expectedQuery));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createLead(string $lastName): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setLastname($lastName);
|
||||
$lead->setEmail(sprintf('%s@mail.tld', $lastName));
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead ...$leads
|
||||
*
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createLeadList(string $name, ...$leads): LeadList
|
||||
{
|
||||
$leadList = new LeadList();
|
||||
$leadList->setName($name);
|
||||
$leadList->setPublicName($name);
|
||||
$leadList->setAlias(mb_strtolower($name));
|
||||
$this->em->persist($leadList);
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$this->addLeadToList($lead, $leadList);
|
||||
}
|
||||
|
||||
return $leadList;
|
||||
}
|
||||
|
||||
private function addLeadToList(Lead $leadOne, LeadList $sourceList): void
|
||||
{
|
||||
$listLead = new ListLead();
|
||||
$listLead->setLead($leadOne);
|
||||
$listLead->setList($sourceList);
|
||||
$listLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($listLead);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,817 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
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 Mautic\LeadBundle\Helper\SegmentCountCacheHelper;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use Mautic\ProjectBundle\Model\ProjectModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class ListControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private ListModel $listModel;
|
||||
|
||||
private LeadListRepository $listRepo;
|
||||
|
||||
protected SegmentCountCacheHelper $segmentCountCacheHelper;
|
||||
|
||||
private LeadRepository $leadRepo;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['update_segment_contact_count_in_background'] = 'testSegmentCountInBackground' === $this->name();
|
||||
parent::setUp();
|
||||
$this->listModel = static::getContainer()->get('mautic.lead.model.list');
|
||||
\assert($this->listModel instanceof ListModel);
|
||||
$this->listRepo = $this->listModel->getRepository();
|
||||
\assert($this->listRepo instanceof LeadListRepository);
|
||||
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$this->segmentCountCacheHelper = static::getContainer()->get('mautic.helper.segment.count.cache');
|
||||
$this->leadRepo = $leadModel->getRepository();
|
||||
}
|
||||
|
||||
public function testUnpublishUsedSegment(): void
|
||||
{
|
||||
$filter = [[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
]];
|
||||
$list1 = $this->saveSegment('s1', 's1', $filter);
|
||||
$filter = [[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => [
|
||||
'filter' => [$list1->getId()],
|
||||
],
|
||||
'display' => '',
|
||||
]];
|
||||
$list2 = $this->saveSegment('s2', 's2', $filter);
|
||||
$this->em->clear();
|
||||
$expectedErrorMessage = sprintf('This segment is used in %s, please go back and check segments before unpublishing', $list2->getName());
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/ajax', ['action' => 'togglePublishStatus', 'model' => 'lead.list', 'id' => $list1->getId()]);
|
||||
$this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString($expectedErrorMessage, $this->client->getResponse()->getContent());
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$list1->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[isPublished]']->setValue('0');
|
||||
$crawler = $this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertStringContainsString($expectedErrorMessage, $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testUnpublishUnUsedSegment(): void
|
||||
{
|
||||
$filter = [[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
]];
|
||||
$list1 = $this->saveSegment('s1', 's1', $filter);
|
||||
$list2 = $this->saveSegment('s2', 's2', $filter);
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/ajax', ['action' => 'togglePublishStatus', 'model' => 'lead.list', 'id' => $list1->getId()]);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$list2->getId());
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[isPublished]']->setValue('0');
|
||||
$crawler = $this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$rows = $this->listRepo->findAll();
|
||||
$this->assertCount(2, $rows);
|
||||
$this->assertFalse($rows[0]->isPublished());
|
||||
$this->assertFalse($rows[1]->isPublished());
|
||||
}
|
||||
|
||||
public function testBCSegmentWithPageHitInLeadObject(): void
|
||||
{
|
||||
$segment = $this->saveSegment(
|
||||
'Legacy Url Hit segment',
|
||||
's1',
|
||||
[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'hit_url',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'filter' => 'unicorn',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segment->getId());
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
Assert::assertGreaterThan(0, $crawler->filter('#leadlist_filters_0_operator option')->count());
|
||||
}
|
||||
|
||||
public function testSegmentWithProject(): void
|
||||
{
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'filter' => null,
|
||||
'display' => null,
|
||||
'operator' => '!empty',
|
||||
],
|
||||
];
|
||||
|
||||
$segment = $this->saveSegment('Segment with Project', 'st1', $filters);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
|
||||
$projectModel = self::getContainer()->get(ProjectModel::class);
|
||||
\assert($projectModel instanceof ProjectModel);
|
||||
$projectModel->saveEntity($project);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segment->getId());
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedSegment = $this->listRepo->find($segment->getId());
|
||||
Assert::assertSame($project->getId(), $savedSegment->getProjects()->first()->getId());
|
||||
}
|
||||
|
||||
private function saveSegment(string $name, string $alias, array $filters = [], ?LeadList $segment = null): LeadList
|
||||
{
|
||||
$segment ??= new LeadList();
|
||||
$segment->setName($name)->setAlias($alias)->setFilters($filters);
|
||||
$this->listModel->saveEntity($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testSegmentCount(): void
|
||||
{
|
||||
// Save segment.
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'filter' => null,
|
||||
'display' => null,
|
||||
'operator' => '!empty',
|
||||
],
|
||||
];
|
||||
$segment = $this->saveSegment('Lead List 1', 'lead-list-1', $filters);
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
// Save manual segment without filters.
|
||||
$manualSegment = $this->saveSegment('Lead List 2', 'lead-list-2');
|
||||
$manualSegmentId = $manualSegment->getId();
|
||||
|
||||
// Verify last built date is not set.
|
||||
self::assertNull($segment->getLastBuiltDate());
|
||||
|
||||
// Check segment count UI for no contacts for manual segment.
|
||||
// And check the filtered segment is Building
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('Building', $html);
|
||||
self::assertSame('label label-info col-count', $spClass);
|
||||
$html = $this->getSegmentCountHtml($crawler, $manualSegmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $manualSegmentId);
|
||||
self::assertSame('No Contacts', $html);
|
||||
self::assertSame('label label-gray col-count', $spClass);
|
||||
|
||||
// Add 4 contacts.
|
||||
$contacts = $this->saveContacts();
|
||||
$contact1Id = $contacts[0]->getId();
|
||||
|
||||
// Rebuild segment - set current count to the cache.
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segmentId, '--env' => 'test']);
|
||||
|
||||
// Verify last built date is set.
|
||||
$this->em->detach($segment);
|
||||
$segment = $this->listRepo->find($segmentId);
|
||||
self::assertNotNull($segment->getLastBuiltDate());
|
||||
|
||||
// Set last built date in the future to allow testing without waiting.
|
||||
// (Same second built date as the modified date is shown as "Building" still in the UI).
|
||||
$segment->setLastBuiltDate(new \DateTime('+5 seconds'));
|
||||
$this->listModel->saveEntity($segment);
|
||||
|
||||
// Check segment count UI for 4 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('View 4 Contacts', $html);
|
||||
self::assertSame('label label-gray col-count', $spClass);
|
||||
|
||||
// Remove 1 contact from segment.
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contact/'.$contact1Id.'/remove');
|
||||
self::assertSame('{"success":1}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Check segment count UI for 3 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('View 3 Contacts', $html);
|
||||
self::assertSame('label label-gray col-count', $spClass);
|
||||
|
||||
// Add 1 contact back to segment.
|
||||
$parameters = ['ids' => [$contact1Id]];
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contacts/add', $parameters);
|
||||
self::assertSame('{"success":1,"details":{"'.$contact1Id.'":{"success":true}}}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Check segment count UI for 4 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('View 4 Contacts', $html);
|
||||
self::assertSame('label label-gray col-count', $spClass);
|
||||
|
||||
// Check segment count AJAX for 4 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 4 Contacts', $response['content']['html']);
|
||||
self::assertSame('label label-gray col-count', $response['content']['className']);
|
||||
self::assertSame(4, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
|
||||
// Remove 1 contact from segment.
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contact/'.$contact1Id.'/remove');
|
||||
self::assertSame('{"success":1}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Check segment count AJAX for 3 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 3 Contacts', $response['content']['html']);
|
||||
self::assertSame('label label-gray col-count', $response['content']['className']);
|
||||
self::assertSame(3, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
|
||||
// Add 1 contact back to segment.
|
||||
$parameters = ['ids' => [$contact1Id]];
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contacts/add', $parameters);
|
||||
self::assertSame('{"success":1,"details":{"'.$contact1Id.'":{"success":true}}}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Check segment count AJAX for 4 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 4 Contacts', $response['content']['html']);
|
||||
self::assertSame('label label-gray col-count', $response['content']['className']);
|
||||
self::assertSame(4, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
|
||||
// Save filtered segment again to trigger rebuild label, setting last built date in the past.
|
||||
$this->em->detach($segment);
|
||||
$segment = $this->listRepo->find($segmentId);
|
||||
$segment->setLastBuiltDate(new \DateTime('-1 year'));
|
||||
// Date modified only updates on specific changes, so change name.
|
||||
$segment->setName('Lead List 1 Updated');
|
||||
$this->listModel->saveEntity($segment);
|
||||
|
||||
// Check segment count UI for bulding with 4 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('Building (4 Contacts)', $html);
|
||||
self::assertSame('label label-info col-count', $spClass);
|
||||
|
||||
// Check segment count AJAX for building 4 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('Building (4 Contacts)', $response['content']['html']);
|
||||
self::assertSame('label label-info col-count', $response['content']['className']);
|
||||
self::assertSame(4, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testSegmentCountInBackground(): void
|
||||
{
|
||||
// Save segment.
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'filter' => null,
|
||||
'display' => null,
|
||||
'operator' => '!empty',
|
||||
],
|
||||
];
|
||||
|
||||
$segment = $this->saveSegment('Lead List 1', 'lead-list-1', $filters);
|
||||
$segmentId = $segment->getId();
|
||||
$this->segmentCountCacheHelper->deleteSegmentContactCount($segmentId);
|
||||
|
||||
// Check segment count UI for no contacts.
|
||||
usleep(1000000);
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segmentId, '--env' => 'test']);
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
self::assertSame('No Contacts', $html);
|
||||
|
||||
// Add 4 contacts.
|
||||
$contacts = $this->saveContacts();
|
||||
$contact1Id = $contacts[0]->getId();
|
||||
|
||||
// Rebuild segment - set current count to the cache.
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segmentId, '--env' => 'test']);
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment count UI for 4 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
self::assertSame('View 4 Contacts', $html);
|
||||
|
||||
// Remove 1 contact from segment.
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contact/'.$contact1Id.'/remove');
|
||||
self::assertSame('{"success":1}', $this->client->getResponse()->getContent());
|
||||
self::assertSame(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment count UI for 3 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
self::assertSame('View 3 Contacts', $html);
|
||||
|
||||
// Add 1 contact back to segment.
|
||||
$parameters = ['ids' => [$contact1Id]];
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contacts/add', $parameters);
|
||||
self::assertSame('{"success":1,"details":{"'.$contact1Id.'":{"success":true}}}', $this->client->getResponse()->getContent());
|
||||
self::assertSame(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment count UI for 4 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
self::assertSame('View 4 Contacts', $html);
|
||||
|
||||
// Check segment count AJAX for 4 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 4 Contacts', $response['content']['html']);
|
||||
self::assertSame(4, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
|
||||
// Remove 1 contact from segment.
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contact/'.$contact1Id.'/remove');
|
||||
self::assertSame('{"success":1}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment count AJAX for 3 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 3 Contacts', $response['content']['html']);
|
||||
self::assertSame(3, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
|
||||
// Add 1 contact back to segment.
|
||||
$parameters = ['ids' => [$contact1Id]];
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contacts/add', $parameters);
|
||||
self::assertSame('{"success":1,"details":{"'.$contact1Id.'":{"success":true}}}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment count AJAX for 4 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 4 Contacts', $response['content']['html']);
|
||||
self::assertSame(4, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
}
|
||||
|
||||
public function testSegmentClone(): void
|
||||
{
|
||||
$segment = $this->saveSegment('Test Segment', 'testsegment');
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
// Number of segments before clone
|
||||
$segmentsCountBefore = $this->em->getRepository(LeadList::class)->count([]);
|
||||
// Go to clone segment action
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/clone/'.(string) $segmentId);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
||||
// First submit
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$crawler = $this->client->submit($form);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), 'Correct Apply');
|
||||
// Second submit
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$this->client->submit($form);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), 'Correct Apply');
|
||||
// Number of segments after clone
|
||||
$segmentsCountAfter = $this->em->getRepository(LeadList::class)->count([]);
|
||||
// Check that just one segment was created
|
||||
$this->assertSame($segmentsCountBefore + 1, $segmentsCountAfter);
|
||||
}
|
||||
|
||||
public function testSegmentAliasCreation(): void
|
||||
{
|
||||
$segment = $this->saveSegment('Test Segment Alias', 'test-segment-alias');
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
// Clone segment
|
||||
$aliasFirst = $this->getAliasWhenCloneSegment($segmentId);
|
||||
// Clone segment again
|
||||
$aliasSecond = $this->getAliasWhenCloneSegment($segmentId);
|
||||
// Check that aliases are not the same
|
||||
$this->assertNotSame($aliasFirst, $aliasSecond);
|
||||
}
|
||||
|
||||
private function getAliasWhenCloneSegment(int $segmentId): string
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/clone/'.(string) $segmentId);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
||||
// Save cloned segment
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$crawler = $this->client->submit($form);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), 'Correct Apply');
|
||||
|
||||
return $crawler->filter('#leadlist_alias')->attr('value');
|
||||
}
|
||||
|
||||
public function testSegmentNotFoundOnAjax(): void
|
||||
{
|
||||
// Emulate invalid request parameter.
|
||||
$parameter = ['id' => 'ABC'];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
|
||||
self::assertSame('No Contacts', $response['content']['html']);
|
||||
self::assertSame(0, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_NOT_FOUND, $response['statusCode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead[]
|
||||
*/
|
||||
private function saveContacts(int $count = 4): array
|
||||
{
|
||||
$contacts = [];
|
||||
|
||||
for ($i = 1; $i <= $count; ++$i) {
|
||||
$contact = new Lead();
|
||||
$contact->setFirstname('Contact '.$i)->setEmail('contact'.$i.'@example.com');
|
||||
$contacts[] = $contact;
|
||||
}
|
||||
|
||||
$this->leadRepo->saveEntities($contacts);
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
private function getSegmentCountHtml(Crawler $crawler, int $id): string
|
||||
{
|
||||
$content = $crawler->filter('span.col-count[data-id="'.$id.'"] a')->html();
|
||||
|
||||
return trim($content);
|
||||
}
|
||||
|
||||
private function getSegmentCountClass(Crawler $crawler, int $id): string
|
||||
{
|
||||
$class = $crawler->filter('span.col-count[data-id="'.$id.'"]')->attr('class');
|
||||
|
||||
return trim($class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $parameter
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function callGetLeadCountAjaxRequest(array $parameter): array
|
||||
{
|
||||
$this->client->request(Request::METHOD_POST, '/s/ajax?action=lead:getLeadCount', $parameter);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
return [
|
||||
'content' => json_decode($clientResponse->getContent(), true),
|
||||
'statusCode' => $this->client->getResponse()->getStatusCode(),
|
||||
];
|
||||
}
|
||||
|
||||
public function testCloneSegment(): void
|
||||
{
|
||||
$segment = $this->saveSegment(
|
||||
'Clone Segment',
|
||||
'clonesegment',
|
||||
);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/clone/'.$segment->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[alias]']->setValue('clonesegment2');
|
||||
$this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$rows = $this->listRepo->findAll();
|
||||
$this->assertCount(2, $rows);
|
||||
|
||||
$this->assertSame('clonesegment', $rows[0]->getAlias());
|
||||
$this->assertSame('clonesegment2', $rows[1]->getAlias());
|
||||
}
|
||||
|
||||
public function testSegmentFilterIcon(): void
|
||||
{
|
||||
// Save segment.
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'filter' => null,
|
||||
'display' => null,
|
||||
'operator' => '!empty',
|
||||
],
|
||||
];
|
||||
$this->saveSegment('Lead List 1', 'lead-list-1', $filters);
|
||||
$this->saveSegment('Lead List 2', 'lead-list-2');
|
||||
|
||||
// Check segment count UI for no contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$leadListsTableRows = $crawler->filterXPath("//table[@id='leadListTable']//tbody//tr");
|
||||
$this->assertEquals(2, $leadListsTableRows->count());
|
||||
$secondColumnOfLine = $leadListsTableRows->first()->filterXPath('//td[2]//div//i[@class="ri-fw ri-filter-2-fill fs-14"]')->count();
|
||||
$this->assertEquals(1, $secondColumnOfLine);
|
||||
$secondColumnOfLine = $leadListsTableRows->eq(1)->filterXPath('//td[2]//div//i[@class="ri-fw ri-filter-2-fill fs-14"]')->count();
|
||||
$this->assertEquals(0, $secondColumnOfLine);
|
||||
}
|
||||
|
||||
public function testUnpublishedSegmentDoesNotShowRebuildingLabel(): void
|
||||
{
|
||||
// Create a segment that would normally show "Building" label
|
||||
$segment = $this->saveSegment('Unpublished Segment', 'unpublished-segment', [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
// Set last built date in the past to trigger "Building" label for published segments
|
||||
$segment->setLastBuiltDate(new \DateTime('-1 year'));
|
||||
|
||||
// Unpublish the segment - this should prevent "Building" label
|
||||
$segment->setIsPublished(false);
|
||||
$this->listModel->saveEntity($segment);
|
||||
$this->em->clear();
|
||||
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
// Check segment count UI - should show "No Contacts" rather than "Building"
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('No Contacts', $html);
|
||||
self::assertSame('label label-gray col-count', $spClass);
|
||||
|
||||
// Check segment count AJAX - should also show "No Contacts"
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('No Contacts', $response['content']['html']);
|
||||
self::assertSame('label label-gray col-count', $response['content']['className']);
|
||||
self::assertSame(0, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
}
|
||||
|
||||
public function testSegmentWarningIcon(): void
|
||||
{
|
||||
$segmentWithOldLastRebuildDate = $this->saveSegment('TEST-Warning-Segment', 'test-warning-segment');
|
||||
$segmentWithFreshLastRebuildDate = $this->saveSegment('TEST-Fresh-Segment', 'test-fresh-segment');
|
||||
$segmentUnpublished = $this->saveSegment('TEST-Unpublished-Segment', 'test-unpublished-segment');
|
||||
|
||||
$segmentWithOldLastRebuildDate->setLastBuiltDate(new \DateTime('-1 year'));
|
||||
$segmentWithFreshLastRebuildDate->setLastBuiltDate(new \DateTime('now'));
|
||||
$segmentUnpublished->setIsPublished(false);
|
||||
|
||||
$this->em->persist($segmentWithOldLastRebuildDate);
|
||||
$this->em->persist($segmentWithFreshLastRebuildDate);
|
||||
$this->em->persist($segmentUnpublished);
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
|
||||
$warningSegmentRow = $crawler->filterXPath("//table[@id='leadListTable']//tbody//tr[contains(., 'TEST-Warning-Segment')]");
|
||||
$warningIcon = $warningSegmentRow->filterXPath('.//i[@class="text-danger ri-error-warning-line fs-14"]');
|
||||
$this->assertEquals(1, $warningIcon->count());
|
||||
|
||||
$freshSegmentRow = $crawler->filterXPath("//table[@id='leadListTable']//tbody//tr[contains(., 'TEST-Fresh-Segment')]");
|
||||
$warningIcon = $freshSegmentRow->filterXPath('.//i[@class="text-danger ri-error-warning-line fs-14"]');
|
||||
$this->assertEquals(0, $warningIcon->count());
|
||||
|
||||
$unpublishedSegmentRow = $crawler->filterXPath("//table[@id='leadListTable']//tbody//tr[contains(., 'TEST-Unpublished-Segment')]");
|
||||
$warningIcon = $unpublishedSegmentRow->filterXPath('.//i[@class="text-danger ri-error-warning-line fs-14"]');
|
||||
$this->assertEquals(0, $warningIcon->count());
|
||||
}
|
||||
|
||||
public function testBatchDeleteWithEmptyMembership(): void
|
||||
{
|
||||
$segment = $this->saveSegment(
|
||||
'Empty Members',
|
||||
'empty-members',
|
||||
[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'object' => 'lead',
|
||||
'type' => 'leadlist',
|
||||
'filter' => null,
|
||||
'display' => null,
|
||||
'operator' => 'empty',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest('POST', "s/segments/batchDelete?ids=[\"{$segmentId}\"]");
|
||||
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertStringContainsString('1 segments have been deleted!', $clientResponse->getContent());
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$segmentExistCheck = $this->listRepo->find($segmentId);
|
||||
Assert::assertNull($segmentExistCheck);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dateFieldProvider')]
|
||||
public function testWarningOnInvalidDateField(?string $filter, bool $shouldContainError, string $operator = '='): void
|
||||
{
|
||||
$segment = $this->saveSegment(
|
||||
'Date Segment',
|
||||
'ds',
|
||||
[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'date_added',
|
||||
'object' => 'lead',
|
||||
'type' => 'date',
|
||||
'filter' => $filter,
|
||||
'display' => null,
|
||||
'operator' => $operator,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segment->getId());
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$this->client->submit($form);
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
if ($shouldContainError) {
|
||||
$this->assertStringContainsString('Date field filter value "'.$filter.'" is invalid', $this->client->getResponse()->getContent());
|
||||
} else {
|
||||
$this->assertStringNotContainsString('Date field filter value', $this->client->getResponse()->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, bool|string|null>>
|
||||
*/
|
||||
public static function dateFieldProvider(): array
|
||||
{
|
||||
return [
|
||||
['Today', true],
|
||||
['birthday', false],
|
||||
['2023-01-01 11:00', false],
|
||||
['2023-01-01 11:00:00', false],
|
||||
['2023-01-01', false],
|
||||
['next week', false],
|
||||
[null, false],
|
||||
['\b\d{4}-(10|11|12)-\d{2}\b', false, 'regexp'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testRecentActivityFeedOnSegmentDetailsPage(): void
|
||||
{
|
||||
// Create segment
|
||||
$segment = $this->saveSegment('Date Segment', 'ds');
|
||||
$this->em->clear();
|
||||
|
||||
// Update segment
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segment->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[isPublished]']->setValue('0');
|
||||
$this->client->submit($form);
|
||||
|
||||
// View segment
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/view/'.$segment->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$translator = self::getContainer()->get('translator');
|
||||
|
||||
$this->assertStringContainsString($translator->trans('mautic.core.recent.activity'), $this->client->getResponse()->getContent());
|
||||
$this->assertCount(2, $crawler->filterXPath('//ul[contains(@class, "media-list-feed")]/li'));
|
||||
}
|
||||
|
||||
public function testActiveContactsStatExcludesDnc(): void
|
||||
{
|
||||
$segment = $this->saveSegment('active-test', 'active-test');
|
||||
$contact1 = new Lead();
|
||||
$contact1->setFirstname('Active');
|
||||
$this->em->persist($contact1);
|
||||
$contact2 = new Lead();
|
||||
$contact2->setFirstname('DNC');
|
||||
$this->em->persist($contact2);
|
||||
$this->em->flush();
|
||||
$segmentContact1 = new \Mautic\LeadBundle\Entity\ListLead();
|
||||
$segmentContact1->setList($segment);
|
||||
$segmentContact1->setLead($contact1);
|
||||
$segmentContact1->setDateAdded(new \DateTime());
|
||||
$segmentContact1->setManuallyAdded(false);
|
||||
$segmentContact1->setManuallyRemoved(false);
|
||||
$this->em->persist($segmentContact1);
|
||||
$segmentContact2 = new \Mautic\LeadBundle\Entity\ListLead();
|
||||
$segmentContact2->setList($segment);
|
||||
$segmentContact2->setLead($contact2);
|
||||
$segmentContact2->setDateAdded(new \DateTime());
|
||||
$segmentContact2->setManuallyAdded(false);
|
||||
$segmentContact2->setManuallyRemoved(false);
|
||||
$this->em->persist($segmentContact2);
|
||||
$this->em->flush();
|
||||
$dnc = new \Mautic\LeadBundle\Entity\DoNotContact();
|
||||
$dnc->setChannel('email');
|
||||
$dnc->setLead($contact2);
|
||||
$dnc->setReason(\Mautic\LeadBundle\Entity\DoNotContact::UNSUBSCRIBED);
|
||||
$dnc->setDateAdded(new \DateTime());
|
||||
$this->em->persist($dnc);
|
||||
$this->em->flush();
|
||||
$this->client->request('GET', sprintf('/s/segments/view/%d', $segment->getId()));
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$html = $response->getContent();
|
||||
$this->assertStringContainsString('Total contacts', $html);
|
||||
$this->assertStringContainsString('2', $html);
|
||||
$this->assertStringContainsString('Active contacts', $html);
|
||||
$this->assertStringContainsString('1', $html);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\UserBundle\Entity\Permission;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
final class ListControllerPermissionFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $nonAdminUser;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $userOne;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $userTwo;
|
||||
|
||||
/**
|
||||
* @var LeadList
|
||||
*/
|
||||
private $segmentA;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->nonAdminUser = $this->createUser([
|
||||
'user-name' => 'non-admin',
|
||||
'email' => 'non-admin@mautic-test.com',
|
||||
'first-name' => 'non-admin',
|
||||
'last-name' => 'non-admin',
|
||||
'role' => [
|
||||
'name' => 'perm_non_admin',
|
||||
'perm' => 'core:themes',
|
||||
'bitwise' => 1024,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->userOne = $this->createUser(
|
||||
[
|
||||
'user-name' => 'user-one',
|
||||
'email' => 'user-one@mautic-test.com',
|
||||
'first-name' => 'user-one',
|
||||
'last-name' => 'user-one',
|
||||
'role' => [
|
||||
'name' => 'perm_user_one',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 40,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->userTwo = $this->createUser([
|
||||
'user-name' => 'user-two',
|
||||
'email' => 'user-two@mautic-test.com',
|
||||
'first-name' => 'user-two',
|
||||
'last-name' => 'user-two',
|
||||
'role' => [
|
||||
'name' => 'perm_user_two',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 16,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->segmentA = $this->createSegment('Segment List A', $this->userOne);
|
||||
}
|
||||
|
||||
public function testIndexPageWithCreatePermission(): void
|
||||
{
|
||||
$this->loginOtherUser($this->userOne->getUserIdentifier());
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->assertCount(1, $crawler->filterXPath('//a[contains(@href,"/s/segments/new")]'), 'Listing page has the New button');
|
||||
}
|
||||
|
||||
public function testIndexPageNonAdmin(): void
|
||||
{
|
||||
$this->loginOtherUser($this->nonAdminUser->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testIndexPageForPaging(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/2');
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testCreateSegmentForUserWithoutPermission(): void
|
||||
{
|
||||
$this->loginOtherUser($this->nonAdminUser->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/new');
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testCreateSegmentForUserWithPermission(): void
|
||||
{
|
||||
$this->loginOtherUser($this->userOne->getUserIdentifier());
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/new');
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// Submitting for cancel button click.
|
||||
$form = $crawler->selectButton('Cancel')->form();
|
||||
$crawlerCancel = $this->client->submit($form);
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('Contact Segments', $crawlerCancel->html());
|
||||
|
||||
// Save the Segment.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/new');
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[name]']->setValue('Segment Test');
|
||||
$form['leadlist[alias]']->setValue('segment_test');
|
||||
$form['leadlist[isPublished]']->setValue('0');
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('Edit Segment - Segment Test', $crawler->html());
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataSegmentCloneUserPermissions')]
|
||||
public function testSegmentCloningOwnedSegmentWithDifferentPermissions(string $name, int $perm, int $expected): void
|
||||
{
|
||||
$user = $this->createUser(
|
||||
[
|
||||
'user-name' => $name,
|
||||
'email' => $name.'@mautic-test.com',
|
||||
'first-name' => $name,
|
||||
'last-name' => $name,
|
||||
'role' => [
|
||||
'name' => 'perm_user_three',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => $perm, // Create and View own
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segment = $this->createSegment('Test Segment for clone test', $user);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/clone/'.$segment->getId());
|
||||
$this->assertEquals($expected, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, mixed[]>
|
||||
*/
|
||||
public static function dataSegmentCloneUserPermissions(): iterable
|
||||
{
|
||||
yield 'Only create' => ['user-clone-1', 32, Response::HTTP_FORBIDDEN];
|
||||
yield 'Create and View own' => ['user-clone-2', 34, Response::HTTP_OK];
|
||||
yield 'Create and View other' => ['user-clone-2', 36, Response::HTTP_FORBIDDEN];
|
||||
}
|
||||
|
||||
public function testSegmentCloningUsingUserHavingPermissions(): void
|
||||
{
|
||||
$user = $this->createUser(
|
||||
[
|
||||
'user-name' => 'user-3',
|
||||
'email' => 'user-3@mautic-test.com',
|
||||
'first-name' => 'user-3',
|
||||
'last-name' => 'user-3',
|
||||
'role' => [
|
||||
'name' => 'perm_user_three',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 36, // Create and view other
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/clone/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testSegmentCloningUsingUserWithoutPermissions(): void
|
||||
{
|
||||
$this->loginOtherUser($this->userTwo->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/clone/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testCloneInvalidSegment(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/clone/2000');
|
||||
// For no entity found it will redirect to index page.
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('/s/segments/1', $this->client->getRequest()->getRequestUri());
|
||||
$this->assertStringContainsString('No segment with an id of 2000 was found!', $crawler->text());
|
||||
}
|
||||
|
||||
public function testEditSegmentAndClickOnButtons(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// Submitting for cancel button click.
|
||||
$form = $crawler->selectButton('Cancel')->form();
|
||||
$crawlerCancel = $this->client->submit($form);
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString($this->segmentA->getName(), $crawlerCancel->html());
|
||||
|
||||
// Save the Segment.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$this->segmentA->getId());
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[isPublished]']->setValue('0');
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('Edit Segment - '.$this->segmentA->getName(), $crawler->html());
|
||||
}
|
||||
|
||||
public function testEditInvalidSegment(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/2000');
|
||||
// For no entity found it will redirect to index page.
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('/s/segments/1', $this->client->getRequest()->getRequestUri());
|
||||
$this->assertStringContainsString('No segment with an id of 2000 was found!', $crawler->text());
|
||||
}
|
||||
|
||||
public function testEditOwnSegment(): void
|
||||
{
|
||||
$this->loginOtherUser($this->userOne->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testEditOthersSegment(): void
|
||||
{
|
||||
$this->loginOtherUser($this->userTwo->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testEditSegmentForUserWithoutPermission(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-edit',
|
||||
'email' => 'user-edit@mautic-test.com',
|
||||
'first-name' => 'user-edit',
|
||||
'last-name' => 'user-edit',
|
||||
'role' => [
|
||||
'name' => 'perm_user_edit',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 8,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testEditSegmentWhileLock(): void
|
||||
{
|
||||
$segmentA = $this->segmentA;
|
||||
$segmentA->setCheckedOut(new \DateTime());
|
||||
$segmentA->setCheckedOutBy($this->userOne);
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// As $segmentA is locked, so it will redirect user to its view page.
|
||||
$this->assertStringContainsString('/s/segments/view/'.$segmentA->getId(), $this->client->getRequest()->getRequestUri());
|
||||
}
|
||||
|
||||
public function testDeleteSegmentWithoutPermission(): void
|
||||
{
|
||||
$this->loginOtherUser($this->nonAdminUser->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_POST, '/s/segments/delete/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDeleteOthersSegmentWithPermission(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-other',
|
||||
'email' => 'user-delete-other@mautic-test.com',
|
||||
'first-name' => 'user-delete-other',
|
||||
'last-name' => 'user-delete-other',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_other',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 128,
|
||||
],
|
||||
]);
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_POST, '/s/segments/delete/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDeleteSegmentWithDependencyAndLockedInWithOtherUser(): void
|
||||
{
|
||||
$listId = $this->segmentA->getId();
|
||||
$filter = [[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => [
|
||||
'filter' => [$listId],
|
||||
],
|
||||
'display' => '',
|
||||
'filter' => [$listId],
|
||||
]];
|
||||
$segmentA = $this->createSegment('Segment List A', $this->userTwo, $filter);
|
||||
|
||||
$this->assertSame($filter, $segmentA->getFilters(), 'Filters');
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/delete/'.$listId);
|
||||
$this->assertStringContainsString("Segment cannot be deleted, it is required by {$segmentA->getName()}.", $crawler->text());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$segmentA->setCheckedOut(new \DateTime());
|
||||
$segmentA->setCheckedOutBy($this->userOne);
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/delete/'.$segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->assertStringContainsString("{$segmentA->getName()} is currently checked out by", $crawler->html());
|
||||
|
||||
// As $segmentA is locked, so it will redirect user to its view page.
|
||||
$this->assertStringContainsString('/s/segments/1', $this->client->getRequest()->getRequestUri());
|
||||
}
|
||||
|
||||
public function testDeleteInvalidSegment(): void
|
||||
{
|
||||
$listId = 99999;
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/delete/'.$listId);
|
||||
$this->assertStringContainsString("No segment with an id of {$listId} was found!", $crawler->html());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testBatchDeleteSegmentWhenUserDoNotHavePermission(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-a',
|
||||
'email' => 'user-delete-a@mautic-test.com',
|
||||
'first-name' => 'user-delete-a',
|
||||
'last-name' => 'user-delete-a',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_a',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 82,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segmentIds = [
|
||||
$this->segmentA->getId(),
|
||||
];
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/batchDelete?ids='.json_encode($segmentIds));
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// The logged-in user do not have permission to delete the segment $this->segmentA.
|
||||
$this->assertStringContainsString('You do not have access to the requested area/action.', $crawler->text());
|
||||
}
|
||||
|
||||
public function testBatchDeleteSegmentWhenUserDoNotHavePermissionAndSegmentIsInvalid(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-a',
|
||||
'email' => 'user-delete-a@mautic-test.com',
|
||||
'first-name' => 'user-delete-a',
|
||||
'last-name' => 'user-delete-a',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_a',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 82,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segmentIds = [
|
||||
101,
|
||||
];
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/batchDelete?ids='.json_encode($segmentIds));
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// The segment 101 is invalid.
|
||||
$this->assertStringContainsString('No segment with an id of 101 was found!', $crawler->text());
|
||||
}
|
||||
|
||||
public function testBatchDeleteSegmentWhenUserHavePermission(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-a',
|
||||
'email' => 'user-delete-a@mautic-test.com',
|
||||
'first-name' => 'user-delete-a',
|
||||
'last-name' => 'user-delete-a',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_a',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 82,
|
||||
],
|
||||
]);
|
||||
|
||||
$segmentA = $this->createSegment('Segment List A', $user);
|
||||
$this->em->flush();
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segmentIds = [
|
||||
$segmentA->getId(),
|
||||
];
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/batchDelete?ids='.json_encode($segmentIds));
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// Only one segments is deleted.
|
||||
$this->assertStringContainsString('1 segments have been deleted!', $crawler->html());
|
||||
}
|
||||
|
||||
public function testBatchDeleteSegmentWhenDeletingLocked(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-a',
|
||||
'email' => 'user-delete-a@mautic-test.com',
|
||||
'first-name' => 'user-delete-a',
|
||||
'last-name' => 'user-delete-a',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_a',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 82,
|
||||
],
|
||||
]);
|
||||
|
||||
$segmentC = $this->createSegment('Segment List C', $user);
|
||||
$segmentC->setCheckedOut(new \DateTime());
|
||||
$segmentC->setCheckedOutBy($this->userOne);
|
||||
$this->em->persist($segmentC);
|
||||
$this->em->flush();
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segmentIds = [
|
||||
$segmentC->getId(),
|
||||
];
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/batchDelete?ids='.json_encode($segmentIds));
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// The segment $segmentC is being locked by user other than logged-in.
|
||||
$this->assertStringContainsString("{$segmentC->getName()} is currently checked out by", $crawler->html());
|
||||
}
|
||||
|
||||
public function testBatchDeleteSegmentWhenDeletingRequiredByOthers(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-a',
|
||||
'email' => 'user-delete-a@mautic-test.com',
|
||||
'first-name' => 'user-delete-a',
|
||||
'last-name' => 'user-delete-a',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_a',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 82,
|
||||
],
|
||||
]);
|
||||
|
||||
$segmentA = $this->createSegment('Segment List A', $user);
|
||||
|
||||
$filter = [[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => [
|
||||
'filter' => [$segmentA->getId()],
|
||||
],
|
||||
'display' => '',
|
||||
'filter' => [$segmentA->getId()],
|
||||
]];
|
||||
|
||||
$segmentB = $this->createSegment('Segment List with filter', $user, $filter);
|
||||
$this->assertSame($filter, $segmentB->getFilters(), 'Filters');
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segmentIds = [
|
||||
$segmentA->getId(),
|
||||
];
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/batchDelete?ids='.json_encode($segmentIds));
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// The segment $segmentA is used as filter in $segmentB.
|
||||
$this->assertStringContainsString("{$segmentA->getName()} cannot be deleted, it is required by other segments.", $crawler->text());
|
||||
}
|
||||
|
||||
public function testViewSegment(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-view-own',
|
||||
'email' => 'user-view-own@mautic-test.com',
|
||||
'first-name' => 'user-view-own',
|
||||
'last-name' => 'user-view-own',
|
||||
'role' => [
|
||||
'name' => 'perm_user_view_own',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 2,
|
||||
],
|
||||
]);
|
||||
$segment = $this->createSegment('Segment News View', $user);
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/view/'.$segment->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->loginOtherUser($this->userOne->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/view/'.$segment->getId());
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testPostOnViewSegment(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_POST, '/s/segments/view/'.$this->segmentA->getId(), [
|
||||
'includeEvents' => [
|
||||
'manually_added',
|
||||
'manually_removed',
|
||||
'filter_added',
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testRemoveLeadFromSegmentWhereUserIsNotOwnerOfSegment(): void
|
||||
{
|
||||
$leadId = $this->createLead($this->userOne)->getId();
|
||||
$this->loginOtherUser($this->userTwo->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_POST, '/s/segments/removeLead/'.$this->segmentA->getId().'?leadId='.$leadId);
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testRemoveLeadFromSegmentWhereUserIsOwnerOfSegment(): void
|
||||
{
|
||||
$leadId = $this->createLead($this->userOne)->getId();
|
||||
$this->loginOtherUser($this->userOne->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_POST, '/s/segments/removeLead/'.$this->segmentA->getId().'?leadId='.$leadId);
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testAddLeadToSegmentForInvalidLeadAndLockedLeadAndInvalidSegment(): void
|
||||
{
|
||||
$leadId = 99999;
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/addLead/'.$this->segmentA->getId().'?leadId='.$leadId);
|
||||
$this->assertStringContainsString("No contact with an id of {$leadId} was found!", $crawler->html());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$listId = 9999;
|
||||
$lead = $this->createLead($this->userOne);
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/addLead/'.$listId.'?leadId='.$lead->getId());
|
||||
$this->assertStringContainsString("No segment with an id of {$listId} was found!", $crawler->html());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$lead->setCheckedOut(new \DateTime());
|
||||
$lead->setCheckedOutBy($this->userOne);
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/addLead/'.$this->segmentA->getId().'?leadId='.$lead->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->assertStringContainsString("{$lead->getPrimaryIdentifier()} is currently checked out by", $crawler->html());
|
||||
}
|
||||
|
||||
public function testUserCanPublishOwnSegmentWithPermission(): void
|
||||
{
|
||||
$userDetails = [
|
||||
'user-name' => 'testuser_publish',
|
||||
'first-name' => 'Test',
|
||||
'last-name' => 'Publisher',
|
||||
'email' => 'testuser_publish@example.com',
|
||||
'role' => [
|
||||
'name' => 'Can Publish Own Segments',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 382, // View own&other, Edit own&other, Create, Delete One, Publish Own
|
||||
],
|
||||
];
|
||||
|
||||
$user = $this->createUser($userDetails);
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segment = $this->createSegment('My Segment to Publish', $user);
|
||||
|
||||
$this->assertTrue($segment->isPublished());
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'togglePublishStatus',
|
||||
'model' => 'lead.list',
|
||||
'id' => $segment->getId(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->em->refresh($segment);
|
||||
$this->assertFalse($segment->isPublished());
|
||||
}
|
||||
|
||||
public function testUserCannotPublishOwnSegmentWithoutPermission(): void
|
||||
{
|
||||
$userDetails = [
|
||||
'user-name' => 'testuser_nopublish',
|
||||
'first-name' => 'Test',
|
||||
'last-name' => 'NoPublisher',
|
||||
'email' => 'testuser_nopublish@example.com',
|
||||
'role' => [
|
||||
'name' => 'No Publish Permission',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 126, // View own&other, Edit own&other, Create, Delete One
|
||||
],
|
||||
];
|
||||
|
||||
$user = $this->createUser($userDetails);
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segment = $this->createSegment('Segment Without Publish', $user);
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'togglePublishStatus',
|
||||
'model' => 'lead.list',
|
||||
'id' => $segment->getId(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->em->refresh($segment);
|
||||
$this->assertTrue($segment->isPublished());
|
||||
}
|
||||
|
||||
private function loginOtherUser(string $name): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/logout');
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => $name]);
|
||||
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', $name);
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $userDetails
|
||||
*/
|
||||
private function createUser(array $userDetails): User
|
||||
{
|
||||
$role = new Role();
|
||||
$role->setName($userDetails['role']['name']);
|
||||
$role->setIsAdmin(false);
|
||||
|
||||
$this->em->persist($role);
|
||||
|
||||
$this->createPermission($role, $userDetails['role']['perm'], $userDetails['role']['bitwise']);
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($userDetails['email']);
|
||||
$user->setUsername($userDetails['user-name']);
|
||||
$user->setFirstName($userDetails['first-name']);
|
||||
$user->setLastName($userDetails['last-name']);
|
||||
$user->setRole($role);
|
||||
|
||||
$hasher = self::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
|
||||
\assert($hasher instanceof PasswordHasherInterface);
|
||||
$user->setPassword($hasher->hash('Maut1cR0cks!'));
|
||||
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function createPermission(Role $role, string $rawPermission, int $bitwise): void
|
||||
{
|
||||
$parts = explode(':', $rawPermission);
|
||||
$permission = new Permission();
|
||||
$permission->setBundle($parts[0]);
|
||||
$permission->setName($parts[1]);
|
||||
$permission->setRole($role);
|
||||
$permission->setBitwise($bitwise);
|
||||
$this->em->persist($permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $filters
|
||||
*/
|
||||
private function createSegment(string $name, User $user, array $filters = []): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName($name);
|
||||
$segment->setPublicName($name);
|
||||
$segment->setAlias(str_shuffle('abcdefghijklmnopqrstuvwxyz'));
|
||||
$segment->setCreatedBy($user);
|
||||
|
||||
if ($filters) {
|
||||
$segment->setFilters($filters);
|
||||
}
|
||||
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
private function createLead(User $user): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setCreatedByUser($user);
|
||||
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
return $lead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Traits\ControllerTrait;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
|
||||
class ListControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
use ControllerTrait;
|
||||
|
||||
/**
|
||||
* Index action should return status code 200.
|
||||
*/
|
||||
public function testIndexAction(): void
|
||||
{
|
||||
$list = $this->createList();
|
||||
|
||||
$this->em->persist($list);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$urlAlias = 'segments';
|
||||
$routeAlias = 'leadlist';
|
||||
$column = 'dateModified';
|
||||
$column2 = 'name';
|
||||
$tableAlias = 'l.';
|
||||
|
||||
$this->getControllerColumnTests($urlAlias, $routeAlias, $column, $tableAlias, $column2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if list contains correct values.
|
||||
*/
|
||||
public function testViewList(): void
|
||||
{
|
||||
$list = $this->createList();
|
||||
$list->setDateAdded(new \DateTime('2020-02-07 20:29:02'));
|
||||
$list->setDateModified(new \DateTime('2020-03-21 20:29:02'));
|
||||
$list->setCreatedByUser('Test User');
|
||||
|
||||
$this->em->persist($list);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->client->request('GET', '/s/segments');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful('Return code must be 200.');
|
||||
$this->assertStringContainsString('February 7, 2020', $clientResponse->getContent());
|
||||
$this->assertStringContainsString('March 21, 2020', $clientResponse->getContent());
|
||||
$this->assertStringContainsString('Test User', $clientResponse->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtering should return status code 200.
|
||||
*/
|
||||
public function testIndexActionWhenFiltering(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/segments?search=has%3Aresults&tmpl=list');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful('Return code must be 200.');
|
||||
}
|
||||
|
||||
public function testSegmentView(): void
|
||||
{
|
||||
$contacts = $this->createContacts();
|
||||
$segment = $this->addContactsToSegment($contacts, 'MySeg');
|
||||
$this->client->request('GET', sprintf('/s/segments/view/%d', $segment->getId()));
|
||||
$response = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertStringContainsString('MySeg', $response->getContent());
|
||||
// Make sure that contact grid is not loaded synchronously
|
||||
self::assertStringNotContainsString('Kane', $response->getContent());
|
||||
self::assertStringNotContainsString('Jacques', $response->getContent());
|
||||
// Make sure the data-target-url is not an absolute URL
|
||||
self::assertStringContainsString(sprintf('data-target-url="/s/segment/view/%s/contact/1"', $segment->getId()), $response->getContent());
|
||||
}
|
||||
|
||||
public function testSegmentContactGrid(): void
|
||||
{
|
||||
$pageId = 1;
|
||||
$contacts = $this->createContacts();
|
||||
$segment = $this->addContactsToSegment($contacts, 'MySeg');
|
||||
$this->client->request('GET', sprintf('/s/segment/view/%d/contact/%d', $segment->getId(), $pageId));
|
||||
$response = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertStringContainsString('Kane', $response->getContent());
|
||||
self::assertStringContainsString('Jacques', $response->getContent());
|
||||
}
|
||||
|
||||
private function createList(string $suffix = 'A'): LeadList
|
||||
{
|
||||
$list = new LeadList();
|
||||
$list->setName("Segment $suffix");
|
||||
$list->setPublicName("Segment $suffix");
|
||||
$list->setAlias("segment-$suffix");
|
||||
$list->setDateAdded(new \DateTime('2020-02-07 20:29:02'));
|
||||
$list->setDateModified(new \DateTime('2020-03-21 20:29:02'));
|
||||
$list->setCreatedByUser('Test User');
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead[]
|
||||
*/
|
||||
private function createContacts(): array
|
||||
{
|
||||
$contact1 = new Lead();
|
||||
$contact1->setFirstname('Kane');
|
||||
$contact1->setLastname('Williamson');
|
||||
$contact1->setEmail('kane.williamson@test.com');
|
||||
|
||||
$contact2 = new Lead();
|
||||
$contact2->setFirstname('Jacques');
|
||||
$contact2->setLastname('Kallis');
|
||||
$contact2->setEmail('jacques.kallis@test.com');
|
||||
|
||||
$this->em->persist($contact1);
|
||||
$this->em->persist($contact2);
|
||||
$this->em->flush();
|
||||
|
||||
return [$contact1, $contact2];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead[] $contacts
|
||||
*/
|
||||
private function addContactsToSegment(array $contacts, string $segmentName): LeadList
|
||||
{
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'company',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'operator' => 'contains',
|
||||
'properties' => [
|
||||
'filter' => 'Acquia',
|
||||
],
|
||||
'filter' => 'Acquia',
|
||||
'display' => null,
|
||||
],
|
||||
];
|
||||
|
||||
$segment = new LeadList();
|
||||
$segment->setName($segmentName);
|
||||
$segment->setPublicName($segmentName);
|
||||
$segment->setAlias(strtolower($segmentName));
|
||||
$segment->isPublished(true);
|
||||
$segment->setDateAdded(new \DateTime());
|
||||
$segment->setFilters($filters);
|
||||
$segment->setIsGlobal(true);
|
||||
$segment->setIsPreferenceCenter(false);
|
||||
$this->em->persist($segment);
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
$segmentContacts = new ListLead();
|
||||
$segmentContacts->setList($segment);
|
||||
$segmentContacts->setLead($contact);
|
||||
$segmentContacts->setDateAdded(new \DateTime());
|
||||
$segmentContacts->setManuallyAdded(false);
|
||||
$segmentContacts->setManuallyRemoved(false);
|
||||
$this->em->persist($segmentContacts);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
public function testCloneSegmentPage(): void
|
||||
{
|
||||
$list = $this->createList('clone');
|
||||
$list->setDateAdded(new \DateTime('2020-02-07 20:29:02'));
|
||||
$list->setDateModified(new \DateTime('2020-03-21 20:29:02'));
|
||||
$list->setCreatedByUser('Test User');
|
||||
|
||||
$this->em->persist($list);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->client->request('GET', sprintf('/s/segments/clone/%d', $list->getId()));
|
||||
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful('Return code must be 200.');
|
||||
self::assertStringContainsString('Segment clone', $clientResponse->getContent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class NoteControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function beforeBeginTransaction(): void
|
||||
{
|
||||
$this->resetAutoincrement([
|
||||
'leads',
|
||||
'companies',
|
||||
'campaigns',
|
||||
'categories',
|
||||
'lead_lists',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick smoke test to ensure the route is successful.
|
||||
*/
|
||||
public function testIndexActionsIsSuccessful(): void
|
||||
{
|
||||
$contact = (new Lead())->setFirstname('Test');
|
||||
static::getContainer()->get('mautic.lead.model.lead')->saveEntity($contact);
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/contacts/notes/'.$contact->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick smoke test to ensure the route is successful.
|
||||
*/
|
||||
public function testNewActionsIsSuccessful(): void
|
||||
{
|
||||
$contact = (new Lead())->setFirstname('Test');
|
||||
static::getContainer()->get('mautic.lead.model.lead')->saveEntity($contact);
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/contacts/notes/'.$contact->getId().'/new');
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class TimelineControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
private const SALES_USER = 'sales';
|
||||
|
||||
public function testIndexActionsIsSuccessful(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', '/s/contacts/timeline/'.$contact->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testFilterCaseInsensitive(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$segment = $this->createSegment('TEST', []);
|
||||
$this->createListLead($segment, $contact);
|
||||
$this->em->flush();
|
||||
$this->createLeadEventLogEntry($contact, 'lead', 'segment', 'added', $segment->getId(), [
|
||||
'object_description' => $segment->getName(),
|
||||
]);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('POST', '/s/contacts/timeline/'.$contact->getId(), [
|
||||
'search' => 'test',
|
||||
'leadId' => $contact->getId(),
|
||||
]);
|
||||
|
||||
$this->assertStringContainsString('Contact added to segment, TEST', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testBatchExportActionAsAdmin(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', '/s/contacts/timeline/batchExport/'.$contact->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testBatchExportActionAsUserNotPermission(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => self::SALES_USER]);
|
||||
$this->loginUser($user);
|
||||
$this->client->request('GET', '/s/contacts/timeline/batchExport/'.$contact->getId());
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\PageBundle\Entity\Hit;
|
||||
use Mautic\PageBundle\Entity\Redirect;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
|
||||
class LoadClickData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
// Create an email for clicks
|
||||
$email = new Email();
|
||||
$email->setSubject('Email Test');
|
||||
$email->setCustomHtml('some content');
|
||||
$email->setName('Email Test');
|
||||
$manager->persist($email);
|
||||
$manager->flush();
|
||||
|
||||
// Create a SMS for clicks
|
||||
$sms = new Sms();
|
||||
$sms->setName('SMS Test');
|
||||
$sms->setMessage('hello');
|
||||
$manager->persist($sms);
|
||||
$manager->flush();
|
||||
|
||||
// Create redirect URL
|
||||
$redirect = new Redirect();
|
||||
$redirect->setUrl('https://mautic.org');
|
||||
$redirect->setRedirectId();
|
||||
$manager->persist($redirect);
|
||||
$manager->flush();
|
||||
|
||||
$hits = [
|
||||
[
|
||||
'redirect' => $redirect,
|
||||
'alias' => 'click-email-1',
|
||||
'source' => 'email',
|
||||
'source_id' => $email->getId(),
|
||||
'email' => $email,
|
||||
'date_hit' => new \DateTime(),
|
||||
'lead_id' => 1,
|
||||
],
|
||||
[
|
||||
'redirect' => $redirect,
|
||||
'alias' => 'click-email-2',
|
||||
'source' => 'email',
|
||||
'source_id' => $email->getId(),
|
||||
'email' => $email,
|
||||
'date_hit' => new \DateTime('-1 day'),
|
||||
'lead_id' => 2,
|
||||
],
|
||||
[
|
||||
'redirect' => $redirect,
|
||||
'alias' => 'click-email-3',
|
||||
'source' => 'email',
|
||||
'source_id' => $email->getId(),
|
||||
'email' => $email,
|
||||
'date_hit' => new \DateTime('+1 day'),
|
||||
'lead_id' => 2,
|
||||
],
|
||||
[
|
||||
'redirect' => $redirect,
|
||||
'alias' => 'click-sms-1',
|
||||
'source' => 'sms',
|
||||
'source_id' => $sms->getId(),
|
||||
'email' => null,
|
||||
'date_hit' => new \DateTime(),
|
||||
'lead_id' => 2,
|
||||
],
|
||||
[
|
||||
'redirect' => $redirect,
|
||||
'alias' => 'click-sms-2',
|
||||
'source' => 'sms',
|
||||
'source_id' => $sms->getId(),
|
||||
'email' => null,
|
||||
'date_hit' => new \DateTime('-1 day'),
|
||||
'lead_id' => 1,
|
||||
],
|
||||
[
|
||||
'redirect' => $redirect,
|
||||
'alias' => 'click-sms-3',
|
||||
'source' => 'sms',
|
||||
'source_id' => $sms->getId(),
|
||||
'email' => null,
|
||||
'date_hit' => new \DateTime('+1 day'),
|
||||
'lead_id' => 3,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($hits as $hitConfig) {
|
||||
$this->createHit($hitConfig, $manager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $hitConfig
|
||||
*/
|
||||
protected function createHit(array $hitConfig, ObjectManager $manager): void
|
||||
{
|
||||
$hit = new Hit();
|
||||
|
||||
$hit->setIpAddress($this->getReference('ipAddress-1'));
|
||||
$hit->setUrl('https://mautic.org');
|
||||
$hit->setReferer('https://google.com');
|
||||
$hit->setUrlTitle('Test Title');
|
||||
$hit->setLead($this->getReference('lead-'.$hitConfig['lead_id']));
|
||||
$hit->setDateHit($hitConfig['date_hit']);
|
||||
$hit->setCode(200);
|
||||
$hit->setTrackingId('abc');
|
||||
if ($hitConfig['email']) {
|
||||
$hit->setEmail($hitConfig['email']);
|
||||
}
|
||||
$hit->setRedirect($hitConfig['redirect']);
|
||||
$hit->setSource($hitConfig['source']);
|
||||
$hit->setSourceId($hitConfig['source_id']);
|
||||
$this->setReference($hitConfig['alias'], $hit);
|
||||
$manager->persist($hit);
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
|
||||
class LoadDncData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$dnc = new DoNotContact();
|
||||
$dnc->setChannel('sms');
|
||||
$dnc->setReason(DoNotContact::MANUAL);
|
||||
$dnc->setDateAdded(new \DateTime());
|
||||
$dnc->setLead($this->getReference('lead-1'));
|
||||
|
||||
$manager->persist($dnc);
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\PageBundle\Entity\Hit;
|
||||
|
||||
class LoadPageHitData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$hits = [
|
||||
[
|
||||
'ipAddress' => $this->getReference('ipAddress-1'),
|
||||
'url' => 'http://test.com',
|
||||
'urlTitle' => 'Test Title',
|
||||
'referer' => 'http://mautic.com',
|
||||
'alias' => 'hit-1',
|
||||
'contact' => $this->getReference('lead-1'),
|
||||
'dateHit' => new \DateTime('-1 day'),
|
||||
'code' => 200,
|
||||
'trackingId' => 'asdf',
|
||||
],
|
||||
[
|
||||
'ipAddress' => $this->getReference('ipAddress-2'),
|
||||
'url' => 'https://test/regex-segment-3.com',
|
||||
'urlTitle' => 'Test Regex Url',
|
||||
'referer' => 'https://test.com',
|
||||
'alias' => 'hit-2',
|
||||
'contact' => $this->getReference('lead-2'),
|
||||
'dateHit' => new \DateTime('-2 day'),
|
||||
'code' => 200,
|
||||
'trackingId' => 'abcdr',
|
||||
],
|
||||
[
|
||||
'ipAddress' => $this->getReference('ipAddress-3'),
|
||||
'url' => 'https://test/regex-segment-2.com',
|
||||
'urlTitle' => 'Test Regex Url',
|
||||
'referer' => 'https://test.com',
|
||||
'alias' => 'hit-3',
|
||||
'contact' => $this->getReference('lead-3'),
|
||||
'dateHit' => new \DateTime('-3 day'),
|
||||
'code' => 200,
|
||||
'trackingId' => 'abcdr',
|
||||
],
|
||||
[
|
||||
'ipAddress' => $this->getReference('ipAddress-4'),
|
||||
'url' => 'https://test/regex-segment-85.com',
|
||||
'urlTitle' => 'Test Regex Url',
|
||||
'referer' => 'https://test.com',
|
||||
'alias' => 'hit-4',
|
||||
'contact' => $this->getReference('lead-4'),
|
||||
'dateHit' => new \DateTime('-5 day'),
|
||||
'code' => 200,
|
||||
'trackingId' => 'abcdr',
|
||||
],
|
||||
[
|
||||
'ipAddress' => $this->getReference('ipAddress-5'),
|
||||
'url' => 'https://test/regex-segment-0.com',
|
||||
'urlTitle' => 'Test Regex Url',
|
||||
'referer' => 'https://test.com',
|
||||
'alias' => 'hit-5',
|
||||
'contact' => $this->getReference('lead-5'),
|
||||
'dateHit' => new \DateTime('-3 day'),
|
||||
'code' => 200,
|
||||
'trackingId' => 'abcdr',
|
||||
],
|
||||
[
|
||||
'ipAddress' => $this->getReference('ipAddress-5'),
|
||||
'url' => 'https://test/regex-segment-other.com',
|
||||
'urlTitle' => 'Test Title',
|
||||
'referer' => 'https://test.com',
|
||||
'alias' => 'hit-6',
|
||||
'contact' => $this->getReference('lead-5'),
|
||||
'dateHit' => new \DateTime('-3 day'),
|
||||
'code' => 200,
|
||||
'trackingId' => 'iomio',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($hits as $hitConfig) {
|
||||
$this->createHit($hitConfig, $manager);
|
||||
}
|
||||
}
|
||||
|
||||
protected function createHit($hitConfig, ObjectManager $manager)
|
||||
{
|
||||
$hit = new Hit();
|
||||
|
||||
$hit->setIpAddress($hitConfig['ipAddress']);
|
||||
$hit->setUrl($hitConfig['url']);
|
||||
$hit->setReferer($hitConfig['referer']);
|
||||
$hit->setUrlTitle($hitConfig['urlTitle']);
|
||||
$hit->setLead($hitConfig['contact']);
|
||||
$hit->setDateHit($hitConfig['dateHit']);
|
||||
$hit->setCode($hitConfig['code']);
|
||||
$hit->setTrackingId($hitConfig['trackingId']);
|
||||
|
||||
$this->setReference($hitConfig['alias'], $hit);
|
||||
|
||||
$manager->persist($hit);
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
|
||||
class LoadTagData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$tag = new Tag('Tag A');
|
||||
$manager->persist($tag);
|
||||
$manager->flush();
|
||||
|
||||
$contact1 = $this->getReference('lead-1');
|
||||
\assert($contact1 instanceof Lead);
|
||||
$contact1->addTag($tag);
|
||||
|
||||
$contact3 = $this->getReference('lead-3');
|
||||
\assert($contact1 instanceof Lead);
|
||||
$contact3->addTag($tag);
|
||||
|
||||
$manager->persist($contact1);
|
||||
$manager->persist($contact3);
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Deduplicate;
|
||||
|
||||
use Mautic\LeadBundle\Deduplicate\CompanyDeduper;
|
||||
use Mautic\LeadBundle\Entity\CompanyRepository;
|
||||
use Mautic\LeadBundle\Exception\UniqueFieldNotFoundException;
|
||||
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class CompanyDeduperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&FieldModel
|
||||
*/
|
||||
private MockObject $fieldModel;
|
||||
|
||||
/**
|
||||
* @var MockObject&CompanyRepository
|
||||
*/
|
||||
private MockObject $companyRepository;
|
||||
|
||||
/**
|
||||
* @var MockObject&FieldsWithUniqueIdentifier
|
||||
*/
|
||||
private MockObject $fieldsWithUniqueIdentifier;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->fieldModel = $this->createMock(FieldModel::class);
|
||||
|
||||
$this->fieldsWithUniqueIdentifier = $this->createMock(FieldsWithUniqueIdentifier::class);
|
||||
|
||||
$this->companyRepository = $this->createMock(CompanyRepository::class);
|
||||
}
|
||||
|
||||
public function testUniqueFieldNotFoundException(): void
|
||||
{
|
||||
$this->expectException(UniqueFieldNotFoundException::class);
|
||||
$this->fieldModel->method('getFieldList')->willReturn([]);
|
||||
$this->getDeduper()->checkForDuplicateCompanies([]);
|
||||
}
|
||||
|
||||
private function getDeduper(): CompanyDeduper
|
||||
{
|
||||
return new CompanyDeduper(
|
||||
$this->fieldModel,
|
||||
$this->fieldsWithUniqueIdentifier,
|
||||
$this->companyRepository
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Deduplicate;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Deduplicate\ContactMerger;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
|
||||
final class ContactMergerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testMergedContactFound(): void
|
||||
{
|
||||
$model = static::getContainer()->get('mautic.lead.model.lead');
|
||||
\assert($model instanceof LeadModel);
|
||||
|
||||
$merger = static::getContainer()->get('mautic.lead.merger');
|
||||
\assert($merger instanceof ContactMerger);
|
||||
|
||||
$bob = new Lead();
|
||||
$bob->setFirstname('Bob')
|
||||
->setLastname('Smith')
|
||||
->setEmail('bob.smith@test.com');
|
||||
$model->saveEntity($bob);
|
||||
$bobId = $bob->getId();
|
||||
|
||||
$jane = new Lead();
|
||||
$jane->setFirstname('Jane')
|
||||
->setLastname('Smith')
|
||||
->setEmail('jane.smith@test.com');
|
||||
$model->saveEntity($jane);
|
||||
$janeId = $jane->getId();
|
||||
|
||||
$merger->merge($jane, $bob);
|
||||
|
||||
// Bob should have been merged into Jane
|
||||
$jane = $model->getEntity($janeId);
|
||||
$this->assertEquals($janeId, $jane->getId());
|
||||
|
||||
// If Bob is queried, Jane should be returned
|
||||
$jane = $model->getEntity($bobId);
|
||||
$this->assertEquals($janeId, $jane->getId());
|
||||
|
||||
// Merge Jane into a third contact
|
||||
$joey = new Lead();
|
||||
$joey->setFirstname('Joey')
|
||||
->setLastname('Smith')
|
||||
->setEmail('joey.smith@test.com');
|
||||
$model->saveEntity($joey);
|
||||
$joeyId = $joey->getId();
|
||||
|
||||
$merger->merge($joey, $jane);
|
||||
|
||||
// Query for Bob which should now return Joey
|
||||
$joey = $model->getEntity($bobId);
|
||||
$this->assertEquals($joeyId, $joey->getId());
|
||||
|
||||
// If Joey is deleted, querying for Bob or Jane should result in null
|
||||
$model->deleteEntity($joey);
|
||||
$bob = $model->getEntity($bobId);
|
||||
$this->assertNull($bob);
|
||||
$jane = $model->getEntity($janeId);
|
||||
$this->assertNull($jane);
|
||||
}
|
||||
|
||||
public function testMergedContactsPointsAreAccurate(): void
|
||||
{
|
||||
$model = static::getContainer()->get('mautic.lead.model.lead');
|
||||
\assert($model instanceof LeadModel);
|
||||
|
||||
$em = static::getContainer()->get('doctrine.orm.entity_manager');
|
||||
\assert($em instanceof EntityManager);
|
||||
|
||||
$merger = static::getContainer()->get('mautic.lead.merger');
|
||||
\assert($merger instanceof ContactMerger);
|
||||
|
||||
// Startout Jane with 50 points
|
||||
$jane = new Lead();
|
||||
$jane->setFirstname('Jane')
|
||||
->setLastname('Smith')
|
||||
->setEmail('jane.smith@test.com')
|
||||
->setPoints(50);
|
||||
|
||||
$model->saveEntity($jane);
|
||||
|
||||
$em->detach($jane);
|
||||
$jane = $model->getEntity($jane->getId());
|
||||
$this->assertEquals(50, $jane->getPoints());
|
||||
$janeId = $jane->getId();
|
||||
|
||||
// Jane is currently a visitor on a different device with 3 points
|
||||
$visitor = new Lead();
|
||||
$visitor->setPoints(3);
|
||||
$model->saveEntity($visitor);
|
||||
$em->detach($visitor);
|
||||
$visitor = $model->getEntity($visitor->getId());
|
||||
$this->assertEquals(3, $visitor->getPoints());
|
||||
|
||||
// Jane submits a form or something that identifies her so the visitor should be merged into Jane giving her 53 points
|
||||
$jane = $model->getEntity($janeId);
|
||||
// Jane should start out with 50 points
|
||||
$this->assertEquals(50, $jane->getPoints());
|
||||
// Jane should come out of the merge as Jane
|
||||
$jane = $merger->merge($jane, $visitor);
|
||||
$this->assertEquals($janeId, $jane->getId());
|
||||
// Jane should now have 53 points
|
||||
$this->assertEquals(53, $jane->getPoints());
|
||||
$em->detach($jane);
|
||||
$em->detach($visitor);
|
||||
// Jane should still have 53 points
|
||||
$jane = $model->getEntity($janeId);
|
||||
$this->assertEquals(53, $jane->getPoints());
|
||||
|
||||
// Jane is on another device again and gets 3 points
|
||||
$visitor2 = new Lead();
|
||||
$visitor2->setPoints(3);
|
||||
$model->saveEntity($visitor2);
|
||||
$em->detach($visitor2);
|
||||
$visitor2 = $model->getEntity($visitor2->getId());
|
||||
$this->assertEquals(3, $visitor2->getPoints());
|
||||
|
||||
// Jane again identifies herself, gets merged into the new visitor and so should now have a total of 56 points
|
||||
$jane = $model->getEntity($janeId);
|
||||
$jane = $merger->merge($jane, $visitor2);
|
||||
$this->assertEquals($janeId, $jane->getId());
|
||||
$em->detach($jane);
|
||||
$em->detach($visitor2);
|
||||
$jane = $model->getEntity($jane->getId());
|
||||
|
||||
$this->assertEquals(56, $jane->getPoints());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,759 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Deduplicate;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\LeadBundle\Deduplicate\ContactMerger;
|
||||
use Mautic\LeadBundle\Deduplicate\Exception\SameContactException;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Entity\MergeRecordRepository;
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Monolog\Logger;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
|
||||
class ContactMergerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|LeadModel
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $leadModel;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject&LeadRepository
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $leadRepo;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|MergeRecordRepository
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $mergeRecordRepo;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|EventDispatcher
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $dispatcher;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|Logger
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->leadModel = $this->createMock(LeadModel::class);
|
||||
$this->leadRepo = $this->createMock(LeadRepository::class);
|
||||
$this->mergeRecordRepo = $this->createMock(MergeRecordRepository::class);
|
||||
$this->dispatcher = $this->createMock(EventDispatcher::class);
|
||||
$this->logger = $this->createMock(Logger::class);
|
||||
|
||||
$this->leadModel->method('getRepository')->willReturn($this->leadRepo);
|
||||
}
|
||||
|
||||
public function testMergeTimestamps(): void
|
||||
{
|
||||
$oldestDateTime = new \DateTime('-60 minutes');
|
||||
$latestDateTime = new \DateTime('-30 minutes');
|
||||
|
||||
$winner = new Lead();
|
||||
$winner->setLastActive($oldestDateTime);
|
||||
$winner->setDateIdentified($latestDateTime);
|
||||
|
||||
$loser = new Lead();
|
||||
$loser->setLastActive($latestDateTime);
|
||||
$loser->setDateIdentified($oldestDateTime);
|
||||
|
||||
$this->getMerger()->mergeTimestamps($winner, $loser);
|
||||
|
||||
$this->assertEquals($latestDateTime, $winner->getLastActive());
|
||||
$this->assertEquals($oldestDateTime, $winner->getDateIdentified());
|
||||
|
||||
// Test with null date identified loser
|
||||
$winner->setDateIdentified($latestDateTime);
|
||||
$loser->setDateIdentified(null);
|
||||
|
||||
$this->getMerger()->mergeTimestamps($winner, $loser);
|
||||
|
||||
$this->assertEquals($latestDateTime, $winner->getDateIdentified());
|
||||
|
||||
// Test with null date identified winner
|
||||
$winner->setDateIdentified(null);
|
||||
$loser->setDateIdentified($latestDateTime);
|
||||
|
||||
$this->getMerger()->mergeTimestamps($winner, $loser);
|
||||
|
||||
$this->assertEquals($latestDateTime, $winner->getDateIdentified());
|
||||
}
|
||||
|
||||
public function testMergeIpAddresses(): void
|
||||
{
|
||||
$winner = new Lead();
|
||||
$winner->addIpAddress((new IpAddress('1.2.3.4'))->setIpDetails(['extra' => 'from winner']));
|
||||
$winner->addIpAddress((new IpAddress('4.3.2.1'))->setIpDetails(['extra' => 'from winner']));
|
||||
$winner->addIpAddress((new IpAddress('5.6.7.8'))->setIpDetails(['extra' => 'from winner']));
|
||||
|
||||
$loser = new Lead();
|
||||
$loser->addIpAddress((new IpAddress('5.6.7.8'))->setIpDetails(['extra' => 'from loser']));
|
||||
$loser->addIpAddress((new IpAddress('8.7.6.5'))->setIpDetails(['extra' => 'from loser']));
|
||||
|
||||
$this->getMerger()->mergeIpAddressHistory($winner, $loser);
|
||||
|
||||
$ipAddresses = $winner->getIpAddresses();
|
||||
$this->assertCount(4, $ipAddresses);
|
||||
|
||||
$ipAddressArray = $ipAddresses->toArray();
|
||||
|
||||
$expectedIpAddressArray = [
|
||||
'1.2.3.4' => ['extra' => 'from winner'],
|
||||
'4.3.2.1' => ['extra' => 'from winner'],
|
||||
'5.6.7.8' => ['extra' => 'from winner'],
|
||||
'8.7.6.5' => ['extra' => 'from loser'],
|
||||
];
|
||||
|
||||
foreach ($expectedIpAddressArray as $ipAddress => $ipId) {
|
||||
$this->assertSame($ipAddress, $ipAddressArray[$ipAddress]->getIpAddress());
|
||||
$this->assertSame($ipId, $ipAddressArray[$ipAddress]->getIpDetails());
|
||||
}
|
||||
}
|
||||
|
||||
public function testMergeFieldDataWithLoserAsNewlyUpdated(): void
|
||||
{
|
||||
$winner = $this->createMock(Lead::class);
|
||||
$winner->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 1,
|
||||
'points' => 10,
|
||||
'email' => 'winner@test.com',
|
||||
]
|
||||
);
|
||||
|
||||
$loser = $this->createMock(Lead::class);
|
||||
$loser->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 2,
|
||||
'points' => 20,
|
||||
'email' => 'loser@test.com',
|
||||
]
|
||||
);
|
||||
|
||||
$merger = $this->getMerger();
|
||||
|
||||
$winnerDateModified = new \DateTime('-30 minutes');
|
||||
$loserDateModified = new \DateTime();
|
||||
$winner->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn($winnerDateModified);
|
||||
$loser->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn($loserDateModified);
|
||||
$winner->expects($this->once())
|
||||
->method('getFieldValue')
|
||||
->with('email')
|
||||
->willReturn('winner@test.com');
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getField')
|
||||
->with('email')
|
||||
->willReturn([
|
||||
'value' => 'winner@test.com',
|
||||
'id' => 22,
|
||||
'label' => 'Email',
|
||||
'alias' => 'email',
|
||||
'type' => 'email',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => true,
|
||||
'default_value' => null,
|
||||
]);
|
||||
|
||||
$winner->expects($this->exactly(3))
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$loser->expects($this->exactly(2))
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
// Loser values are newest so should be kept
|
||||
// id and points should not be set addUpdatedField should only be called once for email
|
||||
$winner->expects($this->once())
|
||||
->method('addUpdatedField')
|
||||
->with('email', 'loser@test.com');
|
||||
|
||||
$merger->mergeFieldData($winner, $loser);
|
||||
}
|
||||
|
||||
public function testMergeFieldDataWithWinnerAsNewlyUpdated(): void
|
||||
{
|
||||
$winner = $this->createMock(Lead::class);
|
||||
$winner->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 1,
|
||||
'points' => 10,
|
||||
'email' => 'winner@test.com',
|
||||
]
|
||||
);
|
||||
|
||||
$loser = $this->createMock(Lead::class);
|
||||
$loser->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 2,
|
||||
'points' => 20,
|
||||
'email' => 'loser@test.com',
|
||||
]
|
||||
);
|
||||
|
||||
$merger = $this->getMerger();
|
||||
|
||||
$winnerDateModified = new \DateTime();
|
||||
$loserDateModified = new \DateTime('-30 minutes');
|
||||
$winner->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn($winnerDateModified);
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getField')
|
||||
->with('email')
|
||||
->willReturn([
|
||||
'value' => 'winner@test.com',
|
||||
'id' => 22,
|
||||
'label' => 'Email',
|
||||
'alias' => 'email',
|
||||
'type' => 'email',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => true,
|
||||
'default_value' => null,
|
||||
]);
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getFieldValue')
|
||||
->with('email')
|
||||
->willReturn('winner@test.com');
|
||||
|
||||
$loser->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn($loserDateModified);
|
||||
|
||||
$winner->expects($this->exactly(4))
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$loser->expects($this->once())
|
||||
->method('getId');
|
||||
|
||||
// Winner values are newest so should be kept
|
||||
// addUpdatedField should never be called as they aren't different values
|
||||
$winner->expects($this->never())
|
||||
->method('addUpdatedField');
|
||||
|
||||
$merger->mergeFieldData($winner, $loser);
|
||||
}
|
||||
|
||||
public function testMergeFieldDataWithLoserAsNewlyCreated(): void
|
||||
{
|
||||
$winner = $this->createMock(Lead::class);
|
||||
$winner->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 1,
|
||||
'points' => 10,
|
||||
'email' => 'winner@test.com',
|
||||
]
|
||||
);
|
||||
|
||||
$loser = $this->createMock(Lead::class);
|
||||
$loser->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 2,
|
||||
'points' => 20,
|
||||
'email' => 'loser@test.com',
|
||||
]
|
||||
);
|
||||
|
||||
$merger = $this->getMerger();
|
||||
|
||||
$winnerDateModified = new \DateTime('-30 minutes');
|
||||
$loserDateModified = new \DateTime();
|
||||
$winner->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn($winnerDateModified);
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getField')
|
||||
->with('email')
|
||||
->willReturn([
|
||||
'value' => 'winner@test.com',
|
||||
'id' => 22,
|
||||
'label' => 'Email',
|
||||
'alias' => 'email',
|
||||
'type' => 'email',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => true,
|
||||
'default_value' => null,
|
||||
]);
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getFieldValue')
|
||||
->with('email')
|
||||
->willReturn('winner@test.com');
|
||||
|
||||
$loser->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn(null);
|
||||
$loser->expects($this->once())
|
||||
->method('getDateAdded')
|
||||
->willReturn($loserDateModified);
|
||||
|
||||
$winner->expects($this->exactly(3))
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$loser->expects($this->exactly(2))
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
// Loser values are newest so should be kept
|
||||
// id and points should not be set addUpdatedField should only be called once for email
|
||||
$winner->expects($this->once())
|
||||
->method('addUpdatedField')
|
||||
->with('email', 'loser@test.com');
|
||||
|
||||
$merger->mergeFieldData($winner, $loser);
|
||||
}
|
||||
|
||||
public function testMergeFieldDataWithWinnerAsNewlyCreated(): void
|
||||
{
|
||||
$winner = $this->createMock(Lead::class);
|
||||
$winner->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 1,
|
||||
'points' => 10,
|
||||
'email' => 'winner@test.com',
|
||||
]
|
||||
);
|
||||
|
||||
$loser = $this->createMock(Lead::class);
|
||||
$loser->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 2,
|
||||
'points' => 20,
|
||||
'email' => 'loser@test.com',
|
||||
]
|
||||
);
|
||||
|
||||
$merger = $this->getMerger();
|
||||
|
||||
$winnerDateModified = new \DateTime();
|
||||
$loserDateModified = new \DateTime('-30 minutes');
|
||||
$winner->expects($this->once())
|
||||
->method('getDateModified')
|
||||
->willReturn(null);
|
||||
$winner->expects($this->once())
|
||||
->method('getDateAdded')
|
||||
->willReturn($winnerDateModified);
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getField')
|
||||
->with('email')
|
||||
->willReturn([
|
||||
'value' => 'winner@test.com',
|
||||
'id' => 22,
|
||||
'label' => 'Email',
|
||||
'alias' => 'email',
|
||||
'type' => 'email',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => true,
|
||||
'default_value' => null,
|
||||
]);
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getFieldValue')
|
||||
->with('email')
|
||||
->willReturn('winner@test.com');
|
||||
|
||||
$loser->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn($loserDateModified);
|
||||
|
||||
$winner->expects($this->exactly(4))
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$loser->expects($this->once())
|
||||
->method('getId');
|
||||
|
||||
// Winner values are newest so should be kept
|
||||
// addUpdatedField should never be called as they aren't different values
|
||||
$winner->expects($this->never())
|
||||
->method('addUpdatedField');
|
||||
|
||||
$merger->mergeFieldData($winner, $loser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: A contact clicks on a tracked email link that goes to a tracked page.
|
||||
* The browser must contain no Mautic cookies. A new contact is created with only default values.
|
||||
* If default values from the new contact overwrite the values of the original contact then data are lost.
|
||||
*/
|
||||
public function testMergeFieldDataWithDefaultValues(): void
|
||||
{
|
||||
$winner = $this->createMock(Lead::class);
|
||||
$loser = $this->createMock(Lead::class);
|
||||
$merger = $this->getMerger();
|
||||
|
||||
$winnerDateModified = new \DateTime('-30 minutes');
|
||||
$loserDateModified = new \DateTime();
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn([
|
||||
'id' => 1,
|
||||
'email' => 'winner@test.com',
|
||||
'consent' => 'Yes',
|
||||
'boolean' => 1,
|
||||
]);
|
||||
|
||||
$loser->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn([
|
||||
'id' => 2,
|
||||
'email' => null,
|
||||
'consent' => 'No',
|
||||
'boolean' => 0,
|
||||
]);
|
||||
|
||||
$winner->method('getDateModified')->willReturn($winnerDateModified);
|
||||
$winner->method('getId')->willReturn(1);
|
||||
|
||||
$loser->method('getDateModified')->willReturn($loserDateModified);
|
||||
$loser->method('getId')->willReturn(2);
|
||||
$loser->method('isAnonymous')->willReturn(true);
|
||||
|
||||
$matcher = $this->exactly(3);
|
||||
$winner->expects($matcher)
|
||||
->method('getFieldValue')
|
||||
->willReturnCallback(function ($parameter) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('email', $parameter);
|
||||
|
||||
return 'winner@test.com';
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('consent', $parameter);
|
||||
|
||||
return 'Yes';
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('boolean', $parameter);
|
||||
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
$matcher2 = $this->exactly(3);
|
||||
$winner->expects($matcher2)
|
||||
->method('getField')
|
||||
->willReturnCallback(function ($parameter) use ($matcher2) {
|
||||
if (1 === $matcher2->numberOfInvocations()) {
|
||||
$this->assertSame('email', $parameter);
|
||||
|
||||
return [
|
||||
'id' => 22,
|
||||
'label' => 'Email',
|
||||
'alias' => 'email',
|
||||
'type' => 'email',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => true,
|
||||
'default_value' => null,
|
||||
];
|
||||
}
|
||||
if (2 === $matcher2->numberOfInvocations()) {
|
||||
$this->assertSame('consent', $parameter);
|
||||
|
||||
return [
|
||||
'id' => 44,
|
||||
'label' => 'Email Consent',
|
||||
'alias' => 'consent',
|
||||
'type' => 'select',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => true,
|
||||
'default_value' => 'No',
|
||||
];
|
||||
}
|
||||
if (3 === $matcher2->numberOfInvocations()) {
|
||||
$this->assertSame('boolean', $parameter);
|
||||
|
||||
return [
|
||||
'id' => 45,
|
||||
'label' => 'Boolean Field',
|
||||
'alias' => 'boolean',
|
||||
'type' => 'boolean',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => true,
|
||||
'default_value' => 0,
|
||||
];
|
||||
}
|
||||
});
|
||||
$matcher3 = $this->exactly(3);
|
||||
|
||||
$winner->expects($matcher3)
|
||||
->method('addUpdatedField')->willReturnCallback(function (...$parameters) use ($matcher3) {
|
||||
if (1 === $matcher3->numberOfInvocations()) {
|
||||
$this->assertSame('email', $parameters[0]);
|
||||
$this->assertSame('winner@test.com', $parameters[1]);
|
||||
}
|
||||
if (2 === $matcher3->numberOfInvocations()) {
|
||||
$this->assertSame('consent', $parameters[0]);
|
||||
$this->assertSame('Yes', $parameters[1]);
|
||||
}
|
||||
if (3 === $matcher3->numberOfInvocations()) {
|
||||
$this->assertSame('boolean', $parameters[0]);
|
||||
$this->assertSame(1, $parameters[1]);
|
||||
}
|
||||
});
|
||||
|
||||
$merger->mergeFieldData($winner, $loser);
|
||||
}
|
||||
|
||||
public function testMergeOwners(): void
|
||||
{
|
||||
$winner = new Lead();
|
||||
$loser = new Lead();
|
||||
|
||||
$winnerOwner = new User();
|
||||
$winnerOwner->setUsername('bob');
|
||||
$winner->setOwner($winnerOwner);
|
||||
|
||||
$loserOwner = new User();
|
||||
$loserOwner->setUsername('susan');
|
||||
$loser->setOwner($loserOwner);
|
||||
|
||||
// Should not have been merged due to winner already having one
|
||||
$this->getMerger()->mergeOwners($winner, $loser);
|
||||
$this->assertEquals($winnerOwner->getUserIdentifier(), $winner->getOwner()->getUserIdentifier());
|
||||
|
||||
$winner->setOwner(null);
|
||||
$this->getMerger()->mergeOwners($winner, $loser);
|
||||
|
||||
// Should be set to loser owner since winner owner was null
|
||||
$this->assertEquals($loserOwner->getUserIdentifier(), $winner->getOwner()->getUserIdentifier());
|
||||
}
|
||||
|
||||
public function testMergePoints(): void
|
||||
{
|
||||
$winner = new Lead();
|
||||
$loser = new Lead();
|
||||
|
||||
$winner->setPoints(100);
|
||||
$loser->setPoints(50);
|
||||
|
||||
$this->getMerger()->mergePoints($winner, $loser);
|
||||
|
||||
$this->assertEquals(150, $winner->getPoints());
|
||||
}
|
||||
|
||||
public function testMergeTags(): void
|
||||
{
|
||||
$winner = new Lead();
|
||||
$loser = new Lead();
|
||||
$loser->addTag(new Tag('loser'));
|
||||
$loser->addTag(new Tag('loser2'));
|
||||
|
||||
$this->leadModel->expects($this->once())
|
||||
->method('modifyTags')
|
||||
->with($winner, ['loser', 'loser2'], null, false);
|
||||
|
||||
$this->getMerger()->mergeTags($winner, $loser);
|
||||
}
|
||||
|
||||
public function testFullMergeThrowsSameContactException(): void
|
||||
{
|
||||
$winner = $this->createMock(Lead::class);
|
||||
$winner->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$loser = $this->createMock(Lead::class);
|
||||
$loser->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$this->expectException(SameContactException::class);
|
||||
|
||||
$this->getMerger()->merge($winner, $loser);
|
||||
}
|
||||
|
||||
public function testFullMerge(): void
|
||||
{
|
||||
$winner = $this->createMock(Lead::class);
|
||||
$winner->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
$winner->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 1,
|
||||
'points' => 10,
|
||||
'email' => 'winner@test.com',
|
||||
]
|
||||
);
|
||||
$winner->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn(new \DateTime('-30 minutes'));
|
||||
|
||||
$loser = $this->createMock(Lead::class);
|
||||
$loser->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
$loser->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 2,
|
||||
'points' => 20,
|
||||
'email' => 'loser@test.com',
|
||||
]
|
||||
);
|
||||
$loser->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
// updateMergeRecords
|
||||
$this->mergeRecordRepo->expects($this->once())
|
||||
->method('moveMergeRecord')
|
||||
->with(2, 1);
|
||||
|
||||
// mergeIpAddresses
|
||||
$ip = new IpAddress('1.2.3..4');
|
||||
$loser->expects($this->once())
|
||||
->method('getIpAddresses')
|
||||
->willReturn(new ArrayCollection([$ip]));
|
||||
$winner->expects($this->once())
|
||||
->method('addIpAddress')
|
||||
->with($ip);
|
||||
|
||||
// mergeFieldData
|
||||
$winner->expects($this->once())
|
||||
->method('getFieldValue')
|
||||
->with('email')
|
||||
->willReturn('winner@test.com');
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getField')
|
||||
->with('email')
|
||||
->willReturn([
|
||||
'value' => 'winner@test.com',
|
||||
'id' => 22,
|
||||
'label' => 'Email',
|
||||
'alias' => 'email',
|
||||
'type' => 'email',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => true,
|
||||
'default_value' => null,
|
||||
]);
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('addUpdatedField')
|
||||
->with('email', 'loser@test.com');
|
||||
|
||||
// mergeOwners
|
||||
$winner->expects($this->never())
|
||||
->method('setOwner');
|
||||
|
||||
// mergePoints
|
||||
$loser->expects($this->once())
|
||||
->method('getPoints')
|
||||
->willReturn(100);
|
||||
$winner->expects($this->once())
|
||||
->method('adjustPoints')
|
||||
->with(100);
|
||||
|
||||
// mergeTags
|
||||
$loser->expects($this->once())
|
||||
->method('getTags')
|
||||
->willReturn(new ArrayCollection());
|
||||
$this->leadModel->expects($this->once())
|
||||
->method('modifyTags')
|
||||
->with($winner, [], null, false);
|
||||
|
||||
$this->getMerger()->merge($winner, $loser);
|
||||
}
|
||||
|
||||
public function testMergeFieldWithEmptyFieldData(): void
|
||||
{
|
||||
$loser = $this->createMock(Lead::class);
|
||||
$winner = $this->createMock(Lead::class);
|
||||
|
||||
$loser->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn(new \DateTime('-10 minutes'));
|
||||
|
||||
$winner->expects($this->exactly(1))
|
||||
->method('getDateModified')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$winner->expects($this->exactly(4))
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$loser->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn([
|
||||
'email' => 'winner@test.com',
|
||||
]);
|
||||
|
||||
$winner->expects($this->once())
|
||||
->method('getField')
|
||||
->with('email')
|
||||
->willReturn(false);
|
||||
|
||||
$this->logger->expects($this->once())
|
||||
->method('info')
|
||||
->with('CONTACT: email is not mergeable for 1 - ');
|
||||
|
||||
$this->getMerger()->mergeFieldData($winner, $loser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ContactMerger
|
||||
*/
|
||||
private function getMerger()
|
||||
{
|
||||
return new ContactMerger(
|
||||
$this->leadModel,
|
||||
$this->mergeRecordRepo,
|
||||
$this->dispatcher,
|
||||
$this->logger
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Deduplicate\Helper;
|
||||
|
||||
use Mautic\LeadBundle\Deduplicate\Exception\ValueNotMergeableException;
|
||||
use Mautic\LeadBundle\Deduplicate\Helper\MergeValueHelper;
|
||||
|
||||
class MergeValueHelperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testGetMergeValueWhenNewAndOldValuesAreIdentical(): void
|
||||
{
|
||||
$newerValue = 'bbb';
|
||||
$olderValue = 'bbb';
|
||||
$winnerValue = null;
|
||||
$defaultValue = null;
|
||||
$newIsAnonymous = false;
|
||||
|
||||
$this->expectException(ValueNotMergeableException::class);
|
||||
MergeValueHelper::getMergeValue($newerValue, $olderValue, $winnerValue, $defaultValue, $newIsAnonymous);
|
||||
}
|
||||
|
||||
public function testGetMergeValueWhenNewAndWinnerValuesAreIdentical(): void
|
||||
{
|
||||
$newerValue = 'bbb';
|
||||
$olderValue = 'aaa';
|
||||
$winnerValue = 'bbb';
|
||||
$defaultValue = null;
|
||||
$newIsAnonymous = false;
|
||||
|
||||
$this->expectException(ValueNotMergeableException::class);
|
||||
MergeValueHelper::getMergeValue($newerValue, $olderValue, $winnerValue, $defaultValue, $newIsAnonymous);
|
||||
}
|
||||
|
||||
public function testGetMergeValueWhenNewerValueIsNotNull(): void
|
||||
{
|
||||
$newerValue = 'aaa';
|
||||
$olderValue = 'bbb';
|
||||
$winnerValue = 'bbb';
|
||||
$defaultValue = null;
|
||||
$newIsAnonymous = false;
|
||||
|
||||
$value = MergeValueHelper::getMergeValue($newerValue, $olderValue, $winnerValue, $defaultValue, $newIsAnonymous);
|
||||
|
||||
$this->assertSame('aaa', $value);
|
||||
}
|
||||
|
||||
public function testGetMergeValueWhenNewerValueIsNotNullAndSameAsDefaultValueForAnonymousContact(): void
|
||||
{
|
||||
$newerValue = 'aaa';
|
||||
$olderValue = 'bbb';
|
||||
$winnerValue = 'bbb';
|
||||
$defaultValue = 'aaa';
|
||||
$newIsAnonymous = true;
|
||||
|
||||
$value = MergeValueHelper::getMergeValue($newerValue, $olderValue, $winnerValue, $defaultValue, $newIsAnonymous);
|
||||
|
||||
$this->assertSame('bbb', $value);
|
||||
}
|
||||
|
||||
public function testGetMergeValueWhenNewerValueIsNotNullAndSameAsDefaultValueForIdentifiedContact(): void
|
||||
{
|
||||
$newerValue = 'aaa';
|
||||
$olderValue = 'bbb';
|
||||
$winnerValue = 'bbb';
|
||||
$defaultValue = 'aaa';
|
||||
$newIsAnonymous = false;
|
||||
|
||||
$value = MergeValueHelper::getMergeValue($newerValue, $olderValue, $winnerValue, $defaultValue, $newIsAnonymous);
|
||||
|
||||
$this->assertSame('aaa', $value);
|
||||
}
|
||||
|
||||
public function testGetMergeValueWhenNewerValueIsNull(): void
|
||||
{
|
||||
$newerValue = null;
|
||||
$olderValue = 'bbb';
|
||||
$winnerValue = 'bbb';
|
||||
$defaultValue = null;
|
||||
$newIsAnonymous = false;
|
||||
|
||||
$value = MergeValueHelper::getMergeValue($newerValue, $olderValue, $winnerValue, $defaultValue, $newIsAnonymous);
|
||||
|
||||
$this->assertSame('bbb', $value);
|
||||
}
|
||||
|
||||
public function testGetMergeValueWhenNewerValueIsNotNullAndDefaultValueIsZero(): void
|
||||
{
|
||||
$newerValue = 0;
|
||||
$olderValue = 1;
|
||||
$winnerValue = 1;
|
||||
$defaultValue = 0;
|
||||
$newIsAnonymous = true;
|
||||
|
||||
$value = MergeValueHelper::getMergeValue($newerValue, $olderValue, $winnerValue, $defaultValue, $newIsAnonymous);
|
||||
|
||||
$this->assertSame($winnerValue, $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
|
||||
use Mautic\LeadBundle\Exception\PrimaryCompanyNotFoundException;
|
||||
|
||||
class CompanyLeadRepositoryTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/** @var \PHPUnit\Framework\MockObject\MockObject|CompanyLeadRepository */
|
||||
private $repoMock;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->repoMock = $this->getMockBuilder(CompanyLeadRepository::class)
|
||||
->onlyMethods(['getCompaniesByLeadId'])
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
}
|
||||
|
||||
public function testGetPrimaryCompanyByLeadIdThrowsExceptionIfPrimaryIsMissing(): void
|
||||
{
|
||||
$this->repoMock->expects($this->once())
|
||||
->method('getCompaniesByLeadId')
|
||||
->willReturn([
|
||||
[
|
||||
'company_name' => 'ACME #1',
|
||||
'is_primary' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->expectException(PrimaryCompanyNotFoundException::class);
|
||||
$this->repoMock->getPrimaryCompanyByLeadId(1);
|
||||
}
|
||||
|
||||
public function testGetPrimaryCompanyByLeadIdReturnsCorrectRecord(): void
|
||||
{
|
||||
$this->repoMock->expects($this->once())
|
||||
->method('getCompaniesByLeadId')
|
||||
->willReturn([
|
||||
[
|
||||
'company_name' => 'ACME #1',
|
||||
'is_primary' => false,
|
||||
],
|
||||
[
|
||||
'company_name' => 'ACME #2',
|
||||
'is_primary' => true,
|
||||
],
|
||||
[
|
||||
'company_name' => 'ACME #3',
|
||||
'is_primary' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$primary = $this->repoMock->getPrimaryCompanyByLeadId(1);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'company_name' => 'ACME #2',
|
||||
'is_primary' => true,
|
||||
],
|
||||
$primary
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Symfony\Component\DomCrawler\Form;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class CompanyTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testChangingPropertiesHydratesFieldChanges(): void
|
||||
{
|
||||
$email = 'foo@bar.com';
|
||||
$company = new Company();
|
||||
$company->addUpdatedField('email', $email);
|
||||
$changes = $company->getChanges();
|
||||
|
||||
$this->assertFalse(empty($changes['fields']['email']));
|
||||
|
||||
$this->assertEquals($email, $changes['fields']['email'][1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MappingException
|
||||
*/
|
||||
public function testScoreValidationOnCompanyCreate(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/companies/new');
|
||||
$buttonCrawler = $crawler->selectButton('Save & Close');
|
||||
$form = $buttonCrawler->form();
|
||||
$form['company[score]']->setValue((string) -3);
|
||||
$this->testCompanyData($form);
|
||||
|
||||
$form['company[score]']->setValue((string) 2147483648);
|
||||
$this->testCompanyData($form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MappingException
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
*/
|
||||
public function testMinScoreValidationOnCompanyEdit(): void
|
||||
{
|
||||
$company = new Company();
|
||||
$company->setScore(-1);
|
||||
|
||||
$this->em->persist($company);
|
||||
$this->em->flush();
|
||||
|
||||
$companyId = $company->getId();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/companies/edit/'.$companyId);
|
||||
$buttonCrawler = $crawler->selectButton('Save & Close');
|
||||
$form = $buttonCrawler->form();
|
||||
$form['company[score]']->setValue((string) -3);
|
||||
$this->testCompanyData($form);
|
||||
|
||||
$form['company[score]']->setValue((string) 2147483648);
|
||||
$this->testCompanyData($form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MappingException
|
||||
*/
|
||||
private function testCompanyData(Form $form): void
|
||||
{
|
||||
$errorMessage = 'This value should be between 0 and 2147483647.';
|
||||
|
||||
$this->client->submit($form);
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$response = $this->client->getResponse()->getContent();
|
||||
$this->assertStringContainsString($errorMessage, (string) $response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class CompanyUnitTest extends TestCase
|
||||
{
|
||||
public function testChanges(): void
|
||||
{
|
||||
$company = new Company();
|
||||
|
||||
Assert::assertSame([], $company->getChanges());
|
||||
|
||||
$company->setEmail('john.doe@email.com');
|
||||
$company->setScore(2);
|
||||
$company->setName('Acquia');
|
||||
$company->setAddress1('Acquia avenue');
|
||||
$company->setAddress2('1234');
|
||||
$company->setPhone('123456789');
|
||||
$company->setCity('Boston');
|
||||
$company->setState('MA');
|
||||
$company->setZipcode('MA1234');
|
||||
$company->setCountry('US');
|
||||
$company->setWebsite('acquia.com');
|
||||
$company->setIndustry('DXP');
|
||||
$company->setDescription('Supports open source');
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'companyemail' => [null, 'john.doe@email.com'],
|
||||
'score' => [0, 2],
|
||||
'companyname' => [null, 'Acquia'],
|
||||
'companyaddress1' => [null, 'Acquia avenue'],
|
||||
'companyaddress2' => [null, '1234'],
|
||||
'companyphone' => [null, '123456789'],
|
||||
'companycity' => [null, 'Boston'],
|
||||
'companystate' => [null, 'MA'],
|
||||
'companyzipcode' => [null, 'MA1234'],
|
||||
'companycountry' => [null, 'US'],
|
||||
'companywebsite' => [null, 'acquia.com'],
|
||||
'companyindustry' => [null, 'DXP'],
|
||||
'companydescription' => [null, 'Supports open source'],
|
||||
],
|
||||
$company->getChanges()
|
||||
);
|
||||
|
||||
$company->setEmail('john.doe@email.com - updated');
|
||||
$company->setScore(5);
|
||||
$company->setName('Acquia - updated');
|
||||
$company->setAddress1('Acquia avenue - updated');
|
||||
$company->setAddress2('1234 - updated');
|
||||
$company->setPhone('123456789 - updated');
|
||||
$company->setCity('Boston - updated');
|
||||
$company->setState('MA - updated');
|
||||
$company->setZipcode('MA1234 - updated');
|
||||
$company->setCountry('US - updated');
|
||||
$company->setWebsite('acquia.com - updated');
|
||||
$company->setIndustry('DXP - updated');
|
||||
$company->setDescription('Supports open source - updated');
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'companyemail' => ['john.doe@email.com', 'john.doe@email.com - updated'],
|
||||
'score' => [2, 5],
|
||||
'companyname' => ['Acquia', 'Acquia - updated'],
|
||||
'companyaddress1' => ['Acquia avenue', 'Acquia avenue - updated'],
|
||||
'companyaddress2' => ['1234', '1234 - updated'],
|
||||
'companyphone' => ['123456789', '123456789 - updated'],
|
||||
'companycity' => ['Boston', 'Boston - updated'],
|
||||
'companystate' => ['MA', 'MA - updated'],
|
||||
'companyzipcode' => ['MA1234', 'MA1234 - updated'],
|
||||
'companycountry' => ['US', 'US - updated'],
|
||||
'companywebsite' => ['acquia.com', 'acquia.com - updated'],
|
||||
'companyindustry' => ['DXP', 'DXP - updated'],
|
||||
'companydescription' => ['Supports open source', 'Supports open source - updated'],
|
||||
],
|
||||
$company->getChanges()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\ContactExportScheduler;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class ContactExportSchedulerTest extends MauticMysqlTestCase
|
||||
{
|
||||
private string $previousTimeZone;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->previousTimeZone = date_default_timezone_get();
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
protected function beforeTearDown(): void
|
||||
{
|
||||
date_default_timezone_set($this->previousTimeZone);
|
||||
}
|
||||
|
||||
public function testScheduledDateTimeIsPersistedAndHydratedProperly(): void
|
||||
{
|
||||
$timezone = 'Asia/Taipei';
|
||||
date_default_timezone_set($timezone);
|
||||
|
||||
$exportScheduler = new ContactExportScheduler();
|
||||
$exportScheduler->setScheduledDateTime(new \DateTimeImmutable());
|
||||
$this->em->persist($exportScheduler);
|
||||
$this->em->flush();
|
||||
|
||||
$id = $exportScheduler->getId();
|
||||
$localDate = $exportScheduler->getScheduledDateTime()->format(DateTimeHelper::FORMAT_DB);
|
||||
$utcDate = $this->convertDateTimezone($localDate, $timezone, 'UTC');
|
||||
Assert::assertSame($timezone, $exportScheduler->getScheduledDateTime()->getTimezone()->getName(), sprintf('Timezone should be %s.', $timezone));
|
||||
Assert::assertSame($utcDate, $this->fetchScheduledDate($id), 'Database value should be converted to UTC.');
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$timezone = 'America/Cayman';
|
||||
date_default_timezone_set($timezone);
|
||||
|
||||
$exportScheduler = $this->em->find(ContactExportScheduler::class, $id);
|
||||
$localDate = $this->convertDateTimezone($this->fetchScheduledDate($id), 'UTC', $timezone);
|
||||
Assert::assertSame($timezone, $exportScheduler->getScheduledDateTime()->getTimezone()->getName(), sprintf('Timezone should be %s.', $timezone));
|
||||
Assert::assertSame($localDate, $exportScheduler->getScheduledDateTime()->format(DateTimeHelper::FORMAT_DB), sprintf('PHP value should be converted to %s.', $timezone));
|
||||
}
|
||||
|
||||
private function convertDateTimezone(string $date, string $timezoneFrom, string $timezoneTo): string
|
||||
{
|
||||
return (new \DateTimeImmutable($date, new \DateTimeZone($timezoneFrom)))
|
||||
->setTimezone(new \DateTimeZone($timezoneTo))
|
||||
->format(DateTimeHelper::FORMAT_DB);
|
||||
}
|
||||
|
||||
private function fetchScheduledDate(int $id): string
|
||||
{
|
||||
$tablePrefix = self::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
$connection = $this->em->getConnection();
|
||||
$query = sprintf('SELECT scheduled_datetime FROM %scontact_export_scheduler WHERE id = :id', $tablePrefix);
|
||||
|
||||
return $connection->executeQuery($query, ['id' => $id])
|
||||
->fetchOne();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Tests\StandardImportTestHelper;
|
||||
|
||||
class CustomFieldRepositoryTraitTest extends StandardImportTestHelper
|
||||
{
|
||||
private $fields = [
|
||||
'firstname' => [
|
||||
'id' => 2,
|
||||
'label' => 'First Name',
|
||||
'alias' => 'firstname',
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => 1,
|
||||
],
|
||||
'lastname' => [
|
||||
'id' => 3,
|
||||
'label' => 'Last Name',
|
||||
'alias' => 'lastname',
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => 1,
|
||||
],
|
||||
'twitter' => [
|
||||
'id' => 27,
|
||||
'label' => 'Twitter',
|
||||
'alias' => 'twitter',
|
||||
'type' => 'text',
|
||||
'group' => 'social',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
private $fieldValues = [
|
||||
'preferred_profile_image' => 'gravatar',
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'Doe',
|
||||
'twitter' => 'johndoe',
|
||||
];
|
||||
|
||||
protected $fixedFields = [
|
||||
'firstname' => 'firstname',
|
||||
'lastname' => 'lastname',
|
||||
];
|
||||
|
||||
protected $baseColumns = [
|
||||
'preferred_profile_image',
|
||||
'firstname',
|
||||
'lastname',
|
||||
];
|
||||
|
||||
protected $fieldGroups = [
|
||||
'core',
|
||||
'social',
|
||||
'personal',
|
||||
'professional',
|
||||
];
|
||||
|
||||
public function testFormatFieldValues(): void
|
||||
{
|
||||
$mockWithTrait = $this->getMockBuilder(LeadRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->enableOriginalClone()
|
||||
->onlyMethods(['getCustomFieldList', 'getBaseColumns', 'getClassName', 'getFieldGroups'])
|
||||
->getMock();
|
||||
$mockWithTrait->method('getCustomFieldList')
|
||||
->willReturn([$this->fields, $this->fixedFields]);
|
||||
|
||||
$mockWithTrait->method('getBaseColumns')
|
||||
->willReturn($this->baseColumns);
|
||||
|
||||
$mockWithTrait->method('getClassName')
|
||||
->willReturn(\Mautic\LeadBundle\Entity\Lead::class);
|
||||
|
||||
$mockWithTrait->method('getFieldGroups')
|
||||
->willReturn($this->fieldGroups);
|
||||
|
||||
$reflectedMockTrait = new \ReflectionObject($mockWithTrait);
|
||||
$method = $reflectedMockTrait->getMethod('formatFieldValues');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$expected = [
|
||||
'core' => [
|
||||
'firstname' => [
|
||||
'id' => 2,
|
||||
'label' => 'First Name',
|
||||
'alias' => 'firstname',
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => 1,
|
||||
'value' => 'John',
|
||||
],
|
||||
'lastname' => [
|
||||
'id' => 3,
|
||||
'label' => 'Last Name',
|
||||
'alias' => 'lastname',
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => 1,
|
||||
'value' => 'Doe',
|
||||
],
|
||||
],
|
||||
'social' => [
|
||||
'twitter' => [
|
||||
'id' => 27,
|
||||
'label' => 'Twitter',
|
||||
'alias' => 'twitter',
|
||||
'type' => 'text',
|
||||
'group' => 'social',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => 0,
|
||||
'value' => 'johndoe',
|
||||
],
|
||||
],
|
||||
'personal' => [],
|
||||
'professional' => [],
|
||||
];
|
||||
|
||||
$result = $method->invokeArgs($mockWithTrait, [$this->fieldValues]);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testFormatFieldValuesWhenAFieldIsUnpublished(): void
|
||||
{
|
||||
$mockWithTrait = $this->getMockBuilder(LeadRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->enableOriginalClone()
|
||||
->onlyMethods(['getCustomFieldList', 'getBaseColumns', 'getClassName', 'getFieldGroups'])
|
||||
->getMock();
|
||||
|
||||
$mockWithTrait->method('getCustomFieldList')
|
||||
->willReturn([$this->fields, $this->fixedFields]);
|
||||
|
||||
$mockWithTrait->method('getBaseColumns')
|
||||
->willReturn($this->baseColumns);
|
||||
|
||||
$mockWithTrait->method('getClassName')
|
||||
->willReturn(\Mautic\LeadBundle\Entity\Lead::class);
|
||||
|
||||
$mockWithTrait->method('getFieldGroups')
|
||||
->willReturn($this->fieldGroups);
|
||||
|
||||
$reflectedMockTrait = new \ReflectionObject($mockWithTrait);
|
||||
$method = $reflectedMockTrait->getMethod('formatFieldValues');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$expected = [
|
||||
'core' => [
|
||||
'firstname' => [
|
||||
'id' => 2,
|
||||
'label' => 'First Name',
|
||||
'alias' => 'firstname',
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => 1,
|
||||
'value' => 'John',
|
||||
],
|
||||
'lastname' => [
|
||||
'id' => 3,
|
||||
'label' => 'Last Name',
|
||||
'alias' => 'lastname',
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => 1,
|
||||
'value' => 1,
|
||||
],
|
||||
],
|
||||
'social' => [
|
||||
'twitter' => [
|
||||
'id' => 27,
|
||||
'label' => 'Twitter',
|
||||
'alias' => 'twitter',
|
||||
'type' => 'text',
|
||||
'group' => 'social',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => 0,
|
||||
'value' => 'johndoe',
|
||||
],
|
||||
],
|
||||
'personal' => [],
|
||||
'professional' => [],
|
||||
];
|
||||
|
||||
$values = $this->fieldValues;
|
||||
|
||||
// Simulate unpublished field:
|
||||
unset($values['lastname']);
|
||||
|
||||
$result = $method->invokeArgs($mockWithTrait, [$values]);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\DoNotContactRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class DoNotContactRepositoryFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testGetChannelList(): void
|
||||
{
|
||||
$john = $this->createContact('Company A');
|
||||
$jane = $this->createContact('Company B');
|
||||
$josh = $this->createContact('Company B');
|
||||
|
||||
$this->createDnc('email', $josh, DoNotContact::IS_CONTACTABLE);
|
||||
$this->createDnc('email', $john, DoNotContact::UNSUBSCRIBED);
|
||||
$this->createDnc('sms', $john, DoNotContact::BOUNCED);
|
||||
$this->createDnc('sms', $jane, DoNotContact::MANUAL);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$repository = $this->em->getRepository(DoNotContact::class);
|
||||
\assert($repository instanceof DoNotContactRepository);
|
||||
|
||||
$allDncRecords = $repository->getChannelList(null);
|
||||
$allSmsRecords = $repository->getChannelList('sms');
|
||||
|
||||
Assert::assertCount(3, $allDncRecords, 'Get all records for all channels (dangerous, do not use, there is no limit. One would expect this to return all 4 records, but they are grouped by contact ID.');
|
||||
Assert::assertCount(2, $allSmsRecords, 'Get all records for sms channel (dangerous, do not use, there is no limit.');
|
||||
Assert::assertCount(0, $repository->getChannelList('sms', []), 'Get all records for sms channel where the user filtered for a contact that do not exist. It must return an empty array. Not all DNC records.');
|
||||
Assert::assertCount(1, $repository->getChannelList('sms', [$john->getId()]));
|
||||
Assert::assertCount(2, $repository->getChannelList('sms', [$john->getId(), $jane->getId(), $josh->getId()]));
|
||||
Assert::assertSame(['email' => (string) DoNotContact::IS_CONTACTABLE], $allDncRecords[$josh->getId()]);
|
||||
Assert::assertSame(['email' => (string) DoNotContact::UNSUBSCRIBED, 'sms' => (string) DoNotContact::BOUNCED], $allDncRecords[$john->getId()]);
|
||||
Assert::assertSame(['sms' => (string) DoNotContact::MANUAL], $allDncRecords[$jane->getId()]);
|
||||
Assert::assertSame((string) DoNotContact::BOUNCED, $allSmsRecords[$john->getId()]);
|
||||
Assert::assertSame((string) DoNotContact::MANUAL, $allSmsRecords[$jane->getId()]);
|
||||
}
|
||||
|
||||
public function createDnc(string $channel, Lead $contact, int $reason): DoNotContact
|
||||
{
|
||||
$dnc = new DoNotContact();
|
||||
$dnc->setChannel($channel);
|
||||
$dnc->setLead($contact);
|
||||
$dnc->setReason($reason);
|
||||
$dnc->setDateAdded(new \DateTime());
|
||||
$this->em->persist($dnc);
|
||||
|
||||
return $dnc;
|
||||
}
|
||||
|
||||
private function createContact(string $firstName): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname($firstName);
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
|
||||
class DoNotContactTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testDoNotContactComments(): void
|
||||
{
|
||||
$doNotContact = new DoNotContact();
|
||||
$doNotContact->setComments(null);
|
||||
$this->assertSame('', $doNotContact->getComments());
|
||||
|
||||
$comment = '<script>alert(\'x\')</script>';
|
||||
$doNotContact->setComments($comment);
|
||||
$this->assertNotSame($comment, $doNotContact->getComments());
|
||||
$this->assertSame('alert(\'x\')', $doNotContact->getComments());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\LeadBundle\Entity\FrequencyRule;
|
||||
use Mautic\LeadBundle\Entity\FrequencyRuleRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class FrequencyRuleRepositoryTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var FrequencyRuleRepository
|
||||
*/
|
||||
private $frequencyRuleRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->frequencyRuleRepository = static::getContainer()->get('mautic.lead.repository.frequency_rule');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testCustomFrequencyRuleViolationsMethodReturnsCorrectData(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Firstname');
|
||||
$lead->setLastname('Lastname');
|
||||
$lead->setEmail('test@test.com');
|
||||
$lead->setPhone('555-666-777');
|
||||
|
||||
$this->em->persist($lead);
|
||||
|
||||
$frequencyRule = new FrequencyRule();
|
||||
$frequencyRule->setFrequencyNumber(1);
|
||||
$frequencyRule->setFrequencyTime('DAY');
|
||||
$frequencyRule->setChannel('email');
|
||||
$frequencyRule->setDateAdded(new \DateTime());
|
||||
$frequencyRule->setLead($lead);
|
||||
|
||||
$this->em->persist($frequencyRule);
|
||||
|
||||
$emailStats1 = new Stat();
|
||||
$emailStats1->setLead($lead);
|
||||
$emailStats1->setEmailAddress('testemail@test.test');
|
||||
$emailStats1->setDateSent(new \DateTime());
|
||||
$emailStats1->setIsRead(true);
|
||||
$emailStats1->setIsFailed(false);
|
||||
$emailStats1->setViewedInBrowser(false);
|
||||
|
||||
$emailStats2 = new Stat();
|
||||
$emailStats2->setLead($lead);
|
||||
$emailStats2->setEmailAddress('testemail@test.test');
|
||||
$emailStats2->setDateSent(new \DateTime());
|
||||
$emailStats2->setIsRead(true);
|
||||
$emailStats2->setIsFailed(false);
|
||||
$emailStats2->setViewedInBrowser(false);
|
||||
|
||||
$this->em->persist($emailStats1);
|
||||
$this->em->persist($emailStats2);
|
||||
$this->em->flush();
|
||||
|
||||
$violations = $this->frequencyRuleRepository->getAppliedFrequencyRules('email', [$lead->getId()], 1, 'DAY');
|
||||
$expectedViolations = [
|
||||
[
|
||||
'lead_id' => (string) $lead->getId(),
|
||||
'frequency_number' => '1',
|
||||
'frequency_time' => 'DAY',
|
||||
],
|
||||
];
|
||||
Assert::assertSame($expectedViolations, $violations);
|
||||
}
|
||||
|
||||
public function testValidateDefaultParameters(): void
|
||||
{
|
||||
$method = new \ReflectionMethod(FrequencyRuleRepository::class, 'validateDefaultParameters');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$this->assertFalse($method->invoke($this->frequencyRuleRepository, false, false));
|
||||
$this->assertFalse($method->invoke($this->frequencyRuleRepository, false, true));
|
||||
$this->assertFalse($method->invoke($this->frequencyRuleRepository, true, false));
|
||||
$this->assertTrue($method->invoke($this->frequencyRuleRepository, true, true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Tests\StandardImportTestHelper;
|
||||
|
||||
class ImportTest extends StandardImportTestHelper
|
||||
{
|
||||
public function testSetPath(): void
|
||||
{
|
||||
$import = $this->initImportEntity();
|
||||
|
||||
$this->assertSame(self::$csvPath, $import->getFilePath());
|
||||
}
|
||||
|
||||
public function testCanProceed(): void
|
||||
{
|
||||
$import = $this->initImportEntity();
|
||||
|
||||
$this->assertTrue($import->canProceed());
|
||||
}
|
||||
|
||||
public function testIsBackgroundProcess(): void
|
||||
{
|
||||
$import = $this->initImportEntity();
|
||||
|
||||
$this->assertTrue($import->isBackgroundProcess());
|
||||
|
||||
$import->setStatus(Import::MANUAL);
|
||||
|
||||
$this->assertFalse($import->isBackgroundProcess());
|
||||
}
|
||||
|
||||
public function testIncreaseInsertedCount(): void
|
||||
{
|
||||
$count = 4;
|
||||
$import = $this->initImportEntity();
|
||||
$import->setInsertedCount($count);
|
||||
|
||||
$this->assertSame($count, $import->getInsertedCount());
|
||||
|
||||
$import->increaseInsertedCount();
|
||||
|
||||
$this->assertSame(5, $import->getInsertedCount());
|
||||
}
|
||||
|
||||
public function testIncreaseUpdatedCount(): void
|
||||
{
|
||||
$count = 4;
|
||||
$import = $this->initImportEntity();
|
||||
$import->setUpdatedCount($count);
|
||||
|
||||
$this->assertSame($count, $import->getUpdatedCount());
|
||||
|
||||
$import->increaseUpdatedCount();
|
||||
|
||||
$this->assertSame(5, $import->getUpdatedCount());
|
||||
}
|
||||
|
||||
public function testIncreaseIgnoredCount(): void
|
||||
{
|
||||
$count = 4;
|
||||
$import = $this->initImportEntity();
|
||||
$import->setIgnoredCount($count);
|
||||
|
||||
$this->assertSame($count, $import->getIgnoredCount());
|
||||
|
||||
$import->increaseIgnoredCount();
|
||||
|
||||
$this->assertSame(5, $import->getIgnoredCount());
|
||||
}
|
||||
|
||||
public function testGetProcessedRows(): void
|
||||
{
|
||||
$count = 4;
|
||||
$import = $this->initImportEntity();
|
||||
|
||||
$this->assertSame(0, $import->getProcessedRows());
|
||||
|
||||
$import->setIgnoredCount($count);
|
||||
$import->setUpdatedCount($count);
|
||||
$import->setInsertedCount($count);
|
||||
|
||||
$expectedCount = (3 * $count);
|
||||
$this->assertSame($expectedCount, $import->getProcessedRows());
|
||||
|
||||
$import->increaseIgnoredCount();
|
||||
$import->increaseIgnoredCount();
|
||||
|
||||
$expectedCount = (int) (2 + $expectedCount);
|
||||
$this->assertSame($expectedCount, $import->getProcessedRows()); // @phpstan-ignore argument.unresolvableType (I don't see anything wrong)
|
||||
}
|
||||
|
||||
public function testGetProgressPercentage(): void
|
||||
{
|
||||
$import = $this->initImportEntity()
|
||||
->setLineCount(100);
|
||||
|
||||
$this->assertSame(0.0, $import->getProgressPercentage());
|
||||
|
||||
$import->setIgnoredCount(3);
|
||||
|
||||
$this->assertEquals(3, $import->getProgressPercentage());
|
||||
|
||||
$import->increaseIgnoredCount()
|
||||
->increaseIgnoredCount();
|
||||
|
||||
$this->assertEquals(5, $import->getProgressPercentage());
|
||||
}
|
||||
|
||||
public function testStart(): void
|
||||
{
|
||||
$import = $this->initImportEntity();
|
||||
|
||||
$this->assertSame(Import::QUEUED, $import->getStatus());
|
||||
$this->assertNull($import->getDateStarted());
|
||||
|
||||
// Date started will be set when the start is called for the first time.
|
||||
$import->start();
|
||||
|
||||
$startDate = $import->getDateStarted();
|
||||
|
||||
$this->assertSame(Import::IN_PROGRESS, $import->getStatus());
|
||||
|
||||
// But the date started will not change when started for the second time.
|
||||
$import->end(false);
|
||||
$import->start();
|
||||
|
||||
$this->assertSame($startDate, $import->getDateStarted());
|
||||
}
|
||||
|
||||
public function testEnd(): void
|
||||
{
|
||||
$import = $this->initImportEntity();
|
||||
|
||||
$this->assertSame(Import::QUEUED, $import->getStatus());
|
||||
$this->assertNull($import->getDateEnded());
|
||||
|
||||
$import->start()->end(false);
|
||||
|
||||
$this->assertSame(Import::IMPORTED, $import->getStatus());
|
||||
$this->assertNotNull($import->getDateEnded());
|
||||
}
|
||||
|
||||
public function testGetRunTime(): void
|
||||
{
|
||||
$import = $this->initImportEntity()->start();
|
||||
|
||||
$this->assertNull($import->getRunTime());
|
||||
|
||||
$import->end(false);
|
||||
|
||||
$this->fakeImportStartDate($import, 10 * 60);
|
||||
|
||||
$this->assertTrue($import->getRunTime() instanceof \DateInterval);
|
||||
$this->assertSame(10, $import->getRunTime()->i);
|
||||
}
|
||||
|
||||
public function testGetRunTimeSeconds(): void
|
||||
{
|
||||
$import = $this->initImportEntity()->start();
|
||||
|
||||
$this->assertSame(0, $import->getRunTimeSeconds());
|
||||
|
||||
$import->end(false);
|
||||
|
||||
$this->fakeImportStartDate($import, 600);
|
||||
|
||||
$this->assertSame(600, $import->getRunTimeSeconds());
|
||||
}
|
||||
|
||||
public function testGetSpeed(): void
|
||||
{
|
||||
$import = $this->initImportEntity()->start();
|
||||
|
||||
$this->assertSame(0.0, $import->getSpeed());
|
||||
|
||||
$import->setInsertedCount(900);
|
||||
$import->end(false);
|
||||
|
||||
$this->fakeImportStartDate($import, 600);
|
||||
|
||||
$this->assertSame(1.5, $import->getSpeed());
|
||||
}
|
||||
|
||||
public function testGetSpeedWhenRunTimeIsUnderOneSecond(): void
|
||||
{
|
||||
$import = $this->initImportEntity()->start();
|
||||
|
||||
$this->assertSame(0.0, $import->getSpeed());
|
||||
|
||||
$import->setInsertedCount(3);
|
||||
$import->end(false);
|
||||
|
||||
$this->assertSame(3.0, $import->getSpeed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake the start date to the past to emulate that the import runs for a while.
|
||||
*
|
||||
* @param int $runtime in seconds
|
||||
*/
|
||||
protected function fakeImportStartDate(Import $import, $runtime = 600)
|
||||
{
|
||||
$dateEnded = $import->getDateEnded();
|
||||
$dateStarted = new \DateTime($dateEnded->format('Y-m-d H:i:s.u'), $dateEnded->getTimezone());
|
||||
$dateStarted->modify('-'.$runtime.' seconds');
|
||||
$import->setDateStarted($dateStarted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
|
||||
#[\PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses]
|
||||
class LeadCategoryRepositoryFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $categoryFlags = [
|
||||
'one' => true,
|
||||
'two' => false,
|
||||
'three' => true,
|
||||
];
|
||||
|
||||
private LeadModel $model;
|
||||
|
||||
private Lead $lead;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $categories;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->model = self::getContainer()->get('mautic.lead.model.lead');
|
||||
$this->lead = $this->createLead('John', 'Doe', 'john@doe.com');
|
||||
|
||||
// Add three categories to the lead.
|
||||
// Subscribed 2
|
||||
// Unsubscribed 1
|
||||
$this->categories = $this->createCategories();
|
||||
$this->setLeadCategories($this->lead, $this->categories);
|
||||
}
|
||||
|
||||
protected function beforeBeginTransaction(): void
|
||||
{
|
||||
$this->resetAutoincrement(['categories', 'lead_categories']);
|
||||
}
|
||||
|
||||
public function testCategoriesOnContactPreferences(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/contactFrequency/'.$this->lead->getId());
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
$subscribedCats = $crawler->filter('select[id="lead_contact_frequency_rules_global_categories"]')->filter('option[selected="selected"]');
|
||||
|
||||
$this->assertCount(2, $subscribedCats, $crawler->html());
|
||||
}
|
||||
|
||||
public function testCategoriesOnContactPreferencesWhenNewCategoryIsAdded(): void
|
||||
{
|
||||
// Add new category, there is no association with the lead.
|
||||
$this->createCategory('Category extra', 'cat-extra');
|
||||
|
||||
// Request the preference on Lead detail page.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/contactFrequency/'.$this->lead->getId());
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
$subscribedCats = $crawler->filter('select[id="lead_contact_frequency_rules_global_categories"]')->filter('option[selected="selected"]');
|
||||
|
||||
// The count should be 3 as we have two subscribed and a new one.
|
||||
$this->assertCount(3, $subscribedCats);
|
||||
|
||||
// The association count is 2.
|
||||
$this->assertCount(2, $this->model->getLeadCategories($this->lead));
|
||||
|
||||
// Now submit the preference form.
|
||||
$form = $crawler->filterXPath('//form[@name="lead_contact_frequency_rules"]')->form();
|
||||
$this->client->submit($form);
|
||||
|
||||
// The association count is bumped up to three.
|
||||
$this->assertCount(3, $this->model->getLeadCategories($this->lead));
|
||||
}
|
||||
|
||||
public function testCategoriesOnContactPreferencesWhenNewCategoryIsAddedAndNotSubscribed(): void
|
||||
{
|
||||
$categoryExtra = $this->createCategory('Category extra', 'cat-extra');
|
||||
|
||||
$crawler = $this->getContactFrequencyCrawler($this->lead);
|
||||
|
||||
$subscribedCats = $crawler->filter('select[id="lead_contact_frequency_rules_global_categories"]')->filter('option[selected="selected"]');
|
||||
|
||||
// The count should be 3 as we have two subscribed and a new one.
|
||||
$this->assertCount(3, $subscribedCats);
|
||||
|
||||
// The association count is 2.
|
||||
$this->assertCount(2, $this->model->getLeadCategories($this->lead));
|
||||
|
||||
// Now submit the preference form.
|
||||
$form = $crawler->filterXPath('//form[@name="lead_contact_frequency_rules"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'lead_contact_frequency_rules[global_categories]' => [
|
||||
$this->categories['one']->getId(),
|
||||
$this->categories['three']->getId(),
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->client->submit($form);
|
||||
|
||||
$subscribed = $this->model->getLeadCategories($this->lead);
|
||||
$unSubscribed = $this->model->getUnsubscribedLeadCategoriesIds($this->lead);
|
||||
|
||||
$this->assertCount(2, $subscribed);
|
||||
$this->assertCount(2, $unSubscribed);
|
||||
|
||||
$this->assertArrayHasKey($this->categories['one']->getId(), $subscribed);
|
||||
$this->assertArrayHasKey($this->categories['three']->getId(), $subscribed);
|
||||
|
||||
$this->assertArrayHasKey($this->categories['two']->getId(), $unSubscribed);
|
||||
$this->assertArrayHasKey($categoryExtra->getId(), $unSubscribed);
|
||||
}
|
||||
|
||||
public function testCategoriesOnContactPreferencesWhenNewCategoryIsAddedAndExistingUnsbscribed(): void
|
||||
{
|
||||
// Add new category, there is no association with the lead.
|
||||
$categoryExtra = $this->createCategory('Category extra', 'cat-extra');
|
||||
|
||||
$crawler = $this->getContactFrequencyCrawler($this->lead);
|
||||
|
||||
$subscribedCats = $crawler->filter('select[id="lead_contact_frequency_rules_global_categories"]')->filter('option[selected="selected"]');
|
||||
|
||||
// The count should be 3 as we have two subscribed and a new one.
|
||||
$this->assertCount(3, $subscribedCats);
|
||||
|
||||
// The association count is 2.
|
||||
$this->assertCount(2, $this->model->getLeadCategories($this->lead));
|
||||
|
||||
// Now submit the preference form.
|
||||
$form = $crawler->filterXPath('//form[@name="lead_contact_frequency_rules"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'lead_contact_frequency_rules[global_categories]' => [
|
||||
$categoryExtra->getId(),
|
||||
$this->categories['three']->getId(),
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->client->submit($form);
|
||||
|
||||
$subscribed = $this->model->getLeadCategories($this->lead);
|
||||
$unSubscribed = $this->model->getUnsubscribedLeadCategoriesIds($this->lead);
|
||||
|
||||
$this->assertArrayHasKey($this->categories['three']->getId(), $subscribed);
|
||||
$this->assertArrayHasKey($categoryExtra->getId(), $subscribed);
|
||||
|
||||
$this->assertArrayHasKey($this->categories['one']->getId(), $unSubscribed);
|
||||
$this->assertArrayHasKey($this->categories['two']->getId(), $unSubscribed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function createCategories(): array
|
||||
{
|
||||
$categories = [];
|
||||
foreach ($this->categoryFlags as $suffix => $name) {
|
||||
$categories[$suffix] = $this->createCategory('Category '.$suffix, 'category '.$suffix);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $categories
|
||||
*/
|
||||
private function setLeadCategories(Lead $lead, array $categories): void
|
||||
{
|
||||
foreach ($this->categoryFlags as $key => $flag) {
|
||||
$this->createLeadCategory($lead, $categories[$key], $flag);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function getContactFrequencyCrawler(Lead $lead): Crawler
|
||||
{
|
||||
// Request the preference on Lead detail page.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/contactFrequency/'.$lead->getId());
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
return $crawler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadDevice;
|
||||
use Mautic\LeadBundle\Entity\LeadDeviceRepository;
|
||||
|
||||
class LeadDeviceRepositoryTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testFindExistingDevice(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setEmail('test@example.com');
|
||||
|
||||
$this->em->persist($lead);
|
||||
|
||||
$initialDevice = new LeadDevice();
|
||||
$initialDevice->setDateAdded(new \DateTime());
|
||||
$initialDevice->setLead($lead);
|
||||
$initialDevice->setClientInfo('client info');
|
||||
$initialDevice->setDevice('device');
|
||||
$initialDevice->setDeviceBrand('device brand');
|
||||
$initialDevice->setDeviceModel('device model');
|
||||
$initialDevice->setDeviceOs([
|
||||
'name' => 'GNU/Linux',
|
||||
'short_name' => 'LIN',
|
||||
]);
|
||||
|
||||
$this->em->persist($initialDevice);
|
||||
$this->em->flush();
|
||||
|
||||
$newDevice = new LeadDevice();
|
||||
$newDevice->setDateAdded(new \DateTime());
|
||||
$newDevice->setLead($lead);
|
||||
$newDevice->setClientInfo('client info');
|
||||
$newDevice->setDevice('device');
|
||||
$newDevice->setDeviceBrand('device brand');
|
||||
$newDevice->setDeviceModel('device model');
|
||||
$newDevice->setDeviceOs([
|
||||
'name' => 'Windows',
|
||||
'short_name' => 'WIN',
|
||||
]);
|
||||
|
||||
/** @var LeadDeviceRepository $leadDeviceRepository */
|
||||
$leadDeviceRepository = $this->em->getRepository(LeadDevice::class);
|
||||
$existingDevice = $leadDeviceRepository->findExistingDevice($newDevice);
|
||||
|
||||
// Using assertTrue instead of assertNull to reduce test output
|
||||
$this->assertTrue(
|
||||
null === $existingDevice,
|
||||
'The existing device should be null because the device has a different OS.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
|
||||
class LeadFieldRepositoryFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
public function testCompareValueEqualsOperator(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('John');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$repository = static::getContainer()->get('mautic.lead.model.field')->getRepository();
|
||||
|
||||
$this->assertTrue($repository->compareValue($lead->getId(), 'firstname', 'John', 'eq'));
|
||||
$this->assertFalse($repository->compareValue($lead->getId(), 'firstname', 'Jack', 'eq'));
|
||||
}
|
||||
|
||||
public function testCompareValueNotEqualsOperator(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Ada');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$repository = static::getContainer()->get('mautic.lead.model.field')->getRepository();
|
||||
|
||||
$this->assertTrue($repository->compareValue($lead->getId(), 'firstname', 'Annie', 'neq'));
|
||||
$this->assertFalse($repository->compareValue($lead->getId(), 'firstname', 'Ada', 'neq'));
|
||||
}
|
||||
|
||||
public function testCompareValueEmptyOperator(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Ada');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$repository = static::getContainer()->get('mautic.lead.model.field')->getRepository();
|
||||
|
||||
$this->assertTrue($repository->compareValue($lead->getId(), 'lastname', null, 'empty'));
|
||||
$this->assertFalse($repository->compareValue($lead->getId(), 'firstname', null, 'empty'));
|
||||
}
|
||||
|
||||
public function testCompareValueNotEmptyOperator(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Ada');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$repository = static::getContainer()->get('mautic.lead.model.field')->getRepository();
|
||||
|
||||
$this->assertTrue($repository->compareValue($lead->getId(), 'firstname', null, 'notEmpty'));
|
||||
$this->assertFalse($repository->compareValue($lead->getId(), 'lastname', null, 'notEmpty'));
|
||||
}
|
||||
|
||||
public function testCompareValueStartsWithOperator(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setEmail('MaryWNevarez@armyspy.com');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$repository = static::getContainer()->get('mautic.lead.model.field')->getRepository();
|
||||
|
||||
$this->assertTrue($repository->compareValue($lead->getId(), 'email', 'Mary', 'startsWith'));
|
||||
$this->assertFalse($repository->compareValue($lead->getId(), 'email', 'Unicorn', 'startsWith'));
|
||||
}
|
||||
|
||||
public function testCompareValueEndWithOperator(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setEmail('MaryWNevarez@armyspy.com');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$repository = static::getContainer()->get('mautic.lead.model.field')->getRepository();
|
||||
|
||||
$this->assertTrue($repository->compareValue($lead->getId(), 'email', 'armyspy.com', 'endsWith'));
|
||||
$this->assertFalse($repository->compareValue($lead->getId(), 'email', 'Unicorn', 'endsWith'));
|
||||
}
|
||||
|
||||
public function testCompareValueContainsOperator(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setEmail('MaryWNevarez@armyspy.com');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$repository = static::getContainer()->get('mautic.lead.model.field')->getRepository();
|
||||
|
||||
$this->assertTrue($repository->compareValue($lead->getId(), 'email', 'Nevarez', 'contains'));
|
||||
$this->assertFalse($repository->compareValue($lead->getId(), 'email', 'Unicorn', 'contains'));
|
||||
}
|
||||
|
||||
public function testCompareValueInOperator(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setCountry('United Kingdom');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$repository = static::getContainer()->get('mautic.lead.model.field')->getRepository();
|
||||
|
||||
$this->assertTrue($repository->compareValue($lead->getId(), 'country', ['United Kingdom', 'South Africa'], 'in'));
|
||||
$this->assertFalse($repository->compareValue($lead->getId(), 'country', ['Poland', 'Canada'], 'in'));
|
||||
}
|
||||
|
||||
public function testCompareValueNotInOperator(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setCountry('United Kingdom');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$repository = static::getContainer()->get('mautic.lead.model.field')->getRepository();
|
||||
|
||||
$this->assertTrue($repository->compareValue($lead->getId(), 'country', ['Australia', 'Poland'], 'notIn'));
|
||||
$this->assertFalse($repository->compareValue($lead->getId(), 'country', ['United Kingdom'], 'notIn'));
|
||||
}
|
||||
|
||||
public function testCompareValueInOperatorWithMultiselectField(): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$field->setType('multiselect');
|
||||
$field->setObject('lead');
|
||||
$field->setAlias('colors');
|
||||
$field->setName('Colors');
|
||||
$field->setProperties(
|
||||
[
|
||||
'list' => [
|
||||
[
|
||||
'label' => 'Red',
|
||||
'value' => 'red',
|
||||
], [
|
||||
'label' => 'Green',
|
||||
'value' => 'green',
|
||||
], [
|
||||
'label' => 'Blue',
|
||||
'value' => 'blue',
|
||||
], [
|
||||
'label' => 'Yellow',
|
||||
'value' => 'yellow',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$fieldModel = self::getContainer()->get(FieldModel::class);
|
||||
\assert($fieldModel instanceof FieldModel);
|
||||
$fieldModel->saveEntity($field);
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->addUpdatedField('colors', 'green|blue');
|
||||
$contactModel = self::getContainer()->get(LeadModel::class);
|
||||
\assert($contactModel instanceof LeadModel);
|
||||
|
||||
$contactModel->saveEntity($lead);
|
||||
$repository = $fieldModel->getRepository();
|
||||
|
||||
$this->assertTrue($repository->compareValue($lead->getId(), 'colors', ['green', 'blue'], 'in'));
|
||||
$this->assertFalse($repository->compareValue($lead->getId(), 'colors', ['red', 'yellow'], 'in'));
|
||||
}
|
||||
|
||||
public function testExcludeUnpublishedField(): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$field->setType('text');
|
||||
$field->setObject('lead');
|
||||
$field->setAlias('colors');
|
||||
$field->setName('Colors');
|
||||
$field->setIsPublished(false);
|
||||
|
||||
$fieldModel = self::getContainer()->get(FieldModel::class);
|
||||
$fieldModel->saveEntity($field);
|
||||
$repository = $fieldModel->getRepository();
|
||||
$allLeadFields = $repository->getFieldsForObject('lead');
|
||||
$colorFieldExist = false;
|
||||
if (!empty($allLeadFields)) {
|
||||
foreach ($allLeadFields as $field) {
|
||||
if ('colors' == $field->getAlias()) {
|
||||
$colorFieldExist = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->assertFalse($colorFieldExist);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder as OrmQueryBuilder;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class LeadFieldRepositoryTest extends TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
private LeadFieldRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->repository = $this->configureRepository(LeadField::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and configures mock objects for date value comparison tests.
|
||||
*
|
||||
* @return array{
|
||||
* builderAlias: MockObject&QueryBuilder,
|
||||
* builderCompare: MockObject&QueryBuilder,
|
||||
* statementAliasResult: MockObject&Result,
|
||||
* statementCompareResult: MockObject&Result,
|
||||
* exprCompare: MockObject&ExpressionBuilder
|
||||
* }
|
||||
*/
|
||||
private function createDateValueComparisonMocks(): array
|
||||
{
|
||||
$builderAlias = $this->createMock(QueryBuilder::class);
|
||||
$builderCompare = $this->createMock(QueryBuilder::class);
|
||||
$statementAliasResult = $this->createMock(Result::class);
|
||||
$statementCompareResult = $this->createMock(Result::class);
|
||||
$exprCompare = $this->createMock(ExpressionBuilder::class);
|
||||
|
||||
$this->entityManager->method('getConnection')->willReturn($this->connection);
|
||||
$builderAlias->method('expr')->willReturn(new ExpressionBuilder($this->connection));
|
||||
$builderCompare->method('expr')->willReturn($exprCompare);
|
||||
|
||||
$this->connection->expects($this->exactly(2))
|
||||
->method('createQueryBuilder')
|
||||
->willReturnOnConsecutiveCalls($builderCompare, $builderAlias);
|
||||
|
||||
return [
|
||||
'builderAlias' => $builderAlias,
|
||||
'builderCompare' => $builderCompare,
|
||||
'statementAliasResult' => $statementAliasResult,
|
||||
'statementCompareResult' => $statementCompareResult,
|
||||
'exprCompare' => $exprCompare,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up expectations for the alias query builder.
|
||||
*/
|
||||
private function setupAliasQueryBuilderExpectations(
|
||||
MockObject $builderAlias,
|
||||
MockObject $statementAliasResult,
|
||||
): void {
|
||||
$builderAlias->expects($this->once())
|
||||
->method('select')
|
||||
->with('f.alias, f.is_unique_identifer as is_unique, f.type, f.object')
|
||||
->willReturnSelf();
|
||||
|
||||
$builderAlias->expects($this->once())
|
||||
->method('from')
|
||||
->with(MAUTIC_TABLE_PREFIX.'lead_fields', 'f')
|
||||
->willReturnSelf();
|
||||
|
||||
$builderAlias->expects($this->once())
|
||||
->method('where')
|
||||
->willReturnSelf();
|
||||
|
||||
$builderAlias->expects($this->once())
|
||||
->method('setParameter')
|
||||
->with('object', 'company')
|
||||
->willReturnSelf();
|
||||
|
||||
$builderAlias->expects($this->once())
|
||||
->method('orderBy')
|
||||
->with('f.field_order', 'ASC')
|
||||
->willReturnSelf();
|
||||
|
||||
$builderAlias->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->willReturn($statementAliasResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up expectations for the compare query builder for contact fields.
|
||||
*/
|
||||
private function setupCompareQueryBuilderForContactField(
|
||||
MockObject $builderCompare,
|
||||
MockObject $exprCompare,
|
||||
MockObject $statementCompareResult,
|
||||
int $contactId,
|
||||
string $fieldAlias,
|
||||
?string $value = null,
|
||||
): void {
|
||||
$exprCompare->expects($this->exactly(null !== $value ? 2 : 1))
|
||||
->method('eq')
|
||||
->willReturnCallback(function (...$parameters) use ($fieldAlias, $value) {
|
||||
static $invocationCount = 0;
|
||||
++$invocationCount;
|
||||
|
||||
if (1 === $invocationCount) {
|
||||
$this->assertSame('l.id', $parameters[0]);
|
||||
$this->assertSame(':lead', $parameters[1]);
|
||||
}
|
||||
if (2 === $invocationCount && null !== $value) {
|
||||
$this->assertSame("l.{$fieldAlias}", $parameters[0]);
|
||||
$this->assertSame(':value', $parameters[1]);
|
||||
}
|
||||
});
|
||||
|
||||
$builderCompare->expects($this->once())
|
||||
->method('select')
|
||||
->with('l.id')
|
||||
->willReturnSelf();
|
||||
|
||||
$builderCompare->expects($this->once())
|
||||
->method('from')
|
||||
->with(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->willReturnSelf();
|
||||
|
||||
$builderCompare->expects($this->once())
|
||||
->method('where')
|
||||
->willReturnSelf();
|
||||
|
||||
$parameterCount = null !== $value ? 2 : 1;
|
||||
$builderCompare->expects($this->exactly($parameterCount))
|
||||
->method('setParameter')
|
||||
->willReturnCallback(function (...$parameters) use ($contactId, $value, $builderCompare) {
|
||||
static $invocationCount = 0;
|
||||
++$invocationCount;
|
||||
|
||||
if (1 === $invocationCount) {
|
||||
$this->assertSame('lead', $parameters[0]);
|
||||
$this->assertSame($contactId, $parameters[1]);
|
||||
}
|
||||
if (2 === $invocationCount && null !== $value) {
|
||||
$this->assertSame('value', $parameters[0]);
|
||||
$this->assertSame($value, $parameters[1]);
|
||||
}
|
||||
|
||||
return $builderCompare;
|
||||
});
|
||||
|
||||
$builderCompare->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->willReturn($statementCompareResult);
|
||||
}
|
||||
|
||||
public function testCompareDateValueForContactField(): void
|
||||
{
|
||||
$contactId = 12;
|
||||
$fieldAlias = 'date_field';
|
||||
$value = '2019-04-30';
|
||||
|
||||
$mocks = $this->createDateValueComparisonMocks();
|
||||
|
||||
$this->setupAliasQueryBuilderExpectations(
|
||||
$mocks['builderAlias'],
|
||||
$mocks['statementAliasResult']
|
||||
);
|
||||
|
||||
// No company column found. Therefore it's a contact field.
|
||||
$mocks['statementAliasResult']->expects($this->once())
|
||||
->method('fetchAllAssociative')
|
||||
->willReturn([]);
|
||||
|
||||
$this->setupCompareQueryBuilderForContactField(
|
||||
$mocks['builderCompare'],
|
||||
$mocks['exprCompare'],
|
||||
$mocks['statementCompareResult'],
|
||||
$contactId,
|
||||
$fieldAlias,
|
||||
$value
|
||||
);
|
||||
|
||||
// No contact ID was found by the value so the result should be false.
|
||||
$mocks['statementCompareResult']->expects($this->once())
|
||||
->method('fetchAssociative')
|
||||
->willReturn([]);
|
||||
|
||||
$this->assertFalse($this->repository->compareDateValue($contactId, $fieldAlias, $value));
|
||||
}
|
||||
|
||||
public function testCompareDateValueForCompanyField(): void
|
||||
{
|
||||
$contactId = 12;
|
||||
$fieldAlias = 'date_field';
|
||||
$value = '2019-04-30';
|
||||
|
||||
$mocks = $this->createDateValueComparisonMocks();
|
||||
|
||||
$this->setupAliasQueryBuilderExpectations(
|
||||
$mocks['builderAlias'],
|
||||
$mocks['statementAliasResult']
|
||||
);
|
||||
|
||||
// A company column found. Therefore it's a company field.
|
||||
$mocks['statementAliasResult']->expects($this->once())
|
||||
->method('fetchAllAssociative')
|
||||
->willReturn([['alias' => $fieldAlias]]);
|
||||
|
||||
$matcher = $this->exactly(2);
|
||||
$mocks['exprCompare']->expects($matcher)
|
||||
->method('eq')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('l.id', $parameters[0]);
|
||||
$this->assertSame(':lead', $parameters[1]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('company.date_field', $parameters[0]);
|
||||
$this->assertSame(':value', $parameters[1]);
|
||||
}
|
||||
});
|
||||
|
||||
$matcher = $this->exactly(2);
|
||||
$mocks['builderCompare']->expects($matcher)
|
||||
->method('leftJoin')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('l', $parameters[0]);
|
||||
$this->assertSame(MAUTIC_TABLE_PREFIX.'companies_leads', $parameters[1]);
|
||||
$this->assertSame('companies_lead', $parameters[2]);
|
||||
$this->assertSame('l.id = companies_lead.lead_id', $parameters[3]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('companies_lead', $parameters[0]);
|
||||
$this->assertSame(MAUTIC_TABLE_PREFIX.'companies', $parameters[1]);
|
||||
$this->assertSame('company', $parameters[2]);
|
||||
$this->assertSame('companies_lead.company_id = company.id', $parameters[3]);
|
||||
}
|
||||
});
|
||||
|
||||
$mocks['builderCompare']->expects($this->once())
|
||||
->method('select')
|
||||
->with('l.id')
|
||||
->willReturnSelf();
|
||||
|
||||
$mocks['builderCompare']->expects($this->once())
|
||||
->method('from')
|
||||
->with(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->willReturnSelf();
|
||||
|
||||
$mocks['builderCompare']->expects($this->once())
|
||||
->method('where')
|
||||
->willReturnSelf();
|
||||
|
||||
$matcher = $this->exactly(2);
|
||||
$mocks['builderCompare']->expects($matcher)
|
||||
->method('setParameter')->willReturnCallback(function (...$parameters) use ($matcher, $contactId, $value, $mocks) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('lead', $parameters[0]);
|
||||
$this->assertSame($contactId, $parameters[1]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('value', $parameters[0]);
|
||||
$this->assertSame($value, $parameters[1]);
|
||||
}
|
||||
|
||||
return $mocks['builderCompare'];
|
||||
});
|
||||
|
||||
$mocks['builderCompare']->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->willReturn($mocks['statementCompareResult']);
|
||||
|
||||
// A contact ID was found by the value so the result should be true.
|
||||
$mocks['statementCompareResult']->expects($this->once())
|
||||
->method('fetchAssociative')
|
||||
->willReturn(['id' => 456]);
|
||||
|
||||
$this->assertTrue($this->repository->compareDateValue($contactId, $fieldAlias, $value));
|
||||
}
|
||||
|
||||
public function testGetListablePublishedFields(): void
|
||||
{
|
||||
$query = $this->createQueryMock();
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('createQuery')
|
||||
->with('SELECT f FROM f INDEX BY f.id WHERE f.isListable = 1 AND f.isPublished = 1 ORDER BY f.object ASC')
|
||||
->willReturn($query);
|
||||
|
||||
$query->method('execute')->willReturn([]);
|
||||
|
||||
$this->repository->getListablePublishedFields();
|
||||
}
|
||||
|
||||
public function testGetFieldSchemaData(): void
|
||||
{
|
||||
$query = $this->createQueryMock();
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('createQuery')
|
||||
->with('SELECT f.alias, f.label, f.type, f.isUniqueIdentifer, f.charLengthLimit FROM f INDEX BY f.alias WHERE f.object = :object')
|
||||
->willReturn($query);
|
||||
|
||||
$result = [];
|
||||
$query->method('execute')->willReturn($result);
|
||||
|
||||
$this->assertSame($result, $this->repository->getFieldSchemaData('lead'));
|
||||
}
|
||||
|
||||
public function testGetFieldThatIsMissingColumnWhenMutlipleColumsMissing(): void
|
||||
{
|
||||
$queryBuilder = $this->createMock(OrmQueryBuilder::class);
|
||||
|
||||
$this->entityManager->method('createQueryBuilder')
|
||||
->willReturn($queryBuilder);
|
||||
|
||||
$queryBuilder->expects(self::once())
|
||||
->method('select')
|
||||
->willReturnSelf();
|
||||
|
||||
$queryBuilder->expects(self::once())
|
||||
->method('from')
|
||||
->willReturnSelf();
|
||||
|
||||
$expr = $this->createMock(Query\Expr::class);
|
||||
$queryBuilder->expects(self::once())
|
||||
->method('expr')
|
||||
->willReturn($expr);
|
||||
|
||||
$comparison = $this->createMock(Query\Expr\Comparison::class);
|
||||
$expr->expects(self::once())
|
||||
->method('eq')
|
||||
->willReturn($comparison);
|
||||
|
||||
$queryBuilder->expects(self::once())
|
||||
->method('where')
|
||||
->with($comparison)
|
||||
->willReturnSelf();
|
||||
|
||||
$queryBuilder->expects(self::once())
|
||||
->method('orderBy')
|
||||
->willReturnSelf();
|
||||
|
||||
$queryBuilder->expects(self::once())
|
||||
->method('setMaxResults')
|
||||
->with(1)
|
||||
->willReturnSelf();
|
||||
|
||||
$query = $this->createMock(AbstractQuery::class);
|
||||
$queryBuilder->expects(self::once())
|
||||
->method('getQuery')
|
||||
->willReturn($query);
|
||||
|
||||
$leadField = $this->createMock(LeadField::class);
|
||||
$query->expects(self::once())
|
||||
->method('getOneOrNullResult')
|
||||
->willReturn($leadField);
|
||||
|
||||
self::assertSame(
|
||||
$leadField,
|
||||
$this->repository->getFieldThatIsMissingColumn()
|
||||
);
|
||||
}
|
||||
|
||||
private function createQueryMock(): MockObject
|
||||
{
|
||||
// This is terrible, but the Query class is final and AbstractQuery doesn't have some methods used.
|
||||
$query = $this->getMockBuilder(Query::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods([
|
||||
'setParameters',
|
||||
'getSingleResult',
|
||||
'getSQL',
|
||||
'_doExecute',
|
||||
'execute',
|
||||
'setFirstResult',
|
||||
'setMaxResults',
|
||||
])
|
||||
->getMock();
|
||||
|
||||
$ormBuilder = new OrmQueryBuilder($this->entityManager);
|
||||
$this->entityManager->method('createQueryBuilder')->willReturn($ormBuilder);
|
||||
$this->entityManager->method('createQuery')->willReturn($query);
|
||||
$query->method('setParameters')->willReturnSelf();
|
||||
$query->method('setFirstResult')->willReturnSelf();
|
||||
$query->method('setMaxResults')->willReturnSelf();
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array{0: string, 1: array<string, int>|array<empty>, 2: bool}>
|
||||
*/
|
||||
public static function dataGetEmptyOperators(): iterable
|
||||
{
|
||||
yield ['empty', ['id' => 123], true];
|
||||
yield ['!empty', ['id' => 123], true];
|
||||
yield ['empty', [], false];
|
||||
yield ['!empty', [], false];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int>|array<empty> $returnValue
|
||||
*/
|
||||
#[DataProvider('dataGetEmptyOperators')]
|
||||
public function testCompareEmptyDateValueForContactField(string $operator, array $returnValue, bool $expected): void
|
||||
{
|
||||
$contactId = 12;
|
||||
$fieldAlias = 'date_field';
|
||||
|
||||
$mocks = $this->createDateValueComparisonMocks();
|
||||
|
||||
$this->setupAliasQueryBuilderExpectations(
|
||||
$mocks['builderAlias'],
|
||||
$mocks['statementAlias'] = $mocks['statementAliasResult']
|
||||
);
|
||||
|
||||
// No company column found. Therefore it's a contact field.
|
||||
$mocks['statementAlias']->expects($this->once())
|
||||
->method('fetchAllAssociative')
|
||||
->willReturn([]);
|
||||
|
||||
$mocks['exprCompare']->expects($this->once())
|
||||
->method('eq')
|
||||
->with('l.id', ':lead');
|
||||
|
||||
$operators = [
|
||||
'empty' => 'isNull',
|
||||
'!empty' => 'isNotNull',
|
||||
];
|
||||
|
||||
$mocks['exprCompare']->expects($this->once())
|
||||
->method($operators[$operator])
|
||||
->with('l.date_field');
|
||||
|
||||
$mocks['builderCompare']->expects($this->once())
|
||||
->method('select')
|
||||
->with('l.id')
|
||||
->willReturnSelf();
|
||||
|
||||
$mocks['builderCompare']->expects($this->once())
|
||||
->method('from')
|
||||
->with(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->willReturnSelf();
|
||||
|
||||
$mocks['builderCompare']->expects($this->once())
|
||||
->method('where')
|
||||
->willReturnSelf();
|
||||
|
||||
$mocks['builderCompare']->expects($this->once())
|
||||
->method('setParameter')
|
||||
->with('lead', $contactId)
|
||||
->willReturnSelf();
|
||||
|
||||
$mocks['builderCompare']->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->willReturn($mocks['statementCompare'] = $mocks['statementCompareResult']);
|
||||
|
||||
// No contact ID was found by the value so the result should be false.
|
||||
$mocks['statementCompare']->expects($this->once())
|
||||
->method('fetchAssociative')
|
||||
->willReturn($returnValue);
|
||||
|
||||
$this->assertSame($expected, $this->repository->compareEmptyDateValue($contactId, $fieldAlias, $operator));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
|
||||
class LeadFieldTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testNewEntity(): void
|
||||
{
|
||||
$leadField = new LeadField();
|
||||
|
||||
$this->assertTrue($leadField->isNew());
|
||||
$this->assertFalse($leadField->getColumnIsNotCreated());
|
||||
}
|
||||
|
||||
public function testColumnNotCreatedForPublishedEntity(): void
|
||||
{
|
||||
$leadField = new LeadField();
|
||||
$leadField->setIsPublished(true);
|
||||
|
||||
$this->assertTrue($leadField->getIsPublished());
|
||||
|
||||
$leadField->setColumnIsNotCreated();
|
||||
|
||||
$this->assertFalse($leadField->getIsPublished(), 'Entity cannot be published until column is not created');
|
||||
$this->assertTrue($leadField->getColumnIsNotCreated());
|
||||
|
||||
$leadField->setColumnWasCreated();
|
||||
|
||||
$this->assertTrue($leadField->getIsPublished());
|
||||
$this->assertFalse($leadField->getColumnIsNotCreated());
|
||||
}
|
||||
|
||||
public function testColumnNotCreatedForUnpublishedEntity(): void
|
||||
{
|
||||
$leadField = new LeadField();
|
||||
$leadField->setIsPublished(false);
|
||||
|
||||
$this->assertFalse($leadField->getIsPublished());
|
||||
|
||||
$leadField->setColumnIsNotCreated();
|
||||
|
||||
$this->assertFalse($leadField->getIsPublished());
|
||||
$this->assertTrue($leadField->getColumnIsNotCreated());
|
||||
|
||||
$leadField->setColumnWasCreated();
|
||||
|
||||
$this->assertFalse($leadField->getIsPublished());
|
||||
$this->assertFalse($leadField->getColumnIsNotCreated());
|
||||
}
|
||||
|
||||
public function testEmailCannotBeUnpublished(): void
|
||||
{
|
||||
$leadField = new LeadField();
|
||||
$leadField->setIsPublished(true);
|
||||
|
||||
$this->assertFalse($leadField->disablePublishChange());
|
||||
|
||||
$leadField->setAlias('email');
|
||||
|
||||
$this->assertTrue($leadField->disablePublishChange());
|
||||
}
|
||||
|
||||
public function testCannotBeUnpublishedUntilColumnIsCreated(): void
|
||||
{
|
||||
$leadField = new LeadField();
|
||||
$leadField->setIsPublished(false);
|
||||
|
||||
$this->assertFalse($leadField->disablePublishChange());
|
||||
|
||||
$leadField->setColumnIsNotCreated();
|
||||
|
||||
$this->assertTrue($leadField->disablePublishChange());
|
||||
|
||||
$leadField->setColumnWasCreated();
|
||||
|
||||
$this->assertFalse($leadField->disablePublishChange());
|
||||
}
|
||||
|
||||
public function testClone(): void
|
||||
{
|
||||
$leadField = new LeadField();
|
||||
$leadField->setLabel('Test value for custom field 4');
|
||||
$leadField->setAlias('test_value_for_custom_field_4');
|
||||
|
||||
$clonedField = clone $leadField;
|
||||
|
||||
$this->assertEquals($leadField->getLabel(), $clonedField->getLabel());
|
||||
$this->assertEquals($leadField->getAlias(), $clonedField->getAlias());
|
||||
$this->assertEquals(0, $clonedField->getOrder());
|
||||
$this->assertTrue($clonedField->getIsCloned());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\AbstractMauticTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
|
||||
class LeadListRepositoryFunctionalTest extends AbstractMauticTestCase
|
||||
{
|
||||
public function testCheckLeadSegmentsByIds(): void
|
||||
{
|
||||
$lead = $this->createLead();
|
||||
$segmentA = $this->createSegment();
|
||||
$segmentB = $this->createSegment('B');
|
||||
$segmentC = $this->createSegment('C');
|
||||
$this->createSegmentMember($segmentA, $lead);
|
||||
$this->createSegmentMember($segmentB, $lead, true);
|
||||
|
||||
$leadListRepository = $this->em->getRepository(LeadList::class);
|
||||
|
||||
$result = $leadListRepository->checkLeadSegmentsByIds($lead, [$segmentA->getId()]);
|
||||
$this->assertTrue($result);
|
||||
|
||||
$result = $leadListRepository->checkLeadSegmentsByIds($lead, [$segmentB->getId()]);
|
||||
$this->assertFalse($result);
|
||||
|
||||
$result = $leadListRepository->checkLeadSegmentsByIds($lead, [$segmentC->getId()]);
|
||||
$this->assertFalse($result);
|
||||
|
||||
$result = $leadListRepository->checkLeadSegmentsByIds($lead, [$segmentA->getId(), $segmentB->getId(), $segmentC->getId()]);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
private function createLead(): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Contact');
|
||||
$lead->setEmail('test@test.com');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createSegment(string $suffix = 'A'): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName("Segment $suffix");
|
||||
$segment->setPublicName("Segment $suffix");
|
||||
$segment->setAlias("segment-$suffix");
|
||||
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
protected function createSegmentMember(LeadList $segment, Lead $lead, bool $isManuallyRemoved = false): void
|
||||
{
|
||||
$segmentMember = new ListLead();
|
||||
$segmentMember->setLead($lead);
|
||||
$segmentMember->setList($segment);
|
||||
$segmentMember->setManuallyRemoved($isManuallyRemoved);
|
||||
$segmentMember->setDateAdded(new \DateTime());
|
||||
$this->em->persist($segmentMember);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadListRepository;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
|
||||
class LeadListRepositoryTest extends TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
private LeadListRepository $repository;
|
||||
|
||||
/**
|
||||
* @var QueryBuilder&MockObject
|
||||
*/
|
||||
private MockObject $queryBuilderMock;
|
||||
|
||||
/**
|
||||
* @var Expr&MockObject
|
||||
*/
|
||||
private MockObject $expressionMock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->queryBuilderMock = $this->createMock(QueryBuilder::class);
|
||||
$this->expressionMock = $this->createMock(Expr::class);
|
||||
$this->repository = $this->configureRepository(LeadList::class);
|
||||
}
|
||||
|
||||
public function testIsContactInAnySegmentFalse(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$this->mockIsContactInAnySegment($contactId, []);
|
||||
self::assertFalse($this->repository->isContactInAnySegment($contactId));
|
||||
}
|
||||
|
||||
public function testIsContactInAnySegmentTrue(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$this->mockIsContactInAnySegment($contactId, [1]);
|
||||
self::assertTrue($this->repository->isContactInAnySegment($contactId));
|
||||
}
|
||||
|
||||
public function testIsNotContactInAnySegmentTrue(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$this->mockIsContactInAnySegment($contactId, []);
|
||||
self::assertTrue($this->repository->isNotContactInAnySegment($contactId));
|
||||
}
|
||||
|
||||
public function testIsNotContactInAnySegmentFalse(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$this->mockIsContactInAnySegment($contactId, [1]);
|
||||
self::assertFalse($this->repository->isNotContactInAnySegment($contactId));
|
||||
}
|
||||
|
||||
public function testIsContactInSegmentsNone(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1];
|
||||
$queryResult = [];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertFalse($this->repository->isContactInSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
public function testIsContactInSegmentsOne(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1, 2];
|
||||
$queryResult = [1];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertTrue($this->repository->isContactInSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
public function testIsContactInSegmentsAll(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1, 2];
|
||||
$queryResult = [1, 2];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertTrue($this->repository->isContactInSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
public function testIsNotContactInSegmentsNone(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1];
|
||||
$queryResult = [0];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertTrue($this->repository->isNotContactInSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
public function testIsNotContactInSegmentsOne(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1, 2];
|
||||
$queryResult = [1];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertFalse($this->repository->isNotContactInSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
public function testIsNotContactInSegmentsAll(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1, 2];
|
||||
$queryResult = [1, 2];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertFalse($this->repository->isNotContactInSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
public function testIsContactInAllSegmentsNoSegments(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1];
|
||||
$queryResult = [];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertFalse($this->repository->isContactInAllSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
public function testIsContactInAllSegmentsDoesNotMatch(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1, 2];
|
||||
$queryResult = [1];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertFalse($this->repository->isContactInAllSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
public function testIsContactInAllSegmentsMatches(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1, 2];
|
||||
$queryResult = [1, 2];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertTrue($this->repository->isContactInSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
public function testIsNotContactInAllSegmentsMatches(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1];
|
||||
$queryResult = [];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertTrue($this->repository->isNotContactInAllSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
public function testIsNotContactInAllSegmentsDoesNotMatch(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$expectedSegmentIds = [1, 2];
|
||||
$queryResult = [1, 2];
|
||||
$this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult);
|
||||
self::assertFalse($this->repository->isNotContactInAllSegments($contactId, $expectedSegmentIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $queryResult
|
||||
*/
|
||||
private function mockIsContactInAnySegment(int $contactId, array $queryResult): void
|
||||
{
|
||||
$prefix = MAUTIC_TABLE_PREFIX;
|
||||
$sql = <<<SQL
|
||||
SELECT leadlist_id
|
||||
FROM {$prefix}lead_lists_leads
|
||||
WHERE lead_id = ?
|
||||
AND manually_removed = 0
|
||||
LIMIT 1
|
||||
SQL;
|
||||
$this->connection->expects(self::once())
|
||||
->method('executeQuery')
|
||||
->with($sql, [$contactId], [\PDO::PARAM_INT])
|
||||
->willReturn($this->result);
|
||||
$this->result->expects(self::once())
|
||||
->method('fetchFirstColumn')
|
||||
->willReturn($queryResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $expectedSegmentIds
|
||||
* @param array<int> $queryResult
|
||||
*/
|
||||
private function mockIsContactInSegments(int $contactId, array $expectedSegmentIds, array $queryResult): void
|
||||
{
|
||||
$prefix = MAUTIC_TABLE_PREFIX;
|
||||
$sql = <<<SQL
|
||||
SELECT leadlist_id
|
||||
FROM {$prefix}lead_lists_leads
|
||||
WHERE lead_id = ?
|
||||
AND leadlist_id IN (?)
|
||||
AND manually_removed = 0
|
||||
SQL;
|
||||
$this->connection->expects(self::once())
|
||||
->method('executeQuery')
|
||||
->with(
|
||||
$sql,
|
||||
[$contactId, $expectedSegmentIds],
|
||||
[\PDO::PARAM_INT, ArrayParameterType::INTEGER]
|
||||
)
|
||||
->willReturn($this->result);
|
||||
|
||||
$this->result->expects(self::once())
|
||||
->method('fetchFirstColumn')
|
||||
->willReturn($queryResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function testGetMultipleLeadCounts(): void
|
||||
{
|
||||
$listIds = [765, 766];
|
||||
$counts = [100, 200];
|
||||
|
||||
$queryResult = [
|
||||
[
|
||||
'leadlist_id' => $listIds[0],
|
||||
'thecount' => $counts[0],
|
||||
],
|
||||
[
|
||||
'leadlist_id' => $listIds[1],
|
||||
'thecount' => $counts[1],
|
||||
],
|
||||
];
|
||||
|
||||
$this->mockGetLeadCount($queryResult, false);
|
||||
|
||||
$this->queryBuilderMock->expects(self::once())
|
||||
->method('from')
|
||||
->with(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'l')
|
||||
->willReturnSelf();
|
||||
|
||||
$this->expressionMock->expects(self::once())
|
||||
->method('in')
|
||||
->with('l.leadlist_id', $listIds)
|
||||
->willReturnSelf();
|
||||
|
||||
$this->expressionMock->expects(self::once())
|
||||
->method('eq')
|
||||
->with('l.manually_removed', ':false')
|
||||
->willReturnSelf();
|
||||
|
||||
$this->queryBuilderMock->expects(self::once())
|
||||
->method('setParameter')
|
||||
->with('false', false, 'boolean')
|
||||
->willReturnSelf();
|
||||
|
||||
self::assertSame(array_combine($listIds, $counts), $this->repository->getLeadCount($listIds));
|
||||
}
|
||||
|
||||
public function testGetSingleLeadCount(): void
|
||||
{
|
||||
$listIds = [765];
|
||||
$counts = [100];
|
||||
$queryResult = [
|
||||
[
|
||||
'leadlist_id' => $listIds[0],
|
||||
'thecount' => $counts[0],
|
||||
],
|
||||
];
|
||||
|
||||
$this->mockGetLeadCount($queryResult);
|
||||
|
||||
$fromPart = [
|
||||
[
|
||||
'alias' => 'l',
|
||||
'table' => MAUTIC_TABLE_PREFIX.'lead_lists_leads',
|
||||
],
|
||||
];
|
||||
|
||||
$this->queryBuilderMock->expects(self::once())
|
||||
->method('getQueryPart')
|
||||
->willReturn($fromPart);
|
||||
$matcher = self::exactly(2);
|
||||
|
||||
$this->queryBuilderMock->expects($matcher)
|
||||
->method('from')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(MAUTIC_TABLE_PREFIX.'lead_lists_leads', $parameters[0]);
|
||||
$this->assertSame('l', $parameters[1]);
|
||||
|
||||
return $this->queryBuilderMock;
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(MAUTIC_TABLE_PREFIX.'lead_lists_leads', $parameters[0]);
|
||||
$this->assertSame('l USE INDEX ('.MAUTIC_TABLE_PREFIX.'manually_removed)', $parameters[1]);
|
||||
|
||||
return $this->queryBuilderMock;
|
||||
}
|
||||
});
|
||||
$matcher = self::exactly(2);
|
||||
|
||||
$this->expressionMock->expects($matcher)
|
||||
->method('eq')->willReturnCallback(function (...$parameters) use ($matcher, $listIds) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('l.leadlist_id', $parameters[0]);
|
||||
$this->assertSame($listIds[0], $parameters[1]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('l.manually_removed', $parameters[0]);
|
||||
$this->assertSame(':false', $parameters[1]);
|
||||
}
|
||||
|
||||
return $this->expressionMock;
|
||||
});
|
||||
|
||||
self::assertSame($counts[0], $this->repository->getLeadCount($listIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $queryResult
|
||||
*/
|
||||
private function mockGetLeadCount(array $queryResult, bool $addParam = true): void
|
||||
{
|
||||
$this->connection->method('createQueryBuilder')
|
||||
->willReturn($this->queryBuilderMock);
|
||||
|
||||
$this->queryBuilderMock->expects(self::once())
|
||||
->method('select')
|
||||
->with('count(l.lead_id) as thecount, l.leadlist_id')
|
||||
->willReturnSelf();
|
||||
|
||||
$this->queryBuilderMock->expects(self::exactly(2))
|
||||
->method('expr')
|
||||
->willReturn($this->expressionMock);
|
||||
|
||||
if ($addParam) {
|
||||
$this->queryBuilderMock->expects(self::once())
|
||||
->method('setParameter')
|
||||
->with('false', false, 'boolean')
|
||||
->willReturnSelf();
|
||||
}
|
||||
|
||||
$this->queryBuilderMock->expects(self::once())
|
||||
->method('where')
|
||||
->with($this->expressionMock)
|
||||
->willReturnSelf();
|
||||
|
||||
$this->queryBuilderMock->expects(self::once())
|
||||
->method('executeQuery')
|
||||
->willReturn($this->result);
|
||||
|
||||
$this->result->expects(self::once())
|
||||
->method('fetchAllAssociative')
|
||||
->willReturn($queryResult);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class LeadListTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testAddLegacyParamsWithEmptyFilters(): void
|
||||
{
|
||||
$entity = new LeadList();
|
||||
$this->assertSame([], $entity->getFilters());
|
||||
}
|
||||
|
||||
public function testAddLegacyParamsWithLegacyFilters(): void
|
||||
{
|
||||
$entity = new LeadList();
|
||||
|
||||
$entity->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
$entity->getFilters()
|
||||
);
|
||||
}
|
||||
|
||||
public function testAddLegacyParamsWithNewFilters(): void
|
||||
{
|
||||
$entity = new LeadList();
|
||||
|
||||
$entity->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'filter' => 'Prague',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
'filter' => '4',
|
||||
'display' => 'John Doe',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'filter' => 'Prague',
|
||||
],
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
],
|
||||
],
|
||||
$entity->getFilters()
|
||||
);
|
||||
}
|
||||
|
||||
public function testAddLegacyParamsWithHybridFilters(): void
|
||||
{
|
||||
$entity = new LeadList();
|
||||
|
||||
$entity->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'filter' => 'outdated_id',
|
||||
'display' => 'Outdated Name',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'filter' => '4',
|
||||
'display' => 'John Doe',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
],
|
||||
$entity->getFilters()
|
||||
);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('setIsGlobalDataProvider')]
|
||||
public function testSetIsGlobal($value, $expected, array $changes): void
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setIsGlobal($value);
|
||||
|
||||
Assert::assertSame($expected, $segment->getIsGlobal());
|
||||
Assert::assertSame($changes, $segment->getChanges());
|
||||
}
|
||||
|
||||
public static function setIsGlobalDataProvider(): iterable
|
||||
{
|
||||
yield [null, false, ['isGlobal' => [true, false]]];
|
||||
yield [true, true, []];
|
||||
yield [false, false, ['isGlobal' => [true, false]]];
|
||||
yield ['', false, ['isGlobal' => [true, false]]];
|
||||
yield [0, false, ['isGlobal' => [true, false]]];
|
||||
yield ['string', true, []];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('setIsPreferenceCenterDataProvider')]
|
||||
public function testSetIsPreferenceCenter($value, $expected, array $changes): void
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setIsPreferenceCenter($value);
|
||||
|
||||
Assert::assertSame($expected, $segment->getIsPreferenceCenter());
|
||||
Assert::assertSame($changes, $segment->getChanges());
|
||||
}
|
||||
|
||||
public static function setIsPreferenceCenterDataProvider(): iterable
|
||||
{
|
||||
yield [null, false, []];
|
||||
yield [true, true, ['isPreferenceCenter' => [false, true]]];
|
||||
yield [false, false, []];
|
||||
yield ['', false, []];
|
||||
yield [0, false, []];
|
||||
yield ['string', true, ['isPreferenceCenter' => [false, true]]];
|
||||
}
|
||||
|
||||
public function testFirstFilterGlueIsAlwaysAnd(): void
|
||||
{
|
||||
$entity = new LeadList();
|
||||
$entity->setFilters([
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'filter' => 'Prague',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
'filter' => '4',
|
||||
'display' => 'John Doe',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'filter' => 'Prague',
|
||||
],
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
],
|
||||
],
|
||||
$entity->getFilters(), 'The first filter glue must be "and".');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class LeadRepositoryFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private Lead $lead;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->lead = $this->createLead();
|
||||
}
|
||||
|
||||
public function testPointsAreAdded(): void
|
||||
{
|
||||
$model = static::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$this->lead->adjustPoints(100);
|
||||
|
||||
$model->saveEntity($this->lead);
|
||||
|
||||
$this->assertEquals(200, $this->lead->getPoints());
|
||||
|
||||
$changes = $this->lead->getChanges(true);
|
||||
$this->assertEquals(200, $changes['points'][1]);
|
||||
}
|
||||
|
||||
public function testPointsAreSubtracted(): void
|
||||
{
|
||||
$model = static::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$this->lead->adjustPoints(100, Lead::POINTS_SUBTRACT);
|
||||
|
||||
$model->saveEntity($this->lead);
|
||||
|
||||
$this->assertEquals(0, $this->lead->getPoints());
|
||||
|
||||
$changes = $this->lead->getChanges(true);
|
||||
$this->assertEquals(0, $changes['points'][1]);
|
||||
}
|
||||
|
||||
public function testPointsAreMultiplied(): void
|
||||
{
|
||||
$model = static::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$this->lead->adjustPoints(2, Lead::POINTS_MULTIPLY);
|
||||
|
||||
$model->saveEntity($this->lead);
|
||||
|
||||
$this->assertEquals(200, $this->lead->getPoints());
|
||||
|
||||
$changes = $this->lead->getChanges(true);
|
||||
$this->assertEquals(200, $changes['points'][1]);
|
||||
}
|
||||
|
||||
public function testPointsAreDivided(): void
|
||||
{
|
||||
$model = static::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$this->lead->adjustPoints(2, Lead::POINTS_DIVIDE);
|
||||
|
||||
$model->saveEntity($this->lead);
|
||||
|
||||
$this->assertEquals(50, $this->lead->getPoints());
|
||||
|
||||
$changes = $this->lead->getChanges(true);
|
||||
$this->assertEquals(50, $changes['points'][1]);
|
||||
}
|
||||
|
||||
public function testMixedOperatorPointsAreCalculated(): void
|
||||
{
|
||||
$model = static::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$this->lead->adjustPoints(100, Lead::POINTS_SUBTRACT);
|
||||
$this->lead->adjustPoints(120, Lead::POINTS_ADD);
|
||||
$this->lead->adjustPoints(2, Lead::POINTS_MULTIPLY);
|
||||
$this->lead->adjustPoints(4, Lead::POINTS_DIVIDE);
|
||||
|
||||
$model->saveEntity($this->lead);
|
||||
|
||||
$this->assertEquals(60, $this->lead->getPoints());
|
||||
|
||||
$changes = $this->lead->getChanges(true);
|
||||
$this->assertEquals(60, $changes['points'][1]);
|
||||
}
|
||||
|
||||
public function testMixedModelAndRepositorySavesDoNotDoublePoints(): void
|
||||
{
|
||||
$model = static::getContainer()->get('mautic.lead.model.lead');
|
||||
$this->lead->adjustPoints(120, Lead::POINTS_ADD);
|
||||
$model->saveEntity($this->lead);
|
||||
// Changes should be stored with points
|
||||
$changes = $this->lead->getChanges(true);
|
||||
$this->assertEquals(220, $changes['points'][1]);
|
||||
// Points should now not be in changes
|
||||
$model->saveEntity($this->lead);
|
||||
$changes = $this->lead->getChanges(true);
|
||||
$this->assertFalse(isset($changes['points']));
|
||||
// Points should remain the same
|
||||
$model->saveEntity($this->lead);
|
||||
$this->em->getRepository(Lead::class)->saveEntity($this->lead);
|
||||
$this->assertEquals(220, $this->lead->getPoints());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[]|string $emails
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataForTestAjaxGetLeadsByFieldValue')]
|
||||
public function testAjaxGetLeadsByFieldValue($emails, bool $createFlag, int $expectedCount): void
|
||||
{
|
||||
$this->createLeads($emails, $createFlag);
|
||||
|
||||
$payload = [
|
||||
'action' => 'lead:getLeadIdsByFieldValue',
|
||||
'field' => 'email',
|
||||
'value' => $emails,
|
||||
];
|
||||
|
||||
$this->client->xmlHttpRequest(Request::METHOD_GET, '/s/ajax', $payload);
|
||||
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode(), print_r(json_decode($this->client->getResponse()->getContent(), true), true));
|
||||
$contentArray = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertCount($expectedCount, $contentArray['items']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, int|string|bool|string[]>>
|
||||
*/
|
||||
public static function dataForTestAjaxGetLeadsByFieldValue(): iterable
|
||||
{
|
||||
yield 'Email passed as string with associated contact' => [
|
||||
'john@doe.com', // Email
|
||||
true,
|
||||
1, // Count
|
||||
];
|
||||
|
||||
yield 'Email passed as string without associated contact' => [
|
||||
'john@doe.com', // Email
|
||||
false,
|
||||
0, // Count
|
||||
];
|
||||
|
||||
yield 'Email passed as array with associated contacts' => [
|
||||
['john@doe.com', 'doe@doe.com'], // Email
|
||||
true,
|
||||
2, // Count
|
||||
];
|
||||
|
||||
yield 'Email passed as array without associated contacts' => [
|
||||
['john@doe.com', 'doe@doe.com'], // Email
|
||||
false,
|
||||
0, // Count
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[]|string $emails
|
||||
*/
|
||||
private function createLeads($emails, bool $flag): void
|
||||
{
|
||||
if (!$flag) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_array($emails)) {
|
||||
$emails = [$emails];
|
||||
}
|
||||
|
||||
foreach ($emails as $email) {
|
||||
$this->createLead($email);
|
||||
}
|
||||
}
|
||||
|
||||
public function testIfLeadExists(): void
|
||||
{
|
||||
/** @var LeadRepository $repo */
|
||||
$repo = $this->em->getRepository(Lead::class);
|
||||
|
||||
$this->assertFalse($repo->exists('654'));
|
||||
|
||||
$lead = $this->createLead();
|
||||
|
||||
$this->assertTrue($repo->exists((string) $lead->getId()));
|
||||
}
|
||||
|
||||
private function createLead(string $email = ''): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setPoints(100);
|
||||
|
||||
if ($email) {
|
||||
$lead->setEmail($email);
|
||||
}
|
||||
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
return $lead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Mautic\CoreBundle\Test\Doctrine\DBALMocker;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class LeadRepositoryTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
private LeadRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->repository = $this->configureRepository(Lead::class);
|
||||
}
|
||||
|
||||
public function testBooleanWithPrepareDbalFieldsForSave(): void
|
||||
{
|
||||
$trait = $this->createMock(LeadRepository::class);
|
||||
$fields = [
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'string' => 'blah',
|
||||
];
|
||||
|
||||
$reflection = new \ReflectionObject($trait);
|
||||
$method = $reflection->getMethod('prepareDbalFieldsForSave');
|
||||
$method->setAccessible(true);
|
||||
$method->invokeArgs($trait, [&$fields]);
|
||||
|
||||
$this->assertEquals(1, $fields['true']);
|
||||
$this->assertEquals(0, $fields['false']);
|
||||
$this->assertEquals('blah', $fields['string']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the emails are bound separately as parameters according to
|
||||
* https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/query-builder.html#line-number-0a267d5a2c69797a7656aae33fcc140d16b0a566-72.
|
||||
*/
|
||||
public function testBuildQueryForGetLeadsByFieldValue(): void
|
||||
{
|
||||
$dbalMock = new DBALMocker($this);
|
||||
|
||||
$mock = $this->getMockBuilder(LeadRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['getEntityManager'])
|
||||
->getMock();
|
||||
|
||||
$mock->method('getEntityManager')
|
||||
->willReturn($dbalMock->getMockEm());
|
||||
|
||||
$reflection = new \ReflectionClass(LeadRepository::class);
|
||||
$refMethod = $reflection->getMethod('buildQueryForGetLeadsByFieldValue');
|
||||
$refMethod->setAccessible(true);
|
||||
|
||||
$refMethod->invoke($mock, 'email', ['test@example.com', 'test2@example.com']);
|
||||
|
||||
$parameters = $dbalMock->getQueryPart('parameters');
|
||||
|
||||
$this->assertCount(2, $parameters, 'There should be two parameters bound because that\'s the number of emails we passed into the method.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the array_combine return value matches the old style.
|
||||
*/
|
||||
public function testGetLeadsByFieldValueArrayMapReturn(): void
|
||||
{
|
||||
/** @var MockObject&LeadRepository */
|
||||
$repository = $this->getMockBuilder(LeadRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['getEntities', 'buildQueryForGetLeadsByFieldValue'])
|
||||
->getMock();
|
||||
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('test@example.com');
|
||||
|
||||
$contact2 = new Lead();
|
||||
$contact2->setEmail('test2@example.com');
|
||||
|
||||
$entities = [$contact, $contact2];
|
||||
|
||||
$repository->method('getEntities')->willReturn($entities);
|
||||
$repository->method('buildQueryForGetLeadsByFieldValue')->willReturn(null);
|
||||
|
||||
$contacts = $repository->getLeadsByFieldValue('email', ['test@example.com', 'test2@example.com']);
|
||||
|
||||
$this->assertSame($entities, $contacts, 'When getting leads without indexing by column, it should match the expected result.');
|
||||
|
||||
$contacts = $repository->getLeadsByFieldValue('email', ['test@example.com', 'test2@example.com'], null, true);
|
||||
$expected = ['test@example.com', 'test2@example.com'];
|
||||
|
||||
$this->assertSame($expected, array_keys($contacts), 'When getting leads with indexing by column, it should match the expected result.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that we will join each table with the same alias only once.
|
||||
*/
|
||||
public function testApplySearchQueryRelationshipJoinOnlyOnce(): void
|
||||
{
|
||||
$queryBuilder = new QueryBuilder($this->connection);
|
||||
$queryBuilder->select('*')->from(MAUTIC_TABLE_PREFIX.'table_a');
|
||||
$tableB = [
|
||||
'alias' => 'alias_b',
|
||||
'from_alias' => MAUTIC_TABLE_PREFIX.'table_a',
|
||||
'table' => 'table_b',
|
||||
'condition' => 'condition_b',
|
||||
];
|
||||
$tableC = [
|
||||
'alias' => 'alias_c',
|
||||
'from_alias' => MAUTIC_TABLE_PREFIX.'table_a',
|
||||
'table' => 'table_c',
|
||||
'condition' => 'condition_c',
|
||||
];
|
||||
|
||||
$this->repository->applySearchQueryRelationship(
|
||||
$queryBuilder,
|
||||
[$tableB, $tableC, $tableB], // Sending 2 table B here.
|
||||
true
|
||||
);
|
||||
|
||||
Assert::assertSame(
|
||||
'SELECT * FROM '.MAUTIC_TABLE_PREFIX.'table_a INNER JOIN '.MAUTIC_TABLE_PREFIX.'table_b alias_b ON condition_b INNER JOIN '.MAUTIC_TABLE_PREFIX.'table_c alias_c ON condition_c GROUP BY l.id',
|
||||
$queryBuilder->getSQL()
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetContactIdsByEmails(): void
|
||||
{
|
||||
$emails = [
|
||||
'somebody1@anywhere.com',
|
||||
'somebody2@anywhere.com',
|
||||
];
|
||||
|
||||
$query = $this->createMock(AbstractQuery::class);
|
||||
$query->expects(self::once())
|
||||
->method('setParameter')
|
||||
->with('emails', $emails, ArrayParameterType::STRING)
|
||||
->willReturn($query);
|
||||
$query->expects(self::once())
|
||||
->method('getArrayResult')
|
||||
->willReturn([
|
||||
0 => [
|
||||
'id' => '1',
|
||||
],
|
||||
1 => [
|
||||
'id' => '2',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->entityManager->expects(self::once())
|
||||
->method('createQuery')
|
||||
->willReturn($query);
|
||||
|
||||
self::assertEquals(
|
||||
[1, 2],
|
||||
$this->repository->getContactIdsByEmails($emails)
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetUniqueIdentifiersOperator(): void
|
||||
{
|
||||
$this->repository->setUniqueIdentifiersOperator(CompositeExpression::TYPE_AND);
|
||||
|
||||
$reflection = new \ReflectionClass(LeadRepository::class);
|
||||
$refMethod = $reflection->getMethod('getUniqueIdentifiersWherePart');
|
||||
$refMethod->setAccessible(true);
|
||||
|
||||
$this->assertEquals('andWhere', $refMethod->invoke($this->repository));
|
||||
|
||||
$this->repository->setUniqueIdentifiersOperator(CompositeExpression::TYPE_OR);
|
||||
|
||||
$this->assertEquals('orWhere', $refMethod->invoke($this->repository));
|
||||
}
|
||||
|
||||
public function testUpdateLastActiveWithId(): void
|
||||
{
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('getConnection')
|
||||
->willReturn($this->connection);
|
||||
|
||||
$this->repository->updateLastActive(1);
|
||||
}
|
||||
|
||||
public function testUpdateLastActiveWithNoId(): void
|
||||
{
|
||||
$this->entityManager->expects($this->never())
|
||||
->method('getConnection')
|
||||
->willReturn($this->connection);
|
||||
|
||||
$this->repository->updateLastActive(null); // @phpstan-ignore-line this tests if we provide null instead which actually happens.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Form\RequestTrait;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\FrequencyRule;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLog;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class LeadTest extends TestCase
|
||||
{
|
||||
use RequestTrait;
|
||||
|
||||
public function testPreferredChannels(): void
|
||||
{
|
||||
$frequencyRules = [
|
||||
'channel1' => [
|
||||
'frequencyNumber' => '',
|
||||
'frequencyTime' => '',
|
||||
'preferredChannel' => 0,
|
||||
'pauseFromDate' => '2016-01-01',
|
||||
'pauseToDate' => '2100-01-01',
|
||||
],
|
||||
'channel2' => [
|
||||
'frequencyNumber' => '',
|
||||
'frequencyTime' => '',
|
||||
'preferredChannel' => 1,
|
||||
'pauseFromDate' => '',
|
||||
'pauseToDate' => '',
|
||||
],
|
||||
'channel3' => [
|
||||
'frequencyNumber' => '7',
|
||||
'frequencyTime' => FrequencyRule::TIME_DAY, // 210
|
||||
'preferredChannel' => 0,
|
||||
'pauseFromDate' => '',
|
||||
'pauseToDate' => '',
|
||||
],
|
||||
'channel4' => [
|
||||
'frequencyNumber' => '',
|
||||
'frequencyTime' => '',
|
||||
'preferredChannel' => 0,
|
||||
'pauseFromDate' => '',
|
||||
'pauseToDate' => '',
|
||||
],
|
||||
'channel5' => [
|
||||
'frequencyNumber' => '10',
|
||||
'frequencyTime' => FrequencyRule::TIME_WEEK, // 40
|
||||
'preferredChannel' => 0,
|
||||
'pauseFromDate' => '',
|
||||
'pauseToDate' => '',
|
||||
],
|
||||
'channel6' => [
|
||||
'frequencyNumber' => 10,
|
||||
'frequencyTime' => FrequencyRule::TIME_MONTH, // 10
|
||||
'preferredChannel' => 0,
|
||||
'pauseFromDate' => '',
|
||||
'pauseToDate' => '',
|
||||
],
|
||||
];
|
||||
|
||||
$lead = new Lead();
|
||||
|
||||
foreach ($frequencyRules as $channel => $rule) {
|
||||
$frequencyRule = (new FrequencyRule())
|
||||
->setPreferredChannel($rule['preferredChannel'])
|
||||
->setFrequencyNumber($rule['frequencyNumber'])
|
||||
->setFrequencyTime($rule['frequencyTime'])
|
||||
->setChannel($channel)
|
||||
->setPauseFromDate(($rule['pauseFromDate']) ? new \DateTime($rule['pauseFromDate']) : null)
|
||||
->setPauseToDate(($rule['pauseToDate']) ? new \DateTime($rule['pauseToDate']) : null);
|
||||
|
||||
$lead->addFrequencyRule($frequencyRule);
|
||||
}
|
||||
|
||||
$dnc = (new DoNotContact())->setChannel('channel4');
|
||||
$lead->addDoNotContactEntry($dnc);
|
||||
|
||||
$channelRules = Lead::generateChannelRules($lead->getFrequencyRules()->toArray(), $lead->getDoNotContact()->toArray());
|
||||
$this->assertEquals(['channel2', 'channel3', 'channel5', 'channel6', 'channel1', 'channel4'], array_keys($channelRules));
|
||||
}
|
||||
|
||||
public function testAdjustPoints(): void
|
||||
{
|
||||
// new lead
|
||||
$this->adjustPointsTest(5, $this->getLeadChangedArray(0, 5), new Lead());
|
||||
$this->adjustPointsTest(5, $this->getLeadChangedArray(0, 5), new Lead(), 'plus');
|
||||
$this->adjustPointsTest(5, $this->getLeadChangedArray(0, -5), new Lead(), 'minus');
|
||||
$this->adjustPointsTest(5, [], new Lead(), 'times');
|
||||
$this->adjustPointsTest(5, [], new Lead(), 'divide');
|
||||
|
||||
// existing lead
|
||||
$lead = new Lead();
|
||||
$lead->setPoints(5);
|
||||
|
||||
$this->adjustPointsTest(5, $this->getLeadChangedArray(5, 10), $lead);
|
||||
$this->adjustPointsTest(5, $this->getLeadChangedArray(10, 15), $lead);
|
||||
$this->adjustPointsTest(10, $this->getLeadChangedArray(15, 150), $lead, 'times');
|
||||
$this->adjustPointsTest(10, $this->getLeadChangedArray(150, 15), $lead, 'divide');
|
||||
}
|
||||
|
||||
public function testCustomFieldGetterSetters(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
|
||||
$fields = [
|
||||
'core' => [
|
||||
'notes' => [
|
||||
'alias' => 'notes',
|
||||
'label' => 'Notes',
|
||||
'type' => 'textarea',
|
||||
'value' => 'Blah blah blah',
|
||||
],
|
||||
'test' => [
|
||||
'alias' => 'test',
|
||||
'label' => 'Test',
|
||||
'type' => 'textarea',
|
||||
'value' => 'Test blah',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$lead->setFields($fields);
|
||||
|
||||
// This should not killover with a segmentation fault due to a loop
|
||||
$lead->setNotes('hello');
|
||||
|
||||
// Not using getNotes because it conflicts with an existing method and not sure what to do about that yet
|
||||
$lead->setTest('hello');
|
||||
$this->assertEquals('hello', $lead->getTest());
|
||||
}
|
||||
|
||||
public function testDataIsCleanedCorrectly(): void
|
||||
{
|
||||
$fields = [
|
||||
'core' => [
|
||||
'boolean' => [
|
||||
'alias' => 'boolean',
|
||||
'label' => 'Boolean',
|
||||
'type' => 'boolean',
|
||||
'value' => false,
|
||||
],
|
||||
'dateField' => [
|
||||
'alias' => 'dateField',
|
||||
'label' => 'Date Time',
|
||||
'type' => 'datetime',
|
||||
'value' => '12-12-2017 23:00:00',
|
||||
],
|
||||
'multiselect' => [
|
||||
'alias' => 'multi',
|
||||
'label' => 'Multi Select',
|
||||
'type' => 'multiselect',
|
||||
'value' => ['a', 'b', 'c'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$data = [
|
||||
'boolean' => 'yes',
|
||||
'dateField' => '12-12-2017 22:03:59',
|
||||
'multi' => 'a|b',
|
||||
];
|
||||
|
||||
$this->cleanFields($data, $fields['core']['boolean']);
|
||||
|
||||
$this->cleanFields($data, $fields['core']['dateField']);
|
||||
|
||||
$this->cleanFields($data, $fields['core']['multiselect']);
|
||||
|
||||
$testDateObject = new \DateTime('12-12-2017 22:03:59');
|
||||
|
||||
$this->assertEquals($testDateObject->format('Y-m-d H:i:s'), $data['dateField']);
|
||||
$this->assertEquals(1, $data['boolean']);
|
||||
$this->assertEquals(['a', 'b'], $data['multi']);
|
||||
}
|
||||
|
||||
public function testCleanBooleanAndNumberAsNullAreNotConverted(): void
|
||||
{
|
||||
$fields = [
|
||||
'core' => [
|
||||
'boolean' => [
|
||||
'alias' => 'boolean',
|
||||
'label' => 'Boolean',
|
||||
'type' => 'boolean',
|
||||
'value' => false,
|
||||
],
|
||||
'number' => [
|
||||
'alias' => 'number',
|
||||
'label' => 'Number',
|
||||
'type' => 'number',
|
||||
'value' => '1234',
|
||||
],
|
||||
],
|
||||
];
|
||||
$data = [
|
||||
'boolean' => null,
|
||||
'number' => null,
|
||||
];
|
||||
|
||||
$this->cleanFields($data, $fields['core']['boolean']);
|
||||
$this->cleanFields($data, $fields['core']['number']);
|
||||
|
||||
$this->assertEquals(null, $data['boolean']);
|
||||
$this->assertEquals(null, $data['number']);
|
||||
}
|
||||
|
||||
public function testAttributionDateIsAdded(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->addUpdatedField('attribution', 100);
|
||||
$lead->checkAttributionDate();
|
||||
$this->assertEquals((new \DateTime())->format('Y-m-d'), $lead->getFieldValue('attribution_date'));
|
||||
$this->assertNotEmpty($lead->getChanges());
|
||||
}
|
||||
|
||||
public function testAttributionDateIsRemoved(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFields(
|
||||
[
|
||||
'core' => [
|
||||
'attribution_date' => [
|
||||
'type' => 'date',
|
||||
'value' => '2017-09-09',
|
||||
],
|
||||
'attribution' => [
|
||||
'type' => 'int',
|
||||
'value' => 100,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$lead->addUpdatedField('attribution', 0);
|
||||
$lead->checkAttributionDate();
|
||||
$this->assertNull($lead->getFieldValue('attribution_date'));
|
||||
$this->assertNotEmpty($lead->getChanges());
|
||||
}
|
||||
|
||||
public function testAttributionDateIsNotChangedWhen0ChangedToNull(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFields(
|
||||
[
|
||||
'core' => [
|
||||
'attribution_date' => [
|
||||
'type' => 'date',
|
||||
'value' => 0,
|
||||
],
|
||||
'attribution' => [
|
||||
'type' => 'int',
|
||||
'value' => 0,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$lead->checkAttributionDate();
|
||||
$this->assertEmpty($lead->getChanges());
|
||||
}
|
||||
|
||||
public function testChangingPropertiesHydratesFieldChanges(): void
|
||||
{
|
||||
$email = 'foo@bar.com';
|
||||
$lead = new Lead();
|
||||
$lead->addUpdatedField('email', $email);
|
||||
$changes = $lead->getChanges();
|
||||
|
||||
$this->assertFalse(empty($changes['email']));
|
||||
$this->assertFalse(empty($changes['fields']['email']));
|
||||
|
||||
$this->assertEquals($email, $changes['email'][1]);
|
||||
$this->assertEquals($email, $changes['fields']['email'][1]);
|
||||
}
|
||||
|
||||
public function testIpAddressChanges(): void
|
||||
{
|
||||
$ip1 = (new IpAddress())->setIpAddress('1.2.3.4');
|
||||
$ip2 = (new IpAddress())->setIpAddress('1.2.3.5');
|
||||
|
||||
$contact = new Lead();
|
||||
|
||||
$this->assertCount(0, $contact->getChanges());
|
||||
|
||||
$contact->addIpAddress($ip1);
|
||||
|
||||
/** @var mixed[] $changes */
|
||||
$changes = $contact->getChanges();
|
||||
|
||||
$this->assertSame(['1.2.3.4' => $ip1], $changes['ipAddressList']);
|
||||
|
||||
$contact->addIpAddress($ip2);
|
||||
|
||||
/** @var mixed[] $changes */
|
||||
$changes = $contact->getChanges();
|
||||
|
||||
$this->assertSame(['1.2.3.4' => $ip1, '1.2.3.5' => $ip2], $changes['ipAddressList']);
|
||||
}
|
||||
|
||||
public function testGetLastEventLogByAction(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
|
||||
$lead->addEventLog((new LeadEventLog())->setAction('first')->setDateAdded(new \DateTime('2017-01-01')));
|
||||
$lead->addEventLog((new LeadEventLog())->setAction('first')->setDateAdded(new \DateTime('2018-01-01')));
|
||||
$lead->addEventLog($lastFirst = (new LeadEventLog())->setAction('first')->setDateAdded(new \DateTime('2019-01-01')));
|
||||
|
||||
$lead->addEventLog((new LeadEventLog())->setAction('second')->setDateAdded(new \DateTime('2017-01-01')));
|
||||
$lead->addEventLog((new LeadEventLog())->setAction('second')->setDateAdded(new \DateTime('2018-01-01')));
|
||||
$lead->addEventLog($lastSecond = (new LeadEventLog())->setAction('second')->setDateAdded(new \DateTime('2019-01-01')));
|
||||
|
||||
Assert::assertSame($lastFirst, $lead->getLastEventLogByAction('first'));
|
||||
Assert::assertSame($lastSecond, $lead->getLastEventLogByAction('second'));
|
||||
Assert::assertNull($lead->getLastEventLogByAction('third'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $operator
|
||||
*/
|
||||
private function adjustPointsTest($points, $expected, Lead $lead, $operator = false): void
|
||||
{
|
||||
if ($operator) {
|
||||
$lead->adjustPoints($points, $operator);
|
||||
} else {
|
||||
$lead->adjustPoints($points);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, $lead->getChanges());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $oldValue
|
||||
* @param int $newValue
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getLeadChangedArray($oldValue = 0, $newValue = 0)
|
||||
{
|
||||
return [
|
||||
'points' => [
|
||||
0 => $oldValue,
|
||||
1 => $newValue,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use Mautic\LeadBundle\Entity\ListLeadRepository;
|
||||
|
||||
final class ListLeadRepositoryTest extends MauticMysqlTestCase
|
||||
{
|
||||
private ListLeadRepository $listLeadRepository;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->listLeadRepository = static::getContainer()->get(ListLeadRepository::class);
|
||||
}
|
||||
|
||||
public function testGetContactsCountBySegment(): void
|
||||
{
|
||||
$filters = ['manually_removed' => 0];
|
||||
$contact = new Lead();
|
||||
$segment = new LeadList();
|
||||
$segmentMember = new ListLead();
|
||||
|
||||
$segment->setName('A segment');
|
||||
$segment->setPublicName('A segment');
|
||||
$segment->setAlias('asegment');
|
||||
|
||||
$segmentMember->setLead($contact);
|
||||
$segmentMember->setList($segment);
|
||||
$segmentMember->setManuallyRemoved(false);
|
||||
$segmentMember->setDateAdded(new \DateTime());
|
||||
|
||||
$this->em->persist($contact);
|
||||
$this->em->persist($segment);
|
||||
$this->em->persist($segmentMember);
|
||||
$this->em->flush();
|
||||
|
||||
$this->assertSame(1, $this->listLeadRepository->getContactsCountBySegment($segment->getId(), $filters));
|
||||
|
||||
$segmentMember->setManuallyRemoved(true);
|
||||
$this->em->persist($segmentMember);
|
||||
$this->em->flush();
|
||||
|
||||
$this->assertSame(0, $this->listLeadRepository->getContactsCountBySegment($segment->getId(), $filters));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
use Mautic\LeadBundle\Entity\TagRepository;
|
||||
|
||||
class TagRepositoryTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
public function testGetTagByNameOrCreateNewOneWithSomeExistingTag(): void
|
||||
{
|
||||
$fetchedEntity = new Tag('sometag');
|
||||
|
||||
$mockRepository = $this->getMockBuilder(TagRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['findOneBy'])
|
||||
->getMock();
|
||||
|
||||
$mockRepository->expects($this->once())
|
||||
->method('findOneBy')
|
||||
->with(['tag' => 'sometag'])
|
||||
->willReturn($fetchedEntity);
|
||||
|
||||
$this->assertSame($fetchedEntity, $mockRepository->getTagByNameOrCreateNewOne('sometag'));
|
||||
}
|
||||
|
||||
public function testGetTagByNameOrCreateNewOneWithSomeNewTag(): void
|
||||
{
|
||||
$mockRepository = $this->getMockBuilder(TagRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['findOneBy'])
|
||||
->getMock();
|
||||
|
||||
$mockRepository->expects($this->once())
|
||||
->method('findOneBy')
|
||||
->with(['tag' => 'sometag'])
|
||||
->willReturn(null);
|
||||
|
||||
$newEntity = $mockRepository->getTagByNameOrCreateNewOne('sometag');
|
||||
|
||||
$this->assertSame('sometag', $newEntity->getTag());
|
||||
$this->assertNull($newEntity->getId());
|
||||
}
|
||||
|
||||
public function testGetTagByNameOrCreateNewOneInputFilter(): void
|
||||
{
|
||||
$fetchedEntity = new Tag('hello" world');
|
||||
|
||||
$mockRepository = $this->getMockBuilder(TagRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['findOneBy'])
|
||||
->getMock();
|
||||
|
||||
$mockRepository->expects($this->once())
|
||||
->method('findOneBy')
|
||||
->with(['tag' => 'hello" world'])
|
||||
->willReturn($fetchedEntity);
|
||||
|
||||
$this->assertSame($fetchedEntity, $mockRepository->getTagByNameOrCreateNewOne('hello" world'));
|
||||
}
|
||||
|
||||
public function testRemoveMinusFromTags(): void
|
||||
{
|
||||
$repository = $this->configureRepository(Tag::class);
|
||||
|
||||
$tags = [
|
||||
'sometag1',
|
||||
'-sometag2',
|
||||
'sometag3',
|
||||
'-sometag4',
|
||||
];
|
||||
|
||||
$expected = [
|
||||
'sometag1',
|
||||
'sometag2',
|
||||
'sometag3',
|
||||
'sometag4',
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $repository->removeMinusFromTags($tags));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
|
||||
class TagTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testSetTagByConstructor(): void
|
||||
{
|
||||
$entity = new Tag('tagA');
|
||||
|
||||
$this->assertSame('tagA', $entity->getTag());
|
||||
}
|
||||
|
||||
public function testSetTagBySetter(): void
|
||||
{
|
||||
$entity = new Tag();
|
||||
$entity->setTag('tagA');
|
||||
|
||||
$this->assertSame('tagA', $entity->getTag());
|
||||
}
|
||||
|
||||
public function testTagValidation(): void
|
||||
{
|
||||
$sampleTags = [
|
||||
'hello world' => 'hello world',
|
||||
'hello" world' => 'hello" world',
|
||||
'trim whitespace' => ' trim whitespace ',
|
||||
'trim tab' => "\ttrim tab\t",
|
||||
'console.log(hello)' => '<script>console.log(hello)</script>',
|
||||
'oěř§ůú.' => 'oěř§ůú.',
|
||||
];
|
||||
|
||||
foreach ($sampleTags as $expected => $tag) {
|
||||
$entity = new Tag($tag);
|
||||
$this->assertSame($expected, $entity->getTag());
|
||||
}
|
||||
}
|
||||
|
||||
public function testDisabledValidation(): void
|
||||
{
|
||||
$sampleTags = [
|
||||
'hello world' => 'hello world',
|
||||
'hello" world' => 'hello" world',
|
||||
'oěř§ůú.' => 'oěř§ůú.',
|
||||
];
|
||||
|
||||
foreach ($sampleTags as $expected => $tag) {
|
||||
$entity = new Tag($tag, false);
|
||||
$this->assertSame($expected, $entity->getTag());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Entity\UtmTag;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class UtmTagTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('setUtmTag')]
|
||||
public function testSetUtmContent(string $utmContent, int $expected): void
|
||||
{
|
||||
$utmTag = new UtmTag();
|
||||
$utmTag->setUtmContent($utmContent);
|
||||
|
||||
Assert::assertEquals($expected, mb_strlen($utmTag->getUtmContent()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array<int,int|string>>
|
||||
*/
|
||||
public static function setUtmTag(): iterable
|
||||
{
|
||||
yield ['custom', 6];
|
||||
yield ['UTM content longer like 191 UTM content longer like 191 UTM content longer like 191 UTM content longer like 191 UTM content longer like 191 UTM content longer like 191 UTM content longer like 191 UTM content longer like 191 UTM content longer like 191 UTM content longer like 191 UTM content longer like 191 ', 191];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('utmTagsDataProvider')]
|
||||
public function testHasUtmTags(?string $utmCampaign, ?string $utmSource, ?string $utmMedium, ?string $utmContent, ?string $utmTerm, bool $expectedResult): void
|
||||
{
|
||||
$utmTag = new UtmTag();
|
||||
$utmTag->setUtmCampaign($utmCampaign);
|
||||
$utmTag->setUtmSource($utmSource);
|
||||
$utmTag->setUtmMedium($utmMedium);
|
||||
$utmTag->setUtmContent($utmContent);
|
||||
$utmTag->setUtmTerm($utmTerm);
|
||||
|
||||
$this->assertEquals($expectedResult, $utmTag->hasUtmTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string|array<bool|string|''>>
|
||||
*/
|
||||
public static function utmTagsDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'All tags are empty' => ['', '', '', '', '', false],
|
||||
'Only utmCampaign is set' => ['campaign', '', '', '', '', true],
|
||||
'Only utmSource is set' => ['', 'source', '', '', '', true],
|
||||
'Only utmMedium is set' => ['', '', 'medium', '', '', true],
|
||||
'Only utmContent is set' => ['', '', '', 'content', '', true],
|
||||
'Only utmTerm is set' => ['', '', '', '', 'term', true],
|
||||
'All tags are set' => ['campaign', 'source', 'medium', 'content', 'term', true],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Event\ChannelSubscriptionChange;
|
||||
|
||||
class ChannelSubscriptionChangeTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Tests that getters returns same values as the contstruct')]
|
||||
public function testGetterReturnConstruct(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$channel = 'email';
|
||||
$oldStatus = DoNotContact::IS_CONTACTABLE;
|
||||
$newStatus = DoNotContact::UNSUBSCRIBED;
|
||||
|
||||
$event = new ChannelSubscriptionChange($lead, $channel, $oldStatus, $newStatus);
|
||||
|
||||
$this->assertEquals($lead, $event->getLead());
|
||||
$this->assertEquals($channel, $event->getChannel());
|
||||
$this->assertEquals($oldStatus, $event->getOldStatus());
|
||||
$this->assertEquals($newStatus, $event->getNewStatus());
|
||||
$this->assertEquals('contactable', $event->getOldStatusVerb());
|
||||
$this->assertEquals('unsubscribed', $event->getNewStatusVerb());
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that the default verb is unsubscribed if not recongized')]
|
||||
public function testGetStatusVerbReturnsUnsubscribedForUnrecognized(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$channel = 'email';
|
||||
$oldStatus = DoNotContact::IS_CONTACTABLE;
|
||||
|
||||
$event = new ChannelSubscriptionChange($lead, $channel, $oldStatus, 456);
|
||||
|
||||
$this->assertEquals('unsubscribed', $event->getNewStatusVerb());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Event\CompanyEvent;
|
||||
|
||||
class CompanyEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testConstructGettersSetters(): void
|
||||
{
|
||||
$company = new Company();
|
||||
$isNew = false;
|
||||
$score = 1;
|
||||
$event = new CompanyEvent($company, $isNew, $score);
|
||||
|
||||
$this->assertEquals($company, $event->getCompany());
|
||||
$this->assertEquals($isNew, $event->isNew());
|
||||
$this->assertEquals($score, $event->getScore());
|
||||
|
||||
$isNew = true;
|
||||
$event = new CompanyEvent($company, $isNew, $score);
|
||||
$this->assertEquals($isNew, $event->isNew());
|
||||
|
||||
$company2 = new Company();
|
||||
$company2->setName('otherCompany');
|
||||
$event->setCompany($company2);
|
||||
$this->assertEquals($company2, $event->getCompany());
|
||||
|
||||
$secondScore = 2;
|
||||
$event->changeScore($secondScore);
|
||||
$this->assertEquals($secondScore, $event->getScore());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Event\CompanyMergeEvent;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CompanyMergeEventTest extends TestCase
|
||||
{
|
||||
public function testConstructGettersSetters(): void
|
||||
{
|
||||
$victor = new Company();
|
||||
$loser = new Company();
|
||||
$event = new CompanyMergeEvent($victor, $loser);
|
||||
|
||||
$this->assertEquals($victor, $event->getVictor());
|
||||
$this->assertEquals($loser, $event->getLoser());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Event\FieldOperatorsEvent;
|
||||
|
||||
final class FieldOperatorsEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testConstructGettersSetters(): void
|
||||
{
|
||||
$type = 'select';
|
||||
$field = 'country';
|
||||
$allOperators = [
|
||||
'=' => [
|
||||
'label' => 'equals',
|
||||
'expr' => 'eq',
|
||||
'negate_expr' => 'neq',
|
||||
],
|
||||
'!=' => [
|
||||
'label' => 'not equal',
|
||||
'expr' => 'neq',
|
||||
'negate_expr' => 'eq',
|
||||
],
|
||||
];
|
||||
|
||||
$defaultOperators = [
|
||||
'equals' => '=',
|
||||
];
|
||||
|
||||
$event = new FieldOperatorsEvent($type, $field, $allOperators, $defaultOperators);
|
||||
|
||||
$this->assertSame($type, $event->getType());
|
||||
$this->assertSame($field, $event->getField());
|
||||
$this->assertSame($defaultOperators, $event->getOperators());
|
||||
|
||||
$event->addOperator('!=');
|
||||
|
||||
$this->assertSame(['equals' => '=', 'not equal' => '!='], $event->getOperators());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Event\FormAdjustmentEvent;
|
||||
use Mautic\LeadBundle\Segment\OperatorOptions;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
final class FormAdjustmentEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&FormInterface<FormInterface<mixed>>
|
||||
*/
|
||||
private MockObject $form;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->form = $this->createMock(FormInterface::class);
|
||||
}
|
||||
|
||||
public function testConstructGettersSetters(): void
|
||||
{
|
||||
$alias = 'email';
|
||||
$object = 'lead';
|
||||
$operator = OperatorOptions::EQUAL_TO;
|
||||
$fieldDetails = [
|
||||
'properties' => [
|
||||
'type' => 'text',
|
||||
'list' => ['one', 'two'],
|
||||
],
|
||||
];
|
||||
$event = new FormAdjustmentEvent($this->form, $alias, $object, $operator, $fieldDetails);
|
||||
|
||||
$this->assertSame($this->form, $event->getForm());
|
||||
$this->assertSame($alias, $event->getFieldAlias());
|
||||
$this->assertSame($object, $event->getFieldObject());
|
||||
$this->assertSame($operator, $event->getOperator());
|
||||
$this->assertSame($fieldDetails, $event->getFieldDetails());
|
||||
$this->assertSame('text', $event->getFieldType());
|
||||
$this->assertSame(['one', 'two'], $event->getFieldChoices());
|
||||
$this->assertFalse($event->filterShouldBeDisabled());
|
||||
$this->assertFalse($event->operatorIsOneOf(OperatorOptions::LESS_THAN));
|
||||
$this->assertFalse($event->operatorIsOneOf(OperatorOptions::LESS_THAN, OperatorOptions::NOT_EQUAL_TO));
|
||||
$this->assertTrue($event->operatorIsOneOf(OperatorOptions::LESS_THAN, OperatorOptions::EQUAL_TO));
|
||||
$this->assertFalse($event->fieldTypeIsOneOf('select'));
|
||||
$this->assertTrue($event->fieldTypeIsOneOf('select', 'text'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Event\LeadEvent;
|
||||
|
||||
class LeadEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testConstructGettersSetters(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$event = new LeadEvent($lead);
|
||||
|
||||
$this->assertEquals($lead, $event->getLead());
|
||||
$this->assertFalse($event->isNew());
|
||||
|
||||
$event = new LeadEvent($lead, false);
|
||||
$this->assertFalse($event->isNew());
|
||||
|
||||
$event = new LeadEvent($lead, true);
|
||||
$this->assertTrue($event->isNew());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Event\LeadListEvent;
|
||||
|
||||
class LeadListEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testConstructGettersSetters(): void
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$event = new LeadListEvent($segment);
|
||||
|
||||
$this->assertEquals($segment, $event->getList());
|
||||
$this->assertEquals(false, $event->isNew());
|
||||
|
||||
$isNew = false;
|
||||
$event = new LeadListEvent($segment, $isNew);
|
||||
$this->assertEquals($isNew, $event->isNew());
|
||||
|
||||
$isNew = true;
|
||||
$event = new LeadListEvent($segment, $isNew);
|
||||
$this->assertEquals($isNew, $event->isNew());
|
||||
|
||||
$segment2 = new LeadList();
|
||||
$segment2->setName('otherSegmentName');
|
||||
$event->setList($segment2);
|
||||
$this->assertEquals($segment2, $event->getList());
|
||||
|
||||
$isNew = true;
|
||||
$event = new LeadListEvent($segment, $isNew);
|
||||
|
||||
$category = new Category();
|
||||
$category->setTitle('Segment Category 1');
|
||||
$category->setAlias('segment-category-1');
|
||||
$category->setBundle('segment');
|
||||
|
||||
$segment3 = new LeadList();
|
||||
$segment3->setName('Segment 1');
|
||||
$segment3->setCategory($category);
|
||||
$event->setList($segment3);
|
||||
$this->assertEquals($segment3, $event->getList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Event\LeadListFiltersChoicesEvent;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class LeadListFiltersChoicesEventTest extends TestCase
|
||||
{
|
||||
public function testGetters(): void
|
||||
{
|
||||
$operators = [
|
||||
'text' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
'empty' => 'empty',
|
||||
'not empty' => '!empty',
|
||||
'like' => 'like',
|
||||
'not like' => '!like',
|
||||
'regexp' => 'regexp',
|
||||
'not regexp' => '!regexp',
|
||||
'starts with' => 'startsWith',
|
||||
'ends with' => 'endsWith',
|
||||
'contains' => 'contains',
|
||||
],
|
||||
'select' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
'empty' => 'empty',
|
||||
'not empty' => '!empty',
|
||||
'including' => 'in',
|
||||
'excluding' => '!in',
|
||||
'regexp' => 'regexp',
|
||||
'not regexp' => '!regexp',
|
||||
],
|
||||
'bool' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
];
|
||||
|
||||
$choices = [0 => 'Choice1', 1 => 'Choice2'];
|
||||
$search = 'Test Search';
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$leadListFiltersChoicesEvent = new LeadListFiltersChoicesEvent($choices, $operators, $translator, new Request(), $search);
|
||||
$this->assertSame($operators, $leadListFiltersChoicesEvent->getOperators());
|
||||
$this->assertSame($choices, $leadListFiltersChoicesEvent->getChoices());
|
||||
$this->assertSame($translator, $leadListFiltersChoicesEvent->getTranslator());
|
||||
$this->assertSame($search, $leadListFiltersChoicesEvent->getSearch());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Event\LeadTimelineEvent;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(LeadTimelineEvent::class)]
|
||||
class LeadTimelineEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Every event in the timeline should have a unique eventId so test that one is generated if the subscriber forgets')]
|
||||
public function testEventIdIsGeneratedIfNotSetBySubscriber(): void
|
||||
{
|
||||
$payload = [
|
||||
[
|
||||
'event' => 'foo',
|
||||
'eventLabel' => 'Foo',
|
||||
'eventType' => 'foo',
|
||||
'timestamp' => new \DateTime(),
|
||||
'extra' => [
|
||||
'something' => 'something',
|
||||
],
|
||||
'icon' => 'ri-speed-up-line',
|
||||
'contactId' => 1,
|
||||
],
|
||||
[
|
||||
'event' => 'bar',
|
||||
'eventLabel' => 'Bar',
|
||||
'eventType' => 'bar',
|
||||
'timestamp' => new \DateTime(),
|
||||
'extra' => [
|
||||
'something' => 'something else',
|
||||
],
|
||||
'icon' => 'ri-speed-up-line',
|
||||
'contactId' => 2,
|
||||
],
|
||||
[
|
||||
'event' => 'foobar',
|
||||
'eventId' => 'foobar123',
|
||||
'eventLabel' => 'Foo Bar',
|
||||
'eventType' => 'foobar',
|
||||
'timestamp' => new \DateTime(),
|
||||
'extra' => [
|
||||
'something' => 'something else',
|
||||
],
|
||||
'icon' => 'ri-speed-up-line',
|
||||
'contactId' => 2,
|
||||
],
|
||||
];
|
||||
|
||||
$event = new LeadTimelineEvent();
|
||||
|
||||
foreach ($payload as $data) {
|
||||
$event->addEvent($data);
|
||||
}
|
||||
|
||||
$events = $event->getEvents();
|
||||
|
||||
$id1 = hash('crc32', json_encode($payload[0]), false);
|
||||
$this->assertTrue(isset($events[0]['eventId']));
|
||||
$this->assertEquals('foo'.$id1, $events[0]['eventId']);
|
||||
|
||||
$id2 = hash('crc32', json_encode($payload[1]), false);
|
||||
$this->assertTrue(isset($events[1]['eventId']));
|
||||
$this->assertEquals('bar'.$id2, $events[1]['eventId']);
|
||||
|
||||
$this->assertTrue(isset($events[2]['eventId']));
|
||||
$this->assertEquals('foobar123', $events[2]['eventId']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Event\ListFieldChoicesEvent;
|
||||
|
||||
final class ListFieldChoicesEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testConstructGettersSetters(): void
|
||||
{
|
||||
$event = new ListFieldChoicesEvent();
|
||||
|
||||
$this->assertSame([], $event->getChoicesForAllListFieldTypes());
|
||||
$this->assertSame([], $event->getChoicesForAllListFieldAliases());
|
||||
|
||||
$event->setChoicesForFieldType('boolean', ['No' => 0, 'Yes' => 1]);
|
||||
$event->setChoicesForFieldAlias('campaign', ['Campaign A' => 1, 'Campaign B' => 2]);
|
||||
$event->setSearchTerm('Test search');
|
||||
|
||||
$this->assertSame(['boolean' => ['No' => 0, 'Yes' => 1]], $event->getChoicesForAllListFieldTypes());
|
||||
$this->assertSame(['campaign' => ['Campaign A' => 1, 'Campaign B' => 2]], $event->getChoicesForAllListFieldAliases());
|
||||
$this->assertSame('Test search', $event->getSearchTerm());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Event\SegmentOperatorQueryBuilderEvent;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
final class SegmentOperatorQueryBuilderEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|QueryBuilder
|
||||
*/
|
||||
private MockObject $queryBuilder;
|
||||
|
||||
/**
|
||||
* @var MockObject|ContactSegmentFilter
|
||||
*/
|
||||
private MockObject $filter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->queryBuilder = $this->createMock(QueryBuilder::class);
|
||||
$this->filter = $this->createMock(ContactSegmentFilter::class);
|
||||
|
||||
$this->queryBuilder->method('getTableAlias')
|
||||
->with(MAUTIC_TABLE_PREFIX.'leads')
|
||||
->willReturn('leads');
|
||||
}
|
||||
|
||||
public function testConstructGettersSetters(): void
|
||||
{
|
||||
$this->filter->method('getOperator')->willReturn('=');
|
||||
$this->filter->method('getGlue')->willReturn('and');
|
||||
|
||||
$event = new SegmentOperatorQueryBuilderEvent($this->queryBuilder, $this->filter, 'parameterHolder1');
|
||||
|
||||
$this->assertSame($this->queryBuilder, $event->getQueryBuilder());
|
||||
$this->assertSame($this->filter, $event->getFilter());
|
||||
$this->assertSame('parameterHolder1', $event->getParameterHolder());
|
||||
$this->assertFalse($event->operatorIsOneOf('like'));
|
||||
$this->assertTrue($event->operatorIsOneOf('=', 'like'));
|
||||
$this->assertFalse($event->wasOperatorHandled());
|
||||
|
||||
$this->queryBuilder->expects($this->once())
|
||||
->method('addLogic')
|
||||
->with('a != b', 'and');
|
||||
|
||||
$event->addExpression('a != b');
|
||||
|
||||
$this->assertTrue($event->wasOperatorHandled());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Event;
|
||||
|
||||
use Mautic\LeadBundle\Event\TypeOperatorsEvent;
|
||||
|
||||
final class TypeOperatorsEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testConstructGettersSetters(): void
|
||||
{
|
||||
$event = new TypeOperatorsEvent();
|
||||
|
||||
$this->assertSame([], $event->getOperatorsForAllFieldTypes());
|
||||
|
||||
$event->setOperatorsForFieldType('email', ['include' => ['=', 'like']]);
|
||||
$event->setOperatorsForFieldType('firsname', ['exclude' => ['!=', '!like']]);
|
||||
|
||||
$this->assertSame([
|
||||
'email' => ['include' => ['=', 'like']],
|
||||
'firsname' => ['exclude' => ['!=', '!like']],
|
||||
], $event->getOperatorsForAllFieldTypes());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,451 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
|
||||
use Mautic\CampaignBundle\Event\PendingEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\EventListener\CampaignSubscriber;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\DoNotContact;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Provider\FilterOperatorProvider;
|
||||
use Mautic\PointBundle\Model\PointGroupModel;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class CampaignSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $configFrom = [
|
||||
'id' => 111,
|
||||
'companyname' => 'Mautic',
|
||||
'companemail' => 'mautic@mautic.com',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $configTo = [
|
||||
'id' => '112',
|
||||
'companyname' => 'Mautic2',
|
||||
'companemail' => 'mautic@mauticsecond.com',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, array<int, string>|bool|int|null>>
|
||||
*/
|
||||
public static function provideFormDNC(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'reason' => 1,
|
||||
'channels' => ['email'],
|
||||
'expected' => true,
|
||||
'dncLead' => 1,
|
||||
],
|
||||
[
|
||||
'reason' => 2,
|
||||
'channels' => ['email'],
|
||||
'expected' => false,
|
||||
'dncLead' => 1,
|
||||
],
|
||||
[
|
||||
'reason' => 3,
|
||||
'channels' => ['email'],
|
||||
'expected' => false,
|
||||
'dncLead' => 1,
|
||||
],
|
||||
[
|
||||
'reason' => 2,
|
||||
'channels' => ['email'],
|
||||
'expected' => true,
|
||||
'dncLead' => 2,
|
||||
],
|
||||
[
|
||||
'reason' => null,
|
||||
'channels' => ['email'],
|
||||
'expected' => true,
|
||||
'dncLead' => 2,
|
||||
],
|
||||
[
|
||||
'reason' => null,
|
||||
'channels' => ['email'],
|
||||
'expected' => false,
|
||||
'dncLead' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $configPageHit = [
|
||||
'startDate' => '2022-06-08 12:45:22.0',
|
||||
'endDate' => '2023-06-08 12:45:22.0',
|
||||
'page' => '1',
|
||||
'page_url' => '',
|
||||
'accumulative_time' => '5',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $configUrlPageHit = [
|
||||
'startDate' => '',
|
||||
'endDate' => '',
|
||||
'page' => '',
|
||||
'page_url' => 'https://example.com',
|
||||
'accumulative_time' => '5',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $configUrlPageHitWithoutSpentTime = [
|
||||
'startDate' => '',
|
||||
'endDate' => '',
|
||||
'page' => '',
|
||||
'page_url' => 'https://example.com',
|
||||
'accumulative_time' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var LeadModel|MockObject
|
||||
*/
|
||||
private $mockLeadModel;
|
||||
|
||||
/**
|
||||
* @var CompanyModel|MockObject
|
||||
*/
|
||||
private $mockCompanyModel;
|
||||
|
||||
/**
|
||||
* @var CampaignSubscriber
|
||||
*/
|
||||
private $subscriber;
|
||||
|
||||
private FilterOperatorProvider $filterOperatorProvider;
|
||||
|
||||
/**
|
||||
* @var DoNotContact|MockObject
|
||||
*/
|
||||
private $doNotContact;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$mockIpLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$this->mockLeadModel = $this->createMock(LeadModel::class);
|
||||
$mockLeadFieldModel = $this->createMock(FieldModel::class);
|
||||
$mockListModel = $this->createMock(ListModel::class);
|
||||
$this->mockCompanyModel = $this->createMock(CompanyModel::class);
|
||||
$mockCampaignModel = $this->createMock(CampaignModel::class);
|
||||
$this->doNotContact = $this->createMock(DoNotContact::class);
|
||||
$mockGroupModel = $this->createMock(PointGroupModel::class);
|
||||
$this->filterOperatorProvider = new FilterOperatorProvider(
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(TranslatorInterface::class)
|
||||
);
|
||||
$mockCoreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$mockCoreParametersHelper->method('getDefaultTimezone')
|
||||
->willReturn('UTC');
|
||||
|
||||
$this->subscriber = new CampaignSubscriber(
|
||||
$mockIpLookupHelper,
|
||||
$this->mockLeadModel,
|
||||
$mockLeadFieldModel,
|
||||
$mockListModel,
|
||||
$this->mockCompanyModel,
|
||||
$mockCampaignModel,
|
||||
$mockCoreParametersHelper,
|
||||
$this->doNotContact,
|
||||
$mockGroupModel,
|
||||
$this->filterOperatorProvider
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnCampaignTriggerActiononUpdateCompany(): void
|
||||
{
|
||||
$companyEntityFrom = $this->createMock(Company::class);
|
||||
$companyEntityFrom->method('getId')
|
||||
->willReturn($this->configFrom['id']);
|
||||
$companyEntityFrom->method('getName')
|
||||
->willReturn($this->configFrom['companyname']);
|
||||
|
||||
$companyEntityTo = $this->createMock(Company::class);
|
||||
$companyEntityTo->method('getId')
|
||||
->willReturn($this->configTo['id']);
|
||||
$companyEntityTo->method('getName')
|
||||
->willReturn($this->configTo['companyname']);
|
||||
$companyEntityTo->method('getProfileFields')
|
||||
->willReturn($this->configTo);
|
||||
|
||||
$this->mockCompanyModel->expects($this->once())->method('getEntity')->willReturn($companyEntityFrom);
|
||||
|
||||
$mockCompanyLeadRepo = $this->createMock(CompanyLeadRepository::class);
|
||||
$mockCompanyLeadRepo->expects($this->once())->method('getCompaniesByLeadId')->willReturn([]);
|
||||
|
||||
$this->mockCompanyModel->expects($this->atLeastOnce())
|
||||
->method('getCompanyLeadRepository')
|
||||
->willReturn($mockCompanyLeadRepo);
|
||||
|
||||
$this->mockCompanyModel->expects($this->once())
|
||||
->method('checkForDuplicateCompanies')
|
||||
->willReturn([$companyEntityTo]);
|
||||
|
||||
$this->mockCompanyModel->expects($this->any())
|
||||
->method('fetchCompanyFields')
|
||||
->willReturn([['alias' => 'companyname']]);
|
||||
|
||||
$mockCoreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$mockCoreParametersHelper->method('get')
|
||||
->with('default_timezone')
|
||||
->willReturn('UTC');
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setId(99);
|
||||
$lead->setPrimaryCompany($this->configFrom);
|
||||
|
||||
$this->mockLeadModel->expects($this->once())->method('setPrimaryCompany')->willReturnCallback(
|
||||
function () use ($lead): void {
|
||||
$lead->setPrimaryCompany($this->configTo);
|
||||
}
|
||||
);
|
||||
|
||||
$args = [
|
||||
'lead' => $lead,
|
||||
'event' => [
|
||||
'type' => 'lead.updatecompany',
|
||||
'properties' => $this->configTo,
|
||||
],
|
||||
'eventDetails' => [],
|
||||
'systemTriggered' => true,
|
||||
'eventSettings' => [],
|
||||
];
|
||||
|
||||
$event = new CampaignExecutionEvent($args, true);
|
||||
$this->subscriber->onCampaignTriggerActionUpdateCompany($event);
|
||||
$this->assertTrue($event->getResult());
|
||||
|
||||
$primaryCompany = $lead->getPrimaryCompany();
|
||||
$this->assertSame($this->configTo['companyname'], $primaryCompany['companyname']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $channels
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('provideFormDNC')]
|
||||
public function testOnCampaignTriggerConditionDNCFlag(?int $reason, array $channels, bool $expected, int $dncLead): void
|
||||
{
|
||||
$mockCoreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$mockCoreParametersHelper->method('get')
|
||||
->with('default_timezone')
|
||||
->willReturn('UTC');
|
||||
|
||||
$this->doNotContact->expects($this->once())->method('isContactable')->willReturn($dncLead);
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setId(99);
|
||||
$args = [
|
||||
'lead' => $lead,
|
||||
'event' => [
|
||||
'type' => 'lead.dnc',
|
||||
'properties' => [
|
||||
'reason' => $reason,
|
||||
'channels' => $channels,
|
||||
],
|
||||
],
|
||||
'eventDetails' => [],
|
||||
'systemTriggered' => true,
|
||||
'eventSettings' => [],
|
||||
];
|
||||
|
||||
$event = new CampaignExecutionEvent($args, true);
|
||||
$this->subscriber->onCampaignTriggerCondition($event);
|
||||
$this->assertSame($expected, $event->getResult());
|
||||
}
|
||||
|
||||
public function testOnCampaignTriggerConditionLeadLandingPageHit(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setId(99);
|
||||
$leadTimeline = [
|
||||
0 => [
|
||||
'events' => [
|
||||
0 => [
|
||||
'event' => 'page.hit',
|
||||
'eventId' => '5',
|
||||
'eventType' => 'Page hit',
|
||||
'timestamp' => new \DateTime('2022-06-08 12:45:22.0'),
|
||||
'contactId' => '1',
|
||||
'details' => [
|
||||
'hit' => [
|
||||
'hitId' => '5',
|
||||
'page_id' => '1',
|
||||
'dateHit' => new \DateTime('2022-06-08 12:45:22.0'),
|
||||
'dateLeft' => new \DateTime('2022-06-08 12:50:42.0'),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
], ];
|
||||
|
||||
$this->mockLeadModel->expects($this->once())->method('getEngagements')->willReturn($leadTimeline);
|
||||
|
||||
$args = [
|
||||
'lead' => $lead,
|
||||
'event' => [
|
||||
'type' => 'lead.pageHit',
|
||||
'properties' => $this->configPageHit,
|
||||
],
|
||||
'eventDetails' => [],
|
||||
'systemTriggered' => true,
|
||||
'eventSettings' => [],
|
||||
];
|
||||
|
||||
$event = new CampaignExecutionEvent($args, true);
|
||||
$this->subscriber->onCampaignTriggerCondition($event);
|
||||
$this->assertTrue($event->getResult());
|
||||
}
|
||||
|
||||
public function testOnCampaignTriggerConditionLeadPageUrlHit(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setId(99);
|
||||
$leadTimeline = [
|
||||
0 => [
|
||||
'events' => [
|
||||
0 => [
|
||||
'event' => 'page.hit',
|
||||
'eventId' => '5',
|
||||
'eventType' => 'Page hit',
|
||||
'timestamp' => new \DateTime('2022-06-08 12:45:22.0'),
|
||||
'contactId' => '1',
|
||||
'details' => [
|
||||
'hit' => [
|
||||
'hitId' => '5',
|
||||
'page_id' => '',
|
||||
'dateHit' => new \DateTime('2022-06-08 12:45:22.0'),
|
||||
'dateLeft' => new \DateTime('2022-06-08 12:50:42.0'),
|
||||
'url' => 'https://example.com',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
], ];
|
||||
|
||||
$this->mockLeadModel->expects($this->once())->method('getEngagements')->willReturn($leadTimeline);
|
||||
|
||||
$args = [
|
||||
'lead' => $lead,
|
||||
'event' => [
|
||||
'type' => 'lead.pageHit',
|
||||
'properties' => $this->configUrlPageHit,
|
||||
],
|
||||
'eventDetails' => [],
|
||||
'systemTriggered' => true,
|
||||
'eventSettings' => [],
|
||||
];
|
||||
|
||||
$event = new CampaignExecutionEvent($args, true);
|
||||
$this->subscriber->onCampaignTriggerCondition($event);
|
||||
$this->assertTrue($event->getResult());
|
||||
}
|
||||
|
||||
public function testOnCampaignTriggerConditionLeadPageUrlHitWithoutSpentTime(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setId(99);
|
||||
$leadTimeline = [
|
||||
0 => [
|
||||
'events' => [
|
||||
0 => [
|
||||
'event' => 'page.hit',
|
||||
'eventId' => '5',
|
||||
'eventType' => 'Page hit',
|
||||
'timestamp' => new \DateTime('2022-06-08 12:45:22.0'),
|
||||
'contactId' => '1',
|
||||
'details' => [
|
||||
'hit' => [
|
||||
'hitId' => '5',
|
||||
'page_id' => '',
|
||||
'dateHit' => new \DateTime('2022-06-08 12:45:22.0'),
|
||||
'dateLeft' => new \DateTime('2022-06-08 12:50:42.0'),
|
||||
'url' => 'https://example.com',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
], ];
|
||||
|
||||
$this->mockLeadModel->expects($this->once())->method('getEngagements')->willReturn($leadTimeline);
|
||||
|
||||
$args = [
|
||||
'lead' => $lead,
|
||||
'event' => [
|
||||
'type' => 'lead.pageHit',
|
||||
'properties' => $this->configUrlPageHitWithoutSpentTime,
|
||||
],
|
||||
'eventDetails' => [],
|
||||
'systemTriggered' => true,
|
||||
'eventSettings' => [],
|
||||
];
|
||||
|
||||
$event = new CampaignExecutionEvent($args, true);
|
||||
$this->subscriber->onCampaignTriggerCondition($event);
|
||||
$this->assertTrue($event->getResult());
|
||||
}
|
||||
|
||||
public function testOnCampaignTriggerActionUpdateLead(): void
|
||||
{
|
||||
$eventAccessor = $this->createMock(ActionAccessor::class);
|
||||
$properties = [
|
||||
'points' => 10,
|
||||
];
|
||||
$event = (new Event())->setProperties($properties);
|
||||
$event->setType('lead.updatelead');
|
||||
$lead = (new Lead())->setEmail('tester@mautic.org');
|
||||
|
||||
$leadEventLog = $this->createMock(LeadEventLog::class);
|
||||
$leadEventLog
|
||||
->method('getLead')
|
||||
->willReturn($lead);
|
||||
$leadEventLog
|
||||
->method('getId')
|
||||
->willReturn(6);
|
||||
$leadEventLog
|
||||
->method('setIsScheduled')
|
||||
->with(false)
|
||||
->willReturn($leadEventLog);
|
||||
|
||||
$logs = new ArrayCollection([$leadEventLog]);
|
||||
|
||||
$this->mockLeadModel->expects($this->exactly(2))
|
||||
->method('setFieldValues')
|
||||
->with($lead, $properties, false, true, false);
|
||||
|
||||
$this->mockLeadModel->expects($this->once())
|
||||
->method('saveEntity')
|
||||
->with($lead);
|
||||
|
||||
$pendingEvent = new PendingEvent($eventAccessor, $event, $logs);
|
||||
$this->subscriber->onCampaignTriggerActionUpdateLead($pendingEvent);
|
||||
|
||||
$this->assertCount(1, $pendingEvent->getSuccessful());
|
||||
$this->assertCount(0, $pendingEvent->getFailures());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Event\CompanyEvent;
|
||||
use Mautic\LeadBundle\EventListener\CompanySubscriber;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
|
||||
class CompanySubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
$ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$entityManager = $this->createMock(EntityManager::class);
|
||||
$subscriber = new CompanySubscriber($ipLookupHelper, $auditLogModel, $entityManager);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
LeadEvents::COMPANY_POST_SAVE => ['onCompanyPostSave', 0],
|
||||
LeadEvents::COMPANY_POST_DELETE => ['onCompanyDelete', 0],
|
||||
],
|
||||
$subscriber->getSubscribedEvents()
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnCompanyPostSave(): void
|
||||
{
|
||||
$this->onCompanyPostSaveMethodCall(false); // update company log
|
||||
$this->onCompanyPostSaveMethodCall(true); // create company log
|
||||
}
|
||||
|
||||
public function testOnCompanyDelete(): void
|
||||
{
|
||||
$companyId = 1;
|
||||
$companyName = 'name';
|
||||
$ip = '127.0.0.2';
|
||||
|
||||
$log = [
|
||||
'bundle' => 'lead',
|
||||
'object' => 'company',
|
||||
'objectId' => $companyId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name', $companyName],
|
||||
'ipAddress' => $ip,
|
||||
];
|
||||
|
||||
$ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$ipLookupHelper->expects($this->once())
|
||||
->method('getIpAddressFromRequest')
|
||||
->willReturn($ip);
|
||||
|
||||
$auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$auditLogModel->expects($this->once())
|
||||
->method('writeToLog')
|
||||
->with($log);
|
||||
|
||||
$entityManager = $this->createMock(EntityManager::class);
|
||||
$subscriber = new CompanySubscriber($ipLookupHelper, $auditLogModel, $entityManager);
|
||||
|
||||
$company = $this->createMock(Company::class);
|
||||
$company->deletedId = $companyId;
|
||||
$company->expects($this->once())
|
||||
->method('getPrimaryIdentifier')
|
||||
->willReturn($companyName);
|
||||
|
||||
$event = $this->createMock(CompanyEvent::class);
|
||||
$event->expects($this->once())
|
||||
->method('getCompany')
|
||||
->willReturn($company);
|
||||
|
||||
$subscriber->onCompanyDelete($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create or update company logging.
|
||||
*
|
||||
* @param bool $isNew
|
||||
*/
|
||||
private function onCompanyPostSaveMethodCall($isNew): void
|
||||
{
|
||||
$companyId = 1;
|
||||
$changes = ['changes'];
|
||||
$ip = '127.0.0.2';
|
||||
|
||||
$log = [
|
||||
'bundle' => 'lead',
|
||||
'object' => 'company',
|
||||
'objectId' => $companyId,
|
||||
'action' => ($isNew) ? 'create' : 'update',
|
||||
'details' => $changes,
|
||||
'ipAddress' => $ip,
|
||||
];
|
||||
|
||||
$ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$ipLookupHelper->expects($this->once())
|
||||
->method('getIpAddressFromRequest')
|
||||
->willReturn($ip);
|
||||
|
||||
$auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$auditLogModel->expects($this->once())
|
||||
->method('writeToLog')
|
||||
->with($log);
|
||||
|
||||
$entityManager = $this->createMock(EntityManager::class);
|
||||
$subscriber = new CompanySubscriber($ipLookupHelper, $auditLogModel, $entityManager);
|
||||
|
||||
$company = $this->createMock(Company::class);
|
||||
$company->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn($companyId);
|
||||
|
||||
$event = $this->createMock(CompanyEvent::class);
|
||||
$event->expects($this->once())
|
||||
->method('getCompany')
|
||||
->willReturn($company);
|
||||
$event->expects($this->once())
|
||||
->method('getChanges')
|
||||
->willReturn($changes);
|
||||
$event->expects($this->once())
|
||||
->method('isNew')
|
||||
->willReturn($isNew);
|
||||
|
||||
$subscriber->onCompanyPostSave($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\ConfigBundle\ConfigEvents;
|
||||
use Mautic\ConfigBundle\Event\ConfigBuilderEvent;
|
||||
use Mautic\LeadBundle\EventListener\ConfigSubscriber;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ConfigSubscriberTest extends TestCase
|
||||
{
|
||||
private ConfigSubscriber $configSubscriber;
|
||||
|
||||
/**
|
||||
* @var ConfigBuilderEvent&MockObject
|
||||
*/
|
||||
private MockObject $configBuilderEvent;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->configSubscriber = new ConfigSubscriber();
|
||||
$this->configBuilderEvent = $this->createMock(ConfigBuilderEvent::class);
|
||||
}
|
||||
|
||||
public function testSubscribedEvents(): void
|
||||
{
|
||||
$subscribedEvents = ConfigSubscriber::getSubscribedEvents();
|
||||
$this->assertArrayHasKey(ConfigEvents::CONFIG_ON_GENERATE, $subscribedEvents);
|
||||
}
|
||||
|
||||
public function testThatWeAreAddingFormsToTheConfig(): void
|
||||
{
|
||||
$leadConfig = [
|
||||
'bundle' => 'LeadBundle',
|
||||
'formAlias' => 'leadconfig',
|
||||
'formType' => \Mautic\LeadBundle\Form\Type\ConfigType::class,
|
||||
'formTheme' => '@MauticLead/FormTheme/Config/_config_companyconfig_widget.html.twig',
|
||||
'parameters' => null,
|
||||
];
|
||||
|
||||
$segmentConfig = [
|
||||
'bundle' => 'LeadBundle',
|
||||
'formAlias' => 'segment_config',
|
||||
'formType' => \Mautic\LeadBundle\Form\Type\SegmentConfigType::class,
|
||||
'formTheme' => '@MauticLead/FormTheme/Config/_config_leadconfig_widget.html.twig',
|
||||
'parameters' => null,
|
||||
];
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->configBuilderEvent
|
||||
->expects($matcher)
|
||||
->method('addForm')->willReturnCallback(function (...$parameters) use ($matcher, $leadConfig, $segmentConfig) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($leadConfig, $parameters[0]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($segmentConfig, $parameters[0]);
|
||||
}
|
||||
});
|
||||
|
||||
$this->configSubscriber->onConfigGenerate($this->configBuilderEvent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Event\DoNotContactAddEvent;
|
||||
use Mautic\LeadBundle\Event\DoNotContactRemoveEvent;
|
||||
use Mautic\LeadBundle\EventListener\DoNotContactSubscriber;
|
||||
use Mautic\LeadBundle\Model\DoNotContact;
|
||||
|
||||
class DoNotContactSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private DoNotContactSubscriber $doNotContactSubscriber;
|
||||
|
||||
private \PHPUnit\Framework\MockObject\MockObject $doNotContact;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->doNotContact = $this->createMock(DoNotContact::class);
|
||||
$this->doNotContactSubscriber = new DoNotContactSubscriber($this->doNotContact);
|
||||
}
|
||||
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
[
|
||||
DoNotContactAddEvent::ADD_DONOT_CONTACT => ['addDncForLead', 0],
|
||||
DoNotContactRemoveEvent::REMOVE_DONOT_CONTACT => ['removeDncForLead', 0],
|
||||
],
|
||||
$this->doNotContactSubscriber->getSubscribedEvents()
|
||||
);
|
||||
}
|
||||
|
||||
public function testAddDncForLeadForNewContacts(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$doNotContactEvent = new DoNotContactAddEvent($lead, 'email');
|
||||
|
||||
$this->doNotContact->expects($this->once())->method('createDncRecord');
|
||||
$this->doNotContact->expects($this->never())->method('addDncForContact');
|
||||
|
||||
$this->doNotContactSubscriber->addDncForLead($doNotContactEvent);
|
||||
}
|
||||
|
||||
public function testAddDncForLeadForExistedContacts(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setId(1);
|
||||
$doNotContactEvent = new DoNotContactAddEvent($lead, 'email');
|
||||
|
||||
$this->doNotContact->expects($this->never())->method('createDncRecord');
|
||||
$this->doNotContact->expects($this->once())->method('addDncForContact');
|
||||
|
||||
$this->doNotContactSubscriber->addDncForLead($doNotContactEvent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\DynamicContentBundle\DynamicContentEvents;
|
||||
use Mautic\DynamicContentBundle\Event\ContactFiltersEvaluateEvent;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadListRepository;
|
||||
use Mautic\LeadBundle\EventListener\DynamicContentSubscriber;
|
||||
use Mautic\LeadBundle\Segment\OperatorOptions;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DynamicContentSubscriberTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var LeadListRepository|MockObject
|
||||
*/
|
||||
private $segmentRepository;
|
||||
|
||||
/**
|
||||
* @var DynamicContentSubscriber
|
||||
*/
|
||||
private $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->segmentRepository = $this->createMock(LeadListRepository::class);
|
||||
$this->subscriber = new DynamicContentSubscriber($this->segmentRepository);
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[
|
||||
DynamicContentEvents::ON_CONTACTS_FILTER_EVALUATE => ['onContactFilterEvaluate', 0],
|
||||
],
|
||||
DynamicContentSubscriber::getSubscribedEvents()
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnContactFilterEvaluateUnknownOperator(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$filters = [
|
||||
[
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'unknownFilter',
|
||||
'filter' => null,
|
||||
],
|
||||
];
|
||||
$contact = (new Lead())->setId($contactId);
|
||||
|
||||
$event = new ContactFiltersEvaluateEvent($filters, $contact);
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$this->subscriber->onContactFilterEvaluate($event);
|
||||
}
|
||||
|
||||
public function testOnContactFilterEvaluateEmpty(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$filters = [
|
||||
[
|
||||
'type' => 'leadlist',
|
||||
'operator' => OperatorOptions::EMPTY,
|
||||
'filter' => null,
|
||||
],
|
||||
];
|
||||
$contact = (new Lead())->setId($contactId);
|
||||
|
||||
$event = new ContactFiltersEvaluateEvent($filters, $contact);
|
||||
|
||||
$this->segmentRepository->expects(self::once())
|
||||
->method('isNotContactInAnySegment')
|
||||
->with($contactId)
|
||||
->willReturn(true);
|
||||
|
||||
$this->subscriber->onContactFilterEvaluate($event);
|
||||
self::assertTrue($event->isEvaluated());
|
||||
self::assertTrue($event->isMatched());
|
||||
}
|
||||
|
||||
public function testOnContactFilterEvaluateNotEmpty(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$filters = [
|
||||
[
|
||||
'type' => 'leadlist',
|
||||
'operator' => OperatorOptions::NOT_EMPTY,
|
||||
'filter' => null,
|
||||
],
|
||||
];
|
||||
$contact = (new Lead())->setId($contactId);
|
||||
|
||||
$event = new ContactFiltersEvaluateEvent($filters, $contact);
|
||||
|
||||
$this->segmentRepository->expects(self::once())
|
||||
->method('isContactInAnySegment')
|
||||
->with($contactId)
|
||||
->willReturn(true);
|
||||
|
||||
$this->subscriber->onContactFilterEvaluate($event);
|
||||
self::assertTrue($event->isEvaluated());
|
||||
self::assertTrue($event->isMatched());
|
||||
}
|
||||
|
||||
public function testOnContactFilterEvaluateNotIn(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$filters = [
|
||||
[
|
||||
'type' => 'leadlist',
|
||||
'operator' => OperatorOptions::INCLUDING_ANY,
|
||||
'filter' => ['something'],
|
||||
],
|
||||
];
|
||||
$contact = (new Lead())->setId($contactId);
|
||||
|
||||
$event = new ContactFiltersEvaluateEvent($filters, $contact);
|
||||
|
||||
$this->segmentRepository->expects(self::once())
|
||||
->method('isContactInSegments')
|
||||
->with($contactId, $filters[0]['filter'])
|
||||
->willReturn(true);
|
||||
|
||||
$this->subscriber->onContactFilterEvaluate($event);
|
||||
self::assertTrue($event->isEvaluated());
|
||||
self::assertTrue($event->isMatched());
|
||||
}
|
||||
|
||||
public function testOnContactFilterEvaluateNotNotIn(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$filters = [
|
||||
[
|
||||
'type' => 'leadlist',
|
||||
'operator' => OperatorOptions::EXCLUDING_ANY,
|
||||
'filter' => ['something'],
|
||||
],
|
||||
];
|
||||
$contact = (new Lead())->setId($contactId);
|
||||
|
||||
$event = new ContactFiltersEvaluateEvent($filters, $contact);
|
||||
|
||||
$this->segmentRepository->expects(self::once())
|
||||
->method('isNotContactInSegments')
|
||||
->with($contactId, $filters[0]['filter'])
|
||||
->willReturn(true);
|
||||
|
||||
$this->subscriber->onContactFilterEvaluate($event);
|
||||
self::assertTrue($event->isEvaluated());
|
||||
self::assertTrue($event->isMatched());
|
||||
}
|
||||
|
||||
public function testOnContactFilterEvaluateNotInAll(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$filters = [
|
||||
[
|
||||
'type' => 'leadlist',
|
||||
'operator' => OperatorOptions::INCLUDING_ALL,
|
||||
'filter' => ['something'],
|
||||
],
|
||||
];
|
||||
$contact = (new Lead())->setId($contactId);
|
||||
|
||||
$event = new ContactFiltersEvaluateEvent($filters, $contact);
|
||||
|
||||
$this->segmentRepository->expects(self::once())
|
||||
->method('isContactInAllSegments')
|
||||
->with($contactId, $filters[0]['filter'])
|
||||
->willReturn(true);
|
||||
|
||||
$this->subscriber->onContactFilterEvaluate($event);
|
||||
self::assertTrue($event->isEvaluated());
|
||||
self::assertTrue($event->isMatched());
|
||||
}
|
||||
|
||||
public function testOnContactFilterEvaluateNotNotInAll(): void
|
||||
{
|
||||
$contactId = 1;
|
||||
$filters = [
|
||||
[
|
||||
'type' => 'leadlist',
|
||||
'operator' => OperatorOptions::EXCLUDING_ALL,
|
||||
'filter' => ['something'],
|
||||
],
|
||||
];
|
||||
$contact = (new Lead())->setId($contactId);
|
||||
|
||||
$event = new ContactFiltersEvaluateEvent($filters, $contact);
|
||||
|
||||
$this->segmentRepository->expects(self::once())
|
||||
->method('isNotContactInAllSegments')
|
||||
->with($contactId, $filters[0]['filter'])
|
||||
->willReturn(true);
|
||||
|
||||
$this->subscriber->onContactFilterEvaluate($event);
|
||||
self::assertTrue($event->isEvaluated());
|
||||
self::assertTrue($event->isMatched());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Event\TokenReplacementEvent;
|
||||
use Mautic\CoreBundle\Helper\BuilderTokenHelperFactory;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\EventListener\EmailSubscriber;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EmailSubscriberTest extends TestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('onEmailAddressReplacementProvider')]
|
||||
public function testOnEmailAddressReplacement(string $value, string $expected): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setFields(['email2' => 'contact.a@email.address']);
|
||||
|
||||
$event = new TokenReplacementEvent($value, $contact);
|
||||
$emailSubscriber = new EmailSubscriber(
|
||||
new class extends BuilderTokenHelperFactory {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$emailSubscriber->onEmailAddressReplacement($event);
|
||||
|
||||
Assert::assertSame($expected, $event->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string[]>
|
||||
*/
|
||||
public static function onEmailAddressReplacementProvider(): \Generator
|
||||
{
|
||||
yield ['{contactfield=unicorn}', ''];
|
||||
yield ['{contactfield=unicorn|default@value.email}', 'default@value.email'];
|
||||
yield ['{contactfield=email2}', 'contact.a@email.address'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,811 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Event\LeadListFiltersChoicesEvent;
|
||||
use Mautic\LeadBundle\Event\LeadListFiltersOperatorsEvent;
|
||||
use Mautic\LeadBundle\EventListener\FilterOperatorSubscriber;
|
||||
use Mautic\LeadBundle\Exception\ChoicesNotFoundException;
|
||||
use Mautic\LeadBundle\Provider\FieldChoicesProviderInterface;
|
||||
use Mautic\LeadBundle\Provider\TypeOperatorProviderInterface;
|
||||
use Mautic\LeadBundle\Segment\OperatorOptions;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class FilterOperatorSubscriberTest extends TestCase
|
||||
{
|
||||
private OperatorOptions $operatorOptions;
|
||||
|
||||
/**
|
||||
* @var MockObject|LeadFieldRepository
|
||||
*/
|
||||
private MockObject $leadFieldRepository;
|
||||
|
||||
/**
|
||||
* @var MockObject|TypeOperatorProviderInterface
|
||||
*/
|
||||
private MockObject $typeOperatorProvider;
|
||||
|
||||
/**
|
||||
* @var MockObject|FieldChoicesProviderInterface
|
||||
*/
|
||||
private MockObject $fieldChoicesProvider;
|
||||
|
||||
/**
|
||||
* @var MockObject|TranslatorInterface
|
||||
*/
|
||||
private MockObject $translator;
|
||||
|
||||
private FilterOperatorSubscriber $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->operatorOptions = new OperatorOptions();
|
||||
$this->leadFieldRepository = $this->createMock(LeadFieldRepository::class);
|
||||
$this->typeOperatorProvider = $this->createMock(TypeOperatorProviderInterface::class);
|
||||
$this->fieldChoicesProvider = $this->createMock(FieldChoicesProviderInterface::class);
|
||||
$this->translator = $this->createMock(TranslatorInterface::class);
|
||||
|
||||
$this->subscriber = new FilterOperatorSubscriber(
|
||||
$this->operatorOptions,
|
||||
$this->leadFieldRepository,
|
||||
$this->typeOperatorProvider,
|
||||
$this->fieldChoicesProvider,
|
||||
$this->translator
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnListOperatorsGenerate(): void
|
||||
{
|
||||
$event = new LeadListFiltersOperatorsEvent([], $this->translator);
|
||||
|
||||
$this->subscriber->onListOperatorsGenerate($event);
|
||||
|
||||
$operators = $event->getOperators();
|
||||
|
||||
// Test that random operators exist:
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.form.operator.notbetween',
|
||||
'expr' => 'notBetween',
|
||||
'negate_expr' => 'between',
|
||||
'hide' => true,
|
||||
],
|
||||
$operators['!between']
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.core.operator.starts.with',
|
||||
'expr' => 'startsWith',
|
||||
'negate_expr' => 'startsWith',
|
||||
],
|
||||
$operators['startsWith']
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.form.operator.in',
|
||||
'expr' => 'in',
|
||||
'negate_expr' => 'notIn',
|
||||
],
|
||||
$operators['in']
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnGenerateSegmentFiltersAddCustomFieldsForBooleanTypes(): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$event = new LeadListFiltersChoicesEvent([], [], $this->translator);
|
||||
|
||||
$field->setType('boolean');
|
||||
$field->setLabel('Test Bool');
|
||||
$field->setAlias('test_bool');
|
||||
$field->setProperties([
|
||||
'no' => 'No',
|
||||
'yes' => 'Yes',
|
||||
]);
|
||||
|
||||
$this->leadFieldRepository->expects($this->once())
|
||||
->method('getListablePublishedFields')
|
||||
->willReturn(new ArrayCollection([$field]));
|
||||
|
||||
$this->typeOperatorProvider->expects($this->once())
|
||||
->method('getOperatorsForFieldType')
|
||||
->with('boolean')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->subscriber->onGenerateSegmentFiltersAddCustomFields($event);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'lead' => [
|
||||
'test_bool' => [
|
||||
'label' => 'Test Bool',
|
||||
'properties' => [
|
||||
'no' => 'No',
|
||||
'yes' => 'Yes',
|
||||
'type' => 'boolean',
|
||||
'list' => [
|
||||
'No' => 0,
|
||||
'Yes' => 1,
|
||||
],
|
||||
],
|
||||
'object' => 'lead',
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'iconClass' => 'ri-shapes-line',
|
||||
],
|
||||
],
|
||||
],
|
||||
$event->getChoices()
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnGenerateSegmentFiltersAddCustomFieldsForSelectTypes(): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$event = new LeadListFiltersChoicesEvent([], [], $this->translator);
|
||||
|
||||
$field->setType('select');
|
||||
$field->setLabel('Test Select');
|
||||
$field->setAlias('test_select');
|
||||
$field->setProperties([
|
||||
'list' => [
|
||||
'one' => 'One',
|
||||
'two' => 'Two',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->leadFieldRepository->expects($this->once())
|
||||
->method('getListablePublishedFields')
|
||||
->willReturn(new ArrayCollection([$field]));
|
||||
|
||||
$this->typeOperatorProvider->expects($this->once())
|
||||
->method('getOperatorsForFieldType')
|
||||
->with('select')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->subscriber->onGenerateSegmentFiltersAddCustomFields($event);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'lead' => [
|
||||
'test_select' => [
|
||||
'label' => 'Test Select',
|
||||
'properties' => [
|
||||
'list' => [
|
||||
'One' => 'one',
|
||||
'Two' => 'two',
|
||||
],
|
||||
'type' => 'select',
|
||||
],
|
||||
'object' => 'lead',
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'iconClass' => 'ri-shapes-line',
|
||||
],
|
||||
],
|
||||
],
|
||||
$event->getChoices()
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnGenerateSegmentFiltersAddCustomFieldsForCountryTypes(): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$event = new LeadListFiltersChoicesEvent([], [], $this->translator);
|
||||
|
||||
$field->setType('country');
|
||||
$field->setLabel('Test Country');
|
||||
$field->setAlias('test_country');
|
||||
|
||||
$this->leadFieldRepository->expects($this->once())
|
||||
->method('getListablePublishedFields')
|
||||
->willReturn(new ArrayCollection([$field]));
|
||||
|
||||
$this->typeOperatorProvider->expects($this->once())
|
||||
->method('getOperatorsForFieldType')
|
||||
->with('country')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->fieldChoicesProvider->expects($this->once())
|
||||
->method('getChoicesForField')
|
||||
->with('country')
|
||||
->willReturn(
|
||||
[
|
||||
'Czech Republic' => 'Czech Republic',
|
||||
'Slovak Republic' => 'Slovak Republic',
|
||||
]
|
||||
);
|
||||
|
||||
$this->subscriber->onGenerateSegmentFiltersAddCustomFields($event);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'lead' => [
|
||||
'test_country' => [
|
||||
'label' => 'Test Country',
|
||||
'properties' => [
|
||||
'type' => 'country',
|
||||
'list' => [
|
||||
'Czech Republic' => 'Czech Republic',
|
||||
'Slovak Republic' => 'Slovak Republic',
|
||||
],
|
||||
],
|
||||
'object' => 'lead',
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'iconClass' => 'ri-shapes-line',
|
||||
],
|
||||
],
|
||||
],
|
||||
$event->getChoices()
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnGenerateSegmentFiltersAddCustomFieldsForTextTypes(): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$event = new LeadListFiltersChoicesEvent([], [], $this->translator);
|
||||
|
||||
$field->setType('text');
|
||||
$field->setLabel('Test Text');
|
||||
$field->setAlias('test_text');
|
||||
$field->setObject('company');
|
||||
|
||||
$this->leadFieldRepository->expects($this->once())
|
||||
->method('getListablePublishedFields')
|
||||
->willReturn(new ArrayCollection([$field]));
|
||||
|
||||
$this->typeOperatorProvider->expects($this->once())
|
||||
->method('getOperatorsForFieldType')
|
||||
->with('text')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->fieldChoicesProvider->expects($this->once())
|
||||
->method('getChoicesForField')
|
||||
->with('text')
|
||||
->willThrowException(new ChoicesNotFoundException());
|
||||
|
||||
$this->subscriber->onGenerateSegmentFiltersAddCustomFields($event);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'company' => [
|
||||
'test_text' => [
|
||||
'label' => 'Test Text',
|
||||
'properties' => [
|
||||
'type' => 'text',
|
||||
],
|
||||
'object' => 'company',
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'iconClass' => 'ri-shapes-line',
|
||||
],
|
||||
],
|
||||
],
|
||||
$event->getChoices()
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnGenerateSegmentFiltersAddStaticFields(): void
|
||||
{
|
||||
// Only displays on segment actions
|
||||
$request = new Request();
|
||||
$request->attributes->set('_route', 'mautic_segment_action');
|
||||
|
||||
$event = new LeadListFiltersChoicesEvent([], [], $this->translator, $request);
|
||||
|
||||
$this->typeOperatorProvider->expects($this->any())
|
||||
->method('getOperatorsForFieldType')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->typeOperatorProvider->expects($this->any())
|
||||
->method('getOperatorsIncluding')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->fieldChoicesProvider->expects($this->any())
|
||||
->method('getChoicesForField')
|
||||
->willReturn(
|
||||
[
|
||||
'Choice A' => 'choice_a',
|
||||
'Choice B' => 'choice_b',
|
||||
]
|
||||
);
|
||||
|
||||
$this->translator->expects($this->any())
|
||||
->method('trans')
|
||||
->willReturnArgument(0);
|
||||
|
||||
$this->subscriber->onGenerateSegmentFiltersAddStaticFields($event);
|
||||
|
||||
$choices = $event->getChoices();
|
||||
|
||||
// Test for some random choices:
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.filter.date_identified',
|
||||
'properties' => [
|
||||
'type' => 'datetime',
|
||||
],
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'object' => 'lead',
|
||||
'iconClass' => 'ri-calendar-todo-line',
|
||||
],
|
||||
$choices['lead']['date_identified']
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.filter.device_model',
|
||||
'properties' => [
|
||||
'type' => 'text',
|
||||
],
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'object' => 'lead',
|
||||
'iconClass' => 'ri-device-line',
|
||||
],
|
||||
$choices['lead']['device_model']
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.filter.dnc_manual_email',
|
||||
'properties' => [
|
||||
'type' => 'boolean',
|
||||
'list' => [
|
||||
'Choice A' => 'choice_a',
|
||||
'Choice B' => 'choice_b',
|
||||
],
|
||||
],
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'object' => 'lead',
|
||||
'iconClass' => 'ri-mail-forbid-line',
|
||||
],
|
||||
$choices['lead']['dnc_manual_email']
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnGenerateSegmentFiltersAddBehaviors(): void
|
||||
{
|
||||
// Only displays on segment actions
|
||||
$request = new Request();
|
||||
$request->attributes->set('_route', 'mautic_segment_action');
|
||||
|
||||
$event = new LeadListFiltersChoicesEvent([], [], $this->translator, $request);
|
||||
|
||||
$this->typeOperatorProvider->expects($this->any())
|
||||
->method('getOperatorsForFieldType')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->typeOperatorProvider->expects($this->any())
|
||||
->method('getOperatorsIncluding')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->fieldChoicesProvider->expects($this->any())
|
||||
->method('getChoicesForField')
|
||||
->willReturn(
|
||||
[
|
||||
'Choice A' => 'choice_a',
|
||||
'Choice B' => 'choice_b',
|
||||
]
|
||||
);
|
||||
|
||||
$this->translator->expects($this->any())
|
||||
->method('trans')
|
||||
->willReturnArgument(0);
|
||||
|
||||
$this->subscriber->onGenerateSegmentFiltersAddBehaviors($event);
|
||||
|
||||
$choices = $event->getChoices();
|
||||
|
||||
// Test for some random choices:
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.filter.lead_email_received',
|
||||
'object' => 'lead',
|
||||
'properties' => [
|
||||
'type' => 'lead_email_received',
|
||||
'list' => [
|
||||
'Choice A' => 'choice_a',
|
||||
'Choice B' => 'choice_b',
|
||||
],
|
||||
],
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'iconClass' => 'ri-mail-open-line',
|
||||
],
|
||||
$choices['behaviors']['lead_email_received']
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.filter.visited_url_count',
|
||||
'properties' => [
|
||||
'type' => 'number',
|
||||
],
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'object' => 'lead',
|
||||
'iconClass' => 'ri-external-link-line',
|
||||
],
|
||||
$choices['behaviors']['hit_url_count']
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnlyCustomFieldsAreLoadedForNonSegmentRoutes(): void
|
||||
{
|
||||
$request = new Request();
|
||||
$request->attributes->set('_route', 'mautic_dynamicContent_action');
|
||||
|
||||
$event = new LeadListFiltersChoicesEvent([], [], $this->translator, $request);
|
||||
|
||||
$field = new LeadField();
|
||||
$field->setType('select');
|
||||
$field->setLabel('Test Select');
|
||||
$field->setAlias('test_select');
|
||||
$field->setProperties([
|
||||
'list' => [
|
||||
'one' => 'One',
|
||||
'two' => 'Two',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->leadFieldRepository->expects($this->once())
|
||||
->method('getListablePublishedFields')
|
||||
->willReturn(new ArrayCollection([$field]));
|
||||
|
||||
$this->typeOperatorProvider->expects($this->any())
|
||||
->method('getOperatorsForFieldType')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->typeOperatorProvider->expects($this->any())
|
||||
->method('getOperatorsIncluding')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->fieldChoicesProvider->expects($this->any())
|
||||
->method('getChoicesForField')
|
||||
->willReturn(
|
||||
[
|
||||
'Choice A' => 'choice_a',
|
||||
'Choice B' => 'choice_b',
|
||||
]
|
||||
);
|
||||
|
||||
$this->translator->expects($this->any())
|
||||
->method('trans')
|
||||
->willReturnArgument(0);
|
||||
|
||||
$this->subscriber->onGenerateSegmentFiltersAddCustomFields($event);
|
||||
$this->subscriber->onGenerateSegmentFiltersAddStaticFields($event);
|
||||
$this->subscriber->onGenerateSegmentFiltersAddBehaviors($event);
|
||||
|
||||
$choices = $event->getChoices();
|
||||
|
||||
// Only custom fields should be shown
|
||||
Assert::assertArrayHasKey('lead', $choices);
|
||||
Assert::assertArrayHasKey('test_select', $choices['lead']);
|
||||
|
||||
// Static fields should not be included
|
||||
Assert::assertArrayNotHasKey('utm_source', $choices['lead']);
|
||||
|
||||
// Behaviors should not be included
|
||||
Assert::assertArrayNotHasKey('behaviors', $choices);
|
||||
}
|
||||
|
||||
public function testOnGenerateSegmentFiltersAddCustomFieldsForTextTypesForValueAjaxRequest(): void
|
||||
{
|
||||
// Only displays on segment actions
|
||||
$request = new Request();
|
||||
$request->attributes->set('action', 'loadSegmentFilterForm');
|
||||
|
||||
$event = new LeadListFiltersChoicesEvent([], [], $this->translator, $request);
|
||||
|
||||
$field = new LeadField();
|
||||
$field->setType('text');
|
||||
$field->setLabel('Test Text');
|
||||
$field->setAlias('test_text');
|
||||
$field->setObject('company');
|
||||
|
||||
$this->leadFieldRepository->expects($this->once())
|
||||
->method('getListablePublishedFields')
|
||||
->willReturn(new ArrayCollection([$field]));
|
||||
|
||||
$this->typeOperatorProvider->expects($this->once())
|
||||
->method('getOperatorsForFieldType')
|
||||
->with('text')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->fieldChoicesProvider->expects($this->once())
|
||||
->method('getChoicesForField')
|
||||
->with('text')
|
||||
->willThrowException(new ChoicesNotFoundException());
|
||||
|
||||
$this->subscriber->onGenerateSegmentFiltersAddCustomFields($event);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'company' => [
|
||||
'test_text' => [
|
||||
'label' => 'Test Text',
|
||||
'properties' => [
|
||||
'type' => 'text',
|
||||
],
|
||||
'object' => 'company',
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'iconClass' => 'ri-shapes-line',
|
||||
],
|
||||
],
|
||||
],
|
||||
$event->getChoices()
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnGenerateSegmentFiltersAddStaticFieldsForValueAjaxRequest(): void
|
||||
{
|
||||
// Only displays on segment actions
|
||||
$request = new Request();
|
||||
$request->attributes->set('action', 'loadSegmentFilterForm');
|
||||
|
||||
$event = new LeadListFiltersChoicesEvent([], [], $this->translator, $request);
|
||||
|
||||
$this->typeOperatorProvider->expects($this->any())
|
||||
->method('getOperatorsForFieldType')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->typeOperatorProvider->expects($this->any())
|
||||
->method('getOperatorsIncluding')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->fieldChoicesProvider->expects($this->any())
|
||||
->method('getChoicesForField')
|
||||
->willReturn(
|
||||
[
|
||||
'Choice A' => 'choice_a',
|
||||
'Choice B' => 'choice_b',
|
||||
]
|
||||
);
|
||||
|
||||
$this->translator->expects($this->any())
|
||||
->method('trans')
|
||||
->willReturnArgument(0);
|
||||
|
||||
$this->subscriber->onGenerateSegmentFiltersAddStaticFields($event);
|
||||
|
||||
$choices = $event->getChoices();
|
||||
|
||||
// Test for some random choices:
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.filter.date_identified',
|
||||
'properties' => [
|
||||
'type' => 'datetime',
|
||||
],
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'object' => 'lead',
|
||||
'iconClass' => 'ri-calendar-todo-line',
|
||||
],
|
||||
$choices['lead']['date_identified']
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.filter.device_model',
|
||||
'properties' => [
|
||||
'type' => 'text',
|
||||
],
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'object' => 'lead',
|
||||
'iconClass' => 'ri-device-line',
|
||||
],
|
||||
$choices['lead']['device_model']
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.filter.dnc_manual_email',
|
||||
'properties' => [
|
||||
'type' => 'boolean',
|
||||
'list' => [
|
||||
'Choice A' => 'choice_a',
|
||||
'Choice B' => 'choice_b',
|
||||
],
|
||||
],
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'object' => 'lead',
|
||||
'iconClass' => 'ri-mail-forbid-line',
|
||||
],
|
||||
$choices['lead']['dnc_manual_email']
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnGenerateSegmentFiltersAddBehaviorsForValueAjaxRequest(): void
|
||||
{
|
||||
// Only displays on segment actions
|
||||
$request = new Request();
|
||||
$request->attributes->set('action', 'loadSegmentFilterForm');
|
||||
|
||||
$event = new LeadListFiltersChoicesEvent([], [], $this->translator, $request);
|
||||
|
||||
$this->typeOperatorProvider->expects($this->any())
|
||||
->method('getOperatorsForFieldType')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->typeOperatorProvider->expects($this->any())
|
||||
->method('getOperatorsIncluding')
|
||||
->willReturn(
|
||||
[
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
]
|
||||
);
|
||||
|
||||
$this->fieldChoicesProvider->expects($this->any())
|
||||
->method('getChoicesForField')
|
||||
->willReturn(
|
||||
[
|
||||
'Choice A' => 'choice_a',
|
||||
'Choice B' => 'choice_b',
|
||||
]
|
||||
);
|
||||
|
||||
$this->translator->expects($this->any())
|
||||
->method('trans')
|
||||
->willReturnArgument(0);
|
||||
|
||||
$this->subscriber->onGenerateSegmentFiltersAddBehaviors($event);
|
||||
|
||||
$choices = $event->getChoices();
|
||||
|
||||
// Test for some random choices:
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.filter.lead_email_received',
|
||||
'object' => 'lead',
|
||||
'properties' => [
|
||||
'type' => 'lead_email_received',
|
||||
'list' => [
|
||||
'Choice A' => 'choice_a',
|
||||
'Choice B' => 'choice_b',
|
||||
],
|
||||
],
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'iconClass' => 'ri-mail-open-line',
|
||||
],
|
||||
$choices['behaviors']['lead_email_received']
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'label' => 'mautic.lead.list.filter.visited_url_count',
|
||||
'properties' => [
|
||||
'type' => 'number',
|
||||
],
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
],
|
||||
'object' => 'lead',
|
||||
'iconClass' => 'ri-external-link-line',
|
||||
],
|
||||
$choices['behaviors']['hit_url_count']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\FormBundle\Entity\Action;
|
||||
use Mautic\FormBundle\Entity\Form;
|
||||
use Mautic\FormBundle\Entity\Submission;
|
||||
use Mautic\FormBundle\Event\SubmissionEvent;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\EventListener\FormSubscriber;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Model\DoNotContact;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PointBundle\Model\PointGroupModel;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class FormSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var DoNotContact|(DoNotContact&MockObject)|MockObject
|
||||
*/
|
||||
private DoNotContact|MockObject $doNotContact;
|
||||
|
||||
/**
|
||||
* @var LeadModel|MockObject
|
||||
*/
|
||||
private MockObject $leadModel;
|
||||
|
||||
/**
|
||||
* @var PointGroupModel|(PointGroupModel&object&MockObject)|(PointGroupModel&MockObject)|(object&MockObject)|MockObject
|
||||
*/
|
||||
private MockObject|PointGroupModel $pointGroupModel;
|
||||
|
||||
private FormSubscriber $subscriber;
|
||||
|
||||
/**
|
||||
* @var MockObject|ContactTracker
|
||||
*/
|
||||
private MockObject $contactTracker;
|
||||
|
||||
/**
|
||||
* @var MockObject|LeadFieldRepository
|
||||
*/
|
||||
private MockObject $leadFieldRepostory;
|
||||
|
||||
/**
|
||||
* @var MockObject|IpLookupHelper
|
||||
*/
|
||||
private MockObject $ipLookupHelper;
|
||||
|
||||
private MockObject $submissionEvent;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->leadModel = $this->createMock(LeadModel::class);
|
||||
$this->contactTracker = $this->createMock(ContactTracker::class);
|
||||
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$this->leadFieldRepostory = $this->createMock(LeadFieldRepository::class);
|
||||
$this->pointGroupModel = $this->createMock(PointGroupModel::class);
|
||||
$this->doNotContact = $this->createMock(DoNotContact::class);
|
||||
$this->submissionEvent = $this->createMock(SubmissionEvent::class);
|
||||
$this->subscriber = new FormSubscriber(
|
||||
$this->leadModel,
|
||||
$this->contactTracker,
|
||||
$this->ipLookupHelper,
|
||||
$this->leadFieldRepostory,
|
||||
$this->pointGroupModel,
|
||||
$this->doNotContact
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnFormSubmitActionChangePoints(): void
|
||||
{
|
||||
$this->contactTracker->method('getContact')->willReturn(new Lead());
|
||||
|
||||
$this->ipLookupHelper->method('getIpAddress')->willReturn(new IpAddress());
|
||||
|
||||
$submission = new Submission();
|
||||
$submission->setForm(new Form());
|
||||
$submission->setLead(new Lead());
|
||||
|
||||
$submissionEvent = new SubmissionEvent($submission, [], [], new Request());
|
||||
|
||||
$action = new Action();
|
||||
$action->setType('lead.pointschange');
|
||||
$action->setProperties(['points' => 1, 'operator' => 'plus']);
|
||||
$submissionEvent->setAction($action);
|
||||
|
||||
$this->subscriber->onFormSubmitActionChangePoints($submissionEvent);
|
||||
|
||||
$this->assertEquals(1, $submissionEvent->getSubmission()->getLead()->getPoints());
|
||||
}
|
||||
|
||||
public function testThatTheLeadIsAddedToTheSegmentOnLeadOnSegmentsChangeEvent(): void
|
||||
{
|
||||
$this->submissionEvent
|
||||
->method('getActionConfig')
|
||||
->willReturn([
|
||||
'addToLists' => 1,
|
||||
'removeFromLists' => null,
|
||||
]);
|
||||
|
||||
$this->leadModel->expects($this->once())->method('addToLists');
|
||||
$this->subscriber->onLeadSegmentsChange($this->submissionEvent);
|
||||
}
|
||||
|
||||
public function testThatTheLeadIsRemovedFromTheSegmentOnLeadOnSegmentsChangeEvent(): void
|
||||
{
|
||||
$this->submissionEvent
|
||||
->method('getActionConfig')
|
||||
->willReturn([
|
||||
'removeFromLists' => 1,
|
||||
'addToLists' => null,
|
||||
]);
|
||||
|
||||
$this->leadModel->expects($this->once())->method('removeFromLists');
|
||||
$this->subscriber->onLeadSegmentsChange($this->submissionEvent);
|
||||
}
|
||||
|
||||
public function testThatTheObserverForTriggerOnLeadSegmentsChangeEventIsFired(): void
|
||||
{
|
||||
$subscribers = FormSubscriber::getSubscribedEvents();
|
||||
$this->assertArrayHasKey(LeadEvents::LEAD_ON_SEGMENTS_CHANGE, $subscribers);
|
||||
$this->assertSame(['onLeadSegmentsChange', 0], $subscribers[LeadEvents::LEAD_ON_SEGMENTS_CHANGE]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Doctrine\GeneratedColumn\GeneratedColumn;
|
||||
use Mautic\CoreBundle\Event\GeneratedColumnsEvent;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Event\LeadListFiltersChoicesEvent;
|
||||
use Mautic\LeadBundle\EventListener\GeneratedColumnSubscriber;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class GeneratedColumnSubscriberTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
private GeneratedColumnSubscriber $generatedColumnSubscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$modelTranslator = $this->createMock(Translator::class);
|
||||
$modelTranslator->expects(self::any())
|
||||
->method('trans')
|
||||
->willReturnArgument(0);
|
||||
|
||||
$segmentModel = new class($modelTranslator) extends ListModel {
|
||||
public function __construct(Translator $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
}
|
||||
};
|
||||
|
||||
$this->translator = $this->createMock(TranslatorInterface::class);
|
||||
$this->generatedColumnSubscriber = new GeneratedColumnSubscriber($segmentModel, $this->translator);
|
||||
}
|
||||
|
||||
public function testInGeneratedColumnsBuild(): void
|
||||
{
|
||||
$event = new GeneratedColumnsEvent();
|
||||
|
||||
$this->generatedColumnSubscriber->onGeneratedColumnsBuild($event);
|
||||
|
||||
/** @var GeneratedColumn $generatedColumn */
|
||||
$generatedColumn = $event->getGeneratedColumns()->current();
|
||||
|
||||
Assert::assertSame(MAUTIC_TABLE_PREFIX.'leads', $generatedColumn->getTableName());
|
||||
Assert::assertSame('generated_email_domain', $generatedColumn->getColumnName());
|
||||
Assert::assertSame('VARCHAR(255) AS (SUBSTRING(email, LOCATE("@", email) + 1)) COMMENT \'(DC2Type:generated)\'', $generatedColumn->getColumnDefinition());
|
||||
}
|
||||
|
||||
public function testOnGenerateSegmentFilters(): void
|
||||
{
|
||||
$event = new LeadListFiltersChoicesEvent(
|
||||
[],
|
||||
[],
|
||||
$this->translator,
|
||||
new Request()
|
||||
);
|
||||
|
||||
$this->translator->method('trans')
|
||||
->with('mautic.email.segment.choice.generated_email_domain')
|
||||
->willReturn('translated string');
|
||||
|
||||
$this->generatedColumnSubscriber->onGenerateSegmentFilters($event);
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'label' => 'translated string',
|
||||
'properties' => ['type' => 'text'],
|
||||
'operators' => [
|
||||
'mautic.lead.list.form.operator.equals' => '=',
|
||||
'mautic.lead.list.form.operator.notequals' => '!=',
|
||||
'mautic.lead.list.form.operator.isempty' => 'empty',
|
||||
'mautic.lead.list.form.operator.isnotempty' => '!empty',
|
||||
'mautic.lead.list.form.operator.islike' => 'like',
|
||||
'mautic.lead.list.form.operator.isnotlike' => '!like',
|
||||
'mautic.lead.list.form.operator.regexp' => 'regexp',
|
||||
'mautic.lead.list.form.operator.notregexp' => '!regexp',
|
||||
'mautic.core.operator.starts.with' => 'startsWith',
|
||||
'mautic.core.operator.ends.with' => 'endsWith',
|
||||
'mautic.core.operator.contains' => 'contains',
|
||||
],
|
||||
'object' => 'lead',
|
||||
'iconClass' => 'ri-at-line',
|
||||
],
|
||||
$event->getChoices()['lead']['generated_email_domain']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLog;
|
||||
use Mautic\LeadBundle\Event\ImportInitEvent;
|
||||
use Mautic\LeadBundle\Event\ImportMappingEvent;
|
||||
use Mautic\LeadBundle\Event\ImportProcessEvent;
|
||||
use Mautic\LeadBundle\Event\ImportValidateEvent;
|
||||
use Mautic\LeadBundle\EventListener\ImportCompanySubscriber;
|
||||
use Mautic\LeadBundle\Field\FieldList;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Translation\Translator;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class ImportCompanySubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testOnImportInitForUknownObject(): void
|
||||
{
|
||||
$subscriber = new ImportCompanySubscriber(
|
||||
$this->getFieldListFake(),
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getCompanyModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$event = new ImportInitEvent('unicorn');
|
||||
$subscriber->onImportInit($event);
|
||||
Assert::assertFalse($event->objectSupported);
|
||||
}
|
||||
|
||||
public function testOnImportInitForContactsObjectWithoutPermissions(): void
|
||||
{
|
||||
$subscriber = new ImportCompanySubscriber(
|
||||
$this->getFieldListFake(),
|
||||
new class extends CorePermissions {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $requestedPermission
|
||||
*/
|
||||
public function isGranted($requestedPermission, $mode = 'MATCH_ALL', $userEntity = null, $allowUnknown = false)
|
||||
{
|
||||
Assert::assertSame('lead:imports:create', $requestedPermission);
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
$this->getCompanyModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$event = new ImportInitEvent('companies');
|
||||
$this->expectException(AccessDeniedException::class);
|
||||
$subscriber->onImportInit($event);
|
||||
}
|
||||
|
||||
public function testOnImportInitForContactsObjectWithPermissions(): void
|
||||
{
|
||||
$subscriber = new ImportCompanySubscriber(
|
||||
$this->getFieldListFake(),
|
||||
new class extends CorePermissions {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $requestedPermission
|
||||
*/
|
||||
public function isGranted($requestedPermission, $mode = 'MATCH_ALL', $userEntity = null, $allowUnknown = false)
|
||||
{
|
||||
Assert::assertSame('lead:imports:create', $requestedPermission);
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
$this->getCompanyModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$event = new ImportInitEvent('companies');
|
||||
$subscriber->onImportInit($event);
|
||||
Assert::assertTrue($event->objectSupported);
|
||||
Assert::assertSame('company', $event->objectSingular);
|
||||
Assert::assertSame('mautic.lead.lead.companies', $event->objectName);
|
||||
Assert::assertSame('#mautic_company_index', $event->activeLink);
|
||||
Assert::assertSame('mautic_company_index', $event->indexRoute);
|
||||
}
|
||||
|
||||
public function testOnFieldMappingForUnknownObject(): void
|
||||
{
|
||||
$subscriber = new ImportCompanySubscriber(
|
||||
$this->getFieldListFake(),
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getCompanyModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$event = new ImportMappingEvent('unicorn');
|
||||
$subscriber->onFieldMapping($event);
|
||||
Assert::assertFalse($event->objectSupported);
|
||||
}
|
||||
|
||||
public function testOnFieldMapping(): void
|
||||
{
|
||||
$subscriber = new ImportCompanySubscriber(
|
||||
new class extends FieldList {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $filters
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getFieldList(bool $byGroup = true, bool $alphabetical = true, array $filters = ['isPublished' => true, 'object' => 'lead']): array
|
||||
{
|
||||
return ['some fields'];
|
||||
}
|
||||
},
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getCompanyModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$event = new ImportMappingEvent('companies');
|
||||
$subscriber->onFieldMapping($event);
|
||||
Assert::assertTrue($event->objectSupported);
|
||||
Assert::assertSame(
|
||||
[
|
||||
'mautic.lead.company' => [
|
||||
'some fields',
|
||||
],
|
||||
'mautic.lead.special_fields' => [
|
||||
'dateAdded' => 'mautic.lead.import.label.dateAdded',
|
||||
'createdByUser' => 'mautic.lead.import.label.createdByUser',
|
||||
'dateModified' => 'mautic.lead.import.label.dateModified',
|
||||
'modifiedByUser' => 'mautic.lead.import.label.modifiedByUser',
|
||||
],
|
||||
],
|
||||
$event->fields
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnImportProcessForUnknownObject(): void
|
||||
{
|
||||
$subscriber = new ImportCompanySubscriber(
|
||||
$this->getFieldListFake(),
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getCompanyModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$import = new Import();
|
||||
$import->setObject('unicorn');
|
||||
$event = new ImportProcessEvent($import, new LeadEventLog(), []);
|
||||
$subscriber->onImportProcess($event);
|
||||
$this->expectException(\UnexpectedValueException::class);
|
||||
$event->wasMerged();
|
||||
}
|
||||
|
||||
public function testOnImportProcessForKnownObject(): void
|
||||
{
|
||||
$subscriber = new ImportCompanySubscriber(
|
||||
$this->getFieldListFake(),
|
||||
$this->getCorePermissionsFake(),
|
||||
new class extends CompanyModel {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function import(array $fields, array $data, $owner = null, bool $skipIfExists = false): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
},
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$import = new Import();
|
||||
$import->setObject('company');
|
||||
$event = new ImportProcessEvent($import, new LeadEventLog(), []);
|
||||
$subscriber->onImportProcess($event);
|
||||
Assert::assertTrue($event->wasMerged());
|
||||
}
|
||||
|
||||
public function testImportCompanySubscriberDoesHaveTranslatorInitialized(): void
|
||||
{
|
||||
/** @var FieldList&MockObject $fieldListMock */
|
||||
$fieldListMock = $this->createMock(FieldList::class);
|
||||
$missingRequiredFields = ['Company Name'];
|
||||
$matchedFields = ['Company Email'];
|
||||
$fieldListMock->expects($this->once())
|
||||
->method('getFieldList')
|
||||
->with(false, false, [
|
||||
'isPublished' => true,
|
||||
'object' => 'company',
|
||||
'isRequired' => true,
|
||||
])
|
||||
->willReturn($missingRequiredFields);
|
||||
|
||||
/** @var TranslatorInterface&MockObject $translatorInterfaceMock */
|
||||
$translatorInterfaceMock = $this->createMock(TranslatorInterface::class);
|
||||
$subscriber = new ImportCompanySubscriber(
|
||||
$fieldListMock,
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getCompanyModelFake(),
|
||||
$translatorInterfaceMock
|
||||
);
|
||||
|
||||
/** @var ImportValidateEvent&MockObject $importValidateEventMock */
|
||||
$importValidateEventMock = $this->createMock(ImportValidateEvent::class);
|
||||
$importValidateEventMock->expects($this->once())
|
||||
->method('importIsForRouteObject')
|
||||
->with('companies')
|
||||
->willReturn(true);
|
||||
|
||||
/** @var Form&MockObject $formMock */
|
||||
$formMock = $this->createMock(Form::class);
|
||||
$importValidateEventMock->expects($this->exactly(2))
|
||||
->method('getForm')
|
||||
->willReturn($formMock);
|
||||
$formMock->expects($this->once())
|
||||
->method('getData')
|
||||
->willReturnOnConsecutiveCalls($matchedFields);
|
||||
$translatorInterfaceMock->expects($this->once())
|
||||
->method('trans')
|
||||
->with(
|
||||
'mautic.import.missing.required.fields',
|
||||
[
|
||||
'%requiredFields%' => implode(', ', $missingRequiredFields),
|
||||
'%fieldOrFields%' => 'field',
|
||||
],
|
||||
'validators'
|
||||
)->willReturn('A translated message');
|
||||
|
||||
$subscriber->onValidateImport($importValidateEventMock);
|
||||
}
|
||||
|
||||
private function getFieldListFake(): FieldList
|
||||
{
|
||||
return new class extends FieldList {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private function getCorePermissionsFake(): CorePermissions
|
||||
{
|
||||
return new class extends CorePermissions {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private function getCompanyModelFake(): CompanyModel
|
||||
{
|
||||
return new class extends CompanyModel {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private function getTranslatorFake(): TranslatorInterface
|
||||
{
|
||||
return new class extends Translator {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLog;
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
use Mautic\LeadBundle\Event\ImportInitEvent;
|
||||
use Mautic\LeadBundle\Event\ImportMappingEvent;
|
||||
use Mautic\LeadBundle\Event\ImportProcessEvent;
|
||||
use Mautic\LeadBundle\Event\ImportValidateEvent;
|
||||
use Mautic\LeadBundle\EventListener\ImportContactSubscriber;
|
||||
use Mautic\LeadBundle\Field\FieldList;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class ImportContactSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testHandleValidateTags(): void
|
||||
{
|
||||
$tag = new Tag();
|
||||
$tag->setTag('tagLabel');
|
||||
|
||||
$formMock = $this->createMock(Form::class);
|
||||
$formMock->method('getData')
|
||||
->willReturn(
|
||||
[
|
||||
'name' => 'Bud',
|
||||
'tags' => new ArrayCollection([$tag]),
|
||||
]
|
||||
);
|
||||
|
||||
$event = new ImportValidateEvent('contacts', $formMock);
|
||||
$subscriber = new ImportContactSubscriber(
|
||||
new class extends FieldList {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function getFieldList(bool $byGroup = true, bool $alphabetical = true, array $filters = ['isPublished' => true, 'object' => 'lead']): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
},
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getLeadModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
|
||||
$subscriber->onValidateImport($event);
|
||||
|
||||
Assert::assertSame(['tagLabel'], $event->getTags());
|
||||
Assert::assertSame(['name' => 'Bud'], $event->getMatchedFields());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/mautic/mautic/issues/11080
|
||||
*/
|
||||
public function testHandleFieldWithIntValues(): void
|
||||
{
|
||||
$formMock = $this->createMock(Form::class);
|
||||
$formMock->method('getData')
|
||||
->willReturn(
|
||||
[
|
||||
'name' => 'Bud',
|
||||
'skip_if_exists' => 1,
|
||||
]
|
||||
);
|
||||
|
||||
$event = new ImportValidateEvent('contacts', $formMock);
|
||||
$subscriber = new ImportContactSubscriber(
|
||||
new class extends FieldList {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function getFieldList(bool $byGroup = true, bool $alphabetical = true, array $filters = ['isPublished' => true, 'object' => 'lead']): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
},
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getLeadModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
|
||||
$subscriber->onValidateImport($event);
|
||||
|
||||
Assert::assertSame(['name' => 'Bud'], $event->getMatchedFields());
|
||||
}
|
||||
|
||||
public function testOnImportInitForUknownObject(): void
|
||||
{
|
||||
$subscriber = new ImportContactSubscriber(
|
||||
$this->getFieldListFake(),
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getLeadModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$event = new ImportInitEvent('unicorn');
|
||||
$subscriber->onImportInit($event);
|
||||
Assert::assertFalse($event->objectSupported);
|
||||
}
|
||||
|
||||
public function testOnImportInitForContactsObjectWithoutPermissions(): void
|
||||
{
|
||||
$subscriber = new ImportContactSubscriber(
|
||||
$this->getFieldListFake(),
|
||||
new class extends CorePermissions {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $requestedPermission
|
||||
*/
|
||||
public function isGranted($requestedPermission, $mode = 'MATCH_ALL', $userEntity = null, $allowUnknown = false): bool
|
||||
{
|
||||
Assert::assertSame('lead:imports:create', $requestedPermission);
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
$this->getLeadModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$event = new ImportInitEvent('contacts');
|
||||
$this->expectException(AccessDeniedException::class);
|
||||
$subscriber->onImportInit($event);
|
||||
}
|
||||
|
||||
public function testOnImportInitForContactsObjectWithPermissions(): void
|
||||
{
|
||||
$subscriber = new ImportContactSubscriber(
|
||||
$this->getFieldListFake(),
|
||||
new class extends CorePermissions {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $requestedPermission
|
||||
*/
|
||||
public function isGranted($requestedPermission, $mode = 'MATCH_ALL', $userEntity = null, $allowUnknown = false): bool
|
||||
{
|
||||
Assert::assertSame('lead:imports:create', $requestedPermission);
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
$this->getLeadModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$event = new ImportInitEvent('contacts');
|
||||
$subscriber->onImportInit($event);
|
||||
Assert::assertTrue($event->objectSupported);
|
||||
Assert::assertSame('lead', $event->objectSingular);
|
||||
Assert::assertSame('mautic.lead.leads', $event->objectName);
|
||||
Assert::assertSame('#mautic_contact_index', $event->activeLink);
|
||||
Assert::assertSame('mautic_contact_index', $event->indexRoute);
|
||||
}
|
||||
|
||||
public function testOnFieldMappingForUnknownObject(): void
|
||||
{
|
||||
$subscriber = new ImportContactSubscriber(
|
||||
$this->getFieldListFake(),
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getLeadModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$event = new ImportMappingEvent('unicorn');
|
||||
$subscriber->onFieldMapping($event);
|
||||
Assert::assertFalse($event->objectSupported);
|
||||
}
|
||||
|
||||
public function testOnFieldMapping(): void
|
||||
{
|
||||
$subscriber = new ImportContactSubscriber(
|
||||
new class extends FieldList {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<bool|string> $filters
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getFieldList(bool $byGroup = true, bool $alphabetical = true, array $filters = ['isPublished' => true, 'object' => 'lead']): array
|
||||
{
|
||||
return ['some fields'];
|
||||
}
|
||||
},
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getLeadModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$event = new ImportMappingEvent('contacts');
|
||||
$subscriber->onFieldMapping($event);
|
||||
Assert::assertTrue($event->objectSupported);
|
||||
Assert::assertSame(
|
||||
[
|
||||
'mautic.lead.contact' => [
|
||||
'id' => 'mautic.lead.import.label.id',
|
||||
'some fields',
|
||||
],
|
||||
'mautic.lead.company' => [
|
||||
'some fields',
|
||||
],
|
||||
'mautic.lead.special_fields' => [
|
||||
'dateAdded' => 'mautic.lead.import.label.dateAdded',
|
||||
'createdByUser' => 'mautic.lead.import.label.createdByUser',
|
||||
'dateModified' => 'mautic.lead.import.label.dateModified',
|
||||
'modifiedByUser' => 'mautic.lead.import.label.modifiedByUser',
|
||||
'lastActive' => 'mautic.lead.import.label.lastActive',
|
||||
'dateIdentified' => 'mautic.lead.import.label.dateIdentified',
|
||||
'ip' => 'mautic.lead.import.label.ip',
|
||||
'stage' => 'mautic.lead.import.label.stage',
|
||||
'doNotEmail' => 'mautic.lead.import.label.doNotEmail',
|
||||
'ownerusername' => 'mautic.lead.import.label.ownerusername',
|
||||
'tags' => 'mautic.lead.import.label.tags',
|
||||
],
|
||||
],
|
||||
$event->fields
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnImportProcessForUnknownObject(): void
|
||||
{
|
||||
$subscriber = new ImportContactSubscriber(
|
||||
$this->getFieldListFake(),
|
||||
$this->getCorePermissionsFake(),
|
||||
$this->getLeadModelFake(),
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$import = new Import();
|
||||
$import->setObject('unicorn');
|
||||
$event = new ImportProcessEvent($import, new LeadEventLog(), []);
|
||||
$subscriber->onImportProcess($event);
|
||||
$this->expectException(\UnexpectedValueException::class);
|
||||
$event->wasMerged();
|
||||
}
|
||||
|
||||
public function testOnImportProcessForKnownObject(): void
|
||||
{
|
||||
$subscriber = new ImportContactSubscriber(
|
||||
$this->getFieldListFake(),
|
||||
$this->getCorePermissionsFake(),
|
||||
new class extends LeadModel {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function import(array $fields, array $data, $owner = null, $list = null, $tags = null, bool $persist = true, ?LeadEventLog $eventLog = null, $importId = null, bool $skipIfExists = false): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
},
|
||||
$this->getTranslatorFake()
|
||||
);
|
||||
$import = new Import();
|
||||
$import->setObject('lead');
|
||||
$event = new ImportProcessEvent($import, new LeadEventLog(), []);
|
||||
$subscriber->onImportProcess($event);
|
||||
Assert::assertTrue($event->wasMerged());
|
||||
}
|
||||
|
||||
private function getFieldListFake(): FieldList
|
||||
{
|
||||
return new class extends FieldList {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private function getCorePermissionsFake(): CorePermissions
|
||||
{
|
||||
return new class extends CorePermissions {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private function getLeadModelFake(): LeadModel
|
||||
{
|
||||
return new class extends LeadModel {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private function getTranslatorFake(): TranslatorInterface
|
||||
{
|
||||
return new class extends Translator {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Tester\ApplicationTester;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Issue9488Test extends MauticMysqlTestCase
|
||||
{
|
||||
private LeadRepository $contactRepository;
|
||||
|
||||
/**
|
||||
* @var array<int, array<string, string|int>>
|
||||
*/
|
||||
private array $contacts = [
|
||||
[
|
||||
'email' => 'contact1@email.com',
|
||||
'firstname' => 'Isaac',
|
||||
'lastname' => 'Asimov',
|
||||
],
|
||||
[
|
||||
'email' => 'contact2@email.com',
|
||||
'firstname' => 'Robert A.',
|
||||
'lastname' => 'Heinlein',
|
||||
'points' => 0,
|
||||
'preferred_locale' => 'af',
|
||||
],
|
||||
[
|
||||
'email' => 'contact3@email.com',
|
||||
'firstname' => 'Arthur C.',
|
||||
'lastname' => 'Clarke',
|
||||
'points' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
/** @var LeadRepository $leadRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
$this->contactRepository = $leadRepository;
|
||||
}
|
||||
|
||||
public function testUsesLocale(): void
|
||||
{
|
||||
$application = new Application(self::$kernel);
|
||||
$application->setAutoExit(false);
|
||||
$applicationTester = new ApplicationTester($application);
|
||||
|
||||
$contactIds = $this->createContacts();
|
||||
$campaign = $this->createCampaign($contactIds);
|
||||
|
||||
// Force Doctrine to re-fetch the entities otherwise the campaign won't know about any events.
|
||||
$this->em->clear();
|
||||
|
||||
// Execute the campaign.
|
||||
$exitCode = $applicationTester->run(
|
||||
[
|
||||
'command' => 'mautic:campaigns:trigger',
|
||||
'--campaign-id' => $campaign->getId(),
|
||||
]
|
||||
);
|
||||
|
||||
Assert::assertSame(0, $exitCode, $applicationTester->getDisplay());
|
||||
|
||||
/** @var Lead $contactA */
|
||||
$contactA = $this->contactRepository->getEntity($contactIds[0]);
|
||||
/** @var Lead $contactB */
|
||||
$contactB = $this->contactRepository->getEntity($contactIds[1]);
|
||||
/** @var Lead $contactC */
|
||||
$contactC = $this->contactRepository->getEntity($contactIds[2]);
|
||||
|
||||
Assert::assertSame(1, $contactA->getPoints());
|
||||
Assert::assertSame(2, $contactB->getPoints());
|
||||
Assert::assertSame(2, $contactC->getPoints());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
private function createContacts(): array
|
||||
{
|
||||
$this->client->request('POST', '/api/contacts/batch/new', $this->contacts);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
Assert::assertEquals(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
Assert::assertEquals(Response::HTTP_CREATED, $response['statusCodes'][0], $clientResponse->getContent());
|
||||
Assert::assertEquals(Response::HTTP_CREATED, $response['statusCodes'][1], $clientResponse->getContent());
|
||||
Assert::assertEquals(Response::HTTP_CREATED, $response['statusCodes'][2], $clientResponse->getContent());
|
||||
|
||||
return [
|
||||
$response['contacts'][0]['id'],
|
||||
$response['contacts'][1]['id'],
|
||||
$response['contacts'][2]['id'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $contactIds
|
||||
*/
|
||||
private function createCampaign(array $contactIds): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test Locale Decision');
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
foreach ($contactIds as $key => $contactId) {
|
||||
$campaignLead = new CampaignLead();
|
||||
$campaignLead->setCampaign($campaign);
|
||||
$campaignLead->setLead($this->em->getReference(Lead::class, $contactId));
|
||||
$campaignLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($campaignLead);
|
||||
$campaign->addLead($key, $campaignLead);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$condition = new Event();
|
||||
$condition->setCampaign($campaign);
|
||||
$condition->setName('Check preferred locale');
|
||||
$condition->setType('lead.field_value');
|
||||
$condition->setEventType(Event::TYPE_CONDITION);
|
||||
$condition->setTriggerMode(Event::TRIGGER_MODE_IMMEDIATE);
|
||||
$condition->setProperties(
|
||||
[
|
||||
'name' => '',
|
||||
'triggerMode' => 'immediate',
|
||||
'triggerDate' => null,
|
||||
'triggerInterval' => '1',
|
||||
'triggerIntervalUnit' => 'd',
|
||||
'triggerHour' => '',
|
||||
'triggerRestrictedStartHour' => '',
|
||||
'triggerRestrictedStopHour' => '',
|
||||
'anchor' => 'leadsource',
|
||||
'properties' => [
|
||||
'field' => 'preferred_locale',
|
||||
'operator' => '=',
|
||||
'value' => 'af',
|
||||
],
|
||||
'type' => 'lead.field_value',
|
||||
'eventType' => 'condition',
|
||||
'anchorEventType' => 'source',
|
||||
'campaignId' => $campaign->getId(),
|
||||
'_token' => 'HgysZwvH_n0uAp47CcAcsGddRnRk65t-3crOnuLx28Y',
|
||||
'buttons' => ['save' => ''],
|
||||
'field' => 'preferred_locale',
|
||||
'operator' => '=',
|
||||
'value' => 'af',
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->persist($condition);
|
||||
$this->em->flush();
|
||||
|
||||
$yesEvent = new Event();
|
||||
$yesEvent->setCampaign($campaign);
|
||||
$yesEvent->setName('Add 2 if locale');
|
||||
$yesEvent->setType('lead.changepoints');
|
||||
$yesEvent->setEventType(Event::TYPE_ACTION);
|
||||
$yesEvent->setTriggerMode(Event::TRIGGER_MODE_IMMEDIATE);
|
||||
$yesEvent->setDecisionPath(Event::PATH_ACTION);
|
||||
$yesEvent->setProperties(
|
||||
[
|
||||
'name' => '',
|
||||
'triggerMode' => 'immediate',
|
||||
'triggerDate' => null,
|
||||
'triggerInterval' => '1',
|
||||
'triggerIntervalUnit' => 'd',
|
||||
'triggerHour' => '',
|
||||
'triggerRestrictedStartHour' => '',
|
||||
'triggerRestrictedStopHour' => '',
|
||||
'anchor' => 'yes',
|
||||
'properties' => [
|
||||
'points' => '2',
|
||||
],
|
||||
'type' => 'lead.changepoints',
|
||||
'eventType' => 'action',
|
||||
'anchorEventType' => 'condition',
|
||||
'campaignId' => $campaign->getId(),
|
||||
'_token' => 'HgysZwvH_n0uAp47CcAcsGddRnRk65t-3crOnuLx28Y',
|
||||
'buttons' => ['save' => ''],
|
||||
'points' => '2',
|
||||
]
|
||||
);
|
||||
$yesEvent->setParent($condition);
|
||||
|
||||
$this->em->persist($yesEvent);
|
||||
$this->em->flush();
|
||||
|
||||
$noEvent = new Event();
|
||||
$noEvent->setCampaign($campaign);
|
||||
$noEvent->setName('Add 1 if not locale');
|
||||
$noEvent->setType('lead.changepoints');
|
||||
$noEvent->setEventType(Event::TYPE_ACTION);
|
||||
$noEvent->setTriggerMode(Event::TRIGGER_MODE_IMMEDIATE);
|
||||
$noEvent->setDecisionPath(Event::PATH_INACTION);
|
||||
$noEvent->setProperties(
|
||||
[
|
||||
'name' => '',
|
||||
'triggerMode' => 'immediate',
|
||||
'triggerDate' => null,
|
||||
'triggerInterval' => '1',
|
||||
'triggerIntervalUnit' => 'd',
|
||||
'triggerHour' => '',
|
||||
'triggerRestrictedStartHour' => '',
|
||||
'triggerRestrictedStopHour' => '',
|
||||
'anchor' => 'no',
|
||||
'properties' => [
|
||||
'points' => '1',
|
||||
],
|
||||
'type' => 'lead.changepoints',
|
||||
'eventType' => 'action',
|
||||
'anchorEventType' => 'condition',
|
||||
'campaignId' => $campaign->getId(),
|
||||
'_token' => 'HgysZwvH_n0uAp47CcAcsGddRnRk65t-3crOnuLx28Y',
|
||||
'buttons' => ['save' => ''],
|
||||
'points' => '1',
|
||||
]
|
||||
);
|
||||
$noEvent->setParent($condition);
|
||||
|
||||
$this->em->persist($noEvent);
|
||||
$this->em->flush();
|
||||
|
||||
$campaign->setCanvasSettings(
|
||||
[
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => $condition->getId(),
|
||||
'positionX' => '848',
|
||||
'positionY' => '139',
|
||||
],
|
||||
[
|
||||
'id' => $noEvent->getId(),
|
||||
'positionX' => '380',
|
||||
'positionY' => '244',
|
||||
],
|
||||
[
|
||||
'id' => $yesEvent->getId(),
|
||||
'positionX' => '948',
|
||||
'positionY' => '244',
|
||||
],
|
||||
[
|
||||
'id' => 'lists',
|
||||
'positionX' => '860',
|
||||
'positionY' => '50',
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => $condition->getId(),
|
||||
'anchors' => [
|
||||
[
|
||||
'endpoint' => 'leadsource',
|
||||
'eventId' => 'lists',
|
||||
],
|
||||
[
|
||||
'endpoint' => 'top',
|
||||
'eventId' => $condition->getId(),
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'sourceId' => $condition->getId(),
|
||||
'targetId' => $yesEvent->getId(),
|
||||
'anchors' => [
|
||||
[
|
||||
'endpoint' => 'yes',
|
||||
'eventId' => $condition->getId(),
|
||||
],
|
||||
[
|
||||
'endpoint' => 'top',
|
||||
'eventId' => $yesEvent->getId(),
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'sourceId' => $condition->getId(),
|
||||
'targetId' => $noEvent->getId(),
|
||||
'anchors' => [
|
||||
[
|
||||
'endpoint' => 'no',
|
||||
'eventId' => $condition->getId(),
|
||||
],
|
||||
[
|
||||
'endpoint' => 'top',
|
||||
'eventId' => $noEvent->getId(),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\CoreBundle\Tests\CommonMocks;
|
||||
use Mautic\LeadBundle\DataObject\LeadManipulator;
|
||||
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLog;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\LeadBundle\Entity\LeadListRepository;
|
||||
use Mautic\LeadBundle\Event\LeadEvent;
|
||||
use Mautic\LeadBundle\Event\LeadTimelineEvent;
|
||||
use Mautic\LeadBundle\EventListener\LeadSubscriber;
|
||||
use Mautic\LeadBundle\Helper\LeadChangeEventDispatcher;
|
||||
use Mautic\LeadBundle\Helper\SegmentCountCacheHelper;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Twig\Helper\DncReasonHelper;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class LeadSubscriberTest extends CommonMocks
|
||||
{
|
||||
/**
|
||||
* @var IpLookupHelper&MockObject
|
||||
*/
|
||||
private MockObject $ipLookupHelper;
|
||||
|
||||
/**
|
||||
* @var AuditLogModel&MockObject
|
||||
*/
|
||||
private MockObject $auditLogModel;
|
||||
|
||||
/**
|
||||
* @var LeadChangeEventDispatcher&MockObject
|
||||
*/
|
||||
private MockObject $leadEventDispatcher;
|
||||
|
||||
private DncReasonHelper $dncReasonHelper;
|
||||
|
||||
/**
|
||||
* @var EntityManager&MockObject
|
||||
*/
|
||||
private MockObject $entityManager;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface&MockObject
|
||||
*/
|
||||
private MockObject $translator;
|
||||
|
||||
/**
|
||||
* @var RouterInterface&MockObject
|
||||
*/
|
||||
private MockObject $router;
|
||||
|
||||
/**
|
||||
* @var ModelFactory<object>&MockObject
|
||||
*/
|
||||
private MockObject $modelFactory;
|
||||
|
||||
private LeadListRepository&MockObject $leadListRepository;
|
||||
|
||||
private SegmentCountCacheHelper&MockObject $segmentCountCacheHelper;
|
||||
|
||||
private CoreParametersHelper&MockObject $coreParametersHelper;
|
||||
|
||||
private CompanyLeadRepository&MockObject $companyLeadRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$this->auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$this->leadEventDispatcher = $this->createMock(LeadChangeEventDispatcher::class);
|
||||
$this->dncReasonHelper = new DncReasonHelper($this->createMock(TranslatorInterface::class));
|
||||
$this->entityManager = $this->createMock(EntityManager::class);
|
||||
$this->translator = $this->createMock(TranslatorInterface::class);
|
||||
$this->router = $this->createMock(RouterInterface::class);
|
||||
$this->modelFactory = $this->createMock(ModelFactory::class);
|
||||
$this->leadListRepository = $this->createMock(LeadListRepository::class);
|
||||
$this->segmentCountCacheHelper = $this->createMock(SegmentCountCacheHelper::class);
|
||||
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$this->companyLeadRepository = $this->createMock(CompanyLeadRepository::class);
|
||||
}
|
||||
|
||||
public function testOnLeadPostSaveWillNotProcessTheSameLeadTwice(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
|
||||
$lead->setId(54);
|
||||
|
||||
$changes = [
|
||||
'title' => [
|
||||
'0' => 'sdf',
|
||||
'1' => 'Mr.',
|
||||
],
|
||||
'fields' => [
|
||||
'firstname' => [
|
||||
'0' => 'Test',
|
||||
'1' => 'John',
|
||||
],
|
||||
'lastname' => [
|
||||
'0' => 'test',
|
||||
'1' => 'Doe',
|
||||
],
|
||||
'email' => [
|
||||
'0' => 'zrosa91@gmail.com',
|
||||
'1' => 'john@gmail.com',
|
||||
],
|
||||
'mobile' => [
|
||||
'0' => '345345',
|
||||
'1' => '555555555',
|
||||
],
|
||||
],
|
||||
'dateModified' => [
|
||||
'0' => '2017-08-21T15:50:57+00:00',
|
||||
'1' => '2017-08-22T08:04:31+00:00',
|
||||
],
|
||||
'dateLastActive' => [
|
||||
'0' => '2017-08-21T15:50:57+00:00',
|
||||
'1' => '2017-08-22T08:04:31+00:00',
|
||||
],
|
||||
];
|
||||
|
||||
$lead->setChanges($changes);
|
||||
|
||||
// This method will be called exactly once
|
||||
// even though the onLeadPostSave was called twice for the same lead
|
||||
$this->auditLogModel->expects($this->once())
|
||||
->method('writeToLog');
|
||||
|
||||
$subscriber = new LeadSubscriber(
|
||||
$this->ipLookupHelper,
|
||||
$this->auditLogModel,
|
||||
$this->leadEventDispatcher,
|
||||
$this->dncReasonHelper,
|
||||
$this->entityManager,
|
||||
$this->translator,
|
||||
$this->router,
|
||||
$this->leadListRepository,
|
||||
$this->segmentCountCacheHelper,
|
||||
$this->coreParametersHelper,
|
||||
$this->companyLeadRepository
|
||||
);
|
||||
|
||||
$subscriber->onLeadPostSave(new LeadEvent($lead));
|
||||
|
||||
Assert::assertEmpty($lead->getChanges()); // changes were reset after they were processed.
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that an timeline entry is created for a lead
|
||||
* that was created through the API.
|
||||
*/
|
||||
public function testAddTimelineApiCreatedEntries(): void
|
||||
{
|
||||
$eventTypeKey = 'lead.apiadded';
|
||||
$eventTypeName = 'Added through API';
|
||||
|
||||
$this->translator->expects($this->once())
|
||||
->method('trans')
|
||||
->willReturn($eventTypeName);
|
||||
|
||||
$lead = new Lead();
|
||||
|
||||
$leadEventLog = [
|
||||
'id' => '1',
|
||||
'lead_id' => '1',
|
||||
'user_id' => null,
|
||||
'user_name' => null,
|
||||
'bundle' => 'lead',
|
||||
'object' => 'api-single',
|
||||
'action' => 'identified_contact',
|
||||
'object_id' => null,
|
||||
'date_added' => new \DateTime(),
|
||||
'properties' => '{"object_description":"Awesome User"}',
|
||||
];
|
||||
|
||||
$logs = [
|
||||
'total' => 1,
|
||||
'results' => [
|
||||
$leadEventLog,
|
||||
],
|
||||
];
|
||||
|
||||
$timelineEvent = [
|
||||
'event' => $eventTypeKey,
|
||||
'eventId' => $eventTypeKey.$leadEventLog['id'],
|
||||
'eventType' => $eventTypeName,
|
||||
'eventLabel' => $eventTypeName,
|
||||
'timestamp' => $leadEventLog['date_added'],
|
||||
'icon' => 'ri-list-settings-line',
|
||||
'extra' => $leadEventLog,
|
||||
'contactId' => $leadEventLog['lead_id'],
|
||||
];
|
||||
|
||||
$leadEvent = new LeadTimelineEvent($lead);
|
||||
$repo = $this->createMock(LeadEventLogRepository::class);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$repo->expects($matcher)
|
||||
->method('getEvents')->willReturnCallback(function (...$parameters) use ($matcher, $lead, $leadEvent, $logs) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($lead, $parameters[0]);
|
||||
$this->assertSame('lead', $parameters[1]);
|
||||
$this->assertSame('api-single', $parameters[2]);
|
||||
$this->assertNull($parameters[3]);
|
||||
$this->assertSame($leadEvent->getQueryOptions(), $parameters[4]);
|
||||
|
||||
return $logs;
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($lead, $parameters[0]);
|
||||
$this->assertSame('lead', $parameters[1]);
|
||||
$this->assertSame('api-batch', $parameters[2]);
|
||||
$this->assertNull($parameters[3]);
|
||||
$this->assertSame($leadEvent->getQueryOptions(), $parameters[4]);
|
||||
|
||||
return ['total' => 0, 'results' => []];
|
||||
}
|
||||
});
|
||||
|
||||
$this->entityManager->method('getRepository')
|
||||
->with(LeadEventLog::class)
|
||||
->willReturn($repo);
|
||||
|
||||
$subscriber = new LeadSubscriber(
|
||||
$this->ipLookupHelper,
|
||||
$this->auditLogModel,
|
||||
$this->leadEventDispatcher,
|
||||
$this->dncReasonHelper,
|
||||
$this->entityManager,
|
||||
$this->translator,
|
||||
$this->router,
|
||||
$this->leadListRepository,
|
||||
$this->segmentCountCacheHelper,
|
||||
$this->coreParametersHelper,
|
||||
$this->companyLeadRepository,
|
||||
$this->modelFactory,
|
||||
true
|
||||
);
|
||||
|
||||
$dispatcher = new EventDispatcher();
|
||||
$dispatcher->addSubscriber($subscriber);
|
||||
|
||||
$dispatcher->dispatch($leadEvent, LeadEvents::TIMELINE_ON_GENERATE);
|
||||
|
||||
$this->assertSame([$timelineEvent], $leadEvent->getEvents());
|
||||
}
|
||||
|
||||
public function testOnLeadPostSaveWillNotProcessTheSameContactMultipleTimesBetweenContacts(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setId(54);
|
||||
$lead->addUpdatedField('title', 'Mr');
|
||||
$lead->addUpdatedField('firstname', 'John');
|
||||
$lead->addUpdatedField('lastname', 'Doe');
|
||||
|
||||
$lead2 = new Lead();
|
||||
$lead2->setId(58);
|
||||
$lead2->addUpdatedField('title', 'Mrs');
|
||||
$lead2->addUpdatedField('firstname', 'Jane');
|
||||
$lead2->addUpdatedField('lastname', 'Doe');
|
||||
|
||||
// Imitate a changed $lead2 but can't clone because it resets stuff in the __clone magic method
|
||||
// namely just need same ID
|
||||
$lead3 = new Lead();
|
||||
$lead3->setId(58);
|
||||
$lead3->addUpdatedField('lastname', 'Somebody');
|
||||
// This method will be called exactly once per set of changes
|
||||
$matcher = $this->exactly(3);
|
||||
|
||||
// This method will be called exactly once per set of changes
|
||||
$this->auditLogModel->expects($matcher)
|
||||
->method('writeToLog')->willReturnCallback(function (...$parameters) use ($matcher, $lead, $lead2, $lead3) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame([
|
||||
'bundle' => 'lead',
|
||||
'object' => 'lead',
|
||||
'objectId' => $lead->getId(),
|
||||
'action' => 'update',
|
||||
'details' => [
|
||||
'title' => [null, 'Mr'],
|
||||
'fields' => [
|
||||
'title' => [null, 'Mr'],
|
||||
'firstname' => [null, 'John'],
|
||||
'lastname' => [null, 'Doe'],
|
||||
],
|
||||
'firstname' => [null, 'John'],
|
||||
'lastname' => [null, 'Doe'],
|
||||
],
|
||||
'ipAddress' => null,
|
||||
], $parameters[0]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame([
|
||||
'bundle' => 'lead',
|
||||
'object' => 'lead',
|
||||
'objectId' => $lead2->getId(),
|
||||
'action' => 'update',
|
||||
'details' => [
|
||||
'title' => [null, 'Mrs'],
|
||||
'fields' => [
|
||||
'title' => [null, 'Mrs'],
|
||||
'firstname' => [null, 'Jane'],
|
||||
'lastname' => [null, 'Doe'],
|
||||
],
|
||||
'firstname' => [null, 'Jane'],
|
||||
'lastname' => [null, 'Doe'],
|
||||
],
|
||||
'ipAddress' => null,
|
||||
], $parameters[0]);
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame([
|
||||
'bundle' => 'lead',
|
||||
'object' => 'lead',
|
||||
'objectId' => $lead3->getId(),
|
||||
'action' => 'update',
|
||||
'details' => [
|
||||
'lastname' => [null, 'Somebody'],
|
||||
'fields' => [
|
||||
'lastname' => [null, 'Somebody'],
|
||||
],
|
||||
],
|
||||
'ipAddress' => null,
|
||||
], $parameters[0]);
|
||||
}
|
||||
});
|
||||
|
||||
$subscriber = new LeadSubscriber(
|
||||
$this->ipLookupHelper,
|
||||
$this->auditLogModel,
|
||||
$this->leadEventDispatcher,
|
||||
$this->dncReasonHelper,
|
||||
$this->entityManager,
|
||||
$this->translator,
|
||||
$this->router,
|
||||
$this->leadListRepository,
|
||||
$this->segmentCountCacheHelper,
|
||||
$this->coreParametersHelper,
|
||||
$this->companyLeadRepository,
|
||||
$this->modelFactory,
|
||||
true
|
||||
);
|
||||
|
||||
$subscriber->onLeadPostSave(new LeadEvent($lead));
|
||||
$subscriber->onLeadPostSave(new LeadEvent($lead));
|
||||
$subscriber->onLeadPostSave(new LeadEvent($lead2));
|
||||
$subscriber->onLeadPostSave(new LeadEvent($lead2));
|
||||
$subscriber->onLeadPostSave(new LeadEvent($lead3));
|
||||
$subscriber->onLeadPostSave(new LeadEvent($lead3));
|
||||
}
|
||||
|
||||
public function testManipulatorLogged(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setId(54);
|
||||
|
||||
$lead->setManipulator(
|
||||
new LeadManipulator('campaign', 'trigger-action', 1, 'Event Name (Campaign Name)')
|
||||
);
|
||||
|
||||
$lead->addUpdatedField('title', 'Mr');
|
||||
$lead->addUpdatedField('firstname', 'John');
|
||||
$lead->addUpdatedField('lastname', 'Test');
|
||||
|
||||
// This method will be called exactly once
|
||||
// even though the onLeadPostSave was called twice for the same lead
|
||||
$this->auditLogModel->expects($this->once())
|
||||
->method('writeToLog')
|
||||
->with(
|
||||
[
|
||||
'bundle' => 'lead',
|
||||
'object' => 'lead',
|
||||
'objectId' => $lead->getId(),
|
||||
'action' => 'update',
|
||||
'details' => [
|
||||
'title' => [null, 'Mr'],
|
||||
'fields' => [
|
||||
'title' => [null, 'Mr'],
|
||||
'firstname' => [null, 'John'],
|
||||
'lastname' => [null, 'Test'],
|
||||
],
|
||||
'firstname' => [null, 'John'],
|
||||
'lastname' => [null, 'Test'],
|
||||
'manipulated_by' => 'Event Name (Campaign Name)',
|
||||
'manipulator_key' => 'campaign:trigger-action:1',
|
||||
],
|
||||
'ipAddress' => null,
|
||||
]
|
||||
);
|
||||
|
||||
$subscriber = new LeadSubscriber(
|
||||
$this->ipLookupHelper,
|
||||
$this->auditLogModel,
|
||||
$this->leadEventDispatcher,
|
||||
$this->dncReasonHelper,
|
||||
$this->entityManager,
|
||||
$this->translator,
|
||||
$this->router,
|
||||
$this->leadListRepository,
|
||||
$this->segmentCountCacheHelper,
|
||||
$this->coreParametersHelper,
|
||||
$this->companyLeadRepository,
|
||||
$this->modelFactory,
|
||||
true
|
||||
);
|
||||
|
||||
$subscriber->onLeadPostSave(new LeadEvent($lead));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user