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,149 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Model;
use Mautic\CategoryBundle\Entity\Category;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\EmailRepository;
use Mautic\EmailBundle\Model\EmailActionModel;
use Mautic\EmailBundle\Model\EmailModel;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class EmailActionModelTest extends TestCase
{
public const NEW_CATEGORY_TITLE = 'New category';
public const OLD_CATEGORY_TITLE = 'Old category';
/**
* @var MockObject&EmailModel
*/
private MockObject $emailModelMock;
/**
* @var MockObject&EmailRepository
*/
private MockObject $emailRepositoryMock;
/**
* @var MockObject&CorePermissions
*/
private MockObject $corePermissionsMock;
private EmailActionModel $emailActionModel;
protected function setUp(): void
{
parent::setUp();
$this->emailModelMock = $this->createMock(EmailModel::class);
$this->emailRepositoryMock = $this->createMock(EmailRepository::class);
$this->corePermissionsMock = $this->createMock(CorePermissions::class);
$this->emailActionModel = new EmailActionModel(
$this->emailModelMock,
$this->emailRepositoryMock,
$this->corePermissionsMock
);
}
public function testSetsNewCategoryForEditableEmails(): void
{
$oldCategory = new Category();
$oldCategory->setTitle(self::OLD_CATEGORY_TITLE);
$newCategory = new Category();
$newCategory->setTitle(self::NEW_CATEGORY_TITLE);
$emails = $this->buildEmailsWithCategory($oldCategory, 3);
$this->configureRepositoryToReturn($emails);
$this->configurePermissionToAllowEdition(true);
$this->configureModelToSave($emails);
$this->tryToSetCategory($emails, $newCategory);
foreach ($emails as $email) {
$this->assertEquals($email->getCategory(), $newCategory);
}
}
public function testDoesntSetNewCategoryForNonEditableEmails(): void
{
$oldCategory = new Category();
$oldCategory->setTitle(self::OLD_CATEGORY_TITLE);
$newCategory = new Category();
$newCategory->setTitle(self::NEW_CATEGORY_TITLE);
$emails = $this->buildEmailsWithCategory($oldCategory, 5);
$this->configureRepositoryToReturn($emails);
$this->configurePermissionToAllowEdition(false);
$this->tryToSetCategory($emails, $newCategory);
foreach ($emails as $email) {
$this->assertEquals($email->getCategory(), $oldCategory);
}
}
/**
* @return array<Email>
*/
private function buildEmailsWithCategory(Category $category, int $quantity): array
{
$emails = [];
for ($i = 0; $i < $quantity; ++$i) {
$email = new Email();
$email->setId($i);
$email->setCategory($category);
$emails[] = $email;
}
return $emails;
}
private function configurePermissionToAllowEdition(bool $allow): void
{
$this->corePermissionsMock
->method('hasEntityAccess')
->willReturn($allow);
}
/**
* @param array<Email> $emails
*/
protected function configureRepositoryToReturn(array $emails): void
{
$this->emailRepositoryMock
->method('findBy')
->with(
['id' => array_map(fn (Email $email) => $email->getId(), $emails)]
)
->willReturn($emails);
}
/**
* @param array<Email> $emails
*/
protected function configureModelToSave(array $emails): void
{
$this->emailModelMock
->expects($this->once())
->method('saveEntities')
->with($emails);
}
/**
* @param array<Email> $emails
*/
protected function tryToSetCategory(array $emails, Category $newCategory): void
{
$this->emailActionModel
->setCategory(
array_map(fn (Email $email) => $email->getId(), $emails),
$newCategory
);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Model;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Model\EmailModel;
class EmailModelBuildUrlTest extends MauticMysqlTestCase
{
protected function setUp(): void
{
$this->configParams['site_url'] = 'https://foo.bar.com';
parent::setUp();
}
public function testSiteUrlAlwaysTakesPrecedenceWhenBuildingUrls(): void
{
/** @var EmailModel $emailModel */
$emailModel = static::getContainer()->get('mautic.email.model.email');
$idHash = uniqid();
$url = $emailModel->buildUrl('mautic_email_unsubscribe', ['idHash' => $idHash]);
self::assertSame('https://foo.bar.com/email/unsubscribe/'.$idHash, $url);
}
}

View File

@@ -0,0 +1,532 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Model;
use Doctrine\DBAL\Exception;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\OptimisticLockException;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\EmailBundle\Model\EmailModel;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\LeadBundle\Entity\ListLead;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\PageBundle\Entity\Hit;
use Mautic\PageBundle\Entity\Redirect;
use Mautic\PageBundle\Entity\Trackable;
use Symfony\Component\DependencyInjection\ContainerInterface;
class EmailModelFunctionalTest extends MauticMysqlTestCase
{
use CreateTestEntitiesTrait;
private EmailModel|ContainerInterface $emailModel;
protected function setUp(): void
{
parent::setUp();
$this->emailModel = static::getContainer()->get('mautic.email.model.email');
}
protected function beforeBeginTransaction(): void
{
$this->resetAutoincrement(['leads']);
}
public function testSendEmailToListsInThreads(): void
{
$contacts = $this->generateContacts(23);
$segment = $this->createSegment();
$this->addContactsToSegment($contacts, $segment);
$email = $this->createEmail($segment);
$emailModel = static::getContainer()->get('mautic.email.model.email');
\assert($emailModel instanceof EmailModel);
[$sentCount] = $this->emailModel->sendEmailToLists($email, [$segment], null, null, null, null, null, 3, 1);
$this->assertEquals($sentCount, 7);
[$sentCount] = $this->emailModel->sendEmailToLists($email, [$segment], null, null, null, null, null, 3, 2);
$this->assertEquals($sentCount, 8);
[$sentCount] = $this->emailModel->sendEmailToLists($email, [$segment], null, null, null, null, null, 3, 3);
$this->assertEquals($sentCount, 8);
}
public function testGetEmailGeneralStats(): void
{
$contacts = $this->generateContacts(12);
$segment = $this->createSegment();
$this->addContactsToSegment($contacts, $segment);
$email = $this->createEmail($segment);
// Send email to segment
[$sentCount] = $this->emailModel->sendEmailToLists($email, [$segment]);
// Emulate email reads
$statRepository = $this->em->getRepository(Stat::class);
$stats = $statRepository->findBy([
'email' => $email,
'lead' => $contacts,
]);
for ($index = 0; $index < $readCount = 4; ++$index) {
$this->emulateEmailRead($stats[$index]);
}
// Emulate clicks
$this->emulateClick($contacts[0], $email, 1, 1);
$this->emulateClick($contacts[1], $email, 1, 1);
// Emulate unsubscribing and bounces
$this->createDnc('email', $contacts[3], DoNotContact::UNSUBSCRIBED, $email->getId());
$this->createDnc('email', $contacts[4], DoNotContact::BOUNCED, $email->getId());
// Emulate failed email
$this->emulateEmailFailed($stats[5]);
$this->em->flush();
$dateFrom = new \DateTime('-7 days');
$dateTo = new \DateTime();
$unit = 'D';
$includeVariants = false;
$result = $this->emailModel->getEmailGeneralStats($email, $includeVariants, $unit, $dateFrom, $dateTo);
$this->assertIsArray($result);
$this->assertCount(6, $result['datasets']);
$this->assertEquals('Sent emails', $result['datasets'][0]['label']);
$this->assertEquals([0, 0, 0, 0, 0, 0, 0, $sentCount], $result['datasets'][0]['data']);
$this->assertEquals('Read emails', $result['datasets'][1]['label']);
$this->assertEquals([0, 0, 0, 0, 0, 0, 0, $readCount], $result['datasets'][1]['data']);
$this->assertEquals('Failed emails', $result['datasets'][2]['label']);
$this->assertEquals([0, 0, 0, 0, 0, 0, 0, 1], $result['datasets'][2]['data']);
$this->assertEquals('Unique Clicked', $result['datasets'][3]['label']);
$this->assertEquals([0, 0, 0, 0, 0, 0, 0, 2], $result['datasets'][3]['data']);
$this->assertEquals('Unsubscribed', $result['datasets'][4]['label']);
$this->assertEquals([0, 0, 0, 0, 0, 0, 0, 1], $result['datasets'][4]['data']);
$this->assertEquals('Bounced', $result['datasets'][5]['label']);
$this->assertEquals([0, 0, 0, 0, 0, 0, 0, 1], $result['datasets'][5]['data']);
}
/**
* @return Lead[]
*/
private function generateContacts(int $howMany): array
{
$contacts = [];
for ($i = 0; $i < $howMany; ++$i) {
$contact = new Lead();
$contact->setEmail("test{$i}@some.email");
$contacts[] = $contact;
}
$contactModel = static::getContainer()->get('mautic.lead.model.lead');
\assert($contactModel instanceof LeadModel);
$contactModel->saveEntities($contacts);
return $contacts;
}
private function createSegment(): LeadList
{
$segment = new LeadList();
$segment->setName('Segment A');
$segment->setPublicName('Segment A');
$segment->setAlias('segment-a');
$this->em->persist($segment);
$this->em->flush();
return $segment;
}
/**
* @param 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(LeadList $segment): Email
{
$email = new Email();
$email->setName('Email');
$email->setSubject('Email Subject');
$email->setCustomHtml('Email content');
$email->setEmailType('list');
$email->setPublishUp(new \DateTime('-1 day'));
$email->setContinueSending(true);
$email->setIsPublished(true);
$email->addList($segment);
$this->em->persist($email);
$this->em->flush();
return $email;
}
public function testSendEmailToLists(): void
{
$contacts = $this->generateContacts(10);
$segment = $this->createSegment();
$this->addContactsToSegment($contacts, $segment);
$email = $this->createEmail($segment);
[$sentCount, $failedCount, $failedRecipientsByList] = $this->emailModel->sendEmailToLists($email, [$segment], 4, 2);
$this->assertEquals($sentCount, 4);
[$sentCount, $failedCount, $failedRecipientsByList] = $this->emailModel->sendEmailToLists($email, [$segment], 3, 2);
$this->assertEquals($sentCount, 3);
[$sentCount, $failedCount, $failedRecipientsByList] = $this->emailModel->sendEmailToLists($email, [$segment], 2);
$this->assertEquals($sentCount, 2);
[$sentCount, $failedCount, $failedRecipientsByList] = $this->emailModel->sendEmailToLists($email, [$segment], 4);
$this->assertEquals($sentCount, 1);
$email = $this->createEmail($segment);
[$sentCount, $failedCount, $failedRecipientsByList] = $this->emailModel->sendEmailToLists($email, [$segment]);
$this->assertEquals($sentCount, 10);
$email = $this->createEmail($segment);
[$sentCount, $failedCount, $failedRecipientsByList] = $this->emailModel->sendEmailToLists($email, [$segment], null, 2);
$this->assertEquals($sentCount, 10);
}
public function testSendEmailToListsWithContinueSendingFalse(): void
{
$contacts = $this->generateContacts(5);
$segment = $this->createSegment();
$this->addContactsToSegment($contacts, $segment);
// Create email with continueSending = false
$email = new Email();
$email->setName('Email with Continue Sending False');
$email->setSubject('Email Subject');
$email->setCustomHtml('Email content');
$email->setEmailType('list');
$email->setPublishUp(new \DateTime('-1 day'));
$email->setContinueSending(false); // This should prevent sending
$email->setIsPublished(true);
$email->addList($segment);
$this->em->persist($email);
$this->em->flush();
// Attempt to send emails - should send 0 because continueSending is false
[$sentCount, $failedCount, $failedRecipientsByList] = $this->emailModel->sendEmailToLists($email, [$segment]);
$this->assertEquals(0, $sentCount, 'No emails should be sent when continueSending is false');
$this->assertEquals(0, $failedCount, 'No emails should fail when continueSending is false');
$this->assertEmpty($failedRecipientsByList, 'No failed recipients when continueSending is false');
}
public function testNotOverwriteChildrenTranslationEmailAfterSaveParent(): void
{
$segment = new LeadList();
$segmentName = 'Test_segment';
$segment->setName($segmentName);
$segment->setPublicName($segmentName);
$segment->setAlias($segmentName);
$this->em->persist($segment);
$emailName = 'Test';
$customHtmlParent = 'test EN';
$parentEmail = new Email();
$parentEmail->setName($emailName);
$parentEmail->setSubject($emailName);
$parentEmail->setCustomHTML($customHtmlParent);
$parentEmail->setEmailType('template');
$parentEmail->setLanguage('en');
$this->em->persist($parentEmail);
$customHtmlChildren = 'test FR';
$childrenEmail = clone $parentEmail;
$childrenEmail->setLanguage('fr');
$childrenEmail->setCustomHTML($customHtmlChildren);
$childrenEmail->setTranslationParent($parentEmail);
$this->em->persist($parentEmail);
$this->em->detach($segment);
$this->em->detach($parentEmail);
$this->em->detach($childrenEmail);
$parentEmail->setName('Test change');
$this->emailModel->saveEntity($parentEmail);
self::assertSame($customHtmlParent, $parentEmail->getCustomHtml());
self::assertSame($customHtmlChildren, $childrenEmail->getCustomHtml());
}
/**
* @throws OptimisticLockException
* @throws ORMException
*/
private function emulateEmailStat(Lead $lead, Email $email, bool $isRead): void
{
$stat = new Stat();
$stat->setEmailAddress('test@test.com');
$stat->setLead($lead);
$stat->setDateSent(new \DateTime('2023-07-22'));
$stat->setEmail($email);
$stat->setIsRead($isRead);
$this->em->persist($stat);
}
/**
* @throws OptimisticLockException
* @throws ORMException
*/
private function emulateClick(Lead $lead, Email $email, int $hits, int $uniqueHits): void
{
$ipAddress = new IpAddress();
$ipAddress->setIpAddress('127.0.0.1');
$this->em->persist($ipAddress);
$this->em->flush();
$redirect = new Redirect();
$redirect->setRedirectId(uniqid());
$redirect->setUrl('https://example.com');
$redirect->setHits($hits);
$redirect->setUniqueHits($uniqueHits);
$this->em->persist($redirect);
$trackable = new Trackable();
$trackable->setChannelId($email->getId());
$trackable->setChannel('email');
$trackable->setHits($hits);
$trackable->setUniqueHits($uniqueHits);
$trackable->setRedirect($redirect);
$this->em->persist($trackable);
$pageHit = new Hit();
$pageHit->setRedirect($redirect);
$pageHit->setIpAddress($ipAddress);
$pageHit->setEmail($email);
$pageHit->setLead($lead);
$pageHit->setDateHit(new \DateTime());
$pageHit->setCode(200);
$pageHit->setUrl($redirect->getUrl());
$pageHit->setTrackingId($redirect->getRedirectId());
$pageHit->setSource('email');
$pageHit->setSourceId($email->getId());
$this->em->persist($pageHit);
}
private function emulateEmailRead(Stat $emailStat): void
{
$emailStat->setIsRead(true);
$emailStat->setDateRead(new \DateTime());
$emailStat->setOpenCount(1);
$email = $emailStat->getEmail();
$email->setReadCount($email->getReadCount() + 1);
$this->em->persist($emailStat);
$this->em->persist($email);
}
private function emulateEmailFailed(Stat $emailStat): void
{
$emailStat->setIsFailed(true);
$this->em->persist($emailStat);
}
private function createDnc(string $channel, Lead $contact, int $reason, ?int $channelId = null): DoNotContact
{
$dnc = new DoNotContact();
$dnc->setChannel($channel);
$dnc->setLead($contact);
$dnc->setReason($reason);
$dnc->setDateAdded(new \DateTime());
if ($channelId) {
$dnc->setChannelId($channelId);
}
$this->em->persist($dnc);
return $dnc;
}
/**
* @throws ORMException
* @throws Exception
*/
public function testGetEmailCountryStatsSingleEmail(): void
{
/** @var EmailModel $emailModel */
$emailModel = $this->getContainer()->get('mautic.email.model.email');
$dateFrom = new \DateTimeImmutable('2023-07-21');
$dateTo = new \DateTimeImmutable('2023-07-24');
$leadsPayload = [
[
'email' => 'example1@test.com',
'country' => 'Italy',
'read' => true,
'click' => true,
],
[
'email' => 'example2@test.com',
'country' => 'Italy',
'read' => true,
'click' => false,
],
[
'email' => 'example3@test.com',
'country' => 'Italy',
'read' => false,
'click' => false,
],
[
'email' => 'example4@test.com',
'country' => '',
'read' => true,
'click' => true,
],
[
'email' => 'example5@test.com',
'country' => 'Poland',
'read' => true,
'click' => false,
],
[
'email' => 'example6@test.com',
'country' => 'Poland',
'read' => true,
'click' => true,
],
];
$email = new Email();
$email->setName('Test email');
$this->em->persist($email);
$this->em->flush();
foreach ($leadsPayload as $l) {
$lead = new Lead();
$lead->setEmail($l['email']);
$lead->setCountry($l['country']);
$this->em->persist($lead);
$this->emulateEmailStat($lead, $email, $l['read']);
if ($l['read'] && $l['click']) {
$hits = rand(1, 5);
$uniqueHits = rand(1, $hits);
$this->emulateClick($lead, $email, $hits, $uniqueHits);
}
}
$this->em->flush();
$results = $emailModel->getCountryStats($email, $dateFrom, $dateTo);
$this->assertCount(2, $results);
$this->assertSame([
'clicked_through_count' => [
[
'clicked_through_count' => '1',
'country' => '',
],
[
'clicked_through_count' => '1',
'country' => 'Italy',
],
[
'clicked_through_count' => '1',
'country' => 'Poland',
],
],
'read_count' => [
[
'read_count' => '1',
'country' => '',
],
[
'read_count' => '2',
'country' => 'Italy',
],
[
'read_count' => '2',
'country' => 'Poland',
],
],
], $results);
}
/**
* @throws OptimisticLockException
* @throws ORMException
*/
public function testGetContextEntity(): void
{
$email = new Email();
$email->setName('Test email');
$this->em->persist($email);
$this->em->flush();
$id = $email->getId();
$result = $this->emailModel->getEntity($id);
$this->assertSame($email, $result);
}
public function testReturnsContactAsIsIfNoId(): void
{
$contact = ['email' => 'test@example.com'];
$result = $this->emailModel->enrichedContactWithCompanies($contact);
$this->assertSame($contact, $result);
}
public function testReturnsContactAsIsIfCompaniesAlreadySet(): void
{
$contact = [
'id' => 1,
'companies' => ['company1'],
];
$result = $this->emailModel->enrichedContactWithCompanies($contact);
$this->assertSame($contact, $result);
}
public function testEnrichesContactWithCompanies(): void
{
$company = $this->createCompany('Mautic', 'hello@mautic.org');
$company->setCity('Pune');
$company->setCountry('India');
$this->em->persist($company);
$contact = $this->createLead('John', 'Doe', 'test@domain.tld');
$this->createPrimaryCompanyForLead($contact, $company);
$this->em->flush();
$contactArray = $contact->convertToArray();
$result = $this->emailModel->enrichedContactWithCompanies($contactArray);
$this->assertArrayHasKey('companies', $result);
$this->assertSame($company->getName(), $result['companies'][0]['companyname']);
$this->assertSame($company->getCity(), $result['companies'][0]['companycity']);
$this->assertSame($company->getCountry(), $result['companies'][0]['companycountry']);
}
public function testEnrichesContactWithEmptyCompaniesIfNoneFound(): void
{
$contact = $this->createLead('John', 'Doe', 'test@domain.tld');
$this->em->flush();
$contactArray = $contact->convertToArray();
$result = $this->emailModel->enrichedContactWithCompanies($contactArray);
$this->assertArrayHasKey('companies', $result);
$this->assertEmpty($result['companies']);
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Model;
use Doctrine\ORM\EntityManager;
use Mautic\EmailBundle\EmailEvents;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\EmailBundle\Entity\StatRepository;
use Mautic\EmailBundle\Model\EmailStatModel;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
final class EmailStatModelTest extends TestCase
{
public function testSave(): void
{
/** @var MockObject&EntityManager */
$entityManager = $this->createMock(EntityManager::class);
/** @var MockObject&StatRepository */
$statRepository = $this->createMock(StatRepository::class);
$entityManager->method('getRepository')->willReturn($statRepository);
$statRepository->expects($this->once())
->method('saveEntities')
->willReturnCallback(
function (array $entities) {
Assert::assertCount(1, $entities);
Assert::assertInstanceOf(StatTest::class, $entities[0]);
// Emulate database adding the entity some autoincrement ID.
$entities[0]->setId('123');
}
);
$dispatcher = new class extends EventDispatcher {
public int $dispatchMethodCounter = 0;
public function __construct()
{
parent::__construct();
}
public function dispatch(object $event, ?string $eventName = null): object
{
switch ($this->dispatchMethodCounter) {
case 0:
Assert::assertSame(EmailEvents::ON_EMAIL_STAT_PRE_SAVE, $eventName);
Assert::assertCount(1, $event->getStats());
Assert::assertNull($event->getStats()[0]->getId());
break;
case 1:
Assert::assertSame(EmailEvents::ON_EMAIL_STAT_POST_SAVE, $eventName);
Assert::assertCount(1, $event->getStats());
Assert::assertSame('123', $event->getStats()[0]->getId());
break;
}
++$this->dispatchMethodCounter;
return $event;
}
};
$emailStatModel = new EmailStatModel($entityManager, $dispatcher);
$emailStat = new StatTest();
$emailStatModel->saveEntity($emailStat);
Assert::assertSame(2, $dispatcher->dispatchMethodCounter);
}
}
class StatTest extends Stat
{
private ?string $id = null;
public function setId(string $id): void
{
$this->id = $id;
}
public function getId(): ?string
{
return $this->id;
}
}

View File

@@ -0,0 +1,760 @@
<?php
namespace Mautic\EmailBundle\Tests\Model;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\AssetBundle\Model\AssetModel;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\CoreBundle\Helper\ThemeHelper;
use Mautic\EmailBundle\Entity\CopyRepository;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\EmailBundle\Event\EmailSendEvent;
use Mautic\EmailBundle\Exception\FailedToSendToContactException;
use Mautic\EmailBundle\Helper\DTO\AddressDTO;
use Mautic\EmailBundle\Helper\FromEmailHelper;
use Mautic\EmailBundle\Helper\MailHashHelper;
use Mautic\EmailBundle\Helper\MailHelper;
use Mautic\EmailBundle\Model\EmailModel;
use Mautic\EmailBundle\Model\EmailStatModel;
use Mautic\EmailBundle\Model\SendEmailToContact;
use Mautic\EmailBundle\MonitoredEmail\Mailbox;
use Mautic\EmailBundle\Stat\StatHelper;
use Mautic\EmailBundle\Tests\Helper\Transport\BatchTransport;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\DoNotContact;
use Mautic\PageBundle\Model\RedirectModel;
use Mautic\PageBundle\Model\TrackableModel;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Routing\Router;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
class SendEmailToContactTest extends \PHPUnit\Framework\TestCase
{
/**
* @var array<array<string,int|string>>
*/
private array $contacts = [
[
'id' => 1,
'email' => 'contact1@somewhere.com',
'firstname' => 'Contact',
'lastname' => '1',
'owner_id' => 1,
],
[
'id' => 2,
'email' => 'contact2@somewhere.com',
'firstname' => 'Contact',
'lastname' => '2',
'owner_id' => 0,
],
[
'id' => 3,
'email' => 'contact3@somewhere.com',
'firstname' => 'Contact',
'lastname' => '3',
'owner_id' => 2,
],
[
'id' => 4,
'email' => 'contact4@somewhere.com',
'firstname' => 'Contact',
'lastname' => '4',
'owner_id' => 1,
],
];
/**
* @var MockObject&FromEmailHelper
*/
private $fromEmaiHelper;
/**
* @var MockObject&CoreParametersHelper
*/
private $coreParametersHelper;
/**
* @var MockObject&Mailbox
*/
private $mailbox;
/**
* @var MockObject&LoggerInterface
*/
private MockObject $loggerMock;
private MailHashHelper $mailHashHelper;
/**
* @var MockObject&TranslatorInterface
*/
private MockObject $translator;
/**
* @var MockObject|MailHelper
*/
private $mailHelper;
/**
* @var MockObject|DoNotContact
*/
private $dncModel;
/**
* @var MockObject|EmailStatModel
*/
private $emailStatModel;
/**
* @var MockObject&Environment
*/
private MockObject $twig;
private StatHelper $statHelper;
protected function setUp(): void
{
parent::setUp();
$this->dncModel = $this->createMock(DoNotContact::class);
$this->mailHelper = $this->createMock(MailHelper::class);
$this->emailStatModel = $this->createMock(EmailStatModel::class);
$this->statHelper = new StatHelper($this->emailStatModel);
$this->fromEmaiHelper = $this->createMock(FromEmailHelper::class);
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$this->mailbox = $this->createMock(Mailbox::class);
$this->loggerMock = $this->createMock(LoggerInterface::class);
$this->mailHashHelper = new MailHashHelper($this->coreParametersHelper);
$this->translator = $this->createMock(TranslatorInterface::class);
$this->twig = $this->createMock(Environment::class);
$this->fromEmaiHelper->method('getFrom')
->willReturn(new AddressDTO('someone@somewhere.com'));
}
#[\PHPUnit\Framework\Attributes\TestDox('Tests that all contacts are temporarily failed if an Email entity happens to be incorrectly configured')]
public function testContactsAreFailedIfSettingEmailEntityFails(): void
{
$this->mailHelper->method('setEmail')
->willReturn(false);
// This should not be called because contact emails are just fine; the problem is with the email entity
$this->dncModel->expects($this->never())
->method('addDncForContact');
$model = new SendEmailToContact($this->mailHelper, $this->statHelper, $this->dncModel, $this->translator);
$email = new Email();
$model->setEmail($email);
foreach ($this->contacts as $contact) {
try {
$model->setContact($contact)
->send();
} catch (FailedToSendToContactException) {
}
}
$model->finalFlush();
$failedContacts = $model->getFailedContacts();
$this->assertCount(4, $failedContacts);
}
#[\PHPUnit\Framework\Attributes\TestDox('Tests that bad emails are failed')]
public function testExceptionIsThrownIfEmailIsSentToBadContact(): void
{
$emailMock = $this->createMock(Email::class);
$emailMock
->expects($this->any())
->method('getId')
->willReturn(1);
$this->mailHelper->method('setEmail')
->willReturn(true);
$this->mailHelper->method('addTo')
->willReturnCallback(
fn ($email) => '@bad.com' !== $email
);
$this->mailHelper->method('queue')
->willReturn([true, []]);
$stat = new Stat();
$stat->setEmail($emailMock);
$this->mailHelper->method('createEmailStat')
->willReturn($stat);
$this->dncModel->expects($this->once())
->method('addDncForContact');
$model = new SendEmailToContact($this->mailHelper, $this->statHelper, $this->dncModel, $this->translator);
$model->setEmail($emailMock);
$contacts = $this->contacts;
$contacts[0]['email'] = '@bad.com';
$exceptionThrown = false;
foreach ($contacts as $contact) {
try {
$model->setContact($contact)
->send();
} catch (FailedToSendToContactException) {
$exceptionThrown = true;
}
}
if (!$exceptionThrown) {
$this->fail('FailedToSendToContactException not thrown');
}
$model->finalFlush();
$failedContacts = $model->getFailedContacts();
$this->assertCount(1, $failedContacts);
}
#[\PHPUnit\Framework\Attributes\TestDox('Test a tokenized transport that limits batches does not throw BatchQueueMaxException on subsequent contacts when one fails')]
public function testBadEmailDoesNotCauseBatchQueueMaxExceptionOnSubsequentContacts(): void
{
$emailMock = $this->createMock(Email::class);
$emailMock->method('getId')->willReturn(1);
$emailMock->method('getFromAddress')->willReturn('test@mautic.com');
$emailMock->method('getSubject')->willReturn('Subject');
$emailMock->method('getCustomHtml')->willReturn('content');
// Use our test token transport limiting to 1 recipient per queue
$transport = new BatchTransport(false, 1);
$mailer = new Mailer($transport);
$routerMock = $this->createMock(Router::class);
$requestStack = new RequestStack();
$this->fromEmaiHelper->method('getFromAddressConsideringOwner')
->willReturn(new AddressDTO('someone@somewhere.com'));
$this->coreParametersHelper->method('get')->willReturnCallback(
fn ($param) => match ($param) {
'mailer_from_email' => 'nobody@nowhere.com',
'secret_key' => 'secret',
default => '',
}
);
$themeHelper = $this->createMock(ThemeHelper::class);
$themeHelper->expects(self::never())
->method('checkForTwigTemplate');
$entityManager = $this->createMock(EntityManagerInterface::class);
$entityManager->expects($this->never()) // Never to make sure that the mock is properly tested if needed.
->method('getReference');
$mailHelper = $this->getMockBuilder(MailHelper::class)
->setConstructorArgs([
$mailer,
$this->fromEmaiHelper,
$this->coreParametersHelper,
$this->mailbox,
$this->loggerMock,
$this->mailHashHelper,
$routerMock,
$this->twig,
$themeHelper,
$this->createMock(PathsHelper::class),
$this->createMock(EventDispatcherInterface::class),
$requestStack,
$entityManager,
$this->createMock(ModelFactory::class),
$this->createMock(AssetModel::class),
$this->createMock(TrackableModel::class),
$this->createMock(RedirectModel::class),
])
->onlyMethods(['createEmailStat'])
->getMock();
$mailHelper->method('createEmailStat')
->willReturnCallback(
function () use ($emailMock) {
$stat = new Stat();
$stat->setEmail($emailMock);
$leadMock = $this->createMock(Lead::class);
$leadMock->method('getId')
->willReturn(1);
$stat->setLead($leadMock);
return $stat;
}
);
// Enable queueing
$mailHelper->enableQueue();
$this->dncModel->expects($this->once())
->method('addDncForContact');
$model = new SendEmailToContact($mailHelper, $this->statHelper, $this->dncModel, $this->translator);
$model->setEmail($emailMock);
$contacts = $this->contacts;
$contacts[0]['email'] = '@bad.com';
foreach ($contacts as $contact) {
try {
$model->setContact($contact)
->send();
} catch (FailedToSendToContactException) {
// We're good here
}
}
$model->finalFlush();
$failedContacts = $model->getFailedContacts();
$this->assertCount(1, $failedContacts);
// Our fake transport should have processed 3 metadatas
$this->assertCount(3, $transport->getMetadatas());
// We made it this far so all of the emails were processed despite a bad email in the batch
}
#[\PHPUnit\Framework\Attributes\TestDox('Test a tokenized transport that fills tokens correctly')]
public function testBatchQueueContactsHaveTokensHydrated(): void
{
$this->coreParametersHelper->method('get')->willReturnMap([['mailer_from_email', null, 'nobody@nowhere.com'], ['secret_key', null, 'secret']]);
$emailMock = $this->createMock(Email::class);
$emailMock->method('getId')->willReturn(1);
$emailMock->method('getFromAddress')->willReturn('test@mautic.com');
$emailMock->method('getSubject')->willReturn('Subject');
$emailMock->method('getCustomHtml')->willReturn('Hi {contactfield=firstname}');
// Use our test token transport limiting to 1 recipient per queue
$transport = new BatchTransport(false, 1);
$mailer = new Mailer($transport);
// Mock factory to remove when factory is completely gone.
$this->coreParametersHelper->method('get')
->willReturnCallback(
fn ($param) => match ($param) {
default => '',
}
);
$mockDispatcher = $this->createMock(EventDispatcher::class);
$mockDispatcher->method('dispatch')
->willReturnCallback(
function (EmailSendEvent $event, $eventName) {
$lead = $event->getLead();
$tokens = [];
foreach ($lead as $field => $value) {
$tokens["{contactfield=$field}"] = $value;
}
$tokens['{hash}'] = $event->getIdHash();
$event->addTokens($tokens);
return $event;
}
);
$copyRepoMock = $this->createMock(CopyRepository::class);
$emailModelMock = $this->createMock(EmailModel::class);
$emailModelMock->method('getCopyRepository')
->willReturn($copyRepoMock);
$modelFactory = $this->createMock(ModelFactory::class);
$modelFactory->method('getModel')
->with(EmailModel::class)
->willReturn($emailModelMock);
$this->fromEmaiHelper->method('getFromAddressConsideringOwner')
->willReturn(new AddressDTO('someone@somewhere.com'));
$themeHelper = $this->createMock(ThemeHelper::class);
$themeHelper->expects(self::never())
->method('checkForTwigTemplate');
$mailHelper = $this->getMockBuilder(MailHelper::class)
->setConstructorArgs([
$mailer,
$this->fromEmaiHelper,
$this->coreParametersHelper,
$this->mailbox,
$this->loggerMock,
$this->mailHashHelper,
$this->createMock(Router::class),
$this->twig,
$themeHelper,
$this->createMock(PathsHelper::class),
$mockDispatcher,
new RequestStack(),
$this->createMock(EntityManager::class),
$modelFactory,
$this->createMock(AssetModel::class),
$this->createMock(TrackableModel::class),
$this->createMock(RedirectModel::class),
])
->onlyMethods([])
->getMock();
// Enable queueing
$mailHelper->enableQueue();
$this->emailStatModel->method('saveEntity')
->willReturnCallback(
function (Stat $stat): void {
$tokens = $stat->getTokens();
$this->assertGreaterThan(1, count($tokens));
$this->assertEquals($stat->getTrackingHash(), $tokens['{hash}']);
}
);
$model = new SendEmailToContact($mailHelper, $this->statHelper, $this->dncModel, $this->translator);
$model->setEmail($emailMock);
foreach ($this->contacts as $contact) {
try {
$model->setContact($contact)
->send();
} catch (FailedToSendToContactException) {
// We're good here
}
}
$model->finalFlush();
$this->assertCount(4, $transport->getMetadatas());
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that stat entries are saved in batches of 20')]
public function testThatStatEntriesAreCreatedAndPersistedEveryBatch(): void
{
$this->coreParametersHelper->method('get')->willReturnMap([['mailer_from_email', null, 'nobody@nowhere.com'], ['secret_key', null, 'secret']]);
$emailMock = $this->createMock(Email::class);
$emailMock->method('getId')->willReturn(1);
$emailMock->method('getFromAddress')->willReturn('test@mautic.com');
$emailMock->method('getSubject')->willReturn('Subject');
$emailMock->method('getCustomHtml')->willReturn('content');
// Use our test token transport limiting to 1 recipient per queue
$transport = new BatchTransport(false, 1);
$mailer = new Mailer($transport);
$this->coreParametersHelper->method('get')
->willReturnCallback(
fn ($param) => match ($param) {
default => '',
}
);
$routerMock = $this->createMock(Router::class);
$this->fromEmaiHelper->method('getFromAddressConsideringOwner')
->willReturn(new AddressDTO('someone@somewhere.com'));
$themeHelper = $this->createMock(ThemeHelper::class);
$themeHelper->expects(self::never())
->method('checkForTwigTemplate');
$entityManager = $this->createMock(EntityManagerInterface::class);
$entityManager->expects($this->never()) // Never to make sure that the mock is properly tested if needed.
->method('getReference');
$requestStack = new RequestStack();
$mailHelper = $this->getMockBuilder(MailHelper::class)
->setConstructorArgs([
$mailer,
$this->fromEmaiHelper,
$this->coreParametersHelper,
$this->mailbox,
$this->loggerMock,
$this->mailHashHelper,
$routerMock,
$this->twig,
$themeHelper,
$this->createMock(PathsHelper::class),
$this->createMock(EventDispatcherInterface::class),
$requestStack,
$entityManager,
$this->createMock(ModelFactory::class),
$this->createMock(AssetModel::class),
$this->createMock(TrackableModel::class),
$this->createMock(RedirectModel::class),
])
->onlyMethods(['createEmailStat'])
->getMock();
$mailHelper->expects($this->exactly(21))
->method('createEmailStat')
->willReturnCallback(
function () use ($emailMock) {
$stat = new Stat();
$stat->setEmail($emailMock);
$leadMock = $this->createMock(Lead::class);
$leadMock->method('getId')
->willReturn(1);
$stat->setLead($leadMock);
return $stat;
}
);
// Enable queueing
$mailHelper->enableQueue();
// Here's the test; this should be called after 20 contacts are processed
$this->emailStatModel->expects($this->exactly(21))
->method('saveEntity');
$this->dncModel->expects($this->never())
->method('addDncForContact');
$model = new SendEmailToContact($mailHelper, $this->statHelper, $this->dncModel, $this->translator);
$model->setEmail($emailMock);
// Let's generate 20 bogus contacts
$contacts = [];
$counter = 0;
while ($counter <= 20) {
$contacts[] = [
'id' => $counter,
'email' => 'email'.uniqid().'@somewhere.com',
'firstname' => 'Contact',
'lastname' => uniqid(),
];
++$counter;
}
foreach ($contacts as $contact) {
try {
$model->setContact($contact)
->send();
} catch (FailedToSendToContactException $exception) {
$this->fail('FailedToSendToContactException thrown: '.$exception->getMessage());
}
}
$model->finalFlush();
$failedContacts = $model->getFailedContacts();
$this->assertCount(0, $failedContacts);
$this->assertCount(21, $transport->getMetadatas());
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a failed email from the transport is handled')]
public function testThatAFailureFromTransportIsHandled(): void
{
$this->coreParametersHelper->method('get')->willReturnMap([['mailer_from_email', null, 'nobody@nowhere.com'], ['secret_key', null, 'secret']]);
$emailMock = $this->createMock(Email::class);
$emailMock->method('getId')->willReturn(1);
$emailMock->method('getFromAddress')->willReturn('test@mautic.com');
$emailMock->method('getSubject')->willReturn(''); // The subject must be empty for the email to fail.
$emailMock->method('getCustomHtml')->willReturn('content');
// Use our test token transport limiting to 1 recipient per queue
$transport = new BatchTransport(true, 1);
$mailer = new Mailer($transport);
$this->coreParametersHelper->method('get')
->willReturnCallback(
fn ($param) => match ($param) {
default => '',
}
);
$this->fromEmaiHelper->method('getFromAddressConsideringOwner')->willReturn(new AddressDTO('someone@somewhere.com'));
$routerMock = $this->createMock(Router::class);
$twig = $this->createMock(Environment::class);
$themeHelper = $this->createMock(ThemeHelper::class);
$themeHelper->expects(self::never())
->method('checkForTwigTemplate');
$entityManager = $this->createMock(EntityManagerInterface::class);
$entityManager->expects($this->never()) // Never to make sure that the mock is properly tested if needed.
->method('getReference');
$requestStack = new RequestStack();
/** @var MockObject&MailHelper $mailHelper */
$mailHelper = $this->getMockBuilder(MailHelper::class)
->setConstructorArgs([
$mailer,
$this->fromEmaiHelper,
$this->coreParametersHelper,
$this->mailbox,
$this->loggerMock,
$this->mailHashHelper,
$routerMock,
$this->twig,
$themeHelper,
$this->createMock(PathsHelper::class),
$this->createMock(EventDispatcherInterface::class),
$requestStack,
$entityManager,
$this->createMock(ModelFactory::class),
$this->createMock(AssetModel::class),
$this->createMock(TrackableModel::class),
$this->createMock(RedirectModel::class),
])
->onlyMethods(['createEmailStat'])
->getMock();
$mailHelper->method('createEmailStat')
->willReturnCallback(
function () use ($emailMock) {
$stat = new Stat();
$stat->setEmail($emailMock);
$leadMock = $this->createMock(Lead::class);
$leadMock->method('getId')->willReturn(1);
$stat->setLead($leadMock);
return $stat;
}
);
// Enable queueing
$mailHelper->enableQueue();
$this->dncModel->expects($this->never())->method('addDncForContact');
$model = new SendEmailToContact($mailHelper, $this->statHelper, $this->dncModel, $this->translator);
$model->setEmail($emailMock);
foreach ($this->contacts as $contact) {
try {
$model->setContact($contact)->send();
} catch (FailedToSendToContactException) {
// We're good here
}
}
$model->finalFlush();
$failedContacts = $model->getFailedContacts();
$this->assertCount(1, $failedContacts);
$counts = $model->getSentCounts();
// Should have increased to 4, one failed via the transport so back down to 3
$this->assertEquals(3, $counts[1]);
// One error message from the transport
$errorMessages = $model->getErrors();
$this->assertCount(1, $errorMessages);
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that sending an email with invalid Bcc address is handled')]
public function testThatInvalidBccFailureIsHandled(): void
{
defined('MAUTIC_ENV') or define('MAUTIC_ENV', 'test');
/** @var MockObject&FromEmailHelper $fromEmailHelper */
$fromEmailHelper = $this->createMock(FromEmailHelper::class);
/** @var MockObject&CoreParametersHelper $coreParametersHelper */
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
/** @var MockObject&Mailbox $mailbox */
$mailbox = $this->createMock(Mailbox::class);
/** @var MockObject&LoggerInterface $logger */
$logger = $this->createMock(LoggerInterface::class);
/** @var MockObject&RouterInterface $router */
$router = $this->createMock(RouterInterface::class);
/** @var MockObject&Environment $twig */
$twig = $this->createMock(Environment::class);
$themeHelper = $this->createMock(ThemeHelper::class);
$themeHelper->expects(self::never())
->method('checkForTwigTemplate');
$coreParametersHelper->method('get')
->willReturnMap(
[
['mailer_from_email', null, 'nobody@nowhere.com'],
['mailer_from_name', null, 'No Body'],
['mailer_return_path', false, null],
]
);
$entityManager = $this->createMock(EntityManagerInterface::class);
$entityManager->expects($this->never()) // Never to make sure that the mock is properly tested if needed.
->method('getReference');
$requestStack = new RequestStack();
$mailer = new Mailer(new BatchTransport());
$mailHelper = new MailHelper(
$mailer,
$fromEmailHelper,
$coreParametersHelper,
$mailbox,
$logger,
$this->mailHashHelper,
$router,
$twig,
$themeHelper,
$this->createMock(PathsHelper::class),
$this->createMock(EventDispatcherInterface::class),
$requestStack,
$entityManager,
$this->createMock(ModelFactory::class),
$this->createMock(AssetModel::class),
$this->createMock(TrackableModel::class),
$this->createMock(RedirectModel::class),
);
$dncModel = $this->createMock(DoNotContact::class);
$translator = $this->createMock(TranslatorInterface::class);
$model = new SendEmailToContact($mailHelper, $this->statHelper, $dncModel, $translator);
$emailMock = $this->createMock(Email::class);
$emailMock->method('getId')->willReturn(1);
$emailMock->method('getSubject')->willReturn('subject');
$emailMock->method('getCustomHtml')->willReturn('content');
// Set invalid BCC (should use comma as separator)
$emailMock
->expects($this->any())
->method('getBccAddress')
->willReturn('test@mautic.com; test@mautic.com');
$model->setEmail($emailMock);
$stat = new Stat();
$stat->setEmail($emailMock);
$this->expectException(FailedToSendToContactException::class);
$this->expectExceptionMessage('Email "test@mautic.com; test@mautic.com" does not comply with addr-spec of RFC 2822.');
// Send should trigger the FailedToSendToContactException
$model->setContact($this->contacts[0])->send();
}
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Mautic\EmailBundle\Tests\Model;
use Mautic\CoreBundle\Event\TokenReplacementEvent;
use Mautic\CoreBundle\Exception\RecordNotPublishedException;
use Mautic\EmailBundle\EmailEvents;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Event\EmailSendEvent;
use Mautic\EmailBundle\Exception\EmailCouldNotBeSentException;
use Mautic\EmailBundle\Exception\InvalidEmailException;
use Mautic\EmailBundle\Helper\EmailValidator;
use Mautic\EmailBundle\Model\EmailModel;
use Mautic\EmailBundle\Model\SendEmailToUser;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Validator\CustomFieldValidator;
use Mautic\UserBundle\Entity\User;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class SendEmailToUserTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject|EmailModel
*/
private MockObject $emailModel;
/**
* @var MockObject|EventDispatcherInterface
*/
private MockObject $dispatcher;
/**
* @var MockObject|CustomFieldValidator
*/
private MockObject $customFieldValidator;
/**
* @var MockObject|EmailValidator
*/
private MockObject $emailValidator;
private SendEmailToUser $sendEmailToUser;
protected function setUp(): void
{
parent::setUp();
$this->emailModel = $this->createMock(EmailModel::class);
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
$this->customFieldValidator = $this->createMock(CustomFieldValidator::class);
$this->emailValidator = $this->createMock(EmailValidator::class);
$this->sendEmailToUser = new SendEmailToUser(
$this->emailModel,
$this->dispatcher,
$this->customFieldValidator,
$this->emailValidator
);
}
public function testEmailNotFound(): void
{
$lead = new Lead();
$this->emailModel->expects($this->once())
->method('getEntity')
->with(100)
->willReturn(null);
$config = [];
$config['useremail']['email'] = 100;
$this->expectException(EmailCouldNotBeSentException::class);
$this->sendEmailToUser->sendEmailToUsers($config, $lead);
}
public function testEmailNotPublished(): void
{
$lead = new Lead();
$email = new Email();
$email->setIsPublished(false);
$this->emailModel->expects($this->once())
->method('getEntity')
->with(100)
->willREturn($email);
$config = [];
$config['useremail']['email'] = 100;
$this->expectException(EmailCouldNotBeSentException::class);
$this->sendEmailToUser->sendEmailToUsers($config, $lead);
}
public function testSendEmailWithNoError(): void
{
$lead = new Lead();
$owner = new class extends User {
public function getId(): int
{
return 10;
}
};
$lead->setOwner($owner);
$email = new Email();
$email->setIsPublished(true);
$this->emailModel->expects($this->once())
->method('getEntity')
->with(33)
->willReturn($email);
$emailSendEvent = new class extends EmailSendEvent {
public int $getTokenMethodCallCounter = 0;
public function __construct()
{
}
/**
* @param bool $includeGlobal
*
* @return string[]
*/
public function getTokens($includeGlobal = true): array
{
++$this->getTokenMethodCallCounter;
return [];
}
};
// Global token for Email
$this->emailModel->expects($this->once())
->method('dispatchEmailSendEvent')
->willReturn($emailSendEvent);
// Different handling of tokens in the To, BC, BCC fields.
$matcher = $this->exactly(3);
// Different handling of tokens in the To, BC, BCC fields.
$this->customFieldValidator->expects($matcher)
->method('validateFieldType')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame('unpublished-field', $parameters[0]);
$this->assertSame('email', $parameters[1]);
throw new RecordNotPublishedException();
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame('unpublished-field', $parameters[0]);
$this->assertSame('email', $parameters[1]);
throw new RecordNotPublishedException();
}
if (3 === $matcher->numberOfInvocations()) {
$this->assertSame('active-field', $parameters[0]);
$this->assertSame('email', $parameters[1]);
return null;
}
});
// The event is dispatched only for valid tokens.
$this->dispatcher->expects($this->once())
->method('dispatch')
->with(
$this->callback(
function (TokenReplacementEvent $event) use ($lead) {
Assert::assertSame('{contactfield=active-field}', $event->getContent());
Assert::assertSame($lead, $event->getLead());
// Emulate a subscriber.
$event->setContent('replaced.token@email.address');
return true;
}
),
EmailEvents::ON_EMAIL_ADDRESS_TOKEN_REPLACEMENT,
);
$matcher = $this->exactly(4);
$this->emailValidator->expects($matcher)
->method('validate')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame('hello@there.com', $parameters[0]);
return null;
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame('bob@bobek.cz', $parameters[0]);
return null;
}
if (3 === $matcher->numberOfInvocations()) {
$this->assertSame('hidden@translation.in', $parameters[0]);
return null;
}
if (4 === $matcher->numberOfInvocations()) {
$this->assertSame('{invalid-token}', $parameters[0]);
return throw new InvalidEmailException('{invalid-token}');
}
});
// Send email method
$this->emailModel
->expects($this->once())
->method('sendEmailToUser')
->willReturnCallback(function ($email, $users, $leadCredentials, $tokens, $assetAttachments, $saveStat, $to, $cc, $bcc): void {
$expectedUsers = [
['id' => 6],
['id' => 7],
['id' => 10], // owner ID
];
$this->assertInstanceOf(Email::class, $email);
$this->assertEquals($expectedUsers, $users);
$this->assertFalse($saveStat);
$this->assertEquals(['hello@there.com', 'bob@bobek.cz', 'default@email.com'], $to);
$this->assertEquals([], $cc);
$this->assertEquals([0 => 'hidden@translation.in', 2 => 'replaced.token@email.address'], $bcc);
});
$config = [
'useremail' => [
'email' => 33,
],
'user_id' => [6, 7],
'to_owner' => true,
'to' => 'hello@there.com, bob@bobek.cz, {contactfield=unpublished-field|default@email.com}, {contactfield=unpublished-field}',
'bcc' => 'hidden@translation.in,{invalid-token}, {contactfield=active-field}',
];
$this->sendEmailToUser->sendEmailToUsers($config, $lead);
Assert::assertSame(1, $emailSendEvent->getTokenMethodCallCounter);
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Tests\Model;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\EmailBundle\Model\EmailStatModel;
use Mautic\EmailBundle\Model\TransportCallback;
use Mautic\EmailBundle\MonitoredEmail\Search\ContactFinder;
use Mautic\EmailBundle\MonitoredEmail\Search\Result;
use Mautic\LeadBundle\Entity\DoNotContact as DNC;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\DoNotContact;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
class TransportCallbackTest extends TestCase
{
public function testStatSave(): void
{
$dncModel = new class extends DoNotContact {
public function __construct()
{
}
public function addDncForContact($contactId, $channel, $reason = DNC::BOUNCED, $comments = '', $persist = true, $checkCurrentStatus = true, $allowUnsubscribeOverride = false)
{
Assert::assertSame('email', $channel);
Assert::assertSame(DNC::BOUNCED, $reason);
return true;
}
};
$contactFinder = new class extends ContactFinder {
public function __construct()
{
}
public function findByHash($hash): Result
{
Assert::assertSame('some-hash-id', $hash);
$result = new Result();
$contact = new Lead();
$stat = new Stat();
$result->addContact($contact);
$result->setStat($stat);
return $result;
}
};
$emailStatModel = new class extends EmailStatModel {
public function __construct()
{
}
public function saveEntity(Stat $stat): void
{
Assert::assertTrue($stat->isFailed());
Assert::assertArrayHasKey('bounces', $stat->getOpenDetails());
Assert::assertArrayHasKey(0, $stat->getOpenDetails()['bounces']);
Assert::assertArrayHasKey('datetime', $stat->getOpenDetails()['bounces'][0]);
Assert::assertArrayHasKey('reason', $stat->getOpenDetails()['bounces'][0]);
Assert::assertSame('some-comments', $stat->getOpenDetails()['bounces'][0]['reason']);
}
};
$transportCallback = new TransportCallback($dncModel, $contactFinder, $emailStatModel);
$transportCallback->addFailureByHashId('some-hash-id', 'some-comments');
}
}