Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
abstract class AbstractSearchTestCase extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @param array<string, string|array<string, string>> $data
|
||||
*/
|
||||
protected function createContact(array $data): void
|
||||
{
|
||||
/** @var LeadModel $leadModel */
|
||||
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$contact = (new Lead())
|
||||
->setFirstname($data['firstname'])
|
||||
->setLastname($data['lastname'])
|
||||
->setEmail($data['email'])
|
||||
->setCompany($data['company']);
|
||||
|
||||
foreach ($data['customFields'] ?? [] as $key => $value) {
|
||||
$contact->addUpdatedField($key, $value);
|
||||
}
|
||||
|
||||
$leadModel->saveEntity($contact);
|
||||
}
|
||||
|
||||
protected function createSearchableField(string $name, string $object): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$field->setName($name);
|
||||
$field->setAlias($name);
|
||||
$field->setObject($object);
|
||||
$field->setDateAdded(new \DateTime());
|
||||
$field->setDateAdded(new \DateTime());
|
||||
$field->setDateModified(new \DateTime());
|
||||
$field->setIsIndex(true);
|
||||
$field->setType('text');
|
||||
|
||||
$fieldModel = static::getContainer()->get('mautic.lead.model.field');
|
||||
$fieldModel->saveEntity($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|array<string, string>> $data
|
||||
*/
|
||||
protected function createCompany(array $data): void
|
||||
{
|
||||
/** @var CompanyModel $companyModel */
|
||||
$companyModel = static::getContainer()->get('mautic.lead.model.company');
|
||||
|
||||
$company = (new Company())
|
||||
->setName($data['name'] ?? null)
|
||||
->setEmail($data['email'] ?? null);
|
||||
|
||||
foreach ($data['customFields'] ?? [] as $key => $value) {
|
||||
$company->addUpdatedField($key, $value);
|
||||
}
|
||||
|
||||
$companyModel->saveEntity($company);
|
||||
|
||||
$this->em->clear();
|
||||
}
|
||||
|
||||
protected function performSearch(string $url): Response
|
||||
{
|
||||
$this->client->xmlHttpRequest(Request::METHOD_GET, $url);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\Command;
|
||||
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Field\Command\CreateCustomFieldCommand;
|
||||
use Mautic\LeadBundle\Field\Notification\CustomFieldNotification;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
|
||||
class CreateCustomFieldCommandTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->useCleanupRollback = false;
|
||||
}
|
||||
|
||||
public function testWithIdAndUserArgs(): void
|
||||
{
|
||||
$leadField = new LeadField();
|
||||
$leadField->setLabel('Custom Field 1');
|
||||
$leadField->setAlias('custom_field_1');
|
||||
$leadField->setObject('lead');
|
||||
$leadField->setColumnIsNotCreated();
|
||||
$leadField->setDateAdded(new \DateTime());
|
||||
$leadField->setCreatedBy(1);
|
||||
$this->em->persist($leadField);
|
||||
$this->em->flush();
|
||||
|
||||
$kernel = static::getContainer()->get('kernel');
|
||||
\assert($kernel instanceof KernelInterface);
|
||||
|
||||
$expectedUserId = 1;
|
||||
$customFieldNotification = self::createMock(CustomFieldNotification::class);
|
||||
$customFieldNotification
|
||||
->expects(self::once())
|
||||
->method('customFieldWasCreated')
|
||||
->with(self::isInstanceOf(LeadField::class), self::equalTo($expectedUserId));
|
||||
$kernel->getContainer()->set('mautic.lead.field.notification.custom_field', $customFieldNotification);
|
||||
|
||||
$application = new Application($kernel);
|
||||
$application->setAutoExit(false);
|
||||
$command = $application->find(CreateCustomFieldCommand::COMMAND_NAME);
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute([
|
||||
'--user' => 1,
|
||||
'--id' => $leadField->getId(),
|
||||
]);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode(), $commandTester->getDisplay());
|
||||
|
||||
$leadTableName = $this->em->getClassMetadata(Lead::class)->getTableName();
|
||||
$columnsSchema = $this->em->getConnection()->createSchemaManager()->listTableColumns($leadTableName);
|
||||
$columnNames = array_map(
|
||||
static fn (Column $column) => $column->getName(),
|
||||
$columnsSchema
|
||||
);
|
||||
|
||||
self::assertContains('custom_field_1', $columnNames);
|
||||
}
|
||||
|
||||
public function testWithNoArgs(): void
|
||||
{
|
||||
$leadField1 = new LeadField();
|
||||
$leadField1->setLabel('Custom Field 1');
|
||||
$leadField1->setAlias('custom_field_1');
|
||||
$leadField1->setObject('lead');
|
||||
$leadField1->setColumnIsNotCreated();
|
||||
$leadField1->setDateAdded(new \DateTime());
|
||||
$leadField1->setCreatedBy(1);
|
||||
|
||||
$leadField2 = new LeadField();
|
||||
$leadField2->setLabel('Custom Field 2');
|
||||
$leadField2->setAlias('custom_field_2');
|
||||
$leadField2->setObject('lead');
|
||||
$leadField2->setColumnIsNotCreated();
|
||||
$leadField2->setDateAdded(new \DateTime());
|
||||
$leadField2->setCreatedBy(1);
|
||||
|
||||
$this->em->persist($leadField1);
|
||||
$this->em->persist($leadField2);
|
||||
$this->em->flush();
|
||||
|
||||
$kernel = static::getContainer()->get('kernel');
|
||||
\assert($kernel instanceof KernelInterface);
|
||||
|
||||
$expectedUserId = 1;
|
||||
$customFieldNotification = self::createMock(CustomFieldNotification::class);
|
||||
$customFieldNotification
|
||||
->expects(self::exactly(2))
|
||||
->method('customFieldWasCreated')
|
||||
->with(self::isInstanceOf(LeadField::class), self::equalTo($expectedUserId));
|
||||
$kernel->getContainer()->set('mautic.lead.field.notification.custom_field', $customFieldNotification);
|
||||
|
||||
$application = new Application($kernel);
|
||||
$application->setAutoExit(false);
|
||||
$command = $application->find(CreateCustomFieldCommand::COMMAND_NAME);
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute([]);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode(), $commandTester->getDisplay());
|
||||
|
||||
$leadTableName = $this->em->getClassMetadata(Lead::class)->getTableName();
|
||||
$columnsSchema = $this->em->getConnection()->createSchemaManager()->listTableColumns($leadTableName);
|
||||
$columnNames = array_map(
|
||||
static fn (Column $column) => $column->getName(),
|
||||
$columnsSchema
|
||||
);
|
||||
|
||||
self::assertContains('custom_field_1', $columnNames);
|
||||
self::assertContains('custom_field_2', $columnNames);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Model\ImportModel;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ImportCommandTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
/**
|
||||
* @var array|string[][]
|
||||
*/
|
||||
private array $csvRows = [
|
||||
['email', 'firstname', 'lastname'],
|
||||
['john1@doe.email', 'John', 'Doe1'],
|
||||
['john2@doe.email', 'John', 'Doe2'],
|
||||
['john3@doe.email', 'John', 'Doe3'],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $csvFiles = [];
|
||||
|
||||
protected function beforeTearDown(): void
|
||||
{
|
||||
foreach ($this->csvFiles as $file) {
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testImportNotification(): void
|
||||
{
|
||||
// Create contact import for ghosting.
|
||||
$this->createCsvContactImport(Import::IN_PROGRESS);
|
||||
$this->createCsvContactImport(Import::IN_PROGRESS);
|
||||
|
||||
// Create another import to be run
|
||||
$import = $this->createCsvContactImport();
|
||||
|
||||
// Run command to import CSV.
|
||||
$this->testSymfonyCommand('mautic:import', ['-e' => 'test', '-i' => $import->getId(), '--limit' => 10000]);
|
||||
|
||||
// See the notifications.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import');
|
||||
$html = $crawler->filterXPath('//div[contains(@id, "notifications")]')->html();
|
||||
$this->assertStringContainsString('Import failed. Reason: The import hasn\'t been updated in 2 hours by the background job. It\'s considered failed', $html, $html);
|
||||
}
|
||||
|
||||
private function generateSmallCSV(): string
|
||||
{
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'mautic_import_test_').'.csv';
|
||||
$file = fopen($tmpFile, 'wb');
|
||||
|
||||
foreach ($this->csvRows as $line) {
|
||||
CsvHelper::putCsv($file, $line);
|
||||
}
|
||||
|
||||
fclose($file);
|
||||
|
||||
$this->csvFiles[$tmpFile] = $tmpFile;
|
||||
|
||||
return $tmpFile;
|
||||
}
|
||||
|
||||
private function createCsvContactImport(int $status = Import::QUEUED): Import
|
||||
{
|
||||
$csvFile = $this->generateSmallCSV();
|
||||
|
||||
$now = new \DateTime();
|
||||
$import = new Import();
|
||||
$import->setIsPublished(true);
|
||||
$import->setDateAdded($now->modify('-4 hours'));
|
||||
$import->setDateModified($now->modify('-3 hours'));
|
||||
$import->setCreatedBy(1);
|
||||
$import->setDir('/tmp');
|
||||
$import->setFile(basename($csvFile));
|
||||
$import->setOriginalFile(basename($csvFile));
|
||||
$import->setLineCount(3);
|
||||
$import->setInsertedCount(0);
|
||||
$import->setUpdatedCount(0);
|
||||
$import->setIgnoredCount(0);
|
||||
$import->setStatus($status);
|
||||
$import->setObject('lead');
|
||||
$properties = [
|
||||
'fields' => [
|
||||
'file' => 'file',
|
||||
'email' => 'email',
|
||||
'firstname' => 'firstname',
|
||||
'lastname' => 'lastname',
|
||||
],
|
||||
'parser' => [
|
||||
'escape' => '\\',
|
||||
'delimiter' => ',',
|
||||
'enclosure' => '"',
|
||||
'batchlimit' => 100,
|
||||
],
|
||||
'headers' => [
|
||||
'file',
|
||||
'email',
|
||||
'firstname',
|
||||
'lastname',
|
||||
],
|
||||
'defaults' => [
|
||||
'list' => null,
|
||||
'owner' => null,
|
||||
],
|
||||
];
|
||||
$import->setProperties($properties);
|
||||
|
||||
/** @var ImportModel $importModel */
|
||||
$importModel = static::getContainer()->get('mautic.lead.model.import');
|
||||
$importModel->saveEntity($import);
|
||||
|
||||
return $import;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\DataFixtures\ORM\LoadLeadData;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ContactExportLimitFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['contact_export_limit'] = 2;
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testExportLimitExceeded(): void
|
||||
{
|
||||
// Load test data
|
||||
$this->loadFixtures([LoadLeadData::class]);
|
||||
|
||||
// Create additional contacts to exceed the limit
|
||||
$contactModel = self::getContainer()->get('mautic.lead.model.lead');
|
||||
for ($i = 0; $i < 3; ++$i) {
|
||||
$contact = new Lead();
|
||||
$contact->setFirstname("Test{$i}");
|
||||
$contact->setLastname("Contact{$i}");
|
||||
$contact->setEmail("test{$i}@test.com");
|
||||
$contactModel->saveEntity($contact);
|
||||
}
|
||||
|
||||
// Request the export
|
||||
$this->client->request(Request::METHOD_GET, '/s/contacts/batchExport?filetype=csv');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
// Assert response code is 400 (Bad Request)
|
||||
Assert::assertSame(Response::HTTP_BAD_REQUEST, $clientResponse->getStatusCode());
|
||||
|
||||
// Decode the JSON response
|
||||
$responseData = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Assert the response structure and content
|
||||
Assert::assertStringContainsString(
|
||||
'Export limit exceeded',
|
||||
$responseData['message']
|
||||
);
|
||||
Assert::assertStringContainsString(
|
||||
'2 contacts', // the limit we set
|
||||
$responseData['message']
|
||||
);
|
||||
Assert::assertStringContainsString(
|
||||
'Export limit exceeded',
|
||||
$responseData['flashes']
|
||||
);
|
||||
Assert::assertStringContainsString(
|
||||
'2 contacts', // the limit we set
|
||||
$responseData['flashes']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
class CompanyControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public const USERNAME = 'jhony';
|
||||
|
||||
public function testMergeAction(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/companies/merge/1');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertEquals(200, $clientResponse->getStatusCode());
|
||||
}
|
||||
|
||||
public function testMergeActionWithoutPermission(): void
|
||||
{
|
||||
$this->createAndLoginUser();
|
||||
$this->client->request('GET', '/s/companies/merge/1');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertEquals(403, $clientResponse->getStatusCode());
|
||||
}
|
||||
|
||||
private function createAndLoginUser(): User
|
||||
{
|
||||
// Create non-admin role
|
||||
$role = $this->createRole();
|
||||
// Create non-admin user
|
||||
$user = $this->createUser($role);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($role);
|
||||
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', self::USERNAME);
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function createRole(bool $isAdmin = false): Role
|
||||
{
|
||||
$role = new Role();
|
||||
$role->setName('Role');
|
||||
$role->setIsAdmin($isAdmin);
|
||||
|
||||
$this->em->persist($role);
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
private function createUser(Role $role): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setFirstName('Jhony');
|
||||
$user->setLastName('Doe');
|
||||
$user->setUsername(self::USERNAME);
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ImportControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
private string $csvFile;
|
||||
|
||||
protected function beforeTearDown(): void
|
||||
{
|
||||
if (isset($this->csvFile) && file_exists($this->csvFile)) {
|
||||
unlink($this->csvFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testScheduleImport(): void
|
||||
{
|
||||
$this->generateSmallCSV();
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
$tagName = 'tag1';
|
||||
$tag = $this->createTag($tagName);
|
||||
|
||||
// Show mapping page.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadButton = $crawler->selectButton('Upload');
|
||||
$form = $uploadButton->form();
|
||||
$form->setValues([
|
||||
'lead_import[file]' => $this->csvFile,
|
||||
'lead_import[batchlimit]' => 100,
|
||||
'lead_import[delimiter]' => ',',
|
||||
'lead_import[enclosure]' => '"',
|
||||
'lead_import[escape]' => '\\',
|
||||
]);
|
||||
$html = $this->client->submit($form);
|
||||
|
||||
Assert::assertStringContainsString(
|
||||
'Match the columns from the imported file to Mautic\'s contact fields.',
|
||||
$html->text(null, false)
|
||||
);
|
||||
|
||||
$importButton = $html->selectButton('Import');
|
||||
$importForm = $importButton->form();
|
||||
$importForm->setValues([
|
||||
'lead_field_import[tags]' => [$tag->getId()],
|
||||
]);
|
||||
$this->client->submit($importForm);
|
||||
|
||||
$importData = $this->em->getRepository(Import::class)->findOneBy(['object' => 'lead']);
|
||||
Assert::assertInstanceOf(Import::class, $importData);
|
||||
$importProperty = $importData->getProperties();
|
||||
Assert::assertSame([$tagName], $importProperty['defaults']['tags']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function dataImportCSV(): iterable
|
||||
{
|
||||
yield [false, '4 lines were processed, 3 items created, 0 items updated, 1 items ignored'];
|
||||
yield [true, '4 lines were processed, 2 items created, 1 items updated, 1 items ignored'];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataImportCSV')]
|
||||
public function testImportCSV(bool $createLead, string $expectedOutput): void
|
||||
{
|
||||
$this->generateSmallCSV();
|
||||
|
||||
if ($createLead) {
|
||||
$this->createLead('john1@doe.email');
|
||||
}
|
||||
|
||||
// Create custom fields
|
||||
$this->createField('text', 'file');
|
||||
$stateProperties = [
|
||||
'list' => [
|
||||
['label' => 'MH', 'value' => 'MH'],
|
||||
['label' => 'MP', 'value' => 'MP'],
|
||||
],
|
||||
];
|
||||
$this->createField('select', 'state_from', $stateProperties);
|
||||
|
||||
// Create import entity
|
||||
$import = $this->createCsvContactImport();
|
||||
|
||||
// Execute import
|
||||
$output = $this->createAndExecuteImport($import);
|
||||
Assert::assertStringContainsString($expectedOutput, $output->getDisplay());
|
||||
|
||||
/** @var LeadRepository $leadRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
$leadCount = $leadRepository->count(['firstname' => 'John']);
|
||||
Assert::assertSame(3, $leadCount);
|
||||
|
||||
if ($createLead) {
|
||||
$lead = $leadRepository->findOneBy(['email' => 'john1@doe.email']);
|
||||
$fieldValue = $lead ? $lead->getFieldValue('state_from') : null;
|
||||
|
||||
// Assert that existing leads are not updated by import
|
||||
Assert::assertNull(
|
||||
$fieldValue,
|
||||
'Existing lead should not be updated with state_from value.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testImportWithSpecialCharacterTag(): void
|
||||
{
|
||||
$this->generateSmallCSV();
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->client->loginUser($user, 'mautic');
|
||||
|
||||
$tagRepository = $this->em->getRepository(Tag::class);
|
||||
$tagCountBefore = $tagRepository->count([]);
|
||||
|
||||
$tagName = 'R&R';
|
||||
$tag = $this->createTag($tagName);
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadButton = $crawler->selectButton('Upload');
|
||||
$form = $uploadButton->form();
|
||||
$form->setValues([
|
||||
'lead_import[file]' => $this->csvFile,
|
||||
'lead_import[batchlimit]' => 100,
|
||||
'lead_import[delimiter]' => ',',
|
||||
'lead_import[enclosure]' => '"',
|
||||
'lead_import[escape]' => '\\',
|
||||
]);
|
||||
$html = $this->client->submit($form);
|
||||
|
||||
$importButton = $html->selectButton('Import');
|
||||
$importForm = $importButton->form();
|
||||
$importForm->setValues(['lead_field_import[tags]' => [$tag->getId()]]);
|
||||
$this->client->submit($importForm);
|
||||
|
||||
$import = $this->em->getRepository(Import::class)->findOneBy(['object' => 'lead']);
|
||||
$output = $this->testSymfonyCommand('mautic:import', [
|
||||
'-e' => 'dev',
|
||||
'--id' => $import->getId(),
|
||||
'--limit' => 10000,
|
||||
]);
|
||||
|
||||
Assert::assertStringContainsString(
|
||||
'4 lines were processed, 3 items created, 0 items updated, 1 items ignored',
|
||||
$output->getDisplay()
|
||||
);
|
||||
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
$leads = $leadRepository->findBy(['firstname' => 'John']);
|
||||
Assert::assertCount(3, $leads);
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$leadTags = $lead->getTags();
|
||||
Assert::assertCount(1, $leadTags);
|
||||
Assert::assertSame($tagName, $leadTags->first()->getTag());
|
||||
}
|
||||
|
||||
$tagCountAfter = $tagRepository->count([]);
|
||||
Assert::assertSame($tagCountBefore + 1, $tagCountAfter);
|
||||
Assert::assertNotNull($tagRepository->findOneBy(['tag' => $tagName]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function dataImportWithInvalidDates(): iterable
|
||||
{
|
||||
yield [false, '7 lines were processed, 2 items created, 0 items updated, 5 items ignored'];
|
||||
yield [true, '7 lines were processed, 1 items created, 1 items updated, 5 items ignored'];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataImportWithInvalidDates')]
|
||||
public function testImportWithInvalidDates(bool $createLead, string $expectedOutput): void
|
||||
{
|
||||
$this->generateSmallCSV([
|
||||
['file', 'email', 'firstname', 'lastname', 'state_from', 'birth_date'],
|
||||
['test1.pdf', 'john1@doe.email', 'John', 'Doe1', 'MP', '2025-08-01 09:05:59'],
|
||||
['test2.pdf', 'john2@doe.email', 'John', 'Doe2', 'MP', '2025-07-22 09:05:59'],
|
||||
['test3.pdf', 'john3@doe.email', 'John', 'Doe3', 'MP', '01-08-2025'],
|
||||
['test4.pdf', 'john4@doe.email', 'John', 'Doe4', 'MP', '2025/08/01'],
|
||||
['test5.pdf', 'john5@doe.email', 'John', 'Doe5', 'MP', '2025/08/01 09:05:59'],
|
||||
['test6.pdf', 'john6@doe.email', 'John', 'Doe6', 'MP', '2025'],
|
||||
]);
|
||||
|
||||
if ($createLead) {
|
||||
$this->createLead('john1@doe.email');
|
||||
}
|
||||
|
||||
// Setup fields
|
||||
$this->createField('text', 'file');
|
||||
$this->createField('select', 'state_from', [
|
||||
'list' => [
|
||||
['label' => 'MH', 'value' => 'MH'],
|
||||
['label' => 'MP', 'value' => 'MP'],
|
||||
],
|
||||
]);
|
||||
$this->createField('datetime', 'birth_date');
|
||||
|
||||
// Run import
|
||||
$import = $this->createCsvContactImport();
|
||||
$output = $this->createAndExecuteImport($import);
|
||||
|
||||
Assert::assertStringContainsString($expectedOutput, $output->getDisplay());
|
||||
|
||||
/** @var LeadRepository $leadRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
$leadCount = $leadRepository->count(['firstname' => 'John']);
|
||||
Assert::assertSame(2, $leadCount);
|
||||
|
||||
// Recheck import entity for ignored count
|
||||
$importEntity = $this->em->getRepository(Import::class)->find($import->getId());
|
||||
Assert::assertSame(5, $importEntity->getIgnoredCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
private function createField(string $type, string $alias, array $properties = []): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$field->setType($type);
|
||||
$field->setObject('lead');
|
||||
$field->setAlias($alias);
|
||||
$field->setName($alias);
|
||||
$field->setProperties($properties);
|
||||
|
||||
$fieldModel = static::getContainer()->get('mautic.lead.model.field');
|
||||
$fieldModel->saveEntity($field);
|
||||
}
|
||||
|
||||
private function createCsvContactImport(): Import
|
||||
{
|
||||
$now = new \DateTime();
|
||||
$import = new Import();
|
||||
$import->setIsPublished(true);
|
||||
$import->setDateAdded($now);
|
||||
$import->setCreatedBy(1);
|
||||
$import->setDir('/tmp');
|
||||
$import->setFile(basename($this->csvFile));
|
||||
$import->setOriginalFile(basename($this->csvFile));
|
||||
$import->setLineCount(3);
|
||||
$import->setStatus(1);
|
||||
$import->setObject('lead');
|
||||
|
||||
$import->setProperties([
|
||||
'fields' => [
|
||||
'file' => 'file',
|
||||
'email' => 'email',
|
||||
'firstname' => 'firstname',
|
||||
'lastname' => 'lastname',
|
||||
'state_from' => 'state_from',
|
||||
'birth_date' => 'birth_date',
|
||||
],
|
||||
'parser' => [
|
||||
'escape' => '\\',
|
||||
'delimiter' => ',',
|
||||
'enclosure' => '"',
|
||||
'batchlimit' => 100,
|
||||
],
|
||||
'headers' => [
|
||||
'file',
|
||||
'email',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'state_from',
|
||||
'birth_date',
|
||||
],
|
||||
'defaults' => [
|
||||
'list' => null,
|
||||
'tags' => ['tag1'],
|
||||
'owner' => null,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->getContainer()->get('mautic.security.user_token_setter')->setUser($import->getCreatedBy());
|
||||
$importModel = static::getContainer()->get('mautic.lead.model.import');
|
||||
$importModel->saveEntity($import);
|
||||
|
||||
return $import;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<int, string>>|null $csvRows
|
||||
*/
|
||||
private function generateSmallCSV(?array $csvRows = null): void
|
||||
{
|
||||
$csvRows = $csvRows ?: [
|
||||
['file', 'email', 'firstname', 'lastname', 'state_from'],
|
||||
['test1.pdf', 'john1@doe.email', 'John', 'Doe1', 'MP'],
|
||||
['test2.pdf', 'john2@doe.email', 'John', 'Doe2', 'MP'],
|
||||
['test3.pdf', 'john3@doe.email', 'John', 'Doe3', 'MP'],
|
||||
];
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'mautic_import_test_').'.csv';
|
||||
$file = fopen($tmpFile, 'wb');
|
||||
foreach ($csvRows as $line) {
|
||||
CsvHelper::putCsv($file, $line);
|
||||
}
|
||||
fclose($file);
|
||||
$this->csvFile = $tmpFile;
|
||||
}
|
||||
|
||||
private function createAndExecuteImport(Import $import): CommandTester
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadButton = $crawler->selectButton('Upload');
|
||||
$form = $uploadButton->form();
|
||||
$form->setValues([
|
||||
'lead_import[file]' => $this->csvFile,
|
||||
'lead_import[batchlimit]' => 100,
|
||||
'lead_import[delimiter]' => ',',
|
||||
'lead_import[enclosure]' => '"',
|
||||
'lead_import[escape]' => '\\',
|
||||
]);
|
||||
$html = $this->client->submit($form);
|
||||
|
||||
Assert::assertStringContainsString(
|
||||
'Match the columns from the imported file to Mautic\'s contact fields.',
|
||||
$html->text()
|
||||
);
|
||||
|
||||
return $this->testSymfonyCommand('mautic:import', [
|
||||
'-e' => 'dev',
|
||||
'--id' => $import->getId(),
|
||||
'--limit' => 10000,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createTag(string $tagName): Tag
|
||||
{
|
||||
$tag = new Tag();
|
||||
$tag->setTag($tagName);
|
||||
|
||||
$tagModel = static::getContainer()->get('mautic.lead.model.tag');
|
||||
$tagModel->saveEntity($tag);
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
private function createLead(?string $email = null): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
if (!empty($email)) {
|
||||
$lead->setEmail($email);
|
||||
}
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Command\ContactScheduledExportCommand;
|
||||
use Mautic\LeadBundle\Entity\ContactExportScheduler;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class LeadControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public const USERNAME = 'jhony';
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $filePaths = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['contact_export_dir'] = '/tmp';
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
protected function beforeTearDown(): void
|
||||
{
|
||||
foreach ($this->filePaths as $filePath) {
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testContactExportIsScheduledForCsvFileType(): void
|
||||
{
|
||||
$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()]);
|
||||
$this->checkContactExportScheduler(0);
|
||||
/** @var CoreParametersHelper $coreParametersHelper */
|
||||
$coreParametersHelper = static::getContainer()->get('mautic.helper.core_parameters');
|
||||
$zipFileName = 'contacts_export_'.$contactExportScheduler->getScheduledDateTime()
|
||||
->format('Y_m_d_H_i_s').'.zip';
|
||||
$this->filePaths[] = $filePath = $coreParametersHelper->get('contact_export_dir').'/'.$zipFileName;
|
||||
Assert::assertFileExists($filePath);
|
||||
|
||||
$link = $this->router->generate(
|
||||
'mautic_contact_export_download',
|
||||
['fileName' => basename($filePath)],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
);
|
||||
$this->client->request(Request::METHOD_GET, $link);
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$notFoundLink = $this->router->generate(
|
||||
'mautic_contact_export_download',
|
||||
['fileName' => 'non_existing.zip'],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
);
|
||||
$this->client->request(Request::METHOD_GET, $notFoundLink);
|
||||
Assert::assertTrue($this->client->getResponse()->isNotFound());
|
||||
}
|
||||
|
||||
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 = static::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;
|
||||
}
|
||||
|
||||
public function testAccessContactQuickAddWithPermission(): void
|
||||
{
|
||||
$this->setAdminUser();
|
||||
$this->client->request(Request::METHOD_GET, '/s/contacts/quickAdd');
|
||||
$this->assertResponseStatusCodeSame(200, (string) $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
private function setAdminUser(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', 'admin');
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
}
|
||||
|
||||
public function testAccessContactQuickAddWithNoPermission(): void
|
||||
{
|
||||
$this->createAndLoginUser();
|
||||
$this->client->request(Request::METHOD_GET, '/s/contacts/quickAdd');
|
||||
$this->assertResponseStatusCodeSame(403, (string) $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testAccessContactBatchOwnersNoPermission(): void
|
||||
{
|
||||
$this->createAndLoginUser();
|
||||
$this->client->request(Request::METHOD_GET, '/s/contacts/batchOwners');
|
||||
$this->assertResponseStatusCodeSame(403, (string) $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testAccessContactBatchOwnersPermission(): void
|
||||
{
|
||||
$this->setAdminUser();
|
||||
$this->client->request(Request::METHOD_GET, '/s/contacts/batchOwners');
|
||||
$this->assertResponseStatusCodeSame(200, (string) $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
private function createAndLoginUser(): User
|
||||
{
|
||||
// Create non-admin role
|
||||
$role = $this->createRole();
|
||||
// Create non-admin user
|
||||
$user = $this->createUser($role);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($role);
|
||||
|
||||
$this->client->loginUser($user, 'mautic');
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', self::USERNAME);
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function createRole(bool $isAdmin = false): Role
|
||||
{
|
||||
$role = new Role();
|
||||
$role->setName('Role');
|
||||
$role->setIsAdmin($isAdmin);
|
||||
|
||||
$this->em->persist($role);
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
private function createUser(Role $role): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setFirstName('Jhony');
|
||||
$user->setLastName('Doe');
|
||||
$user->setUsername(self::USERNAME);
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class SendEmailToContactTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
public function testPreheaderConfigIsApplied(): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('john@doe.email');
|
||||
$contact->setFirstname('John');
|
||||
|
||||
$emailEntity = new Email();
|
||||
$emailEntity->setName('Email A');
|
||||
$emailEntity->setFromAddress('overwrite@address.com');
|
||||
$emailEntity->setFromName('Overwrite Name');
|
||||
$emailEntity->setSubject('Subject to overwrite');
|
||||
$emailEntity->setCustomHtml('<html><body><p>This should be overwritten by the form content</p></body></html>');
|
||||
$emailEntity->setPreheaderText('This is a preheader text');
|
||||
|
||||
$this->em->persist($contact);
|
||||
$this->em->persist($emailEntity);
|
||||
$this->em->flush();
|
||||
|
||||
// Fetch the form
|
||||
$this->client->request(Request::METHOD_GET, '/s/contacts/email/'.$contact->getId());
|
||||
$this->assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
$content = $this->client->getResponse()->getContent();
|
||||
$content = json_decode($content)->newContent;
|
||||
$crawler = new Crawler($content, $this->client->getInternalRequest()->getUri());
|
||||
$formCrawler = $crawler->filter('form');
|
||||
$this->assertCount(1, $formCrawler);
|
||||
$form = $formCrawler->form();
|
||||
|
||||
// Send email to contact
|
||||
$form->setValues([
|
||||
'lead_quickemail[fromname]' => 'Admin',
|
||||
'lead_quickemail[from]' => 'admin@test-beta.mautibot.com',
|
||||
'lead_quickemail[subject]' => 'Some interesting subject for {contactfield=firstname}',
|
||||
'lead_quickemail[body]' => '<html><body><p>Hey {contactfield=firstname}...</p></body></html>',
|
||||
'lead_quickemail[list]' => 0,
|
||||
'lead_quickemail[templates]' => $emailEntity->getId(),
|
||||
]);
|
||||
$this->client->submit($form);
|
||||
$this->assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$message = $this->getMailerMessagesByToAddress('john@doe.email')[0];
|
||||
$email = $message->getBody()->toString();
|
||||
Assert::assertStringContainsString('Hey John...', $email);
|
||||
Assert::assertStringContainsString('<title>Some interesting subject for John</title>', $email);
|
||||
Assert::assertStringContainsString('Some interesting subject for John', $message->getSubject());
|
||||
Assert::assertStringContainsString('preheader text', $email);
|
||||
Assert::assertStringContainsString('admin@test-beta.mautibot.com', $message->getFrom()[0]->getAddress());
|
||||
Assert::assertStringContainsString('Admin', $message->getFrom()[0]->getName());
|
||||
Assert::assertStringNotContainsString('This should be overwritten by the form content', $email);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class DncSearchFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
private const MESSAGE_EMAIL_DNC_SHOULD_APPEAR_IN_EMAIL_SEARCH = 'Contact with email DNC should appear in dnc:email search';
|
||||
|
||||
public function testDncSearchWithAnyChannel(): void
|
||||
{
|
||||
$contact1 = $this->createContact('contact1@test.com');
|
||||
$contact2 = $this->createContact('contact2@test.com');
|
||||
$contact3 = $this->createContact('contact3@test.com');
|
||||
|
||||
$this->addDncRecord($contact1->getId(), 'email');
|
||||
$this->addDncRecord($contact2->getId(), 'sms');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Aany');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseText = $crawler->text();
|
||||
|
||||
$this->assertStringContainsString($contact1->getEmail(), $responseText, 'Contact with email DNC should appear in dnc:any search');
|
||||
$this->assertStringContainsString($contact2->getEmail(), $responseText, 'Contact with SMS DNC should appear in dnc:any search');
|
||||
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without DNC should not appear in dnc:any search');
|
||||
}
|
||||
|
||||
public function testDncSearchWithSpecificChannel(): void
|
||||
{
|
||||
$contact1 = $this->createContact('email-dnc@test.com');
|
||||
$contact2 = $this->createContact('sms-dnc@test.com');
|
||||
$contact3 = $this->createContact('no-dnc@test.com');
|
||||
|
||||
$this->addDncRecord($contact1->getId(), 'email');
|
||||
$this->addDncRecord($contact2->getId(), 'sms');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Aemail');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseText = $crawler->text();
|
||||
|
||||
$this->assertStringContainsString($contact1->getEmail(), $responseText, self::MESSAGE_EMAIL_DNC_SHOULD_APPEAR_IN_EMAIL_SEARCH);
|
||||
$this->assertStringNotContainsString($contact2->getEmail(), $responseText, 'Contact with SMS DNC should not appear in dnc:email search');
|
||||
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without DNC should not appear in dnc:email search');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Asms');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseText = $crawler->text();
|
||||
|
||||
$this->assertStringNotContainsString($contact1->getEmail(), $responseText, 'Contact with email DNC should not appear in dnc:sms search');
|
||||
$this->assertStringContainsString($contact2->getEmail(), $responseText, 'Contact with SMS DNC should appear in dnc:sms search');
|
||||
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without DNC should not appear in dnc:sms search');
|
||||
}
|
||||
|
||||
public function testDncSearchNegation(): void
|
||||
{
|
||||
$contact1 = $this->createContact('dnc-contact@test.com');
|
||||
$contact2 = $this->createContact('normal-contact@test.com');
|
||||
|
||||
$this->addDncRecord($contact1->getId(), 'email');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=!dnc%3Aany');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseText = $crawler->text();
|
||||
|
||||
$this->assertStringNotContainsString($contact1->getEmail(), $responseText, 'Contact with DNC should not appear in negative dnc:any search');
|
||||
$this->assertStringContainsString($contact2->getEmail(), $responseText, 'Contact without DNC should appear in negative dnc:any search');
|
||||
}
|
||||
|
||||
public function testDncSearchWithMultipleChannelsOnSameContact(): void
|
||||
{
|
||||
$contact1 = $this->createContact('multi-dnc@test.com');
|
||||
$contact2 = $this->createContact('single-dnc@test.com');
|
||||
$contact3 = $this->createContact('no-dnc-multiple@test.com');
|
||||
|
||||
$this->addDncRecord($contact1->getId(), 'email');
|
||||
$this->addDncRecord($contact1->getId(), 'sms');
|
||||
|
||||
$this->addDncRecord($contact2->getId(), 'email');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Aany');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseText = $crawler->text();
|
||||
|
||||
$this->assertStringContainsString($contact1->getEmail(), $responseText, 'Contact with multiple DNC channels should appear in dnc:any search');
|
||||
$this->assertStringContainsString($contact2->getEmail(), $responseText, 'Contact with single DNC channel should appear in dnc:any search');
|
||||
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without DNC should not appear in dnc:any search');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Aemail');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseText = $crawler->text();
|
||||
|
||||
$this->assertStringContainsString($contact1->getEmail(), $responseText, self::MESSAGE_EMAIL_DNC_SHOULD_APPEAR_IN_EMAIL_SEARCH);
|
||||
$this->assertStringContainsString($contact2->getEmail(), $responseText, self::MESSAGE_EMAIL_DNC_SHOULD_APPEAR_IN_EMAIL_SEARCH);
|
||||
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without email DNC should not appear in dnc:email search');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Asms');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseText = $crawler->text();
|
||||
|
||||
$this->assertStringContainsString($contact1->getEmail(), $responseText, 'Contact with SMS DNC should appear in dnc:sms search');
|
||||
$this->assertStringNotContainsString($contact2->getEmail(), $responseText, 'Contact without SMS DNC should not appear in dnc:sms search');
|
||||
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without SMS DNC should not appear in dnc:sms search');
|
||||
}
|
||||
|
||||
private function createContact(string $email): Lead
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail($email);
|
||||
$contact->setDateIdentified(new \DateTime());
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
private function addDncRecord(int $contactId, string $channel): void
|
||||
{
|
||||
$this->em->getConnection()->executeStatement(
|
||||
'INSERT INTO '.MAUTIC_TABLE_PREFIX.'lead_donotcontact (lead_id, channel, reason, comments, date_added) VALUES (?, ?, ?, ?, ?)',
|
||||
[$contactId, $channel, 1, 'Test DNC', new \DateTime()],
|
||||
[\Doctrine\DBAL\Types\Types::INTEGER, \Doctrine\DBAL\Types\Types::STRING, \Doctrine\DBAL\Types\Types::INTEGER, \Doctrine\DBAL\Types\Types::STRING, \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Helper\MailHelper;
|
||||
use Mautic\EmailBundle\Tests\Helper\Transport\SmtpTransport;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
|
||||
final class CompanyRepositoryTest extends MauticMysqlTestCase
|
||||
{
|
||||
private SmtpTransport $transport;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->setUpMailer();
|
||||
}
|
||||
|
||||
protected function beforeTearDown(): void
|
||||
{
|
||||
// Clear owners cache (to leave a clean environment for future tests):
|
||||
$mailHelper = static::getContainer()->get('mautic.helper.mailer');
|
||||
$this->setPrivateProperty($mailHelper, 'leadOwners', []);
|
||||
}
|
||||
|
||||
public function testEmailSendWithCompanyTokens(): void
|
||||
{
|
||||
$suffix = random_int(10, 100);
|
||||
$companyA = $this->createCompany('ABC Co.'.$suffix, 'First Street'.$suffix);
|
||||
$contactA = $this->createContact('John'.$suffix, 'JohnDoe'.$suffix.'@email.com', $companyA);
|
||||
$companyB = $this->createCompany('XYZ Co.'.$suffix, 'Second Street'.$suffix);
|
||||
$this->editContact($contactA, $companyB, $companyA);
|
||||
$segment = $this->createSegment('Segment A'.$suffix, 'segment-a-'.$suffix);
|
||||
$this->addContactsToSegment([$contactA], $segment);
|
||||
$emailId = $this->createEmail('EmailName'.$suffix, 'Subject'.$suffix, 'list', $segment->getId());
|
||||
$this->sendEmailViaApi($emailId);
|
||||
$testEmail = function () use ($suffix): void {
|
||||
$message = $this->transport->sentMessage;
|
||||
Assert::assertSame($message->getSubject(), 'Subject'.$suffix);
|
||||
Assert::assertSame($message->getTo()[0]->getAddress(), 'JohnDoe'.$suffix.'@email.com');
|
||||
Assert::assertSame($message->getTo()[0]->getName(), 'John'.$suffix);
|
||||
$messageBody = $message->getBody()->toString();
|
||||
Assert::assertStringContainsString('JohnDoe'.$suffix.'@email.com', $messageBody);
|
||||
Assert::assertStringContainsString('XYZ Co.'.$suffix, $messageBody);
|
||||
Assert::assertStringContainsString('Second Street'.$suffix, $messageBody);
|
||||
};
|
||||
$testEmail();
|
||||
}
|
||||
|
||||
private function createCompany(string $name, string $address1 = ''): Company
|
||||
{
|
||||
/** @var CompanyModel $model */
|
||||
$model = static::getContainer()->get('mautic.lead.model.company');
|
||||
$company = new Company();
|
||||
$company->setIsPublished(true)->setName($name)->setAddress1($address1);
|
||||
$model->saveEntity($company);
|
||||
|
||||
return $company;
|
||||
}
|
||||
|
||||
private function createContact(string $firstName, string $email, Company $company): Lead
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/new');
|
||||
$formCrawler = $crawler->filter('form[name=lead]');
|
||||
$this->assertCount(1, $formCrawler);
|
||||
|
||||
$form = $formCrawler->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'lead[firstname]' => $firstName,
|
||||
'lead[email]' => $email,
|
||||
'lead[companies]' => [$company->getId()],
|
||||
]
|
||||
);
|
||||
$this->client->submit($form);
|
||||
|
||||
return $this->em->getRepository(Lead::class)->findOneBy(
|
||||
[
|
||||
'firstname' => $firstName,
|
||||
'email' => $email,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function editContact(Lead $contact, Company $primaryCompany, Company $secondaryCompany): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/edit/'.$contact->getId());
|
||||
$formCrawler = $crawler->filter('form[name=lead]');
|
||||
$this->assertCount(1, $formCrawler);
|
||||
|
||||
$form = $formCrawler->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'lead[companies]' => [$primaryCompany->getId(), $secondaryCompany->getId()],
|
||||
]
|
||||
);
|
||||
$this->client->submit($form);
|
||||
}
|
||||
|
||||
private function createSegment(string $name, string $alias): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName($name);
|
||||
$segment->setPublicName($name);
|
||||
$segment->setAlias($alias);
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Lead> $contacts
|
||||
*/
|
||||
private function addContactsToSegment(array $contacts, LeadList $segment): void
|
||||
{
|
||||
foreach ($contacts as $contact) {
|
||||
$reference = new ListLead();
|
||||
$reference->setLead($contact);
|
||||
$reference->setList($segment);
|
||||
$reference->setDateAdded(new \DateTime());
|
||||
$this->em->persist($reference);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function createEmail(string $name, string $subject, string $emailType, ?int $segmentId = null): int
|
||||
{
|
||||
$payload = [
|
||||
'name' => $name,
|
||||
'subject' => $subject,
|
||||
'emailType' => $emailType,
|
||||
'customHtml' => '{contactfield=email} {contactfield=companyname} {contactfield=companyaddress1}',
|
||||
];
|
||||
|
||||
if ('list' === $emailType) {
|
||||
$payload['lists'] = [$segmentId];
|
||||
}
|
||||
|
||||
$this->client->request('POST', '/api/emails/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
return $response['email']['id'];
|
||||
}
|
||||
|
||||
private function setUpMailer(): void
|
||||
{
|
||||
$mailHelper = static::getContainer()->get('mautic.helper.mailer');
|
||||
$transport = new SmtpTransport();
|
||||
$mailer = new Mailer($transport);
|
||||
$this->setPrivateProperty($mailHelper, 'mailer', $mailer);
|
||||
$this->setPrivateProperty($mailHelper, 'transport', $transport);
|
||||
$this->transport = $transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function setPrivateProperty(MailHelper $object, string $property, $value): void
|
||||
{
|
||||
$reflector = new \ReflectionProperty($object::class, $property);
|
||||
$reflector->setAccessible(true);
|
||||
$reflector->setValue($object, $value);
|
||||
}
|
||||
|
||||
private function sendEmailViaApi(int $emailId): void
|
||||
{
|
||||
$this->client->request('POST', "/api/emails/{$emailId}/send");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
Assert::assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
Assert::assertSame(
|
||||
json_decode($clientResponse->getContent(), true, 512, JSON_THROW_ON_ERROR),
|
||||
[
|
||||
'success' => 1,
|
||||
'sentCount' => 1,
|
||||
'failedRecipients' => 0,
|
||||
],
|
||||
$clientResponse->getContent()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class LeadRepositoryTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->clientOptions = ['debug' => true];
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, list<array<string, bool>>>
|
||||
*/
|
||||
public static function joinIpAddressesProvider(): array
|
||||
{
|
||||
return [
|
||||
[[]],
|
||||
[['joinIpAddresses' => true]],
|
||||
[['joinIpAddresses' => false]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $args
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('joinIpAddressesProvider')]
|
||||
public function testSaveIpAddressToContacts($args): void
|
||||
{
|
||||
$contactRepo = $this->em->getRepository(Lead::class);
|
||||
|
||||
$ipRepo = $this->em->getRepository(IpAddress::class);
|
||||
|
||||
$ip = new IpAddress('127.0.0.1');
|
||||
$contact = new Lead();
|
||||
$contact->addIpAddress($ip);
|
||||
$this->em->persist($contact);
|
||||
$this->em->persist($ip);
|
||||
$this->em->flush();
|
||||
|
||||
$q = $contactRepo->getEntitiesOrmQueryBuilder('(CASE WHEN u.id=1 THEN 1 ELSE 2 END) AS HIDDEN ORD', $args);
|
||||
$results = $q->getQuery()
|
||||
->getResult();
|
||||
|
||||
/** @var Lead $r */
|
||||
foreach ($results as $r) {
|
||||
$ipAddresses = $r->getIpAddresses();
|
||||
$ipAddress = $ipAddresses->first();
|
||||
$this->assertEquals($ipAddress->getIpAddress(), '127.0.0.1');
|
||||
}
|
||||
|
||||
$this->client->enableProfiler();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/contacts');
|
||||
|
||||
$profile = $this->client->getProfile();
|
||||
/** @var DoctrineDataCollector $dbCollector */
|
||||
$dbCollector = $profile->getCollector('db');
|
||||
$queries = $dbCollector->getQueries();
|
||||
|
||||
$finalQueries = array_filter(
|
||||
$queries['default'],
|
||||
fn (array $query) => str_contains($query['sql'], 'SELECT (CASE WHEN t0_.id = 1 THEN 1 ELSE 2 END)')
|
||||
);
|
||||
|
||||
foreach ($finalQueries as $query) {
|
||||
if ($args['joinIpAddresses'] ?? true) {
|
||||
$this->assertStringContainsString('LEFT JOIN test_ip_addresses', $query['sql']);
|
||||
} else {
|
||||
$this->assertStringNotContainsString('LEFT JOIN test_ip_addresses', $query['sql']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadDevice;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\EventListener\CampaignSubscriber;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
class CampaignSubscriberTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
private CampaignSubscriber $campaignSubscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->campaignSubscriber = $this->getContainer()->get(CampaignSubscriber::class);
|
||||
}
|
||||
|
||||
public function testOnCampaignTriggerConditionReturnsCorrectResultForLeadDeviceContext(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test');
|
||||
$this->em->persist($lead);
|
||||
|
||||
$now = new \DateTime();
|
||||
$leadDevice1 = new LeadDevice();
|
||||
$leadDevice1->setLead($lead);
|
||||
$leadDevice1->setDateAdded($now);
|
||||
$leadDevice1->setDevice('desktop');
|
||||
$leadDevice1->setDeviceBrand('AP');
|
||||
$leadDevice1->setDeviceModel('MacBook');
|
||||
$leadDevice1->setDeviceOsName('Mac');
|
||||
$this->em->persist($leadDevice1);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('My campaign');
|
||||
$campaign->setIsPublished(true);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
$entityEvent = new Event();
|
||||
$entityEvent->setCampaign($campaign);
|
||||
$entityEvent->setName('Test Condition');
|
||||
$entityEvent->setEventType('condition');
|
||||
$entityEvent->setType('lead.device');
|
||||
$entityEvent->setProperties([
|
||||
'device_type' => [
|
||||
'desktop',
|
||||
'mobile',
|
||||
'tablet',
|
||||
],
|
||||
'device_brand' => [
|
||||
'AP',
|
||||
'NOKIA',
|
||||
'SAMSUNG',
|
||||
],
|
||||
'device_os' => [
|
||||
'Chrome OS',
|
||||
'Mac',
|
||||
'iOS',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->em->persist($entityEvent);
|
||||
$this->em->flush();
|
||||
|
||||
$eventProperties = [
|
||||
'lead' => $lead,
|
||||
'event' => $entityEvent,
|
||||
'eventDetails' => [],
|
||||
'systemTriggered' => false,
|
||||
'eventSettings' => [],
|
||||
];
|
||||
|
||||
$campaignExecutionEvent = new CampaignExecutionEvent($eventProperties, false); // @phpstan-ignore new.deprecated
|
||||
$result = $this->campaignSubscriber->onCampaignTriggerCondition($campaignExecutionEvent);
|
||||
Assert::assertInstanceOf(CampaignExecutionEvent::class, $result); // @phpstan-ignore classConstant.deprecatedClass
|
||||
Assert::assertTrue($result->getResult());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array{0: array<string, string>, 1: array<string, string>, 2: bool}>
|
||||
*/
|
||||
public static function dataEventProperties(): iterable
|
||||
{
|
||||
yield [
|
||||
['type' => 'datetime', 'alias' => 'date_field'],
|
||||
['field' => 'date_field', 'operator' => 'empty'],
|
||||
true,
|
||||
];
|
||||
yield [
|
||||
['type' => 'datetime', 'alias' => 'date_field_another'],
|
||||
['field' => 'date_field_another', 'operator' => '!empty'],
|
||||
false,
|
||||
];
|
||||
yield [
|
||||
['type' => 'text', 'alias' => 'test_text_field'],
|
||||
['field' => 'firstname', 'operator' => 'empty'],
|
||||
false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $field
|
||||
* @param array<string, string> $properties
|
||||
*/
|
||||
#[DataProvider('dataEventProperties')]
|
||||
public function testOnCampaignTriggerConditionReturnsCorrectResultsForLeadFieldContext(array $field, array $properties, bool $expected): void
|
||||
{
|
||||
$this->makeField($field);
|
||||
$lead = $this->createTestLead($field);
|
||||
|
||||
// Create a campaign.
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('My campaign');
|
||||
$campaign->setIsPublished(true);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
// Create an event for campaign.
|
||||
$entityEvent = new Event();
|
||||
$entityEvent->setCampaign($campaign);
|
||||
$entityEvent->setName('Test Condition');
|
||||
$entityEvent->setEventType('condition');
|
||||
$entityEvent->setType('lead.field_value');
|
||||
$entityEvent->setProperties($properties);
|
||||
|
||||
$this->em->persist($entityEvent);
|
||||
$this->em->flush();
|
||||
|
||||
$eventProperties = [
|
||||
'lead' => $lead,
|
||||
'event' => $entityEvent,
|
||||
'eventDetails' => [],
|
||||
'systemTriggered' => false,
|
||||
'eventSettings' => [],
|
||||
];
|
||||
|
||||
$campaignExecutionEvent = new CampaignExecutionEvent($eventProperties, false); // @phpstan-ignore new.deprecated
|
||||
$result = $this->campaignSubscriber->onCampaignTriggerCondition($campaignExecutionEvent);
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $result); // @phpstan-ignore classConstant.deprecatedClass
|
||||
$this->assertSame($expected, $result->getResult());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $fieldDetails
|
||||
*/
|
||||
private function makeField(array $fieldDetails): void
|
||||
{
|
||||
// Create a field and add it to the lead object.
|
||||
$field = new LeadField();
|
||||
$field->setLabel($fieldDetails['alias']);
|
||||
$field->setType($fieldDetails['type']);
|
||||
$field->setObject('lead');
|
||||
$field->setGroup('core');
|
||||
$field->setAlias($fieldDetails['alias']);
|
||||
|
||||
/** @var FieldModel $fieldModel */
|
||||
$fieldModel = $this->getContainer()->get('mautic.lead.model.field');
|
||||
$fieldModel->saveEntity($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $fieldDetails
|
||||
*/
|
||||
private function createTestLead(array $fieldDetails): Lead
|
||||
{
|
||||
// Create a contact
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test');
|
||||
$lead->setFields([
|
||||
'core' => [
|
||||
$fieldDetails['alias'] => [
|
||||
'value' => '',
|
||||
'type' => $fieldDetails['type'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
return $lead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\EventListener;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CoreBundle\Entity\AuditLog;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
|
||||
class CompanySubscriberFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testCreateCompany(): void
|
||||
{
|
||||
/** @var UserModel $userModel */
|
||||
$userModel = static::getContainer()->get('mautic.user.model.user');
|
||||
$users = $userModel->getRepository()->findAll();
|
||||
$user = reset($users);
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
|
||||
$company = new Company();
|
||||
$company->setName('Test company');
|
||||
$company->setOwner($user);
|
||||
$companyModel = static::getContainer()->get('mautic.lead.model.company');
|
||||
$companyModel->saveEntity($company);
|
||||
|
||||
$auditLogRepository = $this->em->getRepository(AuditLog::class);
|
||||
$auditLogs = $auditLogRepository->findOneBy(['bundle' => 'lead', 'object' => 'company', 'action' => 'create', 'objectId' => $company->getId()]);
|
||||
$this->assertInstanceOf(AuditLog::class, $auditLogs);
|
||||
$auditLogDetail = $auditLogs->getDetails();
|
||||
$this->assertArrayHasKey('owner', $auditLogDetail);
|
||||
$this->assertSame([null, "Admin User ({$user->getId()})"], $auditLogDetail['owner']);
|
||||
}
|
||||
|
||||
public function testCompanyGetsDeletedInLeadsTable(): void
|
||||
{
|
||||
$company = new Company();
|
||||
$company->setName('Test Delete Company');
|
||||
$companyModel = static::getContainer()->get('mautic.lead.model.company');
|
||||
$companyModel->saveEntity($company);
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test name');
|
||||
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
|
||||
$leadModel->saveEntity($lead);
|
||||
$companyModel->addLeadToCompany($company, $lead);
|
||||
$leadModel->saveEntity($lead);
|
||||
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
$lead = $leadRepository->findOneBy(['firstname' => 'Test name']);
|
||||
$this->assertInstanceOf(Lead::class, $lead);
|
||||
$this->assertSame('Test Delete Company', $lead->getCompany());
|
||||
|
||||
$companyModel->deleteEntity($company);
|
||||
|
||||
$this->em->refresh($lead);
|
||||
$this->assertInstanceOf(Lead::class, $lead);
|
||||
$this->assertNull($lead->getCompany());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadListRepository;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class SegmentSubscriberTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $filters
|
||||
* @param string[] $expectedTranslations
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('filterProvider')]
|
||||
public function testSegmentFilterAlertMessages(array $filters, array $expectedTranslations): void
|
||||
{
|
||||
$segment = $this->saveSegment('Segment D', 'segment-d', $filters);
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segment->getId());
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
/** @var TranslatorInterface $translator */
|
||||
$translator = $this->getContainer()->get('translator');
|
||||
|
||||
$expectedTranslationString = implode(' ', array_map(fn ($trans) => $translator->trans($trans), $expectedTranslations));
|
||||
|
||||
$crawlerText = $crawler->filter('#leadlist_filters_0_properties')->filter('.alert')->text();
|
||||
$this->assertStringContainsString($expectedTranslationString, $crawlerText);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<array<mixed>>
|
||||
*/
|
||||
public static function filterProvider(): \Generator
|
||||
{
|
||||
yield [[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => 'like',
|
||||
],
|
||||
], ['mautic.lead_list.filter.alert.like', 'mautic.lead_list.filter.alert.email']];
|
||||
yield [[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'firstname',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'operator' => 'contains',
|
||||
],
|
||||
], ['mautic.lead_list.filter.alert.contain']];
|
||||
yield [[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'firstname',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'operator' => 'like',
|
||||
],
|
||||
], ['mautic.lead_list.filter.alert.like']];
|
||||
yield [[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'firstname',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'operator' => 'endsWith',
|
||||
],
|
||||
], ['mautic.lead_list.filter.alert.endwith']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $filters
|
||||
*/
|
||||
private function saveSegment(string $name, string $alias, array $filters): LeadList
|
||||
{
|
||||
$segmentRepo = $this->em->getRepository(LeadList::class);
|
||||
\assert($segmentRepo instanceof LeadListRepository);
|
||||
$segment = new LeadList();
|
||||
$segment->setName($name)
|
||||
->setPublicName($name)
|
||||
->setFilters($filters)
|
||||
->setAlias($alias);
|
||||
$segmentRepo->saveEntity($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\Helper;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Helper\SegmentCountCacheHelper;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class SegmentCountCacheHelperTest extends MauticMysqlTestCase
|
||||
{
|
||||
private const SEGMENT_ID = 1;
|
||||
|
||||
private SegmentCountCacheHelper $segmentCountCacheHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->segmentCountCacheHelper = $this->getContainer()->get(SegmentCountCacheHelper::class);
|
||||
|
||||
// Delete the cache before each test starts.
|
||||
$this->segmentCountCacheHelper->setSegmentContactCount(self::SEGMENT_ID, 20);
|
||||
$this->segmentCountCacheHelper->deleteSegmentContactCount(self::SEGMENT_ID);
|
||||
}
|
||||
|
||||
public function testWorkflowForSegmentCount(): void
|
||||
{
|
||||
Assert::assertSame(0, $this->segmentCountCacheHelper->getSegmentContactCount(self::SEGMENT_ID));
|
||||
Assert::assertFalse($this->segmentCountCacheHelper->hasSegmentContactCount(self::SEGMENT_ID));
|
||||
|
||||
$this->segmentCountCacheHelper->setSegmentContactCount(self::SEGMENT_ID, 100);
|
||||
Assert::assertSame(100, $this->segmentCountCacheHelper->getSegmentContactCount(self::SEGMENT_ID));
|
||||
Assert::assertTrue($this->segmentCountCacheHelper->hasSegmentContactCount(self::SEGMENT_ID));
|
||||
|
||||
$this->segmentCountCacheHelper->incrementSegmentContactCount(self::SEGMENT_ID);
|
||||
Assert::assertSame(101, $this->segmentCountCacheHelper->getSegmentContactCount(self::SEGMENT_ID));
|
||||
|
||||
$this->segmentCountCacheHelper->decrementSegmentContactCount(self::SEGMENT_ID);
|
||||
Assert::assertSame(100, $this->segmentCountCacheHelper->getSegmentContactCount(self::SEGMENT_ID));
|
||||
|
||||
$this->segmentCountCacheHelper->deleteSegmentContactCount(self::SEGMENT_ID);
|
||||
Assert::assertSame(0, $this->segmentCountCacheHelper->getSegmentContactCount(self::SEGMENT_ID));
|
||||
Assert::assertFalse($this->segmentCountCacheHelper->hasSegmentContactCount(self::SEGMENT_ID));
|
||||
}
|
||||
|
||||
public function testDecrementCannotGoNegative(): void
|
||||
{
|
||||
Assert::assertSame(0, $this->segmentCountCacheHelper->getSegmentContactCount(self::SEGMENT_ID));
|
||||
|
||||
// Ensure we cannot decrement bellow zero.
|
||||
$this->segmentCountCacheHelper->decrementSegmentContactCount(self::SEGMENT_ID);
|
||||
Assert::assertSame(0, $this->segmentCountCacheHelper->getSegmentContactCount(self::SEGMENT_ID));
|
||||
}
|
||||
|
||||
public function testWorkflowForSegmentReount(): void
|
||||
{
|
||||
Assert::assertFalse($this->segmentCountCacheHelper->hasSegmentIdForReCount(self::SEGMENT_ID));
|
||||
|
||||
$this->segmentCountCacheHelper->invalidateSegmentContactCount(self::SEGMENT_ID);
|
||||
|
||||
Assert::assertTrue($this->segmentCountCacheHelper->hasSegmentIdForReCount(self::SEGMENT_ID));
|
||||
|
||||
// Setting the count will delete the invalidation.
|
||||
$this->segmentCountCacheHelper->setSegmentContactCount(self::SEGMENT_ID, 100);
|
||||
|
||||
Assert::assertFalse($this->segmentCountCacheHelper->hasSegmentIdForReCount(self::SEGMENT_ID));
|
||||
|
||||
// Cleanup.
|
||||
$this->segmentCountCacheHelper->deleteSegmentContactCount(self::SEGMENT_ID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional\Model;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\CompanyLead;
|
||||
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
|
||||
final class CompanyModelFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
|
||||
public function testAddLeadToCompanyWithLeadAsArray(): void
|
||||
{
|
||||
// Create a lead
|
||||
$lead = $this->createLead('User', 'One', 'user@company_a.com');
|
||||
// Create a company
|
||||
$company = $this->createCompany('Company A', 'contact@company_a.com');
|
||||
$this->em->flush();
|
||||
|
||||
/** @var CompanyLeadRepository $companyLeadRepo */
|
||||
$companyLeadRepo = $this->em->getRepository(CompanyLead::class);
|
||||
|
||||
$this->assertEquals(0, $companyLeadRepo->count([]));
|
||||
|
||||
/** @var CompanyModel $companyModel */
|
||||
$companyModel = self::getContainer()->get('mautic.lead.model.company');
|
||||
$companyModel->addLeadToCompany($company, $lead->convertToArray());
|
||||
|
||||
$this->assertEquals(1, $companyLeadRepo->count([]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional;
|
||||
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class SearchWithCustomFieldDataFunctionalTest extends AbstractSearchTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataTestCreatingCustomFieldIndexableAndSearchable')]
|
||||
public function testCreatingCustomFieldIndexableAndSearchable(int $isIndex, string $expectedValue): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, 's/contacts/fields/new');
|
||||
$this->assertResponseIsSuccessful('Failed to load the form: '.$this->client->getResponse()->getContent());
|
||||
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
|
||||
$defaultValues = [
|
||||
'leadfield[label]' => 'Custom field',
|
||||
'leadfield[alias]' => 'custom_field',
|
||||
'leadfield[object]' => 'lead',
|
||||
'leadfield[type]' => 'text',
|
||||
'leadfield[group]' => 'core',
|
||||
'leadfield[isIndex]' => $isIndex,
|
||||
];
|
||||
|
||||
$form->setValues($defaultValues);
|
||||
|
||||
$this->client->submit($form);
|
||||
$formValuesUpdated = $form->getValues();
|
||||
|
||||
$this->assertSame($expectedValue, $formValuesUpdated['leadfield[isIndex]'], 'Mismatch for field isIndex');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{0: int, 1: string}>
|
||||
*/
|
||||
public static function dataTestCreatingCustomFieldIndexableAndSearchable(): iterable
|
||||
{
|
||||
yield 'When "Add to Search Index" is enabled' => [1, '1'];
|
||||
|
||||
yield 'When "Add to Search Index" is disabled' => [0, '0'];
|
||||
}
|
||||
|
||||
public function testGlobalSearchForContactsUsingCustomFieldsData(): void
|
||||
{
|
||||
// Create a custom field for Contact
|
||||
$customFieldAlias = 'client_id';
|
||||
$this->createSearchableField($customFieldAlias, 'lead');
|
||||
|
||||
// Create three contacts, one without custom field data.
|
||||
$contactData = [
|
||||
[
|
||||
'firstname' => 'Contact',
|
||||
'lastname' => 'One',
|
||||
'email' => 'c@one.com',
|
||||
'company' => 'One',
|
||||
'customFields' => [$customFieldAlias => 'client_1'],
|
||||
],
|
||||
[
|
||||
'firstname' => 'Contact',
|
||||
'lastname' => 'Two',
|
||||
'email' => 'c@two.com',
|
||||
'company' => 'Two',
|
||||
'customFields' => [$customFieldAlias => 'client_2'],
|
||||
],
|
||||
[
|
||||
'firstname' => 'Contact',
|
||||
'lastname' => 'Three',
|
||||
'email' => 'c@three.com',
|
||||
'company' => 'Three',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($contactData as $contactDatum) {
|
||||
$this->createContact($contactDatum);
|
||||
}
|
||||
|
||||
// Search
|
||||
$response = $this->performSearch('/s/ajax?action=globalSearch&global_search=client&tmp=list');
|
||||
$content = \json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($content['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$this->assertSame('Contacts 2', $crawler->filterXPath('//div[@id="globalSearchPanel"]//div[contains(@class, "text-secondary")]')->text());
|
||||
|
||||
$results = $crawler->filterXPath('//ul[contains(@class, "pa-0")]');
|
||||
$this->assertCount(2, $results->filter('li'));
|
||||
|
||||
foreach ($results->filter('li')->each(fn ($li) => $li->filter('a')->eq(0)->html()) as $i => $result) {
|
||||
$this->assertStringContainsString($contactData[$i]['firstname'], $results->text());
|
||||
$this->assertStringContainsString($contactData[$i]['lastname'], $results->text());
|
||||
}
|
||||
}
|
||||
|
||||
public function testSearchCompaniesWithCustomFields(): void
|
||||
{
|
||||
// Create a custom field for Company
|
||||
$customFieldAlias = 'client_id';
|
||||
$this->createSearchableField($customFieldAlias, 'company');
|
||||
|
||||
// Create companies
|
||||
$this->createCompany([
|
||||
'name' => 'ABC',
|
||||
'email' => 'hello@abcexample.com',
|
||||
'customFields' => [$customFieldAlias => 'client_id'],
|
||||
]);
|
||||
|
||||
$this->createCompany([
|
||||
'name' => 'XYZ',
|
||||
'email' => 'hello@xyzexample.com',
|
||||
]);
|
||||
|
||||
// Search
|
||||
$response = $this->performSearch('/s/companies?search=client&tmp=list');
|
||||
$content = \json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($content['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
|
||||
$this->assertStringContainsString('ABC', $crawler->html());
|
||||
$this->assertStringNotContainsString('XYZ', $crawler->html());
|
||||
|
||||
$translator = static::getContainer()->get('translator');
|
||||
|
||||
$this->assertStringContainsString(
|
||||
$translator->trans('mautic.core.pagination.items', ['%count%' => 1]),
|
||||
$crawler->html()
|
||||
);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
$translator->trans('mautic.core.pagination.pages', ['%count%' => 1]),
|
||||
$crawler->html()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Functional;
|
||||
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
class SearchWithSpecialCharactersInFieldTest extends AbstractSearchTestCase
|
||||
{
|
||||
public function testGlobalSearchContactWithSpecialCharacterInName(): void
|
||||
{
|
||||
// Create a contact with first name 'R&D'
|
||||
$this->createContact([
|
||||
'firstname' => 'R&D',
|
||||
'lastname' => 'Contact',
|
||||
'email' => 'randd@example.com',
|
||||
'company' => 'TestCompany',
|
||||
]);
|
||||
|
||||
// URL-encode the ampersand: R&D becomes R%26D
|
||||
$response = $this->performSearch('/s/ajax?action=globalSearch&global_search=R%26D&tmp=list');
|
||||
$content = \json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($content['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
|
||||
// Assert that the contact name 'R&D' appears in the search results (html encoded)
|
||||
$this->assertStringContainsString('R&D', $crawler->html());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user