Initial commit: CloudOps infrastructure platform

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

View File

@@ -0,0 +1,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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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']
);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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]
);
}
}

View File

@@ -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()
);
}
}

View File

@@ -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']);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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([]));
}
}

View File

@@ -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()
);
}
}

View File

@@ -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&amp;D', $crawler->html());
}
}