Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PageBundle\Entity\Hit;
|
||||
use Mautic\PageBundle\Entity\Redirect;
|
||||
use Mautic\PageBundle\Entity\Trackable;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Mime\Email as EmailMime;
|
||||
|
||||
class AjaxControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testSendTestEmailAction(): void
|
||||
{
|
||||
/** @var CoreParametersHelper $parameters */
|
||||
$parameters = self::getContainer()->get('mautic.helper.core_parameters');
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/s/ajax?action=email:sendTestEmail');
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$this->assertQueuedEmailCount(0, message: 'Test emails should never be queued.');
|
||||
$this->assertEmailCount(1);
|
||||
|
||||
$email = KernelTestCase::getMailerMessage();
|
||||
\assert($email instanceof EmailMime);
|
||||
|
||||
/** @var UserHelper $userHelper */
|
||||
$userHelper = static::getContainer()->get(UserHelper::class);
|
||||
$user = $userHelper->getUser();
|
||||
|
||||
Assert::assertSame('Mautic test email', $email->getSubject());
|
||||
Assert::assertSame('Hi! This is a test email from Mautic. Testing...testing...1...2...3!', $email->getTextBody());
|
||||
Assert::assertCount(1, $email->getFrom());
|
||||
Assert::assertSame($parameters->get('mailer_from_name'), $email->getFrom()[0]->getName());
|
||||
Assert::assertSame($parameters->get('mailer_from_email'), $email->getFrom()[0]->getAddress());
|
||||
Assert::assertCount(1, $email->getTo());
|
||||
Assert::assertSame($user->getFirstName().' '.$user->getLastName(), $email->getTo()[0]->getName());
|
||||
Assert::assertSame($user->getEmail(), $email->getTo()[0]->getAddress());
|
||||
}
|
||||
|
||||
public function testGetDeliveredCount(): void
|
||||
{
|
||||
$contact1 = $this->createContact('john@example.com');
|
||||
$contact2 = $this->createContact('paul@example.com');
|
||||
|
||||
$this->em->flush();
|
||||
$email = $this->createEmailWithParams(
|
||||
'Email A',
|
||||
'Email A Subject',
|
||||
'list',
|
||||
'beefree-empty',
|
||||
'Test html'
|
||||
);
|
||||
$this->em->flush();
|
||||
|
||||
$this->createEmailStat($contact1, $email);
|
||||
$this->createEmailStat($contact2, $email);
|
||||
$email->setSentCount(2);
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
$this->createDoNotContact($contact2, $email, DoNotContact::BOUNCED);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->xmlHttpRequest(Request::METHOD_GET, "/s/ajax?action=email:getEmailDeliveredCount&id={$email->getId()}");
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertTrue($response->isOk());
|
||||
$this->assertSame([
|
||||
'success' => 1,
|
||||
'delivered' => 1,
|
||||
], json_decode($response->getContent(), true));
|
||||
}
|
||||
|
||||
public function testGetDeliveredCountWithTranslations(): void
|
||||
{
|
||||
$contactEn1 = $this->createContact('john@example.com');
|
||||
$contactEn2 = $this->createContact('paul@example.com');
|
||||
$contactPl1 = $this->createContact('szczepan@example.com');
|
||||
$contactPl2 = $this->createContact('jadwiga@example.com');
|
||||
$this->em->flush();
|
||||
|
||||
$emailEn = $this->createEmailWithParams(
|
||||
'Email EN',
|
||||
'Email EN Subject',
|
||||
'list',
|
||||
'beefree-empty',
|
||||
'Test html EN'
|
||||
);
|
||||
$emailEn->setLanguage('en');
|
||||
$this->em->flush();
|
||||
|
||||
$emailPl = $this->createEmailWithParams(
|
||||
'Email PL',
|
||||
'Email PL Subject',
|
||||
'list',
|
||||
'beefree-empty',
|
||||
'Test html PL'
|
||||
);
|
||||
$emailEn->setLanguage('pl_PL');
|
||||
$this->em->persist($emailPl);
|
||||
$this->em->flush();
|
||||
|
||||
$emailPl->setTranslationParent($emailEn);
|
||||
$emailEn->addTranslationChild($emailPl);
|
||||
$this->createEmailStat($contactEn1, $emailEn);
|
||||
$this->createEmailStat($contactEn2, $emailEn);
|
||||
$this->createEmailStat($contactPl1, $emailPl);
|
||||
$this->createEmailStat($contactPl2, $emailPl);
|
||||
$emailEn->setSentCount(2);
|
||||
$emailPl->setSentCount(2);
|
||||
$this->em->persist($emailEn);
|
||||
$this->em->persist($emailPl);
|
||||
$this->em->flush();
|
||||
|
||||
$this->createDoNotContact($contactEn1, $emailEn, DoNotContact::BOUNCED);
|
||||
$this->createDoNotContact($contactPl1, $emailPl, DoNotContact::BOUNCED);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->client->xmlHttpRequest(Request::METHOD_GET, "/s/ajax?action=email:getEmailDeliveredCount&id={$emailEn->getId()}");
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertTrue($response->isOk());
|
||||
$this->assertSame([
|
||||
'success' => 1,
|
||||
'delivered' => 1,
|
||||
], json_decode($response->getContent(), true));
|
||||
}
|
||||
|
||||
public function testHeatmapAction(): void
|
||||
{
|
||||
$contacts = [
|
||||
$this->createContact('john@example.com'),
|
||||
$this->createContact('paul@example.com'),
|
||||
];
|
||||
|
||||
$this->em->flush();
|
||||
$email = $this->createEmailWithParams(
|
||||
'Email A',
|
||||
'Email A Subject',
|
||||
'list',
|
||||
'beefree-empty',
|
||||
'Test html'
|
||||
);
|
||||
$this->em->flush();
|
||||
|
||||
$this->createEmailStat($contacts[0], $email);
|
||||
$this->createEmailStat($contacts[1], $email);
|
||||
$email->setSentCount(2);
|
||||
$this->em->flush();
|
||||
$this->em->persist($email);
|
||||
|
||||
$trackables = [
|
||||
$this->createTrackable('https://example.com/1', $email->getId()),
|
||||
$this->createTrackable('https://example.com/2', $email->getId()),
|
||||
];
|
||||
$this->em->flush();
|
||||
|
||||
$this->emulateLinkClick($email, $trackables[0], $contacts[0], 3);
|
||||
$this->emulateLinkClick($email, $trackables[1], $contacts[0]);
|
||||
$this->emulateLinkClick($email, $trackables[1], $contacts[1]);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->xmlHttpRequest(Request::METHOD_GET, "/s/ajax?action=email:heatmap&id={$email->getId()}");
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertTrue($response->isOk());
|
||||
$content = json_decode($response->getContent(), true);
|
||||
$this->assertSame('Test html', $content['content']);
|
||||
$this->assertSame([
|
||||
[
|
||||
'redirect_id' => $trackables[0]->getRedirect()->getRedirectId(),
|
||||
'url' => 'https://example.com/1',
|
||||
'id' => (string) $trackables[0]->getRedirect()->getId(),
|
||||
'hits' => '3',
|
||||
'unique_hits' => '1',
|
||||
'unique_hits_rate' => 0.3333,
|
||||
'unique_hits_text' => '1 click',
|
||||
'hits_rate' => 0.6,
|
||||
'hits_text' => '3 clicks',
|
||||
],
|
||||
[
|
||||
'redirect_id' => $trackables[1]->getRedirect()->getRedirectId(),
|
||||
'url' => 'https://example.com/2',
|
||||
'id' => (string) $trackables[1]->getRedirect()->getId(),
|
||||
'hits' => '2',
|
||||
'unique_hits' => '2',
|
||||
'unique_hits_rate' => 0.6667,
|
||||
'unique_hits_text' => '2 clicks',
|
||||
'hits_rate' => 0.4,
|
||||
'hits_text' => '2 clicks',
|
||||
],
|
||||
], $content['clickStats']);
|
||||
$this->assertSame(3, $content['totalUniqueClicks']);
|
||||
$this->assertSame(5, $content['totalClicks']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test email lookup with name with special chars.
|
||||
*/
|
||||
public function testEmailGetLookupChoiceListAction(): void
|
||||
{
|
||||
$emailName = 'It\'s an email';
|
||||
$email = new Email();
|
||||
$email->setName($emailName);
|
||||
$email->setSubject('Email Subject');
|
||||
$email->setEmailType('template');
|
||||
$this->em->persist($email);
|
||||
$this->em->flush($email);
|
||||
|
||||
$payload = [
|
||||
'action' => 'email:getLookupChoiceList',
|
||||
'email_type' => 'template',
|
||||
'top_level' => 'variant',
|
||||
'searchKey' => 'email',
|
||||
'email' => $emailName,
|
||||
];
|
||||
|
||||
$this->client->xmlHttpRequest(Request::METHOD_GET, '/s/ajax', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode());
|
||||
$this->assertNotEmpty($response);
|
||||
$this->assertEquals($emailName, $response[0]['items'][$email->getId()]);
|
||||
}
|
||||
|
||||
private function createContact(string $email): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setEmail($email);
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createEmailStat(Lead $contact, Email $email): Stat
|
||||
{
|
||||
$emailStat = new Stat();
|
||||
$emailStat->setEmail($email);
|
||||
$emailStat->setLead($contact);
|
||||
$emailStat->setEmailAddress($contact->getEmail());
|
||||
$emailStat->setDateSent(new \DateTime());
|
||||
$this->em->persist($emailStat);
|
||||
|
||||
return $emailStat;
|
||||
}
|
||||
|
||||
private function createDoNotContact(Lead $contact, Email $email, int $reason): DoNotContact
|
||||
{
|
||||
$dnc = new DoNotContact();
|
||||
$dnc->setLead($contact);
|
||||
$dnc->setChannel('email');
|
||||
$dnc->setChannelId($email->getId());
|
||||
$dnc->setDateAdded(new \DateTime());
|
||||
$dnc->setReason($reason);
|
||||
$dnc->setComments('Test DNC');
|
||||
$this->em->persist($dnc);
|
||||
|
||||
return $dnc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $segments
|
||||
*
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
*/
|
||||
private function createEmailWithParams(string $name, string $subject, string $emailType, string $template, string $customHtml, array $segments = []): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName($name);
|
||||
$email->setSubject($subject);
|
||||
$email->setEmailType($emailType);
|
||||
$email->setTemplate($template);
|
||||
$email->setCustomHtml($customHtml);
|
||||
$email->setLists($segments);
|
||||
$this->em->persist($email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
private function createTrackable(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;
|
||||
}
|
||||
|
||||
private function emulateLinkClick(Email $email, Trackable $trackable, Lead $lead, 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());
|
||||
$pageHit->setCode(200);
|
||||
$pageHit->setUrl($redirect->getUrl());
|
||||
$pageHit->setTrackingId($redirect->getRedirectId());
|
||||
$pageHit->setSource('email');
|
||||
$pageHit->setSourceId($email->getId());
|
||||
$this->em->persist($pageHit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\Controller\AjaxController;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
|
||||
class AjaxControllerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|Session
|
||||
*/
|
||||
private MockObject $sessionMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|ModelFactory<EmailModel>
|
||||
*/
|
||||
private MockObject $modelFactoryMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|Container
|
||||
*/
|
||||
private MockObject $containerMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|EmailModel
|
||||
*/
|
||||
private MockObject $modelMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|Email
|
||||
*/
|
||||
private MockObject $emailMock;
|
||||
|
||||
private AjaxController $controller;
|
||||
|
||||
/**
|
||||
* @var MockObject&ManagerRegistry
|
||||
*/
|
||||
private MockObject $managerRegistry;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->sessionMock = $this->createMock(Session::class);
|
||||
$this->containerMock = $this->createMock(Container::class);
|
||||
$this->modelMock = $this->createMock(EmailModel::class);
|
||||
$this->emailMock = $this->createMock(Email::class);
|
||||
|
||||
$this->managerRegistry = $this->createMock(ManagerRegistry::class);
|
||||
$this->modelFactoryMock = $this->createMock(ModelFactory::class);
|
||||
$userHelper = $this->createMock(UserHelper::class);
|
||||
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$translator = $this->createMock(Translator::class);
|
||||
$flashBag = $this->createMock(FlashBag::class);
|
||||
$requestStack = new RequestStack();
|
||||
$security = $this->createMock(CorePermissions::class);
|
||||
|
||||
$this->controller = new AjaxController(
|
||||
$this->managerRegistry,
|
||||
$this->modelFactoryMock,
|
||||
$userHelper,
|
||||
$coreParametersHelper,
|
||||
$dispatcher,
|
||||
$translator,
|
||||
$flashBag,
|
||||
$requestStack,
|
||||
$security
|
||||
);
|
||||
$this->controller->setContainer($this->containerMock);
|
||||
|
||||
$parameterBag = $this->createMock(ContainerBagInterface::class);
|
||||
$parameterBag->expects(self::once())
|
||||
->method('get')
|
||||
->with('kernel.environment')
|
||||
->willReturn('test');
|
||||
$this->containerMock->expects(self::once())
|
||||
->method('has')
|
||||
->with('parameter_bag')
|
||||
->willReturn(true);
|
||||
$this->containerMock->expects(self::once())
|
||||
->method('get')
|
||||
->with('parameter_bag')
|
||||
->willReturn($parameterBag);
|
||||
}
|
||||
|
||||
public function testSendBatchActionWhenNoIdProvided(): void
|
||||
{
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('getModel')
|
||||
->with('email')
|
||||
->willReturn($this->modelMock);
|
||||
|
||||
$response = $this->controller->sendBatchAction(new Request([], []));
|
||||
|
||||
$this->assertEquals('{"success":0}', $response->getContent());
|
||||
}
|
||||
|
||||
public function testSendBatchActionWhenIdProvidedButEmailNotPublished(): void
|
||||
{
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('getModel')
|
||||
->with('email')
|
||||
->willReturn($this->modelMock);
|
||||
|
||||
$this->modelMock->expects($this->once())
|
||||
->method('getEntity')
|
||||
->with(5)
|
||||
->willReturn($this->emailMock);
|
||||
|
||||
$this->modelMock->expects($this->never())
|
||||
->method('sendEmailToLists');
|
||||
$matcher = $this->exactly(3);
|
||||
|
||||
$this->sessionMock->expects($matcher)
|
||||
->method('get')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('mautic.email.send.progress', $parameters[0]);
|
||||
|
||||
return [0, 100];
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('mautic.email.send.stats', $parameters[0]);
|
||||
|
||||
return ['sent' => 0, 'failed' => 0, 'failedRecipients' => []];
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('mautic.email.send.active', $parameters[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$this->emailMock->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(false);
|
||||
|
||||
$request = new Request([], ['id' => 5, 'pending' => 100]);
|
||||
$request->setSession($this->sessionMock);
|
||||
$response = $this->controller->sendBatchAction($request);
|
||||
$expected = '{"success":1,"percent":0,"progress":[0,100],"stats":{"sent":0,"failed":0,"failedRecipients":[]}}';
|
||||
$this->assertEquals($expected, $response->getContent());
|
||||
}
|
||||
|
||||
public function testSendBatchActionWhenIdProvidedAndEmailIsPublished(): void
|
||||
{
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('getModel')
|
||||
->with('email')
|
||||
->willReturn($this->modelMock);
|
||||
|
||||
$this->modelMock->expects($this->once())
|
||||
->method('getEntity')
|
||||
->with(5)
|
||||
->willReturn($this->emailMock);
|
||||
|
||||
$this->modelMock->expects($this->once())
|
||||
->method('sendEmailToLists')
|
||||
->with($this->emailMock, null, 50)
|
||||
->willReturn([50, 0, []]);
|
||||
$matcher = $this->exactly(3);
|
||||
|
||||
$this->sessionMock->expects($matcher)
|
||||
->method('get')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('mautic.email.send.progress', $parameters[0]);
|
||||
|
||||
return [0, 100];
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('mautic.email.send.stats', $parameters[0]);
|
||||
|
||||
return ['sent' => 0, 'failed' => 0, 'failedRecipients' => []];
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('mautic.email.send.active', $parameters[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$this->emailMock->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(true);
|
||||
|
||||
$request = new Request([], ['id' => 5, 'pending' => 100, 'batchlimit' => 50]);
|
||||
$request->setSession($this->sessionMock);
|
||||
$response = $this->controller->sendBatchAction($request);
|
||||
$expected = '{"success":1,"percent":50,"progress":[50,100],"stats":{"sent":50,"failed":0,"failedRecipients":[]}}';
|
||||
$this->assertEquals($expected, $response->getContent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,828 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller\Api;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\EmailBundle\Entity\StatRepository;
|
||||
use Mautic\EmailBundle\Tests\Helper\Transport\SmtpTransport;
|
||||
use Mautic\LeadBundle\DataFixtures\ORM\LoadCategoryData;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
class EmailApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private SmtpTransport $transport;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['mailer_from_name'] = 'Mautic Admin';
|
||||
$this->configParams['default_signature_text'] = 'Best regards, |FROM_NAME|';
|
||||
parent::setUp();
|
||||
$this->loadFixtures([LoadCategoryData::class]);
|
||||
$this->setUpMailer();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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', []);
|
||||
}
|
||||
|
||||
protected function beforeBeginTransaction(): void
|
||||
{
|
||||
$this->resetAutoincrement(['categories', 'emails']);
|
||||
}
|
||||
|
||||
public function testCreateWithDynamicContent(): void
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName('API segment');
|
||||
$segment->setPublicName('API segment');
|
||||
$segment->setAlias('API segment');
|
||||
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
$payload = [
|
||||
'name' => 'test',
|
||||
'subject' => 'API test email',
|
||||
'customHtml' => '<h1>Hi there!</h1>',
|
||||
'emailType' => 'list',
|
||||
'lists' => [$segment->getId()],
|
||||
'dynamicContent' => [
|
||||
[
|
||||
'tokenName' => 'test content name',
|
||||
'content' => 'Some default <strong>content</strong>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => 'Variation 1',
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'content' => 'Variation 2',
|
||||
'filters' => [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'filter' => null, // Doesn't matter what value is here, it will be null-ed in the response for the emtpy param since PR 13526.
|
||||
'display' => null,
|
||||
'operator' => '!empty',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'tokenName' => 'test content name2',
|
||||
'content' => 'Some default <strong>content2</strong>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => 'Variation 3',
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'content' => 'Variation 4',
|
||||
'filters' => [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'filter' => 'Raleigh',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/api/emails/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
Assert::assertArrayHasKey('email', $response);
|
||||
|
||||
$response = $response['email'];
|
||||
|
||||
Assert::assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
Assert::assertSame($payload['name'], $response['name']);
|
||||
Assert::assertSame($payload['subject'], $response['subject']);
|
||||
Assert::assertSame($payload['customHtml'], $response['customHtml']);
|
||||
Assert::assertSame($payload['lists'][0], $response['lists'][0]['id']);
|
||||
Assert::assertSame('API segment', $response['lists'][0]['name']);
|
||||
Assert::assertSame($payload['dynamicContent'], $response['dynamicContent']);
|
||||
}
|
||||
|
||||
public function testSingleEmailWorkflow(): void
|
||||
{
|
||||
// Create a couple of segments first:
|
||||
$payload = [
|
||||
[
|
||||
'name' => 'API segment A',
|
||||
'description' => 'Segment created via API test',
|
||||
],
|
||||
[
|
||||
'name' => 'API segment B',
|
||||
'description' => 'Segment created via API test',
|
||||
],
|
||||
];
|
||||
|
||||
$this->client->request('POST', '/api/segments/batch/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$segmentResponse = json_decode($clientResponse->getContent(), true);
|
||||
$segmentAId = $segmentResponse['lists'][0]['id'];
|
||||
$segmentBId = $segmentResponse['lists'][1]['id'];
|
||||
|
||||
$this->assertSame(201, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertGreaterThan(0, $segmentAId);
|
||||
|
||||
// Create email with the new segment:
|
||||
$payload = [
|
||||
'name' => 'API email',
|
||||
'subject' => 'Email created via API test',
|
||||
'emailType' => 'list',
|
||||
'lists' => [$segmentAId],
|
||||
'customHtml' => '<h1>Email content created by an API test</h1>',
|
||||
];
|
||||
|
||||
$this->client->request('POST', '/api/emails/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$emailId = $response['email']['id'];
|
||||
|
||||
$this->assertSame(201, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertGreaterThan(0, $emailId);
|
||||
$this->assertEquals($payload['name'], $response['email']['name']);
|
||||
$this->assertEquals($payload['subject'], $response['email']['subject']);
|
||||
$this->assertEquals($payload['emailType'], $response['email']['emailType']);
|
||||
$this->assertCount(1, $response['email']['lists']);
|
||||
$this->assertEquals($segmentAId, $response['email']['lists'][0]['id']);
|
||||
$this->assertEquals($payload['customHtml'], $response['email']['customHtml']);
|
||||
$this->assertFalse($response['email']['publicPreview']);
|
||||
|
||||
// Edit PATCH:
|
||||
$patchPayload = [
|
||||
'name' => 'API email renamed',
|
||||
'lists' => [$segmentBId],
|
||||
'publicPreview' => true,
|
||||
];
|
||||
$this->client->request('PATCH', "/api/emails/{$emailId}/edit", $patchPayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertSame($emailId, $response['email']['id']);
|
||||
$this->assertEquals('API email renamed', $response['email']['name']);
|
||||
$this->assertEquals($payload['subject'], $response['email']['subject']);
|
||||
$this->assertCount(1, $response['email']['lists']);
|
||||
$this->assertEquals($segmentBId, $response['email']['lists'][0]['id']);
|
||||
$this->assertEquals($payload['emailType'], $response['email']['emailType']);
|
||||
$this->assertEquals($payload['customHtml'], $response['email']['customHtml']);
|
||||
$this->assertEquals($patchPayload['publicPreview'], $response['email']['publicPreview']);
|
||||
|
||||
// Edit PUT:
|
||||
$payload['subject'] .= ' renamed';
|
||||
$payload['lists'] = [$segmentAId, $segmentBId];
|
||||
$payload['language'] = 'en'; // Must be present for PUT as all empty values are being cleared.
|
||||
$payload['publicPreview'] = false; // Must be present for PUT as all empty values are being cleared.
|
||||
$this->client->request('PUT', "/api/emails/{$emailId}/edit", $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertSame($emailId, $response['email']['id']);
|
||||
$this->assertEquals($payload['name'], $response['email']['name']);
|
||||
$this->assertEquals('Email created via API test renamed', $response['email']['subject']);
|
||||
$this->assertCount(2, $response['email']['lists']);
|
||||
$this->assertEquals($segmentAId, $response['email']['lists'][1]['id']);
|
||||
$this->assertEquals($segmentBId, $response['email']['lists'][0]['id']);
|
||||
$this->assertEquals($payload['emailType'], $response['email']['emailType']);
|
||||
$this->assertEquals($payload['customHtml'], $response['email']['customHtml']);
|
||||
$this->assertEquals($payload['publicPreview'], $response['email']['publicPreview']);
|
||||
|
||||
// Get:
|
||||
$this->client->request('GET', "/api/emails/{$emailId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertSame($emailId, $response['email']['id']);
|
||||
$this->assertEquals($payload['name'], $response['email']['name']);
|
||||
$this->assertEquals($payload['subject'], $response['email']['subject']);
|
||||
$this->assertCount(2, $response['email']['lists']);
|
||||
$this->assertEquals($payload['emailType'], $response['email']['emailType']);
|
||||
$this->assertEquals($payload['customHtml'], $response['email']['customHtml']);
|
||||
|
||||
// Delete:
|
||||
$this->client->request('DELETE', "/api/emails/{$emailId}/delete");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertNull($response['email']['id']);
|
||||
$this->assertEquals($payload['name'], $response['email']['name']);
|
||||
$this->assertEquals($payload['subject'], $response['email']['subject']);
|
||||
$this->assertCount(2, $response['email']['lists']);
|
||||
$this->assertEquals($payload['emailType'], $response['email']['emailType']);
|
||||
$this->assertEquals($payload['customHtml'], $response['email']['customHtml']);
|
||||
|
||||
// Get (ensure it's deleted):
|
||||
$this->client->request('GET', "/api/emails/{$emailId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(404, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertSame(404, $response['errors'][0]['code']);
|
||||
|
||||
// Delete also testing segments:
|
||||
$this->client->request('DELETE', "/api/segments/batch/delete?ids={$segmentAId},{$segmentBId}", []);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Response should include the two entities that we just deleted
|
||||
$this->assertSame(2, count($response['lists']));
|
||||
$this->assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
}
|
||||
|
||||
public function testReplyActionIfNotFound(): void
|
||||
{
|
||||
$trackingHash = 'tracking_hash_123';
|
||||
|
||||
// Create new email reply.
|
||||
$this->client->request('POST', "/api/emails/reply/{$trackingHash}");
|
||||
$response = $this->client->getResponse();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
|
||||
$this->assertSame(Response::HTTP_NOT_FOUND, $response->getStatusCode());
|
||||
$this->assertSame('Email Stat with tracking hash tracking_hash_123 was not found', $responseData['errors'][0]['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider publishNewPermissionProvider
|
||||
*
|
||||
* @param string[] $permissions
|
||||
*/
|
||||
public function testCreateEmailWithoutPublishPermissionWillBeIgnored(array $permissions, ?bool $expectedIsPublished, ?string $expectedPublishUp, ?string $expectedPublishDown): void
|
||||
{
|
||||
$user = $this->getUser('sales');
|
||||
Assert::assertNotNull($user);
|
||||
|
||||
$this->setPermission($user->getRole(), ['email:emails' => $permissions]);
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
|
||||
$payload = [
|
||||
'name' => 'API email',
|
||||
'subject' => 'Email created via API test',
|
||||
'customHtml' => '<h1>Email content created by an API test</h1>',
|
||||
'isPublished' => true,
|
||||
'publishUp' => '2024-11-21 15:45',
|
||||
'publishDown' => '2024-12-21 15:45',
|
||||
];
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/api/emails/new', $payload);
|
||||
|
||||
Assert::assertSame(
|
||||
Response::HTTP_CREATED,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
|
||||
$createdEmail = json_decode($this->client->getResponse()->getContent(), true)['email'];
|
||||
Assert::assertSame($expectedIsPublished, $createdEmail['isPublished']);
|
||||
Assert::assertSame($expectedPublishUp, $createdEmail['publishUp']);
|
||||
Assert::assertSame($expectedPublishDown, $createdEmail['publishDown']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, mixed[]>
|
||||
*/
|
||||
public static function publishNewPermissionProvider(): iterable
|
||||
{
|
||||
yield 'User without the publish permission cannot publish' => [
|
||||
'permissions' => ['create'],
|
||||
'expectedIsPublished' => false,
|
||||
'expectedPublishUp' => null,
|
||||
'expectedPublishDown' => null,
|
||||
];
|
||||
yield 'User with the publish permission can publish other ownly' => [
|
||||
'permissions' => ['create', 'publishother'],
|
||||
'expectedIsPublished' => false,
|
||||
'expectedPublishUp' => null,
|
||||
'expectedPublishDown' => null,
|
||||
];
|
||||
yield 'User with the publish permission can publish own only' => [
|
||||
'permissions' => ['create', 'publishown'],
|
||||
'expectedIsPublished' => true,
|
||||
'expectedPublishUp' => '2024-11-21T15:45:00+00:00',
|
||||
'expectedPublishDown' => '2024-12-21T15:45:00+00:00',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider publishExistingPermissionProvider
|
||||
*
|
||||
* @param string[] $permissions
|
||||
*/
|
||||
public function testEditEmailWithoutPublishPermissionWillBeIgnored(string $creatorUsername, array $permissions, ?bool $expectedIsPublished, ?string $expectedPublishUp, ?string $expectedPublishDown): void
|
||||
{
|
||||
$owner = $this->getUser($creatorUsername);
|
||||
$email = $this->createEmail('Email C', 'Email C Subject', 'template', 'empty', 'Test html');
|
||||
$email->setIsPublished(false);
|
||||
$email->setCreatedBy($owner->getId());
|
||||
$this->em->flush();
|
||||
|
||||
$emailId = $email->getId();
|
||||
|
||||
$user = $this->getUser('sales');
|
||||
$this->setPermission($user->getRole(), ['email:emails' => $permissions]);
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
|
||||
$payload = [
|
||||
'isPublished' => true,
|
||||
'publishUp' => '2024-11-21 15:45',
|
||||
'publishDown' => '2024-12-21 15:45',
|
||||
];
|
||||
|
||||
$this->client->request(Request::METHOD_PATCH, "/api/emails/{$emailId}/edit", $payload);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$editedEmail = json_decode($this->client->getResponse()->getContent(), true)['email'];
|
||||
Assert::assertSame($expectedIsPublished, $editedEmail['isPublished']);
|
||||
Assert::assertSame($expectedPublishUp, $editedEmail['publishUp']);
|
||||
Assert::assertSame($expectedPublishDown, $editedEmail['publishDown']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, mixed[]>
|
||||
*/
|
||||
public static function publishExistingPermissionProvider(): iterable
|
||||
{
|
||||
yield 'Sales user without the publish permission cannot publish own email' => [
|
||||
'creatorUsername' => 'sales',
|
||||
'permissions' => ['editown'],
|
||||
'expectedIsPublished' => false,
|
||||
'expectedPublishUp' => null,
|
||||
'expectedPublishDown' => null,
|
||||
];
|
||||
|
||||
yield 'Sales user without the publish permission cannot publish admin\'s email' => [
|
||||
'creatorUsername' => 'admin',
|
||||
'permissions' => ['editother'],
|
||||
'expectedIsPublished' => false,
|
||||
'expectedPublishUp' => null,
|
||||
'expectedPublishDown' => null,
|
||||
];
|
||||
|
||||
yield 'Sales user with the publish other permission cannot publish own email' => [
|
||||
'creatorUsername' => 'sales',
|
||||
'permissions' => ['editown', 'publishother'],
|
||||
'expectedIsPublished' => false,
|
||||
'expectedPublishUp' => null,
|
||||
'expectedPublishDown' => null,
|
||||
];
|
||||
|
||||
yield 'Sales user with the publish other permission can publish admin\'s email' => [
|
||||
'creatorUsername' => 'admin',
|
||||
'permissions' => ['editother', 'publishother'],
|
||||
'expectedIsPublished' => true,
|
||||
'expectedPublishUp' => '2024-11-21T15:45:00+00:00',
|
||||
'expectedPublishDown' => '2024-12-21T15:45:00+00:00',
|
||||
];
|
||||
|
||||
yield 'Sales user with the publish own permission can publish own email' => [
|
||||
'creatorUsername' => 'sales',
|
||||
'permissions' => ['editown', 'publishown'],
|
||||
'expectedIsPublished' => true,
|
||||
'expectedPublishUp' => '2024-11-21T15:45:00+00:00',
|
||||
'expectedPublishDown' => '2024-12-21T15:45:00+00:00',
|
||||
];
|
||||
|
||||
yield 'Sales user with the publish own permission cannot publish admin\'s email' => [
|
||||
'creatorUsername' => 'admin',
|
||||
'permissions' => ['editother', 'publishown'],
|
||||
'expectedIsPublished' => false,
|
||||
'expectedPublishUp' => null,
|
||||
'expectedPublishDown' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function testReplyAction(): void
|
||||
{
|
||||
$trackingHash = 'tracking_hash_123';
|
||||
|
||||
/** @var StatRepository $statRepository */
|
||||
$statRepository = static::getContainer()->get('mautic.email.repository.stat');
|
||||
|
||||
// Create a test email stat.
|
||||
$stat = new Stat();
|
||||
$stat->setTrackingHash($trackingHash);
|
||||
$stat->setEmailAddress('john@doe.email');
|
||||
$stat->setDateSent(new \DateTime());
|
||||
|
||||
$statRepository->saveEntity($stat);
|
||||
|
||||
// Create new email reply.
|
||||
$this->client->request('POST', "/api/emails/reply/{$trackingHash}");
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertSame(Response::HTTP_CREATED, $response->getStatusCode());
|
||||
$this->assertSame(['success' => true], json_decode($response->getContent(), true));
|
||||
|
||||
// Get the email reply that was just created from the stat API.
|
||||
$statReplyQuery = ['where' => [['col' => 'stat_id', 'expr' => 'eq', 'val' => $stat->getId()]]];
|
||||
$this->client->request('GET', '/api/stats/email_stat_replies', $statReplyQuery);
|
||||
$this->assertResponseIsSuccessful();
|
||||
$fetchedReplyData = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
// Check that the email reply was created correctly.
|
||||
$this->assertSame('1', $fetchedReplyData['total']);
|
||||
$this->assertSame($stat->getId(), $fetchedReplyData['stats'][0]['stat_id']);
|
||||
$this->assertMatchesRegularExpression('/api-[a-z0-9]*/', $fetchedReplyData['stats'][0]['message_id']);
|
||||
|
||||
// Get the email stat that was just updated from the stat API.
|
||||
$statQuery = ['where' => [['col' => 'id', 'expr' => 'eq', 'val' => $stat->getId()]]];
|
||||
$this->client->request('GET', '/api/stats/email_stats', $statQuery);
|
||||
$fetchedStatData = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
// Check that the email stat was updated correctly/
|
||||
$this->assertSame('1', $fetchedStatData['total']);
|
||||
$this->assertSame($stat->getId(), $fetchedStatData['stats'][0]['id']);
|
||||
$this->assertSame('1', $fetchedStatData['stats'][0]['is_read']);
|
||||
$this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}/', $fetchedStatData['stats'][0]['date_read']);
|
||||
}
|
||||
|
||||
public function testSendAction(): void
|
||||
{
|
||||
// Create a user (to test use onwer as mailer):
|
||||
$role = new Role();
|
||||
$role->setName('Role');
|
||||
$this->em->persist($role);
|
||||
|
||||
$user = new User();
|
||||
$user->setUserName('apitest');
|
||||
$user->setFirstName('John');
|
||||
$user->setLastName('Doe');
|
||||
$user->setEmail('john@api.test');
|
||||
$user->setSignature('Best regards, |FROM_NAME|');
|
||||
$user->setRole($role);
|
||||
|
||||
$hasher = static::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
|
||||
\assert($hasher instanceof PasswordHasherInterface);
|
||||
|
||||
$user->setPassword($hasher->hash('password'));
|
||||
$this->em->persist($user);
|
||||
|
||||
// Create a contact:
|
||||
$contact = new Lead();
|
||||
$contact->setFirstName('Jane');
|
||||
$contact->setLastName('Doe');
|
||||
$contact->setEmail('jane@api.test');
|
||||
$contact->setOwner($user);
|
||||
$this->em->persist($contact);
|
||||
|
||||
// Create a segment:
|
||||
$segment = new LeadList();
|
||||
$segment->setName('API segment');
|
||||
$segment->setPublicName('API segment');
|
||||
$segment->setAlias('API segment');
|
||||
$segment->setDescription('Segment created via API test');
|
||||
$segment->setIsPublished(true);
|
||||
$this->em->persist($segment);
|
||||
|
||||
// Add contact to segment:
|
||||
$segmentContact = new ListLead();
|
||||
$segmentContact->setLead($contact);
|
||||
$segmentContact->setList($segment);
|
||||
$segmentContact->setDateAdded(new \DateTime());
|
||||
$this->em->persist($segmentContact);
|
||||
|
||||
// Commit
|
||||
$this->em->flush();
|
||||
|
||||
$contactId = $contact->getId();
|
||||
|
||||
// Create an email:
|
||||
$createEmail = function () use ($segment) {
|
||||
$email = new Email();
|
||||
$email->setName('API email');
|
||||
$email->setSubject('Email created via API test');
|
||||
$email->setEmailType('list');
|
||||
$email->addList($segment);
|
||||
$email->setCustomHtml('<h1>Email content created by an API test</h1>{custom-token}<br>{signature}');
|
||||
$email->setIsPublished(true);
|
||||
$email->setFromAddress('from@api.test');
|
||||
$email->setFromName('API Test');
|
||||
$email->setReplyToAddress('reply@api.test');
|
||||
$email->setBccAddress('bcc@api.test');
|
||||
|
||||
return $email;
|
||||
};
|
||||
|
||||
$email = $createEmail();
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
$emailId = $email->getId();
|
||||
|
||||
// Send to segment:
|
||||
$this->client->request('POST', "/api/emails/{$emailId}/send");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$sendResponse = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertEquals($sendResponse, ['success' => true, 'sentCount' => 1, 'failedRecipients' => 0], $clientResponse->getContent());
|
||||
|
||||
$testEmail = function (string $customToken): void {
|
||||
$message = $this->transport->sentMessage;
|
||||
$this->assertSame($message->getSubject(), 'Email created via API test');
|
||||
$bodyRegExp = '#<h1>Email content created by an API test</h1>'.$customToken.'<br>Best regards, Mautic Admin<img height="1" width="1" src="[^"]+" alt="" />#';
|
||||
$this->assertMatchesRegularExpression($bodyRegExp, $message->getHtmlBody());
|
||||
$this->assertSame([$message->getTo()[0]->getAddress() => $message->getTo()[0]->getName()], ['jane@api.test' => 'Jane Doe']);
|
||||
$this->assertSame([$message->getFrom()[0]->getAddress() => $message->getFrom()[0]->getName()], ['from@api.test' => 'API Test']);
|
||||
$this->assertSame([$message->getReplyTo()[0]->getAddress() => $message->getReplyTo()[0]->getName()], ['reply@api.test' => '']);
|
||||
$this->assertSame([$message->getBcc()[0]->getAddress() => $message->getBcc()[0]->getName()], ['bcc@api.test' => '']);
|
||||
};
|
||||
$testEmail('{custom-token}');
|
||||
|
||||
// Send to contact:
|
||||
$email = $createEmail();
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
$emailId = $email->getId();
|
||||
$this->client->request('POST', "/api/emails/{$emailId}/contact/{$contactId}/send", ['tokens' => ['{custom-token}' => 'custom <b>value</b>']]);
|
||||
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
|
||||
$sendResponse = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertEquals($sendResponse, ['success' => true], $clientResponse->getContent());
|
||||
$testEmail('custom <b>value</b>');
|
||||
|
||||
// Test use owner as mailer:
|
||||
$email = $createEmail();
|
||||
$email->setUseOwnerAsMailer(true);
|
||||
$email->setReplyToAddress(null);
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
$emailId = $email->getId();
|
||||
// Send to segment:
|
||||
$this->client->request('POST', "/api/emails/{$emailId}/send");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$sendResponse = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertEquals($sendResponse, ['success' => true, 'sentCount' => 1, 'failedRecipients' => 0], $clientResponse->getContent());
|
||||
|
||||
$testEmailOwnerAsMailer = function (): void {
|
||||
$message = $this->transport->sentMessage;
|
||||
$this->assertSame($message->getSubject(), 'Email created via API test');
|
||||
$bodyRegExp = '#<h1>Email content created by an API test</h1>{custom-token}<br>Best regards, John Doe<img height="1" width="1" src="[^"]+" alt="" />#';
|
||||
$this->assertMatchesRegularExpression($bodyRegExp, $message->getHtmlBody());
|
||||
$this->assertSame([$message->getTo()[0]->getAddress() => $message->getTo()[0]->getName()], ['jane@api.test' => 'Jane Doe']);
|
||||
$this->assertSame([$message->getFrom()[0]->getAddress() => $message->getFrom()[0]->getName()], ['john@api.test' => 'John Doe']);
|
||||
$this->assertSame([$message->getReplyTo()[0]->getAddress() => $message->getReplyTo()[0]->getName()], ['john@api.test' => '']);
|
||||
$this->assertSame([$message->getBcc()[0]->getAddress() => $message->getBcc()[0]->getName()], ['bcc@api.test' => '']);
|
||||
};
|
||||
$testEmailOwnerAsMailer();
|
||||
|
||||
// Send to contact:
|
||||
$email = $createEmail();
|
||||
$email->setUseOwnerAsMailer(true);
|
||||
$email->setReplyToAddress(null);
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
$emailId = $email->getId();
|
||||
$this->client->request('POST', "/api/emails/{$emailId}/contact/{$contactId}/send");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
|
||||
$sendResponse = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertEquals($sendResponse, ['success' => true], $clientResponse->getContent());
|
||||
$testEmailOwnerAsMailer();
|
||||
|
||||
// Test Custom Reply-To Address
|
||||
$email = $createEmail();
|
||||
$email->setUseOwnerAsMailer(true);
|
||||
$email->setReplyToAddress('reply@email.domain');
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
$emailId = $email->getId();
|
||||
|
||||
$this->client->request('POST', "/api/emails/{$emailId}/contact/{$contactId}/send");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
|
||||
$sendResponse = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertEquals($sendResponse, ['success' => true], $clientResponse->getContent());
|
||||
|
||||
$testCustomReplyTo = function (): void {
|
||||
$message = $this->transport->sentMessage;
|
||||
$this->assertSame($message->getSubject(), 'Email created via API test');
|
||||
$bodyRegExp = '#<h1>Email content created by an API test</h1>{custom-token}<br>Best regards, John Doe<img height="1" width="1" src="[^"]+" alt="" />#';
|
||||
$this->assertMatchesRegularExpression($bodyRegExp, $message->getHtmlBody());
|
||||
$this->assertSame([$message->getTo()[0]->getAddress() => $message->getTo()[0]->getName()], ['jane@api.test' => 'Jane Doe']);
|
||||
$this->assertSame([$message->getFrom()[0]->getAddress() => $message->getFrom()[0]->getName()], ['john@api.test' => 'John Doe']);
|
||||
$this->assertSame([$message->getReplyTo()[0]->getAddress() => $message->getReplyTo()[0]->getName()], ['reply@email.domain' => '']);
|
||||
$this->assertSame([$message->getBcc()[0]->getAddress() => $message->getBcc()[0]->getName()], ['bcc@api.test' => '']);
|
||||
};
|
||||
|
||||
$testCustomReplyTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function setPrivateProperty(object $object, string $property, $value): void
|
||||
{
|
||||
$reflector = new \ReflectionProperty($object::class, $property);
|
||||
$reflector->setAccessible(true);
|
||||
$reflector->setValue($object, $value);
|
||||
}
|
||||
|
||||
public function testGetEmails(): void
|
||||
{
|
||||
$segment1 = $this->createSegment('Segment A', 'segment-a');
|
||||
$segment2 = $this->createSegment('Segment B', 'segment-b');
|
||||
$segment3 = $this->createSegment('Segment C', 'segment-c');
|
||||
$segment4 = $this->createSegment('Segment D', 'segment-d');
|
||||
$this->em->flush();
|
||||
$segments = [
|
||||
$segment1->getId() => $segment1,
|
||||
$segment2->getId() => $segment2,
|
||||
$segment3->getId() => $segment3,
|
||||
$segment4->getId() => $segment4,
|
||||
];
|
||||
$email1 = $this->createEmail('Email A', 'Email A Subject', 'list', 'beefree-empty', 'Test html', $segments);
|
||||
$email2 = $this->createEmail('Email B', 'Email B Subject', 'list', 'beefree-empty', 'Test html', $segments);
|
||||
$email3 = $this->createEmail('Email C', 'Email C Subject', 'list', 'beefree-empty', 'Test html', $segments);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('get', '/api/emails?limit=2');
|
||||
$response = $this->client->getResponse();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertCount(2, $responseData['emails']);
|
||||
$this->assertSame([$email1->getId(), $email2->getId()], array_keys($responseData['emails']));
|
||||
|
||||
$this->client->request('get', '/api/emails?limit=3');
|
||||
$response = $this->client->getResponse();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertCount(3, $responseData['emails']);
|
||||
$this->assertSame([$email1->getId(), $email2->getId(), $email3->getId()], array_keys($responseData['emails']));
|
||||
}
|
||||
|
||||
private function createSegment(string $name, string $alias): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName($name);
|
||||
$segment->setPublicName($name);
|
||||
$segment->setAlias($alias);
|
||||
$this->em->persist($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $segments
|
||||
*
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
*/
|
||||
private function createEmail(string $name, string $subject, string $emailType, string $template, string $customHtml, array $segments = []): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName($name);
|
||||
$email->setSubject($subject);
|
||||
$email->setEmailType($emailType);
|
||||
$email->setTemplate($template);
|
||||
$email->setCustomHtml($customHtml);
|
||||
$email->setLists($segments);
|
||||
$this->em->persist($email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int|string> $payload
|
||||
*
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
*/
|
||||
#[DataProvider('getDataForUpdatingTranslatedEmailDoesNotRemoveParentRelation')]
|
||||
public function testUpdatingTranslatedEmailDoesNotRemoveParentRelation(array $payload): void
|
||||
{
|
||||
$parentEmail = $this->createEmail('Parent Email', 'Parent Email Subject', 'template', 'blank', 'Parent Email');
|
||||
$childEmail = $this->createEmail('Child Email', 'Child Email Subject', 'template', 'blank', 'Child Email');
|
||||
$childEmail->setTranslationParent($parentEmail);
|
||||
$this->em->persist($childEmail);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(
|
||||
Request::METHOD_PATCH,
|
||||
sprintf('/api/emails/%s/edit', $childEmail->getId()),
|
||||
$payload
|
||||
);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
|
||||
$emailData = $responseData['email'];
|
||||
$this->assertArrayHasKey('translationParent', $emailData);
|
||||
$this->assertNotEmpty($emailData['translationParent']);
|
||||
|
||||
$this->assertEquals(
|
||||
$parentEmail->getId(),
|
||||
$emailData['translationParent']['id'],
|
||||
'The translation parent ID should remain unchanged after updating the child email.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{0: array<string, int|string>}>
|
||||
*/
|
||||
public static function getDataForUpdatingTranslatedEmailDoesNotRemoveParentRelation(): iterable
|
||||
{
|
||||
yield 'When children set to unpublished, parent relation should remain' => [
|
||||
[
|
||||
'isPublished' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
yield 'When children updated for name, parent relation should remain' => [
|
||||
[
|
||||
'name' => 'Updated Child Email',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getUser(string $userName): ?User
|
||||
{
|
||||
$repository = $this->em->getRepository(User::class);
|
||||
$user = $repository->findOneBy(['username' => $userName]);
|
||||
if (!$user instanceof User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string[]> $permissions
|
||||
*/
|
||||
private function setPermission(Role $role, array $permissions): void
|
||||
{
|
||||
$roleModel = $this->getContainer()->get('mautic.user.model.role');
|
||||
$roleModel->setRolePermissions($role, $permissions);
|
||||
$this->em->persist($role);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ConfigControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testValuesAreEscapedProperly(): void
|
||||
{
|
||||
$data = [
|
||||
'scheme' => 'smtp',
|
||||
'host' => 'local+@$#/:*!host',
|
||||
'port' => '25',
|
||||
'path' => 'pa+@$#/:*!th',
|
||||
'user' => 'us+@$#/:*!er',
|
||||
'password' => 'pass+@$#/:*!word',
|
||||
'type' => 'ty+@$#/:*!pe',
|
||||
];
|
||||
|
||||
// request config edit page
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
// set form data
|
||||
$form = $crawler->selectButton('config[buttons][save]')->form();
|
||||
$values = $form->getPhpValues();
|
||||
|
||||
$values['config']['leadconfig']['contact_columns'] = ['name', 'email', 'id']; // required
|
||||
$values['config']['emailconfig']['mailer_dsn']['scheme'] = $data['scheme'];
|
||||
$values['config']['emailconfig']['mailer_dsn']['host'] = $data['host'];
|
||||
$values['config']['emailconfig']['mailer_dsn']['port'] = $data['port'];
|
||||
$values['config']['emailconfig']['mailer_dsn']['path'] = $data['path'];
|
||||
$values['config']['emailconfig']['mailer_dsn']['user'] = $data['user'];
|
||||
$values['config']['emailconfig']['mailer_dsn']['password'] = $data['password'];
|
||||
$values['config']['emailconfig']['mailer_dsn']['options']['list']['0']['label'] = 'type';
|
||||
$values['config']['emailconfig']['mailer_dsn']['options']['list']['0']['value'] = $data['type'];
|
||||
|
||||
$this->client->request($form->getMethod(), $form->getUri(), $values);
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
// check the DSN is escaped properly in the config file (both using double percent signs and URL encoded)
|
||||
$configParameters = $this->getConfigParameters();
|
||||
Assert::assertSame($this->escape(
|
||||
$data['scheme']
|
||||
.'://'.urlencode($data['user'])
|
||||
.':'.urlencode($data['password'])
|
||||
.'@'.urlencode($data['host'])
|
||||
.':'.$data['port']
|
||||
.'/'.urlencode($data['path'])
|
||||
.'?type='.urlencode($data['type'])
|
||||
), $configParameters['mailer_dsn']);
|
||||
|
||||
// check values are unescaped properly in the edit form
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$form = $crawler->selectButton('config[buttons][save]')->form();
|
||||
Assert::assertEquals($data['scheme'], $form['config[emailconfig][mailer_dsn][scheme]']->getValue());
|
||||
Assert::assertEquals($data['host'], $form['config[emailconfig][mailer_dsn][host]']->getValue());
|
||||
Assert::assertEquals($data['port'], $form['config[emailconfig][mailer_dsn][port]']->getValue());
|
||||
Assert::assertEquals($data['path'], $form['config[emailconfig][mailer_dsn][path]']->getValue());
|
||||
Assert::assertEquals($data['user'], $form['config[emailconfig][mailer_dsn][user]']->getValue());
|
||||
Assert::assertEquals('🔒', $form['config[emailconfig][mailer_dsn][password]']->getValue());
|
||||
Assert::assertEquals($data['type'], $form['config[emailconfig][mailer_dsn][options][list][0][value]']->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $data
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataInvalidDsn')]
|
||||
public function testInvalidDsn(array $data, string $expectedMessage): void
|
||||
{
|
||||
// request config edit page
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
// set form data
|
||||
$form = $crawler->selectButton('config[buttons][save]')->form();
|
||||
$form->setValues($data + [
|
||||
'config[leadconfig][contact_columns]' => ['name', 'email', 'id'], // required
|
||||
]);
|
||||
|
||||
// check if there is the given validation error
|
||||
$crawler = $this->client->submit($form);
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
Assert::assertStringContainsString($expectedMessage, $crawler->text());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed[]>
|
||||
*/
|
||||
public static function dataInvalidDsn(): iterable
|
||||
{
|
||||
yield 'Unsupported scheme' => [
|
||||
[
|
||||
'config[emailconfig][mailer_dsn][scheme]' => 'unknown',
|
||||
],
|
||||
'The "unknown" scheme is not supported.',
|
||||
];
|
||||
|
||||
yield 'Invalid DSN' => [
|
||||
[
|
||||
'config[emailconfig][mailer_dsn][scheme]' => 'smtp',
|
||||
'config[emailconfig][mailer_dsn][host]' => '',
|
||||
],
|
||||
'The mailer DSN is invalid.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function getConfigParameters(): array
|
||||
{
|
||||
$parameters = [];
|
||||
include self::getContainer()->get('kernel')->getLocalConfigFile();
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
private function escape(string $value): string
|
||||
{
|
||||
return str_replace('%', '%%', $value);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\Controller\EmailController;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Event\ManualWinnerEvent;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\FormBundle\Helper\FormFieldHelper;
|
||||
use Mautic\LeadBundle\Helper\FakeContactHelper;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormFactory;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\Routing\Router;
|
||||
use Twig\Environment;
|
||||
|
||||
class EmailControllerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const NEW_CATEGORY_TITLE = 'New category';
|
||||
private MockObject $translatorMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|Session
|
||||
*/
|
||||
private MockObject $sessionMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|ModelFactory<EmailModel>
|
||||
*/
|
||||
private MockObject $modelFactoryMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|Container
|
||||
*/
|
||||
private MockObject $containerMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|Router
|
||||
*/
|
||||
private MockObject $routerMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|EmailModel
|
||||
*/
|
||||
private MockObject $modelMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|Email
|
||||
*/
|
||||
private MockObject $emailMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|FlashBag
|
||||
*/
|
||||
private MockObject $flashBagMock;
|
||||
|
||||
private EmailController $controller;
|
||||
|
||||
/**
|
||||
* @var MockObject|CorePermissions
|
||||
*/
|
||||
private MockObject $corePermissionsMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|FormFactory
|
||||
*/
|
||||
private MockObject $formFactoryMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|Form
|
||||
*/
|
||||
private MockObject $formMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|Environment
|
||||
*/
|
||||
private MockObject $twigMock;
|
||||
|
||||
private RequestStack $requestStack;
|
||||
|
||||
/**
|
||||
* @var MockObject|EventDispatcherInterface
|
||||
*/
|
||||
private MockObject $dispatcher;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->sessionMock = $this->createMock(Session::class);
|
||||
$this->containerMock = $this->createMock(Container::class);
|
||||
$this->routerMock = $this->createMock(Router::class);
|
||||
$this->modelMock = $this->createMock(EmailModel::class);
|
||||
$this->emailMock = $this->createMock(Email::class);
|
||||
$this->formMock = $this->createMock(Form::class);
|
||||
$this->twigMock = $this->createMock(Environment::class);
|
||||
|
||||
$this->formFactoryMock = $this->createMock(FormFactory::class);
|
||||
$formFieldHelper = $this->createMock(FormFieldHelper::class);
|
||||
$doctrine = $this->createMock(ManagerRegistry::class);
|
||||
$this->modelFactoryMock = $this->createMock(ModelFactory::class);
|
||||
$helperUserMock = $this->createMock(UserHelper::class);
|
||||
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->translatorMock = $this->createMock(Translator::class);
|
||||
$this->flashBagMock = $this->createMock(FlashBag::class);
|
||||
$this->requestStack = new RequestStack();
|
||||
$this->corePermissionsMock = $this->createMock(CorePermissions::class);
|
||||
|
||||
$helperUserMock->method('getUser')
|
||||
->willReturn(new User(false));
|
||||
|
||||
$this->controller = new EmailController(
|
||||
$this->formFactoryMock,
|
||||
$formFieldHelper,
|
||||
$doctrine,
|
||||
$this->modelFactoryMock,
|
||||
$helperUserMock,
|
||||
$coreParametersHelper,
|
||||
$this->dispatcher,
|
||||
$this->translatorMock,
|
||||
$this->flashBagMock,
|
||||
$this->requestStack,
|
||||
$this->corePermissionsMock
|
||||
);
|
||||
$this->controller->setContainer($this->containerMock);
|
||||
$this->sessionMock->method('getFlashBag')->willReturn($this->createMock(FlashBagInterface::class));
|
||||
}
|
||||
|
||||
public function testSendActionWhenNoEntityFound(): void
|
||||
{
|
||||
$this->containerMock->expects($this->once())
|
||||
->method('get')
|
||||
->with('router')
|
||||
->willReturn($this->routerMock);
|
||||
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('getModel')
|
||||
->with('email')
|
||||
->willReturn($this->modelMock);
|
||||
|
||||
$this->modelMock->expects($this->once())
|
||||
->method('getEntity')
|
||||
->with(5)
|
||||
->willReturn(null);
|
||||
|
||||
$this->routerMock->expects($this->any())
|
||||
->method('generate')
|
||||
->willReturn('https://some.url');
|
||||
|
||||
$this->emailMock->expects($this->never())
|
||||
->method('isPublished');
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->expects(self::once())
|
||||
->method('getSession')
|
||||
->willReturn($this->sessionMock);
|
||||
$this->requestStack->push($request);
|
||||
$response = $this->controller->sendAction($request, 5);
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testSendActionWhenEntityFoundButNotPublished(): void
|
||||
{
|
||||
$this->containerMock->expects($this->once())
|
||||
->method('get')
|
||||
->with('router')
|
||||
->willReturn($this->routerMock);
|
||||
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('getModel')
|
||||
->with('email')
|
||||
->willReturn($this->modelMock);
|
||||
|
||||
$this->modelMock->expects($this->once())
|
||||
->method('getEntity')
|
||||
->with(5)
|
||||
->willReturn($this->emailMock);
|
||||
|
||||
$this->routerMock->expects($this->any())
|
||||
->method('generate')
|
||||
->willReturn('https://some.url');
|
||||
|
||||
$this->emailMock->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(false);
|
||||
|
||||
$this->emailMock->expects($this->never())
|
||||
->method('getEmailType');
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->expects(self::once())
|
||||
->method('getSession')
|
||||
->willReturn($this->sessionMock);
|
||||
$this->requestStack->push($request);
|
||||
$response = $this->controller->sendAction($request, 5);
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testThatExampleEmailsHaveTestStringInTheirSubject(): void
|
||||
{
|
||||
$this->emailMock->expects($this->once())
|
||||
->method('setSubject')
|
||||
->with($this->stringStartsWith(EmailController::EXAMPLE_EMAIL_SUBJECT_PREFIX));
|
||||
|
||||
$services = [
|
||||
['router', Container::EXCEPTION_ON_INVALID_REFERENCE, $this->routerMock],
|
||||
['form.factory', Container::EXCEPTION_ON_INVALID_REFERENCE, $this->formFactoryMock],
|
||||
['twig', Container::EXCEPTION_ON_INVALID_REFERENCE, $this->twigMock],
|
||||
];
|
||||
|
||||
$serviceExists = fn ($key) => count(array_filter($services, fn ($service) => $service[0] === $key)) > 0;
|
||||
|
||||
$this->containerMock->method('has')->willReturnCallback($serviceExists);
|
||||
$this->containerMock->method('get')->willReturnMap($services);
|
||||
|
||||
$this->modelMock->expects($this->once())
|
||||
->method('getEntity')
|
||||
->with(1)
|
||||
->willReturn($this->emailMock);
|
||||
|
||||
$this->corePermissionsMock->expects($this->once())
|
||||
->method('hasEntityAccess')
|
||||
->with('email:emails:viewown', 'email:emails:viewother', null)
|
||||
->willReturn(true);
|
||||
|
||||
$this->routerMock->expects($this->once())
|
||||
->method('generate')
|
||||
->with('mautic_email_action', [
|
||||
'objectAction' => 'sendExample',
|
||||
'objectId' => 1,
|
||||
], 1)
|
||||
->willReturn('someUrl');
|
||||
|
||||
$this->formFactoryMock->expects($this->once())
|
||||
->method('create')
|
||||
->with(\Mautic\EmailBundle\Form\Type\ExampleSendType::class,
|
||||
[
|
||||
'emails' => [
|
||||
'list' => [
|
||||
0 => null,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'action' => 'someUrl',
|
||||
]
|
||||
)
|
||||
->willReturn($this->formMock);
|
||||
|
||||
$this->twigMock->expects($this->once())
|
||||
->method('render')
|
||||
->willReturn('');
|
||||
|
||||
$request = new Request();
|
||||
$this->requestStack->push($request);
|
||||
$this->controller->sendExampleAction($request, 1, $this->corePermissionsMock, $this->modelMock, $this->createMock(LeadModel::class), $this->createMock(FakeContactHelper::class));
|
||||
}
|
||||
|
||||
public function testWinnerActionForDispatchManualWinnerEvent(): void
|
||||
{
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->expects(self::once())
|
||||
->method('getSession')
|
||||
->willReturn($this->sessionMock);
|
||||
|
||||
$this->routerMock->expects($this->exactly(2))
|
||||
->method('generate')
|
||||
->willReturn('/test-url');
|
||||
$this->containerMock->expects($this->exactly(2))
|
||||
->method('get')
|
||||
->with('router')
|
||||
->willReturn($this->routerMock);
|
||||
|
||||
$this->corePermissionsMock->expects($this->once())
|
||||
->method('hasEntityAccess')
|
||||
->with('email:emails:editown', 'email:emails:editother', null)
|
||||
->willReturn(true);
|
||||
|
||||
$request->expects(self::once())
|
||||
->method('getMethod')
|
||||
->willReturn(Request::METHOD_POST);
|
||||
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('getModel')
|
||||
->with('email')
|
||||
->willReturn($this->modelMock);
|
||||
|
||||
$this->emailMock->expects($this->once())
|
||||
->method('getVariantParent')
|
||||
->willReturn($this->emailMock);
|
||||
|
||||
$this->modelMock->expects($this->once())
|
||||
->method('getEntity')
|
||||
->with(5)
|
||||
->willReturn($this->emailMock);
|
||||
|
||||
$this->dispatcher->expects($this->once())
|
||||
->method('dispatch')
|
||||
->with(new ManualWinnerEvent($this->emailMock));
|
||||
|
||||
$this->requestStack->push($request);
|
||||
$this->controller->winnerAction($request, 5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\EmailDraft;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class EmailDraftFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['email_draft_enabled'] = 'testEmailDraftNotConfigured' !== $this->name();
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testEmailDraftNotConfigured(): void
|
||||
{
|
||||
$email = $this->createNewEmail();
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/edit/{$email->getId()}");
|
||||
Assert::assertEquals(0, $crawler->selectButton('Save as Draft')->count());
|
||||
Assert::assertEquals(0, $crawler->selectButton('Apply Draft')->count());
|
||||
Assert::assertEquals(0, $crawler->selectButton('Discard Draft')->count());
|
||||
}
|
||||
|
||||
public function testEmailDraftConfigured(): void
|
||||
{
|
||||
$email = $this->createNewEmail();
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/edit/{$email->getId()}");
|
||||
|
||||
Assert::assertEquals(1, $crawler->selectButton('Save as Draft')->count());
|
||||
Assert::assertEquals(0, $crawler->selectButton('Apply Draft')->count());
|
||||
Assert::assertEquals(0, $crawler->selectButton('Discard Draft')->count());
|
||||
}
|
||||
|
||||
public function testCheckDraftInList(): void
|
||||
{
|
||||
$email = $this->createNewEmail();
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/emails');
|
||||
$this->assertStringNotContainsString('Has Draft', $crawler->filter('#app-content a[href="/s/emails/view/'.$email->getId().'"]')->html());
|
||||
$this->saveDraft($email);
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/emails');
|
||||
$this->assertStringContainsString('Has Draft', $crawler->filter('#app-content a[href="/s/emails/view/'.$email->getId().'"]')->html());
|
||||
}
|
||||
|
||||
public function testPreviewDraft(): void
|
||||
{
|
||||
$email = $this->createNewEmail();
|
||||
$this->saveDraft($email);
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/email/preview/{$email->getId()}");
|
||||
$this->assertEquals('Test html', $crawler->text());
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/email/preview/{$email->getId()}/draft");
|
||||
$this->assertEquals('Test html Draft', $crawler->text());
|
||||
}
|
||||
|
||||
public function testSaveDraftAndApplyDraftForLegacy(): void
|
||||
{
|
||||
$email = $this->createNewEmail();
|
||||
$this->applyDraft($email);
|
||||
}
|
||||
|
||||
public function testDiscardDraftForLegacy(): void
|
||||
{
|
||||
$email = $this->createNewEmail();
|
||||
$this->discardDraft($email);
|
||||
}
|
||||
|
||||
public function testEmailDeleteCascade(): void
|
||||
{
|
||||
$email = $this->createNewEmail();
|
||||
$this->saveDraft($email);
|
||||
$this->client->request(Request::METHOD_POST, "/s/emails/delete/{$email->getId()}");
|
||||
$emailDraft = $this->em->getRepository(EmailDraft::class)->findOneBy(['email' => $email]);
|
||||
Assert::assertNull($emailDraft);
|
||||
}
|
||||
|
||||
private function applyDraft(Email $email): void
|
||||
{
|
||||
$this->saveDraft($email);
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/edit/{$email->getId()}");
|
||||
$form = $crawler->selectButton('Apply Draft')->form();
|
||||
$this->client->submit($form);
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$emailDraft = $this->em->getRepository(EmailDraft::class)->findOneBy(['email' => $email]);
|
||||
|
||||
Assert::assertNull($emailDraft);
|
||||
Assert::assertSame('Test html Draft', $email->getCustomHtml());
|
||||
}
|
||||
|
||||
private function discardDraft(Email $email): void
|
||||
{
|
||||
$this->saveDraft($email);
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/edit/{$email->getId()}");
|
||||
$form = $crawler->selectButton('Discard Draft')->form();
|
||||
$this->client->submit($form);
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$emailDraft = $this->em->getRepository(EmailDraft::class)->findOneBy(['email' => $email]);
|
||||
|
||||
Assert::assertNull($emailDraft);
|
||||
Assert::assertSame('Test html', $email->getCustomHtml());
|
||||
}
|
||||
|
||||
private function saveDraft(Email $email): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/edit/{$email->getId()}");
|
||||
|
||||
$form = $crawler->selectButton('Save as Draft')->form();
|
||||
$form['emailform[customHtml]'] = 'Test html Draft';
|
||||
$this->client->submit($form);
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$emailDraft = $this->em->getRepository(EmailDraft::class)->findOneBy(['email' => $email]);
|
||||
Assert::assertEquals('Test html Draft', $emailDraft->getHtml());
|
||||
Assert::assertSame('Test html', $email->getCustomHtml());
|
||||
}
|
||||
|
||||
private function createNewEmail(string $templateName = 'blank', string $templateContent = 'Test html'): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName('Email A');
|
||||
$email->setSubject('Email A Subject');
|
||||
$email->setEmailType('template');
|
||||
$email->setTemplate($templateName);
|
||||
$email->setCustomHtml($templateContent);
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
return $email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class EmailExampleFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['mailer_spool_type'] = 'file';
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testSendExampleEmailWithContact(): void
|
||||
{
|
||||
$company = $this->createCompany('Mautic', 'hello@mautic.org');
|
||||
$company->setCity('Pune');
|
||||
$company->setCountry('India');
|
||||
|
||||
$this->em->persist($company);
|
||||
|
||||
$lead = $this->createLead('John', 'Doe', 'test@domain.tld');
|
||||
$this->createPrimaryCompanyForLead($lead, $company);
|
||||
|
||||
$email = $this->createEmail();
|
||||
$email->setCustomHtml('Contact emails is {contactfield=email}. Company details: {contactfield=companyname}, {contactfield=companycity}.');
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/sendExample/{$email->getId()}");
|
||||
$formCrawler = $crawler->filter('form[name=example_send]');
|
||||
Assert::assertCount(1, $formCrawler);
|
||||
$form = $formCrawler->form();
|
||||
$form->setValues([
|
||||
'example_send[emails][list][0]' => 'admin@yoursite.com',
|
||||
'example_send[contact]' => 'somebody',
|
||||
'example_send[contact_id]' => $lead->getId(),
|
||||
]);
|
||||
$this->client->submit($form);
|
||||
|
||||
$message = $this->getMailerMessagesByToAddress('admin@yoursite.com')[0];
|
||||
|
||||
Assert::assertSame('[TEST] [TEST] Email subject', $message->getSubject());
|
||||
Assert::assertStringContainsString(
|
||||
'Contact emails is test@domain.tld. Company details: Mautic, Pune.',
|
||||
$message->getBody()->toString()
|
||||
);
|
||||
}
|
||||
|
||||
public function testSendExampleEmailWithOutContact(): void
|
||||
{
|
||||
$email = $this->createEmail();
|
||||
$email->setCustomHtml('Contact emails is {contactfield=email}. Company details: {contactfield=companyname}, {contactfield=companycity}.');
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/sendExample/{$email->getId()}");
|
||||
$formCrawler = $crawler->filter('form[name=example_send]');
|
||||
self::assertCount(1, $formCrawler);
|
||||
$form = $formCrawler->form();
|
||||
$form->setValues(['example_send[emails][list][0]' => 'admin@yoursite.com']);
|
||||
$this->client->submit($form);
|
||||
|
||||
$message = $this->getMailerMessagesByToAddress('admin@yoursite.com')[0];
|
||||
|
||||
Assert::assertSame('[TEST] [TEST] Email subject', $message->getSubject());
|
||||
Assert::assertStringContainsString('Contact emails is [Email]. Company details: [Company Name], [City].', $message->getBody()->toString());
|
||||
}
|
||||
|
||||
public function testSendExampleEmailForDynamicContentVariantsWithCustomFieldWithNoContact(): void
|
||||
{
|
||||
// Create custom field
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/fields/contact/new',
|
||||
[
|
||||
'label' => 'bool',
|
||||
'type' => 'boolean',
|
||||
'properties' => [
|
||||
'no' => 'No',
|
||||
'yes' => 'Yes',
|
||||
],
|
||||
]
|
||||
);
|
||||
$response = $this->client->getResponse()->getContent();
|
||||
self::assertResponseStatusCodeSame(201, $response);
|
||||
self::assertJson($response);
|
||||
|
||||
// Create email with dynamic content variant
|
||||
$email = $this->createEmail();
|
||||
$dynamicContent = [
|
||||
[
|
||||
'tokenName' => 'Dynamic Content 1',
|
||||
'content' => '<p>Default Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'tokenName' => 'Dynamic Content 2',
|
||||
'content' => '<p>Default Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => '<p>Variant 1 Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'bool',
|
||||
'object' => 'lead',
|
||||
'type' => 'boolean',
|
||||
'filter' => '1',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$email->setCustomHtml('<div>{dynamiccontent="Dynamic Content 2"}</div>');
|
||||
$email->setDynamicContent($dynamicContent);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/sendExample/{$email->getId()}");
|
||||
$formCrawler = $crawler->filter('form[name=example_send]');
|
||||
Assert::assertCount(1, $formCrawler);
|
||||
$form = $formCrawler->form();
|
||||
$form->setValues(['example_send[emails][list][0]' => 'admin@yoursite.com']);
|
||||
$this->client->submit($form);
|
||||
|
||||
$message = $this->getMailerMessagesByToAddress('admin@yoursite.com')[0];
|
||||
|
||||
Assert::assertSame('[TEST] [TEST] Email subject', $message->getSubject());
|
||||
Assert::assertStringContainsString('Default Dynamic Content', $message->getBody()->toString());
|
||||
}
|
||||
|
||||
public function testSendExampleEmailForDynamicContentVariantsWithCustomFieldWithMatchFilterContact(): void
|
||||
{
|
||||
// Create custom field
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/fields/contact/new',
|
||||
[
|
||||
'label' => 'bool',
|
||||
'type' => 'boolean',
|
||||
'properties' => [
|
||||
'no' => 'No',
|
||||
'yes' => 'Yes',
|
||||
],
|
||||
]
|
||||
);
|
||||
$response = $this->client->getResponse()->getContent();
|
||||
self::assertResponseStatusCodeSame(201, $response);
|
||||
self::assertJson($response);
|
||||
|
||||
// Create email with dynamic content variant
|
||||
$email = $this->createEmail();
|
||||
$dynamicContent = [
|
||||
[
|
||||
'tokenName' => 'Dynamic Content 1',
|
||||
'content' => '<p>Default Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'tokenName' => 'Dynamic Content 2',
|
||||
'content' => '<p>Default Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => '<p>Variant 1 Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'bool',
|
||||
'object' => 'lead',
|
||||
'type' => 'boolean',
|
||||
'filter' => '1',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$email->setCustomHtml('<div>{dynamiccontent="Dynamic Content 2"}</div>');
|
||||
$email->setDynamicContent($dynamicContent);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Create some contacts
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/contacts/batch/new',
|
||||
[
|
||||
[
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'A',
|
||||
'email' => 'john.a@email.com',
|
||||
'bool' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
self::assertResponseStatusCodeSame(201, $this->client->getResponse()->getContent());
|
||||
$contacts = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/sendExample/{$email->getId()}");
|
||||
$formCrawler = $crawler->filter('form[name=example_send]');
|
||||
Assert::assertCount(1, $formCrawler);
|
||||
$form = $formCrawler->form();
|
||||
$form->setValues([
|
||||
'example_send[emails][list][0]' => 'admin@yoursite.com',
|
||||
'example_send[contact]' => $contacts['contacts'][0]['fields']['core']['firstname']['value'],
|
||||
'example_send[contact_id]' => $contacts['contacts'][0]['id'],
|
||||
]);
|
||||
$this->client->submit($form);
|
||||
|
||||
$message = $this->getMailerMessagesByToAddress('admin@yoursite.com')[0];
|
||||
|
||||
Assert::assertSame('[TEST] [TEST] Email subject', $message->getSubject());
|
||||
Assert::assertStringContainsString('Variant 1 Dynamic Content', $message->getBody()->toString());
|
||||
}
|
||||
|
||||
public function testSendExampleEmailForDynamicContentVariantsWithCustomFieldWithNoMatchFilterContact(): void
|
||||
{
|
||||
// Create custom field
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/fields/contact/new',
|
||||
[
|
||||
'label' => 'bool',
|
||||
'type' => 'boolean',
|
||||
'properties' => [
|
||||
'no' => 'No',
|
||||
'yes' => 'Yes',
|
||||
],
|
||||
]
|
||||
);
|
||||
$response = $this->client->getResponse()->getContent();
|
||||
self::assertResponseStatusCodeSame(201, $response);
|
||||
self::assertJson($response);
|
||||
|
||||
// Create email with dynamic content variant
|
||||
$email = $this->createEmail();
|
||||
$dynamicContent = [
|
||||
[
|
||||
'tokenName' => 'Dynamic Content 1',
|
||||
'content' => '<p>Default Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'tokenName' => 'Dynamic Content 2',
|
||||
'content' => '<p>Default Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => '<p>Variant 1 Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'bool',
|
||||
'object' => 'lead',
|
||||
'type' => 'boolean',
|
||||
'filter' => '1',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$email->setCustomHtml('<div>{dynamiccontent="Dynamic Content 2"}</div>');
|
||||
$email->setDynamicContent($dynamicContent);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Create some contacts
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/contacts/batch/new',
|
||||
[
|
||||
[
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'A',
|
||||
'email' => 'john.a@email.com',
|
||||
'bool' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
self::assertResponseStatusCodeSame(201, $this->client->getResponse()->getContent());
|
||||
$contacts = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/sendExample/{$email->getId()}");
|
||||
$formCrawler = $crawler->filter('form[name=example_send]');
|
||||
Assert::assertCount(1, $formCrawler);
|
||||
$form = $formCrawler->form();
|
||||
$form->setValues([
|
||||
'example_send[emails][list][0]' => 'admin@yoursite.com',
|
||||
'example_send[contact]' => $contacts['contacts'][0]['fields']['core']['firstname']['value'],
|
||||
'example_send[contact_id]' => $contacts['contacts'][0]['id'],
|
||||
]);
|
||||
$this->client->submit($form);
|
||||
|
||||
$message = $this->getMailerMessagesByToAddress('admin@yoursite.com')[0];
|
||||
|
||||
Assert::assertSame('[TEST] [TEST] Email subject', $message->getSubject());
|
||||
Assert::assertStringContainsString('Default Dynamic Content', $message->getBody()->toString());
|
||||
}
|
||||
|
||||
private function createEmail(): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setDateAdded(new \DateTime());
|
||||
$email->setName('Email name');
|
||||
$email->setSubject('Email subject');
|
||||
$email->setTemplate('Blank');
|
||||
$email->setCustomHtml('Contact emails is {contactfield=email}');
|
||||
$this->em->persist($email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Mautic\CoreBundle\Entity\AuditLog;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class EmailFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public const SAVE_AND_CLOSE = 'Save & Close';
|
||||
|
||||
public function testExcludedSegmentsConflicting(): void
|
||||
{
|
||||
$listOne = $this->createLeadList('One');
|
||||
$listTwo = $this->createLeadList('Two');
|
||||
$listThree = $this->createLeadList('Three');
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$email = $this->createEmail();
|
||||
$email->addList($listOne);
|
||||
$email->addExcludedList($listTwo);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/edit/{$email->getId()}");
|
||||
$this->assertResponseOk();
|
||||
$form = $crawler->selectButton(self::SAVE_AND_CLOSE)->form();
|
||||
|
||||
// change lists/excludedLists and submit the form
|
||||
$form['emailform[excludedLists]']->setValue([$listOne->getId(), $listThree->getId()]); // @phpstan-ignore-line
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$this->assertResponseOk();
|
||||
Assert::assertStringContainsString('The same segment cannot be excluded and included in the same time.', $crawler->html());
|
||||
}
|
||||
|
||||
public function testExcludedSegmentsFieldIsUpdated(): void
|
||||
{
|
||||
$listOne = $this->createLeadList('One');
|
||||
$listTwo = $this->createLeadList('Two');
|
||||
$listThree = $this->createLeadList('Three');
|
||||
$listFour = $this->createLeadList('Four');
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$email = $this->createEmail();
|
||||
$email->addList($listOne);
|
||||
$email->addExcludedList($listTwo);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/edit/{$email->getId()}");
|
||||
$this->assertResponseOk();
|
||||
|
||||
$form = $crawler->selectButton(self::SAVE_AND_CLOSE)->form();
|
||||
|
||||
/** @var ChoiceFormField $listsField */
|
||||
$listsField = $form['emailform[lists]'];
|
||||
/** @var ChoiceFormField $excludedListsField */
|
||||
$excludedListsField = $form['emailform[excludedLists]'];
|
||||
|
||||
$expectedAvailableOptions = [
|
||||
$listOne->getId(),
|
||||
$listTwo->getId(),
|
||||
$listThree->getId(),
|
||||
$listFour->getId(),
|
||||
];
|
||||
$this->assertChoiceOptions($listsField, $expectedAvailableOptions, [$listOne->getId()]);
|
||||
$this->assertChoiceOptions($excludedListsField, $expectedAvailableOptions, [$listTwo->getId()]);
|
||||
|
||||
// change lists/excludedLists and submit the form
|
||||
$listsField->setValue([$listOne->getId(), $listFour->getId()]);
|
||||
$excludedListsField->setValue([$listTwo->getId(), $listThree->getId()]);
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseOk();
|
||||
|
||||
$email = $this->em->find(Email::class, $email->getId());
|
||||
|
||||
// assert lists/excludedLists changed accordingly
|
||||
$this->assertEmailLists([
|
||||
$listOne->getId(),
|
||||
$listFour->getId(),
|
||||
], $email->getLists());
|
||||
$this->assertEmailLists([
|
||||
$listTwo->getId(),
|
||||
$listThree->getId(),
|
||||
], $email->getExcludedLists());
|
||||
|
||||
// assert audit log
|
||||
$auditLogs = $this->em->getRepository(AuditLog::class)->findBy([
|
||||
'bundle' => 'email',
|
||||
'object' => 'email',
|
||||
]);
|
||||
Assert::assertCount(1, $auditLogs);
|
||||
/** @var AuditLog $auditLog */
|
||||
$auditLog = reset($auditLogs);
|
||||
Assert::assertInstanceOf(AuditLog::class, $auditLog);
|
||||
$details = $auditLog->getDetails();
|
||||
Assert::assertIsArray($details);
|
||||
Assert::assertArrayHasKey('lists', $details);
|
||||
Assert::assertSame([
|
||||
[$listOne->getId()],
|
||||
[$listOne->getId(), $listFour->getId()],
|
||||
], $details['lists']);
|
||||
Assert::assertArrayHasKey('excludedLists', $details);
|
||||
Assert::assertSame([
|
||||
[$listTwo->getId()],
|
||||
[$listTwo->getId(), $listThree->getId()],
|
||||
], $details['excludedLists']);
|
||||
}
|
||||
|
||||
public function testPreferenceCenterChangeIsTrackedInAuditLog(): void
|
||||
{
|
||||
$preferenceCenterOne = $this->createPreferenceCenterPage('Preference Center One');
|
||||
$preferenceCenterTwo = $this->createPreferenceCenterPage('Preference Center Two');
|
||||
$listOne = $this->createLeadList('One');
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$email = $this->createEmail();
|
||||
$email->addList($listOne);
|
||||
$email->setPreferenceCenter($preferenceCenterOne);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/edit/{$email->getId()}");
|
||||
$this->assertResponseOk();
|
||||
|
||||
$form = $crawler->selectButton(self::SAVE_AND_CLOSE)->form();
|
||||
|
||||
$preferenceCenterField = $form['emailform[preferenceCenter]'];
|
||||
Assert::assertSame((string) $preferenceCenterOne->getId(), $preferenceCenterField->getValue());
|
||||
|
||||
$preferenceCenterField->setValue((string) $preferenceCenterTwo->getId());
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseOk();
|
||||
|
||||
$email = $this->em->find(Email::class, $email->getId());
|
||||
|
||||
Assert::assertSame($preferenceCenterTwo->getId(), $email->getPreferenceCenter()->getId());
|
||||
|
||||
$auditLogs = $this->em->getRepository(AuditLog::class)->findBy([
|
||||
'bundle' => 'email',
|
||||
'object' => 'email',
|
||||
]);
|
||||
Assert::assertCount(1, $auditLogs);
|
||||
|
||||
/** @var AuditLog $auditLog */
|
||||
$auditLog = reset($auditLogs);
|
||||
Assert::assertInstanceOf(AuditLog::class, $auditLog);
|
||||
|
||||
$details = $auditLog->getDetails();
|
||||
Assert::assertIsArray($details);
|
||||
Assert::assertArrayHasKey('preferenceCenter', $details);
|
||||
Assert::assertSame([
|
||||
$preferenceCenterOne->getId(),
|
||||
$preferenceCenterTwo->getId(),
|
||||
], $details['preferenceCenter']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createLeadList(string $name): LeadList
|
||||
{
|
||||
$leadList = new LeadList();
|
||||
$leadList->setName($name);
|
||||
$leadList->setPublicName($name);
|
||||
$leadList->setAlias(mb_strtolower($name));
|
||||
$this->em->persist($leadList);
|
||||
|
||||
return $leadList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $expected
|
||||
* @param mixed[] $actual
|
||||
*/
|
||||
private function assertArrayValuesEquals(array $expected, array $actual): void
|
||||
{
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
|
||||
Assert::assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $expectedAvailableOptions
|
||||
* @param mixed[] $expectedValue
|
||||
*/
|
||||
private function assertChoiceOptions(ChoiceFormField $field, array $expectedAvailableOptions, array $expectedValue): void
|
||||
{
|
||||
$this->assertArrayValuesEquals($expectedAvailableOptions, $field->availableOptionValues());
|
||||
$this->assertArrayValuesEquals($expectedValue, $field->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $expectedListIds
|
||||
*/
|
||||
private function assertEmailLists(array $expectedListIds, Collection $collection): void
|
||||
{
|
||||
$this->assertArrayValuesEquals($expectedListIds, $collection->map(function (LeadList $leadList) {
|
||||
return $leadList->getId();
|
||||
})->toArray());
|
||||
}
|
||||
|
||||
private function createEmail(): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName('Email name');
|
||||
$email->setSubject('Email subject');
|
||||
$email->setEmailType('list');
|
||||
$email->setTemplate('some-template');
|
||||
$email->setCustomHtml('{}');
|
||||
$this->em->persist($email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createPreferenceCenterPage(string $name): Page
|
||||
{
|
||||
$page = new Page();
|
||||
$page->setTitle($name);
|
||||
$page->setAlias(mb_strtolower(str_replace(' ', '-', $name)));
|
||||
$page->setIsPreferenceCenter(true);
|
||||
$page->setCustomHtml('<html><body>Preference Center Page</body></html>');
|
||||
$page->setIsPublished(true);
|
||||
$this->em->persist($page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
private function assertResponseOk(): void
|
||||
{
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class EmailGraphStatsControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
|
||||
public function testTemplateViewAction(): void
|
||||
{
|
||||
$email = $this->createAndPersistEmail('Email A');
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/emails-graph-stats/{$email->getId()}/0/2022-08-21/2022-09-21");
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
}
|
||||
|
||||
public function testSegmentViewAction(): void
|
||||
{
|
||||
$segment = $this->createSegment('segment-B', []);
|
||||
$email = $this->createAndPersistEmail('Email B', $segment);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/emails-graph-stats/{$email->getId()}/0/2022-08-21/2022-09-21");
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
}
|
||||
|
||||
private function createAndPersistEmail(string $name, ?LeadList $segment = null): Email
|
||||
{
|
||||
$email = $this->createEmail($name);
|
||||
if (null !== $segment) {
|
||||
$email->addList($segment);
|
||||
}
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
return $email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
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\Redirect;
|
||||
use Mautic\PageBundle\Entity\Trackable;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EmailMapStatsControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testViewAction(): void
|
||||
{
|
||||
$leadsPayload = [
|
||||
[
|
||||
'email' => 'example1@test.com',
|
||||
'country' => 'Italy',
|
||||
'read' => true,
|
||||
'click' => true,
|
||||
],
|
||||
[
|
||||
'email' => 'example2@test.com',
|
||||
'country' => 'France',
|
||||
'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();
|
||||
|
||||
$this->client->request('GET', "s/emails-map-stats/{$email->getId()}/false/2023-07-20/2023-07-25");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$crawler = new Crawler($clientResponse->getContent(), $this->client->getInternalRequest()->getUri());
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
|
||||
$this->assertSame('Emails', $crawler->filter('.map-options__title')->innerText());
|
||||
$this->assertCount(1, $crawler->filter('div.map-options'));
|
||||
$this->assertCount(1, $crawler->filter('div.vector-map'));
|
||||
|
||||
$readOption = $crawler->filter('label.map-options__item')->filter('[data-stat-unit="Read"]');
|
||||
$this->assertCount(1, $readOption);
|
||||
$this->assertSame('Total: 4 (3 with country)', $readOption->attr('data-legend-text'));
|
||||
$this->assertSame('{"IT":1,"PL":2}', $readOption->attr('data-map-series'));
|
||||
|
||||
$clickOption = $crawler->filter('label.map-options__item')->filter('[data-stat-unit="Click"]');
|
||||
$this->assertCount(1, $clickOption);
|
||||
$this->assertSame('Total: 3 (2 with country)', $clickOption->attr('data-legend-text'));
|
||||
$this->assertSame('{"IT":1,"PL":1}', $clickOption->attr('data-map-series'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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-21'));
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class EmailProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
|
||||
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
|
||||
{
|
||||
$projectOne = $this->createProject('Project One');
|
||||
$projectTwo = $this->createProject('Project Two');
|
||||
$projectThree = $this->createProject('Project Three');
|
||||
|
||||
$emailAlpha = $this->createEmail('Email Alpha');
|
||||
$emailBeta = $this->createEmail('Email Beta');
|
||||
$this->createEmail('Email Gamma');
|
||||
$this->createEmail('Email Delta');
|
||||
|
||||
$emailAlpha->addProject($projectOne);
|
||||
$emailAlpha->addProject($projectTwo);
|
||||
$emailBeta->addProject($projectTwo);
|
||||
$emailBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/emails', '/s/emails']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
|
||||
*/
|
||||
public static function searchDataProvider(): \Generator
|
||||
{
|
||||
yield 'search by one project' => [
|
||||
'searchTerm' => 'project:"Project Two"',
|
||||
'expectedEntities' => ['Email Alpha', 'Email Beta'],
|
||||
'unexpectedEntities' => ['Email Gamma', 'Email Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND email name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Email Beta'],
|
||||
'unexpectedEntities' => ['Email Alpha', 'Email Gamma', 'Email Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR email name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Email Alpha', 'Email Beta', 'Email Gamma'],
|
||||
'unexpectedEntities' => ['Email Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Email Gamma', 'Email Delta'],
|
||||
'unexpectedEntities' => ['Email Alpha', 'Email Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Email Beta'],
|
||||
'unexpectedEntities' => ['Email Alpha', 'Email Gamma', 'Email Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Email Gamma', 'Email Delta'],
|
||||
'unexpectedEntities' => ['Email Alpha', 'Email Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Email Alpha', 'Email Beta'],
|
||||
'unexpectedEntities' => ['Email Gamma', 'Email Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Email Alpha', 'Email Gamma', 'Email Delta'],
|
||||
'unexpectedEntities' => ['Email Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createEmail(string $name): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName($name);
|
||||
$this->em->persist($email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Mailer\Message\MauticMessage;
|
||||
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 EmailSendFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['disable_trackable_urls'] = false;
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testSendEmailWithContact(): void
|
||||
{
|
||||
$segment = $this->createSegment('Segment A', 'seg-a');
|
||||
$leads = $this->createContacts(2, $segment);
|
||||
$content = '<!DOCTYPE html><htm><body><a href="https://localhost">link</a>
|
||||
<a id="{unsubscribe_url}">unsubscribe here</a>
|
||||
<a href="{resubscribe_url}">resubscribe here</a>
|
||||
</body></html>';
|
||||
$email = $this->createEmail(
|
||||
'test subject',
|
||||
[$segment->getId() => $segment],
|
||||
$content
|
||||
);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(
|
||||
Request::METHOD_POST,
|
||||
'/s/ajax?action=email:sendBatch',
|
||||
['id' => $email->getId(), 'pending' => 2]
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful($response->getContent());
|
||||
Assert::assertSame(
|
||||
'{"success":1,"percent":100,"progress":[2,2],"stats":{"sent":2,"failed":0,"failedRecipients":[]}}',
|
||||
$response->getContent()
|
||||
);
|
||||
|
||||
$messages = [
|
||||
$this->getMailerMessagesByToAddress('contact-flood-0@doe.com')[0],
|
||||
$this->getMailerMessagesByToAddress('contact-flood-1@doe.com')[0],
|
||||
];
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$body = quoted_printable_decode($message->getBody()->bodyToString());
|
||||
preg_match('/<a href=\"([^\"]*)\">(.*)<\/a>/iU', $body, $match);
|
||||
Assert::assertArrayHasKey(1, $match, $body);
|
||||
parse_str(parse_url($match[1], PHP_URL_QUERY), $queryParams);
|
||||
$clickThrough = unserialize(base64_decode($queryParams['ct']));
|
||||
Assert::assertArrayHasKey($message->getTo()[0]->toString(), $leads);
|
||||
Assert::assertSame($leads[$message->getTo()[0]->toString()]->getId(), (int) $clickThrough['lead']);
|
||||
}
|
||||
|
||||
// Sort messages by to address as the order can differ
|
||||
usort(
|
||||
$messages,
|
||||
static fn (MauticMessage $a, MauticMessage $b) => $a->getTo()[0]->toString() <=> $b->getTo()[0]->toString()
|
||||
);
|
||||
|
||||
$unsubscribeUrlPattern = '/https?:\/\/[^\/]+\/email\/unsubscribe\/([0-9a-z]{20})/';
|
||||
$resubscribeUrlPattern = '/https?:\/\/[^\/]+\/email\/resubscribe\/([0-9a-z]{20})/';
|
||||
|
||||
// First email:
|
||||
Assert::assertStringContainsString('contact-flood-0@doe.com', $messages[0]->toString());
|
||||
preg_match($unsubscribeUrlPattern, $messages[0]->getHtmlBody(), $unsubscribeMatches1);
|
||||
preg_match($resubscribeUrlPattern, $messages[0]->getHtmlBody(), $resubscribeMatches1);
|
||||
|
||||
Assert::assertSame(20, strlen($unsubscribeMatches1[1]), $messages[0]->getHtmlBody());
|
||||
Assert::assertEquals($unsubscribeMatches1[1], $resubscribeMatches1[1], $messages[0]->getHtmlBody());
|
||||
|
||||
// Second email:
|
||||
Assert::assertStringContainsString('contact-flood-1@doe.com', $messages[1]->toString());
|
||||
preg_match($unsubscribeUrlPattern, $messages[1]->getHtmlBody(), $unsubscribeMatches2);
|
||||
preg_match($resubscribeUrlPattern, $messages[1]->getHtmlBody(), $resubscribeMatches2);
|
||||
|
||||
Assert::assertSame(20, strlen($unsubscribeMatches2[1]), $messages[1]->getHtmlBody());
|
||||
Assert::assertEquals($unsubscribeMatches2[1], $resubscribeMatches2[1], $messages[1]->getHtmlBody());
|
||||
|
||||
// The email stat hashes cannot be the same in different emails:
|
||||
Assert::assertNotEquals($unsubscribeMatches1[1], $unsubscribeMatches2[1], $messages[0]->getHtmlBody());
|
||||
}
|
||||
|
||||
public function testSendEmailWithContactWithInvalidClickthrough(): void
|
||||
{
|
||||
$segment = $this->createSegment('Segment A', 'seg-a');
|
||||
$this->createContacts(1, $segment);
|
||||
$content = '<!DOCTYPE html><htm><body><a href="https://localhost/">link</a>
|
||||
<a id="{unsubscribe_url}">unsubscribe here</a>
|
||||
<a href="{resubscribe_url}">resubscribe here</a>
|
||||
</body></html>';
|
||||
$email = $this->createEmail(
|
||||
'test subject',
|
||||
[$segment->getId() => $segment],
|
||||
$content
|
||||
);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(
|
||||
Request::METHOD_POST,
|
||||
'/s/ajax?action=email:sendBatch',
|
||||
['id' => $email->getId(), 'pending' => 1]
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful($response->getContent());
|
||||
Assert::assertSame(
|
||||
'{"success":1,"percent":100,"progress":[1,1],"stats":{"sent":1,"failed":0,"failedRecipients":[]}}',
|
||||
$response->getContent()
|
||||
);
|
||||
|
||||
$message = self::getMailerMessagesByToAddress('contact-flood-0@doe.com')[0];
|
||||
|
||||
$body = quoted_printable_decode($message->getBody()->bodyToString());
|
||||
preg_match('/<a href=\"([^\"]*)\">(.*)<\/a>/iU', $body, $match);
|
||||
Assert::assertArrayHasKey(1, $match, $body);
|
||||
$urlParts = parse_url($match[1]);
|
||||
$queryParams = [];
|
||||
parse_str($urlParts['query'], $queryParams);
|
||||
self::assertArrayHasKey('ct', $queryParams);
|
||||
$queryParams['ct'] = substr($queryParams['ct'], 0, -5);
|
||||
|
||||
// Log out and call as an anonymous.
|
||||
$this->client->followRedirects(false);
|
||||
$this->logoutUser();
|
||||
|
||||
// Do not request an absolute URL in tests.
|
||||
$uri = $urlParts['path'];
|
||||
$this->client->request(Request::METHOD_GET, $uri, $queryParams);
|
||||
$this->client->getResponse();
|
||||
self::assertResponseRedirects('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, LeadList> $segments
|
||||
*/
|
||||
private function createEmail(string $subject, array $segments, string $emailContent): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setDateAdded(new \DateTime());
|
||||
$email->setName('Email name');
|
||||
$email->setSubject($subject);
|
||||
$email->setEmailType('list');
|
||||
$email->setLists($segments);
|
||||
$email->setTemplate('Blank');
|
||||
$email->setCustomHtml($emailContent);
|
||||
$this->em->persist($email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Lead>
|
||||
*/
|
||||
private function createContacts(int $count, LeadList $segment): array
|
||||
{
|
||||
$contacts = [];
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
$contact = new Lead();
|
||||
$email = "contact-flood-{$i}@doe.com";
|
||||
$contact->setEmail($email);
|
||||
$this->em->persist($contact);
|
||||
|
||||
$this->addContactToSegment($segment, $contact);
|
||||
$contacts[$email] = $contact;
|
||||
}
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
private function createSegment(string $name, string $alias): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName($name);
|
||||
$segment->setPublicName($name);
|
||||
$segment->setAlias($alias);
|
||||
$this->em->persist($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
private function addContactToSegment(LeadList $segment, Lead $lead): void
|
||||
{
|
||||
$listLead = new ListLead();
|
||||
$listLead->setLead($lead);
|
||||
$listLead->setList($segment);
|
||||
$listLead->setDateAdded(new \DateTime());
|
||||
|
||||
$this->em->persist($listLead);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PreviewFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
private const PREHEADER_TEXT = 'Preheader text';
|
||||
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
public function testPreviewPage(): void
|
||||
{
|
||||
$lead = $this->createLead('John', 'Doe', 'test@domain.tld');
|
||||
$email = $this->createEmail();
|
||||
$this->em->flush();
|
||||
|
||||
$url = "/email/preview/{$email->getId()}";
|
||||
$urlWithContact = "{$url}?contactId={$lead->getId()}";
|
||||
$contentNoContactInfo = 'Contact emails is [Email]';
|
||||
$contentWithContactInfo = sprintf('Contact emails is %s', $lead->getEmail());
|
||||
|
||||
// Admin user
|
||||
$this->assertPageContent($url, $contentNoContactInfo, self::PREHEADER_TEXT);
|
||||
$this->assertPageContent($urlWithContact, $contentWithContactInfo, self::PREHEADER_TEXT);
|
||||
|
||||
$this->logoutUser();
|
||||
|
||||
// Anonymous visitor
|
||||
$this->assertPageContent($url, $contentNoContactInfo, self::PREHEADER_TEXT);
|
||||
$this->assertPageContent($urlWithContact, $contentNoContactInfo, self::PREHEADER_TEXT);
|
||||
}
|
||||
|
||||
private function assertPageContent(string $url, string ...$expectedContents): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, $url);
|
||||
self::assertResponseIsSuccessful();
|
||||
foreach ($expectedContents as $expectedContent) {
|
||||
self::assertStringContainsString($expectedContent, $crawler->text());
|
||||
}
|
||||
}
|
||||
|
||||
private function createEmail(bool $publicPreview = true): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setDateAdded(new \DateTime());
|
||||
$email->setName('Email name');
|
||||
$email->setSubject('Email subject');
|
||||
$email->setTemplate('Blank');
|
||||
$email->setPublicPreview($publicPreview);
|
||||
$email->setCustomHtml('<html><body>Contact emails is {contactfield=email}</body></html>');
|
||||
$email->setPreheaderText(self::PREHEADER_TEXT);
|
||||
$this->em->persist($email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
public function testPreviewEmailWithCorrectDCVariationFilterSegmentMembership(): void
|
||||
{
|
||||
$segment1 = $this->createSegment('Segment 1');
|
||||
$segment2 = $this->createSegment('Segment 2');
|
||||
$lead = $this->createLead('John', 'Doe', 'test@domain.tld');
|
||||
$this->addLeadToSegment($lead, $segment1);
|
||||
$email = $this->createEmail();
|
||||
|
||||
$email->setDynamicContent([
|
||||
[
|
||||
'tokenName' => 'Dynamic Content 1',
|
||||
'content' => '<p>Default Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => '<p>Variation 1</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'object' => 'lead',
|
||||
'type' => 'leadlist',
|
||||
'filter' => [
|
||||
$segment1->getId(),
|
||||
$segment2->getId(),
|
||||
],
|
||||
'display' => 'Segment Membership',
|
||||
'operator' => 'in',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$email->setCustomHtml('<html><body>{dynamiccontent="Dynamic Content 1"}</body></html>');
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
$url = "/email/preview/{$email->getId()}";
|
||||
$urlWithContact = "{$url}?contactId={$lead->getId()}";
|
||||
$contentNoContactInfo = 'Default Dynamic Content';
|
||||
$contentWithContactInfo = 'Variation 1';
|
||||
|
||||
// Admin user
|
||||
$this->assertPageContent($url, $contentNoContactInfo, self::PREHEADER_TEXT);
|
||||
$this->assertPageContent($urlWithContact, $contentWithContactInfo, self::PREHEADER_TEXT);
|
||||
|
||||
$this->logoutUser();
|
||||
|
||||
// Anonymous visitor
|
||||
$this->assertPageContent($url, $contentNoContactInfo, self::PREHEADER_TEXT);
|
||||
$this->assertPageContent($urlWithContact, $contentNoContactInfo, self::PREHEADER_TEXT);
|
||||
}
|
||||
|
||||
public function testPreviewEmailForDynamicContentVariantsWithCustomField(): void
|
||||
{
|
||||
// Create custom field
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/fields/contact/new',
|
||||
[
|
||||
'label' => 'bool',
|
||||
'type' => 'boolean',
|
||||
'properties' => [
|
||||
'no' => 'No',
|
||||
'yes' => 'Yes',
|
||||
],
|
||||
]
|
||||
);
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
self::assertJson($this->client->getResponse()->getContent());
|
||||
|
||||
// Create some contacts
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/contacts/batch/new',
|
||||
[
|
||||
[
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'A',
|
||||
'email' => 'john.a@email.com',
|
||||
'bool' => true,
|
||||
],
|
||||
[
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'B',
|
||||
'email' => 'john.b@email.com',
|
||||
'bool' => false,
|
||||
],
|
||||
[
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'C',
|
||||
'email' => 'john.c@email.com',
|
||||
'bool' => null,
|
||||
],
|
||||
]
|
||||
);
|
||||
self::assertResponseStatusCodeSame(201, $this->client->getResponse()->getContent());
|
||||
$contacts = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
// Create email with dynamic content variant
|
||||
$email = $this->createEmail();
|
||||
$dynamicContent = [
|
||||
[
|
||||
'tokenName' => 'Dynamic Content 1',
|
||||
'content' => '<p>Default Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'tokenName' => 'Dynamic Content 2',
|
||||
'content' => '<p>Default Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'content' => '<p>Variant 1 Dynamic Content</p>',
|
||||
'filters' => [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'bool',
|
||||
'object' => 'lead',
|
||||
'type' => 'boolean',
|
||||
'filter' => '1',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$email->setCustomHtml('<html><body><div>{dynamiccontent="Dynamic Content 2"}</div></body></html>');
|
||||
$email->setDynamicContent($dynamicContent);
|
||||
$this->em->flush();
|
||||
|
||||
$url = "/email/preview/{$email->getId()}";
|
||||
$defaultContent = 'Default Dynamic Content';
|
||||
$variantContent = 'Variant 1 Dynamic Content';
|
||||
|
||||
// Admin user with contact preview - show variant content - true filter matches
|
||||
$urlWithContact1 = "{$url}?contactId={$contacts['contacts'][0]['id']}";
|
||||
$this->assertPageContent($urlWithContact1, $variantContent);
|
||||
|
||||
// Admin user with contact preview - show variant content - false filter doesn't matches
|
||||
$urlWithContact2 = "{$url}?contactId={$contacts['contacts'][1]['id']}";
|
||||
$this->assertPageContent($urlWithContact2, $defaultContent);
|
||||
|
||||
// Admin user with contact preview - show variant content - null filter doesn't matches
|
||||
$urlWithContact3 = "{$url}?contactId={$contacts['contacts'][2]['id']}";
|
||||
$this->assertPageContent($urlWithContact3, $defaultContent);
|
||||
|
||||
$this->logoutUser();
|
||||
|
||||
// Non admin user - show default content
|
||||
$this->assertPageContent($url, $defaultContent);
|
||||
|
||||
// Non admin user with contact preview - show default content
|
||||
$urlWithContact1 = "{$url}?contactId={$contacts['contacts'][0]['id']}";
|
||||
$this->assertPageContent($urlWithContact1, $defaultContent);
|
||||
}
|
||||
|
||||
public function testPreviewEmailWithInvalidIdThrows404Error(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/email/preview/5009');
|
||||
self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
self::assertStringContainsString('404 Not Found - Requested URL not found: /email/preview/5009', $crawler->text());
|
||||
}
|
||||
|
||||
private function createSegment(string $name = 'Segment 1'): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName($name);
|
||||
$segment->setPublicName($name);
|
||||
$segment->setAlias(strtolower($name));
|
||||
$segment->isPublished(true);
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
private function addLeadToSegment(Lead $lead, LeadList $segment): ListLead
|
||||
{
|
||||
$listLead = new ListLead();
|
||||
$listLead->setLead($lead);
|
||||
$listLead->setList($segment);
|
||||
$listLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($listLead);
|
||||
$this->em->flush();
|
||||
|
||||
return $listLead;
|
||||
}
|
||||
|
||||
public function testPreviewEmailForContactWithPrimaryCompany(): void
|
||||
{
|
||||
$company = $this->createCompany('Mautic', 'hello@mautic.org');
|
||||
$company->setCity('Pune');
|
||||
$company->setCountry('India');
|
||||
|
||||
$this->em->persist($company);
|
||||
|
||||
$lead = $this->createLead('John', 'Doe', 'test@domain.tld');
|
||||
$lead->setCompany($company->getName());
|
||||
$this->em->persist($lead);
|
||||
|
||||
$this->createPrimaryCompanyForLead($lead, $company);
|
||||
|
||||
$email = $this->createEmail();
|
||||
$email->setCustomHtml('<html><body>Contact emails is {contactfield=email}. Company Name: {contactfield=companyname} and Company City: {contactfield=companycity}</body></html>');
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$url = "/email/preview/{$email->getId()}";
|
||||
$urlWithContact = "{$url}?contactId={$lead->getId()}";
|
||||
$contentNoContactInfo = 'Contact emails is [Email]. Company Name: [Company Name] and Company City: [City]';
|
||||
$contentWithContactInfo = sprintf('Contact emails is %s. Company Name: %s and Company City: %s', $lead->getEmail(), $company->getName(), $company->getCity());
|
||||
|
||||
// Admin user
|
||||
$this->assertPageContent($url, $contentNoContactInfo);
|
||||
$this->assertPageContent($urlWithContact, $contentWithContactInfo);
|
||||
|
||||
$this->logoutUser();
|
||||
|
||||
// Anonymous visitor
|
||||
$this->assertPageContent($url, $contentNoContactInfo);
|
||||
$this->assertPageContent($urlWithContact, $contentNoContactInfo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class PreviewSettingsFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testPreviewSettingsAllEnabled(): void
|
||||
{
|
||||
$emailMain = new Email();
|
||||
$emailMain->setIsPublished(true);
|
||||
$emailMain->setDateAdded(new \DateTime());
|
||||
$emailMain->setName('Preview settings test');
|
||||
$emailMain->setSubject('email-main');
|
||||
$emailMain->setTemplate('Blank');
|
||||
$emailMain->setCustomHtml('Test Html');
|
||||
$emailMain->setLanguage('en');
|
||||
|
||||
$this->em->persist($emailMain);
|
||||
$this->em->flush();
|
||||
|
||||
$mainPageId = $emailMain->getId();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/emails');
|
||||
self::assertStringContainsString($emailMain->getName(), $crawler->text());
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/view/{$mainPageId}");
|
||||
|
||||
// Translation choice is not visible
|
||||
self::assertCount(
|
||||
0,
|
||||
$crawler->filterXPath('//*[@id="content_preview_settings_translation"]')
|
||||
);
|
||||
|
||||
// Variant choice is not visible
|
||||
self::assertCount(
|
||||
0,
|
||||
$crawler->filterXPath('//*[@id="content_preview_settings_variant"]')
|
||||
);
|
||||
|
||||
// Contact lookup is not visible
|
||||
self::assertCount(
|
||||
1,
|
||||
$crawler->filterXPath('//*[@id="content_preview_settings_contact"]')
|
||||
);
|
||||
|
||||
$emailTranslated = new Email();
|
||||
$emailTranslated->setIsPublished(true);
|
||||
$emailTranslated->setDateAdded(new \DateTime());
|
||||
$emailTranslated->setName('Preview settings test - NL translation');
|
||||
$emailTranslated->setSubject('page-trans-nl');
|
||||
$emailTranslated->setTemplate('Blank');
|
||||
$emailTranslated->setCustomHtml('Test Html');
|
||||
$emailTranslated->setLanguage('nl_CW');
|
||||
|
||||
// Add translation relationship to main page
|
||||
$emailMain->addTranslationChild($emailTranslated);
|
||||
$emailTranslated->setTranslationParent($emailMain);
|
||||
|
||||
$emailVariant = new Email();
|
||||
$emailVariant->setIsPublished(true);
|
||||
$emailVariant->setDateAdded(new \DateTime());
|
||||
$emailVariant->setName('Preview settings test - B variant');
|
||||
$emailVariant->setSubject('page-variant-b');
|
||||
$emailVariant->setTemplate('Blank');
|
||||
$emailVariant->setCustomHtml('Test Html');
|
||||
$emailVariant->setLanguage('en');
|
||||
|
||||
// Add variant relationship to main page
|
||||
$emailMain->addVariantChild($emailVariant);
|
||||
|
||||
$this->em->persist($emailMain);
|
||||
$this->em->persist($emailTranslated);
|
||||
$this->em->persist($emailVariant);
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/emails/view/{$mainPageId}");
|
||||
|
||||
// Translation choice is visible
|
||||
self::assertCount(
|
||||
1,
|
||||
$crawler->filterXPath('//*[@id="content_preview_settings_translation"]')
|
||||
);
|
||||
|
||||
self::assertCount(
|
||||
1,
|
||||
$crawler->filterXPath('//*[@id="content_preview_settings_translation"]/option[@value="'.$emailTranslated->getId().'"]')
|
||||
);
|
||||
|
||||
// Variant choice is visible
|
||||
self::assertCount(
|
||||
1,
|
||||
$crawler->filterXPath('//*[@id="content_preview_settings_variant"]')
|
||||
);
|
||||
|
||||
self::assertCount(
|
||||
1,
|
||||
$crawler->filterXPath('//*[@id="content_preview_settings_variant"]/option[@value="'.$emailVariant->getId().'"]')
|
||||
);
|
||||
|
||||
// Contact lookup is visible
|
||||
self::assertCount(
|
||||
1,
|
||||
$crawler->filterXPath('//*[@id="content_preview_settings_contact"]')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\EmailEvents;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\EmailBundle\Event\TransportWebhookEvent;
|
||||
use Mautic\FormBundle\Entity\Form;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\DoNotContactRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PublicControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $leadId;
|
||||
|
||||
/**
|
||||
* Tests that use the classic unsubscribe page. Not preference center.
|
||||
*/
|
||||
private const UNSUBSCRIBE_TESTS = [
|
||||
'testUnsubscribeWithEmailStat',
|
||||
'testUnsubscribeEmail',
|
||||
];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['show_contact_segments'] = 0;
|
||||
$this->configParams['show_contact_frequency'] = 0;
|
||||
$this->configParams['show_contact_pause_dates'] = 0;
|
||||
$this->configParams['show_contact_categories'] = 0;
|
||||
$this->configParams['show_contact_preferred_channels'] = 0;
|
||||
|
||||
if (in_array($this->name(), self::UNSUBSCRIBE_TESTS)) {
|
||||
$this->configParams['show_contact_preferences'] = 0;
|
||||
} else {
|
||||
$this->configParams['show_contact_preferences'] = 1;
|
||||
}
|
||||
|
||||
if (in_array($this->name(), ['testContactPreferencesSaveMessage'])) {
|
||||
$this->configParams['show_contact_segments'] = 1;
|
||||
$this->configParams['show_contact_frequency'] = 1;
|
||||
$this->configParams['show_contact_pause_dates'] = 1;
|
||||
$this->configParams['show_contact_categories'] = 1;
|
||||
$this->configParams['show_contact_preferred_channels'] = 1;
|
||||
}
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testMailerCallbackWhenNoTransportProccessesIt(): void
|
||||
{
|
||||
$this->client->request('POST', '/mailer/callback');
|
||||
|
||||
Assert::assertSame(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode());
|
||||
Assert::assertSame('No email transport that could process this callback was found', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testMailerCallbackWhenTransportDoesNotProccessIt(): void
|
||||
{
|
||||
self::getContainer()->get('event_dispatcher')->addListener(EmailEvents::ON_TRANSPORT_WEBHOOK, fn () => null /* exists but does nothing */);
|
||||
$this->client->request('POST', '/mailer/callback');
|
||||
|
||||
Assert::assertSame(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode());
|
||||
Assert::assertSame('No email transport that could process this callback was found', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testMailerCallbackWhenTransportProccessesIt(): void
|
||||
{
|
||||
self::getContainer()->get('event_dispatcher')->addListener(EmailEvents::ON_TRANSPORT_WEBHOOK, fn (TransportWebhookEvent $event) => $event->setResponse(new Response('OK')));
|
||||
$this->client->request('POST', '/mailer/callback');
|
||||
|
||||
Assert::assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
Assert::assertSame('OK', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testUnsubscribeFormActionWithoutTheme(): void
|
||||
{
|
||||
$form = $this->getForm(null);
|
||||
|
||||
$stat = $this->getStat($form);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request('GET', '/email/unsubscribe/'.$stat->getTrackingHash());
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent(), true));
|
||||
|
||||
self::assertStringContainsString('form/submit?formId='.$stat->getEmail()->getUnsubscribeForm()->getId(), $crawler->filter('form')->eq(0)->attr('action'));
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
}
|
||||
|
||||
public function testContactPreferencesLandingPageTracking(): void
|
||||
{
|
||||
$this->logoutUser();
|
||||
$lead = $this->createLead();
|
||||
$preferenceCenterPage = $this->getPreferencesCenterLandingPage();
|
||||
$stat = $this->getStat(null, $lead, $preferenceCenterPage);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', '/email/unsubscribe/'.$stat->getTrackingHash());
|
||||
|
||||
$this->em->clear(Page::class);
|
||||
|
||||
$entity = $this->em->getRepository(Page::class)->getEntity($stat->getEmail()->getPreferenceCenter()->getId());
|
||||
$this->assertSame(1, $entity->getHits(), $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testContactPreferencesSaveMessage(): void
|
||||
{
|
||||
$lead = $this->createLead();
|
||||
$stat = $this->getStat(null, $lead);
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request('GET', '/email/unsubscribe/'.$stat->getTrackingHash());
|
||||
|
||||
self::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
$form = $crawler->filter('form')->form();
|
||||
|
||||
// Unsubscribe from email.
|
||||
$form->setValues(['lead_contact_frequency_rules[lead_channels][subscribed_channels][0]' => false]);
|
||||
|
||||
$this->assertStringContainsString('/email/unsubscribe/tracking_hash_unsubscribe_form_email', $form->getUri());
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
self::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$this->assertEquals(1, $crawler->filter('#success-message-text')->count());
|
||||
$expectedMessage = static::getContainer()->get('translator')->trans('mautic.email.preferences_center_success_message.text');
|
||||
$this->assertEquals($expectedMessage, trim($crawler->filter('#success-message-text')->text(null, false)));
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
// Assert that the contact has the DNC record now.
|
||||
$dncRepository = $this->em->getRepository(DoNotContact::class);
|
||||
\assert($dncRepository instanceof DoNotContactRepository);
|
||||
$dncRecords = $dncRepository->findBy(['lead' => $lead->getId()]);
|
||||
Assert::assertCount(1, $dncRecords);
|
||||
Assert::assertSame(DoNotContact::UNSUBSCRIBED, $dncRecords[0]->getReason());
|
||||
Assert::assertSame('email', $dncRecords[0]->getChannel());
|
||||
Assert::assertSame($stat->getEmail()->getId(), $dncRecords[0]->getChannelId());
|
||||
}
|
||||
|
||||
public function testUnsubscribeFormActionWithThemeWithoutFormSupport(): void
|
||||
{
|
||||
$form = $this->getForm('aurora');
|
||||
|
||||
$stat = $this->getStat($form);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request('GET', '/email/unsubscribe/'.$stat->getTrackingHash());
|
||||
|
||||
self::assertStringContainsString('form/submit?formId='.$stat->getEmail()->getUnsubscribeForm()->getId(), $crawler->filter('form')->eq(0)->attr('action'));
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
}
|
||||
|
||||
public function testUnsubscribeFormActionWithThemeWithFormSupport(): void
|
||||
{
|
||||
$form = $this->getForm('blank');
|
||||
|
||||
$stat = $this->getStat($form);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request('GET', '/email/unsubscribe/'.$stat->getTrackingHash());
|
||||
|
||||
self::assertStringContainsString('form/submit?formId='.$stat->getEmail()->getUnsubscribeForm()->getId(), $crawler->filter('form')->eq(0)->attr('action'));
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
}
|
||||
|
||||
public function testWithoutUnsubscribeFormAction(): void
|
||||
{
|
||||
$this->getForm('blank');
|
||||
|
||||
$stat = $this->getStat();
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request('GET', '/email/unsubscribe/'.$stat->getTrackingHash());
|
||||
|
||||
self::assertStringNotContainsString('form/submit?formId=', $crawler->html());
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
}
|
||||
|
||||
public function testOneClickUnsubscribeAction(): void
|
||||
{
|
||||
$lead = $this->createLead();
|
||||
$stat = $this->getStat(null, $lead);
|
||||
$this->em->flush();
|
||||
$this->client->request('POST', '/email/unsubscribe/'.$stat->getTrackingHash(), [
|
||||
'List-Unsubscribe' => 'One-Click',
|
||||
]);
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
$dncCollection = $stat->getLead()->getDoNotContact();
|
||||
$this->assertEquals(1, $dncCollection->count());
|
||||
$this->assertEquals(DoNotContact::UNSUBSCRIBED, $dncCollection->first()->getReason());
|
||||
}
|
||||
|
||||
public function testUnsubscribeActionWithCustomPreferenceCenterHasCsrfToken(): void
|
||||
{
|
||||
$this->logoutUser();
|
||||
$lead = $this->createLead();
|
||||
$preferencesCenter = $this->createCustomPreferencesPage('{segmentlist}{saveprefsbutton}');
|
||||
$stat = $this->getStat(null, $lead, $preferencesCenter);
|
||||
$this->em->flush();
|
||||
$crawler = $this->client->request('GET', '/email/unsubscribe/'.$stat->getTrackingHash());
|
||||
$this->assertResponseIsSuccessful();
|
||||
$tokenInput = $crawler->filter('input[name="lead_contact_frequency_rules[_token]"]');
|
||||
$this->assertEquals(1, $tokenInput->count(), $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
private function getPreferencesCenterLandingPage(): Page
|
||||
{
|
||||
$page = new Page();
|
||||
$page->setTitle('Preference center');
|
||||
$page->setAlias('Preference-center');
|
||||
$page->setIsPublished(true);
|
||||
$page->setIsPreferenceCenter(true);
|
||||
$page->setCustomHtml('<html><body>{saveprefsbutton}</body></html>');
|
||||
$this->em->persist($page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
public function testUnsubscribeFormActionWithUsingLandingPageWithoutContactLocale(): void
|
||||
{
|
||||
$lead = $this->createLead();
|
||||
$page = $this->createPage();
|
||||
|
||||
$stat = $this->getStat(null, $lead, $page);
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request('GET', '/email/unsubscribe/'.$stat->getTrackingHash());
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
$this->assertStringContainsString('Save preferences', $crawler->html());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{contactLocale: string|null, pageLocale: string|null, expectedLocale: string}>
|
||||
*/
|
||||
public static function dataForTestUnsubscribeFormActionWithUsingLandingPage(): iterable
|
||||
{
|
||||
yield 'No page or contact locale, default to "en"' => [
|
||||
'contactLocale' => null,
|
||||
'pageLocale' => null,
|
||||
'expectedLocale' => 'en',
|
||||
];
|
||||
|
||||
yield 'Page locale is set, default to page locale' => [
|
||||
'contactLocale' => null,
|
||||
'pageLocale' => 'de',
|
||||
'expectedLocale' => 'de',
|
||||
];
|
||||
|
||||
yield 'Contact locale is set, default to contact locale' => [
|
||||
'contactLocale' => 'de',
|
||||
'pageLocale' => null,
|
||||
'expectedLocale' => 'de',
|
||||
];
|
||||
|
||||
yield 'Contact locale overrides page locale' => [
|
||||
'contactLocale' => 'fr',
|
||||
'pageLocale' => 'de',
|
||||
'expectedLocale' => 'fr',
|
||||
];
|
||||
|
||||
yield 'Both locales same, use shared locale' => [
|
||||
'contactLocale' => 'fr',
|
||||
'pageLocale' => 'fr',
|
||||
'expectedLocale' => 'fr',
|
||||
];
|
||||
|
||||
yield 'Invalid page locale, fallback to contact locale' => [
|
||||
'contactLocale' => 'de',
|
||||
'pageLocale' => 'xx', // Assume 'xx' is not a valid locale
|
||||
'expectedLocale' => 'de',
|
||||
];
|
||||
|
||||
yield 'Invalid contact locale, fallback to page locale' => [
|
||||
'contactLocale' => 'yy', // Assume 'yy' is not a valid locale
|
||||
'pageLocale' => 'fr',
|
||||
'expectedLocale' => 'fr',
|
||||
];
|
||||
|
||||
yield 'Both locales invalid, fallback to default "en"' => [
|
||||
'contactLocale' => 'zz', // Assume 'zz' is not a valid locale
|
||||
'pageLocale' => 'xx', // Assume 'xx' is not a valid locale
|
||||
'expectedLocale' => 'en',
|
||||
];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataForTestUnsubscribeFormActionWithUsingLandingPage')]
|
||||
public function testUnsubscribeFormActionWithUsingLandingPage(?string $contactLocale, ?string $pageLocale, string $expectedLocale): void
|
||||
{
|
||||
$lead = $this->createLead($contactLocale);
|
||||
$page = $this->createPage($pageLocale);
|
||||
|
||||
$stat = $this->getStat(null, $lead, $page);
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request('GET', '/email/unsubscribe/'.$stat->getTrackingHash());
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$translator = static::getContainer()->get('translator');
|
||||
$needle = $translator->trans('mautic.page.form.saveprefs', [], null, $expectedLocale);
|
||||
|
||||
$this->assertStringContainsString($needle, $crawler->html());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
protected function getStat(?Form $form = null, ?Lead $lead = null, ?Page $preferenceCenter = null): Stat
|
||||
{
|
||||
$trackingHash = 'tracking_hash_unsubscribe_form_email';
|
||||
$emailName = 'Test unsubscribe form email';
|
||||
|
||||
$email = new Email();
|
||||
$email->setName($emailName);
|
||||
$email->setSubject($emailName);
|
||||
$email->setEmailType('template');
|
||||
$email->setUnsubscribeForm($form);
|
||||
$email->setPreferenceCenter($preferenceCenter);
|
||||
$this->em->persist($email);
|
||||
|
||||
// Create a test email stat.
|
||||
$stat = new Stat();
|
||||
$stat->setTrackingHash($trackingHash);
|
||||
$stat->setEmailAddress('john@doe.email');
|
||||
$stat->setLead($lead);
|
||||
$stat->setDateSent(new \DateTime());
|
||||
$stat->setEmail($email);
|
||||
$this->em->persist($stat);
|
||||
|
||||
return $stat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
protected function getForm(?string $formTemplate): Form
|
||||
{
|
||||
$formName = 'unsubscribe_test_form';
|
||||
|
||||
$form = new Form();
|
||||
$form->setName($formName);
|
||||
$form->setAlias($formName);
|
||||
$form->setTemplate($formTemplate);
|
||||
$this->em->persist($form);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
protected function createLead(?string $locale = null): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setEmail('john@doe.email');
|
||||
$lead->addUpdatedField('preferred_locale', $locale);
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
protected function createCustomPreferencesPage(string $html = ''): Page
|
||||
{
|
||||
$page = new Page();
|
||||
$page->setTitle('Contact Preferences');
|
||||
$page->setAlias('contact-preferences');
|
||||
$page->setTemplate('blank');
|
||||
$page->setIsPreferenceCenter(true);
|
||||
$page->setIsPublished(true);
|
||||
$page->setCustomHtml($html);
|
||||
$this->em->persist($page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
protected function createPage(?string $locale = ''): Page
|
||||
{
|
||||
$page = new Page();
|
||||
$page->setTitle('Page:Page:LandingPagePrefCenter');
|
||||
$page->setAlias('page-page-landingPagePrefCenter');
|
||||
$page->setIsPublished(true);
|
||||
$page->setTemplate('blank');
|
||||
$page->setCustomHtml('<h1>Preference center page</h1><br>{saveprefsbutton}');
|
||||
$page->setIsPreferenceCenter(true);
|
||||
|
||||
if ($locale) {
|
||||
$page->setLanguage($locale);
|
||||
}
|
||||
|
||||
$this->em->persist($page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
public function testPreviewDisabledByDefault(): void
|
||||
{
|
||||
$emailName = 'Test preview email';
|
||||
|
||||
$email = new Email();
|
||||
$email->setName($emailName);
|
||||
$email->setSubject($emailName);
|
||||
$email->setEmailType('template');
|
||||
$email->setCustomHtml('some content');
|
||||
$this->em->persist($email);
|
||||
|
||||
$this->client->request('GET', '/email/preview/'.$email->getId());
|
||||
$this->assertTrue($this->client->getResponse()->isNotFound(), $this->client->getResponse()->getContent());
|
||||
|
||||
$email->setPublicPreview(true);
|
||||
$this->em->persist($email);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', '/email/preview/'.$email->getId());
|
||||
$this->assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testPreviewForExpiredEmailForAnonymousUser(): void
|
||||
{
|
||||
$this->logoutUser();
|
||||
$emailName = 'Test preview email';
|
||||
|
||||
$email = new Email();
|
||||
$email->setName($emailName);
|
||||
$email->setSubject($emailName);
|
||||
$email->setPublishUp(new \DateTime('-2 day'));
|
||||
$email->setPublishDown(new \DateTime('-1 day'));
|
||||
$email->setEmailType('template');
|
||||
$email->setCustomHtml('some content');
|
||||
$email->setPublicPreview(true);
|
||||
$this->em->persist($email);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', '/email/preview/'.$email->getId());
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testUnsubscribeEmail(): void
|
||||
{
|
||||
foreach ($this->getUnsubscribeProvider() as $parameters) {
|
||||
$this->runTestUnsubscribeAction(...$parameters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function runTestUnsubscribeAction(
|
||||
string $statHash,
|
||||
string $email,
|
||||
string $emailHash,
|
||||
string $message,
|
||||
bool $addedRow,
|
||||
): void {
|
||||
$uri = '/email/unsubscribe/'.$statHash.'/'.$email.'/'.$emailHash;
|
||||
$this->client->request(Request::METHOD_GET, $uri);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$this->assertStringContainsString($message, $clientResponse->getContent());
|
||||
$doNotContacts = $this->em->getRepository(DoNotContact::class)->findBy(['lead' => $this->leadId]);
|
||||
$isAddedDoNotContact = (bool) count($doNotContacts);
|
||||
$addedDoNotContact = $isAddedDoNotContact ? $doNotContacts[0] : null;
|
||||
$this->assertSame($addedRow, $isAddedDoNotContact);
|
||||
// Cleaning
|
||||
if ($isAddedDoNotContact) {
|
||||
$this->em->remove($addedDoNotContact);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array<string|bool>>
|
||||
*
|
||||
* @throws ORMException
|
||||
*
|
||||
* @see self::testUnsubscribeEmail()
|
||||
*/
|
||||
private function getUnsubscribeProvider(): array
|
||||
{
|
||||
// Emails
|
||||
$wrongEmail = 'test@mautictest.sk';
|
||||
$rightEmail = 'test@mautictest.cz';
|
||||
$lead = new Lead();
|
||||
$lead->setEmail($rightEmail);
|
||||
$this->em->persist($lead);
|
||||
// Email hash
|
||||
$coreParametersHelper = self::getContainer()->get('mautic.helper.core_parameters');
|
||||
$configSecretEmailHash = $coreParametersHelper->get('secret_key');
|
||||
$rightHashForWrongEmail = hash_hmac('sha256', $wrongEmail, $configSecretEmailHash);
|
||||
$rightHashForRightEmail = hash_hmac('sha256', $rightEmail, $configSecretEmailHash);
|
||||
$wrongHash = hash_hmac('sha256', 'wrong', $configSecretEmailHash);
|
||||
// Stat hash
|
||||
$wrongStatHash = 'wrong';
|
||||
$rightStatHash = 'right';
|
||||
$stat = new Stat();
|
||||
$stat->setTrackingHash($rightStatHash);
|
||||
$stat->setLead($lead);
|
||||
$stat->setEmailAddress($rightEmail);
|
||||
$stat->setDateSent(new \DateTime());
|
||||
$this->em->persist($stat);
|
||||
// Flush
|
||||
$this->em->flush();
|
||||
$this->leadId = $lead->getId();
|
||||
|
||||
return [
|
||||
'ok' => [
|
||||
$rightStatHash,
|
||||
$rightEmail,
|
||||
$rightHashForRightEmail,
|
||||
'We are sorry to see you go!',
|
||||
true,
|
||||
],
|
||||
'ok_right_stat_hash' => [
|
||||
$rightStatHash,
|
||||
$wrongEmail,
|
||||
$wrongHash,
|
||||
'We are sorry to see you go!',
|
||||
true,
|
||||
],
|
||||
'ok_right_email_and_hash' => [
|
||||
$wrongStatHash,
|
||||
$rightEmail,
|
||||
$rightHashForRightEmail,
|
||||
'We are sorry to see you go!',
|
||||
true,
|
||||
],
|
||||
'ko_right_email_and_wrong_hash' => [
|
||||
$wrongStatHash,
|
||||
$rightEmail,
|
||||
$wrongHash,
|
||||
'Record not found',
|
||||
false,
|
||||
],
|
||||
'ko_wrong_email_and_right_hash' => [
|
||||
$wrongStatHash,
|
||||
$wrongEmail,
|
||||
$rightHashForWrongEmail,
|
||||
'Record not found',
|
||||
false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testUnsubscribeNotFoundEmailStat(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/email/unsubscribe/non-existant-hash');
|
||||
Assert::assertStringContainsString(
|
||||
'Record not found.',
|
||||
strip_tags((string) $this->client->getResponse()->getContent())
|
||||
);
|
||||
Assert::assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testUnsubscribeWithEmailStat(): void
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName('Email A');
|
||||
$email->setSubject('Email A Subject');
|
||||
$email->setEmailType('template');
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('john@doe.email');
|
||||
$emailStat = new Stat();
|
||||
$emailStat->setEmail($email);
|
||||
$emailStat->setLead($contact);
|
||||
$emailStat->setEmailAddress($contact->getEmail());
|
||||
$emailStat->setDateSent(new \DateTime());
|
||||
$emailStat->setTrackingHash('existing-tracking-hash');
|
||||
$this->em->persist($email);
|
||||
$this->em->persist($contact);
|
||||
$this->em->persist($emailStat);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/email/unsubscribe/existing-tracking-hash');
|
||||
|
||||
Assert::assertStringContainsString(
|
||||
'We are sorry to see you go! john@doe.email will no longer receive emails from us. If this was by mistake, click here to re-subscribe.',
|
||||
strip_tags((string) $this->client->getResponse()->getContent())
|
||||
);
|
||||
Assert::assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
/** @var DoNotContactRepository $dncRepository */
|
||||
$dncRepository = $this->em->getRepository(DoNotContact::class);
|
||||
|
||||
/** @var DoNotContact[] $dncRecords */
|
||||
$dncRecords = $dncRepository->findAll();
|
||||
|
||||
Assert::assertCount(1, $dncRecords);
|
||||
Assert::assertSame($contact->getId(), $dncRecords[0]->getLead()->getId());
|
||||
Assert::assertSame('email', $dncRecords[0]->getChannel());
|
||||
Assert::assertSame((int) $email->getId(), (int) $dncRecords[0]->getChannelId());
|
||||
Assert::assertSame('User unsubscribed.', $dncRecords[0]->getComments());
|
||||
}
|
||||
|
||||
public function testUnsubscribeAllFromPreferencesPage(): void
|
||||
{
|
||||
// Create a lead and email stat
|
||||
$lead = $this->createLead();
|
||||
$stat = $this->getStat(null, $lead);
|
||||
$this->em->flush();
|
||||
|
||||
// Get the unsubscribe page
|
||||
$crawler = $this->client->request('GET', '/email/unsubscribe/'.$stat->getTrackingHash());
|
||||
|
||||
// Assert that the response is OK
|
||||
self::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
// Assert that the link for unsubscribe all exists
|
||||
$unsubscribeAllLink = $crawler->filter('a[href^="/email/dnc/"]')->first();
|
||||
$this->assertCount(1, $unsubscribeAllLink, 'Unsubscribe all link not found');
|
||||
$href = $unsubscribeAllLink->attr('href');
|
||||
|
||||
// Click the link for unsubscribe all
|
||||
$this->client->request('GET', $href);
|
||||
|
||||
// Assert that the response is OK
|
||||
self::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
// Assert that the response contains the expected string
|
||||
$this->assertStringContainsString(
|
||||
'We are sorry to see you go! john@doe.email will no longer receive emails from us',
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
|
||||
// Assert that a DoNotContact record was created
|
||||
/** @var DoNotContactRepository $dncRepository */
|
||||
$dncRepository = $this->em->getRepository(DoNotContact::class);
|
||||
|
||||
/** @var DoNotContact[] $dncRecords */
|
||||
$dncRecords = $dncRepository->findBy(['lead' => $lead]);
|
||||
|
||||
$this->assertCount(1, $dncRecords, 'Expected one DoNotContact record');
|
||||
$this->assertEquals(DoNotContact::UNSUBSCRIBED, $dncRecords[0]->getReason(), 'Expected reason to be UNSUBSCRIBED');
|
||||
$this->assertEquals('email', $dncRecords[0]->getChannel(), 'Expected channel to be email');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user