Initial commit: CloudOps infrastructure platform

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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