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,87 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Entity\Stat;
use Symfony\Component\HttpFoundation\Request;
final class BotRatioHelperFunctionalTest extends MauticMysqlTestCase
{
private const DO_NOT_TRACK_IP = '218.30.65.10';
private const BOT_BLOCKED_IP = '218.30.65.11';
private const IP_NOT_IN_ANY_BLOCK_LIST = '218.30.65.12';
private const IP_NOT_IN_ANY_BLOCK_LIST2 = '218.30.65.111';
private const BOT_BLOCKED_USER_AGENTS = [
'AHC/2.1',
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6',
'Mozilla/5.0 (compatible; Codewisebot/2.0; +http://www.nosite.com/somebot.htm)',
'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B411 Safari/600.1.4 (compatible; YandexMobileBot/3.0; +http://yandex.com/bots)',
'serpstatbot/2.0 beta (advanced backlink tracking bot; http://serpstatbot.com/; abuse@serpstatbot.com)',
'Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; AspiegelBot)',
'serpstatbot/2.1 (advanced backlink tracking bot; https://serpstatbot.com/; abuse@serpstatbot.com)',
];
protected function setUp(): void
{
$this->configParams['do_not_track_ips'] = [self::DO_NOT_TRACK_IP];
$this->configParams['bot_helper_blocked_ip_addresses'] = [self::BOT_BLOCKED_IP];
$this->configParams['bot_helper_blocked_user_agents'] = self::BOT_BLOCKED_USER_AGENTS;
parent::setUp();
}
/**
* @throws \Doctrine\ORM\ORMException
*/
#[\PHPUnit\Framework\Attributes\DataProvider('hitBotScenariosProvider')]
public function testIsHitByBotFunctional(string $trackingHash, string $sentBefore, string $userAgent, string $ipAddress, bool $isRead): void
{
$stat = new Stat();
$emailSendTime = new \DateTime();
$stat->setDateSent($emailSendTime->modify($sentBefore));
$stat->setTrackingHash($trackingHash);
$stat->setEmailAddress('lukas@mautic.test');
$this->em->persist($stat);
$this->em->flush();
$statId = $stat->getId();
$server = [
'HTTP_USER_AGENT' => $userAgent,
'REMOTE_ADDR' => $ipAddress,
];
$this->client->request(Request::METHOD_GET, '/email/'.$stat->getTrackingHash().'.gif', [], [], $server);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$updatedStat = $this->em->getRepository(Stat::class)->findOneBy(['id'=>$statId]);
$this->assertSame($isRead, $updatedStat->getIsRead());
if ($isRead) {
$this->assertNotNull($updatedStat->getLastOpened());
} else {
$this->assertNull($updatedStat->getLastOpened());
}
}
/**
* @return iterable<string, array<mixed>>
*/
public static function hitBotScenariosProvider(): iterable
{
// $trackingHash, $sentBefore, $userAgent, $ipAddress, $isRead
yield 'All good' => ['test_hash_bot_ratio_1', '-80 second', 'Mozilla/5.0', self::IP_NOT_IN_ANY_BLOCK_LIST, true];
yield 'Time and User' => ['test_hash_bot_ratio_2', '+80 second', 'AHC/2.1', self::IP_NOT_IN_ANY_BLOCK_LIST, false];
yield 'Time and IP' => ['test_hash_bot_ratio_3', '+80 second', 'Mozilla/5.0', self::BOT_BLOCKED_IP, false];
yield 'Permanently blocked IP' => ['test_hash_bot_ratio_4', '-80 second', 'Mozilla/5.0', self::DO_NOT_TRACK_IP, false];
yield 'Bot Blocked IP address only' => ['test_hash_bot_ratio_5', '-80 second', 'Mozilla/5.0', self::BOT_BLOCKED_IP, true];
yield 'Bot Blocked User Agent only' => ['test_hash_bot_ratio_6', '-80 second', 'AHC/2.1', self::IP_NOT_IN_ANY_BLOCK_LIST, true];
yield 'Time Only' => ['test_hash_bot_ratio_7', '+80 second', 'Mozilla/5.0', self::IP_NOT_IN_ANY_BLOCK_LIST, true];
yield 'Time and Bot User Agent and Bot IP' => ['test_hash_bot_ratio_8', '+80 second', 'AHC/2.1', self::BOT_BLOCKED_IP, false];
yield 'Bot User Agent and Bot IP' => ['test_hash_bot_ratio_9', '-80 second', 'AHC/2.1', self::BOT_BLOCKED_IP, false];
yield 'Permanently blocked User Agent' => ['test_hash_bot_ratio_10', '-80 second', 'MSNBOT', self::IP_NOT_IN_ANY_BLOCK_LIST2, false];
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PageBundle\Entity\Hit;
use Mautic\PageBundle\Entity\HitRepository;
use Mautic\PageBundle\Entity\Page;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
final class EmailClickTrackingTest extends MauticMysqlTestCase
{
public function testEmailClick(): void
{
$contact = new Lead();
$contact->setEmail('john@doe.cz');
$email = new Email();
$email->setName('Test email');
$email->setSubject('Test email');
$email->setCustomHtml('<html><head></head><body>Test email</body></html>');
$stat = new Stat();
$stat->setLead($contact);
$stat->setEmail($email);
$stat->setEmailAddress('john@doe.cz');
$stat->setTrackingHash('67167f57a4c05265936091');
$stat->setDateSent(new \DateTime());
$page = new Page();
$page->setTitle('Test page');
$page->setAlias('test-page');
$page->setCustomHtml('<html><head></head><body>Test page</body></html>');
$this->em->persist($contact);
$this->em->persist($email);
$this->em->persist($stat);
$this->em->persist($page);
$this->em->flush();
$this->logoutUser();
$this->client->request(Request::METHOD_GET, '/test-page?&ct=YToxOntzOjQ6InN0YXQiO3M6MjI6IjY3MTY3ZjU3YTRjMDUyNjU5MzYwOTEiO30%3D');
Assert::assertTrue($this->client->getResponse()->isSuccessful());
$pageHitRepository = $this->em->getRepository(Hit::class);
\assert($pageHitRepository instanceof HitRepository);
$hit = $pageHitRepository->findOneBy(['page' => $page]);
Assert::assertSame($contact->getId(), $hit->getLead()->getId());
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Functional;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\Persistence\Mapping\MappingException;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
use Mautic\CoreBundle\Tests\Functional\UserEntityTrait;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Entity\UserRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class EmailContactGridTest extends MauticMysqlTestCase
{
use CreateTestEntitiesTrait;
use UserEntityTrait;
/**
* @throws OptimisticLockException
* @throws ORMException
* @throws MappingException
*/
public function testEmailContactsGridWithValidPermissions(): void
{
list($email, $contactOne, $contactTwo) = $this->setupData();
// create users
$nonAdminUser = $this->createUserWithPermission([
'user-name' => 'non-admin',
'email' => 'non-admin@mautic-test.com',
'first-name' => 'non-admin',
'last-name' => 'non-admin',
'role' => [
'name' => 'perm_non_admin',
'permissions' => [
'lead:leads' => 6,
'email:emails' => 6,
],
],
]);
$this->em->flush();
$this->em->clear();
$this->loginOtherUser($nonAdminUser);
$this->client->request(Request::METHOD_GET, '/s/emails/view/'.$email->getId());
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
$content = $this->client->getResponse()->getContent();
$this->assertStringContainsString($contactOne->getName(), $content);
$this->assertStringContainsString($contactTwo->getName(), $content);
}
/**
* @throws OptimisticLockException
* @throws MappingException
* @throws ORMException
*/
public function testEmailContactsGridWithIncompletePermissions(): void
{
/** @var Email $email */
list($email, $contactOne, $contactTwo) = $this->setupData();
// create users
$nonAdminUser = $this->createUserWithPermission([
'user-name' => 'non-admin',
'email' => 'non-admin@mautic-test.com',
'first-name' => 'non-admin',
'last-name' => 'non-admin',
'role' => [
'name' => 'perm_non_admin',
'permissions' => [
'lead:leads' => 2,
'email:emails' => 6,
],
],
]);
$email->setCreatedBy($nonAdminUser);
$this->em->persist($email);
$this->em->flush();
$this->em->clear();
$this->loginOtherUser($nonAdminUser);
$this->client->request(Request::METHOD_GET, '/s/emails/view/'.$email->getId());
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
$content = $this->client->getResponse()->getContent();
$this->assertStringContainsString('No Contacts Found', $content, $content);
}
/**
* @throws ORMException
*/
private function createStats(Email $email, Lead $contactOne): void
{
$emailStat = new Stat();
$emailStat->setEmail($email);
$emailStat->setLead($contactOne);
$emailStat->setDateSent(new \DateTime());
$emailStat->setEmailAddress($contactOne->getEmail());
$this->em->persist($emailStat);
}
/**
* @param array<string, mixed> $userDetails
*/
private function createUserWithPermission(array $userDetails): User
{
$role = $this->createRole($userDetails['role']['name']);
foreach ($userDetails['role']['permissions'] as $permission => $bitwise) {
$this->createPermission($role, $permission, $bitwise);
}
return $this->createUser($userDetails['email'], $userDetails['user-name'], $userDetails['first-name'], $userDetails['last-name'], $role);
}
/**
* @return array<mixed>
*
* @throws ORMException
*/
private function setupData(): array
{
/** @var UserRepository $userRepository */
$userRepository = $this->em->getRepository(User::class);
$adminUser = $userRepository->findOneBy(['username' => 'admin']);
$segment = $this->createSegment('SegmentOne', []);
$email = $this->createEmail('Hello');
$email->setEmailType('list');
$email->addList($segment);
$email->setCustomHtml('<h1>Email content created by an API test</h1>{custom-token}<br>{signature}');
$email->setIsPublished(true);
$this->em->persist($email);
// Create Contact
$contactOne = $this->createLead('John', '', 'john@contact.email', $adminUser);
$contactTwo = $this->createLead('Alex', '', 'alex@contact.email', $adminUser);
// Create stats
$this->createStats($email, $contactOne);
$this->createStats($email, $contactTwo);
return [$email, $contactOne, $contactTwo];
}
}

View File

@@ -0,0 +1,313 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Functional;
use Mautic\CampaignBundle\Tests\Functional\Fixtures\FixtureHelper;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Tests\Functional\Fixtures\EmailFixturesHelper;
use Mautic\FormBundle\Entity\Action;
use Mautic\FormBundle\Entity\Form;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\PointBundle\Entity\Point;
use Mautic\PointBundle\Entity\Trigger;
use Mautic\PointBundle\Entity\TriggerEvent;
use Mautic\ReportBundle\Entity\Report;
final class EmailDependenciesFunctionalTest extends MauticMysqlTestCase
{
private FixtureHelper $campaignFixturesHelper;
private EmailFixturesHelper $emailFixturesHelper;
protected function setUp(): void
{
parent::setUp();
$this->campaignFixturesHelper = new FixtureHelper($this->em);
$this->emailFixturesHelper = new EmailFixturesHelper($this->em);
}
public function testEmailUsageInSegments(): void
{
$email = $this->emailFixturesHelper->createEmail();
$this->em->flush();
$segmentRead = $this->createSegment('read-email', [
[
'glue' => 'and',
'field' => 'lead_email_received',
'object' => 'behaviors',
'type' => 'lead_email_received',
'operator' => 'in',
'properties' => [
'filter' => [
$email->getId(),
],
],
],
]);
$segmentSent = $this->createSegment('sent-email', [
[
'glue' => 'and',
'field' => 'lead_email_sent',
'object' => 'behaviors',
'type' => 'lead_email_received', // it is saved like this
'operator' => 'in',
'properties' => [
'filter' => [
$email->getId(),
],
],
],
]);
$this->createSegment('other');
$this->em->persist($email);
$this->em->flush();
$this->client->request('GET', "/s/ajax?action=email:getEmailUsages&id={$email->getId()}");
$clientResponse = $this->client->getResponse();
$jsonResponse = json_decode($clientResponse->getContent(), true);
$searchIds = join(',', [$segmentRead->getId(), $segmentSent->getId()]);
$this->assertStringContainsString("/s/segments?search=ids:{$searchIds}", $jsonResponse['usagesHtml']);
}
public function testEmailUsageInCampaigns(): void
{
$email = $this->emailFixturesHelper->createEmail();
$this->em->flush();
$campaign = $this->campaignFixturesHelper->createCampaignWithEmailSent($email->getId());
$this->client->request('GET', "/s/ajax?action=email:getEmailUsages&id={$email->getId()}");
$clientResponse = $this->client->getResponse();
$jsonResponse = json_decode($clientResponse->getContent(), true);
$searchIds = join(',', [$campaign->getId()]);
$this->assertStringContainsString("/s/campaigns?search=ids:{$searchIds}", $jsonResponse['usagesHtml']);
}
public function testEmailUsageWithoutDuplicates(): void
{
$email = $this->emailFixturesHelper->createEmail();
$this->em->flush();
$formWithEmailSend = $this->createForm('form-with-email-send');
$this->createFormActionEmailSend($formWithEmailSend, $email->getId());
$this->createFormActionEmailSendToUser($formWithEmailSend, $email->getId());
$this->client->request('GET', "/s/ajax?action=email:getEmailUsages&id={$email->getId()}");
$clientResponse = $this->client->getResponse();
$jsonResponse = json_decode($clientResponse->getContent(), true);
$formId = $formWithEmailSend->getId();
$this->assertStringNotContainsString("/s/forms?search=ids:{$formId},{$formId}", $jsonResponse['usagesHtml']);
}
public function testEmailUsageInForms(): void
{
$email = $this->emailFixturesHelper->createEmail();
$this->em->flush();
$formWithEmailSend = $this->createForm('form-with-email-send');
$this->createFormActionEmailSend($formWithEmailSend, $email->getId());
$formWithEmailSendToUser = $this->createForm('form-with-email-send-to-user');
$this->createFormActionEmailSendToUser($formWithEmailSendToUser, $email->getId());
$this->client->request('GET', "/s/ajax?action=email:getEmailUsages&id={$email->getId()}");
$clientResponse = $this->client->getResponse();
$jsonResponse = json_decode($clientResponse->getContent(), true);
$searchIds = join(',', [$formWithEmailSend->getId(), $formWithEmailSendToUser->getId()]);
$this->assertStringContainsString("/s/forms?search=ids:{$searchIds}", $jsonResponse['usagesHtml']);
}
public function testEmailUsageInPointActions(): void
{
$email = $this->emailFixturesHelper->createEmail();
$this->em->flush();
$pointActionIsSent = $this->createEmailPointAction($email->getId(), 'email.send');
$pointActionIsOpen = $this->createEmailPointAction($email->getId(), 'email.open');
$this->client->request('GET', "/s/ajax?action=email:getEmailUsages&id={$email->getId()}");
$clientResponse = $this->client->getResponse();
$jsonResponse = json_decode($clientResponse->getContent(), true);
$searchIds = join(',', [$pointActionIsSent->getId(), $pointActionIsOpen->getId()]);
$this->assertStringContainsString("/s/points?search=ids:{$searchIds}", $jsonResponse['usagesHtml']);
}
public function testEmailUsageInPointTriggers(): void
{
$email = $this->emailFixturesHelper->createEmail();
$this->em->flush();
$pointActionIsSent = $this->createPointTriggerWithEmailSendEvent($email->getId(), 'email.send');
$this->client->request('GET', "/s/ajax?action=email:getEmailUsages&id={$email->getId()}");
$clientResponse = $this->client->getResponse();
$jsonResponse = json_decode($clientResponse->getContent(), true);
$searchIds = join(',', [$pointActionIsSent->getId()]);
$this->assertStringContainsString("/s/points/triggers?search=ids:{$searchIds}", $jsonResponse['usagesHtml']);
}
public function testEmailUsageInReports(): void
{
$email = $this->emailFixturesHelper->createEmail();
$this->em->flush();
$emailReport = $this->createEmailReport($email->getId());
$emailStatsReport = $this->createEmailStatsReport($email->getId());
$this->client->request('GET', "/s/ajax?action=email:getEmailUsages&id={$email->getId()}");
$clientResponse = $this->client->getResponse();
$jsonResponse = json_decode($clientResponse->getContent(), true);
$searchIds = join(',', [$emailReport->getId(), $emailStatsReport->getId()]);
$this->assertStringContainsString("/s/reports?search=ids:{$searchIds}", $jsonResponse['usagesHtml']);
}
private function createEmailReport(int $emailId): Report
{
$report = new Report();
$report->setName('Contact report');
$report->setSource('emails');
$report->setColumns([
'e.id',
'e.name',
]);
$report->setFilters([
[
'column' => 'e.id',
'glue' => 'and',
'dynamic' => null,
'condition' => 'eq',
'value' => $emailId,
],
]);
$this->em->persist($report);
$this->em->flush();
return $report;
}
private function createEmailStatsReport(int $emailId): Report
{
$report = new Report();
$report->setName('Contact report');
$report->setSource('email.stats');
$report->setColumns([
'l.id',
'es.date_read',
'es.date_sent',
'e.id',
'e.name',
]);
$report->setFilters([
[
'column' => 'e.id',
'glue' => 'and',
'dynamic' => null,
'condition' => 'eq',
'value' => $emailId,
],
]);
$this->em->persist($report);
$this->em->flush();
return $report;
}
private function createEmailPointAction(int $emailId, string $type): Point
{
$pointAction = new Point();
$pointAction->setName('Is sent email');
$pointAction->setDelta(1);
$pointAction->setType($type);
$pointAction->setProperties(['emails' => [$emailId]]);
$this->em->persist($pointAction);
$this->em->flush();
return $pointAction;
}
private function createPointTriggerWithEmailSendEvent(int $emailId, string $type): Trigger
{
$pointTrigger = new Trigger();
$pointTrigger->setName('trigger');
$this->em->persist($pointTrigger);
$this->em->flush();
$triggerEvent = new TriggerEvent();
$triggerEvent->setTrigger($pointTrigger);
$triggerEvent->setName('event');
$triggerEvent->setType($type);
$triggerEvent->setProperties(['email'=>$emailId]);
$this->em->persist($triggerEvent);
$this->em->flush();
return $pointTrigger;
}
private function createForm(string $alias): Form
{
$form = new Form();
$form->setName($alias);
$form->setAlias($alias);
$this->em->persist($form);
$this->em->flush();
return $form;
}
private function createFormActionEmailSend(Form $form, int $emailId): Action
{
$action = new Action();
$action->setName('send email');
$action->setForm($form);
$action->setType('email.send.lead');
$action->setProperties(['email'=> $emailId]);
$this->em->persist($action);
$this->em->flush();
return $action;
}
private function createFormActionEmailSendToUser(Form $form, int $emailId): Action
{
$action = new Action();
$action->setName('send email');
$action->setForm($form);
$action->setType('email.send.lead');
$action->setProperties([
'useremail' => ['email' => $emailId],
'user_id' => [1],
]);
$this->em->persist($action);
$this->em->flush();
return $action;
}
/**
* @param array<int, array<string, mixed>> $filters
*/
private function createSegment(string $alias, array $filters = []): LeadList
{
$segment = new LeadList();
$segment->setName($alias);
$segment->setPublicName($alias);
$segment->setAlias($alias);
$segment->setFilters($filters);
$this->em->persist($segment);
$this->em->flush();
return $segment;
}
}

View File

@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\EmailBundle\Entity\StatRepository;
use Mautic\EmailBundle\Model\EmailModel;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadField;
use PHPUnit\Framework\Assert;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
class EmailTokenTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testEmailTokens(): void
{
$lead = $this->createLeadWithAllFields();
$email = new Email();
$email->setEmailType('list');
$email->setName('CO token test email');
$email->setSubject('CO token test email');
$email->setCustomHtml('
Dear %7Bcontactfield=firstname%7D {contactfield=lastname},
Check these fields:
Mobile: %7Bcontactfield%3Dmobile%7D
Address: {contactfield=address1}, {contactfield=address2}, {contactfield=city}, {contactfield=country}
Email: {contactfield=email}
Custom Values:
Contact:
Bool: {contactfield=boollead},
Date: {contactfield=datelead},
Date/Time: {contactfield=datetimelead}
Email: {contactfield=emaillead}
HTML: {contactfield=htmllead}
Country: {contactfield=countrylead}
Locale: {contactfield=localelead}
Number: {contactfield=numberlead}
Phone: {contactfield=phonelead}
Region: {contactfield=regionlead}
Text: {contactfield=textlead}
Textarea: {contactfield=textarealead}
Time: {contactfield=timelead}
Timezone: {contactfield=timezonelead}
URL: {contactfield=urllead}
');
$this->em->persist($email);
$this->em->flush();
/** @var EmailModel $emailModel */
$emailModel = self::getContainer()->get('mautic.email.model.email');
$emailModel->sendEmail(
$email,
[
[
'id' => $lead->getId(),
'email' => $lead->getEmail(),
'firstname' => $lead->getFirstname(),
'lastname' => $lead->getLastname(),
'mobile' => $lead->getMobile(),
'address1' => $lead->getAddress1(),
'address2' => $lead->getAddress2(),
'city' => $lead->getCity(),
'country' => $lead->getCountry(),
'boollead' => $lead->getUpdatedFields()['boollead'],
'datelead' => $lead->getUpdatedFields()['datelead'],
'datetimelead' => $lead->getUpdatedFields()['datetimelead'],
'emaillead' => $lead->getUpdatedFields()['emaillead'],
'htmllead' => $lead->getUpdatedFields()['htmllead'],
'countrylead' => $lead->getUpdatedFields()['countrylead'],
'localelead' => $lead->getUpdatedFields()['localelead'],
'numberlead' => $lead->getUpdatedFields()['numberlead'],
'phonelead' => $lead->getUpdatedFields()['phonelead'],
'regionlead' => $lead->getUpdatedFields()['regionlead'],
'textlead' => $lead->getUpdatedFields()['textlead'],
'textarealead' => $lead->getUpdatedFields()['textarealead'],
'timelead' => $lead->getUpdatedFields()['timelead'],
'timezonelead' => $lead->getUpdatedFields()['timezonelead'],
'urllead' => $lead->getUpdatedFields()['urllead'],
],
]
);
/** @var StatRepository $emailStatRepository */
$emailStatRepository = $this->em->getRepository(Stat::class);
/** @var Stat|null $emailStat */
$emailStat = $emailStatRepository->findOneBy(
[
'email' => $email->getId(),
'lead' => $lead->getId(),
]
);
Assert::assertNotNull($emailStat);
$crawler = $this->client->request(Request::METHOD_GET, "/email/view/{$emailStat->getTrackingHash()}");
$body = $crawler->filter('body');
// Remove the tracking tags that are causing troubles with different Mautic configurations.
$body->filter('a,img,div')->each(function (Crawler $crawler) {
foreach ($crawler as $node) {
$node->parentNode->removeChild($node);
}
});
Assert::assertSame(
$this->stripWhiteSpaces('Dear Test Lead,
Check these fields:
Mobile: 012
Address: Lane 11, Near Post Office, Pune, India
Email: test@domain.tld
Custom Values:
Contact:
Bool: 1,
Date: 2022-07-01,
Date/Time: 2022-07-01 20:22
Email: test@test.com
HTML: <p>This is some normal text.</p>
Country: India
Locale: Hindi
Number: 400
Phone: 1234567
Region: Maharashtra
Text: this is text
Textarea: This is a paragraph
Time: 20:00
Timezone: Kolkata
URL: www.example.com'),
$this->stripWhiteSpaces($body->html())
);
}
/**
* @return array <mixed>
*/
private function customFieldTypes(): array
{
return [
'bool' => ['boolean', true],
'date' => ['date', '2022-07-01'],
'datetime' => ['datetime', '2022-07-01 20:22'],
'email' => ['email', 'test@test.com'],
'html' => ['html', '<p>This is some normal text.</p>'],
'country' => ['country', 'India'],
'locale' => ['locale', 'Hindi'],
'number' => ['number', 400],
'phone' => ['tel', 1234567],
'region' => ['region', 'Maharashtra'],
'text' => ['text', 'this is text'],
'textarea' => ['textarea', 'This is a paragraph'],
'time' => ['time', '20:00'],
'timezone' => ['timezone', 'Kolkata'],
'url' => ['url', 'www.example.com'],
];
}
private function createLeadWithAllFields(): Lead
{
$leadModel = self::getContainer()->get('mautic.lead.model.lead');
$fieldModel = self::getContainer()->get('mautic.lead.model.field');
$lead = new Lead();
$lead->setFirstname('Test');
$lead->setLastname('Lead');
$lead->setMobile('012');
$lead->setAddress1('Lane 11');
$lead->setAddress2('Near Post Office');
$lead->setCity('Pune');
$lead->setCountry('India');
$lead->setEmail('test@domain.tld');
$lead->setCompany('Acquia');
foreach ($this->customFieldTypes() as $alias => [$type, $value]) {
$customFieldLead = new LeadField();
$customFieldLead->setLabel($alias.'lead');
$customFieldLead->setAlias($alias.'lead');
$customFieldLead->setType($type);
$customFieldLead->setObject('lead');
$customFieldLead->setIsPublished(true);
$fieldModel->saveEntity($customFieldLead);
$lead->addUpdatedField($customFieldLead->getAlias(), $value);
}
$leadModel->saveEntity($lead);
$this->em->clear();
return $lead;
}
private function stripWhiteSpaces(string $string): string
{
return preg_replace('/\s+/', '', $string);
}
}

View File

@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Functional;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\OptimisticLockException;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\CampaignBundle\Entity\Event;
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\EmailBundle\Entity\StatRepository;
use Mautic\LeadBundle\Entity\Lead;
use PHPUnit\Framework\Assert;
final class EmailVariantInCampaignFunctionalTest extends MauticMysqlTestCase
{
public function testMarketingEmailWithVariantShouldBeSentOnce(): void
{
$contact = new Lead();
$contact->setEmail('test@example.com');
$this->em->persist($contact);
$this->em->flush();
$email = $this->createEmailWithVariant();
$campaign = $this->createCampaignWithDoubleEmailSent($email->getId());
$campaignLead = new CampaignLead();
$campaignLead->setCampaign($campaign);
$campaignLead->setLead($contact);
$campaignLead->setDateAdded(new \DateTime());
$this->em->persist($campaignLead);
$campaign->addLead(0, $campaignLead);
$this->em->flush();
$commandResult = $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId()]);
Assert::assertStringContainsString('2 total events(s) to be processed in batches', $commandResult->getDisplay());
$variant = $email->getVariantChildren()->first();
/** @var StatRepository $emailStatRepository */
$emailStatRepository = $this->em->getRepository(Stat::class);
$countVariantSent = $emailStatRepository->count([
'email' => $variant->getId(),
'lead' => $contact->getId(),
]);
// the email should be sent only one to the contact
$this->assertEquals(1, $countVariantSent);
}
private function createEmailWithVariant(): Email
{
$email = new Email();
$email->setName('Email Parent');
$email->setSubject('Email Parent subject');
$email->setEmailType('template');
$email->setIsPublished(true);
$this->em->persist($email);
$this->em->flush();
$variant = new Email();
$variant->setName('Email Variant');
$variant->setSubject('Email Variant subject');
$variant->setEmailType('template');
$variant->setIsPublished(true);
$variant->setVariantParent($email);
$variant->setVariantSettings(['weight' => 100, 'winnerCriteria' => 'email.openrate']);
$email->addVariantChild($variant);
$this->em->persist($email);
$this->em->persist($variant);
$this->em->flush();
return $email;
}
/**
* Creates campaign that will try to send the same marketing email twice.
*
* Campaign diagram:
* -------------------
* - Start segment -
* -------------------
* | |
* -------------------- ------------------
* - Send email - - Send email -
* -------------------- ------------------
*
* @throws ORMException
* @throws OptimisticLockException
*/
private function createCampaignWithDoubleEmailSent(int $emailId): Campaign
{
$campaign = new Campaign();
$campaign->setName('Test Update contact');
$this->em->persist($campaign);
$this->em->flush();
$emailSend1 = new Event();
$emailSend1->setCampaign($campaign);
$emailSend1->setName('Send email');
$emailSend1->setType('email.send');
$emailSend1->setChannel('email');
$emailSend1->setChannelId($emailId);
$emailSend1->setEventType('action');
$emailSend1->setTriggerMode('immediate');
$emailSend1->setOrder(1);
$emailSend1->setProperties(
[
'canvasSettings' => [
'droppedX' => '549',
'droppedY' => '155',
],
'name' => '',
'triggerMode' => 'immediate',
'triggerDate' => null,
'triggerInterval' => '1',
'triggerIntervalUnit' => 'd',
'triggerHour' => '',
'triggerRestrictedStartHour' => '',
'triggerRestrictedStopHour' => '',
'anchor' => 'leadsource',
'properties' => [
'email' => $emailId,
'email_type' => 'marketing',
'priority' => '2',
'attempts' => '3',
],
'type' => 'email.send',
'eventType' => 'action',
'anchorEventType' => 'source',
'campaignId' => 'mautic_ce6c7dddf8444e579d741c0125f18b33a5d49b45',
'_token' => 'HgysZwvH_n0uAp47CcAcsGddRnRk65t-3crOnuLx28Y',
'buttons' => [
'save' => '',
],
'email' => $emailId,
'email_type' => 'marketing',
'priority' => 2,
'attempts' => 3.0,
]
);
$this->em->persist($emailSend1);
$this->em->flush();
$emailSend2 = new Event();
$emailSend2->setCampaign($campaign);
$emailSend2->setName('Send email 2');
$emailSend2->setType('email.send');
$emailSend2->setChannel('email');
$emailSend2->setChannelId($emailId);
$emailSend2->setEventType('action');
$emailSend2->setTriggerMode('immediate');
$emailSend2->setOrder(1);
$emailSend2->setProperties(
[
'canvasSettings' => [
'droppedX' => '849',
'droppedY' => '155',
],
'name' => '',
'triggerMode' => 'immediate',
'triggerDate' => null,
'triggerInterval' => '1',
'triggerIntervalUnit' => 'd',
'triggerHour' => '',
'triggerRestrictedStartHour' => '',
'triggerRestrictedStopHour' => '',
'anchor' => 'leadsource',
'properties' => [
'email' => $emailId,
'email_type' => 'marketing',
'priority' => '2',
'attempts' => '3',
],
'type' => 'email.send',
'eventType' => 'action',
'anchorEventType' => 'source',
'campaignId' => 'mautic_ce6c7dddf8444e579d741c0125f18b33a5d49b45',
'_token' => 'HgysZwvH_n0uAp47CcAcsGddRnRk65t-3crOnuLx28Y',
'buttons' => [
'save' => '',
],
'email' => $emailId,
'email_type' => 'marketing',
'priority' => 2,
'attempts' => 3.0,
]
);
$this->em->persist($emailSend2);
$this->em->flush();
$campaign->setCanvasSettings(
[
'nodes' => [
[
'id' => $emailSend2->getId(),
'positionX' => '849',
'positionY' => '155',
],
[
'id' => $emailSend1->getId(),
'positionX' => '549',
'positionY' => '155',
],
[
'id' => 'lists',
'positionX' => '796',
'positionY' => '50',
],
],
'connections' => [
[
'sourceId' => 'lists',
'targetId' => $emailSend1->getId(),
'anchors' => [
'source' => 'leadsource',
'target' => 'top',
],
],
[
'sourceId' => 'lists',
'targetId' => $emailSend2->getId(),
'anchors' => [
'source' => 'leadsource',
'target' => 'top',
],
],
],
]
);
$campaign->addEvent(0, $emailSend1);
$campaign->addEvent(1, $emailSend2);
$this->em->persist($campaign);
$this->em->flush();
return $campaign;
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Functional\Fixtures;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PageBundle\Entity\Hit;
use Mautic\PageBundle\Entity\Redirect;
use Mautic\PageBundle\Entity\Trackable;
final class EmailFixturesHelper
{
public function __construct(private EntityManagerInterface $em)
{
}
/**
* @param array<int, mixed> $segments
*/
public function createEmail(
string $name = 'Test email',
string $subject = 'Test subject',
string $emailType = 'template',
bool $isPublished = true,
string $template = 'blank',
string $customHtml = 'Test Html',
array $segments = [],
): Email {
$email = (new Email())
->setName($name)
->setSubject($subject)
->setEmailType($emailType)
->setIsPublished($isPublished)
->setTemplate($template)
->setCustomHtml($customHtml);
if (!empty($segments)) {
$email->setLists($segments);
}
$this->em->persist($email);
return $email;
}
public function emulateEmailSend(Lead $contact, Email $email, string $date = 'now', ?string $source = null, ?int $sourceId = null): Stat
{
$emailStat = new Stat();
$emailStat->setEmail($email);
$emailStat->setLead($contact);
$emailStat->setEmailAddress($contact->getEmail());
$emailStat->setDateSent(new \DateTime($date));
if ($source && $sourceId) {
$emailStat->setSource($source);
$emailStat->setSourceId($sourceId);
}
$email->setSentCount($email->getSentCount() + 1);
$this->em->persist($emailStat);
$this->em->persist($email);
return $emailStat;
}
public function emulateEmailRead(Stat $emailStat, Email $email, string $date = 'now'): Stat
{
$emailStat->setIsRead(true);
$emailStat->setDateRead(new \DateTime($date));
$email->setReadCount($email->getReadCount() + 1);
$this->em->persist($emailStat);
$this->em->persist($email);
return $emailStat;
}
public function createEmailLink(string $url, int $channelId, int $hits = 0, int $uniqueHits = 0): Trackable
{
$redirect = new Redirect();
$redirect->setRedirectId(uniqid());
$redirect->setUrl($url);
$redirect->setHits($hits);
$redirect->setUniqueHits($uniqueHits);
$this->em->persist($redirect);
$trackable = new Trackable();
$trackable->setChannelId($channelId);
$trackable->setChannel('email');
$trackable->setHits($hits);
$trackable->setUniqueHits($uniqueHits);
$trackable->setRedirect($redirect);
$this->em->persist($trackable);
return $trackable;
}
public function emulateLinkClick(Email $email, Trackable $trackable, Lead $lead, string $date = 'now', int $count = 1): void
{
$trackable->setHits($trackable->getHits() + $count);
$trackable->setUniqueHits($trackable->getUniqueHits() + 1);
$this->em->persist($trackable);
$redirect = $trackable->getRedirect();
$ip = new IpAddress();
$ip->setIpAddress('127.0.0.1');
$this->em->persist($ip);
for ($i = 0; $i < $count; ++$i) {
$pageHit = new Hit();
$pageHit->setRedirect($redirect);
$pageHit->setEmail($email);
$pageHit->setLead($lead);
$pageHit->setIpAddress($ip);
$pageHit->setDateHit(new \DateTime($date));
$pageHit->setCode(200);
$pageHit->setUrl($redirect->getUrl());
$pageHit->setTrackingId($redirect->getRedirectId());
$pageHit->setSource('email');
$pageHit->setSourceId($email->getId());
$this->em->persist($pageHit);
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\LeadBundle\Entity\ListLead;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
final class PendingCountTest extends MauticMysqlTestCase
{
/**
* There was an issue that if there is a lead_id = null in the email_stats associated with an email
* then the pending count was always 0 even if there are contacts waiting for sent.
*/
public function testPendingCountWithDeletedContactsInEmailStats(): void
{
$contact = new Lead();
$contact->setEmail('john@doe.email');
$segment = new LeadList();
$segment->setName('Segment A');
$segment->setPublicName('Segment A');
$segment->setAlias('segment-a');
$segmentRef = new ListLead();
$segmentRef->setLead($contact);
$segmentRef->setList($segment);
$segmentRef->setDateAdded(new \DateTime());
$email = new Email();
$email->setName('Email A');
$email->setSubject('Email A Subject');
$email->setEmailType('list');
$email->addList($segment);
$emailStat = new Stat();
$emailStat->setEmail($email);
$emailStat->setLead(null);
$emailStat->setEmailAddress('deleted@contact.email');
$emailStat->setDateSent(new \DateTime());
$this->em->persist($segment);
$this->em->persist($contact);
$this->em->persist($segmentRef);
$this->em->persist($email);
$this->em->persist($emailStat);
$this->em->flush();
// The counts are loaded via ajax call after the email list page loads, so checking the ajax request instead of the HTML.
$this->client->request(Request::METHOD_GET, '/s/ajax?action=email:getEmailCountStats', ['id' => $email->getId()]);
Assert::assertSame(
'{"id":'.$email->getId().',"pending":"1 Pending","queued":0,"sentCount":"0 Sent","readCount":"0 Read","readPercent":"0% Read"}',
$this->client->getResponse()->getContent()
);
}
}