Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\EmailRepository;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadCategory;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class EmailRepositoryFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private EmailRepository $emailRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
/** @var EmailRepository $repository */
|
||||
$repository = $this->em->getRepository(Email::class);
|
||||
|
||||
$this->emailRepository = $repository;
|
||||
}
|
||||
|
||||
public function testGetDoNotEmailListEmpty(): void
|
||||
{
|
||||
$result = $this->emailRepository->getDoNotEmailList();
|
||||
|
||||
Assert::assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testGetDoNotEmailListNotEmpty(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setEmail('name@domain.tld');
|
||||
$this->em->persist($lead);
|
||||
|
||||
$doNotContact = new DoNotContact();
|
||||
$doNotContact->setLead($lead);
|
||||
$doNotContact->setDateAdded(new \DateTime());
|
||||
$doNotContact->setChannel('email');
|
||||
$this->em->persist($doNotContact);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
// no $leadIds
|
||||
$result = $this->emailRepository->getDoNotEmailList();
|
||||
Assert::assertSame([$lead->getId() => $lead->getEmail()], $result);
|
||||
|
||||
// matching $leadIds
|
||||
$result = $this->emailRepository->getDoNotEmailList([$lead->getId()]);
|
||||
Assert::assertSame([$lead->getId() => $lead->getEmail()], $result);
|
||||
|
||||
// mismatching $leadIds
|
||||
$result = $this->emailRepository->getDoNotEmailList([-1]);
|
||||
Assert::assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testCheckDoNotEmailNonExistent(): void
|
||||
{
|
||||
$result = $this->emailRepository->checkDoNotEmail('name@domain.tld');
|
||||
|
||||
Assert::assertFalse($result);
|
||||
}
|
||||
|
||||
public function testCheckDoNotEmailExistent(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setEmail('name@domain.tld');
|
||||
$this->em->persist($lead);
|
||||
|
||||
$doNotContact = new DoNotContact();
|
||||
$doNotContact->setLead($lead);
|
||||
$doNotContact->setDateAdded(new \DateTime());
|
||||
$doNotContact->setChannel('email');
|
||||
$doNotContact->setReason(1);
|
||||
$doNotContact->setComments('Some comment');
|
||||
$this->em->persist($doNotContact);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->emailRepository->checkDoNotEmail('name@domain.tld');
|
||||
Assert::assertNotFalse($result);
|
||||
|
||||
Assert::assertSame([
|
||||
'id' => (string) $doNotContact->getId(),
|
||||
'unsubscribed' => true,
|
||||
'bounced' => false,
|
||||
'manual' => false,
|
||||
'comments' => $doNotContact->getComments(),
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testGetEmailPendingQueryWithSubscribedCategory(): void
|
||||
{
|
||||
// create some leads
|
||||
$leadOne = $this->createLead('one');
|
||||
$leadTwo = $this->createLead('two');
|
||||
$leadThree = $this->createLead('three');
|
||||
$leadFour = $this->createLead('four');
|
||||
|
||||
// create some categories
|
||||
$catOne = $this->createCategory('one');
|
||||
$catTwo = $this->createCategory('two');
|
||||
$catThree = $this->createCategory('three');
|
||||
|
||||
// lead to subscribe categories
|
||||
$this->subscribeCategory($leadOne, true, $catOne, $catTwo);
|
||||
$this->subscribeCategory($leadTwo, true, $catOne, $catThree);
|
||||
$this->subscribeCategory($leadThree, true, $catTwo, $catThree);
|
||||
$this->subscribeCategory($leadFour, true, $catOne, $catThree);
|
||||
|
||||
// lead to unsubscribe categories
|
||||
$this->subscribeCategory($leadOne, false, $catThree);
|
||||
|
||||
$sourceListOne = $this->createLeadList('Source', $leadOne, $leadTwo, $leadThree, $leadFour);
|
||||
|
||||
// create an email with included/excluded lists
|
||||
$email = new Email();
|
||||
$email->setName('Email');
|
||||
$email->setSubject('Subject');
|
||||
$email->setEmailType('list');
|
||||
$email->addList($sourceListOne);
|
||||
$email->setCategory($catThree);
|
||||
$this->em->persist($email);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$result = $this->emailRepository->getEmailPendingQuery($email->getId())
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$actualLeadIds = array_map('intval', array_column($result, 'id'));
|
||||
sort($actualLeadIds);
|
||||
|
||||
$expectedLeadIds = [$leadTwo->getId(), $leadThree->getId(), $leadFour->getId()];
|
||||
sort($expectedLeadIds);
|
||||
|
||||
$this->assertSame($expectedLeadIds, $actualLeadIds);
|
||||
}
|
||||
|
||||
public function testGetEmailPendingQueryWithExcludedLists(): void
|
||||
{
|
||||
// create some leads
|
||||
$leadOne = $this->createLead('one');
|
||||
$leadTwo = $this->createLead('two');
|
||||
$leadThree = $this->createLead('three');
|
||||
$leadFour = $this->createLead('four');
|
||||
$leadFive = $this->createLead('five');
|
||||
$leadSix = $this->createLead('six');
|
||||
|
||||
// add some leads in lists for inclusion
|
||||
$sourceListOne = $this->createLeadList('Source', $leadOne, $leadTwo, $leadThree);
|
||||
$sourceListTwo = $this->createLeadList('Source', $leadOne, $leadFour, $leadFive, $leadSix);
|
||||
|
||||
// add some leads in lists for exclusion
|
||||
$excludeListOne = $this->createLeadList('Exclude', $leadTwo, $leadSix);
|
||||
$excludeListTwo = $this->createLeadList('Exclude', $leadTwo, $leadThree);
|
||||
|
||||
// create an email with included/excluded lists
|
||||
$email = new Email();
|
||||
$email->setName('Email');
|
||||
$email->setSubject('Subject');
|
||||
$email->setEmailType('list');
|
||||
$email->addList($sourceListOne);
|
||||
$email->addList($sourceListTwo);
|
||||
$email->addExcludedList($excludeListOne);
|
||||
$email->addExcludedList($excludeListTwo);
|
||||
$this->em->persist($email);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$actualLeadIds = $this->emailRepository->getEmailPendingQuery($email->getId())
|
||||
->executeQuery()
|
||||
->fetchFirstColumn();
|
||||
sort($actualLeadIds);
|
||||
|
||||
$expectedLeadIds = [$leadOne->getId(), $leadFour->getId(), $leadFive->getId()];
|
||||
$expectedLeadIds = array_map(fn (int $id) => (string) $id, $expectedLeadIds);
|
||||
sort($expectedLeadIds);
|
||||
|
||||
Assert::assertSame($expectedLeadIds, $actualLeadIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createLead(string $lastName): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setLastname($lastName);
|
||||
$lead->setEmail(sprintf('%s@mail.tld', $lastName));
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead ...$leads
|
||||
*
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createLeadList(string $name, ...$leads): LeadList
|
||||
{
|
||||
$leadList = new LeadList();
|
||||
$leadList->setName($name);
|
||||
$leadList->setPublicName($name);
|
||||
$leadList->setAlias(mb_strtolower($name));
|
||||
$this->em->persist($leadList);
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$this->addLeadToList($lead, $leadList);
|
||||
}
|
||||
|
||||
return $leadList;
|
||||
}
|
||||
|
||||
private function addLeadToList(Lead $leadOne, LeadList $sourceList): void
|
||||
{
|
||||
$listLead = new ListLead();
|
||||
$listLead->setLead($leadOne);
|
||||
$listLead->setList($sourceList);
|
||||
$listLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($listLead);
|
||||
}
|
||||
|
||||
private function createCategory(string $string): Category
|
||||
{
|
||||
$category = new Category();
|
||||
$category->setTitle('Category '.$string);
|
||||
$category->setAlias('category-'.$string);
|
||||
$category->setBundle('global');
|
||||
$this->em->persist($category);
|
||||
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Category ...$categories
|
||||
*/
|
||||
private function subscribeCategory(Lead $lead, bool $subscribed, ...$categories): void
|
||||
{
|
||||
foreach ($categories as $category) {
|
||||
$leadCategory = new LeadCategory();
|
||||
$leadCategory->setLead($lead);
|
||||
$leadCategory->setCategory($category);
|
||||
$leadCategory->setDateAdded(new \DateTime());
|
||||
$leadCategory->setManuallyAdded($subscribed);
|
||||
$leadCategory->setManuallyRemoved(!$subscribed);
|
||||
$this->em->persist($leadCategory);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\EmailRepository;
|
||||
|
||||
class EmailRepositoryIncrementReadTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
private QueryBuilder $queryBuilder;
|
||||
|
||||
private QueryBuilder $subQueryBuilder;
|
||||
|
||||
/**
|
||||
* @var EmailRepository|object
|
||||
*/
|
||||
private $repo;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->repo = $this->configureRepository(Email::class);
|
||||
$this->queryBuilder = new QueryBuilder($this->connection);
|
||||
$this->subQueryBuilder = new QueryBuilder($this->connection);
|
||||
$this->connection->method('createQueryBuilder')->willReturnOnConsecutiveCalls(
|
||||
$this->queryBuilder,
|
||||
$this->subQueryBuilder
|
||||
);
|
||||
}
|
||||
|
||||
public function testIncrementRead(): void
|
||||
{
|
||||
$this->connection
|
||||
->expects($this->exactly(1))
|
||||
->method('executeStatement')
|
||||
->willReturn(1);
|
||||
|
||||
$this->repo->incrementRead(11, '21');
|
||||
$generatedSql = $this->queryBuilder->getSQL();
|
||||
|
||||
// Assert that the generated SQL matches our expectations
|
||||
$expectedSql = 'UPDATE test_emails e SET read_count = read_count + 1 WHERE (e.id = :emailId) AND (e.id NOT IN (SELECT es.email_id FROM test_email_stats es WHERE (es.id = :statId) AND (es.is_read = 1)))';
|
||||
$this->assertEquals($expectedSql, $generatedSql);
|
||||
}
|
||||
|
||||
public function testIncrementReadWithVariant(): void
|
||||
{
|
||||
$this->connection
|
||||
->expects($this->exactly(1))
|
||||
->method('executeStatement')
|
||||
->willReturn(1);
|
||||
|
||||
$this->repo->incrementRead(11, '21', true);
|
||||
$generatedSql = $this->queryBuilder->getSQL();
|
||||
|
||||
// Assert that the generated SQL matches our expectations
|
||||
$expectedSql = 'UPDATE test_emails e SET read_count = read_count + 1, variant_read_count = variant_read_count + 1 WHERE (e.id = :emailId) AND (e.id NOT IN (SELECT es.email_id FROM test_email_stats es WHERE (es.id = :statId) AND (es.is_read = 1)))';
|
||||
$this->assertEquals($expectedSql, $generatedSql);
|
||||
}
|
||||
|
||||
public function testUpCountWithTwoErrors(): void
|
||||
{
|
||||
$this->connection
|
||||
->expects($this->exactly(3))
|
||||
->method('executeStatement')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
$this->throwException(new DBALException()),
|
||||
$this->throwException(new DBALException()),
|
||||
1
|
||||
);
|
||||
|
||||
$this->repo->incrementRead(45, '616');
|
||||
}
|
||||
|
||||
public function testUpCountWithFourErrors(): void
|
||||
{
|
||||
$this->connection
|
||||
->expects($this->exactly(3))
|
||||
->method('executeStatement')
|
||||
->will($this->throwException(new DBALException()));
|
||||
|
||||
$this->expectException(DBALException::class);
|
||||
$this->repo->incrementRead(45, '616');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\EmailRepository;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class EmailRepositoryTest extends TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
private EmailRepository $repo;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->repo = $this->configureRepository(Email::class);
|
||||
$this->connection->method('createQueryBuilder')->willReturnCallback(fn () => new QueryBuilder($this->connection));
|
||||
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$translator->method('trans')->willReturnCallback(fn ($id) => match ($id) {
|
||||
'mautic.email.email.searchcommand.isexpired' => 'is:expired',
|
||||
'mautic.email.email.searchcommand.ispending' => 'is:pending',
|
||||
default => $id,
|
||||
});
|
||||
$this->repo->setTranslator($translator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $variantIds
|
||||
* @param int[] $excludedListIds
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataGetEmailPendingQueryForCount')]
|
||||
public function testGetEmailPendingQueryForCount(?array $variantIds, bool $countWithMaxMin, array $excludedListIds, string $expectedQuery): void
|
||||
{
|
||||
$this->mockExcludedListIds($excludedListIds);
|
||||
|
||||
$emailId = 5;
|
||||
$listIds = [22, 33];
|
||||
$countOnly = true;
|
||||
$limit = null;
|
||||
$minContactId = null;
|
||||
$maxContactId = null;
|
||||
|
||||
$query = $this->repo->getEmailPendingQuery(
|
||||
$emailId,
|
||||
$variantIds,
|
||||
$listIds,
|
||||
$countOnly,
|
||||
$limit,
|
||||
$minContactId,
|
||||
$maxContactId,
|
||||
$countWithMaxMin
|
||||
);
|
||||
|
||||
$this->assertEquals($this->replaceQueryPrefix($expectedQuery), $query->getSql());
|
||||
$this->assertEquals(['false' => false], $query->getParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<mixed[]>
|
||||
*/
|
||||
public static function dataGetEmailPendingQueryForCount(): iterable
|
||||
{
|
||||
yield [null, false, [], "SELECT count(*) as count FROM test_leads l WHERE (l.id IN (SELECT ll.lead_id FROM test_lead_lists_leads ll WHERE (ll.lead_id = l.id) AND (ll.leadlist_id IN (22, 33)) AND (ll.manually_removed = :false))) AND (l.id NOT IN (SELECT dnc.lead_id FROM test_lead_donotcontact dnc WHERE (dnc.lead_id = l.id) AND (dnc.channel = 'email'))) AND (l.id NOT IN (SELECT stat.lead_id FROM test_email_stats stat WHERE (stat.lead_id IS NOT NULL) AND (stat.email_id = 5))) AND (l.id NOT IN (SELECT mq.lead_id FROM test_message_queue mq WHERE (mq.lead_id = l.id) AND (mq.status <> 'sent') AND (mq.channel = 'email') AND (mq.channel_id = 5))) AND (l.id NOT IN (SELECT lc.lead_id FROM test_lead_categories lc INNER JOIN test_emails e ON e.category_id = lc.category_id WHERE (e.id = 5) AND (lc.manually_removed = 1))) AND ((l.email IS NOT NULL) AND (l.email <> ''))"];
|
||||
yield [[6], false, [16], "SELECT count(*) as count FROM test_leads l WHERE (l.id IN (SELECT ll.lead_id FROM test_lead_lists_leads ll WHERE (ll.lead_id = l.id) AND (ll.leadlist_id IN (22, 33)) AND (ll.manually_removed = :false))) AND (l.id NOT IN (SELECT dnc.lead_id FROM test_lead_donotcontact dnc WHERE (dnc.lead_id = l.id) AND (dnc.channel = 'email'))) AND (l.id NOT IN (SELECT stat.lead_id FROM test_email_stats stat WHERE (stat.lead_id IS NOT NULL) AND (stat.email_id IN (6, 5)))) AND (l.id NOT IN (SELECT mq.lead_id FROM test_message_queue mq WHERE (mq.lead_id = l.id) AND (mq.status <> 'sent') AND (mq.channel = 'email') AND (mq.channel_id IN (6, 5)))) AND (l.id NOT IN (SELECT lc.lead_id FROM test_lead_categories lc INNER JOIN test_emails e ON e.category_id = lc.category_id WHERE (e.id = 5) AND (lc.manually_removed = 1))) AND ((l.email IS NOT NULL) AND (l.email <> ''))"];
|
||||
yield [null, true, [9, 7], "SELECT count(*) as count, MIN(l.id) as min_id, MAX(l.id) as max_id FROM test_leads l WHERE (l.id IN (SELECT ll.lead_id FROM test_lead_lists_leads ll WHERE (ll.lead_id = l.id) AND (ll.leadlist_id IN (22, 33)) AND (ll.manually_removed = :false))) AND (l.id NOT IN (SELECT dnc.lead_id FROM test_lead_donotcontact dnc WHERE (dnc.lead_id = l.id) AND (dnc.channel = 'email'))) AND (l.id NOT IN (SELECT stat.lead_id FROM test_email_stats stat WHERE (stat.lead_id IS NOT NULL) AND (stat.email_id = 5))) AND (l.id NOT IN (SELECT mq.lead_id FROM test_message_queue mq WHERE (mq.lead_id = l.id) AND (mq.status <> 'sent') AND (mq.channel = 'email') AND (mq.channel_id = 5))) AND (l.id NOT IN (SELECT lc.lead_id FROM test_lead_categories lc INNER JOIN test_emails e ON e.category_id = lc.category_id WHERE (e.id = 5) AND (lc.manually_removed = 1))) AND ((l.email IS NOT NULL) AND (l.email <> ''))"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $excludedListIds
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataGetEmailPendingQueryForMaxMinIdCountWithMaxMinIdsDefined')]
|
||||
public function testGetEmailPendingQueryForMaxMinIdCountWithMaxMinIdsDefined(array $excludedListIds, string $expectedQuery): void
|
||||
{
|
||||
$this->mockExcludedListIds($excludedListIds);
|
||||
|
||||
$emailId = 5;
|
||||
$variantIds = null;
|
||||
$listIds = [22, 33];
|
||||
$countOnly = true;
|
||||
$limit = null;
|
||||
$minContactId = 10;
|
||||
$maxContactId = 1000;
|
||||
$countWithMaxMin = true;
|
||||
|
||||
$query = $this->repo->getEmailPendingQuery(
|
||||
$emailId,
|
||||
$variantIds,
|
||||
$listIds,
|
||||
$countOnly,
|
||||
$limit,
|
||||
$minContactId,
|
||||
$maxContactId,
|
||||
$countWithMaxMin
|
||||
);
|
||||
|
||||
$expectedParams = [
|
||||
'false' => false,
|
||||
'minContactId' => 10,
|
||||
'maxContactId' => 1000,
|
||||
];
|
||||
|
||||
$this->assertEquals($this->replaceQueryPrefix($expectedQuery), $query->getSql());
|
||||
$this->assertEquals($expectedParams, $query->getParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<mixed[]>
|
||||
*/
|
||||
public static function dataGetEmailPendingQueryForMaxMinIdCountWithMaxMinIdsDefined(): iterable
|
||||
{
|
||||
yield [[], "SELECT count(*) as count, MIN(l.id) as min_id, MAX(l.id) as max_id FROM test_leads l WHERE (l.id IN (SELECT ll.lead_id FROM test_lead_lists_leads ll WHERE (ll.lead_id = l.id) AND (ll.leadlist_id IN (22, 33)) AND (ll.manually_removed = :false))) AND (l.id NOT IN (SELECT dnc.lead_id FROM test_lead_donotcontact dnc WHERE (dnc.lead_id = l.id) AND (dnc.channel = 'email'))) AND (l.id NOT IN (SELECT stat.lead_id FROM test_email_stats stat WHERE (stat.lead_id IS NOT NULL) AND (stat.email_id = 5))) AND (l.id NOT IN (SELECT mq.lead_id FROM test_message_queue mq WHERE (mq.lead_id = l.id) AND (mq.status <> 'sent') AND (mq.channel = 'email') AND (mq.channel_id = 5))) AND (l.id NOT IN (SELECT lc.lead_id FROM test_lead_categories lc INNER JOIN test_emails e ON e.category_id = lc.category_id WHERE (e.id = 5) AND (lc.manually_removed = 1))) AND (l.id >= :minContactId) AND (l.id <= :maxContactId) AND ((l.email IS NOT NULL) AND (l.email <> ''))"];
|
||||
yield [[96, 98, 103], "SELECT count(*) as count, MIN(l.id) as min_id, MAX(l.id) as max_id FROM test_leads l WHERE (l.id IN (SELECT ll.lead_id FROM test_lead_lists_leads ll WHERE (ll.lead_id = l.id) AND (ll.leadlist_id IN (22, 33)) AND (ll.manually_removed = :false))) AND (l.id NOT IN (SELECT dnc.lead_id FROM test_lead_donotcontact dnc WHERE (dnc.lead_id = l.id) AND (dnc.channel = 'email'))) AND (l.id NOT IN (SELECT stat.lead_id FROM test_email_stats stat WHERE (stat.lead_id IS NOT NULL) AND (stat.email_id = 5))) AND (l.id NOT IN (SELECT mq.lead_id FROM test_message_queue mq WHERE (mq.lead_id = l.id) AND (mq.status <> 'sent') AND (mq.channel = 'email') AND (mq.channel_id = 5))) AND (l.id NOT IN (SELECT lc.lead_id FROM test_lead_categories lc INNER JOIN test_emails e ON e.category_id = lc.category_id WHERE (e.id = 5) AND (lc.manually_removed = 1))) AND (l.id >= :minContactId) AND (l.id <= :maxContactId) AND ((l.email IS NOT NULL) AND (l.email <> ''))"];
|
||||
}
|
||||
|
||||
public function testGetUniqueCliks(): void
|
||||
{
|
||||
$queryBuilder = $this->createMock(QueryBuilder::class);
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('select')
|
||||
->with('SUM( tr.unique_hits) as `unique_clicks`')
|
||||
->willReturnSelf();
|
||||
|
||||
$resultMock = $this->createMock(Result::class);
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->willReturn($resultMock);
|
||||
|
||||
$resultMock->expects($this->once())
|
||||
->method('fetchOne')
|
||||
->willReturn(10);
|
||||
|
||||
$repository = $this->getMockBuilder(EmailRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['addTrackableTablesForEmailStats'])
|
||||
->getMock();
|
||||
|
||||
$result = $repository->getUniqueClicks($queryBuilder);
|
||||
|
||||
$this->assertEquals(10, $result);
|
||||
}
|
||||
|
||||
public function testGetUnsubscribedCount(): void
|
||||
{
|
||||
$queryBuilder = $this->createMock(QueryBuilder::class);
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('resetQueryParts')
|
||||
->with(['join'])
|
||||
->willReturnSelf();
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('select')
|
||||
->with('e.id as email_id, dnc.lead_id')
|
||||
->willReturnSelf();
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('andWhere')
|
||||
->with('dnc.reason='.DoNotContact::UNSUBSCRIBED)
|
||||
->willReturnSelf();
|
||||
|
||||
$resultMock = $this->createMock(Result::class);
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->willReturn($resultMock);
|
||||
|
||||
$resultMock->expects($this->once())
|
||||
->method('rowCount')
|
||||
->willReturn(5);
|
||||
|
||||
$repository = $this->getMockBuilder(EmailRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['addDNCTableForEmails'])
|
||||
->getMock();
|
||||
|
||||
$result = $repository->getUnsubscribedCount($queryBuilder);
|
||||
|
||||
$this->assertEquals(5, $result);
|
||||
}
|
||||
|
||||
public function testGetSentReadNotReadCount(): void
|
||||
{
|
||||
$queryBuilder = $this->createMock(QueryBuilder::class);
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('resetQueryPart')
|
||||
->with('groupBy')
|
||||
->willReturnSelf();
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('resetQueryParts')
|
||||
->with(['join'])
|
||||
->willReturnSelf();
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('select')
|
||||
->with('SUM( e.sent_count) as sent_count, SUM( e.read_count) as read_count')
|
||||
->willReturnSelf();
|
||||
|
||||
$resultMock = $this->createMock(Result::class);
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->willReturn($resultMock);
|
||||
|
||||
$resultMock->expects($this->once())
|
||||
->method('fetchAssociative')
|
||||
->willReturn([
|
||||
'sent_count' => '100',
|
||||
'read_count' => '60',
|
||||
]);
|
||||
|
||||
$result = $this->repo->getSentReadNotReadCount($queryBuilder);
|
||||
|
||||
$this->assertEquals([
|
||||
'sent_count' => 100,
|
||||
'read_count' => 60,
|
||||
'not_read' => 40,
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testGetSentReadNotReadCountEmptyResults(): void
|
||||
{
|
||||
$queryBuilder = $this->createMock(QueryBuilder::class);
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('resetQueryPart')
|
||||
->with('groupBy')
|
||||
->willReturnSelf();
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('resetQueryParts')
|
||||
->with(['join'])
|
||||
->willReturnSelf();
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('select')
|
||||
->with('SUM( e.sent_count) as sent_count, SUM( e.read_count) as read_count')
|
||||
->willReturnSelf();
|
||||
|
||||
$resultMock = $this->createMock(Result::class);
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->willReturn($resultMock);
|
||||
|
||||
$resultMock->expects($this->once())
|
||||
->method('fetchAssociative')
|
||||
->willReturn(false);
|
||||
|
||||
$result = $this->repo->getSentReadNotReadCount($queryBuilder);
|
||||
|
||||
$this->assertEquals([
|
||||
'sent_count' => 0,
|
||||
'read_count' => 0,
|
||||
'not_read' => 0,
|
||||
], $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $excludedListIds
|
||||
*/
|
||||
private function mockExcludedListIds(array $excludedListIds): void
|
||||
{
|
||||
$resultMock = $this->createMock(Result::class);
|
||||
$resultMock->method('fetchAllAssociative')
|
||||
->willReturn(array_map(fn (int $id) => [$id], $excludedListIds));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($resultMock);
|
||||
}
|
||||
|
||||
private function replaceQueryPrefix(string $query): string
|
||||
{
|
||||
return str_replace('{prefix}', MAUTIC_TABLE_PREFIX, $query);
|
||||
}
|
||||
|
||||
public function testAddSearchCommandWhereClauseHandlesExpirationFilters(): void
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$filter = (object) ['command' => 'is:expired', 'string' => '', 'not' => false, 'strict' => false];
|
||||
|
||||
$method = new \ReflectionMethod(EmailRepository::class, 'addSearchCommandWhereClause');
|
||||
$method->setAccessible(true);
|
||||
|
||||
[$expr, $params] = $method->invoke($this->repo, $qb, $filter);
|
||||
|
||||
self::assertSame(
|
||||
'(e.isPublished = :par1 AND e.publishDown IS NOT NULL AND e.publishDown <> \'\' AND e.publishDown < CURRENT_TIMESTAMP())',
|
||||
(string) $expr
|
||||
);
|
||||
self::assertSame(['par1' => true], $params);
|
||||
}
|
||||
|
||||
public function testAddSearchCommandWhereClauseHandlesPendingFilters(): void
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$filter = (object) ['command' => 'is:pending', 'string' => '', 'not' => false, 'strict' => false];
|
||||
|
||||
$method = new \ReflectionMethod(EmailRepository::class, 'addSearchCommandWhereClause');
|
||||
$method->setAccessible(true);
|
||||
|
||||
[$expr, $params] = $method->invoke($this->repo, $qb, $filter);
|
||||
|
||||
self::assertSame(
|
||||
'(e.isPublished = :par1 AND e.publishUp IS NOT NULL AND e.publishUp <> \'\' AND e.publishUp > CURRENT_TIMESTAMP())',
|
||||
(string) $expr
|
||||
);
|
||||
self::assertSame(['par1' => true], $params);
|
||||
}
|
||||
|
||||
public function testGetSearchCommandsContainsExpirationFilters(): void
|
||||
{
|
||||
$commands = $this->repo->getSearchCommands();
|
||||
self::assertContains('mautic.email.email.searchcommand.isexpired', $commands);
|
||||
self::assertContains('mautic.email.email.searchcommand.ispending', $commands);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\EmailRepository;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class EmailRepositoryUpCountSentTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
/**
|
||||
* @var MockObject|QueryBuilder
|
||||
*/
|
||||
private MockObject $queryBuilderMock;
|
||||
|
||||
private QueryBuilder $queryBuilder;
|
||||
|
||||
private EmailRepository $repo;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->repo = $this->configureRepository(Email::class);
|
||||
$this->queryBuilderMock = $this->createMock(QueryBuilder::class);
|
||||
$this->queryBuilder = new QueryBuilder($this->connection);
|
||||
}
|
||||
|
||||
public function testUpCountSentWithNoIncrease(): void
|
||||
{
|
||||
$this->connection->method('createQueryBuilder')->willReturn($this->queryBuilderMock);
|
||||
$this->queryBuilderMock->expects($this->never())
|
||||
->method('update');
|
||||
|
||||
$this->repo->upCountSent(45, 0);
|
||||
}
|
||||
|
||||
public function testUpCountSentWithId(): void
|
||||
{
|
||||
$this->connection->method('createQueryBuilder')->willReturn($this->queryBuilder);
|
||||
|
||||
$this->connection
|
||||
->expects($this->exactly(1))
|
||||
->method('executeStatement')
|
||||
->willReturn(1);
|
||||
|
||||
$this->repo->upCountSent(11);
|
||||
$generatedSql = $this->queryBuilder->getSQL();
|
||||
|
||||
// Assert that the generated SQL matches our expectations
|
||||
$expectedSql = 'UPDATE test_emails SET sent_count = sent_count + :increaseBy WHERE id = :id';
|
||||
$this->assertEquals($expectedSql, $generatedSql);
|
||||
|
||||
// Assert parameters are properly set up
|
||||
$this->assertEquals(11, $this->queryBuilder->getParameter('id'));
|
||||
$this->assertEquals(1, $this->queryBuilder->getParameter('increaseBy'));
|
||||
}
|
||||
|
||||
public function testUpCountWithVariant(): void
|
||||
{
|
||||
$this->connection->method('createQueryBuilder')->willReturn($this->queryBuilder);
|
||||
|
||||
$this->connection
|
||||
->expects($this->exactly(1))
|
||||
->method('executeStatement')
|
||||
->willReturn(1);
|
||||
|
||||
$this->repo->upCountSent(11, 2, true);
|
||||
$generatedSql = $this->queryBuilder->getSQL();
|
||||
|
||||
// Assert that the generated SQL matches our expectations
|
||||
$expectedSql = 'UPDATE test_emails SET sent_count = sent_count + :increaseBy, variant_sent_count = variant_sent_count + :increaseBy WHERE id = :id';
|
||||
$this->assertEquals($expectedSql, $generatedSql);
|
||||
}
|
||||
|
||||
public function testUpCountWithTwoErrors(): void
|
||||
{
|
||||
$this->connection->method('createQueryBuilder')->willReturn($this->queryBuilder);
|
||||
|
||||
$this->connection
|
||||
->expects($this->exactly(3))
|
||||
->method('executeStatement')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
$this->throwException(new DBALException()),
|
||||
$this->throwException(new DBALException()),
|
||||
1
|
||||
);
|
||||
|
||||
$this->repo->upCountSent(45);
|
||||
}
|
||||
|
||||
public function testUpCountWithFourErrors(): void
|
||||
{
|
||||
$this->connection->method('createQueryBuilder')->willReturn($this->queryBuilder);
|
||||
$this->connection
|
||||
->expects($this->exactly(3))
|
||||
->method('executeStatement')
|
||||
->will($this->throwException(new DBALException()));
|
||||
|
||||
$this->expectException(DBALException::class);
|
||||
$this->repo->upCountSent(45);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Entity;
|
||||
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EmailTest extends TestCase
|
||||
{
|
||||
public function testCloneResetPublishDates(): void
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setPublishUp(new \DateTime());
|
||||
$email->setPublishDown(new \DateTime());
|
||||
$emailClone = clone $email;
|
||||
$this->assertNull($emailClone->getPublishUp());
|
||||
$this->assertNull($emailClone->getPublishDown());
|
||||
}
|
||||
|
||||
public function testCloneResetPlainText(): void
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setPlainText('foo');
|
||||
$emailClone = clone $email;
|
||||
$this->assertNull($emailClone->getPlainText());
|
||||
}
|
||||
|
||||
#[DataProvider('setIsDuplicateDataProvider')]
|
||||
public function testIsDuplicate(bool $isDuplicate): void
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setIsDuplicate($isDuplicate);
|
||||
Assert::assertIsBool($email->isDuplicate());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array{bool}>
|
||||
*/
|
||||
public static function setIsDuplicateDataProvider(): iterable
|
||||
{
|
||||
yield [true];
|
||||
yield [false];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
/**
|
||||
* This test ensures that the pending query will work even if a contact was deleted between batches.
|
||||
* After the refactoring from NOT EXISTS to NOT IN the single deleted contact could cause the
|
||||
* pending query to find no contacts due to null value in the lead_id column.
|
||||
*/
|
||||
class PendingQueryFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testDelayedSends(): void
|
||||
{
|
||||
$emailRepository = $this->em->getRepository(Email::class);
|
||||
|
||||
$contactCount = 4;
|
||||
$oneBatchCount = $contactCount / 2;
|
||||
$contacts = $this->generateContacts($contactCount);
|
||||
$batch1 = array_slice($contacts, 0, $oneBatchCount);
|
||||
$segment = $this->createSegment();
|
||||
$email = $this->createEmail($segment);
|
||||
$this->addContactsToSegment($contacts, $segment);
|
||||
|
||||
Assert::assertSame($contactCount, (int) $emailRepository->getEmailPendingLeads($email->getId(), null, null, true));
|
||||
|
||||
$this->emulateEmailSend($email, $batch1);
|
||||
|
||||
Assert::assertSame($oneBatchCount, (int) $emailRepository->getEmailPendingLeads($email->getId(), null, null, true));
|
||||
|
||||
$this->em->remove($batch1[0]);
|
||||
$this->em->flush();
|
||||
|
||||
// The pending count must be the same even if one of the email_stat records has lead_id = null.
|
||||
Assert::assertSame($oneBatchCount, (int) $emailRepository->getEmailPendingLeads($email->getId(), null, null, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead[]
|
||||
*/
|
||||
private function generateContacts(int $howMany): array
|
||||
{
|
||||
$contacts = [];
|
||||
|
||||
for ($i = 0; $i < $howMany; ++$i) {
|
||||
$contact = new Lead();
|
||||
$contact->setEmail("test{$i}@some.email");
|
||||
$contacts[] = $contact;
|
||||
}
|
||||
|
||||
$contactModel = static::getContainer()->get('mautic.lead.model.lead');
|
||||
\assert($contactModel instanceof LeadModel);
|
||||
$contactModel->saveEntities($contacts);
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
private function createSegment(): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName('Segment A');
|
||||
$segment->setPublicName('Segment A');
|
||||
$segment->setAlias('segment-a');
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead[] $contacts
|
||||
*/
|
||||
private function addContactsToSegment(array $contacts, LeadList $segment): void
|
||||
{
|
||||
foreach ($contacts as $contact) {
|
||||
$reference = new ListLead();
|
||||
$reference->setLead($contact);
|
||||
$reference->setList($segment);
|
||||
$reference->setDateAdded(new \DateTime());
|
||||
$this->em->persist($reference);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function createEmail(LeadList $segment): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName('Email A');
|
||||
$email->setSubject('Email A Subject');
|
||||
$email->setEmailType('list');
|
||||
$email->addList($segment);
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead[] $contacts
|
||||
*/
|
||||
private function emulateEmailSend(Email $email, array $contacts): void
|
||||
{
|
||||
foreach ($contacts as $contact) {
|
||||
$emailStat = new Stat();
|
||||
$emailStat->setEmail($email);
|
||||
$emailStat->setEmailAddress($contact->getEmail());
|
||||
$emailStat->setLead($contact);
|
||||
$emailStat->setDateSent(new \DateTime());
|
||||
$this->em->persist($emailStat);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\EmailBundle\Entity\StatRepository;
|
||||
|
||||
final class StatRepositoryTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
private StatRepository $statRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->statRepository = $this->configureRepository(Stat::class);
|
||||
$this->connection->method('createQueryBuilder')->willReturnCallback(fn () => new QueryBuilder($this->connection));
|
||||
}
|
||||
|
||||
public function testGetStatsSummaryForContacts(): void
|
||||
{
|
||||
$expectedQuery = 'SELECT l.id AS `lead_id`, COUNT(es.id) AS `sent_count`, SUM(IF(es.is_read IS NULL, 0, es.is_read)) AS `read_count`, SUM(IF(sq.hits is NULL, 0, 1)) AS `clicked_through_count` FROM '.MAUTIC_TABLE_PREFIX.'email_stats es RIGHT JOIN '.MAUTIC_TABLE_PREFIX.'leads l ON es.lead_id=l.id LEFT JOIN (SELECT COUNT(ph.id) AS hits, COUNT(DISTINCT(ph.redirect_id)) AS unique_hits, cut.channel_id, ph.lead_id FROM '.MAUTIC_TABLE_PREFIX.'channel_url_trackables cut INNER JOIN '.MAUTIC_TABLE_PREFIX."page_hits ph ON cut.redirect_id = ph.redirect_id AND cut.channel_id = ph.source_id WHERE (cut.channel = 'email' AND ph.source = 'email') AND (ph.lead_id in (:contacts)) GROUP BY cut.channel_id, ph.lead_id) sq ON es.email_id = sq.channel_id AND es.lead_id = sq.lead_id WHERE l.id in (:contacts) GROUP BY l.id";
|
||||
|
||||
$this->connection->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->with(
|
||||
$expectedQuery,
|
||||
['contacts' => [6, 8]],
|
||||
['contacts' => 101]
|
||||
)
|
||||
->willReturn($this->result);
|
||||
|
||||
$this->result->method('fetchAllAssociative')
|
||||
->willReturn([
|
||||
[
|
||||
'lead_id' => '6',
|
||||
'sent_count' => '12',
|
||||
'read_count' => '6',
|
||||
'clicked_through_count' => '3',
|
||||
],
|
||||
[
|
||||
'lead_id' => '8',
|
||||
'sent_count' => '13',
|
||||
'read_count' => '7',
|
||||
'clicked_through_count' => '6',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'6' => [
|
||||
'sent_count' => 12,
|
||||
'read_count' => 6,
|
||||
'clicked_count' => 3,
|
||||
'open_rate' => 0.5,
|
||||
'click_through_rate' => 0.25,
|
||||
'click_through_open_rate' => 0.5,
|
||||
],
|
||||
'8' => [
|
||||
'sent_count' => 13,
|
||||
'read_count' => 7,
|
||||
'clicked_count' => 6,
|
||||
'open_rate' => 0.5385,
|
||||
'click_through_rate' => 0.4615,
|
||||
'click_through_open_rate' => 0.8571,
|
||||
],
|
||||
],
|
||||
$this->statRepository->getStatsSummaryForContacts([6, 8])
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetReadCount(): void
|
||||
{
|
||||
$expectedQuery = 'SELECT count(s.id) as count FROM test_email_stats s WHERE (s.email_id IN (1)) AND (is_read = :true) AND (s.date_read BETWEEN :dateFrom AND :dateTo)';
|
||||
$this->connection->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->with(
|
||||
$expectedQuery,
|
||||
[
|
||||
'true' => true,
|
||||
'dateFrom' => '2023-01-01 00:00:00',
|
||||
'dateTo' => '2023-01-31 23:59:59',
|
||||
]
|
||||
)
|
||||
->willReturn($this->result);
|
||||
|
||||
$this->result->method('fetchAllAssociative')
|
||||
->willReturn([
|
||||
[
|
||||
'count' => 1,
|
||||
],
|
||||
]);
|
||||
$query = new ChartQuery($this->connection, new \DateTime('2023-01-01'), new \DateTime('2023-01-31'));
|
||||
|
||||
$this->assertSame(1, $this->statRepository->getReadCount(1, null, $query));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Tests\Entity;
|
||||
|
||||
use Mautic\EmailBundle\Entity\EmailReply;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class StatTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @param int $count How many openDetails to add to the entity
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('addOpenDetailsTestProvider')]
|
||||
public function testAddOpenDetails(int $count): void
|
||||
{
|
||||
$stat = new Stat();
|
||||
|
||||
// Add as many openDetails entries as specified in $count
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
$stat->addOpenDetails(sprintf('Open %d of %d', $i + 1, $count));
|
||||
}
|
||||
|
||||
// Assert that the openCount reflects the total number of openDetails
|
||||
$this->assertEquals($count, $stat->getOpenCount());
|
||||
|
||||
// Assert that the number of entries stored in the openDetails array
|
||||
// is equal to the lower of the two values openCount and
|
||||
// Stat::MAX_OPEN_DETAILS
|
||||
$this->assertEquals(
|
||||
min(Stat::MAX_OPEN_DETAILS, $stat->getOpenCount()),
|
||||
count($stat->getOpenDetails())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for addOpenDetails.
|
||||
*/
|
||||
public static function addOpenDetailsTestProvider(): array
|
||||
{
|
||||
return [
|
||||
'no openDetails' => [0],
|
||||
'one openDetail' => [1],
|
||||
'low number of openDetails' => [10],
|
||||
'one away from threshold' => [Stat::MAX_OPEN_DETAILS - 1],
|
||||
'exactly at threshold' => [Stat::MAX_OPEN_DETAILS],
|
||||
'one past threshold' => [Stat::MAX_OPEN_DETAILS + 1],
|
||||
'slightly above threshold' => [Stat::MAX_OPEN_DETAILS + 10],
|
||||
'well beyond threshold' => [Stat::MAX_OPEN_DETAILS * 10],
|
||||
];
|
||||
}
|
||||
|
||||
public function testChanges(): void
|
||||
{
|
||||
$stat = new Stat();
|
||||
$stat->setEmailAddress('john@doe.email');
|
||||
$stat->setIsFailed(true);
|
||||
$stat->setDateRead(new \DateTime());
|
||||
$stat->setDateSent(new \DateTime());
|
||||
$stat->setLastOpened(new \DateTime());
|
||||
$stat->setIsRead(false);
|
||||
$stat->setOpenCount(2);
|
||||
$stat->setRetryCount(3);
|
||||
$stat->setSource('campaign');
|
||||
$stat->setSourceId(123);
|
||||
$stat->addReply(new EmailReply($stat, '456'));
|
||||
|
||||
Assert::assertSame([null, 'john@doe.email'], $stat->getChanges()['emailAddress']);
|
||||
Assert::assertSame([false, true], $stat->getChanges()['isFailed']);
|
||||
Assert::assertSame([0, 2], $stat->getChanges()['openCount']);
|
||||
Assert::assertSame([0, 3], $stat->getChanges()['retryCount']);
|
||||
Assert::assertSame([null, 'campaign'], $stat->getChanges()['source']);
|
||||
Assert::assertSame([null, 123], $stat->getChanges()['sourceId']);
|
||||
Assert::assertSame([false, true], $stat->getChanges()['replyAdded']);
|
||||
Assert::assertArrayNotHasKey('isRead', $stat->getChanges()); // Don't want to record changes from false to false.
|
||||
Assert::assertNull($stat->getChanges()['dateRead'][0]);
|
||||
Assert::assertInstanceOf(\DateTime::class, $stat->getChanges()['dateRead'][1]);
|
||||
Assert::assertNull($stat->getChanges()['dateSent'][0]);
|
||||
Assert::assertInstanceOf(\DateTime::class, $stat->getChanges()['dateSent'][1]);
|
||||
Assert::assertNull($stat->getChanges()['lastOpened'][0]);
|
||||
Assert::assertInstanceOf(\DateTime::class, $stat->getChanges()['lastOpened'][1]);
|
||||
|
||||
$stat->upOpenCount();
|
||||
$stat->upRetryCount();
|
||||
$stat->setEmailAddress('john@doe.email');
|
||||
$stat->setDateRead(new \DateTime());
|
||||
$stat->setIsRead(true);
|
||||
$stat->setSource('campaign');
|
||||
$stat->setSourceId(321);
|
||||
$stat->addReply(new EmailReply($stat, '456'));
|
||||
|
||||
Assert::assertSame([null, 'john@doe.email'], $stat->getChanges()['emailAddress']);
|
||||
Assert::assertSame([false, true], $stat->getChanges()['isFailed']);
|
||||
Assert::assertSame([2, 3], $stat->getChanges()['openCount']);
|
||||
Assert::assertSame([3, 4], $stat->getChanges()['retryCount']);
|
||||
Assert::assertSame([null, 'campaign'], $stat->getChanges()['source']);
|
||||
Assert::assertSame([123, 321], $stat->getChanges()['sourceId']);
|
||||
Assert::assertSame([false, true], $stat->getChanges()['replyAdded']);
|
||||
Assert::assertSame([false, true], $stat->getChanges()['isRead']);
|
||||
Assert::assertInstanceOf(\DateTime::class, $stat->getChanges()['dateRead'][0]);
|
||||
Assert::assertInstanceOf(\DateTime::class, $stat->getChanges()['dateRead'][1]);
|
||||
Assert::assertNull($stat->getChanges()['dateSent'][0]);
|
||||
Assert::assertInstanceOf(\DateTime::class, $stat->getChanges()['dateSent'][1]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user