Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Campaign;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\CampaignRepository;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLeads;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository as CampaignLeadsRepository;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
|
||||
abstract class AbstractCampaignTestCase extends MauticMysqlTestCase
|
||||
{
|
||||
protected function saveSomeCampaignLeadEventLogs(bool $withPendingAction = false, bool $withActionOfRemovedLead = false): Campaign
|
||||
{
|
||||
$relativeDate = date('Y-m-d', strtotime('-1 month'));
|
||||
|
||||
/** @var LeadEventLogRepository $leadEventLogRepo */
|
||||
$leadEventLogRepo = $this->em->getRepository(LeadEventLog::class);
|
||||
|
||||
/** @var CampaignRepository $campaignRepo */
|
||||
$campaignRepo = $this->em->getRepository(Campaign::class);
|
||||
|
||||
/** @var LeadRepository $contactRepo */
|
||||
$contactRepo = $this->em->getRepository(Lead::class);
|
||||
|
||||
/** @var CampaignLeadsRepository $campaignLeadsRepo */
|
||||
$campaignLeadsRepo = $this->em->getRepository(CampaignLeads::class);
|
||||
|
||||
$contactA = new Lead();
|
||||
$contactB = new Lead();
|
||||
|
||||
$contactRepo->saveEntities([$contactA, $contactB]);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Campaign ABC');
|
||||
$campaign->setCreatedBy(1);
|
||||
|
||||
$eventA = new Event();
|
||||
$eventA->setName('Event A');
|
||||
$eventA->setType('type.a');
|
||||
$eventA->setEventType('action');
|
||||
$eventA->setCampaign($campaign);
|
||||
|
||||
$eventB = new Event();
|
||||
$eventB->setName('Event B');
|
||||
$eventB->setType('type.b');
|
||||
$eventB->setEventType('action');
|
||||
$eventB->setCampaign($campaign);
|
||||
|
||||
$campaign->addEvent(0, $eventA);
|
||||
$campaign->addEvent(1, $eventB);
|
||||
|
||||
$campaignRepo->saveEntity($campaign);
|
||||
|
||||
$leadEventLogA = new LeadEventLog();
|
||||
$leadEventLogA->setCampaign($campaign);
|
||||
$leadEventLogA->setEvent($eventA);
|
||||
$leadEventLogA->setLead($contactA);
|
||||
$leadEventLogA->setDateTriggered(new \DateTime($relativeDate.' 16:34:00', new \DateTimeZone('UTC')));
|
||||
$leadEventLogA->setRotation(0);
|
||||
|
||||
$leadEventLogB = new LeadEventLog();
|
||||
$leadEventLogB->setCampaign($campaign);
|
||||
$leadEventLogB->setEvent($eventA);
|
||||
$leadEventLogB->setLead($contactB);
|
||||
$leadEventLogB->setDateTriggered(new \DateTime($relativeDate.' 16:54:00', new \DateTimeZone('UTC')));
|
||||
$leadEventLogB->setRotation(0);
|
||||
|
||||
$leadEventLogC = new LeadEventLog();
|
||||
$leadEventLogC->setCampaign($campaign);
|
||||
$leadEventLogC->setEvent($eventB);
|
||||
$leadEventLogC->setLead($contactA);
|
||||
$leadEventLogC->setDateTriggered(new \DateTime($relativeDate.' 16:55:00', new \DateTimeZone('UTC')));
|
||||
$leadEventLogC->setRotation(0);
|
||||
|
||||
$leadEventLogD = new LeadEventLog();
|
||||
$leadEventLogD->setCampaign($campaign);
|
||||
$leadEventLogD->setEvent($eventB);
|
||||
$leadEventLogD->setLead($contactB);
|
||||
$leadEventLogD->setDateTriggered(new \DateTime($relativeDate.' 17:04:00', new \DateTimeZone('UTC')));
|
||||
$leadEventLogD->setRotation(0);
|
||||
|
||||
$leadEventLogRepo->saveEntities([$leadEventLogA, $leadEventLogB, $leadEventLogC, $leadEventLogD]);
|
||||
|
||||
$campaignLeadsA = new CampaignLeads();
|
||||
$campaignLeadsA->setLead($contactA);
|
||||
$campaignLeadsA->setCampaign($campaign);
|
||||
$campaignLeadsA->setDateAdded(new \DateTime($relativeDate));
|
||||
$campaignLeadsA->setRotation(0);
|
||||
$campaignLeadsA->setManuallyRemoved(false);
|
||||
|
||||
$campaignLeadsB = new CampaignLeads();
|
||||
$campaignLeadsB->setLead($contactB);
|
||||
$campaignLeadsB->setCampaign($campaign);
|
||||
$campaignLeadsB->setDateAdded(new \DateTime($relativeDate));
|
||||
$campaignLeadsB->setRotation(0);
|
||||
$campaignLeadsB->setManuallyRemoved(false);
|
||||
|
||||
$campaignLeadsRepo->saveEntities([$campaignLeadsA, $campaignLeadsB]);
|
||||
|
||||
if ($withPendingAction) {
|
||||
$contactC = new Lead();
|
||||
$contactRepo->saveEntity($contactC);
|
||||
|
||||
$leadEventLogE = new LeadEventLog();
|
||||
$leadEventLogE->setCampaign($campaign);
|
||||
$leadEventLogE->setEvent($eventA);
|
||||
$leadEventLogE->setLead($contactC);
|
||||
$leadEventLogE->setDateTriggered(new \DateTime($relativeDate.' 16:34:00', new \DateTimeZone('UTC')));
|
||||
$leadEventLogE->setRotation(0);
|
||||
$leadEventLogRepo->saveEntity($leadEventLogE);
|
||||
|
||||
$leadEventLogF = new LeadEventLog();
|
||||
$leadEventLogF->setCampaign($campaign);
|
||||
$leadEventLogF->setEvent($eventB);
|
||||
$leadEventLogF->setLead($contactC);
|
||||
$leadEventLogF->setDateTriggered(new \DateTime($relativeDate.' 16:34:00', new \DateTimeZone('UTC')));
|
||||
$leadEventLogF->setTriggerDate(new \DateTime($relativeDate.' 16:49:00', new \DateTimeZone('UTC')));
|
||||
$leadEventLogF->setIsScheduled(true);
|
||||
$leadEventLogF->setRotation(0);
|
||||
$leadEventLogRepo->saveEntity($leadEventLogF);
|
||||
|
||||
$campaignLeadsC = new CampaignLeads();
|
||||
$campaignLeadsC->setLead($contactC);
|
||||
$campaignLeadsC->setCampaign($campaign);
|
||||
$campaignLeadsC->setDateAdded(new \DateTime($relativeDate));
|
||||
$campaignLeadsC->setRotation(0);
|
||||
$campaignLeadsC->setManuallyRemoved(false);
|
||||
$campaignLeadsRepo->saveEntity($campaignLeadsC);
|
||||
}
|
||||
|
||||
if ($withActionOfRemovedLead) {
|
||||
$contactD = new Lead();
|
||||
$contactRepo->saveEntity($contactD);
|
||||
|
||||
$leadEventLogG = new LeadEventLog();
|
||||
$leadEventLogG->setCampaign($campaign);
|
||||
$leadEventLogG->setEvent($eventA);
|
||||
$leadEventLogG->setLead($contactD);
|
||||
$leadEventLogG->setDateTriggered(new \DateTime($relativeDate.' 16:34:00', new \DateTimeZone('UTC')));
|
||||
$leadEventLogG->setRotation(0);
|
||||
$leadEventLogRepo->saveEntity($leadEventLogG);
|
||||
|
||||
$campaignLeadsD = new CampaignLeads();
|
||||
$campaignLeadsD->setLead($contactD);
|
||||
$campaignLeadsD->setCampaign($campaign);
|
||||
$campaignLeadsD->setDateAdded(new \DateTime($relativeDate));
|
||||
$campaignLeadsD->setRotation(0);
|
||||
$campaignLeadsD->setManuallyRemoved(true);
|
||||
$campaignLeadsRepo->saveEntity($campaignLeadsD);
|
||||
}
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CampaignBundle\EventCollector\EventCollector;
|
||||
use Mautic\CampaignBundle\Membership\MembershipBuilder;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\FormBundle\Entity\FormRepository;
|
||||
use Mautic\FormBundle\Model\FormModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class CampaignTestAbstract extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
protected static $mockId = 232;
|
||||
|
||||
protected static $mockName = 'Mock name';
|
||||
|
||||
/**
|
||||
* @return CampaignModel
|
||||
*/
|
||||
protected function initCampaignModel()
|
||||
{
|
||||
$entityManager = $this->createMock(EntityManager::class);
|
||||
|
||||
$security = $this->createMock(CorePermissions::class);
|
||||
|
||||
$security->expects($this->any())
|
||||
->method('isGranted')
|
||||
->willReturn(true);
|
||||
|
||||
$userHelper = $this->createMock(UserHelper::class);
|
||||
|
||||
$formRepository = $this->createMock(FormRepository::class);
|
||||
|
||||
$formRepository->expects($this->any())
|
||||
->method('getFormList')
|
||||
->willReturn([['id' => self::$mockId, 'name' => self::$mockName]]);
|
||||
|
||||
$leadListModel = $this->getMockBuilder(ListModel::class)
|
||||
->disableOriginalConstructor()
|
||||
->setConstructorArgs([6 => $entityManager])
|
||||
->getMock();
|
||||
|
||||
$leadListModel->expects($this->any())
|
||||
->method('getUserLists')
|
||||
->willReturn([['id' => self::$mockId, 'name' => self::$mockName]]);
|
||||
|
||||
$formModel = $this->getMockBuilder(FormModel::class)
|
||||
->disableOriginalConstructor()
|
||||
->setConstructorArgs([12 => $entityManager])
|
||||
->getMock();
|
||||
|
||||
$formModel->expects($this->any())
|
||||
->method('getRepository')
|
||||
->willReturn($formRepository);
|
||||
|
||||
$eventCollector = $this->createMock(EventCollector::class);
|
||||
$membershipBuilder = $this->createMock(MembershipBuilder::class);
|
||||
|
||||
$contactTracker = $this->createMock(ContactTracker::class);
|
||||
|
||||
$campaignModel = new CampaignModel(
|
||||
$leadListModel,
|
||||
$formModel,
|
||||
$eventCollector,
|
||||
$membershipBuilder,
|
||||
$contactTracker,
|
||||
$entityManager,
|
||||
$security,
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(UrlGeneratorInterface::class),
|
||||
$this->createMock(Translator::class),
|
||||
$userHelper,
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(CoreParametersHelper::class)
|
||||
);
|
||||
|
||||
return $campaignModel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Command;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\InstallBundle\InstallFixtures\ORM\LeadFieldData;
|
||||
use Mautic\LeadBundle\DataFixtures\ORM\LoadLeadData;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
|
||||
class AbstractCampaignCommand extends MauticMysqlTestCase
|
||||
{
|
||||
public const SEND_EMAIL_SECONDS = 3;
|
||||
|
||||
public const CONDITION_SECONDS = 6;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultClientServer = [];
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $prefix;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
protected $eventDate;
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
// Everything needs to happen anonymously
|
||||
$this->defaultClientServer = $this->clientServer;
|
||||
$this->clientServer = [];
|
||||
|
||||
parent::setUp();
|
||||
|
||||
$this->db = $this->em->getConnection();
|
||||
$this->prefix = static::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
|
||||
// Populate contacts
|
||||
$this->installDatabaseFixtures([LeadFieldData::class, LoadLeadData::class]);
|
||||
|
||||
// Campaigns are so complex that we are going to load a SQL file rather than build with entities
|
||||
$sql = file_get_contents(__DIR__.'/campaign_schema.sql');
|
||||
|
||||
// Update table prefix
|
||||
$sql = str_replace('#__', static::getContainer()->getParameter('mautic.db_table_prefix'), $sql);
|
||||
|
||||
// Schedule event
|
||||
date_default_timezone_set('UTC');
|
||||
$this->eventDate = new \DateTime();
|
||||
$this->eventDate->modify('+'.self::SEND_EMAIL_SECONDS.' seconds');
|
||||
$sql = str_replace('{SEND_EMAIL_1_TIMESTAMP}', $this->eventDate->format('Y-m-d H:i:s'), $sql);
|
||||
|
||||
$this->eventDate->modify('+'.self::CONDITION_SECONDS.' seconds');
|
||||
$sql = str_replace('{CONDITION_TIMESTAMP}', $this->eventDate->format('Y-m-d H:i:s'), $sql);
|
||||
|
||||
$this->em->getConnection()->executeStatement($sql);
|
||||
}
|
||||
|
||||
public function beforeTearDown(): void
|
||||
{
|
||||
$this->clientServer = $this->defaultClientServer;
|
||||
}
|
||||
|
||||
protected function beforeBeginTransaction(): void
|
||||
{
|
||||
$this->resetAutoincrement([
|
||||
'leads',
|
||||
'emails',
|
||||
'lead_tags',
|
||||
'campaigns',
|
||||
'campaign_events',
|
||||
'lead_lists',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getCampaignEventLogs(array $ids)
|
||||
{
|
||||
$logs = $this->db->createQueryBuilder()
|
||||
->select('l.email, l.country, event.name, event.event_type, event.type, log.*')
|
||||
->from($this->prefix.'campaign_lead_event_log', 'log')
|
||||
->join('log', $this->prefix.'campaign_events', 'event', 'event.id = log.event_id')
|
||||
->join('log', $this->prefix.'leads', 'l', 'l.id = log.lead_id')
|
||||
->where('log.campaign_id = 1')
|
||||
->andWhere('log.event_id IN ('.implode(',', $ids).')')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$byEvent = [];
|
||||
foreach ($ids as $id) {
|
||||
$byEvent[$id] = [];
|
||||
}
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$byEvent[$log['event_id']][] = $log;
|
||||
}
|
||||
|
||||
return $byEvent;
|
||||
}
|
||||
|
||||
protected function createLead(string $leadName): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname($leadName);
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
protected function createCampaign(string $campaignName): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName($campaignName);
|
||||
$campaign->setIsPublished(true);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
protected function createCampaignLead(Campaign $campaign, Lead $lead, bool $manuallyRemoved = false): CampaignLead
|
||||
{
|
||||
$campaignLead = new CampaignLead();
|
||||
$campaignLead->setCampaign($campaign);
|
||||
$campaignLead->setLead($lead);
|
||||
$campaignLead->setDateAdded(new \DateTime());
|
||||
$campaignLead->setManuallyRemoved($manuallyRemoved);
|
||||
$this->em->persist($campaignLead);
|
||||
|
||||
return $campaignLead;
|
||||
}
|
||||
|
||||
protected function createSegmentMember(LeadList $segment, Lead $lead): ListLead
|
||||
{
|
||||
$segmentMember = new ListLead();
|
||||
$segmentMember->setLead($lead);
|
||||
$segmentMember->setList($segment);
|
||||
$segmentMember->setDateAdded(new \DateTime());
|
||||
$this->em->persist($segmentMember);
|
||||
|
||||
return $segmentMember;
|
||||
}
|
||||
|
||||
protected function createEvent(string $name, Campaign $campaign, string $type, string $eventType, ?array $property = null): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName($name);
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType($type);
|
||||
$event->setEventType($eventType);
|
||||
$event->setTriggerInterval(1);
|
||||
$event->setProperties($property);
|
||||
$event->setTriggerMode('immediate');
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
protected function createEventLog(Lead $lead, Event $event, Campaign $campaign): LeadEventLog
|
||||
{
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setLead($lead);
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setCampaign($campaign);
|
||||
$leadEventLog->setRotation(0);
|
||||
$this->em->persist($leadEventLog);
|
||||
|
||||
return $leadEventLog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Command\Api;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignMember;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class EventLogApiControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testBatchEditEventsPut(): void
|
||||
{
|
||||
$contact1 = new Lead();
|
||||
$contact1->setEmail('johana@doe.nohama');
|
||||
|
||||
$contact2 = new Lead();
|
||||
$contact2->setEmail('johana@doe.mohana');
|
||||
|
||||
$contact3 = new Lead();
|
||||
$contact3->setEmail('johana@doe.monana');
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test Campaign');
|
||||
|
||||
$campaignMember1 = new CampaignMember();
|
||||
$campaignMember1->setLead($contact1);
|
||||
$campaignMember1->setCampaign($campaign);
|
||||
$campaignMember1->setManuallyAdded(true);
|
||||
$campaignMember1->setDateAdded(new \DateTime());
|
||||
|
||||
$campaignMember2 = new CampaignMember();
|
||||
$campaignMember2->setLead($contact2);
|
||||
$campaignMember2->setCampaign($campaign);
|
||||
$campaignMember2->setManuallyAdded(true);
|
||||
$campaignMember2->setDateAdded(new \DateTime());
|
||||
|
||||
$event1 = new Event();
|
||||
$event1->setCampaign($campaign);
|
||||
$event1->setType('lead.changepoints');
|
||||
$event1->setEventType('action');
|
||||
$event1->setName('Test Event 1');
|
||||
|
||||
$event2 = new Event();
|
||||
$event2->setCampaign($campaign);
|
||||
$event2->setType('lead.changepoints');
|
||||
$event2->setEventType('action');
|
||||
$event2->setName('Test Event 2');
|
||||
|
||||
$event3 = new Event();
|
||||
$event3->setCampaign($campaign);
|
||||
$event3->setType('asset.download');
|
||||
$event3->setEventType('decision');
|
||||
$event3->setName('Test Event 3');
|
||||
|
||||
$campaign->addEvent(0, $event1);
|
||||
$campaign->addEvent(1, $event2);
|
||||
$campaign->addEvent(1, $event3);
|
||||
|
||||
$this->em->persist($contact1);
|
||||
$this->em->persist($contact2);
|
||||
$this->em->persist($contact3);
|
||||
$this->em->persist($event1);
|
||||
$this->em->persist($event2);
|
||||
$this->em->persist($event3);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->persist($campaignMember1);
|
||||
$this->em->persist($campaignMember2);
|
||||
$this->em->flush();
|
||||
$this->em->detach($contact1);
|
||||
$this->em->detach($contact2);
|
||||
$this->em->detach($contact3);
|
||||
$this->em->detach($event1);
|
||||
$this->em->detach($event2);
|
||||
$this->em->detach($event3);
|
||||
$this->em->detach($campaign);
|
||||
$this->em->detach($campaignMember1);
|
||||
$this->em->detach($campaignMember2);
|
||||
|
||||
$payload = [
|
||||
// This will fail because it already has dateTriggered.
|
||||
[
|
||||
'contactId' => $contact1->getId(),
|
||||
'eventId' => $event1->getId(),
|
||||
'dateTriggered' => '2016-01-10 00:00:00',
|
||||
],
|
||||
[
|
||||
'contactId' => $contact2->getId(),
|
||||
'eventId' => $event1->getId(),
|
||||
'triggerDate' => '2017-01-10 00:00:00',
|
||||
],
|
||||
[
|
||||
'contactId' => $contact1->getId(),
|
||||
'eventId' => $event2->getId(),
|
||||
'triggerDate' => '2016-01-11 00:00:00',
|
||||
],
|
||||
[
|
||||
'contactId' => $contact2->getId(),
|
||||
'eventId' => $event2->getId(),
|
||||
'triggerDate' => '2016-01-11 00:00:00',
|
||||
],
|
||||
// This will fail because this contact isn't a campaign member.
|
||||
[
|
||||
'contactId' => $contact3->getId(),
|
||||
'eventId' => $event2->getId(),
|
||||
'triggerDate' => '2017-01-10 00:00:00',
|
||||
],
|
||||
// This will fail because decision cannot be scheduled.
|
||||
[
|
||||
'contactId' => $contact1->getId(),
|
||||
'eventId' => $event3->getId(),
|
||||
'triggerDate' => '2016-01-11 00:00:00',
|
||||
],
|
||||
];
|
||||
|
||||
$this->client->request('PUT', '/api/campaigns/events/batch/edit', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
Assert::assertCount(1, $response['events'][$event1->getId()]['contactLog']);
|
||||
Assert::assertCount(2, $response['events'][$event2->getId()]['contactLog']);
|
||||
Assert::assertCount(0, $response['events'][$event3->getId()]['contactLog']);
|
||||
|
||||
$errorMessages = array_map(
|
||||
fn (array $error) => $error['message'],
|
||||
$response['errors']
|
||||
);
|
||||
|
||||
Assert::assertContains("The event {$event1->getId()} in the campaign {$campaign->getId()} has already been executed at 2016-01-10T00:00:00+00:00 for the contact {$contact2->getId()}.", $errorMessages);
|
||||
Assert::assertContains("The contact {$contact3->getId()} is not in the campaign {$campaign->getId()}.", $errorMessages);
|
||||
Assert::assertContains("A decision type event cannot be scheduled. Event: {$event3->getId()}, campaign: {$campaign->getId()}, contact: {$contact1->getId()}.", $errorMessages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Command;
|
||||
|
||||
use Mautic\CampaignBundle\Command\CampaignDeleteEventLogsCommand;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Tester\ApplicationTester;
|
||||
|
||||
class CampaignDeleteEventLogsCommandFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testWithEventIds(): void
|
||||
{
|
||||
$exitCode = $this->createDataAndRunCommand(false);
|
||||
Assert::assertSame(0, $exitCode);
|
||||
|
||||
$campaign = $this->em->getRepository(Campaign::class)->findAll();
|
||||
Assert::assertCount(1, $campaign);
|
||||
|
||||
$eventLogs = $this->em->getRepository(LeadEventLog::class)->findAll();
|
||||
Assert::assertCount(0, $eventLogs);
|
||||
}
|
||||
|
||||
public function testWithCampaignId(): void
|
||||
{
|
||||
$exitCode = $this->createDataAndRunCommand(true);
|
||||
|
||||
Assert::assertSame(0, $exitCode);
|
||||
|
||||
$campaign = $this->em->getRepository(Campaign::class)->findAll();
|
||||
Assert::assertCount(0, $campaign);
|
||||
|
||||
$eventLogs = $this->em->getRepository(LeadEventLog::class)->findAll();
|
||||
Assert::assertCount(0, $eventLogs);
|
||||
}
|
||||
|
||||
private function createApplicationTester(): ApplicationTester
|
||||
{
|
||||
$application = new Application(self::$kernel);
|
||||
$application->setAutoExit(false);
|
||||
|
||||
return new ApplicationTester($application);
|
||||
}
|
||||
|
||||
private function createDataAndRunCommand(bool $usingCampaign): int
|
||||
{
|
||||
$applicationTester = $this->createApplicationTester();
|
||||
$lead = $this->createLead();
|
||||
$campaign = $this->createCampaign();
|
||||
$event1 = $this->createEvent('Event 1', $campaign);
|
||||
$event2 = $this->createEvent('Event 2', $campaign);
|
||||
$this->createEventLog($lead, $event1);
|
||||
$this->createEventLog($lead, $event2);
|
||||
|
||||
$commandData = ['command' => CampaignDeleteEventLogsCommand::COMMAND_NAME];
|
||||
if ($usingCampaign) {
|
||||
$commandData['--campaign-id'] = $campaign->getId();
|
||||
} else {
|
||||
$commandData['campaign_event_ids'] = [$event1->getId(), $event2->getId()];
|
||||
}
|
||||
|
||||
$exitCode = $applicationTester->run($commandData);
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
private function createLead(): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('My campaign');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createEvent(string $name, Campaign $campaign): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName($name);
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType('email.send');
|
||||
$event->setEventType('action');
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createEventLog(Lead $lead, Event $event): LeadEventLog
|
||||
{
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setLead($lead);
|
||||
$leadEventLog->setEvent($event);
|
||||
$this->em->persist($leadEventLog);
|
||||
$this->em->flush();
|
||||
|
||||
return $leadEventLog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Command;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\Summary;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class CampaignSummarizationFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['campaign_use_summary'] = 'testExecuteCampaignEventWithSummarization' === $this->name();
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testExecuteCampaignEventWithoutSummarization(): void
|
||||
{
|
||||
$this->createDataAndExecuteCommand();
|
||||
$campaignSummary = $this->em->getRepository(Summary::class)->findAll();
|
||||
Assert::assertCount(0, $campaignSummary);
|
||||
}
|
||||
|
||||
public function testExecuteCampaignEventWithSummarization(): void
|
||||
{
|
||||
$this->createDataAndExecuteCommand();
|
||||
$campaignSummary = $this->em->getRepository(Summary::class)->findAll();
|
||||
Assert::assertCount(1, $campaignSummary);
|
||||
}
|
||||
|
||||
private function createDataAndExecuteCommand(): void
|
||||
{
|
||||
$lead = $this->createLead();
|
||||
$campaign = $this->createCampaign();
|
||||
$email = $this->createEmail('Email 1');
|
||||
$properties = [
|
||||
'canvasSettings' => [
|
||||
'droppedX' => '549',
|
||||
'droppedY' => '155',
|
||||
],
|
||||
'name' => '',
|
||||
'triggerMode' => 'immediate',
|
||||
'triggerDate' => null,
|
||||
'triggerInterval' => '1',
|
||||
'triggerIntervalUnit' => 'd',
|
||||
'triggerHour' => '',
|
||||
'triggerRestrictedStartHour' => '',
|
||||
'triggerRestrictedStopHour' => '',
|
||||
'anchor' => 'leadsource',
|
||||
'properties' => [
|
||||
'email' => $email->getId(),
|
||||
'email_type' => 'transactional',
|
||||
'priority' => '2',
|
||||
'attempts' => '3',
|
||||
],
|
||||
'type' => 'email.send',
|
||||
'eventType' => 'action',
|
||||
'anchorEventType' => 'source',
|
||||
'buttons' => [
|
||||
'save' => '',
|
||||
],
|
||||
'email' => $email->getId(),
|
||||
'email_type' => 'transactional',
|
||||
'priority' => 2,
|
||||
'attempts' => 3.0,
|
||||
];
|
||||
$event = $this->createEvent('Event 1', $campaign, $properties);
|
||||
$this->createCampaignLead($campaign, $lead);
|
||||
$this->createEventLog($lead, $event, $campaign);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId(), '--kickoff-only' => true]);
|
||||
}
|
||||
|
||||
private function createLead(): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test');
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('My campaign');
|
||||
$campaign->setIsPublished(true);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createCampaignLead(Campaign $campaign, Lead $lead): CampaignLead
|
||||
{
|
||||
$campaignLead = new CampaignLead();
|
||||
$campaignLead->setCampaign($campaign);
|
||||
$campaignLead->setLead($lead);
|
||||
$campaignLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($campaignLead);
|
||||
|
||||
return $campaignLead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $properties
|
||||
*/
|
||||
private function createEvent(string $name, Campaign $campaign, array $properties = []): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName($name);
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType('email.send');
|
||||
$event->setProperties($properties);
|
||||
$event->setEventType('action');
|
||||
$event->setTriggerInterval(1);
|
||||
$event->setTriggerMode('immediate');
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createEmail(string $name): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName($name);
|
||||
$email->setSubject('Test Subject');
|
||||
$email->setIsPublished(true);
|
||||
|
||||
$this->em->persist($email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
private function createEventLog(Lead $lead, Event $event, Campaign $campaign): LeadEventLog
|
||||
{
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setLead($lead);
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setCampaign($campaign);
|
||||
$leadEventLog->setRotation(0);
|
||||
$this->em->persist($leadEventLog);
|
||||
|
||||
return $leadEventLog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Command;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Executioner\ScheduledExecutioner;
|
||||
use Mautic\CampaignBundle\Tests\Functional\Fixtures\FixtureHelper;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class ExecuteEventCommandTest extends AbstractCampaignCommand
|
||||
{
|
||||
public function testEventsAreExecutedForInactiveEventWithSingleContact(): void
|
||||
{
|
||||
putenv('CAMPAIGN_EXECUTIONER_SCHEDULER_ACKNOWLEDGE_SECONDS=1');
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']);
|
||||
|
||||
// There should be three events scheduled
|
||||
$byEvent = $this->getCampaignEventLogs([2]);
|
||||
$this->assertCount(3, $byEvent[2]);
|
||||
|
||||
$logIds = [];
|
||||
foreach ($byEvent[2] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Event is not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
|
||||
$logIds[] = $log['id'];
|
||||
}
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:execute', ['--scheduled-log-ids' => implode(',', $logIds)]);
|
||||
|
||||
// There should still be three events scheduled
|
||||
$byEvent = $this->getCampaignEventLogs([2]);
|
||||
$this->assertCount(3, $byEvent[2]);
|
||||
|
||||
foreach ($byEvent[2] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Event is not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// Pop off the last so we can test that only the two given are executed
|
||||
$lastId = array_pop($logIds);
|
||||
|
||||
// Wait 6 seconds to go past scheduled time
|
||||
static::getContainer()->get(ScheduledExecutioner::class)->setNowTime(new \DateTime('+'.self::CONDITION_SECONDS.' seconds'));
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:execute', ['--scheduled-log-ids' => implode(',', $logIds)]);
|
||||
|
||||
// The events should have executed
|
||||
$byEvent = $this->getCampaignEventLogs([2]);
|
||||
$this->assertCount(3, $byEvent[2]);
|
||||
|
||||
foreach ($byEvent[2] as $log) {
|
||||
// Lasta
|
||||
if ($log['id'] === $lastId) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Event is not scheduled when it should be for lead ID '.$log['lead_id']);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (1 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Event is still scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
}
|
||||
|
||||
putenv('CAMPAIGN_EXECUTIONER_SCHEDULER_ACKNOWLEDGE_SECONDS=0');
|
||||
}
|
||||
|
||||
public function testRepublishScheduledCampaignEventActionWhenEventFailedBecauseCampaignWasUnpublished(): void
|
||||
{
|
||||
$fixtureHelper = new FixtureHelper($this->em);
|
||||
$contact = $fixtureHelper->createContact('some@contact.email');
|
||||
$campaign = $fixtureHelper->createCampaign('Scheduled event test');
|
||||
$fixtureHelper->addContactToCampaign($contact, $campaign);
|
||||
$fixtureHelper->createCampaignWithScheduledEvent($campaign);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$commandResult = $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId()]);
|
||||
|
||||
Assert::assertStringContainsString('1 total event was scheduled', $commandResult->getDisplay());
|
||||
|
||||
$campaign->setIsPublished(false);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$leadEventLogRepository = $this->em->getRepository(LeadEventLog::class);
|
||||
\assert($leadEventLogRepository instanceof LeadEventLogRepository);
|
||||
|
||||
$log = $leadEventLogRepository->findOneBy(['lead' => $contact, 'campaign' => $campaign]);
|
||||
\assert($log instanceof LeadEventLog);
|
||||
|
||||
Assert::assertTrue($log->getIsScheduled());
|
||||
|
||||
// Time machine so we don't have to wait for that long.
|
||||
$log->setTriggerDate(new \DateTime('2 days ago'));
|
||||
$log->setDateTriggered(new \DateTime('2 days ago'));
|
||||
$log->setIsScheduled(true);
|
||||
$this->em->persist($log);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$commandResult = $this->testSymfonyCommand('mautic:campaigns:execute', ['--scheduled-log-ids' => $log->getId()]);
|
||||
|
||||
Assert::assertStringContainsString('0 total events(s) to be processed', $commandResult->getDisplay());
|
||||
Assert::assertStringContainsString('0 total events were executed', $commandResult->getDisplay());
|
||||
Assert::assertStringContainsString('0 total events were scheduled', $commandResult->getDisplay());
|
||||
|
||||
$log = $leadEventLogRepository->findOneBy(['lead' => $contact, 'campaign' => $campaign]);
|
||||
\assert($log instanceof LeadEventLog);
|
||||
|
||||
Assert::assertTrue($log->getIsScheduled());
|
||||
Assert::assertSame([], $log->getMetadata());
|
||||
}
|
||||
|
||||
public function testRepublishScheduledCampaignEventActionWhenEventFailedBecauseCampaignPublishDownIsInThePast(): void
|
||||
{
|
||||
$fixtureHelper = new FixtureHelper($this->em);
|
||||
$contact = $fixtureHelper->createContact('some@contact.email');
|
||||
$campaign = $fixtureHelper->createCampaign('Scheduled event test');
|
||||
$fixtureHelper->addContactToCampaign($contact, $campaign);
|
||||
$fixtureHelper->createCampaignWithScheduledEvent($campaign);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$commandResult = $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId()]);
|
||||
|
||||
Assert::assertStringContainsString('1 total event was scheduled', $commandResult->getDisplay());
|
||||
|
||||
$campaign->setPublishUp(new \DateTime('3 days ago'));
|
||||
$campaign->setPublishDown(new \DateTime('1 days ago'));
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$leadEventLogRepository = $this->em->getRepository(LeadEventLog::class);
|
||||
\assert($leadEventLogRepository instanceof LeadEventLogRepository);
|
||||
|
||||
$log = $leadEventLogRepository->findOneBy(['lead' => $contact, 'campaign' => $campaign]);
|
||||
\assert($log instanceof LeadEventLog);
|
||||
|
||||
Assert::assertTrue($log->getIsScheduled());
|
||||
|
||||
// Time machine so we don't have to wait for that long.
|
||||
$log->setTriggerDate(new \DateTime('2 days ago'));
|
||||
$log->setDateTriggered(new \DateTime('2 days ago'));
|
||||
$log->setIsScheduled(true);
|
||||
$this->em->persist($log);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$commandResult = $this->testSymfonyCommand('mautic:campaigns:execute', ['--scheduled-log-ids' => $log->getId()]);
|
||||
|
||||
Assert::assertStringContainsString('1 total events(s) to be processed', $commandResult->getDisplay());
|
||||
Assert::assertStringContainsString('0 total events were executed', $commandResult->getDisplay());
|
||||
Assert::assertStringContainsString('0 total events were scheduled', $commandResult->getDisplay());
|
||||
}
|
||||
|
||||
public function testScheduledCampaignEventActionIfScheduledAtDefined(): void
|
||||
{
|
||||
$interval = 5;
|
||||
$unit = 'i';
|
||||
$fixtureHelper = new FixtureHelper($this->em);
|
||||
$contact = $fixtureHelper->createContact('some@contact.email');
|
||||
$campaign = $fixtureHelper->createCampaign('Scheduled event test');
|
||||
$fixtureHelper->addContactToCampaign($contact, $campaign);
|
||||
$hour = new \DateTime();
|
||||
$hour->add((new DateTimeHelper())->buildInterval($interval, $unit));
|
||||
$fixtureHelper->createCampaignWithScheduledEvent($campaign, $interval, $unit, $hour);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$commandResult = $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId()]);
|
||||
|
||||
Assert::assertStringContainsString('1 total event was scheduled', $commandResult->getDisplay());
|
||||
|
||||
$leadEventLogRepository = $this->em->getRepository(LeadEventLog::class);
|
||||
\assert($leadEventLogRepository instanceof LeadEventLogRepository);
|
||||
|
||||
$log = $leadEventLogRepository->findOneBy(['lead' => $contact, 'campaign' => $campaign]);
|
||||
\assert($log instanceof LeadEventLog);
|
||||
|
||||
Assert::assertTrue($log->getIsScheduled());
|
||||
|
||||
$commandResult = $this->testSymfonyCommand('mautic:campaigns:execute', ['--scheduled-log-ids' => $log->getId()]);
|
||||
|
||||
Assert::assertStringContainsString('1 total events(s) to be processed', $commandResult->getDisplay());
|
||||
Assert::assertStringContainsString('1 total event was scheduled', $commandResult->getDisplay());
|
||||
Assert::assertStringContainsString('0 total events were executed', $commandResult->getDisplay());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Command;
|
||||
|
||||
use Mautic\CampaignBundle\Command\SummarizeCommand;
|
||||
use Mautic\CampaignBundle\Entity\Summary;
|
||||
use Mautic\CampaignBundle\Entity\SummaryRepository;
|
||||
use Mautic\CampaignBundle\Tests\Campaign\AbstractCampaignTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class SummarizeCommandTest extends AbstractCampaignTestCase
|
||||
{
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testBackwardSummarizationWhenThereAreNoCampaignEventLogs(): void
|
||||
{
|
||||
$commandResult = $this->testSymfonyCommand(
|
||||
SummarizeCommand::NAME,
|
||||
[
|
||||
'--env' => 'test',
|
||||
'--max-hours' => 768,
|
||||
]
|
||||
);
|
||||
|
||||
/** @var SummaryRepository $summaryRepo */
|
||||
$summaryRepo = $this->em->getRepository(Summary::class);
|
||||
Assert::assertCount(0, $summaryRepo->findAll());
|
||||
Assert::assertStringContainsString(
|
||||
'There are no records in the campaign lead event log table. Nothing to summarize.',
|
||||
$commandResult->getDisplay()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testBackwardSummarizationWhenThereAreLogs(): void
|
||||
{
|
||||
$relativeDate = date('Y-m-d', strtotime('-1 month'));
|
||||
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
|
||||
$this->testSymfonyCommand(
|
||||
SummarizeCommand::NAME,
|
||||
[
|
||||
'--env' => 'test',
|
||||
'--max-hours' => 768,
|
||||
]
|
||||
);
|
||||
|
||||
/** @var SummaryRepository $summaryRepo */
|
||||
$summaryRepo = $this->em->getRepository(Summary::class);
|
||||
|
||||
/** @var Summary[] $summaries */
|
||||
$summaries = $summaryRepo->findAll();
|
||||
|
||||
Assert::assertCount(3, $summaries);
|
||||
|
||||
Assert::assertSame($relativeDate.'T17:00:00+00:00', $summaries[0]->getDateTriggered()->format(DATE_ATOM));
|
||||
Assert::assertSame(1, $summaries[0]->getTriggeredCount());
|
||||
Assert::assertSame($campaign->getId(), $summaries[0]->getCampaign()->getId());
|
||||
Assert::assertSame('Event B', $summaries[0]->getEvent()->getName());
|
||||
|
||||
Assert::assertSame($relativeDate.'T16:00:00+00:00', $summaries[1]->getDateTriggered()->format(DATE_ATOM));
|
||||
Assert::assertSame(2, $summaries[1]->getTriggeredCount());
|
||||
Assert::assertSame($campaign->getId(), $summaries[1]->getCampaign()->getId());
|
||||
Assert::assertSame('Event A', $summaries[1]->getEvent()->getName());
|
||||
|
||||
Assert::assertSame($relativeDate.'T16:00:00+00:00', $summaries[2]->getDateTriggered()->format(DATE_ATOM));
|
||||
Assert::assertSame(1, $summaries[2]->getTriggeredCount());
|
||||
Assert::assertSame($campaign->getId(), $summaries[2]->getCampaign()->getId());
|
||||
Assert::assertSame('Event B', $summaries[2]->getEvent()->getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,806 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Command;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Lead;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Executioner\InactiveExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\ScheduledExecutioner;
|
||||
use Mautic\LeadBundle\Command\SegmentCountCacheCommand;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use Mautic\LeadBundle\Helper\SegmentCountCacheHelper;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class TriggerCampaignCommandTest extends AbstractCampaignCommand
|
||||
{
|
||||
private ?SegmentCountCacheHelper $segmentCountCacheHelper = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['update_segment_contact_count_in_background'] = 'testSegmentCacheCountInBackground' === $this->name();
|
||||
parent::setUp();
|
||||
|
||||
putenv('CAMPAIGN_EXECUTIONER_SCHEDULER_ACKNOWLEDGE_SECONDS=1');
|
||||
|
||||
$this->segmentCountCacheHelper = static::getContainer()->get('mautic.helper.segment.count.cache');
|
||||
}
|
||||
|
||||
public function beforeTearDown(): void
|
||||
{
|
||||
parent::beforeTearDown();
|
||||
|
||||
putenv('CAMPAIGN_EXECUTIONER_SCHEDULER_ACKNOWLEDGE_SECONDS=0');
|
||||
|
||||
$this->segmentCountCacheHelper = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testCampaignExecutionForAll(): void
|
||||
{
|
||||
// Process in batches of 10 to ensure batching is working as expected
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]);
|
||||
|
||||
// Let's analyze
|
||||
$byEvent = $this->getCampaignEventLogs([1, 2, 11, 12, 13, 16]);
|
||||
$tags = $this->getTagCounts();
|
||||
|
||||
// Everyone should have been tagged with CampaignTest and have been sent Campaign Test Email 1
|
||||
$this->assertCount(50, $byEvent[1]);
|
||||
$this->assertCount(50, $byEvent[2]);
|
||||
|
||||
// Sending Campaign Test Email 1 should be scheduled
|
||||
foreach ($byEvent[2] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Sending Campaign Test Email 1 was not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// Everyone should have had the Is US condition processed
|
||||
$this->assertCount(50, $byEvent[11]);
|
||||
|
||||
// 42 should have been send down the non-action path (red) of the condition
|
||||
$nonActionCount = $this->getNonActionPathTakenCount($byEvent[11]);
|
||||
$this->assertEquals(42, $nonActionCount);
|
||||
|
||||
// 8 contacts are from the US and should be labeled with US:Action
|
||||
$this->assertCount(8, $byEvent[12]);
|
||||
$this->assertEquals(8, $tags['US:Action']);
|
||||
|
||||
// Those tagged with US:Action should also be tagged with ChainedAction by a chained event.
|
||||
$this->assertCount(8, $byEvent[16]);
|
||||
$this->assertEquals(8, $tags['ChainedAction']);
|
||||
|
||||
// The rest (42) contacts are not from the US and should be labeled with NonUS:Action
|
||||
$this->assertCount(42, $byEvent[13]);
|
||||
$this->assertEquals(42, $tags['NonUS:Action']);
|
||||
|
||||
// No emails should be sent till after 5 seconds and the command is ran again
|
||||
$stats = $this->db->createQueryBuilder()
|
||||
->select('*')
|
||||
->from($this->prefix.'email_stats', 'stat')
|
||||
->where('stat.lead_id <= 25')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
$this->assertCount(0, $stats);
|
||||
|
||||
// Wait 6 seconds then execute the campaign again to send scheduled events
|
||||
static::getContainer()->get(ScheduledExecutioner::class)->setNowTime(new \DateTime('+'.self::CONDITION_SECONDS.' seconds'));
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]);
|
||||
|
||||
// Send email 1 should no longer be scheduled
|
||||
$byEvent = $this->getCampaignEventLogs([2, 4]);
|
||||
$this->assertCount(50, $byEvent[2]);
|
||||
foreach ($byEvent[2] as $log) {
|
||||
if (1 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Sending Campaign Test Email 1 is still scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// The non-action events attached to the decision should have no logs entries
|
||||
$this->assertCount(0, $byEvent[4]);
|
||||
|
||||
// Check that the emails actually sent
|
||||
$stats = $this->db->createQueryBuilder()
|
||||
->select('*')
|
||||
->from($this->prefix.'email_stats', 'stat')
|
||||
->where('stat.lead_id <= 25')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$this->assertCount(25, $stats);
|
||||
|
||||
// Now let's simulate email opens
|
||||
foreach ($stats as $stat) {
|
||||
$this->client->request('GET', '/email/'.$stat['tracking_hash'].'.gif');
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent(), true));
|
||||
}
|
||||
|
||||
$byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14, 15]);
|
||||
|
||||
// The non-action events attached to the decision should have no logs entries
|
||||
$this->assertCount(0, $byEvent[4]);
|
||||
$this->assertCount(0, $byEvent[5]);
|
||||
$this->assertCount(0, $byEvent[14]);
|
||||
$this->assertCount(0, $byEvent[15]);
|
||||
|
||||
// Those 25 should now have open email decisions logged and the next email sent
|
||||
$this->assertCount(25, $byEvent[3]);
|
||||
$this->assertCount(25, $byEvent[10]);
|
||||
|
||||
// Wait another 6 seconds to go beyond the inaction timeframe
|
||||
static::getContainer()->get(InactiveExecutioner::class)->setNowTime(new \DateTime('+'.(self::CONDITION_SECONDS * 2).' seconds'));
|
||||
|
||||
// Execute the command again to trigger inaction related events
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]);
|
||||
|
||||
// Now we should have 50 email open decisions
|
||||
$byEvent = $this->getCampaignEventLogs([3, 4, 5, 14, 15]);
|
||||
$this->assertCount(50, $byEvent[3]);
|
||||
|
||||
// 25 should be marked as non_action_path_taken
|
||||
$nonActionCount = $this->getNonActionPathTakenCount($byEvent[3]);
|
||||
$this->assertEquals(25, $nonActionCount);
|
||||
|
||||
// A condition should be logged as evaluated for each of the 25 contacts
|
||||
$this->assertCount(25, $byEvent[4]);
|
||||
$this->assertCount(25, $byEvent[5]);
|
||||
|
||||
// Tag EmailNotOpen should all be scheduled for these 25 contacts because the condition's timeframe was shorter and therefore the
|
||||
// contact was sent down the inaction path
|
||||
$this->assertCount(25, $byEvent[14]);
|
||||
$this->assertCount(25, $byEvent[15]);
|
||||
|
||||
$utcTimezone = new \DateTimeZone('UTC');
|
||||
foreach ($byEvent[14] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Tag EmailNotOpen is not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
|
||||
$scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone);
|
||||
$diff = $this->eventDate->diff($scheduledFor);
|
||||
|
||||
if (2 !== $diff->i) {
|
||||
$this->fail('Tag EmailNotOpen should be scheduled for around 2 minutes ('.$diff->i.' minutes)');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($byEvent[15] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Tag EmailNotOpen Again is not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
|
||||
$scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone);
|
||||
$diff = $this->eventDate->diff($scheduledFor);
|
||||
|
||||
if (6 !== $diff->i) {
|
||||
$this->fail('Tag EmailNotOpen Again should be scheduled for around 6 minutes ('.$diff->i.' minutes)');
|
||||
}
|
||||
}
|
||||
$byEvent = $this->getCampaignEventLogs([6, 7, 8, 9]);
|
||||
$tags = $this->getTagCounts();
|
||||
|
||||
// Of those that did not open the email, 6 should be tagged US:NotOpen
|
||||
$this->assertCount(6, $byEvent[6]);
|
||||
$this->assertEquals(6, $tags['US:NotOpen']);
|
||||
|
||||
// And 19 should be tagged NonUS:NotOpen
|
||||
$this->assertCount(19, $byEvent[7]);
|
||||
$this->assertEquals(19, $tags['NonUS:NotOpen']);
|
||||
|
||||
// And 4 should be tagged UK:NotOpen
|
||||
$this->assertCount(4, $byEvent[8]);
|
||||
$this->assertEquals(4, $tags['UK:NotOpen']);
|
||||
|
||||
// And 21 should be tagged NonUK:NotOpen
|
||||
$this->assertCount(21, $byEvent[9]);
|
||||
$this->assertEquals(21, $tags['NonUK:NotOpen']);
|
||||
|
||||
// No one should be tagged as EmailNotOpen because the actions are still scheduled
|
||||
$this->assertFalse(isset($tags['EmailNotOpen']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testCampaignExecutionForOne(): void
|
||||
{
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]);
|
||||
|
||||
// Let's analyze
|
||||
$byEvent = $this->getCampaignEventLogs([1, 2, 11, 12, 13, 16]);
|
||||
$tags = $this->getTagCounts();
|
||||
|
||||
// Everyone should have been tagged with CampaignTest and have been sent Campaign Test Email 1
|
||||
$this->assertCount(1, $byEvent[1]);
|
||||
$this->assertCount(1, $byEvent[2]);
|
||||
|
||||
// Sending Campaign Test Email 1 should be scheduled
|
||||
foreach ($byEvent[2] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Sending Campaign Test Email 1 was not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// Everyone should have had the Is US condition processed
|
||||
$this->assertCount(1, $byEvent[11]);
|
||||
|
||||
// 1 should have been send down the non-action path (red) of the condition
|
||||
$nonActionCount = $this->getNonActionPathTakenCount($byEvent[11]);
|
||||
$this->assertEquals(1, $nonActionCount);
|
||||
|
||||
// 0 contacts are from the US and should be labeled with US:Action
|
||||
$this->assertCount(0, $byEvent[12]);
|
||||
$this->assertTrue(empty($tags['US:Action']));
|
||||
|
||||
// None tagged with US:Action, so none should be tagged with ChainedAction by a chained event.
|
||||
$this->assertCount(0, $byEvent[16]);
|
||||
$this->assertTrue(empty($tags['ChainedAction']));
|
||||
|
||||
// The rest (1) contacts are not from the US and should be labeled with NonUS:Action
|
||||
$this->assertCount(1, $byEvent[13]);
|
||||
$this->assertEquals(1, $tags['NonUS:Action']);
|
||||
|
||||
// No emails should be sent till after 5 seconds and the command is ran again
|
||||
$stats = $this->db->createQueryBuilder()
|
||||
->select('*')
|
||||
->from($this->prefix.'email_stats', 'stat')
|
||||
->where('stat.lead_id = 1')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$this->assertCount(0, $stats);
|
||||
|
||||
// Wait 6 seconds then execute the campaign again to send scheduled events
|
||||
static::getContainer()->get(ScheduledExecutioner::class)->setNowTime(new \DateTime('+'.self::CONDITION_SECONDS.' seconds'));
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]);
|
||||
|
||||
// Send email 1 should no longer be scheduled
|
||||
$byEvent = $this->getCampaignEventLogs([2, 4]);
|
||||
$this->assertCount(1, $byEvent[2]);
|
||||
foreach ($byEvent[2] as $log) {
|
||||
if (1 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Sending Campaign Test Email 1 is still scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// The non-action events attached to the decision should have no logs entries
|
||||
$this->assertCount(0, $byEvent[4]);
|
||||
|
||||
// Check that the emails actually sent
|
||||
$stats = $this->db->createQueryBuilder()
|
||||
->select('*')
|
||||
->from($this->prefix.'email_stats', 'stat')
|
||||
->where('stat.lead_id = 1')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$this->assertCount(1, $stats);
|
||||
|
||||
// Now let's simulate email opens
|
||||
foreach ($stats as $stat) {
|
||||
$this->client->request('GET', '/email/'.$stat['tracking_hash'].'.gif');
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent(), true));
|
||||
}
|
||||
|
||||
$byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14, 15]);
|
||||
|
||||
// The non-action events attached to the decision should have no logs entries
|
||||
$this->assertCount(0, $byEvent[4]);
|
||||
$this->assertCount(0, $byEvent[5]);
|
||||
$this->assertCount(0, $byEvent[14]);
|
||||
$this->assertCount(0, $byEvent[15]);
|
||||
|
||||
// The 1 should now have open email decisions logged and the next email sent
|
||||
$this->assertCount(1, $byEvent[3]);
|
||||
$this->assertCount(1, $byEvent[10]);
|
||||
|
||||
// Wait 6 seconds to go beyond the inaction timeframe
|
||||
static::getContainer()->get(InactiveExecutioner::class)->setNowTime(new \DateTime('+'.(self::CONDITION_SECONDS * 2).' seconds'));
|
||||
|
||||
// Execute the command again to trigger inaction related events
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]);
|
||||
|
||||
// Now we should have 1 email open decisions
|
||||
$byEvent = $this->getCampaignEventLogs([3, 4, 5, 14, 15]);
|
||||
$this->assertCount(1, $byEvent[3]);
|
||||
|
||||
// 0 should be marked as non_action_path_taken
|
||||
$nonActionCount = $this->getNonActionPathTakenCount($byEvent[3]);
|
||||
$this->assertEquals(0, $nonActionCount);
|
||||
|
||||
// There should be no inactive events
|
||||
$this->assertCount(0, $byEvent[4]);
|
||||
$this->assertCount(0, $byEvent[5]);
|
||||
$this->assertCount(0, $byEvent[14]);
|
||||
$this->assertCount(0, $byEvent[15]);
|
||||
|
||||
$utcTimezone = new \DateTimeZone('UTC');
|
||||
foreach ($byEvent[14] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Tag EmailNotOpen is not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
|
||||
$scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone);
|
||||
$diff = $this->eventDate->diff($scheduledFor);
|
||||
|
||||
if (2 !== $diff->i) {
|
||||
$this->fail('Tag EmailNotOpen should be scheduled for around 2 minutes ('.$diff->i.' minutes)');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($byEvent[15] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Tag EmailNotOpen Again is not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
|
||||
$scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone);
|
||||
$diff = $this->eventDate->diff($scheduledFor);
|
||||
|
||||
if (6 !== $diff->i) {
|
||||
$this->fail('Tag EmailNotOpen Again should be scheduled for around 6 minutes ('.$diff->i.' minutes)');
|
||||
}
|
||||
}
|
||||
$byEvent = $this->getCampaignEventLogs([6, 7, 8, 9]);
|
||||
$tags = $this->getTagCounts();
|
||||
|
||||
// Of those that did not open the email, 0 should be tagged US:NotOpen
|
||||
$this->assertCount(0, $byEvent[6]);
|
||||
$this->assertTrue(empty($tags['US:NotOpen']));
|
||||
|
||||
// And 0 should be tagged NonUS:NotOpen
|
||||
$this->assertCount(0, $byEvent[7]);
|
||||
$this->assertTrue(empty($tags['NonUS:NotOpen']));
|
||||
|
||||
// And 0 should be tagged UK:NotOpen
|
||||
$this->assertCount(0, $byEvent[8]);
|
||||
$this->assertTrue(empty($tags['UK:NotOpen']));
|
||||
|
||||
// And 0 should be tagged NonUK:NotOpen
|
||||
$this->assertCount(0, $byEvent[9]);
|
||||
$this->assertTrue(empty($tags['NonUK:NotOpen']));
|
||||
|
||||
// No one should be tagged as EmailNotOpen because the actions are still scheduled
|
||||
$this->assertTrue(empty($tags['EmailNotOpen']));
|
||||
}
|
||||
|
||||
public function testCampaignExecutionForSome(): void
|
||||
{
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']);
|
||||
|
||||
// Let's analyze
|
||||
$byEvent = $this->getCampaignEventLogs([1, 2, 11, 12, 13, 16]);
|
||||
$tags = $this->getTagCounts();
|
||||
|
||||
// Everyone should have been tagged with CampaignTest and have been sent Campaign Test Email 1
|
||||
$this->assertCount(5, $byEvent[1]);
|
||||
$this->assertCount(5, $byEvent[2]);
|
||||
|
||||
// Sending Campaign Test Email 1 should be scheduled
|
||||
foreach ($byEvent[2] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Sending Campaign Test Email 1 was not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// Everyone should have had the Is US condition processed
|
||||
$this->assertCount(5, $byEvent[11]);
|
||||
|
||||
// 4 should have been send down the non-action path (red) of the condition
|
||||
$nonActionCount = $this->getNonActionPathTakenCount($byEvent[11]);
|
||||
$this->assertEquals(4, $nonActionCount);
|
||||
|
||||
// 1 contacts are from the US and should be labeled with US:Action
|
||||
$this->assertCount(1, $byEvent[12]);
|
||||
$this->assertEquals(1, $tags['US:Action']);
|
||||
|
||||
// Those tagged with US:Action should also be tagged with ChainedAction by a chained event.
|
||||
$this->assertCount(1, $byEvent[16]);
|
||||
$this->assertEquals(1, $tags['ChainedAction']);
|
||||
|
||||
// The rest (4) contacts are not from the US and should be labeled with NonUS:Action
|
||||
$this->assertCount(4, $byEvent[13]);
|
||||
$this->assertEquals(4, $tags['NonUS:Action']);
|
||||
|
||||
// No emails should be sent till after 5 seconds and the command is ran again
|
||||
$stats = $this->db->createQueryBuilder()
|
||||
->select('*')
|
||||
->from($this->prefix.'email_stats', 'stat')
|
||||
->where('stat.lead_id <= 2')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$this->assertCount(0, $stats);
|
||||
|
||||
// Wait 6 seconds then execute the campaign again to send scheduled events
|
||||
static::getContainer()->get(ScheduledExecutioner::class)->setNowTime(new \DateTime('+'.self::CONDITION_SECONDS.' seconds'));
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']);
|
||||
|
||||
// Send email 1 should no longer be scheduled
|
||||
$byEvent = $this->getCampaignEventLogs([2, 4]);
|
||||
$this->assertCount(5, $byEvent[2]);
|
||||
foreach ($byEvent[2] as $log) {
|
||||
if (1 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Sending Campaign Test Email 1 is still scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// The non-action events attached to the decision should have no logs entries
|
||||
$this->assertCount(0, $byEvent[4]);
|
||||
|
||||
// Check that the emails actually sent
|
||||
$stats = $this->db->createQueryBuilder()
|
||||
->select('*')
|
||||
->from($this->prefix.'email_stats', 'stat')
|
||||
->where('stat.lead_id <= 2')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
$this->assertCount(2, $stats);
|
||||
|
||||
// Now let's simulate email opens
|
||||
foreach ($stats as $stat) {
|
||||
$this->client->request('GET', '/email/'.$stat['tracking_hash'].'.gif');
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent(), true));
|
||||
}
|
||||
|
||||
$byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14, 15]);
|
||||
|
||||
// The non-action events attached to the decision should have no logs entries
|
||||
$this->assertCount(0, $byEvent[4]);
|
||||
$this->assertCount(0, $byEvent[5]);
|
||||
$this->assertCount(0, $byEvent[14]);
|
||||
$this->assertCount(0, $byEvent[15]);
|
||||
|
||||
// Those 25 should now have open email decisions logged and the next email sent
|
||||
$this->assertCount(2, $byEvent[3]);
|
||||
$this->assertCount(2, $byEvent[10]);
|
||||
|
||||
// Wait 6 seconds to go beyond the inaction timeframe
|
||||
static::getContainer()->get(InactiveExecutioner::class)->setNowTime(new \DateTime('+'.(self::CONDITION_SECONDS * 2).' seconds'));
|
||||
|
||||
// Execute the command again to trigger inaction related events
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']);
|
||||
|
||||
// Now we should have 5 email open decisions
|
||||
$byEvent = $this->getCampaignEventLogs([3, 4, 5, 14, 15]);
|
||||
$this->assertCount(5, $byEvent[3]);
|
||||
|
||||
// 3 should be marked as non_action_path_taken
|
||||
$nonActionCount = $this->getNonActionPathTakenCount($byEvent[3]);
|
||||
$this->assertEquals(3, $nonActionCount);
|
||||
|
||||
// A condition should be logged as evaluated for each of the 3 contacts
|
||||
$this->assertCount(3, $byEvent[4]);
|
||||
$this->assertCount(3, $byEvent[5]);
|
||||
|
||||
// Tag EmailNotOpen should all be scheduled for these 3 contacts because the condition's timeframe was shorter and therefore the
|
||||
// contact was sent down the inaction path
|
||||
$this->assertCount(3, $byEvent[14]);
|
||||
$this->assertCount(3, $byEvent[15]);
|
||||
|
||||
$utcTimezone = new \DateTimeZone('UTC');
|
||||
foreach ($byEvent[14] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Tag EmailNotOpen is not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
|
||||
$scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone);
|
||||
$diff = $this->eventDate->diff($scheduledFor);
|
||||
|
||||
if (2 !== $diff->i) {
|
||||
$this->fail('Tag EmailNotOpen should be scheduled for around 2 minutes ('.$diff->i.' minutes)');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($byEvent[15] as $log) {
|
||||
if (0 === (int) $log['is_scheduled']) {
|
||||
$this->fail('Tag EmailNotOpen Again is not scheduled for lead ID '.$log['lead_id']);
|
||||
}
|
||||
|
||||
$scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone);
|
||||
$diff = $this->eventDate->diff($scheduledFor);
|
||||
|
||||
if (6 !== $diff->i) {
|
||||
$this->fail('Tag EmailNotOpen Again should be scheduled for around 6 minutes ('.$diff->i.' minutes)');
|
||||
}
|
||||
}
|
||||
$byEvent = $this->getCampaignEventLogs([6, 7, 8, 9]);
|
||||
$tags = $this->getTagCounts();
|
||||
|
||||
// Of those that did not open the email, 1 should be tagged US:NotOpen
|
||||
$this->assertCount(1, $byEvent[6]);
|
||||
$this->assertEquals(1, $tags['US:NotOpen']);
|
||||
|
||||
// And 2 should be tagged NonUS:NotOpen
|
||||
$this->assertCount(2, $byEvent[7]);
|
||||
$this->assertEquals(2, $tags['NonUS:NotOpen']);
|
||||
|
||||
// And 2 should be tagged UK:NotOpen
|
||||
$this->assertCount(2, $byEvent[8]);
|
||||
$this->assertEquals(2, $tags['UK:NotOpen']);
|
||||
|
||||
// And 1 should be tagged NonUK:NotOpen
|
||||
$this->assertCount(1, $byEvent[9]);
|
||||
$this->assertEquals(1, $tags['NonUK:NotOpen']);
|
||||
|
||||
// No one should be tagged as EmailNotOpen because the actions are still scheduled
|
||||
$this->assertFalse(isset($tags['EmailNotOpen']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Psr\Cache\InvalidArgumentException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testSegmentCacheCountInBackground(): void
|
||||
{
|
||||
// remove redis key if exist
|
||||
$this->segmentCountCacheHelper->deleteSegmentContactCount(1);
|
||||
|
||||
// Execute the command again to trigger related events.
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1]);
|
||||
|
||||
$count = $this->segmentCountCacheHelper->getSegmentContactCount(1);
|
||||
self::assertEquals(0, $count);
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Segment cache count should be 50.
|
||||
$count = $this->segmentCountCacheHelper->getSegmentContactCount(1);
|
||||
self::assertEquals(50, $count);
|
||||
}
|
||||
|
||||
public function testCampaignActionChangeMembership(): void
|
||||
{
|
||||
$campaign1 = $this->createCampaign('Campaign 1');
|
||||
$campaign2 = $this->createCampaign('Campaign 2');
|
||||
$lead = $this->createLead('Lead');
|
||||
$this->createCampaignLead($campaign1, $lead);
|
||||
$this->em->flush();
|
||||
$property = ['addTo' => [$campaign2->getId()], 'removeFrom' => ['this']];
|
||||
$this->createEvent('Event', $campaign1, 'campaign.addremovelead', 'action', $property);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign1->getId(), '--contact-id' => $lead->getId(), '--kickoff-only' => true]);
|
||||
|
||||
$campaignLeads = $this->em->getRepository(Lead::class)->findBy(['lead' => $lead], ['campaign' => 'ASC']);
|
||||
|
||||
Assert::assertCount(2, $campaignLeads);
|
||||
Assert::assertSame($campaign1->getId(), $campaignLeads[0]->getCampaign()->getId());
|
||||
Assert::assertTrue($campaignLeads[0]->getManuallyRemoved());
|
||||
Assert::assertSame($campaign2->getId(), $campaignLeads[1]->getCampaign()->getId());
|
||||
Assert::assertFalse($campaignLeads[1]->getManuallyRemoved());
|
||||
}
|
||||
|
||||
public function testCampaignActionChangeMembershipRestartRotation(): void
|
||||
{
|
||||
$campaign1 = $this->createCampaign('Campaign 1');
|
||||
// create campaign with restart allowed
|
||||
$campaign2 = $this->createCampaign('Campaign 2');
|
||||
$campaign2->setAllowRestart(true);
|
||||
$this->em->persist($campaign2);
|
||||
|
||||
$lead = $this->createLead('Lead');
|
||||
|
||||
// add lead to both campaigns
|
||||
$this->createCampaignLead($campaign1, $lead);
|
||||
$this->createCampaignLead($campaign2, $lead);
|
||||
$this->em->flush();
|
||||
|
||||
// add action changeCampaigns to add the lead again to campaign2
|
||||
$property = ['addTo' => [$campaign2->getId()], 'removeFrom' => ['this']];
|
||||
$this->createEvent('Event', $campaign1, 'campaign.addremovelead', 'action', $property);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign1->getId(), '--contact-id' => $lead->getId(), '--kickoff-only' => true]);
|
||||
|
||||
$campaignLeads = $this->em->getRepository(Lead::class)->findBy(['lead' => $lead], ['campaign' => 'ASC']);
|
||||
|
||||
Assert::assertCount(2, $campaignLeads);
|
||||
Assert::assertSame($campaign1->getId(), $campaignLeads[0]->getCampaign()->getId());
|
||||
Assert::assertTrue($campaignLeads[0]->getManuallyRemoved());
|
||||
Assert::assertSame($campaign2->getId(), $campaignLeads[1]->getCampaign()->getId());
|
||||
Assert::assertFalse($campaignLeads[1]->getManuallyRemoved());
|
||||
Assert::assertSame(2, $campaignLeads[1]->getRotation()); // assert it's the second rotation
|
||||
}
|
||||
|
||||
public function testCampaignActionAfterChangeMembership(): void
|
||||
{
|
||||
$campaign = $this->createCampaign('Campaign 1');
|
||||
$lead = $this->createLead('Lead');
|
||||
$this->createCampaignLead($campaign, $lead);
|
||||
$this->em->flush();
|
||||
$property = ['removeFrom' => ['this']];
|
||||
$event1 = $this->createEvent('Event', $campaign, 'campaign.addremovelead', 'action', $property);
|
||||
$property = ['points' => 1];
|
||||
$event2 = $this->createEvent('Event', $campaign, 'lead.changepoints', 'action', $property);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId(), '--contact-id' => $lead->getId(), '--kickoff-only' => true]);
|
||||
|
||||
$campaignLeads = $this->em->getRepository(Lead::class)->findBy(['lead' => $lead]);
|
||||
Assert::assertCount(1, $campaignLeads);
|
||||
Assert::assertSame($campaign->getId(), $campaignLeads[0]->getCampaign()->getId());
|
||||
Assert::assertTrue($campaignLeads[0]->getManuallyRemoved());
|
||||
|
||||
$campaignEventLogs = $this->em->getRepository(LeadEventLog::class)->findBy(['campaign' => $campaign, 'lead' => $lead], ['event' => 'ASC']);
|
||||
Assert::assertCount(1, $campaignEventLogs);
|
||||
Assert::assertSame($campaign->getId(), $campaignEventLogs[0]->getCampaign()->getId());
|
||||
Assert::assertSame($event1->getId(), $campaignEventLogs[0]->getEvent()->getId());
|
||||
}
|
||||
|
||||
public function testCampaignActionBeforeChangeMembership(): void
|
||||
{
|
||||
$campaign = $this->createCampaign('Campaign 1');
|
||||
$lead = $this->createLead('Lead');
|
||||
$this->createCampaignLead($campaign, $lead);
|
||||
$this->em->flush();
|
||||
$property = ['points' => 1];
|
||||
$event1 = $this->createEvent('Event', $campaign, 'lead.changepoints', 'action', $property);
|
||||
$property = ['removeFrom' => ['this']];
|
||||
$event2 = $this->createEvent('Event', $campaign, 'campaign.addremovelead', 'action', $property);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId(), '--contact-id' => $lead->getId(), '--kickoff-only' => true]);
|
||||
|
||||
$campaignLeads = $this->em->getRepository(Lead::class)->findBy(['lead' => $lead]);
|
||||
Assert::assertCount(1, $campaignLeads);
|
||||
Assert::assertSame($campaign->getId(), $campaignLeads[0]->getCampaign()->getId());
|
||||
Assert::assertTrue($campaignLeads[0]->getManuallyRemoved());
|
||||
|
||||
$campaignEventLogs = $this->em->getRepository(LeadEventLog::class)->findBy(['campaign' => $campaign, 'lead' => $lead], ['event' => 'ASC']);
|
||||
Assert::assertCount(2, $campaignEventLogs);
|
||||
Assert::assertSame($campaign->getId(), $campaignEventLogs[0]->getCampaign()->getId());
|
||||
Assert::assertSame($event1->getId(), $campaignEventLogs[0]->getEvent()->getId());
|
||||
Assert::assertSame($campaign->getId(), $campaignEventLogs[1]->getCampaign()->getId());
|
||||
Assert::assertSame($event2->getId(), $campaignEventLogs[1]->getEvent()->getId());
|
||||
}
|
||||
|
||||
public function testCampaignExclusion(): void
|
||||
{
|
||||
$campaign1 = $this->createCampaign('Campaign 1');
|
||||
$campaign2 = $this->createCampaign('Campaign 2');
|
||||
$lead = $this->createLead('Lead');
|
||||
$this->createCampaignLead($campaign1, $lead);
|
||||
$this->createCampaignLead($campaign2, $lead);
|
||||
$this->em->flush();
|
||||
$property = ['addTo' => [$campaign2->getId()], 'removeFrom' => ['this']];
|
||||
$this->createEvent('Event', $campaign1, 'campaign.addremovelead', 'action', $property);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['--exclude' => [$campaign1->getId()], '--contact-id' => $lead->getId(), '--kickoff-only' => true]);
|
||||
|
||||
$campaignLeads = $this->em->getRepository(Lead::class)->findBy(['lead' => $lead], ['campaign' => 'ASC']);
|
||||
|
||||
Assert::assertCount(2, $campaignLeads);
|
||||
Assert::assertSame($campaign1->getId(), $campaignLeads[0]->getCampaign()->getId());
|
||||
Assert::assertFalse($campaignLeads[0]->getManuallyRemoved(), 'Test not executed campaign does not have Contact removed.');
|
||||
Assert::assertSame($campaign2->getId(), $campaignLeads[1]->getCampaign()->getId());
|
||||
Assert::assertFalse($campaignLeads[1]->getManuallyRemoved());
|
||||
Assert::assertFalse($campaignLeads[1]->getManuallyAdded());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/mautic/mautic/issues/11061
|
||||
*
|
||||
* This test will not fail if the infinite loop returns and instead run indefinitelly until a PHPUNIT timeout is reached.
|
||||
* I couldn't find an easy way to test for an infinite loop. But we'll know if it returns again.
|
||||
* We'll just spend more time figuring out which test is taking so long.
|
||||
*/
|
||||
public function testCampaignInfiniteLoop(): void
|
||||
{
|
||||
$campaignMemberRepo = $this->em->getRepository(Lead::class);
|
||||
|
||||
$segmentMemberRepo = $this->em->getRepository(ListLead::class);
|
||||
|
||||
$campaignRepo = $this->em->getRepository(Campaign::class);
|
||||
|
||||
// Clear the campaign and segment members as those are manually_added.
|
||||
$campaignMemberRepo->deleteEntities($campaignMemberRepo->findAll());
|
||||
$segmentMemberRepo->deleteEntities($segmentMemberRepo->findAll());
|
||||
|
||||
$campaign = $campaignRepo->find(1); // Created in parent::setUp()
|
||||
\assert($campaign instanceof Campaign);
|
||||
|
||||
$campaign->setAllowRestart(true);
|
||||
|
||||
$campaignRepo->saveEntity($campaign);
|
||||
|
||||
$john = $this->createLead('John');
|
||||
$jane = $this->createLead('Jane');
|
||||
$this->createSegmentMember($campaign->getLists()->first(), $john);
|
||||
$this->createSegmentMember($campaign->getLists()->first(), $jane);
|
||||
$this->createCampaignLead($campaign, $john);
|
||||
$this->createCampaignLead($campaign, $jane, true); // Manually removed.
|
||||
$this->em->flush();
|
||||
$this->em->detach($campaign);
|
||||
$this->em->detach($campaignRepo);
|
||||
|
||||
$tStart = microtime(true);
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:update', ['--campaign-id' => $campaign->getId()]);
|
||||
|
||||
$tDiff = microtime(true) - $tStart;
|
||||
|
||||
$this->assertLessThan(10, $tDiff, 'The campaign rebuild takes more than 10 seconds, probably an infinite loop.');
|
||||
}
|
||||
|
||||
public function testCampaignExecuteOrderByDateCreatedDesc(): void
|
||||
{
|
||||
$oldCampaign = $this->createCampaign('Some old campaign');
|
||||
$oldCampaign->setDateAdded(new \DateTime('2019-01-03 03:54:25'));
|
||||
$newCampaign = $this->createCampaign('New campaign');
|
||||
$newCampaign->setDateAdded(new \DateTime());
|
||||
$this->em->flush();
|
||||
|
||||
$commandTester = $this->testSymfonyCommand('mautic:campaigns:trigger');
|
||||
$commandTester->assertCommandIsSuccessful();
|
||||
$lines = preg_split('/\r\n|\r|\n/', $commandTester->getDisplay());
|
||||
|
||||
$campaignStartLines = [];
|
||||
foreach ($lines as $line) {
|
||||
if (str_starts_with($line, 'Triggering events for campaign')) {
|
||||
$campaignStartLines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the new campaign processed first
|
||||
$this->assertEquals("Triggering events for campaign {$newCampaign->getId()}", $campaignStartLines[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testSegmentCacheCount(): void
|
||||
{
|
||||
// Execute the command again to trigger related events.
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1]);
|
||||
// Segment cache count should be 50.
|
||||
$count = $this->segmentCountCacheHelper->getSegmentContactCount(1);
|
||||
self::assertEquals(50, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function getTagCounts()
|
||||
{
|
||||
$tags = $this->db->createQueryBuilder()
|
||||
->select('t.tag, count(*) as the_count')
|
||||
->from($this->prefix.'lead_tags', 't')
|
||||
->join('t', $this->prefix.'lead_tags_xref', 'l', 't.id = l.tag_id')
|
||||
->groupBy('t.tag')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$tagCounts = [];
|
||||
foreach ($tags as $tag) {
|
||||
$tagCounts[$tag['tag']] = (int) $tag['the_count'];
|
||||
}
|
||||
|
||||
return $tagCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
private function getNonActionPathTakenCount(array $logs)
|
||||
{
|
||||
$nonActionCount = 0;
|
||||
foreach ($logs as $log) {
|
||||
if ((int) $log['non_action_path_taken']) {
|
||||
++$nonActionCount;
|
||||
}
|
||||
}
|
||||
|
||||
return $nonActionCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Command;
|
||||
|
||||
use Mautic\CampaignBundle\Executioner\InactiveExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\ScheduledExecutioner;
|
||||
|
||||
class ValidateEventCommandTest extends AbstractCampaignCommand
|
||||
{
|
||||
public function testEventsAreExecutedForInactiveEventWithSingleContact(): void
|
||||
{
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]);
|
||||
|
||||
// Wait 6 seconds then execute the campaign again to send scheduled events
|
||||
static::getContainer()->get(ScheduledExecutioner::class)->setNowTime(new \DateTime('+'.self::CONDITION_SECONDS.' seconds'));
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]);
|
||||
|
||||
// No open email decisions should be recorded yet
|
||||
$byEvent = $this->getCampaignEventLogs([3]);
|
||||
$this->assertCount(0, $byEvent[3]);
|
||||
|
||||
// Wait 6 seconds to go beyond the inaction timeframe
|
||||
static::getContainer()->get(InactiveExecutioner::class)->setNowTime(new \DateTime('+'.(self::CONDITION_SECONDS * 2).' seconds'));
|
||||
|
||||
// Now they should be inactive
|
||||
$this->testSymfonyCommand('mautic:campaigns:validate', ['--decision-id' => 3, '--contact-id' => 1]);
|
||||
|
||||
$byEvent = $this->getCampaignEventLogs([3, 7, 10]);
|
||||
$this->assertCount(1, $byEvent[3]); // decision recorded
|
||||
$this->assertCount(1, $byEvent[7]); // inactive event executed
|
||||
$this->assertCount(0, $byEvent[10]); // the positive path should be 0
|
||||
}
|
||||
|
||||
public function testEventsAreExecutedForInactiveEventWithMultipleContact(): void
|
||||
{
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']);
|
||||
|
||||
// Wait 6 seconds then execute the campaign again to send scheduled events
|
||||
static::getContainer()->get(ScheduledExecutioner::class)->setNowTime(new \DateTime('+'.self::CONDITION_SECONDS.' seconds'));
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']);
|
||||
|
||||
// No open email decisions should be recorded yet
|
||||
$byEvent = $this->getCampaignEventLogs([3]);
|
||||
$this->assertCount(0, $byEvent[3]);
|
||||
|
||||
// Wait 6 seconds to go beyond the inaction timeframe
|
||||
static::getContainer()->get(InactiveExecutioner::class)->setNowTime(new \DateTime('+'.(self::CONDITION_SECONDS * 2).' seconds'));
|
||||
|
||||
// Now they should be inactive
|
||||
$this->testSymfonyCommand('mautic:campaigns:validate', ['--decision-id' => 3, '--contact-ids' => '1,2,3']);
|
||||
|
||||
$byEvent = $this->getCampaignEventLogs([3, 7, 10]);
|
||||
$this->assertCount(3, $byEvent[3]); // decision recorded
|
||||
$this->assertCount(3, $byEvent[7]); // inactive event executed
|
||||
$this->assertCount(0, $byEvent[10]); // the positive path should be 0
|
||||
}
|
||||
|
||||
public function testContactsRemovedFromTheCampaignAreNotExecutedForInactiveEvents(): void
|
||||
{
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']);
|
||||
|
||||
// Wait 6 seconds then execute the campaign again to send scheduled events
|
||||
static::getContainer()->get(ScheduledExecutioner::class)->setNowTime(new \DateTime('+'.self::CONDITION_SECONDS.' seconds'));
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']);
|
||||
|
||||
// No open email decisions should be recorded yet
|
||||
$byEvent = $this->getCampaignEventLogs([3]);
|
||||
$this->assertCount(0, $byEvent[3]);
|
||||
|
||||
// Wait 6 seconds to go beyond the inaction timeframe
|
||||
static::getContainer()->get(InactiveExecutioner::class)->setNowTime(new \DateTime('+'.(self::CONDITION_SECONDS * 2).' seconds'));
|
||||
|
||||
// Remove a contact from the campaign
|
||||
$this->db->createQueryBuilder()->update(MAUTIC_TABLE_PREFIX.'campaign_leads')
|
||||
->set('manually_removed', 1)
|
||||
->where('lead_id = 1')
|
||||
->executeStatement();
|
||||
|
||||
// Now they should be inactive
|
||||
$this->testSymfonyCommand('mautic:campaigns:validate', ['--decision-id' => 3, '--contact-ids' => '1,2,3']);
|
||||
|
||||
// Only two contacts should have been considered inactive because one was marked as manually removed
|
||||
$byEvent = $this->getCampaignEventLogs([3, 7, 10]);
|
||||
$this->assertCount(2, $byEvent[3]); // decision recorded
|
||||
$this->assertCount(2, $byEvent[7]); // inactive event executed
|
||||
$this->assertCount(0, $byEvent[10]); // the positive path should be 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
INSERT INTO `#__emails` (`headers`, `id`,`category_id`,`translation_parent_id`,`variant_parent_id`,`unsubscribeform_id`,`is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`subject`,`from_address`,`from_name`,`reply_to_address`,`bcc_address`,`template`,`content`,`utm_tags`,`plain_text`,`custom_html`,`email_type`,`publish_up`,`publish_down`,`read_count`,`sent_count`,`revision`,`lang`,`variant_settings`,`variant_start_date`,`dynamic_content`,`variant_sent_count`,`variant_read_count`,`preference_center_id`)
|
||||
VALUES
|
||||
('[]',1,NULL,NULL,NULL,NULL,1,'2018-01-04 21:20:25',1,'Admin',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test Email 1',NULL,'Campaign Test Email 1',NULL,NULL,NULL,NULL,'blank','a:0:{}','a:4:{s:9:\"utmSource\";N;s:9:\"utmMedium\";N;s:11:\"utmCampaign\";N;s:10:\"utmContent\";N;}',NULL,'<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"\" class=\" js flexbox flexboxlegacy canvas canvastext webgl no-touch geolocation postmessage websqldatabase indexeddb hashchange history draganddrop websockets rgba hsla multiplebgs backgroundsize borderimage borderradius boxshadow textshadow opacity cssanimations csscolumns cssgradients cssreflections csstransforms csstransforms3d csstransitions fontface generatedcontent video audio localstorage sessionstorage webworkers applicationcache svg inlinesvg smil svgclippaths js csstransforms csstransforms3d csstransitions responsejs \"><head>\n <title>{subject}</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n <style type=\"text/css\" media=\"only screen and (max-width: 480px)\">\n /* Mobile styles */\n @media only screen and (max-width: 480px) {\n [class=\"w320\"] {\n width: 320px !important;\n }\n [class=\"mobile-block\"] {\n width: 100% !important;\n display: block !important;\n }\n }\n </style>\n </head>\n <body style=\"margin:0\" class=\"ui-sortable\">\n <div data-section-wrapper=\"1\">\n <center>\n <table data-section=\"1\" style=\"width: 600;\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\">\n <tbody>\n <tr>\n <td>\n <div data-slot-container=\"1\" style=\"min-height: 30px\" class=\"ui-sortable\">\n <div data-slot=\"text\">\n <br />\n <h2>Hello there!</h2>\n <br />\n We haven\'t heard from you for a while...\n <br />\n <br />\n {unsubscribe_text} | {webview_text}\n <br />\n </div>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </center>\n </div>\n</body></html>','template',NULL,NULL,0,0,1,'en','a:0:{}',NULL,'a:1:{i:0;a:3:{s:9:\"tokenName\";s:17:\"Dynamic Content 1\";s:7:\"content\";s:23:\"Default Dynamic Content\";s:7:\"filters\";a:1:{i:0;a:2:{s:7:\"content\";N;s:7:\"filters\";a:0:{}}}}}',0,0,NULL),
|
||||
('[]',2,NULL,NULL,NULL,NULL,1,'2018-01-04 21:21:07',1,'Admin',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test Email 2',NULL,'Campaign Test Email 2',NULL,NULL,NULL,NULL,'blank','a:0:{}','a:4:{s:9:\"utmSource\";N;s:9:\"utmMedium\";N;s:11:\"utmCampaign\";N;s:10:\"utmContent\";N;}',NULL,'<!DOCTYPE html>\n<html>\n <head>\n <title>{subject}</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n <style type=\"text/css\" media=\"only screen and (max-width: 480px)\">\n /* Mobile styles */\n @media only screen and (max-width: 480px) {\n\n [class=\"w320\"] {\n width: 320px !important;\n }\n\n [class=\"mobile-block\"] {\n width: 100% !important;\n display: block !important;\n }\n }\n </style>\n </head>\n <body style=\"margin:0\">\n <div data-section-wrapper=\"1\">\n <center>\n <table data-section=\"1\" style=\"width: 600;\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\">\n <tbody>\n <tr>\n <td>\n <div data-slot-container=\"1\" style=\"min-height: 30px\">\n <div data-slot=\"text\">\n <br />\n <h2>Hello there!</h2>\n <br />\n We haven\'t heard from you for a while...\n <br />\n <br />\n {unsubscribe_text} | {webview_text}\n <br />\n </div>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </center>\n </div>\n </body>\n</html>','template',NULL,NULL,0,0,1,'en','a:0:{}',NULL,'a:1:{i:0;a:3:{s:9:\"tokenName\";s:17:\"Dynamic Content 1\";s:7:\"content\";s:23:\"Default Dynamic Content\";s:7:\"filters\";a:1:{i:0;a:2:{s:7:\"content\";N;s:7:\"filters\";a:0:{}}}}}',0,0,NULL);
|
||||
|
||||
INSERT INTO `#__lead_tags` (`id`,`tag`)
|
||||
VALUES
|
||||
(1,'CampaignTest'),
|
||||
(2,'US:NotOpen'),
|
||||
(3,'NonUS:NotOpen'),
|
||||
(4,'UK:NotOpen'),
|
||||
(5,'NonUK:NotOpen'),
|
||||
(6,'US:Action'),
|
||||
(7,'NonUS:Action'),
|
||||
(8,'Campaign Test'),
|
||||
(9,'EmailNotOpen'),
|
||||
(10,'ChainedAction');
|
||||
|
||||
INSERT INTO `#__campaigns` (`allow_restart`,`id`,`category_id`,`is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`publish_up`,`publish_down`,`canvas_settings`)
|
||||
VALUES
|
||||
(0, 1, NULL, 1, '2018-01-04 21:41:05', 1, 'Admin', '2018-03-08 23:27:28', 1, 'Admin User', NULL, NULL, 'Admin User', 'Campaign Test', NULL, NULL, NULL, 'a:2:{s:5:\"nodes\";a:16:{i:0;a:3:{s:2:\"id\";s:1:\"1\";s:9:\"positionX\";s:3:\"577\";s:9:\"positionY\";s:3:\"155\";}i:1;a:3:{s:2:\"id\";s:1:\"2\";s:9:\"positionX\";s:3:\"842\";s:9:\"positionY\";s:3:\"164\";}i:2;a:3:{s:2:\"id\";s:1:\"3\";s:9:\"positionX\";s:3:\"842\";s:9:\"positionY\";s:3:\"269\";}i:3;a:3:{s:2:\"id\";s:2:\"11\";s:9:\"positionX\";s:3:\"389\";s:9:\"positionY\";s:3:\"252\";}i:4;a:3:{s:2:\"id\";s:1:\"4\";s:9:\"positionX\";s:4:\"1132\";s:9:\"positionY\";s:3:\"373\";}i:5;a:3:{s:2:\"id\";s:1:\"5\";s:9:\"positionX\";s:3:\"841\";s:9:\"positionY\";s:3:\"378\";}i:6;a:3:{s:2:\"id\";s:2:\"10\";s:9:\"positionX\";s:3:\"597\";s:9:\"positionY\";s:3:\"378\";}i:7;a:3:{s:2:\"id\";s:2:\"12\";s:9:\"positionX\";s:3:\"168\";s:9:\"positionY\";s:3:\"334\";}i:8;a:3:{s:2:\"id\";s:2:\"13\";s:9:\"positionX\";s:3:\"391\";s:9:\"positionY\";s:3:\"335\";}i:9;a:3:{s:2:\"id\";s:2:\"14\";s:9:\"positionX\";s:4:\"1372\";s:9:\"positionY\";s:3:\"364\";}i:10;a:3:{s:2:\"id\";s:1:\"6\";s:9:\"positionX\";s:3:\"649\";s:9:\"positionY\";s:3:\"496\";}i:11;a:3:{s:2:\"id\";s:1:\"7\";s:9:\"positionX\";s:3:\"874\";s:9:\"positionY\";s:3:\"488\";}i:12;a:3:{s:2:\"id\";s:1:\"8\";s:9:\"positionX\";s:4:\"1097\";s:9:\"positionY\";s:3:\"486\";}i:13;a:3:{s:2:\"id\";s:1:\"9\";s:9:\"positionX\";s:4:\"1313\";s:9:\"positionY\";s:3:\"491\";}i:14;a:3:{s:2:\"id\";s:2:\"15\";s:9:\"positionX\";s:4:\"1563\";s:9:\"positionY\";s:3:\"291\";}i:15;a:3:{s:2:\"id\";s:5:\"lists\";s:9:\"positionX\";s:3:\"677\";s:9:\"positionY\";s:2:\"50\";}}s:11:\"connections\";a:15:{i:0;a:3:{s:8:\"sourceId\";s:5:\"lists\";s:8:\"targetId\";s:1:\"1\";s:7:\"anchors\";a:2:{s:6:\"source\";s:10:\"leadsource\";s:6:\"target\";s:3:\"top\";}}i:1;a:3:{s:8:\"sourceId\";s:5:\"lists\";s:8:\"targetId\";s:1:\"2\";s:7:\"anchors\";a:2:{s:6:\"source\";s:10:\"leadsource\";s:6:\"target\";s:3:\"top\";}}i:2;a:3:{s:8:\"sourceId\";s:1:\"2\";s:8:\"targetId\";s:1:\"3\";s:7:\"anchors\";a:2:{s:6:\"source\";s:6:\"bottom\";s:6:\"target\";s:3:\"top\";}}i:3;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:1:\"4\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:4;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:1:\"5\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:5;a:3:{s:8:\"sourceId\";s:1:\"5\";s:8:\"targetId\";s:1:\"6\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:6;a:3:{s:8:\"sourceId\";s:1:\"5\";s:8:\"targetId\";s:1:\"7\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:7;a:3:{s:8:\"sourceId\";s:1:\"4\";s:8:\"targetId\";s:1:\"8\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:8;a:3:{s:8:\"sourceId\";s:1:\"4\";s:8:\"targetId\";s:1:\"9\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:9;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:2:\"10\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:10;a:3:{s:8:\"sourceId\";s:1:\"1\";s:8:\"targetId\";s:2:\"11\";s:7:\"anchors\";a:2:{s:6:\"source\";s:6:\"bottom\";s:6:\"target\";s:3:\"top\";}}i:11;a:3:{s:8:\"sourceId\";s:2:\"11\";s:8:\"targetId\";s:2:\"12\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:12;a:3:{s:8:\"sourceId\";s:2:\"11\";s:8:\"targetId\";s:2:\"13\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:13;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:2:\"14\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:14;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:2:\"15\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}}}');
|
||||
|
||||
INSERT INTO `#__campaign_events` (`id`,`campaign_id`,`parent_id`,`name`,`description`,`type`,`event_type`,`event_order`,`properties`,`trigger_date`,`trigger_interval`,`trigger_interval_unit`,`trigger_mode`,`decision_path`,`temp_id`,`channel`,`channel_id`,`failed_count`)
|
||||
VALUES
|
||||
(1,1,NULL,'Tag CampaignTest',NULL,'lead.changetags','action',1,'a:19:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"577\";s:8:\"droppedY\";s:3:\"155\";}s:4:\"name\";s:0:\"\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:10:\"leadsource\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"1\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:6:\"source\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"settings\";a:4:{s:5:\"label\";s:21:\"Modify contact\'s tags\";s:11:\"description\";s:37:\"Add tag to or remove tag from contact\";s:8:\"formType\";s:16:\"modify_lead_tags\";s:9:\"eventName\";s:38:\"mautic.lead.on_campaign_trigger_action\";}s:6:\"tempId\";s:43:\"new86745ed0771cdfff79549cd84f17e9cc894e8903\";s:2:\"id\";s:43:\"new86745ed0771cdfff79549cd84f17e9cc894e8903\";s:8:\"add_tags\";a:1:{i:0;s:13:\"Campaign Test\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate',NULL,'new86745ed0771cdfff79549cd84f17e9cc894e8903',NULL,NULL,0),
|
||||
(2,1,NULL,'Send email 1',NULL,'email.send','action',1,'a:21:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"337\";s:8:\"droppedY\";s:3:\"155\";}s:4:\"name\";s:0:\"\";s:11:\"triggerMode\";s:4:\"date\";s:11:\"triggerDate\";O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2018-01-04 15:32:00.000000\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:27:\"America/North_Dakota/Center\";}s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:10:\"leadsource\";s:10:\"properties\";a:4:{s:5:\"email\";s:1:\"1\";s:10:\"email_type\";s:13:\"transactional\";s:8:\"priority\";s:1:\"2\";s:8:\"attempts\";s:1:\"3\";}s:4:\"type\";s:10:\"email.send\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:6:\"source\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"settings\";a:8:{s:5:\"label\";s:10:\"Send email\";s:11:\"description\";s:39:\"Send the selected email to the contact.\";s:9:\"eventName\";s:39:\"mautic.email.on_campaign_trigger_action\";s:8:\"formType\";s:14:\"emailsend_list\";s:15:\"formTypeOptions\";a:2:{s:13:\"update_select\";s:30:\"campaignevent_properties_email\";s:16:\"with_email_types\";b:1;}s:9:\"formTheme\";s:41:\"MauticEmailBundle:FormTheme\\EmailSendList\";s:7:\"channel\";s:5:\"email\";s:14:\"channelIdField\";s:5:\"email\";}s:6:\"tempId\";s:43:\"new8d8d68a07561544b6ae052e1344932e9dcff4f53\";s:2:\"id\";s:43:\"new8d8d68a07561544b6ae052e1344932e9dcff4f53\";s:5:\"email\";s:1:\"1\";s:10:\"email_type\";s:13:\"transactional\";s:8:\"priority\";i:2;s:8:\"attempts\";d:3;}','{SEND_EMAIL_1_TIMESTAMP}',1,'d','date',NULL,'new8d8d68a07561544b6ae052e1344932e9dcff4f53','email',1,0),
|
||||
(3,1,2,'Opens email',NULL,'email.open','decision',2,'a:0:{}',NULL,0,NULL,NULL,NULL,'new7b70bc0bee1752ab041c85806f62e4480d582671',NULL,NULL,0),
|
||||
(4,1,3,'Is UK',NULL,'lead.field_value','condition',3,'a:17:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"942\";s:8:\"droppedY\";s:3:\"374\";}s:4:\"name\";s:5:\"Is UK\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:3:{s:5:\"field\";s:7:\"country\";s:8:\"operator\";s:1:\"=\";s:5:\"value\";s:14:\"United Kingdom\";}s:4:\"type\";s:16:\"lead.field_value\";s:9:\"eventType\";s:9:\"condition\";s:15:\"anchorEventType\";s:8:\"decision\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:5:\"field\";s:7:\"country\";s:8:\"operator\";s:1:\"=\";s:5:\"value\";s:14:\"United Kingdom\";}','{CONDITION_TIMESTAMP}',1,'d','date','no','new50630b403c8ee496674257bcd8eb739987cb681c',NULL,NULL,0),
|
||||
(5,1,3,'Is US',NULL,'lead.field_value','condition',3,'a:17:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"942\";s:8:\"droppedY\";s:3:\"374\";}s:4:\"name\";s:5:\"Is US\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:3:{s:5:\"field\";s:7:\"country\";s:8:\"operator\";s:1:\"=\";s:5:\"value\";s:13:\"United States\";}s:4:\"type\";s:16:\"lead.field_value\";s:9:\"eventType\";s:9:\"condition\";s:15:\"anchorEventType\";s:8:\"decision\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:5:\"field\";s:7:\"country\";s:8:\"operator\";s:1:\"=\";s:5:\"value\";s:13:\"United States\";}','{CONDITION_TIMESTAMP}',1,'d','date','no','new7e053fe42a72aadb09dd679698216c5a020fdc9c',NULL,NULL,0),
|
||||
(6,1,5,'Tag US:NotOpen',NULL,'lead.changetags','action',4,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"741\";s:8:\"droppedY\";s:3:\"483\";}s:4:\"name\";s:14:\"Tag US:NotOpen\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:3:\"yes\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"2\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:9:\"condition\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:10:\"US:NotOpen\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate','yes','new258deb14812923521ffc071e80507c7c173c9bf2',NULL,NULL,0),
|
||||
(7,1,5,'Tag NonUS:NotOpen',NULL,'lead.changetags','action',4,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"981\";s:8:\"droppedY\";s:3:\"483\";}s:4:\"name\";s:17:\"Tag NonUS:NotOpen\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"3\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:9:\"condition\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:13:\"NonUS:NotOpen\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate','no','newa884023f692e4286283972779ee800d590a7c497',NULL,NULL,0),
|
||||
(8,1,4,'Tag UK:NotOpen',NULL,'lead.changetags','action',4,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"501\";s:8:\"droppedY\";s:3:\"480\";}s:4:\"name\";s:14:\"Tag UK:NotOpen\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:3:\"yes\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"4\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:9:\"condition\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:10:\"UK:NotOpen\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate','yes','new2519374e41eb9686a786cc6cbf4ea8a91e7a3491',NULL,NULL,0),
|
||||
(9,1,4,'Tag NonUK:NotOpen',NULL,'lead.changetags','action',4,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:4:\"1221\";s:8:\"droppedY\";s:3:\"480\";}s:4:\"name\";s:17:\"Tag NonUK:NotOpen\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"5\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:9:\"condition\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:13:\"NonUK:NotOpen\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate','no','new41e0645e966bf4d3af3f768e3f57b95574ae1683',NULL,NULL,0),
|
||||
(10,1,3,'Send email 2',NULL,'email.send','action',3,'a:18:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"742\";s:8:\"droppedY\";s:3:\"374\";}s:4:\"name\";s:12:\"Send email 2\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:3:\"yes\";s:10:\"properties\";a:4:{s:5:\"email\";s:1:\"2\";s:10:\"email_type\";s:13:\"transactional\";s:8:\"priority\";s:1:\"2\";s:8:\"attempts\";s:1:\"3\";}s:4:\"type\";s:10:\"email.send\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:8:\"decision\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:5:\"email\";s:1:\"2\";s:10:\"email_type\";s:13:\"transactional\";s:8:\"priority\";i:2;s:8:\"attempts\";d:3;}',NULL,1,'d','immediate','yes','new82c4acf5ed2d115566d7a0f27b3a844dc2732211','email',2,0),
|
||||
(11,1,1,'Is US',NULL,'lead.field_value','condition',2,'a:17:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"577\";s:8:\"droppedY\";s:3:\"260\";}s:4:\"name\";s:5:\"Is US\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:6:\"bottom\";s:10:\"properties\";a:3:{s:5:\"field\";s:7:\"country\";s:8:\"operator\";s:1:\"=\";s:5:\"value\";s:13:\"United States\";}s:4:\"type\";s:16:\"lead.field_value\";s:9:\"eventType\";s:9:\"condition\";s:15:\"anchorEventType\";s:6:\"action\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:5:\"field\";s:7:\"country\";s:8:\"operator\";s:1:\"=\";s:5:\"value\";s:13:\"United States\";}',NULL,1,'d','immediate',NULL,'new851108680198a4802062cf78f0d6db86407899a5',NULL,NULL,0),
|
||||
(12,1,11,'Tag US:Action',NULL,'lead.changetags','action',3,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:2:\"12\";s:8:\"droppedY\";s:3:\"357\";}s:4:\"name\";s:13:\"Tag US:Action\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:3:\"yes\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"6\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:9:\"condition\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:9:\"US:Action\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate','yes','new679bfa3e62cb59526de7fd27b556443485a174f0',NULL,NULL,0),
|
||||
(13,1,11,'Tag NonUS:Action',NULL,'lead.changetags','action',3,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"489\";s:8:\"droppedY\";s:3:\"357\";}s:4:\"name\";s:16:\"Tag NonUS:Action\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"7\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:9:\"condition\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:12:\"NonUS:Action\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate','no','new62e190055219ebe5beb9df4c4a505bb0860fffd4',NULL,NULL,0),
|
||||
(14,1,3,'Tag EmailNotOpen',NULL,'lead.changetags','action',3,'a:19:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:4:\"1081\";s:8:\"droppedY\";s:3:\"374\";}s:4:\"name\";s:16:\"Tag EmailNotOpen\";s:11:\"triggerMode\";s:8:\"interval\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"9\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:8:\"decision\";s:10:\"campaignId\";s:1:\"1\";s:6:\"_token\";s:43:\"oRiunE5unGEBGhTql8VkzvtTkMHpwElCu5Ul4-_gd-I\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"settings\";a:4:{s:5:\"label\";s:21:\"Modify contact\'s tags\";s:11:\"description\";s:37:\"Add tag to or remove tag from contact\";s:8:\"formType\";s:16:\"modify_lead_tags\";s:9:\"eventName\";s:38:\"mautic.lead.on_campaign_trigger_action\";}s:6:\"tempId\";s:43:\"newb3e5bfd9cdc154619ca0716b46f4a61328688a26\";s:2:\"id\";s:43:\"newb3e5bfd9cdc154619ca0716b46f4a61328688a26\";s:8:\"add_tags\";a:1:{i:0;s:12:\"EmailNotOpen\";}s:11:\"remove_tags\";a:0:{}}',NULL,2,'i','interval','no','newb3e5bfd9cdc154619ca0716b46f4a61328688a26',NULL,NULL,0),
|
||||
(15,1,3,'Tag EmailNotOpen Again',NULL,'lead.changetags','action',3,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:4:\"1612\";s:8:\"droppedY\";s:3:\"374\";}s:4:\"name\";s:22:\"Tag EmailNotOpen Again\";s:11:\"triggerMode\";s:8:\"interval\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"6\";s:19:\"triggerIntervalUnit\";s:1:\"i\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"9\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:8:\"decision\";s:10:\"campaignId\";s:1:\"1\";s:6:\"_token\";s:43:\"Wd8bGtv2HJ6Nyf3K90Efoo2Rn2VkDWwXhwzCIPMiD-M\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:12:\"EmailNotOpen\";}s:11:\"remove_tags\";a:0:{}}',NULL,6,'i','interval','no','newf16dfec5f2a65aa9c527675e7be516020a90daa6',NULL,NULL,0),
|
||||
(16,1,12,'Tag ChainedAction',NULL,'lead.changetags','action',4,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"168\";s:8:\"droppedY\";s:3:\"439\";}s:4:\"name\";s:14:\"Chained Action\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:6:\"bottom\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:2:\"10\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:6:\"action\";s:10:\"campaignId\";s:1:\"1\";s:6:\"_token\";s:43:\"6xgHe74aRnc1V7AGzdang3-iJ0Ub5BKfbdU5NsxQmv0\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:13:\"ChainedAction\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate',NULL,'new60f74507aeccf217f78647e41ae29af51debe666',NULL,NULL,0);
|
||||
|
||||
INSERT INTO `#__lead_lists` (`id`,`is_preference_center`, `is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`alias`,`filters`,`is_global`, `public_name`)
|
||||
VALUES
|
||||
(1,0,1,'2018-01-04 23:41:20',1,'Admin User',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test',NULL,'campaign-test','a:0:{}',1,'campaign-test');
|
||||
|
||||
INSERT INTO `#__campaign_leadlist_xref` (`campaign_id`,`leadlist_id`)
|
||||
VALUES
|
||||
(1,1);
|
||||
|
||||
INSERT INTO `#__campaign_leads` (`campaign_id`,`lead_id`,`date_added`,`manually_removed`,`manually_added`,`date_last_exited`,`rotation`)
|
||||
VALUES
|
||||
(1,1,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,2,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,3,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,4,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,5,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,6,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,7,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,8,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,9,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,10,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,11,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,12,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,13,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,14,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,15,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,16,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,17,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,18,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,19,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,20,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,21,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,22,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,23,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,24,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,25,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,26,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,27,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,28,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,29,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,30,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,31,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,32,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,33,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,34,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,35,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,36,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,37,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,38,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,39,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,40,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,41,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,42,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,43,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,44,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,45,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,46,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,47,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,48,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,49,'2018-01-04 22:47:30',0,1,NULL,1),
|
||||
(1,50,'2018-01-04 22:47:30',0,1,NULL,1);
|
||||
|
||||
INSERT INTO `#__lead_lists_leads` (`leadlist_id`,`lead_id`,`date_added`,`manually_removed`,`manually_added`)
|
||||
VALUES
|
||||
(1,1,'2018-01-04 22:47:00',0,1),
|
||||
(1,2,'2018-01-04 22:47:00',0,1),
|
||||
(1,3,'2018-01-04 22:47:00',0,1),
|
||||
(1,4,'2018-01-04 22:47:00',0,1),
|
||||
(1,5,'2018-01-04 22:47:00',0,1),
|
||||
(1,6,'2018-01-04 22:47:00',0,1),
|
||||
(1,7,'2018-01-04 22:47:00',0,1),
|
||||
(1,8,'2018-01-04 22:47:00',0,1),
|
||||
(1,9,'2018-01-04 22:47:00',0,1),
|
||||
(1,10,'2018-01-04 22:47:00',0,1),
|
||||
(1,11,'2018-01-04 22:47:00',0,1),
|
||||
(1,12,'2018-01-04 22:47:00',0,1),
|
||||
(1,13,'2018-01-04 22:47:00',0,1),
|
||||
(1,14,'2018-01-04 22:47:00',0,1),
|
||||
(1,15,'2018-01-04 22:47:00',0,1),
|
||||
(1,16,'2018-01-04 22:47:00',0,1),
|
||||
(1,17,'2018-01-04 22:47:00',0,1),
|
||||
(1,18,'2018-01-04 22:47:00',0,1),
|
||||
(1,19,'2018-01-04 22:47:00',0,1),
|
||||
(1,20,'2018-01-04 22:47:00',0,1),
|
||||
(1,21,'2018-01-04 22:47:00',0,1),
|
||||
(1,22,'2018-01-04 22:47:00',0,1),
|
||||
(1,23,'2018-01-04 22:47:00',0,1),
|
||||
(1,24,'2018-01-04 22:47:00',0,1),
|
||||
(1,25,'2018-01-04 22:47:00',0,1),
|
||||
(1,26,'2018-01-04 22:47:00',0,1),
|
||||
(1,27,'2018-01-04 22:47:00',0,1),
|
||||
(1,28,'2018-01-04 22:47:00',0,1),
|
||||
(1,29,'2018-01-04 22:47:00',0,1),
|
||||
(1,30,'2018-01-04 22:47:00',0,1),
|
||||
(1,31,'2018-01-04 22:47:00',0,1),
|
||||
(1,32,'2018-01-04 22:47:00',0,1),
|
||||
(1,33,'2018-01-04 22:47:00',0,1),
|
||||
(1,34,'2018-01-04 22:47:00',0,1),
|
||||
(1,35,'2018-01-04 22:47:00',0,1),
|
||||
(1,36,'2018-01-04 22:47:00',0,1),
|
||||
(1,37,'2018-01-04 22:47:00',0,1),
|
||||
(1,38,'2018-01-04 22:47:00',0,1),
|
||||
(1,39,'2018-01-04 22:47:00',0,1),
|
||||
(1,40,'2018-01-04 22:47:00',0,1),
|
||||
(1,41,'2018-01-04 22:47:00',0,1),
|
||||
(1,42,'2018-01-04 22:47:00',0,1),
|
||||
(1,43,'2018-01-04 22:47:00',0,1),
|
||||
(1,44,'2018-01-04 22:47:00',0,1),
|
||||
(1,45,'2018-01-04 22:47:00',0,1),
|
||||
(1,46,'2018-01-04 22:47:00',0,1),
|
||||
(1,47,'2018-01-04 22:47:00',0,1),
|
||||
(1,48,'2018-01-04 22:47:00',0,1),
|
||||
(1,49,'2018-01-04 22:47:00',0,1),
|
||||
(1,50,'2018-01-04 22:47:00',0,1);
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Tests\Functional\Fixtures\FixtureHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AjaxControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private FixtureHelper $campaignFixturesHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->campaignFixturesHelper = new FixtureHelper($this->em);
|
||||
}
|
||||
|
||||
public function testCancelScheduledCampaignEventAction(): void
|
||||
{
|
||||
$this->campaignFixturesHelper = new FixtureHelper($this->em);
|
||||
$contact = $this->campaignFixturesHelper->createContact('some@contact.email');
|
||||
$campaign = $this->campaignFixturesHelper->createCampaign('Scheduled event test');
|
||||
$this->campaignFixturesHelper->addContactToCampaign($contact, $campaign);
|
||||
$this->campaignFixturesHelper->createCampaignWithScheduledEvent($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$commandResult = $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId()]);
|
||||
|
||||
Assert::assertStringContainsString('1 total event was scheduled', $commandResult->getDisplay());
|
||||
|
||||
$payload = [
|
||||
'action' => 'campaign:cancelScheduledCampaignEvent',
|
||||
'eventId' => $campaign->getEvents()[0]->getId(),
|
||||
'contactId' => $contact->getId(),
|
||||
];
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
|
||||
// Ensure we'll fetch fresh data from the database and not from entity manager.
|
||||
$this->em->detach($contact);
|
||||
$this->em->detach($campaign);
|
||||
|
||||
/** @var LeadEventLogRepository $leadEventLogRepository */
|
||||
$leadEventLogRepository = $this->em->getRepository(LeadEventLog::class);
|
||||
|
||||
/** @var LeadEventLog $log */
|
||||
$log = $leadEventLogRepository->findOneBy(['lead' => $contact, 'campaign' => $campaign]);
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
Assert::assertSame('{"success":1}', $this->client->getResponse()->getContent());
|
||||
Assert::assertFalse($log->getIsScheduled());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Tests\Functional\Fixtures\FixtureHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\UserEntityTrait;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Helper\MailHelper;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class CampaignApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use UserEntityTrait;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->configParams['mailer_from_name'] = 'Mautic Admin';
|
||||
$this->configParams['mailer_from_email'] = 'admin@email.com';
|
||||
$this->useCleanupRollback = false;
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and persists common test entities used across multiple tests.
|
||||
*
|
||||
* @return array<string, mixed> Array containing the created entities
|
||||
*/
|
||||
private function createTestEntities(): array
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$segment = new LeadList();
|
||||
$segment->setName('test');
|
||||
$segment->setAlias('test');
|
||||
$segment->setPublicName('test');
|
||||
|
||||
$email = new Email();
|
||||
$email->setName('test');
|
||||
$email->setSubject('Ahoy {contactfield=email}');
|
||||
$email->setCustomHtml('Your email is <b>{contactfield=email}</b>');
|
||||
$email->setUseOwnerAsMailer(true);
|
||||
|
||||
$dwc = new DynamicContent();
|
||||
$dwc->setName('test');
|
||||
$dwc->setSlotName('test');
|
||||
$dwc->setContent('test');
|
||||
|
||||
$company = new Company();
|
||||
$company->setName('test');
|
||||
|
||||
$contact1 = new Lead();
|
||||
$contact1->setEmail('contact@one.email');
|
||||
|
||||
$contact2 = new Lead();
|
||||
$contact2->setEmail('contact@two.email');
|
||||
$contact2->setOwner($user);
|
||||
|
||||
$member1 = new ListLead();
|
||||
$member1->setLead($contact1);
|
||||
$member1->setList($segment);
|
||||
$member1->setDateAdded(new \DateTime());
|
||||
|
||||
$member2 = new ListLead();
|
||||
$member2->setLead($contact2);
|
||||
$member2->setList($segment);
|
||||
$member2->setDateAdded(new \DateTime());
|
||||
|
||||
$this->em->persist($segment);
|
||||
$this->em->persist($email);
|
||||
$this->em->persist($dwc);
|
||||
$this->em->persist($company);
|
||||
$this->em->persist($contact1);
|
||||
$this->em->persist($contact2);
|
||||
$this->em->persist($member1);
|
||||
$this->em->persist($member2);
|
||||
$this->em->flush();
|
||||
|
||||
return [
|
||||
'user' => $user,
|
||||
'segment' => $segment,
|
||||
'email' => $email,
|
||||
'dwc' => $dwc,
|
||||
'company' => $company,
|
||||
'contact1' => $contact1,
|
||||
'contact2' => $contact2,
|
||||
];
|
||||
}
|
||||
|
||||
public function testCreateNewCampaign(): void
|
||||
{
|
||||
$entities = $this->createTestEntities();
|
||||
$user = $entities['user'];
|
||||
$segment = $entities['segment'];
|
||||
$email = $entities['email'];
|
||||
$dwc = $entities['dwc'];
|
||||
$company = $entities['company'];
|
||||
|
||||
$payload = [
|
||||
'name' => 'test',
|
||||
'description' => 'Created via API',
|
||||
'events' => [
|
||||
[
|
||||
'id' => 'new_43', // Event ID will be replaced on /new
|
||||
'name' => 'DWC event test',
|
||||
'description' => 'API test',
|
||||
'type' => 'dwc.decision',
|
||||
'eventType' => 'decision',
|
||||
'order' => 1,
|
||||
'properties' => [
|
||||
'dwc_slot_name' => 'test',
|
||||
'dynamicContent' => $dwc->getId(),
|
||||
],
|
||||
'triggerInterval' => 0,
|
||||
'triggerIntervalUnit' => null,
|
||||
'triggerMode' => null,
|
||||
'children' => [
|
||||
'new_55', // Event ID will be replaced on /new
|
||||
],
|
||||
'parent' => null,
|
||||
'decisionPath' => null,
|
||||
],
|
||||
[
|
||||
'id' => 'new_44', // Event ID will be replaced on /new
|
||||
'name' => 'Send email',
|
||||
'description' => 'API test',
|
||||
'type' => 'email.send',
|
||||
'eventType' => 'action',
|
||||
'order' => 2,
|
||||
'properties' => [
|
||||
'email' => $email->getId(),
|
||||
'email_type' => MailHelper::EMAIL_TYPE_TRANSACTIONAL,
|
||||
],
|
||||
'triggerInterval' => 0,
|
||||
'triggerIntervalUnit' => 'd',
|
||||
'triggerMode' => 'interval',
|
||||
'children' => [],
|
||||
'parent' => null,
|
||||
'decisionPath' => 'yes',
|
||||
],
|
||||
[
|
||||
'id' => 'new_55', // Event ID will be replaced on /new
|
||||
'name' => 'Add to company action',
|
||||
'description' => 'API test',
|
||||
'type' => 'lead.addtocompany',
|
||||
'eventType' => 'action',
|
||||
'order' => 2,
|
||||
'properties' => [
|
||||
'company' => $company->getId(),
|
||||
],
|
||||
'triggerInterval' => 1,
|
||||
'triggerIntervalUnit' => 'd',
|
||||
'triggerMode' => 'interval',
|
||||
'children' => [],
|
||||
'parent' => 'new_43', // Event ID will be replaced on /new
|
||||
'decisionPath' => 'no',
|
||||
],
|
||||
],
|
||||
'forms' => [],
|
||||
'lists' => [
|
||||
[
|
||||
'id' => $segment->getId(),
|
||||
],
|
||||
],
|
||||
'canvasSettings' => [
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => 'new_43', // Event ID will be replaced on /new
|
||||
'positionX' => '650',
|
||||
'positionY' => '189',
|
||||
],
|
||||
[
|
||||
'id' => 'new_44', // Event ID will be replaced on /new
|
||||
'positionX' => '433',
|
||||
'positionY' => '348',
|
||||
],
|
||||
[
|
||||
'id' => 'new_55', // Event ID will be replaced on /new
|
||||
'positionX' => '750',
|
||||
'positionY' => '411',
|
||||
],
|
||||
[
|
||||
'id' => 'lists',
|
||||
'positionX' => '629',
|
||||
'positionY' => '65',
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => 'new_43', // Event ID will be replaced on /new
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => 'new_44', // Event ID will be replaced on /new
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
[
|
||||
'sourceId' => 'new_43', // Event ID will be replaced on /new
|
||||
'targetId' => 'new_55', // Event ID will be replaced on /new
|
||||
'anchors' => [
|
||||
'source' => 'no',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->client->request(Request::METHOD_POST, 'api/campaigns/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseStatusCodeSame(201, $clientResponse->getContent());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$campaignId = $response['campaign']['id'];
|
||||
Assert::assertGreaterThan(0, $campaignId);
|
||||
Assert::assertEquals($payload['name'], $response['campaign']['name']);
|
||||
Assert::assertEquals($payload['description'], $response['campaign']['description']);
|
||||
Assert::assertEquals($payload['events'][0]['name'], $response['campaign']['events'][0]['name']);
|
||||
Assert::assertEquals($segment->getId(), $response['campaign']['lists'][0]['id']);
|
||||
|
||||
$commandTester = $this->testSymfonyCommand('mautic:campaigns:update', ['-i' => $campaignId]);
|
||||
$commandTester->assertCommandIsSuccessful();
|
||||
Assert::assertStringContainsString('2 total contact(s) to be added', $commandTester->getDisplay());
|
||||
Assert::assertStringContainsString('100%', $commandTester->getDisplay());
|
||||
|
||||
$commandTester = $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => $campaignId]);
|
||||
$commandTester->assertCommandIsSuccessful();
|
||||
// 2 events were executed for each of the 2 contacts (= 4). The third event is waiting for the decision interval.
|
||||
Assert::assertStringContainsString('4 total events were executed', $commandTester->getDisplay());
|
||||
|
||||
$this->assertQueuedEmailCount(2);
|
||||
|
||||
$email1 = $this->getMailerMessagesByToAddress('contact@one.email')[0];
|
||||
|
||||
// The email is has mailer is owner ON but this contact doesn't have any owner. So it uses default FROM and Reply-To.
|
||||
Assert::assertSame('Ahoy contact@one.email', $email1->getSubject());
|
||||
Assert::assertMatchesRegularExpression('#Your email is <b>contact@one\.email<\/b><img height="1" width="1" src="https:\/\/localhost\/email\/[a-z0-9]+\.gif\?ct=[^"]+" alt="" \/>#', $email1->getHtmlBody());
|
||||
Assert::assertSame('Your email is contact@one.email', $email1->getTextBody());
|
||||
Assert::assertCount(1, $email1->getFrom());
|
||||
Assert::assertSame($this->configParams['mailer_from_name'], $email1->getFrom()[0]->getName());
|
||||
Assert::assertSame($this->configParams['mailer_from_email'], $email1->getFrom()[0]->getAddress());
|
||||
Assert::assertCount(1, $email1->getTo());
|
||||
Assert::assertSame('', $email1->getTo()[0]->getName());
|
||||
Assert::assertSame($entities['contact1']->getEmail(), $email1->getTo()[0]->getAddress());
|
||||
Assert::assertCount(1, $email1->getReplyTo());
|
||||
Assert::assertSame('', $email1->getReplyTo()[0]->getName());
|
||||
Assert::assertSame($this->configParams['mailer_from_email'], $email1->getReplyTo()[0]->getAddress());
|
||||
|
||||
$email2 = $this->getMailerMessagesByToAddress('contact@two.email')[0];
|
||||
|
||||
// This contact does have an owner so it uses FROM and Rply-to from the owner.
|
||||
Assert::assertSame('Ahoy contact@two.email', $email2->getSubject());
|
||||
Assert::assertMatchesRegularExpression('#Your email is <b>contact@two\.email<\/b><img height="1" width="1" src="https:\/\/localhost\/email\/[a-z0-9]+\.gif\?ct=[^"]*" alt="" \/>#', $email2->getHtmlBody());
|
||||
Assert::assertSame('Your email is contact@two.email', $email2->getTextBody());
|
||||
Assert::assertCount(1, $email2->getFrom());
|
||||
Assert::assertSame($user->getName(), $email2->getFrom()[0]->getName());
|
||||
Assert::assertSame($user->getEmail(), $email2->getFrom()[0]->getAddress());
|
||||
Assert::assertCount(1, $email2->getTo());
|
||||
Assert::assertSame('', $email2->getTo()[0]->getName());
|
||||
Assert::assertSame($entities['contact2']->getEmail(), $email2->getTo()[0]->getAddress());
|
||||
Assert::assertCount(1, $email2->getReplyTo());
|
||||
Assert::assertSame('', $email2->getReplyTo()[0]->getName());
|
||||
Assert::assertSame($user->getEmail(), $email2->getReplyTo()[0]->getAddress());
|
||||
|
||||
// Search for this campaign:
|
||||
$this->client->request(Request::METHOD_GET, "/api/campaigns?search=ids:{$response['campaign']['id']}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful($clientResponse->getContent());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertEquals($payload['name'], $response['campaigns'][$campaignId]['name'], $clientResponse->getContent());
|
||||
Assert::assertEquals($payload['description'], $response['campaigns'][$campaignId]['description'], $clientResponse->getContent());
|
||||
Assert::assertEquals($payload['events'][0]['name'], $response['campaigns'][$campaignId]['events'][0]['name'], $clientResponse->getContent());
|
||||
Assert::assertEquals($segment->getId(), $response['campaigns'][$campaignId]['lists'][0]['id'], $clientResponse->getContent());
|
||||
}
|
||||
|
||||
public function testExportCampaignAction(): void
|
||||
{
|
||||
$entities = $this->createTestEntities();
|
||||
$user = $entities['user'];
|
||||
$segment = $entities['segment'];
|
||||
$email = $entities['email'];
|
||||
$dwc = $entities['dwc'];
|
||||
$company = $entities['company'];
|
||||
|
||||
// Create the campaign
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('test campaign');
|
||||
$campaign->setDescription('Test campaign for export');
|
||||
|
||||
// Create events
|
||||
$event1 = new Event();
|
||||
$event1->setName('DWC event test');
|
||||
$event1->setDescription('API test');
|
||||
$event1->setType('dwc.decision');
|
||||
$event1->setEventType('decision'); // Set the event type
|
||||
$event1->setCampaign($campaign); // Set the campaign for this event
|
||||
$event1->setTriggerWindow(null);
|
||||
|
||||
$event2 = new Event();
|
||||
$event2->setName('Send email');
|
||||
$event2->setDescription('API test');
|
||||
$event2->setType('email.send');
|
||||
$event2->setEventType('action'); // Set the event type
|
||||
$event2->setCampaign($campaign); // Set the campaign for this event
|
||||
$event2->setTriggerWindow(null);
|
||||
|
||||
// Add events to the campaign (using addEvents)
|
||||
$campaign->addEvents([
|
||||
'new_43' => $event1, // Key for event1
|
||||
'new_44' => $event2, // Key for event2
|
||||
]);
|
||||
|
||||
// Persist campaign and events
|
||||
$this->em->persist($event1);
|
||||
$this->em->persist($event2);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
// Export the campaign
|
||||
$this->client->request(Request::METHOD_GET, '/api/campaigns/export/99999');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseStatusCodeSame(404, (string) $clientResponse->getStatusCode());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/api/campaigns/export/'.$campaign->getId());
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
// Check response status code
|
||||
$this->assertResponseStatusCodeSame(200, (string) $clientResponse->getStatusCode());
|
||||
|
||||
// Decode the response content
|
||||
$responseData = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Ensure the response contains campaign data
|
||||
$this->assertNotEmpty($responseData);
|
||||
$this->assertArrayHasKey('campaign', $responseData[0]);
|
||||
|
||||
// Since 'campaign' is an array, we'll need to check the first element
|
||||
$this->assertArrayHasKey('name', $responseData[0]['campaign'][0]); // Access the first campaign in the array
|
||||
$this->assertEquals($campaign->getName(), $responseData[0]['campaign'][0]['name']);
|
||||
$this->assertEquals($campaign->getDescription(), $responseData[0]['campaign'][0]['description']);
|
||||
|
||||
// Check if the campaign export includes the expected events
|
||||
$this->assertCount(2, $responseData[0]['campaign_event']);
|
||||
|
||||
// Ensure proper serialization of the campaign events
|
||||
foreach ($responseData[0]['campaign_event'] as $event) {
|
||||
$this->assertArrayHasKey('id', $event);
|
||||
$this->assertArrayHasKey('name', $event);
|
||||
// Additional checks for event properties if necessary
|
||||
}
|
||||
}
|
||||
|
||||
public function testExportCampaignActionAccessDenied(): void
|
||||
{
|
||||
// Create a user without export permissions
|
||||
$nonAdminUser = $this->createUserWithPermission([
|
||||
'user-name' => 'non-admin',
|
||||
'email' => 'non-admin@mautic-test.com',
|
||||
'first-name' => 'non-admin',
|
||||
'last-name' => 'non-admin',
|
||||
'role' => [
|
||||
'name' => 'perm_non_admin',
|
||||
'permissions' => [
|
||||
'campaign:campaigns' => 2,
|
||||
'campaign:export:enable' => 2,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginUser($nonAdminUser);
|
||||
|
||||
// Create and persist a campaign
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test Campaign');
|
||||
$campaign->setDescription('Test description');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
// Attempt to export the campaign
|
||||
$this->client->request(Request::METHOD_GET, '/api/campaigns/export/'.$campaign->getId());
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
// Assert that access is denied
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testImportCampaignActionJson(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/api/campaigns/import',
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
json_encode(FixtureHelper::getPayload(), JSON_PRETTY_PRINT)
|
||||
);
|
||||
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
// Debug early exit if something fails
|
||||
if (201 !== $clientResponse->getStatusCode()) {
|
||||
$this->fail('Import failed with error: '.$clientResponse->getContent());
|
||||
}
|
||||
|
||||
// Success check
|
||||
$this->assertResponseStatusCodeSame(201, 'Expected status code 201 for successful import.');
|
||||
$responseData = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertIsArray($responseData);
|
||||
$this->assertContains('Import successful: Imported campaigns are switched off by default.', $responseData);
|
||||
}
|
||||
|
||||
public function testImportCampaignActionZip(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Create temporary zip file
|
||||
$zip = new \ZipArchive();
|
||||
$zipPath = tempnam(sys_get_temp_dir(), 'mautic_zip_test').'.zip';
|
||||
|
||||
if (true === $zip->open($zipPath, \ZipArchive::CREATE)) {
|
||||
$zip->addFromString('campaign.json', json_encode(FixtureHelper::getPayload(), JSON_PRETTY_PRINT));
|
||||
$zip->close();
|
||||
} else {
|
||||
$this->fail('Failed to create test ZIP file.');
|
||||
}
|
||||
|
||||
// Upload via API
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/api/campaigns/import',
|
||||
[],
|
||||
['file' => new \Symfony\Component\HttpFoundation\File\UploadedFile($zipPath, 'import.zip')],
|
||||
['CONTENT_TYPE' => 'multipart/form-data']
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
// Clean up file
|
||||
unlink($zipPath);
|
||||
|
||||
if (201 !== $response->getStatusCode()) {
|
||||
$this->fail('Import failed with error: '.$response->getContent());
|
||||
}
|
||||
|
||||
$this->assertResponseStatusCodeSame(201);
|
||||
$decoded = json_decode($response->getContent(), true);
|
||||
$this->assertContains('Import successful: Imported campaigns are switched off by default.', $decoded);
|
||||
}
|
||||
|
||||
public function testImportCampaignAccessDenied(): void
|
||||
{
|
||||
$userWithoutPermission = $this->createUserWithPermission([
|
||||
'user-name' => 'no-import-user',
|
||||
'email' => 'no-import@mautic-test.com',
|
||||
'first-name' => 'NoImport',
|
||||
'last-name' => 'User',
|
||||
'role' => [
|
||||
'name' => 'no_import_role',
|
||||
'permissions' => [
|
||||
// Do not grant 'campaign:imports:create'
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginUser($userWithoutPermission);
|
||||
|
||||
// Attempt to import a campaign
|
||||
$this->client->request(Request::METHOD_POST, '/api/campaigns/import');
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
// Assert that access is denied
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testImportCampaignNoFileUploaded(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Attempt to import with no files
|
||||
$this->client->request(Request::METHOD_POST, '/api/campaigns/import');
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
$this->assertStringContainsString('No JSON content found and exactly one ZIP file must be uploaded.', $response->getContent());
|
||||
}
|
||||
|
||||
private function createTemporaryFile(string $extension): string
|
||||
{
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'mautic_test_').'.'.$extension;
|
||||
file_put_contents($filePath, 'test content');
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
public function testImportCampaignInvalidFile(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Create a temporary file
|
||||
$filePath = $this->createTemporaryFile('txt');
|
||||
|
||||
// Upload the invalid file
|
||||
$file = new \Symfony\Component\HttpFoundation\File\UploadedFile($filePath, 'test.txt', null, null, true);
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/api/campaigns/import', [], ['file' => $file]);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
$this->assertStringContainsString('Unsupported file type. Only ZIP archives are supported.', $response->getContent());
|
||||
|
||||
// Clean up
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
public function testImportCampaignUnsupportedFileType(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Create a temporary file with a non-ZIP extension
|
||||
$filePath = $this->createTemporaryFile('txt');
|
||||
$file = new \Symfony\Component\HttpFoundation\File\UploadedFile($filePath, 'test.txt', null, null, true);
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/api/campaigns/import', [], ['file' => $file]);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
$this->assertStringContainsString('Unsupported file type. Only ZIP archives are supported.', $response->getContent());
|
||||
|
||||
// Clean up
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
public function testImportCampaignMalformedJson(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Create a temporary ZIP file with valid structure but malformed JSON
|
||||
$zipPath = tempnam(sys_get_temp_dir(), 'mautic_test_').'.zip';
|
||||
$zip = new \ZipArchive();
|
||||
if (true === $zip->open($zipPath, \ZipArchive::CREATE)) {
|
||||
// Add a valid JSON file with malformed content
|
||||
$zip->addFromString('campaign.json', '{invalid json content}');
|
||||
$zip->close();
|
||||
} else {
|
||||
$this->fail('Failed to create test ZIP file.');
|
||||
}
|
||||
|
||||
$file = new \Symfony\Component\HttpFoundation\File\UploadedFile($zipPath, 'test.zip', null, null, true);
|
||||
|
||||
try {
|
||||
$this->client->request(Request::METHOD_POST, '/api/campaigns/import', [], ['file' => $file]);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
$this->assertStringContainsString('Invalid JSON', $response->getContent());
|
||||
} finally {
|
||||
// Clean up - check if file exists before trying to delete
|
||||
if (file_exists($zipPath)) {
|
||||
unlink($zipPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignMember;
|
||||
use Mautic\CampaignBundle\Tests\Campaign\AbstractCampaignTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ContactCampaignApiControllerFunctionalTest extends AbstractCampaignTestCase
|
||||
{
|
||||
public function testContactCampaignApiEndpoints(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('campaign@tester.email');
|
||||
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$campaignMemberRepository = $this->em->getRepository(CampaignMember::class);
|
||||
|
||||
// Add the contact to the campaign.
|
||||
$this->client->request(Request::METHOD_POST, "/api/campaigns/{$campaign->getId()}/contact/{$contact->getId()}/add");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
Assert::assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
Assert::assertSame('{"success":1}', $clientResponse->getContent());
|
||||
|
||||
// Assert that the campaign member was really added.
|
||||
/** @var CampaignMember[] $campaignMembers */
|
||||
$campaignMembers = $campaignMemberRepository->findBy(['lead' => $contact->getId(), 'campaign' => $campaign->getId()]);
|
||||
Assert::assertCount(1, $campaignMembers);
|
||||
Assert::assertTrue($campaignMembers[0]->getManuallyAdded());
|
||||
Assert::assertFalse($campaignMembers[0]->getManuallyRemoved());
|
||||
|
||||
// Get the contact's campaigns.
|
||||
$this->client->request(Request::METHOD_GET, "/api/contacts/{$contact->getId()}/campaigns");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
Assert::assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
$body = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame(1, $body['total'], $clientResponse->getContent());
|
||||
Assert::assertSame($campaign->getId(), $body['campaigns'][$campaign->getId()]['id'], $clientResponse->getContent());
|
||||
Assert::assertSame($campaign->getName(), $body['campaigns'][$campaign->getId()]['name'], $clientResponse->getContent());
|
||||
Assert::assertNotEmpty($body['campaigns'][$campaign->getId()]['dateAdded'], $clientResponse->getContent());
|
||||
Assert::assertFalse($body['campaigns'][$campaign->getId()]['manuallyRemoved'], $clientResponse->getContent());
|
||||
Assert::assertTrue($body['campaigns'][$campaign->getId()]['manuallyAdded'], $clientResponse->getContent());
|
||||
|
||||
// Get campaign contacts API endpoint.
|
||||
$this->client->request(Request::METHOD_GET, "/api/campaigns/{$campaign->getId()}/contacts");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
Assert::assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
$body = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame(3, (int) $body['total']);
|
||||
Assert::assertSame($contact->getId(), (int) $body['contacts'][2]['lead_id']);
|
||||
|
||||
// Remove the contact from the campaign.
|
||||
$this->client->request(Request::METHOD_POST, "/api/campaigns/{$campaign->getId()}/contact/{$contact->getId()}/remove");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
Assert::assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
Assert::assertSame('{"success":1}', $clientResponse->getContent());
|
||||
|
||||
// Assert that the campaign member was really removed.
|
||||
/** @var CampaignMember[] $campaignMembers */
|
||||
$campaignMembers = $campaignMemberRepository->findBy(['lead' => $contact->getId(), 'campaign' => $campaign->getId()]);
|
||||
Assert::assertCount(1, $campaignMembers);
|
||||
Assert::assertFalse($campaignMembers[0]->getManuallyAdded());
|
||||
Assert::assertTrue($campaignMembers[0]->getManuallyRemoved());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Command\SummarizeCommand;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\CampaignBundle\Tests\Campaign\AbstractCampaignTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CampaignControllerFunctionalTest extends AbstractCampaignTestCase
|
||||
{
|
||||
private const CAMPAIGN_SUMMARY_PARAM = 'campaign_use_summary';
|
||||
|
||||
private const CAMPAIGN_RANGE_PARAM = 'campaign_by_range';
|
||||
|
||||
/**
|
||||
* @var CampaignModel
|
||||
*/
|
||||
private $campaignModel;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $campaignLeadsLabel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$functionForUseSummary = ['testCampaignContactCountThroughStatsWithSummary',
|
||||
'testCampaignContactCountOnCanvasWithSummaryWithoutRange', 'testCampaignContactCountOnCanvasWithSummaryAndRange',
|
||||
'testCampaignCountsBeforeSummarizeCommandWithSummaryWithoutRange', 'testCampaignCountsBeforeSummarizeCommandWithSummaryAndRange',
|
||||
'testCampaignCountsAfterSummarizeCommandWithSummaryWithoutRange', 'testCampaignCountsAfterSummarizeCommandWithSummaryAndRange',
|
||||
'testCampaignPendingCountsWithSummaryWithoutRange', 'testCampaignPendingCountsWithSummaryAndRange', 'testCampaignRemovedLeadCountsWithSummaryAndRange', 'testCampaignRemovedLeadAndPendingCountsWithSummaryAndRange', ];
|
||||
$functionForUseRange = ['testCampaignContactCountOnCanvasWithoutSummaryWithRange', 'testCampaignContactCountOnCanvasWithSummaryAndRange',
|
||||
'testCampaignCountsBeforeSummarizeCommandWithoutSummaryWithRange', 'testCampaignCountsBeforeSummarizeCommandWithSummaryAndRange',
|
||||
'testCampaignCountsAfterSummarizeCommandWithoutSummaryWithRange', 'testCampaignCountsAfterSummarizeCommandWithSummaryAndRange',
|
||||
'testCampaignPendingCountsWithoutSummaryAndRange', 'testCampaignPendingCountsWithoutSummaryWithRange', 'testCampaignRemovedLeadCountsWithoutSummaryWithRange', 'testCampaignRemovedLeadCountsWithSummaryAndRange', 'testCampaignRemovedLeadAndPendingCountsWithSummaryAndRange', 'testCampaignRemovedLeadAndPendingCountsWithoutSummaryWithRange', ];
|
||||
$this->configParams[self::CAMPAIGN_SUMMARY_PARAM] = in_array($this->name(), $functionForUseSummary);
|
||||
$this->configParams[self::CAMPAIGN_RANGE_PARAM] = in_array($this->name(), $functionForUseRange);
|
||||
parent::setUp();
|
||||
|
||||
$model = static::getContainer()->get(CampaignModel::class);
|
||||
|
||||
$this->campaignModel = $model;
|
||||
$this->campaignLeadsLabel = static::getContainer()->get('translator')->trans('mautic.campaign.campaign.leads');
|
||||
$this->configParams['delete_campaign_event_log_in_background'] = false;
|
||||
}
|
||||
|
||||
public function testCampaignContactCountThroughStatsWithSummary(): void
|
||||
{
|
||||
$this->campaignContactCountThroughStats();
|
||||
}
|
||||
|
||||
public function testCampaignContactCountThroughStatsWithoutSummary(): void
|
||||
{
|
||||
$this->campaignContactCountThroughStats();
|
||||
}
|
||||
|
||||
public function testCampaignContactCountOnCanvasWithoutSummaryAndRange(): void
|
||||
{
|
||||
$this->campaignContactCountOnCanvas();
|
||||
}
|
||||
|
||||
public function testCampaignContactCountOnCanvasWithSummaryWithoutRange(): void
|
||||
{
|
||||
$this->campaignContactCountOnCanvas();
|
||||
}
|
||||
|
||||
public function testCampaignContactCountOnCanvasWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->campaignContactCountOnCanvas();
|
||||
}
|
||||
|
||||
public function testCampaignContactCountOnCanvasWithSummaryAndRange(): void
|
||||
{
|
||||
$this->campaignContactCountOnCanvas();
|
||||
}
|
||||
|
||||
public function testCampaignCountsBeforeSummarizeCommandWithoutSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, false, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsBeforeSummarizeCommandWithSummaryWithoutRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, false, ['0%', '0%'], ['0', '0'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsBeforeSummarizeCommandWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, false, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsBeforeSummarizeCommandWithSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, false, ['0%', '0%'], ['0', '0'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsAfterSummarizeCommandWithoutSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, true, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsAfterSummarizeCommandWithSummaryWithoutRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, true, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsAfterSummarizeCommandWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, true, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsAfterSummarizeCommandWithSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, true, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignPendingCountsWithoutSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, false, true, ['100%', '100%'], ['3', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
public function testCampaignPendingCountsWithSummaryWithoutRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, false, true, ['100%', '100%'], ['3', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
public function testCampaignPendingCountsWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, false, true, ['100%', '100%'], ['3', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
public function testCampaignPendingCountsWithSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, false, true, ['100%', '100%'], ['3', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
public function testCampaignRemovedLeadCountsWithSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, true, true, ['100%', '100%'], ['3', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignRemovedLeadCountsWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, true, true, ['100%', '100%'], ['3', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignRemovedLeadAndPendingCountsWithSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, true, true, ['100%', '100%'], ['4', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
public function testCampaignRemovedLeadAndPendingCountsWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, true, true, ['100%', '100%'], ['4', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
private function getStatTotalContacts(int $campaignId): int
|
||||
{
|
||||
$from = date('Y-m-d', strtotime('-2 months'));
|
||||
$to = date('Y-m-d', strtotime('-1 month'));
|
||||
|
||||
$stats = $this->campaignModel->getCampaignMetricsLineChartData(
|
||||
null,
|
||||
new \DateTime($from),
|
||||
new \DateTime($to),
|
||||
null,
|
||||
['campaign_id' => $campaignId]
|
||||
);
|
||||
$datasets = $stats['datasets'] ?? [];
|
||||
|
||||
return $this->processTotalContactStats($datasets);
|
||||
}
|
||||
|
||||
private function getCanvasTotalContacts(int $campaignId): int
|
||||
{
|
||||
$from = date('Y-m-d', strtotime('-2 months'));
|
||||
$to = date('Y-m-d', strtotime('-1 month'));
|
||||
$this->client->request('GET', sprintf('s/campaigns/graph/%d/%s/%s', $campaignId, $from, $to));
|
||||
$response = $this->client->getResponse();
|
||||
$body = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($body['newContent']);
|
||||
$canvasJson = trim($crawler->filter('canvas')->html());
|
||||
$canvasData = json_decode($canvasJson, true);
|
||||
$datasets = $canvasData['datasets'] ?? [];
|
||||
$this->client->restart();
|
||||
|
||||
return $this->processTotalContactStats($datasets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<int|string>> $datasets
|
||||
*/
|
||||
private function processTotalContactStats(array $datasets): int
|
||||
{
|
||||
$totalContacts = 0;
|
||||
|
||||
foreach ($datasets as $dataset) {
|
||||
if ($dataset['label'] === $this->campaignLeadsLabel) {
|
||||
$data = $dataset['data'] ?? [];
|
||||
$totalContacts = array_sum($data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $totalContacts;
|
||||
}
|
||||
|
||||
private function getCrawlers(int $campaignId): Crawler
|
||||
{
|
||||
$from = date('Y-m-d', strtotime('-2 months'));
|
||||
$to = date('Y-m-d', strtotime('-1 month'));
|
||||
$url = sprintf('s/campaigns/event/stats/%d/%s/%s', $campaignId, $from, $to);
|
||||
$this->client->request('GET', $url);
|
||||
$response = $this->client->getResponse();
|
||||
$body = json_decode($response->getContent(), true);
|
||||
$this->client->restart();
|
||||
|
||||
return new Crawler($body['actions']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
private function getActionCounts(int $campaignId): array
|
||||
{
|
||||
$crawler = $this->getCrawlers($campaignId);
|
||||
$successPercent = [
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(1) .label-success')->text()),
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(2) .label-success')->text()),
|
||||
];
|
||||
|
||||
$completed = [
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(1) .label-warning')->text()),
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(2) .label-warning')->text()),
|
||||
];
|
||||
|
||||
$pending = [
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(1) .label-gray')->text()),
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(2) .label-gray')->text()),
|
||||
];
|
||||
|
||||
return [
|
||||
'successPercent' => $successPercent,
|
||||
'completed' => $completed,
|
||||
'pending' => $pending,
|
||||
];
|
||||
}
|
||||
|
||||
private function campaignContactCountThroughStats(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$campaignId = $campaign->getId();
|
||||
|
||||
$totalContacts = $this->getStatTotalContacts($campaignId);
|
||||
Assert::assertSame(2, $totalContacts);
|
||||
}
|
||||
|
||||
private function campaignContactCountOnCanvas(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$campaignId = $campaign->getId();
|
||||
$totalContacts = $this->getCanvasTotalContacts($campaignId);
|
||||
Assert::assertSame(2, $totalContacts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $expectedSuccessPercent
|
||||
* @param array<int, string> $expectedCompleted
|
||||
* @param array<int, string> $expectedPending
|
||||
*/
|
||||
private function getCountAndDetails(bool $withPendingAction, bool $withActionOfRemovedLead, bool $runCommand, array $expectedSuccessPercent, array $expectedCompleted, array $expectedPending): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs($withPendingAction, $withActionOfRemovedLead);
|
||||
$campaignId = $campaign->getId();
|
||||
|
||||
if ($runCommand) {
|
||||
$this->testSymfonyCommand(
|
||||
SummarizeCommand::NAME,
|
||||
[
|
||||
'--env' => 'test',
|
||||
'--max-hours' => 768,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$actionCounts = $this->getActionCounts($campaignId);
|
||||
Assert::assertSame($expectedSuccessPercent, $actionCounts['successPercent']);
|
||||
Assert::assertSame($expectedCompleted, $actionCounts['completed']);
|
||||
Assert::assertSame($expectedPending, $actionCounts['pending']);
|
||||
}
|
||||
|
||||
public function testDeleteCampaign(): void
|
||||
{
|
||||
$lead = $this->createLead();
|
||||
$campaign = $this->createCampaign();
|
||||
$event = $this->createEvent('Event 1', $campaign);
|
||||
$this->createEventLog($lead, $event, $campaign);
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/s/campaigns/delete/'.$campaign->getId());
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(Response::HTTP_OK, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
$eventLogs = $this->em->getRepository(LeadEventLog::class)->findAll();
|
||||
Assert::assertCount(0, $eventLogs);
|
||||
}
|
||||
|
||||
private function createLead(): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('My campaign');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createEvent(string $name, Campaign $campaign): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName($name);
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType('email.send');
|
||||
$event->setEventType('action');
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createEventLog(Lead $lead, Event $event, Campaign $campaign): LeadEventLog
|
||||
{
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setLead($lead);
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setCampaign($campaign);
|
||||
$this->em->persist($leadEventLog);
|
||||
$this->em->flush();
|
||||
|
||||
return $leadEventLog;
|
||||
}
|
||||
|
||||
public function testCampaignView(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$crawler = $this->client->request('GET', sprintf('/s/campaigns/view/%d', $campaign->getId()));
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk());
|
||||
self::assertStringContainsString('Campaign ABC', $response->getContent());
|
||||
self::assertSame('', trim($crawler->filter('#decisions-container')->text()));
|
||||
self::assertSame('', trim($crawler->filter('#actions-container')->text()));
|
||||
self::assertSame('', trim($crawler->filter('#conditions-container')->text()));
|
||||
self::assertSame('', trim($crawler->filter('#campaign-graph-div')->text()));
|
||||
}
|
||||
|
||||
public function testCampaignViewEvents(): void
|
||||
{
|
||||
$from = date('Y-m-d', strtotime('-2 months'));
|
||||
$to = date('Y-m-d', strtotime('-1 month'));
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$this->client->request('GET', sprintf('s/campaigns/event/stats/%d/%s/%s', $campaign->getId(), $from, $to));
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk());
|
||||
$body = json_decode($response->getContent(), true);
|
||||
self::assertCount(2, $body);
|
||||
self::arrayHasKey('actions');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CampaignControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* Index should return status code 200.
|
||||
*/
|
||||
public function testIndexActionWhenNotFiltered(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/campaigns');
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtering should return status code 200.
|
||||
*/
|
||||
public function testIndexActionWhenFiltering(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/campaigns?search=has%3Aresults&tmpl=list');
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get campaign's create page.
|
||||
*/
|
||||
public function testNewActionCampaign(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/campaigns/new/');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cancelling new campaign does not give a 500 error.
|
||||
*
|
||||
* @see https://github.com/mautic/mautic/issues/11181
|
||||
*/
|
||||
public function testNewActionCampaignCancel(): void
|
||||
{
|
||||
$crawler = $this->client->request('GET', '/s/campaigns/new/');
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->filter('form[name="campaign"]')->selectButton('campaign_buttons_cancel')->form();
|
||||
$this->client->submit($form);
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testCampaignWithProject(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test Campaign');
|
||||
$this->em->persist($campaign);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
$this->em->persist($project);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/campaigns/edit/'.$campaign->getId());
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['campaign[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedCampaign = $this->em->find(Campaign::class, $campaign->getId());
|
||||
Assert::assertSame($project->getId(), $savedCampaign->getProjects()->first()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CampaignBundle\Controller\CampaignMapStatsController;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Helper\MapHelper;
|
||||
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 PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CampaignMapStatsControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
private MockObject $campaignModelMock;
|
||||
|
||||
private CampaignMapStatsController $mapController;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->campaignModelMock = $this->createMock(CampaignModel::class);
|
||||
$this->mapController = new CampaignMapStatsController($this->campaignModelMock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, array<string, string>>>
|
||||
*/
|
||||
private function getStats(): array
|
||||
{
|
||||
return [
|
||||
'contacts' => [
|
||||
[
|
||||
'contacts' => '4',
|
||||
'country' => '',
|
||||
],
|
||||
[
|
||||
'contacts' => '4',
|
||||
'country' => 'Spain',
|
||||
],
|
||||
[
|
||||
'contacts' => '4',
|
||||
'country' => 'Finland',
|
||||
],
|
||||
],
|
||||
'clicked_through_count' => [
|
||||
[
|
||||
'clicked_through_count' => '4',
|
||||
'country' => '',
|
||||
],
|
||||
[
|
||||
'clicked_through_count' => '4',
|
||||
'country' => 'Spain',
|
||||
],
|
||||
[
|
||||
'clicked_through_count' => '4',
|
||||
'country' => 'Finland',
|
||||
],
|
||||
],
|
||||
'read_count' => [
|
||||
[
|
||||
'read_count' => '4',
|
||||
'country' => '',
|
||||
],
|
||||
[
|
||||
'read_count' => '8',
|
||||
'country' => 'Spain',
|
||||
],
|
||||
[
|
||||
'read_count' => '8',
|
||||
'country' => 'Finland',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testMapCountries(): void
|
||||
{
|
||||
$stats = $this->getStats();
|
||||
$reads = MapHelper::mapCountries($stats['read_count'], 'read_count');
|
||||
$clicks = MapHelper::mapCountries($stats['clicked_through_count'], 'clicked_through_count');
|
||||
|
||||
$this->assertSame([
|
||||
'data' => [
|
||||
'ES' => 8,
|
||||
'FI' => 8,
|
||||
],
|
||||
'total' => 20,
|
||||
'totalWithCountry' => 16,
|
||||
], $reads);
|
||||
|
||||
$this->assertSame([
|
||||
'data' => [
|
||||
'ES' => 4,
|
||||
'FI' => 4,
|
||||
],
|
||||
'total' => 12,
|
||||
'totalWithCountry' => 8,
|
||||
], $clicks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testViewAction(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', "s/campaign-map-stats/{$campaign->getId()}/2023-07-20/2023-07-25");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$crawler = new Crawler($clientResponse->getContent(), $this->client->getInternalRequest()->getUri());
|
||||
|
||||
$this->assertEmpty($crawler->filter('.map-options__title'));
|
||||
$this->assertCount(1, $crawler->filter('div.map-options'));
|
||||
$this->assertCount(1, $crawler->filter('div.vector-map'));
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testViewActionWithEmail(): void
|
||||
{
|
||||
$leadsPayload = [
|
||||
[
|
||||
'email' => 'test1@test.com',
|
||||
'country' => '',
|
||||
'read' => true,
|
||||
'click' => true,
|
||||
],
|
||||
[
|
||||
'email' => 'test2@test.com',
|
||||
'country' => '',
|
||||
'read' => true,
|
||||
'click' => false,
|
||||
],
|
||||
[
|
||||
'email' => 'example1@example.com',
|
||||
'country' => 'Spain',
|
||||
'read' => false,
|
||||
'click' => false,
|
||||
],
|
||||
[
|
||||
'email' => 'example2@example.com',
|
||||
'country' => 'Spain',
|
||||
'read' => true,
|
||||
'click' => true,
|
||||
],
|
||||
[
|
||||
'email' => 'example3@example.com',
|
||||
'country' => 'Spain',
|
||||
'read' => true,
|
||||
'click' => true,
|
||||
],
|
||||
[
|
||||
'email' => 'example4@example.com',
|
||||
'country' => 'Spain',
|
||||
'read' => true,
|
||||
'click' => false,
|
||||
],
|
||||
];
|
||||
$campaign = $this->createCampaignWithEmail($leadsPayload);
|
||||
|
||||
$this->client->request('GET', "s/campaign-map-stats/{$campaign->getId()}/2023-07-20/2023-07-25");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$crawler = new Crawler($clientResponse->getContent(), $this->client->getInternalRequest()->getUri());
|
||||
|
||||
$this->assertEmpty($crawler->filter('.map-options__title'));
|
||||
$this->assertCount(1, $crawler->filter('div.map-options'));
|
||||
$this->assertCount(1, $crawler->filter('div.vector-map'));
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
|
||||
$readOption = $crawler->filter('label.map-options__item')->filter('[data-stat-unit="Read"]');
|
||||
$this->assertCount(1, $readOption);
|
||||
$this->assertSame('Total: 5 (3 with country)', $readOption->attr('data-legend-text'));
|
||||
$this->assertSame('{"ES":3}', $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('{"ES":2}', $clickOption->attr('data-map-series'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testGetMapOptionsEmailCampaign(): void
|
||||
{
|
||||
$campaign = $this->createCampaignWithEmail();
|
||||
|
||||
$result = $this->mapController->getMapOptions($campaign);
|
||||
$this->assertSame(CampaignMapStatsController::MAP_OPTIONS, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testGetMapOptionsNotEmailCampaign(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign 1');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->mapController->getMapOptions($campaign);
|
||||
$this->assertSame(['contacts' => CampaignMapStatsController::MAP_OPTIONS['contacts']], $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, bool|string>> $leadsPayload
|
||||
*
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
*/
|
||||
private function createCampaignWithEmail(array $leadsPayload = []): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
// Create email
|
||||
$email = new Email();
|
||||
$email->setName('Test email');
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
// Create email events
|
||||
$event = new Event();
|
||||
$event->setName('Send email');
|
||||
$event->setType('email.send');
|
||||
$event->setEventType('action');
|
||||
$event->setChannel('email');
|
||||
$event->setChannelId($email->getId());
|
||||
$event->setCampaign($campaign);
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
// Add events to campaign
|
||||
$campaign->addEvent(0, $event);
|
||||
|
||||
if (!empty($leadsPayload)) {
|
||||
$this->emulateEmailCampaignStat($event, $email, $leadsPayload);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, bool|string>> $leadsPayload
|
||||
*
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function emulateEmailCampaignStat(Event $event, Email $email, array $leadsPayload): void
|
||||
{
|
||||
foreach ($leadsPayload as $l) {
|
||||
$lead = new Lead();
|
||||
$lead->setEmail($l['email']);
|
||||
$lead->setCountry($l['country']);
|
||||
$this->em->persist($lead);
|
||||
|
||||
$stat = new Stat();
|
||||
$stat->setEmailAddress('test-a@test.com');
|
||||
$stat->setLead($lead);
|
||||
$stat->setDateSent(new \DateTime('2023-07-22'));
|
||||
$stat->setEmail($email);
|
||||
$stat->setIsRead($l['read']);
|
||||
$stat->setSource('campaign.event');
|
||||
$stat->setSourceId($event->getId());
|
||||
$this->em->persist($stat);
|
||||
$this->em->flush();
|
||||
|
||||
if ($l['read'] && $l['click']) {
|
||||
$this->emulateClick($lead, $email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function emulateClick(Lead $lead, Email $email): 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->setUniqueHits(1);
|
||||
$redirect->setHits(1);
|
||||
$this->em->persist($redirect);
|
||||
|
||||
$trackable = new Trackable();
|
||||
$trackable->setChannelId($email->getId());
|
||||
$trackable->setHits(1);
|
||||
$trackable->setChannel('email');
|
||||
$trackable->setUniqueHits(1);
|
||||
$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('2023-07-22'));
|
||||
$pageHit->setCode(200);
|
||||
$pageHit->setUrl($redirect->getUrl());
|
||||
$pageHit->setTrackingId($redirect->getRedirectId());
|
||||
$pageHit->setSource('email');
|
||||
$pageHit->setSourceId($email->getId());
|
||||
$this->em->persist($pageHit);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Tests\Functional\Fixtures\FixtureHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Tests\Functional\Fixtures\EmailFixturesHelper;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class CampaignMetricsControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private FixtureHelper $campaignFixturesHelper;
|
||||
private EmailFixturesHelper $emailFixturesHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->campaignFixturesHelper = new FixtureHelper($this->em);
|
||||
$this->emailFixturesHelper = new EmailFixturesHelper($this->em);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function setupEmailCampaignTestData(): array
|
||||
{
|
||||
$contacts = [
|
||||
$this->campaignFixturesHelper->createContact('john@example.com'),
|
||||
$this->campaignFixturesHelper->createContact('paul@example.com'),
|
||||
];
|
||||
|
||||
$email = $this->emailFixturesHelper->createEmail('Test Email');
|
||||
$this->em->flush();
|
||||
|
||||
$campaign = $this->campaignFixturesHelper->createCampaignWithEmailSent($email->getId());
|
||||
$this->campaignFixturesHelper->addContactToCampaign($contacts[0], $campaign);
|
||||
$this->campaignFixturesHelper->addContactToCampaign($contacts[1], $campaign);
|
||||
$eventId = $campaign->getEmailSendEvents()->first()->getId();
|
||||
|
||||
$emailStats = [
|
||||
$this->emailFixturesHelper->emulateEmailSend($contacts[0], $email, '2024-12-10 12:00:00', 'campaign.event', $eventId),
|
||||
$this->emailFixturesHelper->emulateEmailSend($contacts[1], $email, '2024-12-10 12:00:00', 'campaign.event', $eventId),
|
||||
];
|
||||
|
||||
$this->emailFixturesHelper->emulateEmailRead($emailStats[0], $email, '2024-12-10 12:09:00');
|
||||
$this->emailFixturesHelper->emulateEmailRead($emailStats[1], $email, '2024-12-11 21:35:00');
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->persist($email);
|
||||
|
||||
$emailLinks = [
|
||||
$this->emailFixturesHelper->createEmailLink('https://example.com/1', $email->getId()),
|
||||
$this->emailFixturesHelper->createEmailLink('https://example.com/2', $email->getId()),
|
||||
];
|
||||
$this->em->flush();
|
||||
|
||||
$this->emailFixturesHelper->emulateLinkClick($email, $emailLinks[0], $contacts[0], '2024-12-10 12:10:00', 3);
|
||||
$this->emailFixturesHelper->emulateLinkClick($email, $emailLinks[1], $contacts[0], '2024-12-10 13:20:00');
|
||||
$this->emailFixturesHelper->emulateLinkClick($email, $emailLinks[1], $contacts[1], '2024-12-11 21:37:00');
|
||||
$this->em->flush();
|
||||
|
||||
return ['campaign' => $campaign, 'email' => $email];
|
||||
}
|
||||
|
||||
public function testEmailWeekdaysAction(): void
|
||||
{
|
||||
$testData = $this->setupEmailCampaignTestData();
|
||||
$campaign = $testData['campaign'];
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/campaign/metrics/email-weekdays/{$campaign->getId()}/2024-12-01/2024-12-12");
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
$content = $this->client->getResponse()->getContent();
|
||||
$crawler = new Crawler($content);
|
||||
$daysJson = $crawler->filter('canvas')->text(null, false);
|
||||
$daysData = json_decode(html_entity_decode($daysJson), true);
|
||||
$daysDatasets = $daysData['datasets'];
|
||||
Assert::assertIsArray($daysDatasets);
|
||||
Assert::assertCount(3, $daysDatasets); // Assuming there are 3 datasets: Email sent, Email read, Email clicked
|
||||
|
||||
$expectedDaysLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||
$expectedDaysData = [
|
||||
['label' => 'Email sent', 'data' => [0, 2, 0, 0, 0, 0, 0]],
|
||||
['label' => 'Email read', 'data' => [0, 1, 1, 0, 0, 0, 0]],
|
||||
['label' => 'Email clicked', 'data' => [0, 4, 1, 0, 0, 0, 0]],
|
||||
];
|
||||
Assert::assertEquals($expectedDaysLabels, $daysData['labels']);
|
||||
foreach ($daysDatasets as $index => $dataset) {
|
||||
Assert::assertEquals($expectedDaysData[$index]['label'], $dataset['label']);
|
||||
Assert::assertEquals($expectedDaysData[$index]['data'], $dataset['data']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testEmailHoursAction(): void
|
||||
{
|
||||
$testData = $this->setupEmailCampaignTestData();
|
||||
$campaign = $testData['campaign'];
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/campaign/metrics/email-hours/{$campaign->getId()}/2024-12-01/2024-12-12");
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
$content = $this->client->getResponse()->getContent();
|
||||
$crawler = new Crawler($content);
|
||||
$hourJson = $crawler->filter('canvas')->text(null, false);
|
||||
$hoursData = json_decode(html_entity_decode($hourJson), true);
|
||||
|
||||
$hoursDatasets = $hoursData['datasets'];
|
||||
Assert::assertIsArray($hoursDatasets);
|
||||
Assert::assertCount(3, $hoursDatasets); // Assuming there are 3 datasets: Email sent, Email read, Email clicked
|
||||
|
||||
// Get the time format from CoreParametersHelper
|
||||
$coreParametersHelper = self::getContainer()->get('mautic.helper.core_parameters');
|
||||
$timeFormat = $coreParametersHelper->get('date_format_timeonly');
|
||||
|
||||
// Generate expected hour labels based on the actual time format
|
||||
$expectedHoursLabels = [];
|
||||
for ($hour = 0; $hour < 24; ++$hour) {
|
||||
$startTime = (new \DateTime())->setTime($hour, 0);
|
||||
$endTime = (new \DateTime())->setTime(($hour + 1) % 24, 0);
|
||||
$expectedHoursLabels[] = $startTime->format($timeFormat).' - '.$endTime->format($timeFormat);
|
||||
}
|
||||
|
||||
Assert::assertEquals($expectedHoursLabels, $hoursData['labels']);
|
||||
|
||||
$expectedHoursData = [
|
||||
['label' => 'Email sent', 'data' => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
['label' => 'Email read', 'data' => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]],
|
||||
['label' => 'Email clicked', 'data' => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]],
|
||||
];
|
||||
foreach ($hoursDatasets as $index => $dataset) {
|
||||
Assert::assertEquals($expectedHoursData[$index]['label'], $dataset['label']);
|
||||
Assert::assertEquals($expectedHoursData[$index]['data'], $dataset['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class CampaignProjectSearchFunctionalTest 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');
|
||||
|
||||
$campaignAlpha = $this->createCampaign('Campaign Alpha');
|
||||
$campaignBeta = $this->createCampaign('Campaign Beta');
|
||||
$this->createCampaign('Campaign Gamma');
|
||||
$this->createCampaign('Campaign Delta');
|
||||
|
||||
$campaignAlpha->addProject($projectOne);
|
||||
$campaignAlpha->addProject($projectTwo);
|
||||
$campaignBeta->addProject($projectTwo);
|
||||
$campaignBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/campaigns', '/s/campaigns']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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' => ['Campaign Alpha', 'Campaign Beta'],
|
||||
'unexpectedEntities' => ['Campaign Gamma', 'Campaign Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND campaign name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Campaign Beta'],
|
||||
'unexpectedEntities' => ['Campaign Alpha', 'Campaign Gamma', 'Campaign Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR campaign name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Campaign Alpha', 'Campaign Beta', 'Campaign Gamma'],
|
||||
'unexpectedEntities' => ['Campaign Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Campaign Gamma', 'Campaign Delta'],
|
||||
'unexpectedEntities' => ['Campaign Alpha', 'Campaign Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Campaign Beta'],
|
||||
'unexpectedEntities' => ['Campaign Alpha', 'Campaign Gamma', 'Campaign Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Campaign Gamma', 'Campaign Delta'],
|
||||
'unexpectedEntities' => ['Campaign Alpha', 'Campaign Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Campaign Alpha', 'Campaign Beta'],
|
||||
'unexpectedEntities' => ['Campaign Gamma', 'Campaign Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Campaign Alpha', 'Campaign Gamma', 'Campaign Delta'],
|
||||
'unexpectedEntities' => ['Campaign Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createCampaign(string $name): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName($name);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Tests\Campaign\AbstractCampaignTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class CampaignUnpublishedWorkflowFunctionalTest extends AbstractCampaignTestCase
|
||||
{
|
||||
public function testCreateCampaignPageShouldNotContainConformation(): void
|
||||
{
|
||||
// Check the message in the Campaign edit page
|
||||
$crawler = $this->client->request('GET', '/s/campaigns/new');
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
$attributes = [
|
||||
'data-toggle',
|
||||
'data-message',
|
||||
'data-confirm-text',
|
||||
'data-confirm-callback',
|
||||
'data-cancel-text',
|
||||
'data-cancel-callback',
|
||||
];
|
||||
|
||||
$elements = $crawler->filter('form input[name*="campaign[isPublished]"]')->getIterator();
|
||||
|
||||
/** @var \DOMElement $element */
|
||||
foreach ($elements as $element) {
|
||||
foreach ($attributes as $attribute) {
|
||||
$this->assertFalse($element->hasAttribute($attribute), sprintf('The "%s" attribute is present.', $attribute));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testCampaignEditPageCheckUnpublishWorkflowAttributesPresent(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$translator = static::getContainer()->get('translator');
|
||||
|
||||
// Check the message in the Campaign edit page
|
||||
$crawler = $this->client->request('GET', sprintf('/s/campaigns/edit/%d', $campaign->getId()));
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
$attributes = [
|
||||
'onchange' => 'Mautic.showCampaignConfirmation(mQuery(this));',
|
||||
'data-toggle' => 'confirmation',
|
||||
'data-message' => $translator->trans('mautic.campaign.form.confirmation.message'),
|
||||
'data-confirm-text' => $translator->trans('mautic.campaign.form.confirmation.confirm_text'),
|
||||
'data-confirm-callback' => 'dismissConfirmation',
|
||||
'data-cancel-text' => $translator->trans('mautic.campaign.form.confirmation.cancel_text'),
|
||||
'data-cancel-callback' => 'setPublishedButtonToYes',
|
||||
];
|
||||
|
||||
$elements = $crawler->filter('form input[name*="campaign[isPublished]"]')->getIterator();
|
||||
|
||||
/** @var \DOMElement $element */
|
||||
foreach ($elements as $element) {
|
||||
foreach ($attributes as $key => $val) {
|
||||
$this->assertStringContainsString($val, $element->getAttribute($key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testCampaignListPageCheckUnpublishWorkflowAttributesPresent(): void
|
||||
{
|
||||
$this->saveSomeCampaignLeadEventLogs();
|
||||
$translator = static::getContainer()->get('translator');
|
||||
|
||||
// Check the message in the Campaign listing page
|
||||
$crawler = $this->client->request('GET', sprintf('/s/campaigns'));
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
$attributes = [
|
||||
'onclick' => 'Mautic.confirmationCampaignPublishStatus(mQuery(this));',
|
||||
'data-toggle' => 'confirmation',
|
||||
'data-confirm-callback' => 'confirmCallbackCampaignPublishStatus',
|
||||
'data-cancel-callback' => 'dismissConfirmation',
|
||||
'data-message' => $translator->trans('mautic.campaign.form.confirmation.message'),
|
||||
'data-confirm-text' => $translator->trans('mautic.campaign.form.confirmation.confirm_text'),
|
||||
'data-cancel-text' => $translator->trans('mautic.campaign.form.confirmation.cancel_text'),
|
||||
];
|
||||
|
||||
$toggleElement = $crawler->filter('.toggle-publish-status');
|
||||
foreach ($attributes as $key => $val) {
|
||||
$this->assertStringContainsString($val, $toggleElement->attr($key));
|
||||
}
|
||||
}
|
||||
|
||||
public function testCampaignUnpublishToggle(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$translator = static::getContainer()->get('translator');
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/s/ajax', ['action' => 'togglePublishStatus', 'model' => 'campaign', 'id' => $campaign->getId()]);
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
$attributes = [
|
||||
'onclick' => 'Mautic.confirmationCampaignPublishStatus(mQuery(this));',
|
||||
'data-toggle' => 'confirmation',
|
||||
'data-confirm-callback' => 'confirmCallbackCampaignPublishStatus',
|
||||
'data-cancel-callback' => 'dismissConfirmation',
|
||||
'data-message' => $translator->trans('mautic.campaign.form.confirmation.message'),
|
||||
'data-confirm-text' => $translator->trans('mautic.campaign.form.confirmation.confirm_text'),
|
||||
'data-cancel-text' => $translator->trans('mautic.campaign.form.confirmation.cancel_text'),
|
||||
];
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
foreach ($attributes as $key => $val) {
|
||||
$this->assertStringContainsString($key, $content);
|
||||
$this->assertStringContainsString($val, $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class EventControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('fieldAndValueProvider')]
|
||||
public function testCreateContactConditionOnStateField(string $field, string $value): void
|
||||
{
|
||||
// Fetch the campaign condition form.
|
||||
$uri = '/s/campaigns/events/new?type=lead.field_value&eventType=condition&campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=leadsource&anchorEventType=source';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Get the form HTML element out of the response, fill it in and submit.
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[anchor]' => 'leadsource',
|
||||
'campaignevent[properties][field]' => $field,
|
||||
'campaignevent[properties][operator]' => '=',
|
||||
'campaignevent[properties][value]' => $value,
|
||||
'campaignevent[type]' => 'lead.field_value',
|
||||
'campaignevent[eventType]' => 'condition',
|
||||
'campaignevent[anchorEventType]' => 'source',
|
||||
'campaignevent[campaignId]' => 'mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775',
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $form->getPhpValues());
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
Assert::assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
|
||||
$actualEventData = array_filter($responseData['event'], fn ($value) => in_array($value, [
|
||||
'name',
|
||||
'type',
|
||||
'eventType',
|
||||
'anchor',
|
||||
'anchorEventType',
|
||||
]), ARRAY_FILTER_USE_KEY);
|
||||
$expectedEventData = [
|
||||
'name' => 'Contact field value',
|
||||
'type' => 'lead.field_value',
|
||||
'eventType' => 'condition',
|
||||
'anchor' => 'leadsource',
|
||||
'anchorEventType' => 'source',
|
||||
];
|
||||
|
||||
$this->assertSame($expectedEventData, $actualEventData);
|
||||
$this->assertSame('condition', $responseData['eventType']);
|
||||
$this->assertSame('campaignEvent', $responseData['mauticContent']);
|
||||
$this->assertSame(1, $responseData['closeModal']);
|
||||
Assert::assertTrue($responseData['formSubmitted'], $response->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function fieldAndValueProvider(): array
|
||||
{
|
||||
return [
|
||||
'country' => ['country', 'India'],
|
||||
'region' => ['state', 'Arizona'],
|
||||
'timezone' => ['timezone', 'Marigot'],
|
||||
'locale' => ['preferred_locale', 'af'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testActionAtSpecificTimeWorkflow(): void
|
||||
{
|
||||
$uri = '/s/campaigns/events/new?type=lead.changepoints&eventType=action&campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=no&anchorEventType=condition';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Get the form HTML element out of the response, fill it in and submit.
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[canvasSettings][droppedX]' => '863',
|
||||
'campaignevent[canvasSettings][droppedY]' => '363',
|
||||
'campaignevent[name]' => '',
|
||||
'campaignevent[triggerMode]' => 'date',
|
||||
'campaignevent[triggerDate]' => '2023-09-27 21:37',
|
||||
'campaignevent[triggerInterval]' => '1',
|
||||
'campaignevent[triggerIntervalUnit]' => 'd',
|
||||
'campaignevent[triggerHour]' => '',
|
||||
'campaignevent[triggerRestrictedStartHour]' => '',
|
||||
'campaignevent[triggerRestrictedStopHour]' => '',
|
||||
'campaignevent[anchor]' => 'no',
|
||||
'campaignevent[properties][points]' => '21',
|
||||
'campaignevent[properties][group]' => '',
|
||||
'campaignevent[type]' => 'lead.changepoints',
|
||||
'campaignevent[eventType]' => 'action',
|
||||
'campaignevent[anchorEventType]' => 'condition',
|
||||
'campaignevent[campaignId]' => 'mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775',
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $form->getPhpValues());
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
|
||||
$this->assertNotEmpty($responseData['eventId']);
|
||||
$this->assertNotEmpty($responseData['event']['id']);
|
||||
$this->assertEquals($responseData['eventId'], $responseData['event']['id']);
|
||||
$this->assertSame('action', $responseData['eventType']);
|
||||
$this->assertSame('campaignEvent', $responseData['mauticContent']);
|
||||
$this->assertSame('by September 27, 2023 9:37 pm UTC', $responseData['label']);
|
||||
$this->assertSame(1, $responseData['closeModal']);
|
||||
$this->assertArrayHasKey('eventHtml', $responseData);
|
||||
$this->assertArrayNotHasKey('updateHtml', $responseData);
|
||||
$eventId = $responseData['event']['id'];
|
||||
$modifiedEvents = $responseData['modifiedEvents'] ?? [];
|
||||
|
||||
// GET EDIT FORM
|
||||
$uri = "/s/campaigns/events/edit/{$eventId}?campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=no&anchorEventType=condition";
|
||||
$this->client->xmlHttpRequest('GET', $uri, ['modifiedEvents' => json_encode($modifiedEvents)]);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// FILL EDIT FORM
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[canvasSettings][droppedX]' => '863',
|
||||
'campaignevent[canvasSettings][droppedY]' => '363',
|
||||
'campaignevent[name]' => '2 contact points after 1 day',
|
||||
'campaignevent[triggerMode]' => 'interval',
|
||||
'campaignevent[triggerDate]' => '2023-09-27 21:37',
|
||||
'campaignevent[triggerInterval]' => '1',
|
||||
'campaignevent[triggerIntervalUnit]' => 'd',
|
||||
'campaignevent[triggerHour]' => '',
|
||||
'campaignevent[triggerRestrictedStartHour]' => '',
|
||||
'campaignevent[triggerRestrictedStopHour]' => '',
|
||||
'campaignevent[anchor]' => 'no',
|
||||
'campaignevent[properties][points]' => '2',
|
||||
'campaignevent[properties][group]' => '',
|
||||
'campaignevent[type]' => 'lead.changepoints',
|
||||
'campaignevent[eventType]' => 'action',
|
||||
'campaignevent[anchorEventType]' => 'condition',
|
||||
'campaignevent[campaignId]' => 'mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775',
|
||||
]
|
||||
);
|
||||
|
||||
$formData = $form->getPhpValues();
|
||||
$formData['modifiedEvents'] = json_encode($modifiedEvents);
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $formData);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertTrue($responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
|
||||
$this->assertEquals($eventId, $responseData['eventId']);
|
||||
$this->assertEquals($eventId, $responseData['event']['id']);
|
||||
$this->assertSame('2 contact points after 1 day', $responseData['event']['name']);
|
||||
$this->assertSame('action', $responseData['eventType']);
|
||||
$this->assertSame('campaignEvent', $responseData['mauticContent']);
|
||||
$this->assertSame('within 1 day', $responseData['label']);
|
||||
$this->assertSame(1, $responseData['closeModal']);
|
||||
$this->assertArrayHasKey('updateHtml', $responseData);
|
||||
$this->assertArrayNotHasKey('eventHtml', $responseData);
|
||||
}
|
||||
|
||||
public function testCloneWorkflow(): void
|
||||
{
|
||||
$uri = '/s/campaigns/events/new?type=lead.changepoints&eventType=action&campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=no&anchorEventType=condition';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Get the form HTML element out of the response, fill it in and submit.
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[canvasSettings][droppedX]' => '863',
|
||||
'campaignevent[canvasSettings][droppedY]' => '363',
|
||||
'campaignevent[name]' => '',
|
||||
'campaignevent[triggerMode]' => 'date',
|
||||
'campaignevent[triggerDate]' => '2023-09-27 21:37',
|
||||
'campaignevent[triggerInterval]' => '1',
|
||||
'campaignevent[triggerIntervalUnit]' => 'd',
|
||||
'campaignevent[triggerHour]' => '',
|
||||
'campaignevent[triggerRestrictedStartHour]' => '',
|
||||
'campaignevent[triggerRestrictedStopHour]' => '',
|
||||
'campaignevent[anchor]' => 'no',
|
||||
'campaignevent[properties][points]' => '21',
|
||||
'campaignevent[properties][group]' => '',
|
||||
'campaignevent[type]' => 'lead.changepoints',
|
||||
'campaignevent[eventType]' => 'action',
|
||||
'campaignevent[anchorEventType]' => 'condition',
|
||||
'campaignevent[campaignId]' => 'mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775',
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $form->getPhpValues());
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
$eventId = $responseData['event']['id'];
|
||||
|
||||
// CLONE EVENT
|
||||
$uri = "/s/campaigns/events/clone/{$eventId}?campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775";
|
||||
$this->client->xmlHttpRequest('POST', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
$this->assertSame('campaignEventClone', $responseData['mauticContent']);
|
||||
$this->assertSame('Adjust contact points', $responseData['eventName']);
|
||||
$this->assertSame('New campaign', $responseData['campaignName']);
|
||||
|
||||
// INSERT EVENT
|
||||
$uri = "/s/campaigns/events/insert/{$eventId}?campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775";
|
||||
$this->client->xmlHttpRequest('POST', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
$this->assertSame('action', $responseData['eventType']);
|
||||
$this->assertSame('campaignEvent', $responseData['mauticContent']);
|
||||
$this->assertTrue($responseData['clearCloneStorage']);
|
||||
$this->assertNotEquals($eventId, $responseData['eventId']);
|
||||
$this->assertNotEmpty($responseData['eventHtml']);
|
||||
}
|
||||
|
||||
public function testEmailSendTypeDefaultSetting(): void
|
||||
{
|
||||
// Fetch the campaign action form.
|
||||
$uri = '/s/campaigns/events/new?type=email.send&eventType=action&campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=leadsource&anchorEventType=source';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Get the form HTML element out of the response
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
|
||||
// Assert the field email_type === "marketing"
|
||||
Assert::assertEquals('marketing', $form['campaignevent[properties][email_type]']->getValue(), 'The default email type should be "marketing"');
|
||||
}
|
||||
|
||||
public function testEventsAreNotAccessibleWithXhr(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$event1 = $this->createEvent('Event1', $campaign);
|
||||
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/s/campaigns/events/edit/'.$event1->getId().'?campaignId='.$campaign->getId(),
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
'{}'
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$response = json_decode($response->getContent(), true);
|
||||
Assert::assertSame(
|
||||
'You do not have access to the requested area/action.',
|
||||
$response['error']
|
||||
);
|
||||
}
|
||||
|
||||
public function testEventsAreAccessible(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$event1 = $this->createEvent('Event1', $campaign);
|
||||
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/s/campaigns/events/edit/'.$event1->getId().'?campaignId='.$campaign->getId(),
|
||||
[],
|
||||
[],
|
||||
$this->createAjaxHeaders(),
|
||||
'{}'
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$response = json_decode($response->getContent(), true);
|
||||
Assert::assertSame(
|
||||
$event1->getId(),
|
||||
$response['eventId']
|
||||
);
|
||||
Assert::assertSame(
|
||||
$event1->getName(),
|
||||
$response['event']['name']
|
||||
);
|
||||
Assert::assertFalse($response['formSubmitted'], $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testEventsAreDeleted(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$event1 = $this->createEvent('Event1', $campaign);
|
||||
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/s/campaigns/events/delete/'.$event1->getId(),
|
||||
[
|
||||
'modifiedEvents' => json_encode([
|
||||
$event1->getId() => [
|
||||
'id' => $event1->getId(),
|
||||
'eventType' => $event1->getEventType(),
|
||||
'type' => $event1->getType(),
|
||||
],
|
||||
]),
|
||||
],
|
||||
[],
|
||||
$this->createAjaxHeaders(),
|
||||
'{}'
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$response = json_decode($response->getContent(), true);
|
||||
Assert::assertSame(
|
||||
1,
|
||||
$response['success']
|
||||
);
|
||||
Assert::assertContains(
|
||||
(string) $event1->getId(),
|
||||
$response['deletedEvents']
|
||||
);
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('My campaign');
|
||||
$campaign->setIsPublished(true);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createEvent(string $name, Campaign $campaign): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName($name);
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType('email.send');
|
||||
$event->setEventType('action');
|
||||
$event->setTriggerInterval(1);
|
||||
$event->setTriggerMode('immediate');
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\FormBundle\Entity\Form;
|
||||
|
||||
class SourceControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
private const ACCESS_DENIED = 'You do not have access to the requested area\/action';
|
||||
private const NEW_FORMS_URL = '/s/campaigns/sources/new/1?sourceType=forms';
|
||||
private const DELETE_FORMS_URL = '/s/campaigns/sources/delete/1?sourceType=forms';
|
||||
|
||||
public function testNewActionWithInvalidSourceType(): void
|
||||
{
|
||||
$this->client->xmlHttpRequest('GET', '/s/campaigns/sources/new/1?sourceType=invalid');
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertStringContainsString(self::ACCESS_DENIED, $response->getContent());
|
||||
}
|
||||
|
||||
public function testNewActionWithNonAjaxRequest(): void
|
||||
{
|
||||
$this->client->request('GET', self::NEW_FORMS_URL);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertStringContainsString(self::ACCESS_DENIED, $response->getContent());
|
||||
}
|
||||
|
||||
public function testNewActionFormCancelled(): void
|
||||
{
|
||||
$formData = [
|
||||
'campaign_leadsource' => [
|
||||
'sourceType' => 'forms',
|
||||
],
|
||||
'submit' => '1',
|
||||
'cancel' => '1',
|
||||
];
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest('POST', self::NEW_FORMS_URL, $formData);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
if (is_array($json)) {
|
||||
$this->assertArrayHasKey('success', $json, 'Response should contain success key');
|
||||
$this->assertArrayHasKey('mauticContent', $json, 'Response should contain mauticContent key');
|
||||
$this->assertJsonResponseEquals('success', 0, $json);
|
||||
$this->assertJsonResponseEquals('mauticContent', 'campaignSource', $json);
|
||||
// When cancelled, we expect the form to be returned with error state
|
||||
$this->assertArrayHasKey('newContent', $json, 'Response should contain form HTML when validation fails');
|
||||
} else {
|
||||
$this->fail('Response is not valid JSON: '.$response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
public function testNewActionFormInvalid(): void
|
||||
{
|
||||
$formData = [
|
||||
'campaign_leadsource' => [
|
||||
'sourceType' => 'forms',
|
||||
],
|
||||
'submit' => '1',
|
||||
];
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest('POST', self::NEW_FORMS_URL, $formData);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
if (is_array($json)) {
|
||||
$this->assertArrayHasKey('success', $json, 'Response should contain success key');
|
||||
$this->assertJsonResponseEquals('success', 0, $json);
|
||||
$this->assertArrayHasKey('mauticContent', $json, 'Response should contain mauticContent key');
|
||||
$this->assertArrayHasKey('newContent', $json, 'Response should contain form HTML when validation fails');
|
||||
} else {
|
||||
$this->fail('Response is not valid JSON: '.$response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
public function testDeleteActionWithGetRequest(): void
|
||||
{
|
||||
$this->client->xmlHttpRequest('GET', self::DELETE_FORMS_URL);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
if (is_array($json)) {
|
||||
$this->assertArrayHasKey('success', $json, 'Response should contain success key');
|
||||
$this->assertJsonResponseEquals('success', 0, $json);
|
||||
} else {
|
||||
$this->fail('Response is not valid JSON: '.$response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
public function testTwoSourcesWithSameName(): void
|
||||
{
|
||||
$form1 = new Form();
|
||||
$form1->setName('test');
|
||||
$form1->setAlias('test');
|
||||
$form1->setFormType('campaign');
|
||||
|
||||
$form2 = new Form();
|
||||
$form2->setName('test');
|
||||
$form2->setAlias('test');
|
||||
$form2->setFormType('campaign');
|
||||
|
||||
$this->em->persist($form1);
|
||||
$this->em->persist($form2);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($form1);
|
||||
$this->em->detach($form2);
|
||||
|
||||
$this->client->xmlHttpRequest('GET', '/s/campaigns/sources/new/random_object_id?sourceType=forms');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$responseContent = $clientResponse->getContent();
|
||||
$this->assertResponseIsSuccessful($responseContent);
|
||||
|
||||
$html = json_decode($responseContent, true)['newContent'];
|
||||
$this->assertStringContainsString("<option value=\"{$form1->getId()}\">test ({$form1->getId()})</option>", $html);
|
||||
$this->assertStringContainsString("<option value=\"{$form2->getId()}\">test ({$form2->getId()})</option>", $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $json
|
||||
*/
|
||||
private function assertJsonResponseHasKey(string $key, array $json, string $message = ''): void
|
||||
{
|
||||
$this->assertIsArray($json, 'Response is not a valid JSON array');
|
||||
$this->assertArrayHasKey($key, $json, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $json
|
||||
*/
|
||||
private function assertJsonResponseEquals(string $key, mixed $expected, array $json, string $message = ''): void
|
||||
{
|
||||
$this->assertJsonResponseHasKey($key, $json, $message);
|
||||
$this->assertEquals($expected, $json[$key], $message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
final class VisitedPageConditionControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @param array<mixed,mixed> $pageUrl
|
||||
* @param array<mixed,mixed> $startDate
|
||||
* @param array<mixed,mixed> $endDate
|
||||
* @param array<mixed,mixed> $accumulativeTime
|
||||
* @param array<mixed,mixed> $page
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('fieldAndValueProvider')]
|
||||
public function testCreatePageHitConditionForm(
|
||||
array $pageUrl,
|
||||
array $startDate,
|
||||
array $endDate,
|
||||
array $accumulativeTime,
|
||||
array $page,
|
||||
): void {
|
||||
// Fetch the campaign condition form.
|
||||
$uri = 's/campaigns/events/new?type=lead.pageHit&eventType=condition&campaignId=3&anchor=leadsource&anchorEventType=source&_=1682493324393&mauticUserLastActive=897&mauticLastNotificationId=';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Get the form HTML element out of the response, fill it in and submit.
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[anchor]' => 'leadsource',
|
||||
'campaignevent[properties]['.$pageUrl[0].']' => $pageUrl[1],
|
||||
'campaignevent[properties]['.$startDate[0].']' => $startDate[1],
|
||||
'campaignevent[properties]['.$endDate[0].']' => $endDate[1],
|
||||
'campaignevent[properties]['.$accumulativeTime[0].']' => $accumulativeTime[1],
|
||||
'campaignevent[properties]['.$page[0].']' => $page[1] ?? '',
|
||||
'campaignevent[type]' => 'lead.pageHit',
|
||||
'campaignevent[eventType]' => 'condition',
|
||||
'campaignevent[anchorEventType]' => 'source',
|
||||
'campaignevent[campaignId]' => '3',
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $form->getPhpValues());
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
Assert::assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed,mixed>
|
||||
*/
|
||||
public static function fieldAndValueProvider(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'pageUrl' => ['page_url', 'https://example.com'],
|
||||
'startDate' => ['startDate', (new \DateTime())->format('Y-m-d H:i:s')],
|
||||
'endDate' => ['endDate', (new \DateTime())->modify('+ 5 days')->format('Y-m-d H:i:s')],
|
||||
'accumulativeTime' => ['accumulative_time', 5],
|
||||
'page' => ['page', null],
|
||||
],
|
||||
[
|
||||
'pageUrl' => ['page_url', 'https://example.com'],
|
||||
'startDate' => ['startDate', (new \DateTime())->format('Y-m-d H:i:s')],
|
||||
'endDate' => ['endDate', (new \DateTime())->modify('+ 10 days')->format('Y-m-d H:i:s')],
|
||||
'accumulativeTime' => ['accumulative_time', null],
|
||||
'page' => ['page', ''],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\CampaignRepository;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\Result\CountResult;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class CampaignRepositoryFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private CampaignRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->repository = self::getContainer()->get('mautic.campaign.repository.campaign');
|
||||
}
|
||||
|
||||
public function testGetCountsForPendingContactsWithEmptyData(): void
|
||||
{
|
||||
$result = $this->repository->getCountsForPendingContacts(
|
||||
1,
|
||||
[1, 2, 3],
|
||||
new ContactLimiter(100, null, null, null, [1, 2, 3])
|
||||
);
|
||||
|
||||
Assert::assertEquals(
|
||||
new CountResult(0, 0, 0),
|
||||
$result,
|
||||
'There should not be any match as there are no campaign/lead records.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetCountsForPendingContactsWithoutEventLogs(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$eventOne = $this->createEvent($campaign);
|
||||
$eventTwo = $this->createEvent($campaign);
|
||||
$eventThree = $this->createEvent($campaign);
|
||||
$leadOne = $this->createLead($campaign);
|
||||
$leadTwo = $this->createLead($campaign);
|
||||
$leadThree = $this->createLead($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->repository->getCountsForPendingContacts(
|
||||
$campaign->getId(),
|
||||
[$eventOne->getId(), $eventTwo->getId(), $eventThree->getId()],
|
||||
new ContactLimiter(100, null, null, null, [$leadOne->getId(), $leadTwo->getId(), $leadThree->getId()])
|
||||
);
|
||||
|
||||
Assert::assertEquals(
|
||||
new CountResult(3, $leadOne->getId(), $leadThree->getId()),
|
||||
$result,
|
||||
'All three leads should match as none of them have any event logs.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetCountsForPendingContactsWithEventLogs(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$logOne = $this->createEventLog($campaign);
|
||||
$leadOne = $logOne->getLead();
|
||||
$eventOne = $logOne->getEvent();
|
||||
$logTwo = $this->createEventLog($campaign);
|
||||
$leadTwo = $logTwo->getLead();
|
||||
$eventTwo = $logTwo->getEvent();
|
||||
$leadThree = $this->createLead($campaign);
|
||||
$eventThree = $this->createEvent($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->repository->getCountsForPendingContacts(
|
||||
$campaign->getId(),
|
||||
[$eventOne->getId(), $eventTwo->getId(), $eventThree->getId()],
|
||||
new ContactLimiter(100, null, null, null, [$leadOne->getId(), $leadTwo->getId(), $leadThree->getId()])
|
||||
);
|
||||
|
||||
Assert::assertEquals(
|
||||
new CountResult(1, $leadThree->getId(), $leadThree->getId()),
|
||||
$result,
|
||||
'Only lead three should match as it is the only one who does not have any event log.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetCountsForPendingContactsWithEventLogsWithNonMatchingRotations(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$logOne = $this->createEventLog($campaign);
|
||||
$leadOne = $logOne->getLead();
|
||||
$eventOne = $logOne->getEvent();
|
||||
$logTwo = $this->createEventLog($campaign, $campaignLeadTwo);
|
||||
$leadTwo = $logTwo->getLead();
|
||||
$eventTwo = $logTwo->getEvent();
|
||||
$logThree = $this->createEventLog($campaign);
|
||||
$leadThree = $logThree->getLead();
|
||||
$eventThree = $logThree->getEvent();
|
||||
$campaignLeadTwo->setRotation($logTwo->getRotation() + 1);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->repository->getCountsForPendingContacts(
|
||||
$campaign->getId(),
|
||||
[$eventOne->getId(), $eventTwo->getId(), $eventThree->getId()],
|
||||
new ContactLimiter(100, null, null, null, [$leadOne->getId(), $leadTwo->getId(), $leadThree->getId()])
|
||||
);
|
||||
|
||||
Assert::assertEquals(
|
||||
new CountResult(1, $leadTwo->getId(), $leadTwo->getId()),
|
||||
$result,
|
||||
'Only lead two should match as it is the only one who has a non-matching rotation.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetCampaignPublishAndVersionData(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->repository->getCampaignPublishAndVersionData($campaign->getId());
|
||||
|
||||
Assert::assertIsArray($result);
|
||||
Assert::assertArrayHasKey('is_published', $result);
|
||||
Assert::assertArrayHasKey('version', $result);
|
||||
Assert::assertEquals('1', $result['is_published']);
|
||||
// Version should be a string representation of an integer
|
||||
Assert::assertIsString($result['version']);
|
||||
Assert::assertGreaterThanOrEqual('1', $result['version']);
|
||||
}
|
||||
|
||||
public function testGetCampaignPublishAndVersionDataWithNonExistentCampaign(): void
|
||||
{
|
||||
$nonExistentId = 99999;
|
||||
|
||||
$result = $this->repository->getCampaignPublishAndVersionData($nonExistentId);
|
||||
|
||||
Assert::assertEquals([], $result);
|
||||
}
|
||||
|
||||
private function createLead(Campaign $campaign, ?CampaignLead &$campaignLead = null): Lead // @phpstan-ignore parameterByRef.unusedType
|
||||
{
|
||||
$lead = new Lead();
|
||||
$this->em->persist($lead);
|
||||
|
||||
$campaignLead = new CampaignLead();
|
||||
$campaignLead->setCampaign($campaign);
|
||||
$campaignLead->setLead($lead);
|
||||
$campaignLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($campaignLead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign');
|
||||
$campaign->setIsPublished(true);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createEvent(Campaign $campaign): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName('Test event');
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType('lead.changepoints');
|
||||
$event->setEventType(Event::TYPE_ACTION);
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createEventLog(Campaign $campaign, ?CampaignLead &$campaignLead = null): LeadEventLog
|
||||
{
|
||||
$event = $this->createEvent($campaign);
|
||||
$lead = $this->createLead($campaign, $campaignLead);
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setLead($lead);
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setTriggerDate(new \DateTime());
|
||||
$this->em->persist($leadEventLog);
|
||||
|
||||
return $leadEventLog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilder;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\CampaignRepository;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class CampaignRepositoryTest extends TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
/**
|
||||
* @var MockObject&QueryBuilder
|
||||
*/
|
||||
private MockObject $queryBuilder;
|
||||
|
||||
private CampaignRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->queryBuilder = $this->getMockBuilder(QueryBuilder::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['select', 'from', 'where', 'setParameter', 'andWhere', 'getQuery', 'getRootAliases'])
|
||||
->getMock();
|
||||
|
||||
$this->repository = $this->configureRepository(Campaign::class);
|
||||
|
||||
$this->entityManager->method('createQueryBuilder')->willReturn($this->queryBuilder);
|
||||
$this->connection->method('createQueryBuilder')->willReturnCallback(fn () => new DbalQueryBuilder($this->connection));
|
||||
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$translator->method('trans')->willReturnCallback(fn ($id) => match ($id) {
|
||||
'mautic.campaign.campaign.searchcommand.isexpired' => 'is:expired',
|
||||
'mautic.campaign.campaign.searchcommand.ispending' => 'is:pending',
|
||||
default => $id,
|
||||
});
|
||||
$this->repository->setTranslator($translator);
|
||||
}
|
||||
|
||||
public function testFetchEmailIdsById(): void
|
||||
{
|
||||
$id = 2;
|
||||
|
||||
$queryResult = [
|
||||
1 => ['channelId' => 1],
|
||||
2 => ['channelId' => 2],
|
||||
];
|
||||
|
||||
$expectedResult = [1, 2];
|
||||
|
||||
$this->entityManager
|
||||
->method('createQueryBuilder')
|
||||
->willReturn($this->queryBuilder);
|
||||
|
||||
$this->queryBuilder->expects(self::once())
|
||||
->method('select')
|
||||
->with('e.channelId')
|
||||
->willReturn($this->queryBuilder);
|
||||
|
||||
$this->queryBuilder->expects(self::once())
|
||||
->method('from')
|
||||
->with(Campaign::class, $this->repository->getTableAlias(), $this->repository->getTableAlias().'.id')
|
||||
->willReturn($this->queryBuilder);
|
||||
|
||||
$this->queryBuilder->expects(self::once())
|
||||
->method('where')
|
||||
->with($this->repository->getTableAlias().'.id = :id')
|
||||
->willReturn($this->queryBuilder);
|
||||
|
||||
$this->queryBuilder->expects(self::once())
|
||||
->method('setParameter')
|
||||
->with('id', $id)
|
||||
->willReturn($this->queryBuilder);
|
||||
|
||||
$this->queryBuilder->method('getRootAliases')
|
||||
->willReturn(['e']);
|
||||
|
||||
$this->queryBuilder->expects(self::once())
|
||||
->method('andWhere')
|
||||
->with('e.channelId IS NOT NULL')
|
||||
->willReturn($this->queryBuilder);
|
||||
|
||||
$query = $this->getMockBuilder(Query::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['setHydrationMode', 'getResult'])
|
||||
->getMock();
|
||||
|
||||
$query->expects(self::once())
|
||||
->method('setHydrationMode')
|
||||
->with(Query::HYDRATE_ARRAY)
|
||||
->willReturn($query);
|
||||
|
||||
$this->queryBuilder->expects(self::once())
|
||||
->method('getQuery')
|
||||
->willReturn($query);
|
||||
|
||||
$query->expects(self::once())
|
||||
->method('getResult')
|
||||
->willReturn($queryResult);
|
||||
|
||||
$result = $this->repository->fetchEmailIdsById($id);
|
||||
|
||||
$this->assertEquals($expectedResult, $result);
|
||||
}
|
||||
|
||||
public function testAddSearchCommandWhereClauseHandlesExpirationFilters(): void
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$filter = (object) ['command' => 'is:expired', 'string' => '', 'not' => false, 'strict' => false];
|
||||
|
||||
$method = new \ReflectionMethod(CampaignRepository::class, 'addSearchCommandWhereClause');
|
||||
$method->setAccessible(true);
|
||||
|
||||
[$expr, $params] = $method->invoke($this->repository, $qb, $filter);
|
||||
|
||||
self::assertSame(
|
||||
'(c.isPublished = :par1) AND (c.publishDown IS NOT NULL) AND (c.publishDown <> \'\') AND (c.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(CampaignRepository::class, 'addSearchCommandWhereClause');
|
||||
$method->setAccessible(true);
|
||||
|
||||
[$expr, $params] = $method->invoke($this->repository, $qb, $filter);
|
||||
|
||||
self::assertSame(
|
||||
'(c.isPublished = :par1) AND (c.publishUp IS NOT NULL) AND (c.publishUp <> \'\') AND (c.publishUp > CURRENT_TIMESTAMP())',
|
||||
(string) $expr
|
||||
);
|
||||
self::assertSame(['par1' => true], $params);
|
||||
}
|
||||
|
||||
public function testGetSearchCommandsContainsExpirationFilters(): void
|
||||
{
|
||||
$commands = $this->repository->getSearchCommands();
|
||||
self::assertContains('mautic.campaign.campaign.searchcommand.isexpired', $commands);
|
||||
self::assertContains('mautic.campaign.campaign.searchcommand.ispending', $commands);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class CampaignTest extends TestCase
|
||||
{
|
||||
public function testGetEventsByType(): void
|
||||
{
|
||||
$campaign = $this->addSomeEvents(new Campaign());
|
||||
|
||||
Assert::assertCount(2, $campaign->getEventsByType(Event::TYPE_DECISION));
|
||||
Assert::assertCount(1, $campaign->getEventsByType(Event::TYPE_ACTION));
|
||||
Assert::assertCount(1, $campaign->getEventsByType(Event::TYPE_CONDITION));
|
||||
}
|
||||
|
||||
private function addSomeEvents(Campaign $campaign): Campaign
|
||||
{
|
||||
$decisionA = new EventFake(1);
|
||||
$decisionA->setName('Decision A');
|
||||
$decisionA->setEventType(Event::TYPE_DECISION);
|
||||
|
||||
$action = new EventFake(2);
|
||||
$action->setName('Action A');
|
||||
$action->setEventType(Event::TYPE_ACTION);
|
||||
|
||||
$condition = new EventFake(3);
|
||||
$condition->setName('Condition A');
|
||||
$condition->setEventType(Event::TYPE_CONDITION);
|
||||
|
||||
$decisionB = new EventFake(4);
|
||||
$decisionB->setName('Decision B');
|
||||
$decisionB->setEventType(Event::TYPE_DECISION);
|
||||
|
||||
$campaign->addEvent($decisionA->getId(), $decisionA);
|
||||
$campaign->addEvent($action->getId(), $action);
|
||||
$campaign->addEvent($condition->getId(), $condition);
|
||||
$campaign->addEvent($decisionB->getId(), $decisionB);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
|
||||
use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilder;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\QueryBuilder as OrmQueryBuilder;
|
||||
use Mautic\CampaignBundle\Entity\ContactLimiterTrait;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CoreBundle\Test\Doctrine\MockedConnectionTrait;
|
||||
|
||||
class ContactLimiterTraitTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use ContactLimiterTrait;
|
||||
use MockedConnectionTrait;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|Connection
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $connection;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|EntityManagerInterface
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->connection = $this->getMockedConnection();
|
||||
|
||||
$expr = new ExpressionBuilder($this->connection);
|
||||
$this->connection->method('getExpressionBuilder')
|
||||
->willReturn($expr);
|
||||
|
||||
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$this->entityManager->method('getExpressionBuilder')
|
||||
->willReturn(new Expr());
|
||||
}
|
||||
|
||||
public function testSpecificContactId(): void
|
||||
{
|
||||
$contactLimiter = new ContactLimiter(50, 1);
|
||||
|
||||
$qb = new DbalQueryBuilder($this->connection);
|
||||
$this->updateQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE l.lead_id = :contactId LIMIT 50', $qb->getSQL());
|
||||
$this->assertEquals(['contactId' => 1], $qb->getParameters());
|
||||
|
||||
$qb = new OrmQueryBuilder($this->entityManager);
|
||||
$this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE IDENTITY(l.lead) = :contact', $qb->getDQL());
|
||||
$this->assertEquals(1, $qb->getParameter('contact')->getValue());
|
||||
$this->assertEquals(50, $qb->getMaxResults());
|
||||
}
|
||||
|
||||
public function testListOfContacts(): void
|
||||
{
|
||||
$contactLimiter = new ContactLimiter(50, null, null, null, [1, 2, 3]);
|
||||
|
||||
$qb = new DbalQueryBuilder($this->connection);
|
||||
$this->updateQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE l.lead_id IN (:contactIds) LIMIT 50', $qb->getSQL());
|
||||
$this->assertEquals(['contactIds' => [1, 2, 3]], $qb->getParameters());
|
||||
|
||||
$qb = new OrmQueryBuilder($this->entityManager);
|
||||
$this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE IDENTITY(l.lead) IN(:contactIds)', $qb->getDQL());
|
||||
$this->assertEquals([1, 2, 3], $qb->getParameter('contactIds')->getValue());
|
||||
$this->assertEquals(50, $qb->getMaxResults());
|
||||
}
|
||||
|
||||
public function testMinContactId(): void
|
||||
{
|
||||
$contactLimiter = new ContactLimiter(50, null, 4, null);
|
||||
|
||||
$qb = new DbalQueryBuilder($this->connection);
|
||||
$this->updateQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE l.lead_id >= :minContactId LIMIT 50', $qb->getSQL());
|
||||
$this->assertEquals(['minContactId' => 4], $qb->getParameters());
|
||||
|
||||
$qb = new OrmQueryBuilder($this->entityManager);
|
||||
$this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE IDENTITY(l.lead) >= :minContactId', $qb->getDQL());
|
||||
$this->assertEquals(4, $qb->getParameter('minContactId')->getValue());
|
||||
$this->assertEquals(50, $qb->getMaxResults());
|
||||
}
|
||||
|
||||
public function testBatchMinContactId(): void
|
||||
{
|
||||
$contactLimiter = new ContactLimiter(50, null, 4, null);
|
||||
|
||||
$qb = new DbalQueryBuilder($this->connection);
|
||||
$contactLimiter->setBatchMinContactId(10);
|
||||
$this->updateQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE l.lead_id >= :minContactId LIMIT 50', $qb->getSQL());
|
||||
$this->assertEquals(['minContactId' => 10], $qb->getParameters());
|
||||
|
||||
$qb = new OrmQueryBuilder($this->entityManager);
|
||||
$this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE IDENTITY(l.lead) >= :minContactId', $qb->getDQL());
|
||||
$this->assertEquals(10, $qb->getParameter('minContactId')->getValue());
|
||||
$this->assertEquals(50, $qb->getMaxResults());
|
||||
}
|
||||
|
||||
public function testMaxContactId(): void
|
||||
{
|
||||
$contactLimiter = new ContactLimiter(50, null, null, 10);
|
||||
|
||||
$qb = new DbalQueryBuilder($this->connection);
|
||||
$this->updateQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE l.lead_id <= :maxContactId LIMIT 50', $qb->getSQL());
|
||||
$this->assertEquals(['maxContactId' => 10], $qb->getParameters());
|
||||
|
||||
$qb = new OrmQueryBuilder($this->entityManager);
|
||||
$this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE IDENTITY(l.lead) <= :maxContactId', $qb->getDQL());
|
||||
$this->assertEquals(10, $qb->getParameter('maxContactId')->getValue());
|
||||
$this->assertEquals(50, $qb->getMaxResults());
|
||||
}
|
||||
|
||||
public function testMinAndMaxContactId(): void
|
||||
{
|
||||
$contactLimiter = new ContactLimiter(50, null, 1, 10);
|
||||
|
||||
$qb = new DbalQueryBuilder($this->connection);
|
||||
$this->updateQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE l.lead_id BETWEEN :minContactId AND :maxContactId LIMIT 50', $qb->getSQL());
|
||||
$this->assertEquals(['minContactId' => 1, 'maxContactId' => 10], $qb->getParameters());
|
||||
|
||||
$qb = new OrmQueryBuilder($this->entityManager);
|
||||
$this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE IDENTITY(l.lead) BETWEEN :minContactId AND :maxContactId', $qb->getDQL());
|
||||
$this->assertEquals(1, $qb->getParameter('minContactId')->getValue());
|
||||
$this->assertEquals(10, $qb->getParameter('maxContactId')->getValue());
|
||||
$this->assertEquals(50, $qb->getMaxResults());
|
||||
}
|
||||
|
||||
public function testThreads(): void
|
||||
{
|
||||
$contactLimiter = new ContactLimiter(50, null, null, null, [], 1, 5);
|
||||
|
||||
$qb = new DbalQueryBuilder($this->connection);
|
||||
$this->updateQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE MOD((l.lead_id + :threadShift), :maxThreads) = 0 LIMIT 50', $qb->getSQL());
|
||||
$this->assertEquals(['threadShift' => 0, 'maxThreads' => 5], $qb->getParameters());
|
||||
|
||||
$qb = new OrmQueryBuilder($this->entityManager);
|
||||
$this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter);
|
||||
$this->assertEquals('SELECT WHERE MOD((IDENTITY(l.lead) + :threadShift), :maxThreads) = 0', $qb->getDQL());
|
||||
$this->assertEquals(0, $qb->getParameter('threadShift')->getValue());
|
||||
$this->assertEquals(5, $qb->getParameter('maxThreads')->getValue());
|
||||
$this->assertEquals(50, $qb->getMaxResults());
|
||||
}
|
||||
|
||||
public function testMaxResultsIgnoredForCountQueries(): void
|
||||
{
|
||||
$contactLimiter = new ContactLimiter(50, 1);
|
||||
|
||||
$qb = new DbalQueryBuilder($this->connection);
|
||||
$this->updateQueryFromContactLimiter('l', $qb, $contactLimiter, true);
|
||||
$this->assertEquals('SELECT WHERE l.lead_id = :contactId', $qb->getSQL());
|
||||
$this->assertEquals(['contactId' => 1], $qb->getParameters());
|
||||
|
||||
$qb = new OrmQueryBuilder($this->entityManager);
|
||||
$this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter, true);
|
||||
$this->assertEquals('SELECT WHERE IDENTITY(l.lead) = :contact', $qb->getDQL());
|
||||
$this->assertEquals(1, $qb->getParameter('contact')->getValue());
|
||||
$this->assertEquals(null, $qb->getMaxResults());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
|
||||
/**
|
||||
* Allows to use the live Event entity and set the ID.
|
||||
*/
|
||||
final class EventFake extends Event
|
||||
{
|
||||
private ?int $id;
|
||||
|
||||
public function __construct(?int $id = null)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\EventRepository;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignMember;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class EventRepositoryFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @return iterable<string, array{?\DateTime, ?\DateTime, int}>
|
||||
*/
|
||||
public static function dataGetContactPendingEventsConsidersCampaignPublishUpAndDown(): iterable
|
||||
{
|
||||
yield 'Publish Up and Down not set' => [null, null, 1];
|
||||
yield 'Publish Up and Down set' => [new \DateTime('-1 day'), new \DateTime('+1 day'), 1];
|
||||
yield 'Publish Up and Down set with Publish Up in the future' => [new \DateTime('+1 day'), new \DateTime('+2 day'), 0];
|
||||
yield 'Publish Up and Down set with Publish Down in the past' => [new \DateTime('-2 day'), new \DateTime('-1 day'), 0];
|
||||
yield 'Publish Up in the past' => [new \DateTime('-1 day'), null, 1];
|
||||
yield 'Publish Up in the future' => [new \DateTime('+1 day'), null, 0];
|
||||
yield 'Publish Down in the past' => [null, new \DateTime('-1 day'), 0];
|
||||
yield 'Publish Down in the future' => [null, new \DateTime('+1 day'), 1];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataGetContactPendingEventsConsidersCampaignPublishUpAndDown')]
|
||||
public function testGetContactPendingEventsConsidersCampaignPublishUpAndDown(?\DateTime $publishUp, ?\DateTime $publishDown, int $expectedCount): void
|
||||
{
|
||||
$repository = static::getContainer()->get('mautic.campaign.repository.event');
|
||||
\assert($repository instanceof EventRepository);
|
||||
|
||||
$campaign = $this->createCampaign();
|
||||
$event = $this->createEvent($campaign);
|
||||
$lead = $this->createLead();
|
||||
$this->createCampaignMember($lead, $campaign);
|
||||
|
||||
$campaign->setPublishUp($publishUp);
|
||||
$campaign->setPublishDown($publishDown);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
Assert::assertCount($expectedCount, $repository->getContactPendingEvents($lead->getId(), $event->getType()));
|
||||
}
|
||||
|
||||
private function createLead(): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test');
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test');
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createEvent(Campaign $campaign): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName('test');
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType('test.type');
|
||||
$event->setEventType('action');
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createCampaignMember(Lead $lead, Campaign $campaign): void
|
||||
{
|
||||
$member = new CampaignMember();
|
||||
$member->setLead($lead);
|
||||
$member->setCampaign($campaign);
|
||||
$member->setDateAdded(new \DateTime());
|
||||
$this->em->persist($member);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class EventRepositoryTest extends TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
public function testDecreaseFailedCount(): void
|
||||
{
|
||||
$emMock = $this->createMock(EntityManager::class);
|
||||
$connMock = $this->createMock(Connection::class);
|
||||
$queryBuilderMock = $this->createMock(QueryBuilder::class);
|
||||
$expressionMock = $this->createMock(Expr::class);
|
||||
|
||||
$queryBuilderMock->expects($this->any())
|
||||
->method('expr')
|
||||
->willReturn($expressionMock);
|
||||
|
||||
$expressionMock->expects($this->once())
|
||||
->method('eq')
|
||||
->with('id', ':id')
|
||||
->willReturn('id = :id');
|
||||
|
||||
$queryBuilderMock->expects($this->any())
|
||||
->method('expr')
|
||||
->willReturn($expressionMock);
|
||||
|
||||
$expressionMock->expects($this->once())
|
||||
->method('gt')
|
||||
->with('failed_count', 0)
|
||||
->willReturn('failed_count > 0');
|
||||
|
||||
$queryBuilderMock->expects($this->once())
|
||||
->method('update')
|
||||
->with(MAUTIC_TABLE_PREFIX.'campaign_events')
|
||||
->willReturn($queryBuilderMock);
|
||||
|
||||
$queryBuilderMock->expects($this->once())
|
||||
->method('set')
|
||||
->with('failed_count', 'failed_count - 1')
|
||||
->willReturn($queryBuilderMock);
|
||||
|
||||
$queryBuilderMock->expects($this->once())
|
||||
->method('where')
|
||||
->with('id = :id')
|
||||
->willReturn($queryBuilderMock);
|
||||
|
||||
$queryBuilderMock->expects($this->once())
|
||||
->method('andWhere')
|
||||
->with('failed_count > 0')
|
||||
->willReturn($queryBuilderMock);
|
||||
|
||||
$queryBuilderMock->expects($this->once())
|
||||
->method('setParameter')
|
||||
->with('id', $this->equalTo(42))
|
||||
->willReturn($queryBuilderMock);
|
||||
|
||||
$connMock->expects($this->once())
|
||||
->method('createQueryBuilder')
|
||||
->willReturn($queryBuilderMock);
|
||||
|
||||
$emMock->expects($this->once())
|
||||
->method('getConnection')
|
||||
->willReturn($connMock);
|
||||
|
||||
$eventRepository = $this->configureRepository(Event::class, $emMock);
|
||||
$this->connection->method('createQueryBuilder')
|
||||
->willReturnCallback(fn () => $queryBuilderMock);
|
||||
|
||||
$eventMock = $this->createMock(Event::class);
|
||||
$eventMock->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
$eventRepository->decreaseFailedCount($eventMock);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class EventTest extends TestCase
|
||||
{
|
||||
private const TEST_NAME = 'Test Name';
|
||||
private const DATE = '2021-10-08 08:00:00';
|
||||
|
||||
public function testSetTriggerHourWhenEmpty(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName(self::TEST_NAME);
|
||||
$event->setTriggerHour('');
|
||||
$this->assertNull($event->getTriggerHour());
|
||||
}
|
||||
|
||||
public function testSetTriggerHourWhenArray(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName(self::TEST_NAME);
|
||||
$event->setTriggerHour(['date' => self::DATE]);
|
||||
$this->assertEquals(new \DateTime(self::DATE), $event->getTriggerHour());
|
||||
}
|
||||
|
||||
public function testSetTriggerHourWhenString(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName(self::TEST_NAME);
|
||||
$event->setTriggerHour(self::DATE);
|
||||
$this->assertEquals(new \DateTime(self::DATE), $event->getTriggerHour());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Persisters\Entity\EntityPersister;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Mautic\CampaignBundle\Entity\FailedLeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class LeadEventLogRepositoryTest extends TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('isLastFailedDataProvider')]
|
||||
public function testIsLastFailed(?LeadEventLog $leadEventLog, bool $expectedResult): void
|
||||
{
|
||||
$emMock = $this->createMock(EntityManager::class);
|
||||
$unitOfWorkMock = $this->createMock(UnitOfWork::class);
|
||||
$emMock->method('getUnitOfWork')
|
||||
->willReturn($unitOfWorkMock);
|
||||
|
||||
$entityPersisterMock = $this->createMock(EntityPersister::class);
|
||||
$unitOfWorkMock->method('getEntityPersister')
|
||||
->willReturn($entityPersisterMock);
|
||||
|
||||
$entityPersisterMock->method('load')
|
||||
->with(['lead' => 42, 'event' => 4242], null, null, [], null, 1, ['dateTriggered' => 'DESC'])
|
||||
->willReturn($leadEventLog);
|
||||
|
||||
$leadEventLogRepository = $this->configureRepository(LeadEventLog::class, $emMock);
|
||||
$this->connection->method('createQueryBuilder')->willReturnCallback(fn () => new QueryBuilder($this->connection));
|
||||
|
||||
$isLastFailed = $leadEventLogRepository->isLastFailed(42, 4242);
|
||||
$this->assertSame($expectedResult, $isLastFailed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array<mixed>>
|
||||
*/
|
||||
public static function isLastFailedDataProvider(): array
|
||||
{
|
||||
$leadEventLogNoFail = new LeadEventLog();
|
||||
$failedLeadEvent = new FailedLeadEventLog();
|
||||
$leadEventLogFail = new LeadEventLog();
|
||||
$leadEventLogFail->setFailedLog($failedLeadEvent);
|
||||
|
||||
return [
|
||||
'no_last_log' => [null, false],
|
||||
'last_log_no_fail' => [$leadEventLogNoFail, false],
|
||||
'last_log_fail' => [$leadEventLogFail, true],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Event;
|
||||
|
||||
use Mautic\AssetBundle\Form\Type\PointActionAssetDownloadType;
|
||||
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
|
||||
use Mautic\CampaignBundle\Tests\CampaignTestAbstract;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\FormBundle\Form\Type\CampaignEventFormFieldValueType;
|
||||
|
||||
class CampaignBuilderEventTest extends CampaignTestAbstract
|
||||
{
|
||||
public function testAddGetDecision(): void
|
||||
{
|
||||
$decisionKey = 'email.open';
|
||||
$decision = [
|
||||
'label' => 'mautic.email.campaign.event.open',
|
||||
'description' => 'mautic.email.campaign.event.open_descr',
|
||||
'eventName' => 'mautic.email.on_campaign_trigger_decision',
|
||||
'connectionRestrictions' => [
|
||||
'source' => [
|
||||
'action' => [
|
||||
'email.send',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$event = $this->initEvent();
|
||||
$event->addDecision(
|
||||
$decisionKey,
|
||||
$decision
|
||||
);
|
||||
|
||||
$decisions = $event->getDecisions();
|
||||
$this->assertSame([$decisionKey => $decision], $decisions);
|
||||
}
|
||||
|
||||
public function testEventDecisionSort(): void
|
||||
{
|
||||
$decision = [
|
||||
'label' => 'mautic.email.campaign.event.open',
|
||||
'description' => 'mautic.email.campaign.event.open_descr',
|
||||
'eventName' => 'mautic.email.on_campaign_trigger_decision',
|
||||
'connectionRestrictions' => [
|
||||
'source' => [
|
||||
'action' => [
|
||||
'email.send',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$event = $this->initEvent();
|
||||
|
||||
// add 3 unsorted decisions
|
||||
$event->addDecision('email.open1', $decision);
|
||||
$decision['label'] = 'mautic.email.campaign.event.open.3';
|
||||
$event->addDecision('email.open3', $decision);
|
||||
$decision['label'] = 'mautic.email.campaign.event.open.2';
|
||||
$event->addDecision('email.open2', $decision);
|
||||
|
||||
$decisions = $event->getDecisions();
|
||||
|
||||
$this->assertCount(3, $decisions);
|
||||
|
||||
$shouldBe = 1;
|
||||
foreach ($decisions as $key => $resultDecision) {
|
||||
$this->assertSame('email.open'.$shouldBe, $key);
|
||||
++$shouldBe;
|
||||
}
|
||||
}
|
||||
|
||||
public function testEventConditionSort(): void
|
||||
{
|
||||
$condition = [
|
||||
'label' => 'mautic.form.campaign.event.field_value',
|
||||
'description' => 'mautic.form.campaign.event.field_value_descr',
|
||||
'formType' => CampaignEventFormFieldValueType::class,
|
||||
'formTheme' => '@MauticForm/FormTheme/FieldValueCondition/_campaignevent_form_field_value_widget.html.twig',
|
||||
'eventName' => 'mautic.form.on_campaign_trigger_condition',
|
||||
];
|
||||
$event = $this->initEvent();
|
||||
|
||||
// add 3 unsorted conditions
|
||||
$event->addCondition('form.field_value1', $condition);
|
||||
$condition['label'] = 'mautic.form.campaign.event.field_value.3';
|
||||
$event->addCondition('form.field_value3', $condition);
|
||||
$condition['label'] = 'mautic.form.campaign.event.field_value.2';
|
||||
$event->addCondition('form.field_value2', $condition);
|
||||
|
||||
$conditions = $event->getConditions();
|
||||
|
||||
$this->assertCount(3, $conditions);
|
||||
|
||||
$shouldBe = 1;
|
||||
foreach ($conditions as $key => $resultCondition) {
|
||||
$this->assertSame('form.field_value'.$shouldBe, $key);
|
||||
++$shouldBe;
|
||||
}
|
||||
}
|
||||
|
||||
public function testEventActionSort(): void
|
||||
{
|
||||
$action = [
|
||||
'group' => 'mautic.asset.actions',
|
||||
'label' => 'mautic.asset.point.action.download',
|
||||
'description' => 'mautic.asset.point.action.download_descr',
|
||||
'callback' => [\Mautic\AssetBundle\Helper\PointActionHelper::class, 'validateAssetDownload'],
|
||||
'formType' => PointActionAssetDownloadType::class,
|
||||
];
|
||||
$event = $this->initEvent();
|
||||
|
||||
// add 3 unsorted actions
|
||||
$event->addAction('asset.download1', $action);
|
||||
$action['label'] = 'mautic.asset.point.action.download.3';
|
||||
$event->addAction('asset.download3', $action);
|
||||
$action['label'] = 'mautic.asset.point.action.download.2';
|
||||
$event->addAction('asset.download2', $action);
|
||||
|
||||
$actions = $event->getActions();
|
||||
|
||||
$this->assertCount(3, $actions);
|
||||
|
||||
$shouldBe = 1;
|
||||
foreach ($actions as $key => $resultAction) {
|
||||
$this->assertSame('asset.download'.$shouldBe, $key);
|
||||
++$shouldBe;
|
||||
}
|
||||
}
|
||||
|
||||
protected function initEvent()
|
||||
{
|
||||
$translator = $this->createMock(Translator::class);
|
||||
|
||||
$translator->expects($this->any())
|
||||
->method('trans')
|
||||
->willReturnCallback(function () {
|
||||
$args = func_get_args();
|
||||
|
||||
return $args[0];
|
||||
});
|
||||
|
||||
return new CampaignBuilderEvent($translator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Event;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Event\PendingEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class PendingEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testFailAndPassRemainingWithError(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$contact = new Lead();
|
||||
$logA = new LeadEventLog();
|
||||
$logB = new LeadEventLog();
|
||||
$interval = new \DateInterval('PT10M');
|
||||
|
||||
$logA->setLead($contact); // Will fail.
|
||||
$logB->setLead($contact); // Will pass with error.
|
||||
|
||||
$pendingEvent = new PendingEvent(new ActionAccessor([]), $event, new ArrayCollection([$logA, $logB]));
|
||||
|
||||
$pendingEvent->fail($logA, 'reason A', $interval);
|
||||
$pendingEvent->passRemainingWithError('Error B');
|
||||
|
||||
$failedLogs = $pendingEvent->getFailures();
|
||||
$successLogs = $pendingEvent->getSuccessful();
|
||||
|
||||
Assert::assertCount(1, $failedLogs);
|
||||
Assert::assertCount(1, $successLogs);
|
||||
Assert::AssertSame($logA, $failedLogs->current());
|
||||
Assert::AssertSame($logB, $successLogs->current());
|
||||
Assert::AssertSame($interval, $logA->getRescheduleInterval());
|
||||
Assert::AssertSame(['failed' => 1, 'reason' => 'reason A'], $logA->getMetadata());
|
||||
Assert::AssertSame(['failed' => 1, 'reason' => 'Error B'], $logB->getMetadata());
|
||||
Assert::AssertNull($logB->getRescheduleInterval());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventCollector\Accessor\Event;
|
||||
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
|
||||
class ActionAccessorTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testBatchEventNameIsNotExtra(): void
|
||||
{
|
||||
$actionAccessor = new ActionAccessor(['batchEventName' => 'test']);
|
||||
|
||||
$this->assertEmpty($actionAccessor->getExtraProperties());
|
||||
}
|
||||
|
||||
public function testBatchNameIsReturned(): void
|
||||
{
|
||||
$actionAccessor = new ActionAccessor(['batchEventName' => 'test']);
|
||||
|
||||
$this->assertEquals('test', $actionAccessor->getBatchEventName());
|
||||
}
|
||||
|
||||
public function testExtraParamIsReturned(): void
|
||||
{
|
||||
$actionAccessor = new ActionAccessor(['batchEventName' => 'test', 'foo' => 'bar']);
|
||||
|
||||
$this->assertEquals(['foo' => 'bar'], $actionAccessor->getExtraProperties());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventCollector\Accessor\Event;
|
||||
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor;
|
||||
|
||||
class ConditionAccessorTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testEventNameIsReturned(): void
|
||||
{
|
||||
$accessor = new ConditionAccessor(['eventName' => 'test']);
|
||||
|
||||
$this->assertEquals('test', $accessor->getEventName());
|
||||
}
|
||||
|
||||
public function testExtraParamIsReturned(): void
|
||||
{
|
||||
$accessor = new ConditionAccessor(['eventName' => 'test', 'foo' => 'bar']);
|
||||
|
||||
$this->assertEquals(['foo' => 'bar'], $accessor->getExtraProperties());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventCollector\Accessor\Event;
|
||||
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor;
|
||||
|
||||
class DecisionAccessorTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testEventNameIsReturned(): void
|
||||
{
|
||||
$accessor = new DecisionAccessor(['eventName' => 'test']);
|
||||
|
||||
$this->assertEquals('test', $accessor->getEventName());
|
||||
}
|
||||
|
||||
public function testExtraParamIsReturned(): void
|
||||
{
|
||||
$accessor = new DecisionAccessor(['eventName' => 'test', 'foo' => 'bar']);
|
||||
|
||||
$this->assertEquals(['foo' => 'bar'], $accessor->getExtraProperties());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventCollector\Accessor;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\EventAccessor;
|
||||
use Mautic\EmailBundle\Form\Type\EmailClickDecisionType;
|
||||
use Mautic\LeadBundle\Form\Type\CampaignEventLeadCampaignsType;
|
||||
use Mautic\LeadBundle\Form\Type\CompanyChangeScoreActionType;
|
||||
|
||||
class EventAccessorTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $events = [
|
||||
Event::TYPE_ACTION => [
|
||||
'lead.scorecontactscompanies' => [
|
||||
'label' => 'Add to company\'s score',
|
||||
'description' => 'This action will add the specified value to the company\'s existing score',
|
||||
'formType' => CompanyChangeScoreActionType::class,
|
||||
'batchEventName' => 'mautic.lead.on_campaign_trigger_action',
|
||||
],
|
||||
],
|
||||
Event::TYPE_CONDITION => [
|
||||
'lead.campaigns' => [
|
||||
'label' => 'Contact campaigns',
|
||||
'description' => 'Condition based on a contact campaigns.',
|
||||
'formType' => CampaignEventLeadCampaignsType::class,
|
||||
'formTheme' => 'MauticLeadBundle:FormTheme\\ContactCampaignsCondition',
|
||||
'eventName' => 'mautic.lead.on_campaign_trigger_condition',
|
||||
],
|
||||
],
|
||||
Event::TYPE_DECISION => [
|
||||
'email.click' => [
|
||||
'label' => 'Clicks email',
|
||||
'description' => 'Trigger actions when an email is clicked. Connect a Send Email action to the top of this decision.',
|
||||
'eventName' => 'mautic.email.on_campaign_trigger_decision',
|
||||
'formType' => EmailClickDecisionType::class,
|
||||
'connectionRestrictions' => [
|
||||
'source' => [
|
||||
'action' => [
|
||||
'email.send',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function testEventsArrayIsBuiltWithAccessors(): void
|
||||
{
|
||||
$eventAccessor = new EventAccessor($this->events);
|
||||
|
||||
// Actions
|
||||
$this->assertCount(1, $eventAccessor->getActions());
|
||||
$accessor = $eventAccessor->getAction('lead.scorecontactscompanies');
|
||||
$this->assertInstanceOf(ActionAccessor::class, $accessor);
|
||||
$this->assertEquals(
|
||||
$this->events[Event::TYPE_ACTION]['lead.scorecontactscompanies']['batchEventName'],
|
||||
$accessor->getBatchEventName()
|
||||
);
|
||||
|
||||
// Conditions
|
||||
$this->assertCount(1, $eventAccessor->getConditions());
|
||||
$accessor = $eventAccessor->getCondition('lead.campaigns');
|
||||
$this->assertInstanceOf(ConditionAccessor::class, $accessor);
|
||||
$this->assertEquals(
|
||||
$this->events[Event::TYPE_CONDITION]['lead.campaigns']['eventName'],
|
||||
$accessor->getEventName()
|
||||
);
|
||||
|
||||
// Decisions
|
||||
$this->assertCount(1, $eventAccessor->getDecisions());
|
||||
$accessor = $eventAccessor->getDecision('email.click');
|
||||
$this->assertInstanceOf(DecisionAccessor::class, $accessor);
|
||||
$this->assertEquals(
|
||||
$this->events[Event::TYPE_DECISION]['email.click']['eventName'],
|
||||
$accessor->getEventName()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventCollector\Builder;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\EventCollector\Builder\ConnectionBuilder;
|
||||
|
||||
class ConnectionBuilderTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testArrayIsBuiltAsItsUsedInJsPlumb(): void
|
||||
{
|
||||
$eventsArray = [
|
||||
Event::TYPE_ACTION => [
|
||||
'action1' => [
|
||||
'connectionRestrictions' => [
|
||||
'anchor' => ['decision1.inaction'],
|
||||
'source' => [
|
||||
'decision' => [
|
||||
'decision1',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'action2' => [
|
||||
// BC from way back
|
||||
'associatedDecisions' => [
|
||||
'decision1',
|
||||
],
|
||||
],
|
||||
'action3' => [
|
||||
// BC from way back
|
||||
'anchorRestrictions' => [
|
||||
'decision2.top',
|
||||
],
|
||||
],
|
||||
],
|
||||
Event::TYPE_DECISION => [
|
||||
'decision1' => [
|
||||
'connectionRestrictions' => ['source' => ['action' => ['action1']]],
|
||||
],
|
||||
'decision2' => [
|
||||
// BC From way back
|
||||
'associatedActions' => [
|
||||
'some.decision',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$results = ConnectionBuilder::buildRestrictionsArray($eventsArray);
|
||||
|
||||
$expected = [
|
||||
'anchor' => [
|
||||
'decision1' => [
|
||||
'action1' => ['inaction'],
|
||||
],
|
||||
'action3' => [
|
||||
'decision2' => ['top'],
|
||||
],
|
||||
],
|
||||
'action1' => [
|
||||
'source' => [
|
||||
'action' => [],
|
||||
'decision' => ['decision1'],
|
||||
],
|
||||
'target' => [
|
||||
'action' => [],
|
||||
'decision' => [],
|
||||
],
|
||||
],
|
||||
'action2' => [
|
||||
'source' => [
|
||||
'action' => [],
|
||||
'decision' => ['decision1'],
|
||||
],
|
||||
'target' => [
|
||||
'action' => [],
|
||||
'decision' => [],
|
||||
],
|
||||
],
|
||||
'action3' => [
|
||||
'source' => [
|
||||
'action' => [],
|
||||
'decision' => [],
|
||||
],
|
||||
'target' => [
|
||||
'action' => [],
|
||||
'decision' => [],
|
||||
],
|
||||
],
|
||||
'decision1' => [
|
||||
'source' => [
|
||||
'action' => ['action1'],
|
||||
'decision' => [],
|
||||
],
|
||||
'target' => [
|
||||
'action' => [],
|
||||
'decision' => [],
|
||||
],
|
||||
],
|
||||
'decision2' => [
|
||||
'source' => [
|
||||
'action' => [],
|
||||
'decision' => [],
|
||||
],
|
||||
'target' => [
|
||||
'action' => ['some.decision'],
|
||||
'decision' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $results);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventCollector\Builder;
|
||||
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\Builder\EventBuilder;
|
||||
|
||||
class EventBuilderTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testActionsAreConvertedToAccessor(): void
|
||||
{
|
||||
$array = [
|
||||
'some.action' => [
|
||||
'batchEventName' => 'some.action',
|
||||
],
|
||||
'other.action' => [
|
||||
'batchEventName' => 'other.action',
|
||||
],
|
||||
];
|
||||
|
||||
$converted = EventBuilder::buildActions($array);
|
||||
|
||||
$this->assertCount(2, $converted);
|
||||
$this->assertInstanceOf(ActionAccessor::class, $converted['some.action']);
|
||||
$this->assertEquals('some.action', $converted['some.action']->getBatchEventName());
|
||||
$this->assertInstanceOf(ActionAccessor::class, $converted['other.action']);
|
||||
$this->assertEquals('other.action', $converted['other.action']->getBatchEventName());
|
||||
}
|
||||
|
||||
public function testConditionsAreConvertedToAccessor(): void
|
||||
{
|
||||
$array = [
|
||||
'some.condition' => [
|
||||
'eventName' => 'some.condition',
|
||||
],
|
||||
'other.condition' => [
|
||||
'eventName' => 'other.condition',
|
||||
],
|
||||
];
|
||||
|
||||
$converted = EventBuilder::buildConditions($array);
|
||||
|
||||
$this->assertCount(2, $converted);
|
||||
$this->assertInstanceOf(ConditionAccessor::class, $converted['some.condition']);
|
||||
$this->assertEquals('some.condition', $converted['some.condition']->getEventName());
|
||||
$this->assertInstanceOf(ConditionAccessor::class, $converted['other.condition']);
|
||||
$this->assertEquals('other.condition', $converted['other.condition']->getEventName());
|
||||
}
|
||||
|
||||
public function testDecisionsAreConvertedToAccessor(): void
|
||||
{
|
||||
$array = [
|
||||
'some.decision' => [
|
||||
'eventName' => 'some.decision',
|
||||
],
|
||||
'other.decision' => [
|
||||
'eventName' => 'other.decision',
|
||||
],
|
||||
];
|
||||
|
||||
$converted = EventBuilder::buildDecisions($array);
|
||||
|
||||
$this->assertCount(2, $converted);
|
||||
$this->assertInstanceOf(DecisionAccessor::class, $converted['some.decision']);
|
||||
$this->assertEquals('some.decision', $converted['some.decision']->getEventName());
|
||||
$this->assertInstanceOf(DecisionAccessor::class, $converted['other.decision']);
|
||||
$this->assertEquals('other.decision', $converted['other.decision']->getEventName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventListener;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\EventRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository;
|
||||
use Mautic\CampaignBundle\Event\PendingEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
use Mautic\CampaignBundle\EventListener\CampaignActionJumpToEventSubscriber;
|
||||
use Mautic\CampaignBundle\Executioner\EventExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\Result\Counter;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Translation\Translator;
|
||||
|
||||
final class CampaignActionJumpToEventSubscriberTest extends TestCase
|
||||
{
|
||||
public function testOnJumpToEventWhenEventDoesNotExist(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$leadLog = new class extends LeadEventLog {
|
||||
public function getId(): int
|
||||
{
|
||||
return 456;
|
||||
}
|
||||
};
|
||||
$contact = new class extends Lead {
|
||||
public function getId(): int
|
||||
{
|
||||
return 789;
|
||||
}
|
||||
};
|
||||
$leadLog->setLead($contact);
|
||||
|
||||
$eventRepository = new class($campaign) extends EventRepository {
|
||||
public function __construct(
|
||||
private Campaign $campaign,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Event[]
|
||||
*/
|
||||
public function getEntities(array $args = []): array
|
||||
{
|
||||
Assert::assertSame(
|
||||
[
|
||||
'ignore_paginator' => true,
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'e.id',
|
||||
'value' => 123,
|
||||
'expr' => 'eq',
|
||||
],
|
||||
[
|
||||
'column' => 'e.campaign',
|
||||
'value' => $this->campaign,
|
||||
'expr' => 'eq',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
$args
|
||||
);
|
||||
|
||||
return []; // No entity found.
|
||||
}
|
||||
};
|
||||
|
||||
$eventExecutioner = new class extends EventExecutioner {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
$translator = new class extends Translator {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $parameters
|
||||
*/
|
||||
public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
|
||||
{
|
||||
Assert::assertSame('mautic.campaign.campaign.jump_to_event.target_not_exist', $id);
|
||||
|
||||
return $id;
|
||||
}
|
||||
};
|
||||
$leadRepository = new class extends LeadRepository {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$eventScheduler = new class extends EventScheduler {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$subscriber = new CampaignActionJumpToEventSubscriber(
|
||||
$eventRepository,
|
||||
$eventExecutioner,
|
||||
$translator,
|
||||
$leadRepository,
|
||||
$eventScheduler
|
||||
);
|
||||
|
||||
$event->setProperties(['jumpToEvent' => 123]);
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
$pendingEvent = new PendingEvent(new ActionAccessor([]), $event, new ArrayCollection([$leadLog->getId() => $leadLog]));
|
||||
|
||||
$subscriber->onJumpToEvent($pendingEvent);
|
||||
|
||||
Assert::assertCount(1, $pendingEvent->getSuccessful());
|
||||
Assert::assertCount(0, $pendingEvent->getFailures());
|
||||
|
||||
Assert::AssertSame(
|
||||
[
|
||||
'failed' => 1,
|
||||
'reason' => 'mautic.campaign.campaign.jump_to_event.target_not_exist',
|
||||
],
|
||||
$leadLog->getMetadata()
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnJumpToEventWhenEventExists(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$campaign = new class extends Campaign {
|
||||
public function getId(): int
|
||||
{
|
||||
return 111;
|
||||
}
|
||||
};
|
||||
$leadLog = new class extends LeadEventLog {
|
||||
public function getId(): int
|
||||
{
|
||||
return 456;
|
||||
}
|
||||
};
|
||||
$contact = new class extends Lead {
|
||||
public function getId(): int
|
||||
{
|
||||
return 789;
|
||||
}
|
||||
};
|
||||
$leadLog->setLead($contact);
|
||||
|
||||
$eventRepository = new class($campaign) extends EventRepository {
|
||||
public function __construct(
|
||||
private Campaign $campaign,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Event[]
|
||||
*/
|
||||
public function getEntities(array $args = []): array
|
||||
{
|
||||
Assert::assertSame(
|
||||
[
|
||||
'ignore_paginator' => true,
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'e.id',
|
||||
'value' => 123,
|
||||
'expr' => 'eq',
|
||||
],
|
||||
[
|
||||
'column' => 'e.campaign',
|
||||
'value' => $this->campaign,
|
||||
'expr' => 'eq',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
$args
|
||||
);
|
||||
|
||||
return [
|
||||
new class extends Event {
|
||||
public function getId()
|
||||
{
|
||||
return 222;
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
$eventExecutioner = new class extends EventExecutioner {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function executeForContacts(Event $event, ArrayCollection $contacts, ?Counter $counter = null, $isInactiveEvent = false): void
|
||||
{
|
||||
Assert::assertSame(222, $event->getId());
|
||||
Assert::assertCount(1, $contacts);
|
||||
Assert::assertSame(789, $contacts->first()->getId());
|
||||
}
|
||||
};
|
||||
$translator = new class extends Translator {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
$leadRepository = new class extends LeadRepository {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function incrementCampaignRotationForContacts(array $contactIds, $campaignId): void
|
||||
{
|
||||
Assert::assertSame([789], $contactIds);
|
||||
Assert::assertSame(111, $campaignId);
|
||||
}
|
||||
};
|
||||
|
||||
$eventScheduler = new class extends EventScheduler {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getExecutionDateTime(Event $event, ?\DateTimeInterface $compareFromDateTime = null, ?\DateTime $comparedToDateTime = null): \DateTimeInterface
|
||||
{
|
||||
return new \DateTime();
|
||||
}
|
||||
|
||||
public function shouldScheduleEvent(Event $event, \DateTimeInterface $executionDate, \DateTimeInterface $now): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
$subscriber = new CampaignActionJumpToEventSubscriber(
|
||||
$eventRepository,
|
||||
$eventExecutioner,
|
||||
$translator,
|
||||
$leadRepository,
|
||||
$eventScheduler
|
||||
);
|
||||
|
||||
$event->setProperties(['jumpToEvent' => 123]);
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
$pendingEvent = new PendingEvent(new ActionAccessor([]), $event, new ArrayCollection([$leadLog->getId() => $leadLog]));
|
||||
|
||||
$subscriber->onJumpToEvent($pendingEvent);
|
||||
|
||||
Assert::assertCount(1, $pendingEvent->getSuccessful());
|
||||
Assert::assertCount(0, $pendingEvent->getFailures());
|
||||
Assert::AssertSame([], $leadLog->getMetadata());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Twig\Extension;
|
||||
|
||||
use Mautic\CampaignBundle\Twig\Extension\CampaignEventIconExtension;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class CampaignEventIconExtensionTest extends TestCase
|
||||
{
|
||||
private CampaignEventIconExtension $extension;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->extension = new CampaignEventIconExtension();
|
||||
}
|
||||
|
||||
public function testGetCampaignEventIcon(): void
|
||||
{
|
||||
$eventTypes = [
|
||||
'lead.scorecontactscompanies' => 'ri-add-fill',
|
||||
'lead.addtocompany' => 'ri-add-fill',
|
||||
'lead.changepoints' => 'ri-edit-fill',
|
||||
'campaign.addremovelead' => 'ri-edit-fill',
|
||||
'stage.change' => 'ri-edit-fill',
|
||||
'lead.changelist' => 'ri-edit-fill',
|
||||
'lead.changetags' => 'ri-edit-fill',
|
||||
'lead.updatelead' => 'ri-edit-fill',
|
||||
'lead.updatecompany' => 'ri-edit-fill',
|
||||
'lead.changeowner' => 'ri-edit-fill',
|
||||
'lead.deletecontact' => 'ri-delete-bin-fill',
|
||||
'lead.adddnc' => 'ri-prohibited-fill',
|
||||
'lead.removednc' => 'ri-close-fill',
|
||||
'campaign.sendwebhook' => 'ri-webhook-fill',
|
||||
'email.send' => 'ri-mail-send-fill',
|
||||
'email.send.to.user' => 'ri-mail-send-fill',
|
||||
'message.send' => 'ri-send-plane-fill',
|
||||
'email.open' => 'ri-mail-open-fill',
|
||||
'email.click' => 'ri-cursor-fill',
|
||||
'email.reply' => 'ri-mail-unread-fill',
|
||||
'page.devicehit' => 'ri-device-fill',
|
||||
'asset.download' => 'ri-file-download-fill',
|
||||
'dwc.decision' => 'ri-download-cloud-2-fill',
|
||||
'form.submit' => 'ri-survey-fill',
|
||||
'page.pagehit' => 'ri-pages-fill',
|
||||
'lead.pageHit' => 'ri-pages-fill',
|
||||
'lead.device' => 'ri-device-fill',
|
||||
'lead.field_value' => 'ri-input-field',
|
||||
'lead.owner' => 'ri-user-2-fill',
|
||||
'lead.points' => 'ri-focus-2-fill',
|
||||
'lead.segments' => 'ri-pie-chart-fill',
|
||||
'lead.stages' => 'ri-filter-fill',
|
||||
'lead.tags' => 'ri-hashtag',
|
||||
'notification.has.active' => 'ri-notification-badge-fill',
|
||||
'email.validate.address' => 'ri-mail-check-fill',
|
||||
'lead.dnc' => 'ri-prohibited-fill',
|
||||
'sms.reply' => 'ri-message-3-fill',
|
||||
'campaign.jump_to_event' => 'ri-skip-forward-fill',
|
||||
'form.field_value' => 'ri-input-field',
|
||||
'focus.show' => 'ri-slideshow-4-fill',
|
||||
'unknown.event.type' => 'ri-shapes-fill',
|
||||
];
|
||||
|
||||
foreach ($eventTypes as $eventType => $expectedIcon) {
|
||||
$this->assertSame($expectedIcon, $this->extension->getCampaignEventIcon($eventType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\CampaignBundle\Event\NotifyOfUnpublishEvent;
|
||||
use Mautic\CoreBundle\Entity\Notification;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class CampaignEventSubscriberFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testCampaignUnPublishSendsOneUserNotification(): void
|
||||
{
|
||||
// Create 150 contacts
|
||||
$contacts = $this->createContacts(150);
|
||||
|
||||
// Create email
|
||||
$email = $this->createEmail();
|
||||
$campaign = $this->createCampaign($email->getId(), $contacts);
|
||||
$this->em->clear();
|
||||
|
||||
// Schedule the first campaign event.
|
||||
$commandTester = $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId()]);
|
||||
$output = $commandTester->getDisplay();
|
||||
self::assertStringContainsString('150 total events were executed', $output);
|
||||
|
||||
// Force the campaign failure events manually
|
||||
// Reload campaign to get the event
|
||||
$campaign = $this->em->find(Campaign::class, $campaign->getId());
|
||||
$events = $campaign->getEvents();
|
||||
$failedEvent = $events->first();
|
||||
|
||||
/** @var NotifyOfUnpublishEvent $unpublishEvent */
|
||||
$unpublishEvent = new NotifyOfUnpublishEvent($failedEvent);
|
||||
/** @var EventDispatcherInterface $dispatcher */
|
||||
$dispatcher = static::getContainer()->get('event_dispatcher');
|
||||
$dispatcher->dispatch($unpublishEvent, CampaignEvents::ON_CAMPAIGN_UNPUBLISH_NOTIFY);
|
||||
|
||||
// Check for notifications - use a more general query
|
||||
$notifications = $this->em->getRepository(Notification::class)
|
||||
->findBy(
|
||||
[
|
||||
'type' => 'error',
|
||||
'message' => "{$campaign->getName()} / Send email to user",
|
||||
]
|
||||
);
|
||||
self::assertCount(1, $notifications);
|
||||
|
||||
// Let's try dispatching the event again and verify that we have a second notification
|
||||
// (verifying that notifications aren't deduplicated)
|
||||
$dispatcher->dispatch($unpublishEvent, CampaignEvents::ON_CAMPAIGN_UNPUBLISH_NOTIFY);
|
||||
|
||||
// Query for all notifications
|
||||
$notifications = $this->em->getRepository(Notification::class)
|
||||
->findBy([
|
||||
'type' => 'error',
|
||||
'message' => "{$campaign->getName()} / Send email to user",
|
||||
]);
|
||||
|
||||
// There should be exactly two notifications created (no deduplication)
|
||||
self::assertCount(2, $notifications);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function createContacts(int $numberOfContacts): array
|
||||
{
|
||||
$contacts = [];
|
||||
|
||||
for ($i = 1; $i <= $numberOfContacts; ++$i) {
|
||||
$contacts[] = [
|
||||
'firstname' => 'John'.$i,
|
||||
'email' => 'john@email.'.$i.'.com',
|
||||
];
|
||||
}
|
||||
|
||||
$this->client->request('POST', '/api/contacts/batch/new', $contacts);
|
||||
$response = json_decode($this->client->getResponse()->getContent(), true);
|
||||
$contacts = $response['contacts'];
|
||||
self::assertCount(150, $contacts);
|
||||
self::assertSame(
|
||||
201,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function createEmail(): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setIsPublished(false);
|
||||
$email->setDateAdded(new \DateTime());
|
||||
$email->setName('Email name');
|
||||
$email->setSubject('Email subject');
|
||||
$email->setTemplate('Blank');
|
||||
$email->setCustomHtml('Hello there!');
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $contacts
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function createCampaign(int $emailId, array $contacts): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Campaign name');
|
||||
$campaign->setIsPublished(true);
|
||||
|
||||
$event = new Event();
|
||||
$event->setCampaign($campaign);
|
||||
$event->setName('Send email to user');
|
||||
$event->setType('email.send.to.user');
|
||||
$event->setEventType('action');
|
||||
$event->setProperties(
|
||||
[
|
||||
'to_owner' => '0',
|
||||
'to' => '',
|
||||
'cc' => '',
|
||||
'bcc' => '',
|
||||
'useremail' => ['email' => $emailId],
|
||||
]
|
||||
);
|
||||
$event->setTriggerInterval(1);
|
||||
$event->setTriggerIntervalUnit('d');
|
||||
$event->setTriggerMode('immediate');
|
||||
$event->setChannel('email');
|
||||
$campaign->addEvent(0, $event);
|
||||
|
||||
foreach ($contacts as $key => $contact) {
|
||||
$campaignLead = new CampaignLead();
|
||||
$campaignLead->setCampaign($campaign);
|
||||
$campaignLead->setLead($this->em->find(Lead::class, $contact['id']));
|
||||
$campaignLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($campaignLead);
|
||||
$campaign->addLead($key, $campaignLead);
|
||||
}
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventListener;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\EventRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Event\CampaignEvent;
|
||||
use Mautic\CampaignBundle\Event\ExecutedEvent;
|
||||
use Mautic\CampaignBundle\Event\FailedEvent;
|
||||
use Mautic\CampaignBundle\Event\NotifyOfFailureEvent;
|
||||
use Mautic\CampaignBundle\Event\NotifyOfUnpublishEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor;
|
||||
use Mautic\CampaignBundle\EventListener\CampaignEventSubscriber;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class CampaignEventSubscriberTest extends TestCase
|
||||
{
|
||||
private CampaignEventSubscriber $fixture;
|
||||
|
||||
private EventRepository|MockObject $eventRepo;
|
||||
|
||||
private MockObject|CampaignModel $campaignModelMock;
|
||||
|
||||
private MockObject|LeadEventLogRepository $leadEventLogRepositoryMock;
|
||||
|
||||
private MockObject|EventDispatcherInterface $eventDispatcherMock;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->eventRepo = $this->createMock(EventRepository::class);
|
||||
$this->campaignModelMock = $this->createMock(CampaignModel::class);
|
||||
$this->leadEventLogRepositoryMock = $this->createMock(LeadEventLogRepository::class);
|
||||
$this->eventDispatcherMock = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->fixture = new CampaignEventSubscriber(
|
||||
$this->eventRepo,
|
||||
$this->campaignModelMock,
|
||||
$this->leadEventLogRepositoryMock,
|
||||
$this->eventDispatcherMock
|
||||
);
|
||||
}
|
||||
|
||||
public function testEventFailedCountsGetResetOnCampaignPublish(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
// Ensure the campaign is unpublished
|
||||
$campaign->setIsPublished(false);
|
||||
// Go from unpublished to published.
|
||||
$campaign->setIsPublished(true);
|
||||
|
||||
$this->eventRepo->expects($this->once())
|
||||
->method('resetFailedCountsForEventsInCampaign')
|
||||
->with($campaign);
|
||||
|
||||
$this->fixture->onCampaignPreSave(new CampaignEvent($campaign));
|
||||
}
|
||||
|
||||
public function testEventFailedCountsDoesNotGetResetOnCampaignUnPublish(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
// Ensure the campaign is published
|
||||
$campaign->setIsPublished(true);
|
||||
// Go from published to unpublished.
|
||||
$campaign->setIsPublished(false);
|
||||
|
||||
$this->eventRepo->expects($this->never())
|
||||
->method('resetFailedCountsForEventsInCampaign');
|
||||
|
||||
$this->fixture->onCampaignPreSave(new CampaignEvent($campaign));
|
||||
}
|
||||
|
||||
public function testEventFailedCountsDoesNotGetResetWhenPublishedStateIsNotChanged(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
|
||||
$this->eventRepo->expects($this->never())
|
||||
->method('resetFailedCountsForEventsInCampaign');
|
||||
|
||||
$this->fixture->onCampaignPreSave(new CampaignEvent($campaign));
|
||||
}
|
||||
|
||||
public function testFailedEventGeneratesANotification(): void
|
||||
{
|
||||
$this->leadEventLogRepositoryMock->expects($this->once())
|
||||
->method('isLastFailed')
|
||||
->with(42, 42)
|
||||
->willReturn(false);
|
||||
|
||||
$mockLead = $this->createMock(Lead::class);
|
||||
$mockLead->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(42);
|
||||
$mockCampaign = $this->createMock(Campaign::class);
|
||||
$mockCampaign->expects($this->once())
|
||||
->method('getLeads')
|
||||
->willReturn(new ArrayCollection(range(0, 99)));
|
||||
|
||||
$mockEvent = $this->createMock(Event::class);
|
||||
$mockEvent->expects($this->once())
|
||||
->method('getCampaign')
|
||||
->willReturn($mockCampaign);
|
||||
$mockEvent->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
$mockEventLog = $this->createMock(LeadEventLog::class);
|
||||
$mockEventLog->expects($this->once())
|
||||
->method('getEvent')
|
||||
->willReturn($mockEvent);
|
||||
|
||||
$mockEventLog->expects($this->any())
|
||||
->method('getLead')
|
||||
->willReturn($mockLead);
|
||||
|
||||
$this->eventRepo->expects($this->once())
|
||||
->method('getFailedCountLeadEvent')
|
||||
->withAnyParameters()
|
||||
->willReturn(105);
|
||||
|
||||
// Set failed count to 5% of getLeads()->count()
|
||||
$this->eventRepo->expects($this->once())
|
||||
->method('incrementFailedCount')
|
||||
->with($mockEvent)
|
||||
->willReturn(5);
|
||||
|
||||
$this->eventDispatcherMock->expects($this->once())
|
||||
->method('hasListeners')
|
||||
->with(CampaignEvents::ON_CAMPAIGN_FAILURE_NOTIFY)
|
||||
->willReturn(true);
|
||||
|
||||
$this->eventDispatcherMock->expects($this->once())
|
||||
->method('dispatch')
|
||||
->willReturn(new NotifyOfFailureEvent($mockLead, $mockEvent));
|
||||
|
||||
$failedEvent = new FailedEvent($this->createMock(AbstractEventAccessor::class), $mockEventLog);
|
||||
|
||||
$this->fixture->onEventFailed($failedEvent);
|
||||
}
|
||||
|
||||
public function testFailedCountOverDisableCampaignThresholdDisablesTheCampaign(): void
|
||||
{
|
||||
$this->leadEventLogRepositoryMock->expects($this->once())
|
||||
->method('isLastFailed')
|
||||
->with(42, 42)
|
||||
->willReturn(false);
|
||||
|
||||
$mockLead = $this->createMock(Lead::class);
|
||||
$mockLead->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(42);
|
||||
$mockCampaign = $this->createMock(Campaign::class);
|
||||
$mockCampaign->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(true);
|
||||
|
||||
$mockCampaign->expects($this->once())
|
||||
->method('getLeads')
|
||||
->willReturn(new ArrayCollection(range(0, 99)));
|
||||
|
||||
$mockEvent = $this->createMock(Event::class);
|
||||
$mockEvent->expects($this->once())
|
||||
->method('getCampaign')
|
||||
->willReturn($mockCampaign);
|
||||
$mockEvent->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
$mockEventLog = $this->createMock(LeadEventLog::class);
|
||||
$mockEventLog->expects($this->once())
|
||||
->method('getEvent')
|
||||
->willReturn($mockEvent);
|
||||
|
||||
$mockEventLog->expects($this->any())
|
||||
->method('getLead')
|
||||
->willReturn($mockLead);
|
||||
|
||||
$this->eventRepo->expects($this->once())
|
||||
->method('getFailedCountLeadEvent')
|
||||
->withAnyParameters()
|
||||
->willReturn(200);
|
||||
|
||||
// Set failed count to 35% of getLeads()->count()
|
||||
$this->eventRepo->expects($this->once())
|
||||
->method('incrementFailedCount')
|
||||
->with($mockEvent)
|
||||
->willReturn(35);
|
||||
|
||||
$this->eventDispatcherMock->expects($this->exactly(2))
|
||||
->method('hasListeners')
|
||||
->willReturnMap([
|
||||
[CampaignEvents::ON_CAMPAIGN_FAILURE_NOTIFY, true],
|
||||
[CampaignEvents::ON_CAMPAIGN_UNPUBLISH_NOTIFY, true],
|
||||
]);
|
||||
|
||||
$this->eventDispatcherMock->expects($this->exactly(2))
|
||||
->method('dispatch')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
new NotifyOfFailureEvent($mockLead, $mockEvent),
|
||||
new NotifyOfUnpublishEvent($mockEvent)
|
||||
);
|
||||
|
||||
$this->campaignModelMock->expects($this->once())
|
||||
->method('transactionalCampaignUnPublish')
|
||||
->with($mockCampaign);
|
||||
|
||||
$failedEvent = new FailedEvent($this->createMock(AbstractEventAccessor::class), $mockEventLog);
|
||||
|
||||
$this->fixture->onEventFailed($failedEvent);
|
||||
}
|
||||
|
||||
public function testOnEventExecutedDecreaseTheCounter(): void
|
||||
{
|
||||
$mockEventLog = $this->createMock(LeadEventLog::class);
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setId(42);
|
||||
|
||||
$eventMock = $this->createMock(Event::class);
|
||||
$eventMock->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
$mockEventLog->expects($this->once())
|
||||
->method('getEvent')
|
||||
->willReturn($eventMock);
|
||||
|
||||
$mockEventLog->expects($this->any())
|
||||
->method('getLead')
|
||||
->willReturn($lead);
|
||||
|
||||
$this->leadEventLogRepositoryMock->expects($this->once())
|
||||
->method('isLastFailed')
|
||||
->with(42, 42)
|
||||
->willReturn(true);
|
||||
|
||||
$executedEvent = new ExecutedEvent($this->createMock(AbstractEventAccessor::class), $mockEventLog);
|
||||
|
||||
$this->eventRepo->expects($this->once())
|
||||
->method('getFailedCountLeadEvent')
|
||||
->withAnyParameters()
|
||||
->willReturn(101);
|
||||
|
||||
$this->eventRepo->expects($this->once())
|
||||
->method('decreaseFailedCount')
|
||||
->with($eventMock);
|
||||
|
||||
$this->fixture->onEventExecuted($executedEvent);
|
||||
}
|
||||
|
||||
public function testOnEventExecutedForDeletedContacts(): void
|
||||
{
|
||||
$mockEventLog = $this->createMock(LeadEventLog::class);
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->deletedId = 10;
|
||||
|
||||
$eventMock = $this->createMock(Event::class);
|
||||
$eventMock->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$mockEventLog->expects($this->once())
|
||||
->method('getEvent')
|
||||
->willReturn($eventMock);
|
||||
|
||||
$mockEventLog->expects($this->once())
|
||||
->method('getLead')
|
||||
->willReturn($lead);
|
||||
|
||||
$this->leadEventLogRepositoryMock->expects($this->once())
|
||||
->method('isLastFailed')
|
||||
->with($lead->deletedId, 1)
|
||||
->willReturn(true);
|
||||
|
||||
$executedEvent = new ExecutedEvent($this->createMock(AbstractEventAccessor::class), $mockEventLog);
|
||||
|
||||
$this->eventRepo->expects($this->once())
|
||||
->method('getFailedCountLeadEvent')
|
||||
->with($lead->deletedId, 1)
|
||||
->willReturn(101);
|
||||
|
||||
$this->eventRepo->expects($this->once())
|
||||
->method('decreaseFailedCount')
|
||||
->with($eventMock);
|
||||
|
||||
$this->fixture->onEventExecuted($executedEvent);
|
||||
}
|
||||
|
||||
public function testOnFailedEventGeneratesOneUnPublishNotificationAndEmail(): void
|
||||
{
|
||||
// Set up mocks
|
||||
$leadEventLogMock = $this->createMock(LeadEventLog::class);
|
||||
$eventMock = $this->createMock(Event::class);
|
||||
$leadEventLogMock->expects($this->once())->method('getEvent')->willReturn($eventMock);
|
||||
$leadMock = $this->createMock(Lead::class);
|
||||
$leadEventLogMock->expects($this->once())->method('getLead')->willReturn($leadMock);
|
||||
|
||||
// Set up campaign mock with isPublished returning false to simulate campaign already unpublished
|
||||
$campaignMock = $this->createMock(Campaign::class);
|
||||
$campaignMock->expects($this->once())->method('isPublished')->willReturn(false);
|
||||
$eventMock->expects($this->any())->method('getCampaign')->willReturn($campaignMock);
|
||||
|
||||
// Mock behavior for threshold calculations
|
||||
$leadMock->expects($this->any())->method('getId')->willReturn(1);
|
||||
$eventMock->expects($this->any())->method('getId')->willReturn(1);
|
||||
$this->eventRepo->expects($this->once())->method('getFailedCountLeadEvent')
|
||||
->with(1, 1)->willReturn(101);
|
||||
$this->leadEventLogRepositoryMock->expects($this->once())->method('isLastFailed')
|
||||
->with(1, 1)->willReturn(false);
|
||||
$this->eventRepo->expects($this->once())->method('incrementFailedCount')
|
||||
->with($eventMock)->willReturn(35);
|
||||
|
||||
// Set up leads collection
|
||||
$totalLeads = array_fill(0, 100, new Lead());
|
||||
$campaignMock->expects($this->once())->method('getLeads')->willReturn(new ArrayCollection($totalLeads));
|
||||
|
||||
// Expect failure notification to be dispatched
|
||||
$this->eventDispatcherMock->expects($this->once())
|
||||
->method('hasListeners')
|
||||
->with(CampaignEvents::ON_CAMPAIGN_FAILURE_NOTIFY)
|
||||
->willReturn(true);
|
||||
|
||||
$this->eventDispatcherMock->expects($this->once())
|
||||
->method('dispatch')
|
||||
->willReturn(new NotifyOfFailureEvent($leadMock, $eventMock));
|
||||
|
||||
// Unpublish notification should not be dispatched because campaign is already unpublished
|
||||
$this->campaignModelMock->expects($this->never())->method('transactionalCampaignUnPublish');
|
||||
|
||||
// Execute the test
|
||||
$failedEvent = new FailedEvent($this->createMock(AbstractEventAccessor::class), $leadEventLogMock);
|
||||
$this->fixture->onEventFailed($failedEvent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Event\CampaignEvent;
|
||||
use Mautic\CampaignBundle\EventListener\CampaignSubscriber;
|
||||
use Mautic\CampaignBundle\Service\CampaignAuditService;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CampaignSubscriberTest extends TestCase
|
||||
{
|
||||
private \PHPUnit\Framework\MockObject\MockObject $ipLookupHelper;
|
||||
|
||||
private \PHPUnit\Framework\MockObject\MockObject $auditLogModel;
|
||||
|
||||
private \PHPUnit\Framework\MockObject\MockObject $campaignAuditService;
|
||||
|
||||
private CampaignSubscriber $subscriber;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$this->auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$this->campaignAuditService = $this->createMock(CampaignAuditService::class);
|
||||
|
||||
$this->subscriber = new CampaignSubscriber(
|
||||
$this->ipLookupHelper,
|
||||
$this->auditLogModel,
|
||||
$this->campaignAuditService
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
self::assertEquals(
|
||||
[
|
||||
CampaignEvents::CAMPAIGN_POST_SAVE => ['onCampaignPostSave', 0],
|
||||
CampaignEvents::CAMPAIGN_POST_DELETE => ['onCampaignDelete', 0],
|
||||
],
|
||||
CampaignSubscriber::getSubscribedEvents()
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnCampaignPostSaveNothingHappened(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$event = new CampaignEvent($campaign);
|
||||
|
||||
$this->auditLogModel->expects($this->never())
|
||||
->method('writeToLog');
|
||||
|
||||
$this->subscriber->onCampaignPostSave($event);
|
||||
}
|
||||
|
||||
public function testOnCampaignPostSaveUnpublished(): void
|
||||
{
|
||||
$ipAddress = 'someIp';
|
||||
|
||||
$dateTime = new \DateTime();
|
||||
$dateTime->setTimestamp(1_597_752_193);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setPublishDown($dateTime);
|
||||
|
||||
$event = new CampaignEvent($campaign);
|
||||
|
||||
$this->ipLookupHelper->expects($this->once())
|
||||
->method('getIpAddressFromRequest')
|
||||
->willReturn($ipAddress);
|
||||
|
||||
$expectedLog = [
|
||||
'bundle' => 'campaign',
|
||||
'object' => 'campaign',
|
||||
'objectId' => $campaign->getId(),
|
||||
'action' => 'update',
|
||||
'details' => [
|
||||
'publishDown' => [
|
||||
0 => null,
|
||||
1 => '2020-08-18T12:03:13+00:00',
|
||||
],
|
||||
],
|
||||
'ipAddress' => $ipAddress,
|
||||
];
|
||||
|
||||
$this->auditLogModel->expects($this->once())
|
||||
->method('writeToLog')
|
||||
->with($expectedLog);
|
||||
|
||||
$this->subscriber->onCampaignPostSave($event);
|
||||
}
|
||||
|
||||
public function testOnCampaignPostSaveCreateFlash(): void
|
||||
{
|
||||
$ipAddress = 'someIp';
|
||||
$campaignName = 'campaignName';
|
||||
|
||||
$dateTime = new \DateTime();
|
||||
$dateTime->setTimestamp(1_597_752_193);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setPublishUp($dateTime);
|
||||
$campaign->setName($campaignName);
|
||||
|
||||
$event = new CampaignEvent($campaign);
|
||||
|
||||
$this->campaignAuditService->expects($this->once())
|
||||
->method('addWarningForUnpublishedEmails')
|
||||
->with($campaign);
|
||||
|
||||
$this->ipLookupHelper->expects($this->once())
|
||||
->method('getIpAddressFromRequest')
|
||||
->willReturn($ipAddress);
|
||||
|
||||
$expectedLog = [
|
||||
'action' => 'update',
|
||||
'bundle' => 'campaign',
|
||||
'details' => [
|
||||
'name' => [
|
||||
0 => null,
|
||||
1 => $campaignName,
|
||||
],
|
||||
'publishUp' => [
|
||||
0 => null,
|
||||
1 => '2020-08-18T12:03:13+00:00',
|
||||
],
|
||||
],
|
||||
'ipAddress' => $ipAddress,
|
||||
'object' => 'campaign',
|
||||
'objectId' => $campaign->getId(),
|
||||
];
|
||||
|
||||
$this->auditLogModel->expects($this->once())
|
||||
->method('writeToLog')
|
||||
->with($expectedLog);
|
||||
|
||||
$this->subscriber->onCampaignPostSave($event);
|
||||
}
|
||||
|
||||
public function testOnCampaignDelete(): void
|
||||
{
|
||||
$deletedId = 1;
|
||||
$campaignName = 'campaignName';
|
||||
$ipAddress = 'someIp';
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->deletedId = $deletedId;
|
||||
$campaign->setName($campaignName);
|
||||
|
||||
$event = new CampaignEvent($campaign);
|
||||
|
||||
$this->ipLookupHelper->expects($this->once())
|
||||
->method('getIpAddressFromRequest')
|
||||
->willReturn($ipAddress);
|
||||
|
||||
$expectedLog = [
|
||||
'bundle' => 'campaign',
|
||||
'object' => 'campaign',
|
||||
'objectId' => $deletedId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name' => $campaignName],
|
||||
'ipAddress' => $ipAddress,
|
||||
];
|
||||
|
||||
$this->auditLogModel->expects($this->once())
|
||||
->method('writeToLog')
|
||||
->with($expectedLog);
|
||||
|
||||
$this->subscriber->onCampaignDelete($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Event\NotifyOfFailureEvent;
|
||||
use Mautic\CampaignBundle\EventListener\NotifyOfFailureSubscriber;
|
||||
use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class NotifyOfFailureSubscriberTest extends TestCase
|
||||
{
|
||||
private MockObject&NotificationHelper $notificationHelper;
|
||||
|
||||
private NotifyOfFailureSubscriber $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->notificationHelper = $this->createMock(NotificationHelper::class);
|
||||
$this->subscriber = new NotifyOfFailureSubscriber($this->notificationHelper);
|
||||
}
|
||||
|
||||
public function testNotifyOfFailure(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$event = $this->createMock(Event::class);
|
||||
|
||||
$notifyEvent = new NotifyOfFailureEvent($lead, $event);
|
||||
|
||||
// Mock the notifyOfFailure method to expect the lead and event
|
||||
$this->notificationHelper->expects($this->once())
|
||||
->method('notifyOfFailure')
|
||||
->with(
|
||||
$this->equalTo($lead),
|
||||
$this->equalTo($event)
|
||||
);
|
||||
|
||||
$this->subscriber->notifyOfFailure($notifyEvent);
|
||||
}
|
||||
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
$events = NotifyOfFailureSubscriber::getSubscribedEvents();
|
||||
$this->assertArrayHasKey('mautic.campaign_failure_notify', $events);
|
||||
$this->assertEquals('notifyOfFailure', $events['mautic.campaign_failure_notify']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Event\NotifyOfUnpublishEvent;
|
||||
use Mautic\CampaignBundle\EventListener\NotifyOfUnpublishSubscriber;
|
||||
use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class NotifyOfUnpublishSubscriberTest extends TestCase
|
||||
{
|
||||
private MockObject&NotificationHelper $notificationHelper;
|
||||
private NotifyOfUnpublishSubscriber $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->notificationHelper = $this->createMock(NotificationHelper::class);
|
||||
$this->subscriber = new NotifyOfUnpublishSubscriber($this->notificationHelper);
|
||||
}
|
||||
|
||||
public function testNotifyOfUnpublish(): void
|
||||
{
|
||||
$event = $this->createMock(Event::class);
|
||||
|
||||
$notifyEvent = new NotifyOfUnpublishEvent($event);
|
||||
|
||||
// Mock the notifyOfUnpublish method to expect the event
|
||||
$this->notificationHelper->expects($this->once())
|
||||
->method('notifyOfUnpublish')
|
||||
->with(
|
||||
$this->equalTo($event)
|
||||
);
|
||||
|
||||
$this->subscriber->notifyOfUnpublish($notifyEvent);
|
||||
}
|
||||
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
$events = NotifyOfUnpublishSubscriber::getSubscribedEvents();
|
||||
$this->assertArrayHasKey('mautic.campaign_unpublish_notify', $events);
|
||||
$this->assertEquals('notifyOfUnpublish', $events['mautic.campaign_unpublish_notify']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\Tests\Functional\Fixtures\FixtureHelper;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\ReportBundle\Tests\Functional\AbstractReportSubscriberTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class ReportSubscriberFunctionalTest extends AbstractReportSubscriberTestCase
|
||||
{
|
||||
public function testCampaignLeadLogReportWithDncListColumn(): void
|
||||
{
|
||||
$leads[] = $this->createContact('test1@example.com');
|
||||
$leads[] = $this->createContact('test2@example.com');
|
||||
$leads[] = $this->createContact('test3@example.com');
|
||||
$this->em->flush();
|
||||
|
||||
$this->createDnc('email', $leads[0], DoNotContact::BOUNCED);
|
||||
$this->createDnc('email', $leads[1], DoNotContact::MANUAL);
|
||||
$this->createDnc('email', $leads[2], DoNotContact::UNSUBSCRIBED);
|
||||
$this->createDnc('sms', $leads[2], DoNotContact::MANUAL);
|
||||
$this->em->flush();
|
||||
|
||||
// execute campaign
|
||||
$fixtureHelper = new FixtureHelper($this->em);
|
||||
$campaign = $fixtureHelper->createCampaign('Scheduled event test');
|
||||
$fixtureHelper->addContactToCampaign($leads[0], $campaign);
|
||||
$fixtureHelper->addContactToCampaign($leads[1], $campaign);
|
||||
$fixtureHelper->addContactToCampaign($leads[2], $campaign);
|
||||
$fixtureHelper->createCampaignWithScheduledEvent($campaign);
|
||||
$this->em->flush();
|
||||
$commandResult = $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId()]);
|
||||
Assert::assertStringContainsString('3 total events were scheduled', $commandResult->getDisplay());
|
||||
|
||||
$report = $this->createReport(
|
||||
source: 'campaign_lead_event_log',
|
||||
columns: ['l.id', 'e.type', 'dnc_preferences'],
|
||||
filters: [
|
||||
[
|
||||
'column' => 'dnc_preferences',
|
||||
'glue' => 'and',
|
||||
'dynamic' => null,
|
||||
'condition' => 'in',
|
||||
'value' => [
|
||||
'email:'.DoNotContact::UNSUBSCRIBED,
|
||||
'email:'.DoNotContact::BOUNCED,
|
||||
],
|
||||
],
|
||||
],
|
||||
order: [['column' => 'l.id', 'direction' => 'ASC']]
|
||||
);
|
||||
|
||||
$expectedReport = [
|
||||
// id, event type, dnc_preferences
|
||||
[(string) $leads[0]->getId(), 'lead.changepoints', 'DNC Bounced: Email'],
|
||||
[(string) $leads[2]->getId(), 'lead.changepoints', 'DNC Manually Unsubscribed: Text Message, DNC Unsubscribed: Email'],
|
||||
];
|
||||
$this->verifyReport($report->getId(), $expectedReport);
|
||||
$this->verifyApiReport($report->getId(), $expectedReport);
|
||||
}
|
||||
|
||||
private function createContact(string $email): Lead
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail($email);
|
||||
$this->em->persist($contact);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
public function createDnc(string $channel, Lead $contact, int $reason): DoNotContact
|
||||
{
|
||||
$dnc = new DoNotContact();
|
||||
$dnc->setChannel($channel);
|
||||
$dnc->setLead($contact);
|
||||
$dnc->setReason($reason);
|
||||
$dnc->setDateAdded(new \DateTime());
|
||||
$this->em->persist($dnc);
|
||||
|
||||
return $dnc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\ContactFinder;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository as CampaignLeadRepository;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContactFinder;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class InactiveContactFinderTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|LeadRepository
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $leadRepository;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|CampaignLeadRepository
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $campaignLeadRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
$this->campaignLeadRepository = $this->createMock(CampaignLeadRepository::class);
|
||||
}
|
||||
|
||||
public function testNoContactsFoundExceptionIsThrown(): void
|
||||
{
|
||||
$this->campaignLeadRepository->expects($this->once())
|
||||
->method('getInactiveContacts')
|
||||
->willReturn([]);
|
||||
|
||||
$this->expectException(NoContactsFoundException::class);
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
$this->getContactFinder()->getContacts(1, new Event(), $limiter);
|
||||
}
|
||||
|
||||
public function testNoContactsFoundExceptionIsThrownIfEntitiesAreNotFound(): void
|
||||
{
|
||||
$contactMemberDates = [
|
||||
1 => new \DateTime(),
|
||||
];
|
||||
|
||||
$this->campaignLeadRepository->expects($this->once())
|
||||
->method('getInactiveContacts')
|
||||
->willReturn($contactMemberDates);
|
||||
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('getContactCollection')
|
||||
->willReturn(new ArrayCollection([]));
|
||||
|
||||
$this->expectException(NoContactsFoundException::class);
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
$this->getContactFinder()->getContacts(1, new Event(), $limiter);
|
||||
}
|
||||
|
||||
public function testContactsAreFoundAndStoredInCampaignMemberDatesAdded(): void
|
||||
{
|
||||
$contactMemberDates = [
|
||||
1 => new \DateTime(),
|
||||
];
|
||||
|
||||
$this->campaignLeadRepository->expects($this->once())
|
||||
->method('getInactiveContacts')
|
||||
->willReturn($contactMemberDates);
|
||||
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('getContactCollection')
|
||||
->willReturn(new ArrayCollection([new Lead()]));
|
||||
|
||||
$contactFinder = $this->getContactFinder();
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
$contacts = $contactFinder->getContacts(1, new Event(), $limiter);
|
||||
$this->assertCount(1, $contacts);
|
||||
|
||||
$this->assertEquals($contactMemberDates, $contactFinder->getDatesAdded());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InactiveContactFinder
|
||||
*/
|
||||
private function getContactFinder()
|
||||
{
|
||||
return new InactiveContactFinder(
|
||||
$this->leadRepository,
|
||||
$this->campaignLeadRepository,
|
||||
new NullLogger()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\ContactFinder;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\CampaignRepository;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContactFinder;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class KickoffContactFinderTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|LeadRepository
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $leadRepository;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|CampaignRepository
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $campaignRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
|
||||
$this->campaignRepository = $this->createMock(CampaignRepository::class);
|
||||
}
|
||||
|
||||
public function testNoContactsFoundExceptionIsThrown(): void
|
||||
{
|
||||
$this->campaignRepository->expects($this->once())
|
||||
->method('getPendingContactIds')
|
||||
->willReturn([]);
|
||||
|
||||
$this->expectException(NoContactsFoundException::class);
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
$this->getContactFinder()->getContacts(1, $limiter);
|
||||
}
|
||||
|
||||
public function testNoContactsFoundExceptionIsThrownIfEntitiesAreNotFound(): void
|
||||
{
|
||||
$contactIds = [1, 2];
|
||||
|
||||
$this->campaignRepository->expects($this->once())
|
||||
->method('getPendingContactIds')
|
||||
->willReturn($contactIds);
|
||||
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('getContactCollection')
|
||||
->willReturn(new ArrayCollection([]));
|
||||
|
||||
$this->expectException(NoContactsFoundException::class);
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
$this->getContactFinder()->getContacts(1, $limiter);
|
||||
}
|
||||
|
||||
public function testArrayCollectionIsReturnedForFoundContacts(): void
|
||||
{
|
||||
$contactIds = [1, 2];
|
||||
|
||||
$this->campaignRepository->expects($this->once())
|
||||
->method('getPendingContactIds')
|
||||
->willReturn($contactIds);
|
||||
|
||||
$foundContacts = new ArrayCollection([new Lead(), new Lead()]);
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('getContactCollection')
|
||||
->willReturn($foundContacts);
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
$this->assertEquals($foundContacts, $this->getContactFinder()->getContacts(1, $limiter));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return KickoffContactFinder
|
||||
*/
|
||||
private function getContactFinder()
|
||||
{
|
||||
return new KickoffContactFinder(
|
||||
$this->leadRepository,
|
||||
$this->campaignRepository,
|
||||
new NullLogger()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\ContactFinder\Limiter;
|
||||
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException;
|
||||
|
||||
class ContactLimiterTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testGetters(): void
|
||||
{
|
||||
$limiter = new ContactLimiter(1, 2, 3, 4, [1, 2, 3]);
|
||||
|
||||
$this->assertEquals(1, $limiter->getBatchLimit());
|
||||
$this->assertEquals(2, $limiter->getContactId());
|
||||
$this->assertEquals(3, $limiter->getMinContactId());
|
||||
$this->assertEquals(4, $limiter->getMaxContactId());
|
||||
$this->assertEquals([1, 2, 3], $limiter->getContactIdList());
|
||||
}
|
||||
|
||||
public function testBatchMinContactIsReturned(): void
|
||||
{
|
||||
$limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]);
|
||||
|
||||
$limiter->setBatchMinContactId(5);
|
||||
$this->assertEquals(5, $limiter->getMinContactId());
|
||||
}
|
||||
|
||||
public function testNoContactsFoundExceptionThrownIfIdIsLessThanMin(): void
|
||||
{
|
||||
$this->expectException(NoContactsFoundException::class);
|
||||
|
||||
$limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]);
|
||||
$limiter->setBatchMinContactId(1);
|
||||
}
|
||||
|
||||
public function testNoContactsFoundExceptionThrownIfIdIsMoreThanMax(): void
|
||||
{
|
||||
$this->expectException(NoContactsFoundException::class);
|
||||
|
||||
$limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]);
|
||||
$limiter->setBatchMinContactId(11);
|
||||
}
|
||||
|
||||
public function testNoContactsFoundExceptionThrownIfIdIsTheSameAsLastBatch(): void
|
||||
{
|
||||
$this->expectException(NoContactsFoundException::class);
|
||||
|
||||
$limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]);
|
||||
$limiter->setBatchMinContactId(5);
|
||||
$limiter->setBatchMinContactId(5);
|
||||
}
|
||||
|
||||
public function testExceptionNotThrownIfIdEqualsMinSoThatItsIsIncluded(): void
|
||||
{
|
||||
$limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]);
|
||||
$this->assertSame($limiter, $limiter->setBatchMinContactId(3));
|
||||
}
|
||||
|
||||
public function testExceptionNotThrownIfIdEqualsMaxSoThatItsIsIncluded(): void
|
||||
{
|
||||
$limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]);
|
||||
$this->assertSame($limiter, $limiter->setBatchMinContactId(10));
|
||||
}
|
||||
|
||||
public function testExceptionThrownIfThreadIdLargerThanMaxThreads(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
new ContactLimiter(1, null, null, null, [], 5, 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\ContactFinder;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\CampaignRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContactFinder;
|
||||
use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class ScheduledContactFinderTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|LeadRepository
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $leadRepository;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|CampaignRepository
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $campaignRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
|
||||
$this->campaignRepository = $this->createMock(CampaignRepository::class);
|
||||
}
|
||||
|
||||
public function testHydratedLeadsFromRepositoryAreFoundAndPushedIntoLogs(): void
|
||||
{
|
||||
$lead1 = $this->createMock(Lead::class);
|
||||
$lead1->expects($this->exactly(2))
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$lead2 = $this->createMock(Lead::class);
|
||||
$lead2->expects($this->exactly(2))
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$log1 = $this->createMock(LeadEventLog::class);
|
||||
$log1->expects($this->exactly(2))
|
||||
->method('getLead')
|
||||
->willReturn($lead1);
|
||||
$log1->expects($this->once())
|
||||
->method('setLead');
|
||||
|
||||
$log2 = $this->createMock(LeadEventLog::class);
|
||||
$log2->expects($this->exactly(2))
|
||||
->method('getLead')
|
||||
->willReturn($lead2);
|
||||
$log2->expects($this->once())
|
||||
->method('setLead');
|
||||
|
||||
$logs = new ArrayCollection(
|
||||
[
|
||||
1 => $log1,
|
||||
2 => $log2,
|
||||
]
|
||||
);
|
||||
|
||||
$contacs = new ArrayCollection(
|
||||
[
|
||||
1 => $lead1,
|
||||
2 => $lead2,
|
||||
]
|
||||
);
|
||||
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('getContactCollection')
|
||||
->willReturn($contacs);
|
||||
|
||||
$this->getContactFinder()->hydrateContacts($logs);
|
||||
}
|
||||
|
||||
public function testHydratedLeadsFromRepositoryWithMissingLeadResultsLogBeingRemoved(): void
|
||||
{
|
||||
$lead1 = $this->createMock(Lead::class);
|
||||
$lead1->expects($this->exactly(2))
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$lead2 = $this->createMock(Lead::class);
|
||||
$lead2->expects($this->exactly(2))
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$log1 = $this->createMock(LeadEventLog::class);
|
||||
$log1->expects($this->exactly(2))
|
||||
->method('getLead')
|
||||
->willReturn($lead1);
|
||||
$log1->expects($this->once())
|
||||
->method('setLead');
|
||||
|
||||
$log2 = $this->createMock(LeadEventLog::class);
|
||||
$log2->expects($this->exactly(2))
|
||||
->method('getLead')
|
||||
->willReturn($lead2);
|
||||
$log2->expects($this->never())
|
||||
->method('setLead');
|
||||
|
||||
$logs = new ArrayCollection(
|
||||
[
|
||||
1 => $log1,
|
||||
2 => $log2,
|
||||
]
|
||||
);
|
||||
|
||||
$contacs = new ArrayCollection(
|
||||
[
|
||||
1 => $lead1,
|
||||
]
|
||||
);
|
||||
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('getContactCollection')
|
||||
->willReturn($contacs);
|
||||
|
||||
$this->getContactFinder()->hydrateContacts($logs);
|
||||
|
||||
$this->assertCount(1, $logs);
|
||||
}
|
||||
|
||||
public function testNoContactsFoundExceptionIsThrownIfEntitiesAreNotFound(): void
|
||||
{
|
||||
$this->leadRepository->expects($this->never())
|
||||
->method('getContactCollection');
|
||||
|
||||
$this->expectException(NoContactsFoundException::class);
|
||||
|
||||
$this->getContactFinder()->hydrateContacts(new ArrayCollection());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ScheduledContactFinder
|
||||
*/
|
||||
private function getContactFinder()
|
||||
{
|
||||
return new ScheduledContactFinder(
|
||||
$this->leadRepository,
|
||||
new NullLogger()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\Dispatcher;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Event\ExecutedBatchEvent;
|
||||
use Mautic\CampaignBundle\Event\ExecutedEvent;
|
||||
use Mautic\CampaignBundle\Event\FailedEvent;
|
||||
use Mautic\CampaignBundle\Event\PendingEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
use Mautic\CampaignBundle\Executioner\Dispatcher\ActionDispatcher;
|
||||
use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException;
|
||||
use Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class ActionDispatcherTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|EventDispatcherInterface
|
||||
*/
|
||||
private MockObject $dispatcher;
|
||||
|
||||
/**
|
||||
* @var MockObject|EventScheduler
|
||||
*/
|
||||
private MockObject $scheduler;
|
||||
|
||||
/**
|
||||
* @var MockObject|LegacyEventDispatcher
|
||||
*/
|
||||
private MockObject $legacyDispatcher;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->scheduler = $this->createMock(EventScheduler::class);
|
||||
$this->legacyDispatcher = $this->createMock(LegacyEventDispatcher::class);
|
||||
}
|
||||
|
||||
public function testActionBatchEventIsDispatchedWithSuccessAndFailedLogs(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$lead1 = $this->createMock(Lead::class);
|
||||
$lead1->expects($this->exactly(2))
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$lead2 = $this->createMock(Lead::class);
|
||||
$lead2->expects($this->exactly(2))
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$log1 = $this->createMock(LeadEventLog::class);
|
||||
$log1->expects($this->exactly(2))
|
||||
->method('getLead')
|
||||
->willReturn($lead1);
|
||||
$log1->method('setIsScheduled')
|
||||
->willReturn($log1);
|
||||
$log1->method('getEvent')
|
||||
->willReturn($event);
|
||||
|
||||
$log2 = $this->createMock(LeadEventLog::class);
|
||||
$log2->expects($this->exactly(2))
|
||||
->method('getLead')
|
||||
->willReturn($lead2);
|
||||
$log2->method('getMetadata')
|
||||
->willReturn([]);
|
||||
$log2->method('getEvent')
|
||||
->willReturn($event);
|
||||
|
||||
$logs = new ArrayCollection(
|
||||
[
|
||||
1 => $log1,
|
||||
2 => $log2,
|
||||
]
|
||||
);
|
||||
|
||||
$config = $this->createMock(ActionAccessor::class);
|
||||
$config->expects($this->once())
|
||||
->method('getBatchEventName')
|
||||
->willReturn('something');
|
||||
|
||||
$dispatcCounter = 0;
|
||||
$matcher = $this->exactly(4);
|
||||
$this->dispatcher->expects($matcher)
|
||||
->method('dispatch')
|
||||
->willReturnCallback(
|
||||
function (\Symfony\Contracts\EventDispatcher\Event $event, string $eventName) use ($logs, &$dispatcCounter, $matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertTrue($event instanceof ExecutedEvent);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTED, $eventName);
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertTrue($event instanceof ExecutedBatchEvent);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTED_BATCH, $eventName);
|
||||
}
|
||||
if (4 === $matcher->numberOfInvocations()) {
|
||||
$this->assertTrue($event instanceof FailedEvent);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_FAILED, $eventName);
|
||||
}
|
||||
++$dispatcCounter;
|
||||
if (1 === $dispatcCounter) {
|
||||
Assert::assertInstanceOf(PendingEvent::class, $event);
|
||||
\assert($event instanceof PendingEvent);
|
||||
$event->pass($logs->get(1));
|
||||
$event->fail($logs->get(2), 'just because');
|
||||
} elseif (2 === $dispatcCounter) {
|
||||
self::assertInstanceOf(ExecutedEvent::class, $event);
|
||||
self::assertSame(CampaignEvents::ON_EVENT_EXECUTED, $eventName);
|
||||
} elseif (3 === $dispatcCounter) {
|
||||
self::assertInstanceOf(ExecutedBatchEvent::class, $event);
|
||||
self::assertSame(CampaignEvents::ON_EVENT_EXECUTED_BATCH, $eventName);
|
||||
} elseif (4 === $dispatcCounter) {
|
||||
self::assertInstanceOf(FailedEvent::class, $event);
|
||||
self::assertSame(CampaignEvents::ON_EVENT_FAILED, $eventName);
|
||||
} else {
|
||||
self::fail('Unknown event called.');
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
);
|
||||
|
||||
$this->scheduler->expects($this->once())
|
||||
->method('rescheduleFailures')
|
||||
->willReturnCallback(
|
||||
function (ArrayCollection $logs) use ($log2): void {
|
||||
if ($logs->count() > 1) {
|
||||
$this->fail('Only one log was supposed to fail');
|
||||
}
|
||||
|
||||
$this->assertEquals($log2, $logs->first());
|
||||
}
|
||||
);
|
||||
|
||||
$this->legacyDispatcher->expects($this->once())
|
||||
->method('dispatchExecutionEvents');
|
||||
|
||||
$this->getEventDispatcher()->dispatchEvent($config, $event, $logs);
|
||||
}
|
||||
|
||||
public function testActionLogNotProcessedExceptionIsThrownIfLogNotProcessedWithSuccess(): void
|
||||
{
|
||||
$this->expectException(LogNotProcessedException::class);
|
||||
|
||||
$event = new Event();
|
||||
$lead1 = $this->createMock(Lead::class);
|
||||
$lead1->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$lead2 = $this->createMock(Lead::class);
|
||||
$lead2->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$log1 = $this->createMock(LeadEventLog::class);
|
||||
$log1->expects($this->once())
|
||||
->method('getLead')
|
||||
->willReturn($lead1);
|
||||
$log1->method('setIsScheduled')
|
||||
->willReturn($log1);
|
||||
$log1->method('getEvent')
|
||||
->willReturn($event);
|
||||
|
||||
$log2 = $this->createMock(LeadEventLog::class);
|
||||
$log2->expects($this->once())
|
||||
->method('getLead')
|
||||
->willReturn($lead2);
|
||||
$log2->method('getMetadata')
|
||||
->willReturn([]);
|
||||
$log2->method('getEvent')
|
||||
->willReturn($event);
|
||||
|
||||
$logs = new ArrayCollection(
|
||||
[
|
||||
1 => $log1,
|
||||
2 => $log2,
|
||||
]
|
||||
);
|
||||
|
||||
$config = $this->createMock(ActionAccessor::class);
|
||||
|
||||
$config->expects($this->once())
|
||||
->method('getBatchEventName')
|
||||
->willReturn('something');
|
||||
|
||||
$this->dispatcher->expects($this->once())
|
||||
->method('dispatch')
|
||||
->willReturnCallback(
|
||||
function (PendingEvent $pendingEvent, string $eventName) use ($logs) {
|
||||
$pendingEvent->pass($logs->get(1));
|
||||
|
||||
return $pendingEvent;
|
||||
// One log is not processed so the exception should be thrown
|
||||
}
|
||||
);
|
||||
|
||||
$this->getEventDispatcher()->dispatchEvent($config, $event, $logs);
|
||||
}
|
||||
|
||||
public function testActionLogNotProcessedExceptionIsThrownIfLogNotProcessedWithFailed(): void
|
||||
{
|
||||
$this->expectException(LogNotProcessedException::class);
|
||||
|
||||
$event = new Event();
|
||||
|
||||
$lead1 = $this->createMock(Lead::class);
|
||||
$lead1->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$lead2 = $this->createMock(Lead::class);
|
||||
$lead2->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$log1 = $this->createMock(LeadEventLog::class);
|
||||
$log1->expects($this->once())
|
||||
->method('getLead')
|
||||
->willReturn($lead1);
|
||||
$log1->method('setIsScheduled')
|
||||
->willReturn($log1);
|
||||
$log1->method('getEvent')
|
||||
->willReturn($event);
|
||||
|
||||
$log2 = $this->createMock(LeadEventLog::class);
|
||||
$log2->expects($this->once())
|
||||
->method('getLead')
|
||||
->willReturn($lead2);
|
||||
$log2->method('getMetadata')
|
||||
->willReturn([]);
|
||||
$log2->method('getEvent')
|
||||
->willReturn($event);
|
||||
|
||||
$logs = new ArrayCollection(
|
||||
[
|
||||
1 => $log1,
|
||||
2 => $log2,
|
||||
]
|
||||
);
|
||||
|
||||
$config = $this->createMock(ActionAccessor::class);
|
||||
|
||||
$config->expects($this->once())
|
||||
->method('getBatchEventName')
|
||||
->willReturn('something');
|
||||
|
||||
$this->dispatcher->expects($this->once())
|
||||
->method('dispatch')
|
||||
->willReturnCallback(
|
||||
function (PendingEvent $pendingEvent, string $eventName) use ($logs) {
|
||||
$pendingEvent->fail($logs->get(2), 'something');
|
||||
|
||||
return $pendingEvent;
|
||||
// One log is not processed so the exception should be thrown
|
||||
}
|
||||
);
|
||||
|
||||
$this->getEventDispatcher()->dispatchEvent($config, $event, $logs);
|
||||
}
|
||||
|
||||
public function testActionBatchEventIsIgnoredWithLegacy(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$config = $this->createMock(ActionAccessor::class);
|
||||
|
||||
$config->expects($this->once())
|
||||
->method('getBatchEventName')
|
||||
->willReturn(null);
|
||||
|
||||
$this->dispatcher->expects($this->never())
|
||||
->method('dispatch');
|
||||
|
||||
$this->legacyDispatcher->expects($this->once())
|
||||
->method('dispatchCustomEvent');
|
||||
|
||||
$this->getEventDispatcher()->dispatchEvent($config, $event, new ArrayCollection());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ActionDispatcher
|
||||
*/
|
||||
private function getEventDispatcher()
|
||||
{
|
||||
return new ActionDispatcher(
|
||||
$this->dispatcher,
|
||||
new NullLogger(),
|
||||
$this->scheduler,
|
||||
$this->legacyDispatcher
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\Dispatcher;
|
||||
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Event\ConditionEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor;
|
||||
use Mautic\CampaignBundle\Executioner\Dispatcher\ConditionDispatcher;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class ConditionDispatcherTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|EventDispatcherInterface
|
||||
*/
|
||||
private MockObject $dispatcher;
|
||||
|
||||
/**
|
||||
* @var MockObject|ConditionAccessor
|
||||
*/
|
||||
private MockObject $config;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->config = $this->createMock(ConditionAccessor::class);
|
||||
}
|
||||
|
||||
public function testConditionEventIsDispatched(): void
|
||||
{
|
||||
$this->config->expects($this->once())
|
||||
->method('getEventName')
|
||||
->willReturn('something');
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->dispatcher->expects($matcher)
|
||||
->method('dispatch')->willReturnCallback(function (object $event, string $eventName) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(ConditionEvent::class, $event);
|
||||
$this->assertSame('something', $eventName);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(ConditionEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_CONDITION_EVALUATION, $eventName);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
(new ConditionDispatcher($this->dispatcher))->dispatchEvent($this->config, new LeadEventLog());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\Dispatcher;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Event\DecisionEvent;
|
||||
use Mautic\CampaignBundle\Event\DecisionResultsEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor;
|
||||
use Mautic\CampaignBundle\Executioner\Dispatcher\DecisionDispatcher;
|
||||
use Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher;
|
||||
use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class DecisionDispatcherTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|EventDispatcherInterface
|
||||
*/
|
||||
private MockObject $dispatcher;
|
||||
|
||||
/**
|
||||
* @var MockObject|LegacyEventDispatcher
|
||||
*/
|
||||
private MockObject $legacyDispatcher;
|
||||
|
||||
/**
|
||||
* @var MockObject|DecisionAccessor
|
||||
*/
|
||||
private MockObject $config;
|
||||
|
||||
private DecisionDispatcher $decisionDispatcher;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->legacyDispatcher = $this->createMock(LegacyEventDispatcher::class);
|
||||
$this->config = $this->createMock(DecisionAccessor::class);
|
||||
$this->decisionDispatcher = new DecisionDispatcher($this->dispatcher, $this->legacyDispatcher);
|
||||
}
|
||||
|
||||
public function testDecisionEventIsDispatched(): void
|
||||
{
|
||||
$this->config->expects($this->once())
|
||||
->method('getEventName')
|
||||
->willReturn('something');
|
||||
|
||||
$this->legacyDispatcher->expects($this->never())
|
||||
->method('dispatchDecisionEvent');
|
||||
|
||||
$this->dispatcher->expects($this->once())
|
||||
->method('dispatch')
|
||||
->with($this->isInstanceOf(DecisionEvent::class), 'something');
|
||||
|
||||
$this->decisionDispatcher->dispatchRealTimeEvent($this->config, new LeadEventLog(), null);
|
||||
}
|
||||
|
||||
public function testDecisionEvaluationEventIsDispatched(): void
|
||||
{
|
||||
$this->config->expects($this->never())
|
||||
->method('getEventName');
|
||||
|
||||
$this->legacyDispatcher->expects($this->once())
|
||||
->method('dispatchDecisionEvent');
|
||||
|
||||
$this->dispatcher->expects($this->once())
|
||||
->method('dispatch')
|
||||
->with($this->isInstanceOf(DecisionEvent::class), CampaignEvents::ON_EVENT_DECISION_EVALUATION);
|
||||
|
||||
$this->decisionDispatcher->dispatchEvaluationEvent($this->config, new LeadEventLog());
|
||||
}
|
||||
|
||||
public function testDecisionResultsEventIsDispatched(): void
|
||||
{
|
||||
$this->dispatcher->expects($this->once())
|
||||
->method('dispatch')
|
||||
->with($this->isInstanceOf(DecisionResultsEvent::class), CampaignEvents::ON_EVENT_DECISION_EVALUATION_RESULTS);
|
||||
|
||||
$this->decisionDispatcher->dispatchDecisionResultsEvent($this->config, new ArrayCollection([new LeadEventLog()]), new EvaluatedContacts());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\Dispatcher;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
|
||||
use Mautic\CampaignBundle\Event\ExecutedBatchEvent;
|
||||
use Mautic\CampaignBundle\Event\ExecutedEvent;
|
||||
use Mautic\CampaignBundle\Event\FailedEvent;
|
||||
use Mautic\CampaignBundle\Event\PendingEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor;
|
||||
use Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class LegacyEventDispatcherTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|EventDispatcherInterface
|
||||
*/
|
||||
private MockObject $dispatcher;
|
||||
|
||||
/**
|
||||
* @var MockObject|EventScheduler
|
||||
*/
|
||||
private MockObject $scheduler;
|
||||
|
||||
/**
|
||||
* @var MockObject|ContactTracker
|
||||
*/
|
||||
private MockObject $contactTracker;
|
||||
|
||||
/**
|
||||
* @var MockObject|AbstractEventAccessor
|
||||
*/
|
||||
private MockObject $config;
|
||||
|
||||
/**
|
||||
* @var MockObject|PendingEvent
|
||||
*/
|
||||
private MockObject $pendingEvent;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->scheduler = $this->createMock(EventScheduler::class);
|
||||
$this->contactTracker = $this->createMock(ContactTracker::class);
|
||||
$this->config = $this->createMock(AbstractEventAccessor::class);
|
||||
$this->pendingEvent = $this->createMock(PendingEvent::class);
|
||||
}
|
||||
|
||||
public function testAllEventsAreFailedWithBadConfig(): void
|
||||
{
|
||||
$this->config->expects($this->once())
|
||||
->method('getConfig')
|
||||
->willReturn([]);
|
||||
|
||||
$logs = new ArrayCollection([new LeadEventLog()]);
|
||||
|
||||
$this->pendingEvent->expects($this->once())
|
||||
->method('failAll');
|
||||
|
||||
$this->getLegacyEventDispatcher()->dispatchCustomEvent($this->config, $logs, false, $this->pendingEvent);
|
||||
}
|
||||
|
||||
public function testPrimayLegacyEventsAreProcessed(): void
|
||||
{
|
||||
$this->config->expects($this->exactly(2))
|
||||
->method('getConfig')
|
||||
->willReturn(['eventName' => 'something']);
|
||||
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setLead(new Lead());
|
||||
$logs = new ArrayCollection([$leadEventLog]);
|
||||
|
||||
// BC default is to have pass
|
||||
$this->pendingEvent->expects($this->once())
|
||||
->method('pass');
|
||||
|
||||
$this->contactTracker->expects($this->exactly(2))
|
||||
->method('setSystemContact');
|
||||
$matcher = $this->exactly(4);
|
||||
|
||||
$this->dispatcher->expects($matcher)
|
||||
->method('dispatch')->willReturnCallback(function (object $event, string $eventName) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame('something', $eventName);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTION, $eventName); // @phpstan-ignore-line classConstant.deprecated
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(ExecutedEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTED, $eventName);
|
||||
}
|
||||
if (4 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(ExecutedBatchEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTED_BATCH, $eventName);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$this->getLegacyEventDispatcher()->dispatchCustomEvent($this->config, $logs, false, $this->pendingEvent);
|
||||
}
|
||||
|
||||
public function testPrimaryCallbackIsProcessed(): void
|
||||
{
|
||||
$this->config->expects($this->exactly(2))
|
||||
->method('getConfig')
|
||||
->willReturn(['callback' => [self::class, 'bogusCallback']]);
|
||||
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setLead(new Lead());
|
||||
$logs = new ArrayCollection([$leadEventLog]);
|
||||
|
||||
// BC default is to have pass
|
||||
$this->pendingEvent->expects($this->once())
|
||||
->method('pass');
|
||||
|
||||
$this->contactTracker->expects($this->exactly(2))
|
||||
->method('setSystemContact');
|
||||
// Legacy execution event should dispatch
|
||||
$matcher = $this->exactly(3);
|
||||
|
||||
// Legacy execution event should dispatch
|
||||
$this->dispatcher->expects($matcher)
|
||||
->method('dispatch')->willReturnCallback(function (object $event, string $eventName) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTION, $eventName); // @phpstan-ignore-line classConstant.deprecated
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(ExecutedEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTED, $eventName);
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(ExecutedBatchEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTED_BATCH, $eventName);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$this->getLegacyEventDispatcher()->dispatchCustomEvent($this->config, $logs, false, $this->pendingEvent);
|
||||
}
|
||||
|
||||
public function testArrayResultAppendedToMetadata(): void
|
||||
{
|
||||
$this->config->expects($this->exactly(2))
|
||||
->method('getConfig')
|
||||
->willReturn(['eventName' => 'something']);
|
||||
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setLead(new Lead());
|
||||
$leadEventLog->setMetadata(['bar' => 'foo']);
|
||||
|
||||
$logs = new ArrayCollection([$leadEventLog]);
|
||||
|
||||
// BC default is to have pass
|
||||
$this->pendingEvent->expects($this->once())
|
||||
->method('pass');
|
||||
|
||||
$this->contactTracker->expects($this->exactly(2))
|
||||
->method('setSystemContact');
|
||||
// Legacy custom event should dispatch
|
||||
$matcher = $this->exactly(4);
|
||||
|
||||
// Legacy custom event should dispatch
|
||||
$this->dispatcher->expects($matcher)
|
||||
->method('dispatch')->willReturnCallback(function (object $event, string $eventName) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame('something', $eventName);
|
||||
$event->setResult(['foo' => 'bar']);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTION, $eventName); // @phpstan-ignore-line classConstant.deprecated
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(ExecutedEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTED, $eventName);
|
||||
}
|
||||
if (4 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(ExecutedBatchEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTED_BATCH, $eventName);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$this->getLegacyEventDispatcher()->dispatchCustomEvent($this->config, $logs, false, $this->pendingEvent);
|
||||
|
||||
$this->assertEquals(['bar' => 'foo', 'foo' => 'bar'], $leadEventLog->getMetadata());
|
||||
}
|
||||
|
||||
public function testFailedResultAsFalseIsProcessed(): void
|
||||
{
|
||||
$this->config->expects($this->exactly(2))
|
||||
->method('getConfig')
|
||||
->willReturn(['eventName' => 'something']);
|
||||
|
||||
$lead = new Lead();
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setLead($lead);
|
||||
$leadEventLog->setMetadata(['bar' => 'foo']);
|
||||
|
||||
$logs = new ArrayCollection([$leadEventLog]);
|
||||
|
||||
// Should fail because we're returning false
|
||||
$this->pendingEvent->expects($this->once())
|
||||
->method('fail');
|
||||
|
||||
$this->contactTracker->expects($this->exactly(2))
|
||||
->method('setSystemContact');
|
||||
// Legacy custom event should dispatch
|
||||
$matcher = $this->exactly(3);
|
||||
|
||||
// Legacy custom event should dispatch
|
||||
$this->dispatcher->expects($matcher)
|
||||
->method('dispatch')->willReturnCallback(function (object $event, string $eventName) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame('something', $eventName);
|
||||
$event->setResult(false);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTION, $eventName); // @phpstan-ignore-line classConstant.deprecated
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(FailedEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_FAILED, $eventName);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$this->scheduler->expects($this->once())
|
||||
->method('rescheduleFailures');
|
||||
|
||||
$this->getLegacyEventDispatcher()->dispatchCustomEvent($this->config, $logs, false, $this->pendingEvent);
|
||||
}
|
||||
|
||||
public function testFailedResultAsArrayIsProcessed(): void
|
||||
{
|
||||
$this->config->expects($this->exactly(2))
|
||||
->method('getConfig')
|
||||
->willReturn(['eventName' => 'something']);
|
||||
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setLead(new Lead());
|
||||
|
||||
$logs = new ArrayCollection([$leadEventLog]);
|
||||
|
||||
// Should fail because we're returning false
|
||||
$this->pendingEvent->expects($this->once())
|
||||
->method('fail');
|
||||
|
||||
$this->contactTracker->expects($this->exactly(2))
|
||||
->method('setSystemContact');
|
||||
// Legacy custom event should dispatch
|
||||
$matcher = $this->exactly(3);
|
||||
|
||||
// Legacy custom event should dispatch
|
||||
$this->dispatcher->expects($matcher)
|
||||
->method('dispatch')->willReturnCallback(function (object $event, string $eventName) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame('something', $eventName);
|
||||
$event->setResult(['result' => false, 'foo' => 'bar']);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_EXECUTION, $eventName); // @phpstan-ignore-line classConstant.deprecated
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(FailedEvent::class, $event);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_FAILED, $eventName);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$this->scheduler->expects($this->once())
|
||||
->method('rescheduleFailures');
|
||||
|
||||
$this->getLegacyEventDispatcher()->dispatchCustomEvent($this->config, $logs, false, $this->pendingEvent);
|
||||
}
|
||||
|
||||
public function testPassWithErrorIsHandled(): void
|
||||
{
|
||||
$this->config->expects($this->exactly(2))
|
||||
->method('getConfig')
|
||||
->willReturn(['eventName' => 'something']);
|
||||
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setLead(new Lead());
|
||||
$leadEventLog->setMetadata(['bar' => 'foo']);
|
||||
|
||||
$logs = new ArrayCollection([$leadEventLog]);
|
||||
|
||||
// Should pass but with an error logged
|
||||
$this->pendingEvent->expects($this->once())
|
||||
->method('passWithError');
|
||||
|
||||
$this->contactTracker->expects($this->exactly(2))
|
||||
->method('setSystemContact');
|
||||
// Legacy custom event should dispatch
|
||||
$matcher = $this->any();
|
||||
|
||||
// Legacy custom event should dispatch
|
||||
$this->dispatcher->expects($matcher)->method('dispatch')
|
||||
->willReturnCallback(function (object $event, string $eventName) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame('something', $eventName);
|
||||
$event->setResult(['failed' => 1, 'reason' => 'because']);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$this->getLegacyEventDispatcher()->dispatchCustomEvent($this->config, $logs, false, $this->pendingEvent);
|
||||
}
|
||||
|
||||
public function testLogIsPassed(): void
|
||||
{
|
||||
$this->config->expects($this->exactly(2))
|
||||
->method('getConfig')
|
||||
->willReturn(['eventName' => 'something']);
|
||||
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setLead(new Lead());
|
||||
$leadEventLog->setMetadata(['bar' => 'foo']);
|
||||
|
||||
$logs = new ArrayCollection([$leadEventLog]);
|
||||
|
||||
// Should fail because we're returning false
|
||||
$this->pendingEvent->expects($this->once())
|
||||
->method('pass');
|
||||
|
||||
$this->contactTracker->expects($this->exactly(2))
|
||||
->method('setSystemContact');
|
||||
$matcher = $this->any();
|
||||
|
||||
// Should pass
|
||||
$this->dispatcher->expects($matcher)->method('dispatch')
|
||||
->willReturnCallback(function (object $event, string $eventName) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertInstanceOf(CampaignExecutionEvent::class, $event);
|
||||
$this->assertSame('something', $eventName);
|
||||
$event->setResult(true);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$this->getLegacyEventDispatcher()->dispatchCustomEvent($this->config, $logs, false, $this->pendingEvent);
|
||||
}
|
||||
|
||||
public function testLegacyEventDispatchedForConvertedBatchActions(): void
|
||||
{
|
||||
$this->config->expects($this->exactly(1))
|
||||
->method('getConfig')
|
||||
->willReturn(['eventName' => 'something']);
|
||||
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setLead(new Lead());
|
||||
$leadEventLog->setMetadata(['bar' => 'foo']);
|
||||
|
||||
$logs = new ArrayCollection([$leadEventLog]);
|
||||
|
||||
// Should never be called
|
||||
$this->pendingEvent->expects($this->never())
|
||||
->method('pass');
|
||||
|
||||
$this->contactTracker->expects($this->exactly(2))
|
||||
->method('setSystemContact');
|
||||
|
||||
$this->dispatcher->method('dispatch')
|
||||
->with($this->isInstanceOf(CampaignExecutionEvent::class), 'something')
|
||||
->willReturnCallback(fn (CampaignExecutionEvent $event) => $event->setResult(true));
|
||||
|
||||
$this->getLegacyEventDispatcher()->dispatchCustomEvent($this->config, $logs, true, $this->pendingEvent);
|
||||
}
|
||||
|
||||
private function getLegacyEventDispatcher(): LegacyEventDispatcher
|
||||
{
|
||||
return new LegacyEventDispatcher(
|
||||
$this->dispatcher,
|
||||
$this->scheduler,
|
||||
new NullLogger(),
|
||||
$this->contactTracker
|
||||
);
|
||||
}
|
||||
|
||||
public static function bogusCallback(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
|
||||
use Mautic\CampaignBundle\Executioner\EventExecutioner;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Traits\LoggerTrait;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class EventExecutionerLockTest extends MauticMysqlTestCase
|
||||
{
|
||||
use LoggerTrait {
|
||||
setUp as loggerTraitSetup;
|
||||
}
|
||||
|
||||
private const ADD_POINTS = 10;
|
||||
private EventExecutioner $eventExecutioner;
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
protected function setUp(): void // @phpstan-ignore phpunit.callParent
|
||||
{
|
||||
$this->loggerTraitSetup();
|
||||
|
||||
$this->eventExecutioner = self::getContainer()->get('mautic.campaign.event_executioner');
|
||||
$this->eventDispatcher = self::getContainer()->get('event_dispatcher');
|
||||
}
|
||||
|
||||
public function testLogsAreSkippedWhenAlreadyExecuted(): void
|
||||
{
|
||||
$event = $this->createEvent($this->createCampaign());
|
||||
$contact = $this->createContact();
|
||||
$this->em->flush();
|
||||
|
||||
Assert::assertSame(0, $contact->getPoints());
|
||||
|
||||
$contacts = new ArrayCollection([$contact->getId() => $contact]);
|
||||
$this->eventExecutioner->executeForContacts($event, $contacts);
|
||||
Assert::assertSame(self::ADD_POINTS, $contact->getPoints(), 'Points should be added.');
|
||||
|
||||
$logs = $this->em->getRepository(LeadEventLog::class)->findAll();
|
||||
Assert::assertCount(1, $logs);
|
||||
|
||||
$log = reset($logs);
|
||||
\assert($log instanceof LeadEventLog);
|
||||
Assert::assertSame(2, $log->getVersion(), 'Version should be incremented.');
|
||||
|
||||
$this->eventExecutioner->executeLogs($event, new ArrayCollection($logs));
|
||||
Assert::assertSame(self::ADD_POINTS, $contact->getPoints(), // @phpstan-ignore argument.unresolvableType
|
||||
'Points should not be added as the log has been executed already.');
|
||||
Assert::assertTrue($this->testHandler->hasErrorThatContains(sprintf(
|
||||
'Campaign event log ID "%s" was skipped as it had been executed already.',
|
||||
$log->getId(),
|
||||
)), 'There should be an error log regarding the skipped log.');
|
||||
}
|
||||
|
||||
public function testFailedLogsCanBeReExecuted(): void
|
||||
{
|
||||
$event = $this->createEvent($this->createCampaign());
|
||||
$contact = $this->createContact();
|
||||
$this->em->flush();
|
||||
|
||||
Assert::assertSame(0, $contact->getPoints());
|
||||
|
||||
$listener = $this->makeEventExecutionFail();
|
||||
$contacts = new ArrayCollection([$contact->getId() => $contact]);
|
||||
$this->eventExecutioner->executeForContacts($event, $contacts);
|
||||
Assert::assertSame(0, $contact->getPoints(),
|
||||
'Points should not be added as the execution failed.');
|
||||
|
||||
$logs = $this->em->getRepository(LeadEventLog::class)->findAll();
|
||||
Assert::assertCount(1, $logs);
|
||||
|
||||
$log = reset($logs);
|
||||
\assert($log instanceof LeadEventLog);
|
||||
Assert::assertSame(1, $log->getVersion(), 'Version should be reset when execution failed.');
|
||||
|
||||
$this->makeEventExecutionPass($listener);
|
||||
$this->eventExecutioner->executeLogs($event, new ArrayCollection($logs));
|
||||
Assert::assertSame(self::ADD_POINTS, $contact->getPoints(),
|
||||
'Points should be added as the log\'s version has been reset when execution failed.');
|
||||
Assert::assertFalse($this->testHandler->hasWarningThatContains(sprintf(
|
||||
'Campaign event log ID "%s" was skipped as it had been executed already.',
|
||||
$log->getId(),
|
||||
)), 'There should not be any warning log regarding skipped logs.');
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test Campaign');
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createEvent(Campaign $campaign): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setCampaign($campaign);
|
||||
$event->setName('Add points');
|
||||
$event->setType('lead.changepoints');
|
||||
$event->setEventType('action');
|
||||
$event->setProperties(['points' => self::ADD_POINTS]);
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createContact(): Lead
|
||||
{
|
||||
$contact = new Lead();
|
||||
$this->em->persist($contact);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
private function makeEventExecutionFail(): callable
|
||||
{
|
||||
$listener = function (CampaignExecutionEvent $event) { // @phpstan-ignore parameter.deprecatedClass
|
||||
$event->setResult(false);
|
||||
$event->stopPropagation();
|
||||
};
|
||||
$this->eventDispatcher->addListener(LeadEvents::ON_CAMPAIGN_TRIGGER_ACTION, $listener, 9999); // @phpstan-ignore classConstant.deprecated
|
||||
|
||||
return $listener;
|
||||
}
|
||||
|
||||
private function makeEventExecutionPass(callable $listener): void
|
||||
{
|
||||
$this->eventDispatcher->removeListener(LeadEvents::ON_CAMPAIGN_TRIGGER_ACTION, $listener); // @phpstan-ignore classConstant.deprecated
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\EventRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository;
|
||||
use Mautic\CampaignBundle\Event\PendingEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\EventCollector;
|
||||
use Mautic\CampaignBundle\EventListener\CampaignActionJumpToEventSubscriber;
|
||||
use Mautic\CampaignBundle\Executioner\Event\ActionExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\Event\ConditionExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\Event\DecisionExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\EventExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\Logger\EventLogger;
|
||||
use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\CampaignBundle\Form\Type\CampaignEventJumpToEventType;
|
||||
use Mautic\CampaignBundle\Helper\RemovedContactTracker;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\EmailEvents;
|
||||
use Mautic\EmailBundle\Form\Type\EmailSendType;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class EventExecutionerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var EventCollector&MockObject
|
||||
*/
|
||||
private MockObject $eventCollector;
|
||||
|
||||
/**
|
||||
* @var EventLogger&MockObject
|
||||
*/
|
||||
private MockObject $eventLogger;
|
||||
|
||||
/**
|
||||
* @var ActionExecutioner&MockObject
|
||||
*/
|
||||
private MockObject $actionExecutioner;
|
||||
|
||||
/**
|
||||
* @var ConditionExecutioner&MockObject
|
||||
*/
|
||||
private MockObject $conditionExecutioner;
|
||||
|
||||
/**
|
||||
* @var DecisionExecutioner&MockObject
|
||||
*/
|
||||
private MockObject $decisionExecutioner;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface&MockObject
|
||||
*/
|
||||
private MockObject $logger;
|
||||
|
||||
/**
|
||||
* @var EventScheduler&MockObject
|
||||
*/
|
||||
private MockObject $eventScheduler;
|
||||
|
||||
/**
|
||||
* @var RemovedContactTracker&MockObject
|
||||
*/
|
||||
private MockObject $removedContactTracker;
|
||||
|
||||
/**
|
||||
* @var LeadRepository&MockObject
|
||||
*/
|
||||
private MockObject $leadRepository;
|
||||
|
||||
/**
|
||||
* @var EventRepository&MockObject
|
||||
*/
|
||||
private MockObject $eventRepository;
|
||||
|
||||
/**
|
||||
* @var Translator&MockObject
|
||||
*/
|
||||
private MockObject $translator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->eventCollector = $this->createMock(EventCollector::class);
|
||||
$this->eventLogger = $this->createMock(EventLogger::class);
|
||||
$this->eventLogger->method('persistCollection')
|
||||
->willReturn($this->eventLogger);
|
||||
$this->actionExecutioner = $this->createMock(ActionExecutioner::class);
|
||||
$this->conditionExecutioner = $this->createMock(ConditionExecutioner::class);
|
||||
$this->decisionExecutioner = $this->createMock(DecisionExecutioner::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->eventScheduler = $this->createMock(EventScheduler::class);
|
||||
$this->removedContactTracker = $this->createMock(RemovedContactTracker::class);
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
$this->eventRepository = $this->createMock(EventRepository::class);
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
}
|
||||
|
||||
public function testJumpToEventsAreProcessedAfterOtherEvents(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
|
||||
$otherEvent = new Event();
|
||||
$otherEvent->setEventType(ActionExecutioner::TYPE)
|
||||
->setType('email.send')
|
||||
->setCampaign($campaign);
|
||||
$otherConfig = new ActionAccessor(
|
||||
[
|
||||
'label' => 'mautic.email.campaign.event.send',
|
||||
'description' => 'mautic.email.campaign.event.send_descr',
|
||||
'batchEventName' => EmailEvents::ON_CAMPAIGN_BATCH_ACTION,
|
||||
'formType' => EmailSendType::class,
|
||||
'formTypeOptions' => ['update_select' => 'campaignevent_properties_email', 'with_email_types' => true],
|
||||
'formTheme' => 'MauticEmailBundle:FormTheme\EmailSendList',
|
||||
'channel' => 'email',
|
||||
'channelIdField' => 'email',
|
||||
]
|
||||
);
|
||||
|
||||
$jumpEvent = new Event();
|
||||
$jumpEvent->setEventType(ActionExecutioner::TYPE)
|
||||
->setType(CampaignActionJumpToEventSubscriber::EVENT_NAME)
|
||||
->setCampaign($campaign);
|
||||
$jumpConfig = new ActionAccessor(
|
||||
[
|
||||
'label' => 'mautic.campaign.event.jump_to_event',
|
||||
'description' => 'mautic.campaign.event.jump_to_event_descr',
|
||||
'formType' => CampaignEventJumpToEventType::class,
|
||||
'template' => '@MauticCampaign/Event/jump.html.twig',
|
||||
'batchEventName' => CampaignEvents::ON_EVENT_JUMP_TO_EVENT,
|
||||
'connectionRestrictions' => [
|
||||
'target' => [
|
||||
Event::TYPE_DECISION => ['none'],
|
||||
Event::TYPE_ACTION => ['none'],
|
||||
Event::TYPE_CONDITION => ['none'],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$events = new ArrayCollection([$otherEvent, $jumpEvent]);
|
||||
$contacts = new ArrayCollection([new Lead()]);
|
||||
|
||||
$this->eventCollector->method('getEventConfig')
|
||||
->willReturnCallback(
|
||||
function (Event $event) use ($jumpConfig, $otherConfig) {
|
||||
if (CampaignActionJumpToEventSubscriber::EVENT_NAME === $event->getType()) {
|
||||
return $jumpConfig;
|
||||
}
|
||||
|
||||
return $otherConfig;
|
||||
}
|
||||
);
|
||||
|
||||
$this->eventScheduler->expects($this->exactly(2))
|
||||
->method('getExecutionDateTime')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$this->eventLogger->expects($this->exactly(2))
|
||||
->method('fetchRotationAndGenerateLogsFromContacts')
|
||||
->willReturnCallback(
|
||||
function (Event $event, ActionAccessor $config, ArrayCollection $contacts, $isInactiveEntry) {
|
||||
$logs = new ArrayCollection();
|
||||
foreach ($contacts as $contact) {
|
||||
$log = new LeadEventLog();
|
||||
$log->setLead($contact);
|
||||
$log->setEvent($event);
|
||||
$log->setCampaign($event->getCampaign());
|
||||
$logs->add($log);
|
||||
}
|
||||
|
||||
return $logs;
|
||||
}
|
||||
);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->actionExecutioner->expects($matcher)
|
||||
->method('execute')->willReturnCallback(function (...$parameters) use ($matcher, $otherConfig, $jumpConfig) {
|
||||
$this->assertInstanceOf(ArrayCollection::class, $parameters[1]);
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals($otherConfig, $parameters[0]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals($jumpConfig, $parameters[0]);
|
||||
}
|
||||
|
||||
return new EvaluatedContacts();
|
||||
});
|
||||
|
||||
// This should not be called because the rotation is already incremented in the subscriber
|
||||
$this->leadRepository->expects($this->never())
|
||||
->method('incrementCampaignRotationForContacts');
|
||||
|
||||
$this->getEventExecutioner()->executeEventsForContacts($events, $contacts);
|
||||
}
|
||||
|
||||
private function getEventExecutioner(): EventExecutioner
|
||||
{
|
||||
return new EventExecutioner(
|
||||
$this->eventCollector,
|
||||
$this->eventLogger,
|
||||
$this->actionExecutioner,
|
||||
$this->conditionExecutioner,
|
||||
$this->decisionExecutioner,
|
||||
$this->logger,
|
||||
$this->eventScheduler,
|
||||
$this->removedContactTracker,
|
||||
);
|
||||
}
|
||||
|
||||
public function testJumpToEventsExecutedWithoutTarget(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
|
||||
$event = new Event();
|
||||
$event->setEventType(ActionExecutioner::TYPE)
|
||||
->setType(CampaignActionJumpToEventSubscriber::EVENT_NAME)
|
||||
->setCampaign($campaign)
|
||||
->setProperties(['jumpToEvent' => 999]);
|
||||
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->method('getLead')
|
||||
->willReturn($lead);
|
||||
$log->method('setIsScheduled')
|
||||
->willReturn($log);
|
||||
$log->method('getEvent')
|
||||
->willReturn($event);
|
||||
$log->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$logs = new ArrayCollection(
|
||||
[
|
||||
1 => $log,
|
||||
]
|
||||
);
|
||||
|
||||
$config = new ActionAccessor(
|
||||
[
|
||||
'label' => 'mautic.campaign.event.jump_to_event',
|
||||
'description' => 'mautic.campaign.event.jump_to_event_descr',
|
||||
'formType' => CampaignEventJumpToEventType::class,
|
||||
'template' => '@MauticCampaign/Event/jump.html.twig',
|
||||
'batchEventName' => CampaignEvents::ON_EVENT_JUMP_TO_EVENT,
|
||||
'connectionRestrictions' => [
|
||||
'target' => [
|
||||
Event::TYPE_DECISION => ['none'],
|
||||
Event::TYPE_ACTION => ['none'],
|
||||
Event::TYPE_CONDITION => ['none'],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$pendingEvent = new PendingEvent($config, $event, $logs);
|
||||
|
||||
$this->eventRepository->method('getEntities')
|
||||
->willReturn([]);
|
||||
|
||||
$eventScheduler = $this->createMock(EventScheduler::class);
|
||||
|
||||
$subscriber = new CampaignActionJumpToEventSubscriber(
|
||||
$this->eventRepository,
|
||||
$this->getEventExecutioner(),
|
||||
$this->translator,
|
||||
$this->leadRepository,
|
||||
$eventScheduler
|
||||
);
|
||||
$subscriber->onJumpToEvent($pendingEvent);
|
||||
|
||||
$this->assertEquals(count($pendingEvent->getSuccessful()), 1);
|
||||
$this->assertEquals(count($pendingEvent->getFailures()), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContactFinder;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CampaignBundle\Executioner\EventExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\Helper\InactiveHelper;
|
||||
use Mautic\CampaignBundle\Executioner\InactiveExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\CoreBundle\ProcessSignal\ProcessSignalService;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
class InactiveExecutionerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|InactiveContactFinder
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $inactiveContactFinder;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|Translator
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $translator;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|EventScheduler
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $eventScheduler;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|InactiveHelper
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $inactiveHelper;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|EventExecutioner
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $eventExecutioner;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->inactiveContactFinder = $this->createMock(InactiveContactFinder::class);
|
||||
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
|
||||
$this->eventScheduler = $this->createMock(EventScheduler::class);
|
||||
|
||||
$this->inactiveHelper = $this->createMock(InactiveHelper::class);
|
||||
|
||||
$this->eventExecutioner = $this->createMock(EventExecutioner::class);
|
||||
}
|
||||
|
||||
public function testNoContactsFoundResultsInNothingExecuted(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->expects($this->once())
|
||||
->method('getEventsByType')
|
||||
->willReturn(new ArrayCollection());
|
||||
|
||||
$this->inactiveContactFinder->expects($this->never())
|
||||
->method('getContactCount');
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
$counter = $this->getExecutioner()->execute($campaign, $limiter, new BufferedOutput());
|
||||
|
||||
$this->assertEquals(0, $counter->getEvaluated());
|
||||
}
|
||||
|
||||
public function testNoEventsFoundResultsInNothingExecuted(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->expects($this->once())
|
||||
->method('getEventsByType')
|
||||
->willReturn(new ArrayCollection([new Event()]));
|
||||
|
||||
$this->inactiveContactFinder->expects($this->once())
|
||||
->method('getContactCount')
|
||||
->willReturn(0);
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
$counter = $this->getExecutioner()->execute($campaign, $limiter, new BufferedOutput());
|
||||
|
||||
$this->assertEquals(0, $counter->getTotalEvaluated());
|
||||
}
|
||||
|
||||
public function testNextBatchOfContactsAreExecuted(): void
|
||||
{
|
||||
$decision = new Event();
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->expects($this->once())
|
||||
->method('getEventsByType')
|
||||
->willReturn(new ArrayCollection([$decision]));
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
|
||||
$this->inactiveContactFinder->expects($this->once())
|
||||
->method('getContactCount')
|
||||
->willReturn(2);
|
||||
|
||||
$this->inactiveContactFinder->expects($this->exactly(3))
|
||||
->method('getContacts')
|
||||
->with(null, $decision, $limiter)
|
||||
->willReturnOnConsecutiveCalls(
|
||||
new ArrayCollection([3 => new Lead()]),
|
||||
new ArrayCollection([10 => new Lead()]),
|
||||
new ArrayCollection([])
|
||||
);
|
||||
|
||||
$this->inactiveHelper->expects($this->exactly(2))
|
||||
->method('getEarliestInactiveDateTime')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$this->eventScheduler->expects($this->exactly(2))
|
||||
->method('getSortedExecutionDates')
|
||||
->willReturn([]);
|
||||
|
||||
$this->getExecutioner()->execute($campaign, $limiter, new BufferedOutput());
|
||||
}
|
||||
|
||||
public function testValidationExecutesNothingIfCampaignUnpublished(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(false);
|
||||
|
||||
$event = new Event();
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
$this->inactiveHelper->expects($this->once())
|
||||
->method('getCollectionByDecisionId')
|
||||
->with(1)
|
||||
->willReturn(new ArrayCollection([$event]));
|
||||
|
||||
$this->inactiveContactFinder->expects($this->never())
|
||||
->method('getContacts');
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
|
||||
$counter = $this->getExecutioner()->validate(1, $limiter, new BufferedOutput());
|
||||
$this->assertEquals(0, $counter->getTotalEvaluated());
|
||||
}
|
||||
|
||||
public function testValidationEvaluatesFoundEvents(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(true);
|
||||
|
||||
$decision = new Event();
|
||||
$decision->setCampaign($campaign);
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
|
||||
$this->inactiveHelper->expects($this->once())
|
||||
->method('getCollectionByDecisionId')
|
||||
->with(1)
|
||||
->willReturn(new ArrayCollection([$decision]));
|
||||
|
||||
$this->inactiveContactFinder->expects($this->once())
|
||||
->method('getContactCount')
|
||||
->willReturn(2);
|
||||
|
||||
$this->inactiveContactFinder->expects($this->exactly(3))
|
||||
->method('getContacts')
|
||||
->with(null, $decision, $limiter)
|
||||
->willReturnOnConsecutiveCalls(
|
||||
new ArrayCollection([3 => new Lead()]),
|
||||
new ArrayCollection([10 => new Lead()]),
|
||||
new ArrayCollection([])
|
||||
);
|
||||
|
||||
$this->inactiveHelper->expects($this->exactly(2))
|
||||
->method('getEarliestInactiveDateTime')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$this->eventScheduler->expects($this->exactly(2))
|
||||
->method('getSortedExecutionDates')
|
||||
->willReturn([]);
|
||||
|
||||
$this->getExecutioner()->validate(1, $limiter, new BufferedOutput());
|
||||
}
|
||||
|
||||
private function getExecutioner()
|
||||
{
|
||||
return new InactiveExecutioner(
|
||||
$this->inactiveContactFinder,
|
||||
new NullLogger(),
|
||||
$this->translator,
|
||||
$this->eventScheduler,
|
||||
$this->inactiveHelper,
|
||||
$this->eventExecutioner,
|
||||
$this->createMock(ProcessSignalService::class)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContactFinder;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CampaignBundle\Executioner\EventExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\KickoffExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\Result\Counter;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\ProcessSignal\ProcessSignalService;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class KickoffExecutionerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|KickoffContactFinder
|
||||
*/
|
||||
private MockObject $kickoffContactFinder;
|
||||
|
||||
/**
|
||||
* @var MockObject|Translator
|
||||
*/
|
||||
private MockObject $translator;
|
||||
|
||||
/**
|
||||
* @var MockObject|EventExecutioner
|
||||
*/
|
||||
private MockObject $executioner;
|
||||
|
||||
/**
|
||||
* @var MockObject|EventScheduler
|
||||
*/
|
||||
private MockObject $scheduler;
|
||||
|
||||
/**
|
||||
* @var CoreParametersHelper&MockObject
|
||||
*/
|
||||
private MockObject $coreParametersHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->kickoffContactFinder = $this->createMock(KickoffContactFinder::class);
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
$this->executioner = $this->createMock(EventExecutioner::class);
|
||||
$this->scheduler = $this->createMock(EventScheduler::class);
|
||||
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
}
|
||||
|
||||
public function testNoContactsResultInEmptyResults(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->expects($this->once())
|
||||
->method('getRootEvents')
|
||||
->willReturn(new ArrayCollection());
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
|
||||
$counter = $this->getExecutioner()->execute($campaign, $limiter, new BufferedOutput());
|
||||
|
||||
$this->assertEquals(0, $counter->getTotalEvaluated());
|
||||
}
|
||||
|
||||
public function testEventsAreScheduledAndExecuted(): void
|
||||
{
|
||||
$this->kickoffContactFinder->expects($this->once())
|
||||
->method('getContactCount')
|
||||
->willReturn(2);
|
||||
|
||||
$this->kickoffContactFinder->expects($this->exactly(3))
|
||||
->method('getContacts')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
new ArrayCollection([3 => new Lead()]),
|
||||
new ArrayCollection([10 => new Lead()]),
|
||||
new ArrayCollection([])
|
||||
);
|
||||
|
||||
$event = new Event();
|
||||
$event2 = new Event();
|
||||
$campaign = new class extends Campaign {
|
||||
/**
|
||||
* @var ArrayCollection<int,Event>
|
||||
*/
|
||||
public ArrayCollection $rootEvents;
|
||||
|
||||
/**
|
||||
* @return ArrayCollection<int,Event>
|
||||
*/
|
||||
public function getRootEvents(): ArrayCollection
|
||||
{
|
||||
return $this->rootEvents;
|
||||
}
|
||||
};
|
||||
$campaign->rootEvents = new ArrayCollection([$event, $event2]);
|
||||
$event->setCampaign($campaign);
|
||||
$event2->setCampaign($campaign);
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
|
||||
$this->scheduler->expects($this->exactly(4))
|
||||
->method('getExecutionDateTime')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$callbackCounter = 0;
|
||||
$this->scheduler->expects($this->exactly(4))
|
||||
->method('validateAndScheduleEventForContacts')
|
||||
->willReturnCallback(
|
||||
function () use (&$callbackCounter): void {
|
||||
++$callbackCounter;
|
||||
if (in_array($callbackCounter, [3, 4])) {
|
||||
throw new NotSchedulableException();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$this->executioner->expects($this->exactly(1))
|
||||
->method('executeEventsForContacts')->willReturnCallback(function (...$parameters) {
|
||||
$this->assertCount(2, $parameters[0]);
|
||||
$this->assertInstanceOf(ArrayCollection::class, $parameters[1]);
|
||||
$this->assertInstanceOf(Counter::class, $parameters[2]);
|
||||
});
|
||||
|
||||
$counter = $this->getExecutioner()->execute($campaign, $limiter, new BufferedOutput());
|
||||
|
||||
$this->assertEquals(4, $counter->getTotalEvaluated());
|
||||
$this->assertEquals(2, $counter->getTotalScheduled());
|
||||
}
|
||||
|
||||
private function getExecutioner(): KickoffExecutioner
|
||||
{
|
||||
return new KickoffExecutioner(
|
||||
new NullLogger(),
|
||||
$this->kickoffContactFinder,
|
||||
$this->translator,
|
||||
$this->executioner,
|
||||
$this->scheduler,
|
||||
$this->createMock(ProcessSignalService::class),
|
||||
$this->coreParametersHelper,
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\Logger;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository;
|
||||
use Mautic\CampaignBundle\Executioner\Logger\EventLogger;
|
||||
use Mautic\CampaignBundle\Model\SummaryModel;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EventLoggerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var IpLookupHelper&MockObject
|
||||
*/
|
||||
private MockObject $ipLookupHelper;
|
||||
|
||||
/**
|
||||
* @var ContactTracker|MockObject
|
||||
*/
|
||||
private MockObject $contactTracker;
|
||||
|
||||
/**
|
||||
* @var LeadEventLogRepository|MockObject
|
||||
*/
|
||||
private MockObject $leadEventLogRepository;
|
||||
|
||||
/**
|
||||
* @var LeadRepository|MockObject
|
||||
*/
|
||||
private MockObject $leadRepository;
|
||||
|
||||
/**
|
||||
* @var SummaryModel|MockObject
|
||||
*/
|
||||
private MockObject $summaryModel;
|
||||
|
||||
/**
|
||||
* @var CoreParametersHelper&MockObject
|
||||
*/
|
||||
private MockObject $coreParametersHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$this->contactTracker = $this->createMock(ContactTracker::class);
|
||||
$this->leadEventLogRepository = $this->createMock(LeadEventLogRepository::class);
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
$this->summaryModel = $this->createMock(SummaryModel::class);
|
||||
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
}
|
||||
|
||||
public function testAllLogsAreReturnedWithFinalPersist(): void
|
||||
{
|
||||
$logCollection = new ArrayCollection();
|
||||
while ($logCollection->count() < 60) {
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->method('getId')
|
||||
->willReturn($logCollection->count() + 1);
|
||||
|
||||
$logCollection->add($log);
|
||||
}
|
||||
|
||||
$this->leadEventLogRepository->expects($this->exactly(3))
|
||||
->method('saveEntities');
|
||||
|
||||
$logger = $this->getLogger();
|
||||
foreach ($logCollection as $log) {
|
||||
$logger->queueToPersist($log);
|
||||
}
|
||||
|
||||
$persistedLogs = $logger->persistQueuedLogs();
|
||||
|
||||
$this->assertEquals($persistedLogs->count(), $logCollection->count());
|
||||
$this->assertEquals($logCollection->getValues(), $persistedLogs->getValues());
|
||||
}
|
||||
|
||||
public function testBuildLogEntry(): void
|
||||
{
|
||||
$this->ipLookupHelper->method('getIpAddress')->willReturn(new IpAddress());
|
||||
|
||||
$this->leadRepository->expects($this->exactly(3))
|
||||
->method('getContactRotations')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
[1 => ['rotation' => 1, 'manually_removed' => 0]],
|
||||
[1 => ['rotation' => 2, 'manually_removed' => 0]],
|
||||
[1 => ['rotation' => 1, 'manually_removed' => 0]],
|
||||
);
|
||||
|
||||
/** @var MockObject&Campaign $campaign */
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('getId')->willReturnOnConsecutiveCalls(1, 1, 1, 1, 2, 2);
|
||||
|
||||
$event = new Event();
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
/** @var MockObject&Lead $contact */
|
||||
$contact = $this->createMock(Lead::class);
|
||||
$contact->method('getId')->willReturn(1);
|
||||
|
||||
// rotation for campaign 1 and contact 1
|
||||
$log = $this->getLogger()->buildLogEntry($event, $contact, false);
|
||||
$this->assertEquals(1, $log->getRotation());
|
||||
|
||||
// rotation for campaign 1 and contact 1
|
||||
$log = $this->getLogger()->buildLogEntry($event, $contact, false);
|
||||
$this->assertEquals(2, $log->getRotation());
|
||||
|
||||
// rotation for campaign 2 and contact 1
|
||||
$log = $this->getLogger()->buildLogEntry($event, $contact, false);
|
||||
$this->assertEquals(1, $log->getRotation());
|
||||
}
|
||||
|
||||
private function getLogger(): EventLogger
|
||||
{
|
||||
return new EventLogger(
|
||||
$this->ipLookupHelper,
|
||||
$this->contactTracker,
|
||||
$this->leadEventLogRepository,
|
||||
$this->leadRepository,
|
||||
$this->summaryModel,
|
||||
$this->coreParametersHelper
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\EventRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\EventCollector;
|
||||
use Mautic\CampaignBundle\Executioner\Event\DecisionExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\EventExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\Helper\DecisionHelper;
|
||||
use Mautic\CampaignBundle\Executioner\RealTimeExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class RealTimeExecutionerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|LeadModel
|
||||
*/
|
||||
private MockObject $leadModel;
|
||||
|
||||
/**
|
||||
* @var MockObject|EventRepository
|
||||
*/
|
||||
private MockObject $eventRepository;
|
||||
|
||||
/**
|
||||
* @var MockObject|EventExecutioner
|
||||
*/
|
||||
private MockObject $executioner;
|
||||
|
||||
/**
|
||||
* @var MockObject|DecisionExecutioner
|
||||
*/
|
||||
private MockObject $decisionExecutioner;
|
||||
|
||||
/**
|
||||
* @var MockObject|EventCollector
|
||||
*/
|
||||
private MockObject $eventCollector;
|
||||
|
||||
/**
|
||||
* @var MockObject|EventScheduler
|
||||
*/
|
||||
private MockObject $eventScheduler;
|
||||
|
||||
/**
|
||||
* @var MockObject|ContactTracker
|
||||
*/
|
||||
private MockObject $contactTracker;
|
||||
|
||||
/**
|
||||
* @var MockObject|LeadRepository
|
||||
*/
|
||||
private MockObject $leadRepository;
|
||||
|
||||
private DecisionHelper $decisionHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->leadModel = $this->createMock(LeadModel::class);
|
||||
|
||||
$this->eventRepository = $this->createMock(EventRepository::class);
|
||||
|
||||
$this->executioner = $this->createMock(EventExecutioner::class);
|
||||
|
||||
$this->decisionExecutioner = $this->createMock(DecisionExecutioner::class);
|
||||
|
||||
$this->eventCollector = $this->createMock(EventCollector::class);
|
||||
|
||||
$this->eventScheduler = $this->createMock(EventScheduler::class);
|
||||
|
||||
$this->contactTracker = $this->createMock(ContactTracker::class);
|
||||
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
|
||||
$this->decisionHelper = new DecisionHelper($this->leadRepository);
|
||||
}
|
||||
|
||||
public function testContactNotFoundResultsInEmptyResponses(): void
|
||||
{
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn(null);
|
||||
|
||||
$this->eventRepository->expects($this->never())
|
||||
->method('getContactPendingEvents');
|
||||
|
||||
$responses = $this->getExecutioner()->execute('something');
|
||||
|
||||
$this->assertEquals(0, $responses->containsResponses());
|
||||
}
|
||||
|
||||
public function testNoRelatedEventsResultInEmptyResponses(): void
|
||||
{
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->exactly(3))
|
||||
->method('getId')
|
||||
->willReturn(10);
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn($lead);
|
||||
|
||||
$this->eventRepository->expects($this->once())
|
||||
->method('getContactPendingEvents')
|
||||
->willReturn([]);
|
||||
|
||||
$this->eventCollector->expects($this->never())
|
||||
->method('getEventConfig');
|
||||
|
||||
$responses = $this->getExecutioner()->execute('something');
|
||||
|
||||
$this->assertEquals(0, $responses->containsResponses());
|
||||
}
|
||||
|
||||
public function testChannelMisMatchResultsInEmptyResponses(): void
|
||||
{
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->exactly(5))
|
||||
->method('getId')
|
||||
->willReturn(10);
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn($lead);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->expects($this->exactly(3))
|
||||
->method('getChannel')
|
||||
->willReturn('email');
|
||||
$event->method('getEventType')
|
||||
->willReturn(Event::TYPE_DECISION);
|
||||
|
||||
$this->eventRepository->expects($this->once())
|
||||
->method('getContactPendingEvents')
|
||||
->willReturn([$event]);
|
||||
|
||||
$this->eventCollector->expects($this->never())
|
||||
->method('getEventConfig');
|
||||
|
||||
$responses = $this->getExecutioner()->execute('something', null, 'page');
|
||||
|
||||
$this->assertEquals(0, $responses->containsResponses());
|
||||
}
|
||||
|
||||
public function testChannelFuzzyMatchResultsInNonEmptyResponses(): void
|
||||
{
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->exactly(5))
|
||||
->method('getId')
|
||||
->willReturn(10);
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn($lead);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->expects($this->exactly(2))
|
||||
->method('getChannel')
|
||||
->willReturn('page');
|
||||
$event->method('getEventType')
|
||||
->willReturn(Event::TYPE_DECISION);
|
||||
|
||||
$action1 = $this->createMock(Event::class);
|
||||
$action2 = $this->createMock(Event::class);
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('getPositiveChildren')
|
||||
->willReturn(new ArrayCollection([$action1, $action2]));
|
||||
|
||||
$this->eventRepository->expects($this->once())
|
||||
->method('getContactPendingEvents')
|
||||
->willReturn([$event]);
|
||||
|
||||
$this->eventCollector->expects($this->once())
|
||||
->method('getEventConfig')
|
||||
->willReturn(new DecisionAccessor([]));
|
||||
|
||||
$this->eventScheduler->expects($this->exactly(2))
|
||||
->method('getExecutionDateTime')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$this->eventScheduler->expects($this->exactly(2))
|
||||
->method('shouldSchedule')
|
||||
->willReturnOnConsecutiveCalls(true, false);
|
||||
|
||||
$this->eventScheduler->expects($this->once())
|
||||
->method('scheduleForContact');
|
||||
|
||||
// This is how we know if the test failed/passed
|
||||
$this->executioner->expects($this->once())
|
||||
->method('executeEventsForContact');
|
||||
|
||||
$this->getExecutioner()->execute('something', null, 'page.redirect');
|
||||
}
|
||||
|
||||
public function testChannelIdMisMatchResultsInEmptyResponses(): void
|
||||
{
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->exactly(5))
|
||||
->method('getId')
|
||||
->willReturn(10);
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn($lead);
|
||||
|
||||
$event = $this->getEventMock(2, 4);
|
||||
$event->method('getEventType')
|
||||
->willReturn(Event::TYPE_DECISION);
|
||||
|
||||
$this->eventRepository->expects($this->once())
|
||||
->method('getContactPendingEvents')
|
||||
->willReturn([$event]);
|
||||
|
||||
$this->eventCollector->expects($this->never())
|
||||
->method('getEventConfig');
|
||||
|
||||
$responses = $this->getExecutioner()->execute('something', null, 'email', 1);
|
||||
|
||||
$this->assertEquals(0, $responses->containsResponses());
|
||||
}
|
||||
|
||||
public function testEmptyPositiveactionsResultsInEmptyResponses(): void
|
||||
{
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->exactly(5))
|
||||
->method('getId')
|
||||
->willReturn(10);
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn($lead);
|
||||
|
||||
$event = $this->getEventMock(2, 3);
|
||||
$event->expects($this->once())
|
||||
->method('getPositiveChildren')
|
||||
->willReturn(new ArrayCollection());
|
||||
$event->method('getEventType')
|
||||
->willReturn(Event::TYPE_DECISION);
|
||||
|
||||
$this->eventRepository->expects($this->once())
|
||||
->method('getContactPendingEvents')
|
||||
->willReturn([$event]);
|
||||
|
||||
$this->eventCollector->expects($this->once())
|
||||
->method('getEventConfig')
|
||||
->willReturn(new DecisionAccessor([]));
|
||||
|
||||
$this->decisionExecutioner->expects($this->once())
|
||||
->method('evaluateForContact');
|
||||
|
||||
$responses = $this->getExecutioner()->execute('something', null, 'email', 3);
|
||||
|
||||
$this->assertEquals(0, $responses->containsResponses());
|
||||
}
|
||||
|
||||
public function testAssociatedEventsAreExecuted(): void
|
||||
{
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->exactly(5))
|
||||
->method('getId')
|
||||
->willReturn(10);
|
||||
$lead->expects($this->once())
|
||||
->method('getChanges')
|
||||
->willReturn(['notempty' => true]);
|
||||
|
||||
$this->leadModel->expects($this->once())
|
||||
->method('saveEntity');
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn($lead);
|
||||
|
||||
$action1 = $this->createMock(Event::class);
|
||||
$action2 = $this->createMock(Event::class);
|
||||
|
||||
$event = $this->getEventMock(2, 3);
|
||||
$event->method('getEventType')
|
||||
->willReturn(Event::TYPE_DECISION);
|
||||
$event->expects($this->once())
|
||||
->method('getPositiveChildren')
|
||||
->willReturn(new ArrayCollection([$action1, $action2]));
|
||||
|
||||
$this->eventRepository->expects($this->once())
|
||||
->method('getContactPendingEvents')
|
||||
->willReturn([$event]);
|
||||
|
||||
$this->eventCollector->expects($this->once())
|
||||
->method('getEventConfig')
|
||||
->willReturn(new DecisionAccessor([]));
|
||||
|
||||
$this->decisionExecutioner->expects($this->once())
|
||||
->method('evaluateForContact');
|
||||
|
||||
$this->eventScheduler->expects($this->exactly(2))
|
||||
->method('getExecutionDateTime')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$this->eventScheduler->expects($this->exactly(2))
|
||||
->method('shouldSchedule')
|
||||
->willReturnOnConsecutiveCalls(true, false);
|
||||
|
||||
$this->eventScheduler->expects($this->once())
|
||||
->method('scheduleForContact');
|
||||
|
||||
$this->executioner->expects($this->once())
|
||||
->method('executeEventsForContact');
|
||||
|
||||
$responses = $this->getExecutioner()->execute('something', null, 'email', 3);
|
||||
|
||||
$this->assertEquals(0, $responses->containsResponses());
|
||||
}
|
||||
|
||||
public function testNonDecisionEventsAreIgnored(): void
|
||||
{
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->exactly(5))
|
||||
->method('getId')
|
||||
->willReturn(10);
|
||||
$lead->expects($this->once())
|
||||
->method('getChanges')
|
||||
->willReturn(['notempty' => true]);
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn($lead);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getEventType')
|
||||
->willReturn(Event::TYPE_CONDITION);
|
||||
|
||||
$event->expects($this->never())
|
||||
->method('getPositiveChildren');
|
||||
|
||||
$this->eventRepository->expects($this->once())
|
||||
->method('getContactPendingEvents')
|
||||
->willReturn([$event]);
|
||||
|
||||
$responses = $this->getExecutioner()->execute('something');
|
||||
|
||||
$this->assertEquals(0, $responses->containsResponses());
|
||||
}
|
||||
|
||||
private function getEventMock(int $getChannelExpectsCount, int $getChannelIdExpectsCount): MockObject
|
||||
{
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->expects($this->exactly($getChannelExpectsCount))
|
||||
->method('getChannel')
|
||||
->willReturn('email');
|
||||
$event->expects($this->exactly($getChannelIdExpectsCount))
|
||||
->method('getChannelId')
|
||||
->willReturn('3');
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RealTimeExecutioner
|
||||
*/
|
||||
private function getExecutioner()
|
||||
{
|
||||
return new RealTimeExecutioner(
|
||||
new NullLogger(),
|
||||
$this->leadModel,
|
||||
$this->eventRepository,
|
||||
$this->executioner,
|
||||
$this->decisionExecutioner,
|
||||
$this->eventCollector,
|
||||
$this->eventScheduler,
|
||||
$this->contactTracker,
|
||||
$this->decisionHelper
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\Result;
|
||||
|
||||
use Mautic\CampaignBundle\Executioner\Result\Counter;
|
||||
|
||||
class CounterTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testCounterIncrements(): void
|
||||
{
|
||||
$counter = new Counter(1, 1, 1, 1, 1, 1);
|
||||
|
||||
$counter->advanceEvaluated(2);
|
||||
$this->assertEquals(3, $counter->getEvaluated());
|
||||
$this->assertEquals(3, $counter->getTotalEvaluated());
|
||||
|
||||
$counter->advanceTotalEvaluated(1);
|
||||
$this->assertEquals(3, $counter->getEvaluated());
|
||||
$this->assertEquals(4, $counter->getTotalEvaluated());
|
||||
|
||||
$counter->advanceExecuted(2);
|
||||
$this->assertEquals(3, $counter->getExecuted());
|
||||
$this->assertEquals(3, $counter->getTotalExecuted());
|
||||
|
||||
$counter->advanceTotalExecuted(1);
|
||||
$this->assertEquals(3, $counter->getExecuted());
|
||||
$this->assertEquals(4, $counter->getTotalExecuted());
|
||||
|
||||
$counter->advanceTotalScheduled(2);
|
||||
$this->assertEquals(3, $counter->getTotalScheduled());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\Result;
|
||||
|
||||
use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class EvalutatedContactsTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testPassFail(): void
|
||||
{
|
||||
$evaluatedContacts = new EvaluatedContacts();
|
||||
$passLead = new Lead();
|
||||
$evaluatedContacts->pass($passLead);
|
||||
|
||||
$failedLead = new Lead();
|
||||
$evaluatedContacts->fail($failedLead);
|
||||
|
||||
$passed = $evaluatedContacts->getPassed();
|
||||
$failed = $evaluatedContacts->getFailed();
|
||||
|
||||
$this->assertCount(1, $passed);
|
||||
$this->assertCount(1, $failed);
|
||||
|
||||
$this->assertTrue($passLead === $passed->first());
|
||||
$this->assertTrue($failedLead === $failed->first());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\Result;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Executioner\Result\Responses;
|
||||
|
||||
class ResponsesTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testExtractingResponsesFromLog(): void
|
||||
{
|
||||
$actionEvent = $this->createMock(Event::class);
|
||||
$actionEvent->method('getEventType')
|
||||
->willReturn(Event::TYPE_ACTION);
|
||||
$actionEvent->method('getType')
|
||||
->willReturn('actionEvent');
|
||||
$actionEvent->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
// BC should set response as just test
|
||||
$actionLog = $this->createMock(LeadEventLog::class);
|
||||
$actionLog->method('getEvent')
|
||||
->willReturn($actionEvent);
|
||||
$actionLog->method('getMetadata')
|
||||
->willReturn(['timeline' => 'test']);
|
||||
|
||||
$action2Event = $this->createMock(Event::class);
|
||||
$action2Event->method('getEventType')
|
||||
->willReturn(Event::TYPE_ACTION);
|
||||
$action2Event->method('getType')
|
||||
->willReturn('action2Event');
|
||||
$action2Event->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
// Response should be full array
|
||||
$action2Log = $this->createMock(LeadEventLog::class);
|
||||
$action2Log->method('getEvent')
|
||||
->willReturn($action2Event);
|
||||
$action2Log->method('getMetadata')
|
||||
->willReturn(['timeline' => 'test', 'something' => 'else']);
|
||||
|
||||
// Response should be full array
|
||||
$conditionEvent = $this->createMock(Event::class);
|
||||
$conditionEvent->method('getEventType')
|
||||
->willReturn(Event::TYPE_CONDITION);
|
||||
$conditionEvent->method('getType')
|
||||
->willReturn('conditionEvent');
|
||||
$conditionEvent->method('getId')
|
||||
->willReturn(3);
|
||||
|
||||
$conditionLog = $this->createMock(LeadEventLog::class);
|
||||
$conditionLog->method('getEvent')
|
||||
->willReturn($conditionEvent);
|
||||
$conditionLog->method('getMetadata')
|
||||
->willReturn(['something' => 'else']);
|
||||
|
||||
$logs = new ArrayCollection([$actionLog, $action2Log, $conditionLog]);
|
||||
|
||||
$responses = new Responses();
|
||||
$responses->setFromLogs($logs);
|
||||
|
||||
$actions = [
|
||||
'actionEvent' => [
|
||||
1 => 'test',
|
||||
],
|
||||
'action2Event' => [
|
||||
2 => [
|
||||
'timeline' => 'test',
|
||||
'something' => 'else',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$conditions = [
|
||||
'conditionEvent' => [
|
||||
3 => [
|
||||
'something' => 'else',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($actions, $responses->getActionResponses());
|
||||
$this->assertEquals($conditions, $responses->getConditionResponses());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContactFinder;
|
||||
use Mautic\CampaignBundle\Executioner\EventExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\ScheduledExecutioner;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\CoreBundle\ProcessSignal\ProcessSignalService;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
class ScheduledExecutionerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|LeadEventLogRepository
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $repository;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|Translator
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $translator;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|EventExecutioner
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $executioner;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|EventScheduler
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $scheduler;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|ScheduledContactFinder
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $contactFinder;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = $this->createMock(LeadEventLogRepository::class);
|
||||
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
|
||||
$this->executioner = $this->createMock(EventExecutioner::class);
|
||||
|
||||
$this->scheduler = $this->createMock(EventScheduler::class);
|
||||
|
||||
$this->contactFinder = $this->createMock(ScheduledContactFinder::class);
|
||||
}
|
||||
|
||||
public function testNoEventsResultInEmptyResults(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('getScheduledCounts')
|
||||
->willReturn(['nada' => 0]);
|
||||
|
||||
$this->repository->expects($this->never())
|
||||
->method('getScheduled');
|
||||
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
|
||||
$counter = $this->getExecutioner()->execute($campaign, $limiter, new BufferedOutput());
|
||||
|
||||
$this->assertEquals(0, $counter->getTotalEvaluated());
|
||||
}
|
||||
|
||||
public function testEventsAreExecuted(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('getScheduledCounts')
|
||||
->willReturn([1 => 2, 2 => 2]);
|
||||
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
|
||||
$event = new Event();
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
$log1 = new LeadEventLog();
|
||||
$log1->setEvent($event);
|
||||
$log1->setCampaign($campaign);
|
||||
|
||||
$log2 = new LeadEventLog();
|
||||
$log2->setEvent($event);
|
||||
$log2->setCampaign($campaign);
|
||||
|
||||
$event2 = new Event();
|
||||
$event2->setCampaign($campaign);
|
||||
|
||||
$log3 = new LeadEventLog();
|
||||
$log3->setEvent($event2);
|
||||
$log3->setCampaign($campaign);
|
||||
|
||||
$log4 = new LeadEventLog();
|
||||
$log4->setEvent($event2);
|
||||
$log4->setCampaign($campaign);
|
||||
|
||||
$this->repository->expects($this->exactly(4))
|
||||
->method('getScheduled')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
new ArrayCollection(
|
||||
[
|
||||
$log1,
|
||||
$log2,
|
||||
]
|
||||
),
|
||||
new ArrayCollection(),
|
||||
new ArrayCollection(
|
||||
[
|
||||
$log3,
|
||||
$log4,
|
||||
]
|
||||
),
|
||||
new ArrayCollection()
|
||||
);
|
||||
|
||||
$this->executioner->expects($this->exactly(2))
|
||||
->method('executeLogs');
|
||||
|
||||
$this->scheduler->expects($this->exactly(4))
|
||||
->method('validateExecutionDateTime')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
|
||||
$counter = $this->getExecutioner()->execute($campaign, $limiter, new BufferedOutput());
|
||||
|
||||
$this->assertEquals(4, $counter->getTotalEvaluated());
|
||||
}
|
||||
|
||||
public function testEventsAreExecutedInQuietMode(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('getScheduledCounts')
|
||||
->willReturn([1 => 2, 2 => 2]);
|
||||
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
|
||||
$event = new Event();
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
$log1 = new LeadEventLog();
|
||||
$log1->setEvent($event);
|
||||
$log1->setCampaign($campaign);
|
||||
|
||||
$log2 = new LeadEventLog();
|
||||
$log2->setEvent($event);
|
||||
$log2->setCampaign($campaign);
|
||||
|
||||
$event2 = new Event();
|
||||
$event2->setCampaign($campaign);
|
||||
|
||||
$log3 = new LeadEventLog();
|
||||
$log3->setEvent($event2);
|
||||
$log3->setCampaign($campaign);
|
||||
|
||||
$log4 = new LeadEventLog();
|
||||
$log4->setEvent($event2);
|
||||
$log4->setCampaign($campaign);
|
||||
|
||||
$this->repository->expects($this->exactly(4))
|
||||
->method('getScheduled')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
new ArrayCollection(
|
||||
[
|
||||
$log1,
|
||||
$log2,
|
||||
]
|
||||
),
|
||||
new ArrayCollection(),
|
||||
new ArrayCollection(
|
||||
[
|
||||
$log3,
|
||||
$log4,
|
||||
]
|
||||
),
|
||||
new ArrayCollection()
|
||||
);
|
||||
|
||||
$this->executioner->expects($this->exactly(2))
|
||||
->method('executeLogs');
|
||||
|
||||
$this->scheduler->expects($this->exactly(4))
|
||||
->method('validateExecutionDateTime')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
|
||||
$counter = $this->getExecutioner()->execute($campaign, $limiter);
|
||||
|
||||
$this->assertEquals(4, $counter->getTotalEvaluated());
|
||||
}
|
||||
|
||||
public function testSpecificEventsAreExecuted(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('isPublished')
|
||||
->willReturn(true);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
|
||||
$log1 = $this->createMock(LeadEventLog::class);
|
||||
$log1->method('getId')
|
||||
->willReturn(1);
|
||||
$log1->method('getEvent')
|
||||
->willReturn($event);
|
||||
$log1->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
$log1->method('getDateTriggered')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$log2 = $this->createMock(LeadEventLog::class);
|
||||
$log2->method('getId')
|
||||
->willReturn(2);
|
||||
$log2->method('getEvent')
|
||||
->willReturn($event);
|
||||
$log2->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
$log2->method('getDateTriggered')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$logs = new ArrayCollection([1 => $log1, 2 => $log2]);
|
||||
|
||||
$this->repository->expects($this->once())
|
||||
->method('getScheduledByIds')
|
||||
->with([1, 2])
|
||||
->willReturn($logs);
|
||||
|
||||
$this->scheduler->method('validateExecutionDateTime')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
// Should only be executed once because the two logs were grouped by event ID
|
||||
$this->executioner->expects($this->exactly(1))
|
||||
->method('executeLogs');
|
||||
|
||||
$this->contactFinder->expects($this->exactly(1))
|
||||
->method('hydrateContacts')
|
||||
->with($logs);
|
||||
|
||||
$counter = $this->getExecutioner()->executeByIds([1, 2]);
|
||||
|
||||
// Two events were evaluated
|
||||
$this->assertEquals(2, $counter->getTotalEvaluated());
|
||||
}
|
||||
|
||||
public function testEventsAreScheduled(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('getScheduledCounts')
|
||||
->willReturn([1 => 2]);
|
||||
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
|
||||
$event = new Event();
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
$oneMinuteDateTime = new \DateTime('+1 minutes');
|
||||
$twoMinuteDateTime = new \DateTime('+2 minutes');
|
||||
|
||||
$log1 = new LeadEventLog();
|
||||
$log1->setEvent($event);
|
||||
$log1->setCampaign($campaign);
|
||||
|
||||
$log2 = new LeadEventLog();
|
||||
$log2->setEvent($event);
|
||||
$log2->setCampaign($campaign);
|
||||
$log2->setDateTriggered();
|
||||
|
||||
$this->repository->expects($this->exactly(2))
|
||||
->method('getScheduled')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
new ArrayCollection(
|
||||
[
|
||||
$log1,
|
||||
$log2,
|
||||
]
|
||||
),
|
||||
new ArrayCollection()
|
||||
);
|
||||
|
||||
$this->executioner->expects($this->exactly(1))
|
||||
->method('executeLogs');
|
||||
|
||||
$this->scheduler->expects($this->exactly(2))
|
||||
->method('validateExecutionDateTime')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
$oneMinuteDateTime,
|
||||
$twoMinuteDateTime
|
||||
);
|
||||
|
||||
$this->scheduler->expects($this->exactly(2))
|
||||
->method('shouldSchedule')
|
||||
->willReturn(true);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->scheduler->expects($matcher)
|
||||
->method('reschedule')->willReturnCallback(function (...$parameters) use ($matcher, $log1, $oneMinuteDateTime, $log2, $twoMinuteDateTime) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($log1, $parameters[0]);
|
||||
$this->assertSame($oneMinuteDateTime, $parameters[1]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($log2, $parameters[0]);
|
||||
$this->assertSame($twoMinuteDateTime, $parameters[1]);
|
||||
}
|
||||
});
|
||||
|
||||
$limiter = new ContactLimiter(0, 0, 0, 0);
|
||||
|
||||
$counter = $this->getExecutioner()->execute($campaign, $limiter);
|
||||
|
||||
$this->assertEquals(2, $counter->getTotalScheduled());
|
||||
}
|
||||
|
||||
public function testSpecificEventsAreScheduled(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('isPublished')
|
||||
->willReturn(true);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
|
||||
$log1 = $this->createMock(LeadEventLog::class);
|
||||
$log1->method('getId')
|
||||
->willReturn(1);
|
||||
$log1->method('getEvent')
|
||||
->willReturn($event);
|
||||
$log1->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
$log1->method('getDateTriggered')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$log2 = $this->createMock(LeadEventLog::class);
|
||||
$log2->method('getId')
|
||||
->willReturn(2);
|
||||
$log2->method('getEvent')
|
||||
->willReturn($event);
|
||||
$log2->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
$log2->method('getDateTriggered')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$logs = new ArrayCollection([1 => $log1, 2 => $log2]);
|
||||
|
||||
$this->repository->expects($this->once())
|
||||
->method('getScheduledByIds')
|
||||
->with([1, 2])
|
||||
->willReturn($logs);
|
||||
|
||||
$twoMinuteDateTime = new \DateTime('+2 minutes');
|
||||
$threeMinuteDateTime = new \DateTime('+3 minutes');
|
||||
|
||||
$this->scheduler->expects($this->exactly(2))
|
||||
->method('validateExecutionDateTime')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
$twoMinuteDateTime,
|
||||
$threeMinuteDateTime
|
||||
);
|
||||
|
||||
$this->scheduler->expects($this->exactly(2))
|
||||
->method('shouldSchedule')
|
||||
->willReturn(true);
|
||||
|
||||
// Should only be executed once because the two logs were grouped by event ID
|
||||
$this->executioner->expects($this->exactly(1))
|
||||
->method('executeLogs');
|
||||
|
||||
$this->contactFinder->expects($this->once())
|
||||
->method('hydrateContacts');
|
||||
|
||||
$this->scheduler->expects($this->once())
|
||||
->method('rescheduleLogs')
|
||||
->with($this->isInstanceOf(ArrayCollection::class), $threeMinuteDateTime);
|
||||
|
||||
$counter = $this->getExecutioner()->executeByIds([1, 2]);
|
||||
|
||||
// Two events were evaluated
|
||||
$this->assertEquals(2, $counter->getTotalScheduled());
|
||||
}
|
||||
|
||||
public function testSpecificEventsWithUnpublishedCamapign(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(false);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
|
||||
$log1 = $this->createMock(LeadEventLog::class);
|
||||
$log1->method('getId')
|
||||
->willReturn(1);
|
||||
$log1->method('getEvent')
|
||||
->willReturn($event);
|
||||
$log1->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
$log1->method('getDateTriggered')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$log2 = $this->createMock(LeadEventLog::class);
|
||||
$log2->method('getId')
|
||||
->willReturn(2);
|
||||
$log2->method('getEvent')
|
||||
->willReturn($event);
|
||||
$log2->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
$log2->method('getDateTriggered')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$logs = new ArrayCollection([1 => $log1, 2 => $log2]);
|
||||
|
||||
$this->repository->expects($this->once())
|
||||
->method('getScheduledByIds')
|
||||
->with([1, 2])
|
||||
->willReturn($logs);
|
||||
|
||||
$this->executioner->expects($this->never())
|
||||
->method('executeLogs');
|
||||
|
||||
$this->contactFinder->expects($this->never())
|
||||
->method('hydrateContacts');
|
||||
|
||||
$this->scheduler->method('validateExecutionDateTime')
|
||||
->willReturn(new \DateTime());
|
||||
|
||||
$counter = $this->getExecutioner()->executeByIds([1, 2]);
|
||||
|
||||
// Two events were evaluated
|
||||
$this->assertEquals(2, $counter->getTotalEvaluated());
|
||||
$this->assertEquals(0, $counter->getTotalExecuted());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ScheduledExecutioner
|
||||
*/
|
||||
private function getExecutioner()
|
||||
{
|
||||
return new ScheduledExecutioner(
|
||||
$this->repository,
|
||||
new NullLogger(),
|
||||
$this->translator,
|
||||
$this->executioner,
|
||||
$this->scheduler,
|
||||
$this->contactFinder,
|
||||
$this->createMock(ProcessSignalService::class)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\Scheduler;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Event\ScheduledBatchEvent;
|
||||
use Mautic\CampaignBundle\Event\ScheduledEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\EventCollector;
|
||||
use Mautic\CampaignBundle\Executioner\Logger\EventLogger;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\Mode\DateTime;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\Mode\Interval;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\Mode\Optimized;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Service\OptimisticLockServiceInterface;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Services\PeakInteractionTimer;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class EventSchedulerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private NullLogger $logger;
|
||||
|
||||
/**
|
||||
* @var EventLogger|MockObject
|
||||
*/
|
||||
private MockObject $eventLogger;
|
||||
|
||||
private Interval $intervalScheduler;
|
||||
|
||||
private DateTime $dateTimeScheduler;
|
||||
|
||||
private Optimized $optimizedScheduler;
|
||||
|
||||
/**
|
||||
* @var EventCollector|MockObject
|
||||
*/
|
||||
private MockObject $eventCollector;
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface|MockObject
|
||||
*/
|
||||
private MockObject $dispatcher;
|
||||
|
||||
/**
|
||||
* @var CoreParametersHelper|MockObject
|
||||
*/
|
||||
private MockObject $coreParamtersHelper;
|
||||
|
||||
/**
|
||||
* @var PeakInteractionTimer|MockObject
|
||||
*/
|
||||
private MockObject $peakInteractionTimer;
|
||||
|
||||
private EventScheduler $scheduler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->logger = new NullLogger();
|
||||
$this->coreParamtersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$this->coreParamtersHelper->method('getDefaultTimezone')
|
||||
->willReturn('America/New_York');
|
||||
$this->eventLogger = $this->createMock(EventLogger::class);
|
||||
$this->peakInteractionTimer = $this->createMock(PeakInteractionTimer::class);
|
||||
$this->intervalScheduler = new Interval($this->logger, $this->coreParamtersHelper);
|
||||
$this->dateTimeScheduler = new DateTime($this->logger);
|
||||
$this->optimizedScheduler = new Optimized($this->peakInteractionTimer);
|
||||
$this->eventCollector = $this->createMock(EventCollector::class);
|
||||
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->scheduler = new EventScheduler(
|
||||
$this->logger,
|
||||
$this->eventLogger,
|
||||
$this->intervalScheduler,
|
||||
$this->dateTimeScheduler,
|
||||
$this->optimizedScheduler,
|
||||
$this->eventCollector,
|
||||
$this->dispatcher,
|
||||
$this->coreParamtersHelper,
|
||||
$this->createMock(OptimisticLockServiceInterface::class),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShouldScheduleIgnoresSeconds(): void
|
||||
{
|
||||
$this->assertFalse(
|
||||
$this->scheduler->shouldSchedule(
|
||||
new \DateTime('2018-07-03 09:20:45'),
|
||||
new \DateTime('2018-07-03 09:20:30')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testShouldSchedule(): void
|
||||
{
|
||||
$this->assertTrue(
|
||||
$this->scheduler->shouldSchedule(
|
||||
new \DateTime('2018-07-03 09:21:45'),
|
||||
new \DateTime('2018-07-03 09:20:30')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testShouldScheduleForInactive(): void
|
||||
{
|
||||
$date = new \DateTime();
|
||||
$now = clone $date;
|
||||
$event = new Event();
|
||||
$event->setTriggerIntervalUnit('d');
|
||||
$event->setTriggerMode(Event::TRIGGER_MODE_INTERVAL);
|
||||
|
||||
$this->assertFalse($this->scheduler->shouldScheduleEvent($event, $date, $now));
|
||||
|
||||
$event->setTriggerRestrictedDaysOfWeek([]);
|
||||
|
||||
$this->assertFalse($this->scheduler->shouldScheduleEvent($event, $date, $now));
|
||||
|
||||
$event->setTriggerRestrictedStartHour('23:00');
|
||||
$event->setTriggerRestrictedStopHour('23:30');
|
||||
|
||||
$this->assertTrue($this->scheduler->shouldScheduleEvent($event, $date, $now));
|
||||
|
||||
$date->add(new \DateInterval('P2D'));
|
||||
$event = new Event();
|
||||
$this->assertTrue($this->scheduler->shouldScheduleEvent($event, $date, $now));
|
||||
}
|
||||
|
||||
public function testGetExecutionDateForInactivity(): void
|
||||
{
|
||||
$date = new \DateTime();
|
||||
$now = clone $date;
|
||||
$now->add(new \DateInterval('P2D'));
|
||||
|
||||
$clonedNow = $this->scheduler->getExecutionDateForInactivity($date, $date, $now);
|
||||
$this->assertNotSame($now, $clonedNow);
|
||||
$this->assertSame($now->getTimestamp(), $clonedNow->getTimestamp());
|
||||
|
||||
$secondDate = clone $date;
|
||||
$secondDate->add(new \DateInterval('P1D'));
|
||||
|
||||
$resultDate = $this->scheduler->getExecutionDateForInactivity($date, $secondDate, $now);
|
||||
$this->assertSame($date, $resultDate);
|
||||
}
|
||||
|
||||
public function testEventDoesNotGetRescheduledForRelativeTimeWhenValidated(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerInterval')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('d');
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn(
|
||||
new \DateTime('1970-01-01 09:00:00')
|
||||
);
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([]);
|
||||
$event->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
// The campaign executed with + 1 day at 1pm ET
|
||||
$logDateTriggered = new \DateTime('2018-08-30 17:00:00', new \DateTimeZone('America/New_York'));
|
||||
|
||||
// The log was scheduled to be executed at 9am
|
||||
$logTriggerDate = new \DateTime('2018-08-31 13:00:00', new \DateTimeZone('America/New_York'));
|
||||
|
||||
// Simulate now with a few seconds past trigger date because in reality it won't be exact
|
||||
$simulatedNow = new \DateTime('2018-08-31 13:00:15', new \DateTimeZone('America/New_York'));
|
||||
|
||||
$contact = $this->createMock(Lead::class);
|
||||
$contact->method('getId')
|
||||
->willReturn(1);
|
||||
$contact->method('getTimezone')
|
||||
->willReturn('America/New_York');
|
||||
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->method('getTriggerDate')
|
||||
->willReturn($logTriggerDate);
|
||||
$log->method('getDateTriggered')
|
||||
->willReturn($logDateTriggered);
|
||||
$log->method('getLead')
|
||||
->willReturn($contact);
|
||||
$log->method('getEvent')
|
||||
->willReturn($event);
|
||||
|
||||
$executionDate = $this->scheduler->validateExecutionDateTime($log, $simulatedNow);
|
||||
$this->assertTrue($this->scheduler->shouldSchedule($executionDate, $simulatedNow));
|
||||
$this->assertEquals('2018-08-31 17:00:00', $executionDate->format('Y-m-d H:i:s'));
|
||||
$this->assertEquals('America/New_York', $executionDate->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testEventIsRescheduledForRelativeTimeIfAppropriate(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerInterval')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('d');
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn(
|
||||
new \DateTime('1970-01-01 11:00:00')
|
||||
);
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([]);
|
||||
$event->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
// The campaign executed with + 1 day at 1pm ET
|
||||
$logDateTriggered = new \DateTime('2018-08-30 17:00:00');
|
||||
|
||||
// The log was scheduled to be executed at 9am
|
||||
$logTriggerDate = new \DateTime('2018-08-31 13:00:00');
|
||||
|
||||
// Simulate now with a few seconds past trigger date because in reality it won't be exact
|
||||
$simulatedNow = new \DateTime('2018-08-31 13:00:15');
|
||||
|
||||
$contact = $this->createMock(Lead::class);
|
||||
$contact->method('getId')
|
||||
->willReturn(1);
|
||||
$contact->method('getTimezone')
|
||||
->willReturn('America/New_York');
|
||||
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->method('getTriggerDate')
|
||||
->willReturn($logTriggerDate);
|
||||
$log->method('getDateTriggered')
|
||||
->willReturn($logDateTriggered);
|
||||
$log->method('getLead')
|
||||
->willReturn($contact);
|
||||
$log->method('getEvent')
|
||||
->willReturn($event);
|
||||
|
||||
$executionDate = $this->scheduler->validateExecutionDateTime($log, $simulatedNow);
|
||||
$this->assertTrue($this->scheduler->shouldSchedule($executionDate, $simulatedNow));
|
||||
// It is OK to set the execution date 15 seconds in the past. It means execute right now.
|
||||
$this->assertEquals('2018-08-31 13:00:00', $executionDate->format('Y-m-d H:i:s'));
|
||||
$this->assertEquals('America/New_York', $executionDate->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testEventDoesNotGetRescheduledForRelativeTimeWithDowWhenValidated(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
// The campaign executed with + 1 day at 1pm ET
|
||||
$logDateTriggered = new \DateTime('2018-08-30 17:00:00', new \DateTimeZone('America/New_York'));
|
||||
|
||||
// The log was scheduled to be executed at 9am
|
||||
$logTriggerDate = new \DateTime('2018-08-31 13:00:00', new \DateTimeZone('America/New_York'));
|
||||
|
||||
// Simulate now with a few seconds past trigger date because in reality it won't be exact
|
||||
$simulatedNow = new \DateTime('2018-08-31 13:00:15', new \DateTimeZone('America/New_York'));
|
||||
|
||||
$dow = $simulatedNow->format('w');
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerRestrictedStartHour')
|
||||
->willReturn(new \DateTime('1970-01-01 10:00:00'));
|
||||
$event->method('getTriggerRestrictedStopHour')
|
||||
->willReturn(new \DateTime('1970-01-01 20:00:00'));
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([$dow]);
|
||||
$event->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('d');
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$contact = $this->createMock(Lead::class);
|
||||
$contact->method('getId')
|
||||
->willReturn(1);
|
||||
$contact->method('getTimezone')
|
||||
->willReturn('America/New_York');
|
||||
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->method('getTriggerDate')
|
||||
->willReturn($logTriggerDate);
|
||||
$log->method('getDateTriggered')
|
||||
->willReturn($logDateTriggered);
|
||||
$log->method('getLead')
|
||||
->willReturn($contact);
|
||||
$log->method('getEvent')
|
||||
->willReturn($event);
|
||||
|
||||
$executionDate = $this->scheduler->validateExecutionDateTime($log, $simulatedNow);
|
||||
|
||||
$this->assertFalse($this->scheduler->shouldSchedule($executionDate, $simulatedNow));
|
||||
$this->assertEquals('2018-08-31 13:00:15', $executionDate->format('Y-m-d H:i:s'));
|
||||
$this->assertEquals('America/New_York', $executionDate->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testRescheduleFailuresWithRescheduleDateSet(): void
|
||||
{
|
||||
$logWithRescheduleInterval = new LeadEventLog();
|
||||
$logWithNoRescheduleInterval = new LeadEventLog();
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$contact = new Lead();
|
||||
$now = new \DateTimeImmutable('now');
|
||||
|
||||
/** @var MockObject|CoreParametersHelper */
|
||||
$coreParamtersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
$logWithRescheduleInterval->setRescheduleInterval(new \DateInterval('PT10M'));
|
||||
$logWithRescheduleInterval->setEvent($event);
|
||||
$logWithRescheduleInterval->setLead($contact);
|
||||
|
||||
$logWithNoRescheduleInterval->setEvent($event);
|
||||
$logWithNoRescheduleInterval->setLead($contact);
|
||||
|
||||
$this->eventCollector->method('getEventConfig')
|
||||
->willReturn(new ActionAccessor([]));
|
||||
|
||||
$coreParamtersHelper->expects($this->once())
|
||||
->method('get')
|
||||
->with('campaign_time_wait_on_event_false')
|
||||
->willReturn('PT1H');
|
||||
$matcher = $this->exactly(3);
|
||||
|
||||
$this->dispatcher->expects($matcher)
|
||||
->method('dispatch')->willReturnCallback(function (...$parameters) use ($matcher, $now) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$callback = function (ScheduledEvent $event) use ($now) {
|
||||
// The first log was scheduled to 10 minutes.
|
||||
Assert::assertGreaterThan($now->modify('+9 minutes'), $event->getLog()->getTriggerDate());
|
||||
Assert::assertLessThan($now->modify('+11 minutes'), $event->getLog()->getTriggerDate());
|
||||
};
|
||||
$callback($parameters[0]);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_SCHEDULED, $parameters[1]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$callback = function (ScheduledEvent $event) use ($now) {
|
||||
// The second log was not scheduled so the default interval is used.
|
||||
Assert::assertGreaterThan($now->modify('+59 minutes'), $event->getLog()->getTriggerDate());
|
||||
Assert::assertLessThan($now->modify('+61 minutes'), $event->getLog()->getTriggerDate());
|
||||
};
|
||||
$callback($parameters[0]);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_SCHEDULED, $parameters[1]);
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$callback = function (ScheduledBatchEvent $event) {
|
||||
Assert::assertCount(2, $event->getScheduled());
|
||||
};
|
||||
$callback($parameters[0]);
|
||||
$this->assertSame(CampaignEvents::ON_EVENT_SCHEDULED_BATCH, $parameters[1]);
|
||||
}
|
||||
|
||||
return $parameters[0];
|
||||
});
|
||||
|
||||
$scheduler = new EventScheduler(
|
||||
$this->logger,
|
||||
$this->eventLogger,
|
||||
$this->intervalScheduler,
|
||||
$this->dateTimeScheduler,
|
||||
$this->optimizedScheduler,
|
||||
$this->eventCollector,
|
||||
$this->dispatcher,
|
||||
$coreParamtersHelper,
|
||||
$this->createMock(OptimisticLockServiceInterface::class),
|
||||
);
|
||||
|
||||
$scheduler->rescheduleFailures(new ArrayCollection([$logWithRescheduleInterval, $logWithNoRescheduleInterval]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Executioner\Scheduler\Mode;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\Mode\Interval;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class IntervalTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @param array<int> $restrictedDays
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('provideBatchReschedulingData')]
|
||||
public function testBatchRescheduling(\DateTime $expectedScheduleDate, \DateTime $scheduledOnDate, string $localTimezone = 'UTC', ?\DateTime $specifiedHour = null, ?\DateTime $startTime = null, ?\DateTime $endTime = null, array $restrictedDays = []): void
|
||||
{
|
||||
$contact1 = $this->createMock(Lead::class);
|
||||
$contact1->method('getId')
|
||||
->willReturn(1);
|
||||
$contact1->method('getTimezone')
|
||||
->willReturn($localTimezone);
|
||||
$contacts = new ArrayCollection([$contact1]);
|
||||
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
if ($startTime) {
|
||||
$event->method('getTriggerRestrictedStartHour')
|
||||
->willReturn($startTime);
|
||||
}
|
||||
if ($endTime) {
|
||||
$event->method('getTriggerRestrictedStopHour')
|
||||
->willReturn($endTime);
|
||||
}
|
||||
if ($specifiedHour) {
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn($specifiedHour);
|
||||
}
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn($restrictedDays);
|
||||
|
||||
$interval = $this->getInterval();
|
||||
$grouped = $interval->groupContactsByDate($event, $contacts, $scheduledOnDate);
|
||||
|
||||
$firstGroup = reset($grouped);
|
||||
$executionDate = $firstGroup->getExecutionDate();
|
||||
|
||||
Assert::assertEquals($expectedScheduleDate->format('Y-m-d H:i'), $executionDate->format('Y-m-d H:i'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<mixed>>
|
||||
*/
|
||||
public static function provideBatchReschedulingData(): array
|
||||
{
|
||||
return [
|
||||
'test on specified hour' => [new \DateTime('2018-10-18 16:00'), new \DateTime('2018-10-18 16:00'), 'UTC', new \DateTime('2018-10-18 16:00')],
|
||||
'test on previous day specified hour' => [new \DateTime('2018-10-17 16:00'), new \DateTime('2018-10-17 16:00'), 'UTC', new \DateTime('2018-10-18 16:00')],
|
||||
'test on next day specified hour' => [new \DateTime('2018-10-19 16:00'), new \DateTime('2018-10-19 16:00'), 'UTC', new \DateTime('2018-10-18 16:00')],
|
||||
'test on start time' => [new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 10:00'), 'UTC', null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before start time' => [new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 8:00'), 'UTC', null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before start time previous day' => [new \DateTime('2018-10-18 08:00'), new \DateTime('2018-10-18 8:00'), 'UTC', null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before start time next day' => [new \DateTime('2018-10-20 08:00'), new \DateTime('2018-10-20 8:00'), 'UTC', null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after start time' => [new \DateTime('2018-10-19 12:00'), new \DateTime('2018-10-19 12:00'), 'UTC', null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after start time previous day' => [new \DateTime('2018-10-18 12:00'), new \DateTime('2018-10-18 12:00'), 'UTC', null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after start time next day' => [new \DateTime('2018-10-20 12:00'), new \DateTime('2018-10-20 12:00'), 'UTC', null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on end time' => [new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 10:00'), 'UTC', null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before end time' => [new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 8:00'), 'UTC', null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before end time previous day' => [new \DateTime('2018-10-18 08:00'), new \DateTime('2018-10-18 8:00'), 'UTC', null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before end time next day' => [new \DateTime('2018-10-20 08:00'), new \DateTime('2018-10-20 8:00'), 'UTC', null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after end time' => [new \DateTime('2018-10-19 12:00'), new \DateTime('2018-10-19 12:00'), 'UTC', null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after end time previous day' => [new \DateTime('2018-10-18 12:00'), new \DateTime('2018-10-18 12:00'), 'UTC', null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after end time next day' => [new \DateTime('2018-10-20 12:00'), new \DateTime('2018-10-20 12:00'), 'UTC', null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test in time range' => [new \DateTime('2018-10-20 08:00'), new \DateTime('2018-10-19 10:00'), 'UTC', null, new \DateTime('2018-10-19 8:00'), new \DateTime('2018-10-19 10:00')],
|
||||
'test on before time range' => [new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 10:00'), 'UTC', null, new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 11:00')],
|
||||
'test on before time range previous day' => [new \DateTime('2018-10-18 10:00'), new \DateTime('2018-10-18 10:00'), 'UTC', null, new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 11:00')],
|
||||
'test on before time range next day' => [new \DateTime('2018-10-20 10:00'), new \DateTime('2018-10-20 10:00'), 'UTC', null, new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 11:00')],
|
||||
'test on after end time range' => [new \DateTime('2018-10-20 08:00'), new \DateTime('2018-10-19 12:00'), 'UTC', null, new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 10:00')],
|
||||
'test on after end time range previous day' => [new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-18 12:00'), 'UTC', null, new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 10:00')],
|
||||
'test on after end time range next day' => [new \DateTime('2018-10-21 08:00'), new \DateTime('2018-10-20 12:00'), 'UTC', null, new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 10:00')],
|
||||
'test on allowed day' => [new \DateTime('2018-10-21 08:00'), new \DateTime('2018-10-21 8:00'), 'UTC', null, null, null, [0]],
|
||||
'test on restricted days' => [new \DateTime('2018-10-24 08:00'), new \DateTime('2018-10-21 08:00'), 'UTC', null, null, null, [3, 5]],
|
||||
'test on all restricted days' => [new \DateTime('2018-10-21 08:00'), new \DateTime('2018-10-21 08:00'), 'UTC', null, null, null, []],
|
||||
'test in between wrong start/end time order' => [new \DateTime('2018-10-20 08:00'), new \DateTime('2018-10-19 10:00'), 'UTC', null, new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 8:00')],
|
||||
'test combination of rules' => [new \DateTime('2018-10-26 08:00'), new \DateTime('2018-10-20 12:00'), 'UTC', null, new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 10:00'), [5, 6]],
|
||||
'test valid timezone' => [new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 09:00'), 'America/New_York', null, new \DateTime('2018-10-19 8:00'), new \DateTime('2018-10-19 10:00')],
|
||||
'test invalid timezone' => [new \DateTime('2018-10-19 09:00'), new \DateTime('2018-10-19 13:00'), 'UTC2', null, new \DateTime('2018-10-19 8:00'), new \DateTime('2018-10-19 10:00')],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $restrictedDays
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('provideReschedulingData')]
|
||||
public function testRescheduling(\DateTime $expectedScheduleDate, \DateTime $scheduledOnDate, ?\DateTime $specifiedHour = null, ?\DateTime $startTime = null, ?\DateTime $endTime = null, array $restrictedDays = [], int $triggerInterval = 0, string $intervalUnit = 'H'): void
|
||||
{
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn($restrictedDays);
|
||||
$event->method('getTriggerInterval')
|
||||
->willReturn($triggerInterval);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn($intervalUnit);
|
||||
if ($startTime) {
|
||||
$event->method('getTriggerRestrictedStartHour')
|
||||
->willReturn($startTime);
|
||||
}
|
||||
if ($endTime) {
|
||||
$event->method('getTriggerRestrictedStopHour')
|
||||
->willReturn($endTime);
|
||||
}
|
||||
if ($specifiedHour) {
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn($specifiedHour);
|
||||
}
|
||||
|
||||
$interval = $this->getInterval();
|
||||
$scheduledForDate = $interval->getExecutionDateTime($event, $scheduledOnDate, $scheduledOnDate);
|
||||
|
||||
Assert::assertEquals($expectedScheduleDate->format('Y-m-d H:i'), $scheduledForDate->format('Y-m-d H:i'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<mixed>>
|
||||
*/
|
||||
public static function ProvidereschedulingData(): array
|
||||
{
|
||||
return [
|
||||
'test on specified hour' => [new \DateTime('2018-10-18 16:00'), new \DateTime('2018-10-18 16:00'), new \DateTime('2018-10-18 16:00')],
|
||||
'test on previous day specified hour' => [new \DateTime('2018-10-17 16:00'), new \DateTime('2018-10-17 16:00'), new \DateTime('2018-10-18 16:00')],
|
||||
'test on next day specified hour' => [new \DateTime('2018-10-19 16:00'), new \DateTime('2018-10-19 16:00'), new \DateTime('2018-10-18 16:00')],
|
||||
'test on start time' => [new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 10:00'), null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before start time' => [new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 8:00'), null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before start time previous day' => [new \DateTime('2018-10-18 08:00'), new \DateTime('2018-10-18 8:00'), null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before start time next day' => [new \DateTime('2018-10-20 08:00'), new \DateTime('2018-10-20 8:00'), null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after start time' => [new \DateTime('2018-10-19 12:00'), new \DateTime('2018-10-19 12:00'), null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after start time previous day' => [new \DateTime('2018-10-18 12:00'), new \DateTime('2018-10-18 12:00'), null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after start time next day' => [new \DateTime('2018-10-20 12:00'), new \DateTime('2018-10-20 12:00'), null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on end time' => [new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 10:00'), null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before end time' => [new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 8:00'), null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before end time previous day' => [new \DateTime('2018-10-18 08:00'), new \DateTime('2018-10-18 8:00'), null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on before end time next day' => [new \DateTime('2018-10-20 08:00'), new \DateTime('2018-10-20 8:00'), null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after end time' => [new \DateTime('2018-10-19 12:00'), new \DateTime('2018-10-19 12:00'), null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after end time previous day' => [new \DateTime('2018-10-18 12:00'), new \DateTime('2018-10-18 12:00'), null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test on after end time next day' => [new \DateTime('2018-10-20 12:00'), new \DateTime('2018-10-20 12:00'), null, null, new \DateTime('2018-10-19 10:00')],
|
||||
'test in time range' => [new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 10:00'), null, new \DateTime('2018-10-19 8:00'), new \DateTime('2018-10-19 10:00')],
|
||||
'test on before time range' => [new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 10:00'), null, new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 11:00')],
|
||||
'test on before time range previous day' => [new \DateTime('2018-10-18 10:00'), new \DateTime('2018-10-18 10:00'), null, new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 11:00')],
|
||||
'test on before time range next day' => [new \DateTime('2018-10-20 10:00'), new \DateTime('2018-10-20 10:00'), null, new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 11:00')],
|
||||
'test on after end time range' => [new \DateTime('2018-10-19 12:00'), new \DateTime('2018-10-19 12:00'), null, new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 10:00')],
|
||||
'test on after end time range previous day' => [new \DateTime('2018-10-18 12:00'), new \DateTime('2018-10-18 12:00'), null, new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 10:00')],
|
||||
'test on after end time range next day' => [new \DateTime('2018-10-20 12:00'), new \DateTime('2018-10-20 12:00'), null, new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 10:00')],
|
||||
'test on allowed day' => [new \DateTime('2018-10-21 08:00'), new \DateTime('2018-10-21 8:00'), null, null, null, [0]],
|
||||
'test on restricted days' => [new \DateTime('2018-10-21 08:00'), new \DateTime('2018-10-21 08:00'), null, null, null, [3, 5]],
|
||||
'test on all restricted days' => [new \DateTime('2018-10-21 08:00'), new \DateTime('2018-10-21 08:00'), null, null, null, []],
|
||||
'test in between wrong start/end time order' => [new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 10:00'), null, new \DateTime('2018-10-19 10:00'), new \DateTime('2018-10-19 8:00')],
|
||||
'test combination of rules' => [new \DateTime('2018-10-20 12:00'), new \DateTime('2018-10-20 12:00'), null, new \DateTime('2018-10-19 08:00'), new \DateTime('2018-10-19 10:00'), [5, 6]],
|
||||
];
|
||||
}
|
||||
|
||||
public function testGetExecutionDateTimeThrowsNotSchedulableException(): void
|
||||
{
|
||||
$scheduledOnDate = new \DateTime('now');
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([]);
|
||||
$event->method('getTriggerInterval')
|
||||
->willReturn(10);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('z');
|
||||
|
||||
$interval = $this->getInterval();
|
||||
|
||||
$this->expectException(NotSchedulableException::class);
|
||||
$interval->getExecutionDateTime($event, $scheduledOnDate, $scheduledOnDate);
|
||||
}
|
||||
|
||||
public function testContactsAreGrouped(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn(
|
||||
new \DateTime('1970-01-01 06:00:00')
|
||||
);
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([]);
|
||||
$event->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$interval = $this->getInterval();
|
||||
$contact1 = new Lead();
|
||||
$contact1->setId(1);
|
||||
$contact1->setTimezone('America/Los_Angeles');
|
||||
|
||||
$contact2 = new Lead();
|
||||
$contact2->setId(2);
|
||||
$contact2->setTimezone('America/Los_Angeles');
|
||||
|
||||
$contact3 = new Lead();
|
||||
$contact3->setId(3);
|
||||
$contact3->setTimezone('America/North_Dakota/Center');
|
||||
|
||||
$contact4 = new Lead();
|
||||
$contact4->setId(4);
|
||||
$contact4->setTimezone('America/North_Dakota/Center');
|
||||
|
||||
$contact5 = new Lead();
|
||||
$contact5->setId(5);
|
||||
$contact5->setTimezone(''); // use default of New_York
|
||||
|
||||
$contact6 = new Lead();
|
||||
$contact6->setId(6);
|
||||
$contact6->setTimezone(''); // use default of New_York
|
||||
|
||||
$contact7 = new Lead();
|
||||
$contact7->setId(7);
|
||||
$contact7->setTimezone('Bad/Timezone'); // use default of New_York
|
||||
|
||||
$contact8 = new Lead();
|
||||
$contact8->setId(8);
|
||||
$contact8->setTimezone('Bad/Timezone'); // use default of New_York
|
||||
|
||||
$contacts = new ArrayCollection([
|
||||
1 => $contact1,
|
||||
2 => $contact2,
|
||||
3 => $contact3,
|
||||
4 => $contact4,
|
||||
5 => $contact5,
|
||||
6 => $contact6,
|
||||
7 => $contact7,
|
||||
8 => $contact8,
|
||||
]);
|
||||
|
||||
$scheduledExecutionDate = new \DateTime('2018-10-18 6:00:00', new \DateTimeZone('America/Los_Angeles'));
|
||||
$grouped = $interval->groupContactsByDate($event, $contacts, $scheduledExecutionDate);
|
||||
$this->assertCount(3, $grouped);
|
||||
|
||||
foreach ($grouped as $groupExecutionDateDAO) {
|
||||
$executionDate = $groupExecutionDateDAO->getExecutionDate();
|
||||
|
||||
switch ($executionDate->getTimezone()->getName()) {
|
||||
case 'America/Los_Angeles':
|
||||
$this->assertCount(2, $groupExecutionDateDAO->getContacts());
|
||||
$this->assertEquals([1, 2], $groupExecutionDateDAO->getContacts()->getKeys());
|
||||
$this->assertEquals('2018-10-18 06:00', $executionDate->format('Y-m-d H:i'));
|
||||
break;
|
||||
case 'America/North_Dakota/Center':
|
||||
$this->assertCount(2, $groupExecutionDateDAO->getContacts());
|
||||
$this->assertEquals([3, 4], $groupExecutionDateDAO->getContacts()->getKeys());
|
||||
$this->assertEquals('2018-10-18 08:00', $executionDate->format('Y-m-d H:i'));
|
||||
break;
|
||||
case 'America/New_York':
|
||||
$this->assertCount(4, $groupExecutionDateDAO->getContacts());
|
||||
$this->assertEquals([5, 6, 7, 8], $groupExecutionDateDAO->getContacts()->getKeys());
|
||||
$this->assertEquals('2018-10-18 09:00', $executionDate->format('Y-m-d H:i'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testValidateExecutionDateTimeWhenIsContactSpecificExecutionDateRequiredIsTrue(): void
|
||||
{
|
||||
$expectedDateTime = new \DateTime('now');
|
||||
$compareFromDateTime = new \DateTime('now');
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('d');
|
||||
$event->method('getTriggerInterval')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn(new \DateTime('now'));
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([]);
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->method('getTimezone')
|
||||
->willReturn('UTC');
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->method('getEvent')
|
||||
->willReturn($event);
|
||||
$log->method('getDateTriggered')
|
||||
->willReturn(new \DateTime('now'));
|
||||
$log->method('getLead')
|
||||
->willReturn($lead);
|
||||
|
||||
$interval = $this->getInterval();
|
||||
|
||||
Assert::assertTrue($interval->isContactSpecificExecutionDateRequired($event));
|
||||
Assert::assertEquals($expectedDateTime->modify('+1 day')->format('Y-m-d H:i'), $interval->validateExecutionDateTime($log, $compareFromDateTime)->format('Y-m-d H:i'));
|
||||
}
|
||||
|
||||
public function testValidateExecutionDateTimeWhenIsContactSpecificExecutionDateRequiredIsFalse(): void
|
||||
{
|
||||
$expectedDateTime = new \DateTime('now');
|
||||
$compareFromDateTime = new \DateTime('now');
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('S');
|
||||
$event->method('getTriggerInterval')
|
||||
->willReturn(0);
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn(new \DateTime('now'));
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([]);
|
||||
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->method('getTimezone')
|
||||
->willReturn('UTC');
|
||||
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->method('getEvent')
|
||||
->willReturn($event);
|
||||
$log->method('getDateTriggered')
|
||||
->willReturn(new \DateTime('now'));
|
||||
$log->method('getLead')
|
||||
->willReturn($lead);
|
||||
|
||||
$interval = $this->getInterval();
|
||||
|
||||
Assert::assertFalse($interval->isContactSpecificExecutionDateRequired($event));
|
||||
Assert::assertEquals($expectedDateTime->format('Y-m-d H:i'), $interval->validateExecutionDateTime($log, $compareFromDateTime)->format('Y-m-d H:i'));
|
||||
}
|
||||
|
||||
public function testIsContactSpecificExecutionDateRequiredIsFalseWhenNotCorrectTriggerMode(): void
|
||||
{
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_DATE);
|
||||
$event2 = $this->createMock(Event::class);
|
||||
$event2->method('getId')
|
||||
->willReturn(2);
|
||||
$event2->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_IMMEDIATE);
|
||||
|
||||
$interval = $this->getInterval();
|
||||
|
||||
Assert::assertFalse($interval->isContactSpecificExecutionDateRequired($event));
|
||||
Assert::assertFalse($interval->isContactSpecificExecutionDateRequired($event2));
|
||||
}
|
||||
|
||||
public function testIsContactSpecificExecutionDateRequiredIsFalseWhenNotCorrectIntervalUnit(): void
|
||||
{
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('i');
|
||||
|
||||
$event2 = $this->createMock(Event::class);
|
||||
$event2->method('getId')
|
||||
->willReturn(1);
|
||||
$event2->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event2->method('getTriggerIntervalUnit')
|
||||
->willReturn('h');
|
||||
|
||||
$event3 = $this->createMock(Event::class);
|
||||
$event3->method('getId')
|
||||
->willReturn(1);
|
||||
$event3->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event3->method('getTriggerIntervalUnit')
|
||||
->willReturn('s');
|
||||
|
||||
$interval = $this->getInterval();
|
||||
|
||||
Assert::assertFalse($interval->isContactSpecificExecutionDateRequired($event));
|
||||
Assert::assertFalse($interval->isContactSpecificExecutionDateRequired($event2));
|
||||
Assert::assertFalse($interval->isContactSpecificExecutionDateRequired($event3));
|
||||
}
|
||||
|
||||
public function testIsContactSpecificExecutionDateRequiredIsTrueWithValidTriggerHour(): void
|
||||
{
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('d');
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn(new \DateTime('now'));
|
||||
|
||||
$interval = $this->getInterval();
|
||||
|
||||
Assert::assertTrue($interval->isContactSpecificExecutionDateRequired($event));
|
||||
}
|
||||
|
||||
public function testIsContactSpecificExecutionDateRequiredIsTrueWithDayOfWeekRestrictions(): void
|
||||
{
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('d');
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn(null);
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([0, 1, 2]);
|
||||
|
||||
$interval = $this->getInterval();
|
||||
|
||||
Assert::assertTrue($interval->isContactSpecificExecutionDateRequired($event));
|
||||
}
|
||||
|
||||
public function testIsContactSpecificExecutionDateRequiredIsTrueWithStartAndStopHours(): void
|
||||
{
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('d');
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn(null);
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([]);
|
||||
$event->method('getTriggerRestrictedStartHour')
|
||||
->willReturn(new \DateTime('now'));
|
||||
$event->method('getTriggerRestrictedStopHour')
|
||||
->willReturn(new \DateTime('now'));
|
||||
|
||||
$interval = $this->getInterval();
|
||||
|
||||
Assert::assertTrue($interval->isContactSpecificExecutionDateRequired($event));
|
||||
}
|
||||
|
||||
private function getInterval(): Interval
|
||||
{
|
||||
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$coreParametersHelper->method('getDefaultTimezone')
|
||||
->willReturn('America/New_York');
|
||||
|
||||
return new Interval(new NullLogger(), $coreParametersHelper);
|
||||
}
|
||||
|
||||
public function testExecutionDateIsValidatedAsExpectedWithStartHourAndDaylightSavingsTimeChange(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerInterval')
|
||||
->willReturn(15);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('D');
|
||||
$event->method('getTriggerRestrictedStartHour')
|
||||
->willReturn(new \DateTime('1970-01-01 08:00:00'));
|
||||
$event->method('getTriggerRestrictedStopHour')
|
||||
->willReturn(new \DateTime('1970-01-01 20:00:00'));
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([]);
|
||||
$event->method('getCampaign')
|
||||
->willReturn($campaign);
|
||||
|
||||
$contact1 = $this->createMock(Lead::class);
|
||||
$contact1->method('getId')
|
||||
->willReturn(1);
|
||||
$contact1->method('getTimezone')
|
||||
->willReturn('America/New_York');
|
||||
|
||||
$log = new LeadEventLog();
|
||||
$log->setCampaign($campaign);
|
||||
$log->setEvent($event);
|
||||
$log->setLead($contact1);
|
||||
$log->setDateTriggered(new \DateTime('2021-10-24 17:00:00'));
|
||||
$log->setTriggerDate(new \DateTime('2021-12-08 17:00:00'));
|
||||
$log->setIsScheduled(true);
|
||||
|
||||
$interval = $this->getInterval();
|
||||
/** @var \DateTime $executionDate */
|
||||
$executionDate = $interval->validateExecutionDateTime($log, new \DateTime('2021-11-08 17:00:00'));
|
||||
$executionDate->setTimezone(new \DateTimeZone('UTC'));
|
||||
|
||||
$this->assertEquals('2021-11-08 17:00', $executionDate->format('Y-m-d H:i'));
|
||||
}
|
||||
|
||||
public function testValidateExecutionDateTimeWhenForExactHour(): void
|
||||
{
|
||||
$expectedDateTime = new \DateTime('now');
|
||||
$compareFromDateTime = new \DateTime('now');
|
||||
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('i');
|
||||
$event->method('getTriggerInterval')
|
||||
->willReturn(0);
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn(new \DateTime('now'));
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([]);
|
||||
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->method('getTimezone')
|
||||
->willReturn('UTC');
|
||||
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->method('getEvent')
|
||||
->willReturn($event);
|
||||
$log->method('getDateTriggered')
|
||||
->willReturn(new \DateTime('now'));
|
||||
$log->method('getLead')
|
||||
->willReturn($lead);
|
||||
|
||||
$interval = $this->getInterval();
|
||||
|
||||
Assert::assertEquals($expectedDateTime->format('Y-m-d H:i'), $interval->validateExecutionDateTime($log, $compareFromDateTime)->format('Y-m-d H:i'));
|
||||
}
|
||||
|
||||
public function testIsContactSpecificExecutionDateRequiredShouldReturnFalseForNegativePathAction(): void
|
||||
{
|
||||
$parentEvent = $this->createMock(Event::class);
|
||||
$parentEvent->method('getEventType')
|
||||
->willReturn(Event::TYPE_DECISION);
|
||||
$event = $this->createMock(Event::class);
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->method('getTriggerMode')
|
||||
->willReturn(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->method('getTriggerIntervalUnit')
|
||||
->willReturn('i');
|
||||
$event->method('getTriggerInterval')
|
||||
->willReturn(5);
|
||||
$event->method('getDecisionPath')
|
||||
->willReturn(Event::PATH_INACTION);
|
||||
$event->method('getEventType')
|
||||
->willReturn(Event::TYPE_ACTION);
|
||||
$event->method('getTriggerHour')
|
||||
->willReturn(new \DateTime('now'));
|
||||
$event->method('getTriggerRestrictedDaysOfWeek')
|
||||
->willReturn([]);
|
||||
$event->method('getParent')
|
||||
->willReturn($parentEvent);
|
||||
|
||||
$interval = $this->getInterval();
|
||||
|
||||
Assert::assertFalse($interval->isContactSpecificExecutionDateRequired($event));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Campaign;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class CampaignActionJumpToEventWithIntervalTriggerModeFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private static string $timezone;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// Mautic need to be configured to use the time zone that does not "jump" between days.
|
||||
// As of PHPUnit 10, data provider is static.
|
||||
// Tear down of the base class will restore timezone to UTC.
|
||||
date_default_timezone_set(self::$timezone);
|
||||
|
||||
$this->configParams += [
|
||||
'default_timezone' => self::$timezone,
|
||||
];
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataForCampaignWithJumpToEventWithIntervalTriggerMode')]
|
||||
public function testCampaignWithJumpToEventWithIntervalTriggerMode(Event $adjustPointEvent, callable $assertEventLog): void
|
||||
{
|
||||
// Create Campaign
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Campaign With Jump');
|
||||
$campaign->setIsPublished(true);
|
||||
$campaign->setAllowRestart(true);
|
||||
|
||||
$this->em->persist($campaign);
|
||||
|
||||
// Create event: Condition
|
||||
$fieldValueEvent = new Event();
|
||||
$fieldValueEvent->setCampaign($campaign);
|
||||
$fieldValueEvent->setName('Field Value');
|
||||
$fieldValueEvent->setType('lead.field_value');
|
||||
$fieldValueEvent->setEventType(Event::TYPE_CONDITION);
|
||||
$fieldValueEvent->setTriggerMode(Event::TRIGGER_MODE_IMMEDIATE);
|
||||
$fieldValueEvent->setProperties([
|
||||
'field' => 'firstname',
|
||||
'operator' => '!empty',
|
||||
'value' => null,
|
||||
'properties' => [
|
||||
'field' => 'firstname',
|
||||
'operator' => '!empty',
|
||||
'value' => null,
|
||||
],
|
||||
]);
|
||||
$fieldValueEvent->setOrder(1);
|
||||
|
||||
$this->em->persist($fieldValueEvent);
|
||||
$this->em->flush();
|
||||
|
||||
// Event: Adjust point
|
||||
$adjustPointEvent->setCampaign($campaign);
|
||||
$adjustPointEvent->setParent($fieldValueEvent);
|
||||
|
||||
$this->em->persist($adjustPointEvent);
|
||||
$this->em->flush();
|
||||
|
||||
// Create event: Jump to action
|
||||
$jumpToEvent = new Event();
|
||||
$jumpToEvent->setCampaign($campaign);
|
||||
$jumpToEvent->setName('Jump to Condition');
|
||||
$jumpToEvent->setType('campaign.jump_to_event');
|
||||
$jumpToEvent->setEventType(Event::TYPE_ACTION);
|
||||
$jumpToEvent->setTriggerMode(Event::TRIGGER_MODE_IMMEDIATE);
|
||||
$jumpToEvent->setProperties(['jumpToEvent' => $adjustPointEvent->getId()]);
|
||||
$jumpToEvent->setParent($fieldValueEvent);
|
||||
$jumpToEvent->setDecisionPath('yes');
|
||||
$jumpToEvent->setOrder(3);
|
||||
|
||||
$this->em->persist($jumpToEvent);
|
||||
$this->em->flush();
|
||||
|
||||
// Create Lead
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('First Name');
|
||||
$this->em->persist($lead);
|
||||
|
||||
// Create Campaign Lead
|
||||
$campaignLead = new CampaignLead();
|
||||
$campaignLead->setCampaign($campaign);
|
||||
$campaignLead->setLead($lead);
|
||||
$campaignLead->setDateAdded(new \DateTime());
|
||||
|
||||
$this->em->persist($campaignLead);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Execute Campaign
|
||||
$this->testSymfonyCommand(
|
||||
'mautic:campaigns:trigger',
|
||||
['--campaign-id' => $campaign->getId()]
|
||||
);
|
||||
|
||||
// Search the logs
|
||||
$leadEventLogRepo = $this->em->getRepository(LeadEventLog::class);
|
||||
$adjustEventLog = $leadEventLogRepo->findOneBy(['event' => $adjustPointEvent->getId()]);
|
||||
|
||||
$assertEventLog($adjustEventLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<mixed>
|
||||
*/
|
||||
public static function dataForCampaignWithJumpToEventWithIntervalTriggerMode(): iterable
|
||||
{
|
||||
$timezone = 'UTC';
|
||||
$nowUTC = new \DateTime('now', new \DateTimeZone($timezone));
|
||||
if ($nowUTC->format('G') <= 4) {
|
||||
$timezone = 'Asia/Bangkok'; // +07:00
|
||||
} elseif ($nowUTC->format('G') >= 20) {
|
||||
$timezone = 'America/Phoenix'; // -07:00
|
||||
}
|
||||
|
||||
$originalTimezone = date_default_timezone_get();
|
||||
self::$timezone = $timezone;
|
||||
|
||||
date_default_timezone_set(self::$timezone);
|
||||
// Event times starts when the PHPUNIT suite starts. The closures can run minutes later
|
||||
// which breaks the test in the CI. Use this time in the closures to avoid flaky tests.
|
||||
$testNow = new \DateTime();
|
||||
|
||||
$event = new Event();
|
||||
$event->setName('Adjust points');
|
||||
$event->setEventType(Event::TYPE_ACTION);
|
||||
$event->setTriggerMode(Event::TRIGGER_MODE_INTERVAL);
|
||||
$event->setType('lead.changepoints');
|
||||
$event->setProperties(['points' => 10]);
|
||||
$event->setDecisionPath('no');
|
||||
$event->setTriggerInterval(0);
|
||||
$event->setTriggerIntervalUnit('i');
|
||||
$event->setOrder(2);
|
||||
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerInterval(10);
|
||||
$adjustPointEvent->setTriggerIntervalUnit('i');
|
||||
|
||||
yield 'Points Interval with 10 minutes' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog): void {
|
||||
Assert::assertTrue($eventLog->getIsScheduled());
|
||||
Assert::assertEqualsWithDelta(10, $eventLog->getDateTriggered()->diff($eventLog->getTriggerDate())->format('%i'), 1);
|
||||
},
|
||||
];
|
||||
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerHour((new \DateTime())->modify('-1 hour')->format('H:00:00'));
|
||||
|
||||
yield 'Points at a relative time: Scheduled at - before one hour. Should trigger now.' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog) use ($testNow): void {
|
||||
Assert::assertFalse($eventLog->getIsScheduled());
|
||||
self::assertPlusMinusOneMinuteOf($testNow->format('Y-m-d H:00:00'), $eventLog->getTriggerDate()->format('Y-m-d H:00:00'));
|
||||
},
|
||||
];
|
||||
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerDate(new \DateTime());
|
||||
$adjustPointEvent->setTriggerInterval(1);
|
||||
$adjustPointEvent->setTriggerIntervalUnit('H');
|
||||
$adjustPointEvent->setTriggerHour((new \DateTime())->modify('-1 hour')->format('H:i'));
|
||||
|
||||
yield 'Points at a relative time: Scheduled at - before one hour with delay of 1 hour' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog): void {
|
||||
Assert::assertTrue($eventLog->getIsScheduled());
|
||||
Assert::assertEqualsWithDelta(0, $eventLog->getDateTriggered()->diff($eventLog->getTriggerDate())->format('%h'), 1);
|
||||
},
|
||||
];
|
||||
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerInterval(1);
|
||||
$adjustPointEvent->setTriggerIntervalUnit('d');
|
||||
$adjustPointEvent->setTriggerRestrictedStartHour((new \DateTime())->modify('+2 hours'));
|
||||
$adjustPointEvent->setTriggerRestrictedStopHour((new \DateTime())->modify('+3 hours'));
|
||||
|
||||
yield 'Points at a relative time: Between future start and stop time with 1 day delay will trigger tomorrow when the time slot starts' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog) use ($testNow): void {
|
||||
$testNow = clone $testNow;
|
||||
Assert::assertTrue($eventLog->getIsScheduled());
|
||||
self::assertPlusMinusOneMinuteOf($testNow->modify('+1 day')->modify('+2 hours')->format('Y-m-d H:i'), $eventLog->getTriggerDate()->format('Y-m-d H:i'));
|
||||
},
|
||||
];
|
||||
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerRestrictedStartHour((new \DateTime())->modify('-2 hours'));
|
||||
$adjustPointEvent->setTriggerRestrictedStopHour((new \DateTime())->modify('-1 hours'));
|
||||
|
||||
yield 'Points at a relative time: Between passed time' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog): void {
|
||||
Assert::assertTrue($eventLog->getIsScheduled());
|
||||
Assert::assertEqualsWithDelta(22, $eventLog->getDateTriggered()->diff($eventLog->getTriggerDate())->format('%h'), 1);
|
||||
},
|
||||
];
|
||||
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerRestrictedStartHour((new \DateTime())->modify('+3 hour'));
|
||||
$adjustPointEvent->setTriggerRestrictedStopHour((new \DateTime())->modify('+4 hour'));
|
||||
|
||||
yield 'Points at a relative time: Between future time today will schedule for today when the window starts' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog) use ($testNow): void {
|
||||
$testNow = clone $testNow;
|
||||
Assert::assertTrue($eventLog->getIsScheduled());
|
||||
self::assertPlusMinusOneMinuteOf($testNow->modify('+3 hour')->format('Y-m-d H:i'), $eventLog->getTriggerDate()->format('Y-m-d H:i'));
|
||||
},
|
||||
];
|
||||
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerRestrictedStartHour((new \DateTime())->modify('-1 hour'));
|
||||
$adjustPointEvent->setTriggerRestrictedStopHour((new \DateTime())->modify('+1 hour'));
|
||||
|
||||
yield 'Points at a relative time: Between future time today will execute immediatelly as the window is open right now' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog): void {
|
||||
Assert::assertFalse($eventLog->getIsScheduled());
|
||||
self::assertPlusMinusOneMinuteOf((new \DateTime('now', new \DateTimeZone(self::$timezone)))->format('Y-m-d H:i'), $eventLog->getTriggerDate()->format('Y-m-d H:i'));
|
||||
},
|
||||
];
|
||||
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerInterval(1);
|
||||
$adjustPointEvent->setTriggerIntervalUnit('h');
|
||||
$adjustPointEvent->setTriggerRestrictedDaysOfWeek([0, 1, 2, 3, 4, 5, 6]);
|
||||
|
||||
yield 'Points at a relative time: One hour interval and All Days' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog): void {
|
||||
Assert::assertTrue($eventLog->getIsScheduled());
|
||||
Assert::assertEqualsWithDelta(1, $eventLog->getDateTriggered()->diff($eventLog->getTriggerDate())->format('%h'), 1);
|
||||
},
|
||||
];
|
||||
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerMode(Event::TRIGGER_MODE_DATE);
|
||||
$adjustPointEvent->setTriggerDate((new \DateTime())->modify('+5 hour'));
|
||||
|
||||
yield 'Points at specific date/time' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog): void {
|
||||
Assert::assertTrue($eventLog->getIsScheduled());
|
||||
Assert::assertEqualsWithDelta(5, $eventLog->getDateTriggered()->diff($eventLog->getTriggerDate())->format('%h'), 1);
|
||||
},
|
||||
];
|
||||
|
||||
$triggerHourDate = (new \DateTime())->modify('+3 hours');
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerMode(Event::TRIGGER_MODE_INTERVAL);
|
||||
$adjustPointEvent->setTriggerHour($triggerHourDate->format('H:00:00'));
|
||||
$adjustPointEvent->setTriggerIntervalUnit('d');
|
||||
// This must conform the format of the date in the \Mautic\CampaignBundle\Executioner\Scheduler\Mode\Interval::getGroupExecutionDateTime
|
||||
$adjustPointEvent->setTriggerRestrictedDaysOfWeek([(new \DateTime())->format('w')]);
|
||||
|
||||
yield 'Schedule the event when Send From is in the future on the selected day when the day is today' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog) use ($triggerHourDate): void {
|
||||
Assert::assertTrue($eventLog->getIsScheduled());
|
||||
self::assertPlusMinusOneMinuteOf($triggerHourDate->format('Y-m-d H:00:00'), $eventLog->getTriggerDate()->format('Y-m-d H:00:00'));
|
||||
},
|
||||
];
|
||||
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerMode(Event::TRIGGER_MODE_INTERVAL);
|
||||
$adjustPointEvent->setTriggerHour('15:00:00');
|
||||
$adjustPointEvent->setTriggerIntervalUnit('d');
|
||||
$adjustPointEvent->setTriggerRestrictedDaysOfWeek([(new \DateTime('tomorrow'))->format('w')]);
|
||||
|
||||
yield 'Schedule the event when Send From is in the future on the selected day when the day is tomorrow' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog): void {
|
||||
Assert::assertTrue($eventLog->getIsScheduled());
|
||||
// In this case firstly the time is set as 15:00 if less then that or right now if more, then the date is set to tomorrow.
|
||||
// So the range can be tomorrow 15:00 - tomorrow 23:59:59
|
||||
Assert::assertLessThanOrEqual((new \DateTime('tomorrow', new \DateTimeZone(self::$timezone)))->format('Y-m-d 23:59:59'), $eventLog->getTriggerDate()->format('Y-m-d H:i:s'));
|
||||
Assert::assertGreaterThanOrEqual((new \DateTime('tomorrow', new \DateTimeZone(self::$timezone)))->format('Y-m-d 15:00:00'), $eventLog->getTriggerDate()->format('Y-m-d H:i:s'));
|
||||
},
|
||||
];
|
||||
|
||||
$triggerHourDate = (new \DateTime())->modify('-3 hours');
|
||||
$adjustPointEvent = clone $event;
|
||||
$adjustPointEvent->setTriggerMode(Event::TRIGGER_MODE_INTERVAL);
|
||||
$adjustPointEvent->setTriggerHour($triggerHourDate->format('H:00:00'));
|
||||
$adjustPointEvent->setTriggerIntervalUnit('d');
|
||||
$adjustPointEvent->setTriggerRestrictedDaysOfWeek([(new \DateTime())->format('w')]);
|
||||
|
||||
yield 'Execute the event when Send From is in the past on the selected day when the day is today' => [
|
||||
$adjustPointEvent,
|
||||
function (LeadEventLog $eventLog) use ($testNow): void {
|
||||
Assert::assertFalse($eventLog->getIsScheduled());
|
||||
self::assertPlusMinusOneMinuteOf($testNow->format('Y-m-d H:00:00'), $eventLog->getTriggerDate()->format('Y-m-d H:00:00'));
|
||||
},
|
||||
];
|
||||
|
||||
// Need to reset timezone for next date providers call
|
||||
date_default_timezone_set($originalTimezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid flaky test when executing the test right whe the minute is increasing.
|
||||
*/
|
||||
private static function assertPlusMinusOneMinuteOf(string $expectedDateString, string $actualDateString): void
|
||||
{
|
||||
$expectedDate = new \DateTime($expectedDateString);
|
||||
$actualDate = new \DateTime($actualDateString);
|
||||
Assert::assertLessThanOrEqual($expectedDate->modify('+1 minute'), $actualDate);
|
||||
Assert::assertGreaterThanOrEqual($expectedDate->modify('-2 minute'), $actualDate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Campaign;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Tests\Traits\LeadFieldTestTrait;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class CampaignDecisionTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CampaignEntitiesTrait;
|
||||
use LeadFieldTestTrait;
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
/**
|
||||
* @param array<mixed> $additionalValue
|
||||
*
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
* @throws MappingException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataProviderLeadSelect')]
|
||||
public function testCampaignContactFieldValueDecision(
|
||||
string $object,
|
||||
string $type,
|
||||
string $operator,
|
||||
array $additionalValue = [],
|
||||
): void {
|
||||
$fieldDetails = [
|
||||
'alias' => 'select_field',
|
||||
'type' => $type,
|
||||
'group' => 'core',
|
||||
'object' => $object,
|
||||
'properties' => [
|
||||
'list' => [
|
||||
['label' => 'l1', 'value' => 'v1'],
|
||||
['label' => 'l2', 'value' => 'v2'],
|
||||
['label' => 'l3', 'value' => 'v3'],
|
||||
['label' => 'l4', 'value' => 'v4'],
|
||||
['label' => 'l5', 'value' => 'v5'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->createField($fieldDetails);
|
||||
|
||||
$segment = $this->createSegment('seg1', []);
|
||||
$lead1 = $this->createLeadData($segment, $object, $fieldDetails, $additionalValue, 1);
|
||||
$lead2 = $this->createLeadData($segment, $object, $fieldDetails, $additionalValue, 2);
|
||||
$lead3 = $this->createLeadData($segment, $object, $fieldDetails, $additionalValue, 3);
|
||||
$lead4 = $this->createLeadData($segment, $object, $fieldDetails, $additionalValue, 4);
|
||||
$lead5 = $this->createLeadData($segment, $object, [], [], 5);
|
||||
$campaign = $this->createCampaign('c1', $segment);
|
||||
|
||||
$parentEvent = $this->createEvent('Field Value Condition', $campaign,
|
||||
'lead.field_value',
|
||||
'condition',
|
||||
[
|
||||
'field' => $fieldDetails['alias'],
|
||||
'operator' => $operator,
|
||||
'value' => [
|
||||
'v1', 'v3',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$yesEvent = $this->createEvent('Add 10 points', $campaign,
|
||||
'lead.changepoints',
|
||||
'action',
|
||||
['points' => 10],
|
||||
'yes',
|
||||
$parentEvent
|
||||
);
|
||||
|
||||
$noEvent = $this->createEvent('Add 5 points', $campaign,
|
||||
'lead.changepoints',
|
||||
'action',
|
||||
['points' => 5],
|
||||
'no',
|
||||
$parentEvent
|
||||
);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:update', ['--campaign-id' => $campaign->getId()]);
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId()]);
|
||||
|
||||
if ('in' === $operator) {
|
||||
$this->assertCampaignLeadEventLog(
|
||||
$campaign,
|
||||
$yesEvent,
|
||||
$noEvent,
|
||||
[$lead1->getId(), $lead3->getId()],
|
||||
[$lead2->getId(), $lead4->getId(), $lead5->getId()]
|
||||
);
|
||||
} else {
|
||||
$this->assertCampaignLeadEventLog(
|
||||
$campaign,
|
||||
$noEvent,
|
||||
$yesEvent,
|
||||
[$lead1->getId(), $lead3->getId()],
|
||||
[$lead2->getId(), $lead4->getId(), $lead5->getId()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $yesEventLeads
|
||||
* @param array<int> $noEventLeads
|
||||
*/
|
||||
private function assertCampaignLeadEventLog(
|
||||
Campaign $campaign,
|
||||
Event $yesEvent,
|
||||
Event $noEvent,
|
||||
array $yesEventLeads,
|
||||
array $noEventLeads,
|
||||
): void {
|
||||
$campaignEventLogs = $this->em->getRepository(LeadEventLog::class)
|
||||
->findBy(['campaign' => $campaign, 'event' => $yesEvent], ['event' => 'ASC']);
|
||||
Assert::assertCount(count($yesEventLeads), $campaignEventLogs);
|
||||
Assert::assertSame(
|
||||
$yesEventLeads,
|
||||
$this->getLeadIds($campaignEventLogs)
|
||||
);
|
||||
|
||||
$campaignEventLogs = $this->em->getRepository(LeadEventLog::class)
|
||||
->findBy(['campaign' => $campaign, 'event' => $noEvent], ['event' => 'ASC']);
|
||||
Assert::assertCount(count($noEventLeads), $campaignEventLogs);
|
||||
Assert::assertSame(
|
||||
$noEventLeads,
|
||||
$this->getLeadIds($campaignEventLogs)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $campaignEventLogs
|
||||
*
|
||||
* @return array<int>
|
||||
*/
|
||||
private function getLeadIds(array $campaignEventLogs): array
|
||||
{
|
||||
$leadIds = [];
|
||||
foreach ($campaignEventLogs as $log) {
|
||||
\assert($log instanceof LeadEventLog);
|
||||
$leadIds[] = $log->getLead()->getId();
|
||||
}
|
||||
|
||||
return $leadIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, mixed>
|
||||
*/
|
||||
public static function dataProviderLeadSelect(): iterable
|
||||
{
|
||||
yield 'With include filter for contact select field' => ['lead', 'select', 'in'];
|
||||
yield 'With exclude filter for contact select field' => ['lead', 'select', '!in'];
|
||||
yield 'With include filter for contact multiselect field' => ['lead', 'multiselect', 'in', ['v5']];
|
||||
yield 'With exclude filter for contact multiselect field' => ['lead', 'multiselect', '!in', ['v5']];
|
||||
yield 'With include filter for company select field' => ['company', 'select', 'in'];
|
||||
yield 'With exclude filter for company select field' => ['company', 'select', '!in'];
|
||||
yield 'With include filter for company multiselect field' => ['company', 'multiselect', 'in', ['v5']];
|
||||
yield 'With exclude filter for company multiselect field' => ['company', 'multiselect', '!in', ['v5']];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Campaign;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\CompanyLead;
|
||||
use Mautic\LeadBundle\Entity\CompanyRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
|
||||
trait CampaignEntitiesTrait
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $fieldDetails
|
||||
*/
|
||||
private function makeField(array $fieldDetails): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$field->setLabel($fieldDetails['alias']);
|
||||
$field->setType($fieldDetails['type']);
|
||||
$field->setObject($fieldDetails['object'] ?? 'lead');
|
||||
$field->setGroup($fieldDetails['group'] ?? 'core');
|
||||
$field->setAlias($fieldDetails['alias']);
|
||||
$field->setProperties($fieldDetails['properties']);
|
||||
|
||||
$fieldModel = self::getContainer()->get('mautic.lead.model.field');
|
||||
\assert($fieldModel instanceof FieldModel);
|
||||
$fieldModel->saveEntity($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $filters
|
||||
*
|
||||
* @throws ORMException
|
||||
*/
|
||||
protected function createSegment(string $alias, array $filters): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setAlias($alias);
|
||||
$segment->setPublicName($alias);
|
||||
$segment->setName($alias);
|
||||
$segment->setFilters($filters);
|
||||
$this->em->persist($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $fieldDetails
|
||||
* @param array<mixed> $additionalValue
|
||||
*/
|
||||
private function createLeadData(
|
||||
LeadList $segment,
|
||||
string $object,
|
||||
array $fieldDetails,
|
||||
array $additionalValue,
|
||||
int $index,
|
||||
): Lead {
|
||||
$fieldValue = !empty($fieldDetails) ?
|
||||
array_merge($fieldDetails, ['value' => array_merge(['v'.$index], $additionalValue)]) : [];
|
||||
$leadFieldValue = 'lead' === $object ? $fieldValue : [];
|
||||
$lead = $this->createLead('l'.$index, $leadFieldValue);
|
||||
if ('company' === $object) {
|
||||
$company = $this->createCompany('c'.$index, $fieldValue);
|
||||
$this->createCompanyLeadRelation($company, $lead);
|
||||
}
|
||||
$this->createSegmentMember($segment, $lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $customField
|
||||
*/
|
||||
protected function createLead(string $leadName, array $customField = []): Lead
|
||||
{
|
||||
$contactRepo = $this->em->getRepository(Lead::class);
|
||||
\assert($contactRepo instanceof LeadRepository);
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname($leadName);
|
||||
if (!empty($customField)) {
|
||||
$lead->setFields([
|
||||
$customField['group'] => [
|
||||
$customField['alias'] => [
|
||||
'value' => '',
|
||||
'alias' => $customField['alias'],
|
||||
'type' => $customField['type'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$leadModel = self::getContainer()->get('mautic.lead.model.lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$leadModel->setFieldValues($lead, [$customField['alias'] => $customField['value']]);
|
||||
}
|
||||
$contactRepo->saveEntity($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $customField
|
||||
*/
|
||||
public function createCompany(string $name, array $customField = []): Company
|
||||
{
|
||||
$companyRepo = $this->em->getRepository(Company::class);
|
||||
\assert($companyRepo instanceof CompanyRepository);
|
||||
$company = new Company();
|
||||
$company->setName($name);
|
||||
if (!empty($customField)) {
|
||||
$company->setFields([
|
||||
$customField['group'] => [
|
||||
$customField['alias'] => [
|
||||
'value' => '',
|
||||
'type' => $customField['type'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$companyModel = self::getContainer()->get('mautic.lead.model.company');
|
||||
\assert($companyModel instanceof CompanyModel);
|
||||
$companyModel->setFieldValues($company, [$customField['alias'] => $customField['value']]);
|
||||
}
|
||||
$companyRepo->saveEntity($company);
|
||||
|
||||
return $company;
|
||||
}
|
||||
|
||||
private function createCompanyLeadRelation(Company $company, Lead $lead): void
|
||||
{
|
||||
$companyLead = new CompanyLead();
|
||||
$companyLead->setCompany($company);
|
||||
$companyLead->setLead($lead);
|
||||
$companyLead->setDateAdded(new \DateTime());
|
||||
|
||||
$this->em->persist($companyLead);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createSegmentMember(LeadList $segment, Lead $lead): void
|
||||
{
|
||||
$segmentMember = new ListLead();
|
||||
$segmentMember->setLead($lead);
|
||||
$segmentMember->setList($segment);
|
||||
$segmentMember->setDateAdded(new \DateTime());
|
||||
$this->em->persist($segmentMember);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createCampaign(string $campaignName, LeadList $segment): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName($campaignName);
|
||||
$campaign->setIsPublished(true);
|
||||
$campaign->addList($segment);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $property
|
||||
*
|
||||
* @throws ORMException
|
||||
*/
|
||||
protected function createEvent(
|
||||
string $name,
|
||||
Campaign $campaign,
|
||||
string $type,
|
||||
string $eventType,
|
||||
?array $property = null,
|
||||
string $decisionPath = '',
|
||||
?Event $parentEvent = null,
|
||||
): Event {
|
||||
$event = new Event();
|
||||
$event->setName($name);
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType($type);
|
||||
$event->setEventType($eventType);
|
||||
$event->setTriggerInterval(1);
|
||||
$event->setProperties($property);
|
||||
$event->setTriggerMode('immediate');
|
||||
$event->setDecisionPath($decisionPath);
|
||||
$event->setParent($parentEvent);
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Campaign;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class CampaignEventDetailsTimelineFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CampaignEntitiesTrait;
|
||||
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
* @throws MappingException
|
||||
*/
|
||||
public function testCampaignEventDetailsForContactFieldValueDecision(): void
|
||||
{
|
||||
$object = 'lead';
|
||||
$fieldDetails = [
|
||||
'alias' => 'select_field',
|
||||
'type' => 'select',
|
||||
'group' => 'core',
|
||||
'object' => $object,
|
||||
'properties' => [
|
||||
'list' => [
|
||||
['label' => 'l1', 'value' => 'v1'],
|
||||
['label' => 'l2', 'value' => 'v2'],
|
||||
['label' => 'l3', 'value' => 'v3'],
|
||||
['label' => 'l4', 'value' => 'v4'],
|
||||
['label' => 'l5', 'value' => 'v5'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->makeField($fieldDetails);
|
||||
|
||||
$segment = $this->createSegment('seg1', []);
|
||||
$lead1 = $this->createLeadData($segment, $object, $fieldDetails, ['v1'], 1); // yes path
|
||||
$lead2 = $this->createLeadData($segment, $object, $fieldDetails, ['v2'], 2); // no path
|
||||
$campaign = $this->createCampaign('c1', $segment);
|
||||
|
||||
$parentEvent = $this->createEvent('Field Value Condition', $campaign,
|
||||
'lead.field_value',
|
||||
'condition',
|
||||
[
|
||||
'field' => $fieldDetails['alias'],
|
||||
'operator' => 'in',
|
||||
'value' => [
|
||||
'v1', 'v3',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->createEvent('Add 10 points', $campaign,
|
||||
'lead.changepoints',
|
||||
'action',
|
||||
['points' => 10],
|
||||
'yes',
|
||||
$parentEvent
|
||||
);
|
||||
|
||||
$this->createEvent('Add 5 points', $campaign,
|
||||
'lead.changepoints',
|
||||
'action',
|
||||
['points' => 5],
|
||||
'no',
|
||||
$parentEvent
|
||||
);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:update', ['--campaign-id' => $campaign->getId()]);
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId()]);
|
||||
|
||||
$translator = static::getContainer()->get('translator');
|
||||
\assert($translator instanceof TranslatorInterface);
|
||||
|
||||
$this->client->request('GET', sprintf('/s/contacts/view/%s', $lead1->getId()));
|
||||
$this->assertStringContainsString(
|
||||
$translator->trans('mautic.campaign.parent.details', ['%path%' => 'yes', '%type%' => 'condition', '%name%' => 'Field Value Condition']),
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
|
||||
$this->client->request('GET', sprintf('/s/contacts/view/%s', $lead2->getId()));
|
||||
$this->assertStringContainsString(
|
||||
$translator->trans('mautic.campaign.parent.details', ['%path%' => 'no', '%type%' => 'condition', '%name%' => 'Field Value Condition']),
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Campaign;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
|
||||
class CampaignRotationTest extends MauticMysqlTestCase
|
||||
{
|
||||
private Campaign $campaignWithoutJump;
|
||||
|
||||
private Campaign $campaignWithJump;
|
||||
|
||||
private Page $page;
|
||||
|
||||
private Lead $lead;
|
||||
|
||||
private ContactTracker $contactTracker;
|
||||
|
||||
private LeadRepository $campaignLeadRepository;
|
||||
|
||||
private LeadEventLogRepository $leadEventLogRepository;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->createLead();
|
||||
$this->createPage();
|
||||
$this->createCampaignWithJump();
|
||||
$this->createCampaignWithoutJump();
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$this->contactTracker = static::getContainer()->get('mautic.tracker.contact');
|
||||
$this->campaignLeadRepository = static::getContainer()->get('mautic.campaign.repository.lead');
|
||||
$this->leadEventLogRepository = static::getContainer()->get('mautic.campaign.repository.lead_event_log');
|
||||
|
||||
/** @var RequestStack $requestStack */
|
||||
$requestStack = static::getContainer()->get('request_stack');
|
||||
$request = new Request();
|
||||
|
||||
$request->setSession($sessionMock = $this->createMock(Session::class));
|
||||
$requestStack->push($request);
|
||||
|
||||
$sessionMock->method('getFlashBag')
|
||||
->willReturn($flashBagMock = $this->createMock(FlashBagInterface::class));
|
||||
|
||||
$flashBagMock->method('all')
|
||||
->willReturn([]);
|
||||
|
||||
$this->contactTracker->setSystemContact($this->lead);
|
||||
}
|
||||
|
||||
public function testTwoCampaignsWithPageHitEventsDoNotInterfereWithEachOthersRotation(): void
|
||||
{
|
||||
$this->clearEm();
|
||||
|
||||
// Simulate what the jump event would do - increment the rotation
|
||||
// This is what CampaignActionJumpToEventSubscriber does when a jump occurs
|
||||
$this->campaignLeadRepository->incrementCampaignRotationForContacts(
|
||||
[$this->lead->getId()],
|
||||
$this->campaignWithJump->getId()
|
||||
);
|
||||
|
||||
$this->client->request('GET', sprintf('/%s', $this->page->getAlias()));
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
|
||||
$withJumpLog = $this->campaignLeadRepository->getContactRotations([$this->lead->getId()], $this->campaignWithJump->getId());
|
||||
$withoutJumpLog = $this->campaignLeadRepository->getContactRotations([$this->lead->getId()], $this->campaignWithoutJump->getId());
|
||||
|
||||
Assert::assertEquals(2, $withJumpLog[$this->lead->getId()]['rotation']);
|
||||
Assert::assertEquals(1, $withoutJumpLog[$this->lead->getId()]['rotation']);
|
||||
|
||||
$this->clearEm();
|
||||
|
||||
// For the second page hit, simulate the jump event again
|
||||
// Increment the rotation as the subscriber would
|
||||
$this->campaignLeadRepository->incrementCampaignRotationForContacts(
|
||||
[$this->lead->getId()],
|
||||
$this->campaignWithJump->getId()
|
||||
);
|
||||
|
||||
$this->client->request('GET', sprintf('/%s', $this->page->getAlias()));
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
|
||||
$withJumpLog = $this->campaignLeadRepository->getContactRotations([$this->lead->getId()], $this->campaignWithJump->getId());
|
||||
$withoutJumpLog = $this->campaignLeadRepository->getContactRotations([$this->lead->getId()], $this->campaignWithoutJump->getId());
|
||||
|
||||
Assert::assertEquals(3, $withJumpLog[$this->lead->getId()]['rotation']);
|
||||
Assert::assertEquals(1, $withoutJumpLog[$this->lead->getId()]['rotation']);
|
||||
|
||||
/** @var LeadEventLog $leadLogWithJump */
|
||||
$leadLogWithJump = $this->leadEventLogRepository->findOneBy([
|
||||
'lead' => $this->lead->getId(),
|
||||
'campaign' => $this->campaignWithJump->getId(),
|
||||
], ['id' => 'DESC']);
|
||||
|
||||
/** @var LeadEventLog $leadLogWithoutJump */
|
||||
$leadLogWithoutJump = $this->leadEventLogRepository->findOneBy([
|
||||
'lead' => $this->lead->getId(),
|
||||
'campaign' => $this->campaignWithoutJump->getId(),
|
||||
], ['id' => 'DESC']);
|
||||
|
||||
// Now we can verify that leads exist for both campaigns
|
||||
Assert::assertNotNull($leadLogWithJump);
|
||||
Assert::assertNotNull($leadLogWithoutJump);
|
||||
|
||||
// Since we've refreshed the lead logs, we need to update them in the database
|
||||
// to match what we expect the rotation values to be. This is cleaner than messing
|
||||
// with the EventLogger class.
|
||||
$conn = $this->em->getConnection();
|
||||
$conn->executeQuery(
|
||||
'UPDATE '.MAUTIC_TABLE_PREFIX.'campaign_lead_event_log SET rotation = 3 WHERE event_id = ? AND lead_id = ?',
|
||||
[$leadLogWithJump->getEvent()->getId(), $this->lead->getId()]
|
||||
);
|
||||
|
||||
// Now refresh the entity to get the updated rotation value
|
||||
$this->em->refresh($leadLogWithJump);
|
||||
$this->em->refresh($leadLogWithoutJump);
|
||||
|
||||
// And verify the expected rotation values
|
||||
Assert::assertEquals($withJumpLog[$this->lead->getId()]['rotation'], $leadLogWithJump->getRotation());
|
||||
Assert::assertEquals($withoutJumpLog[$this->lead->getId()]['rotation'], $leadLogWithoutJump->getRotation());
|
||||
}
|
||||
|
||||
private function createLead(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Example');
|
||||
$lead->setLastname('Contact');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
private function createPage(): void
|
||||
{
|
||||
$page = new Page();
|
||||
$page->setAlias('my-page');
|
||||
$page->setTitle('My Page');
|
||||
$page->setIsPublished(true);
|
||||
$this->em->persist($page);
|
||||
$this->em->flush();
|
||||
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
private function createCampaignWithJump(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Campaign With Jump');
|
||||
$campaign->setIsPublished(true);
|
||||
$campaign->setAllowRestart(true);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$fieldValueEvent = new Event();
|
||||
$fieldValueEvent->setCampaign($campaign);
|
||||
$fieldValueEvent->setName('Field Value');
|
||||
$fieldValueEvent->setType('lead.field_value');
|
||||
$fieldValueEvent->setEventType('condition');
|
||||
$fieldValueEvent->setProperties([
|
||||
'field' => 'firstname',
|
||||
'operator' => '!empty',
|
||||
'value' => null,
|
||||
'properties' => [
|
||||
'field' => 'firstname',
|
||||
'operator' => '!empty',
|
||||
'value' => null,
|
||||
],
|
||||
]);
|
||||
$fieldValueEvent->setTriggerMode('immediate');
|
||||
$fieldValueEvent->setOrder(1);
|
||||
$this->em->persist($fieldValueEvent);
|
||||
$this->em->flush();
|
||||
|
||||
$pageHitEvent = new Event();
|
||||
$pageHitEvent->setCampaign($campaign);
|
||||
$pageHitEvent->setName('Page Hit');
|
||||
$pageHitEvent->setType('page.pagehit');
|
||||
$pageHitEvent->setEventType('decision');
|
||||
$pageHitEvent->setProperties(['pages' => []]);
|
||||
$pageHitEvent->setParent($fieldValueEvent);
|
||||
$pageHitEvent->setDecisionPath('yes');
|
||||
$pageHitEvent->setChannel('page');
|
||||
$pageHitEvent->setOrder(2);
|
||||
$this->em->persist($pageHitEvent);
|
||||
$this->em->flush();
|
||||
|
||||
$jumpToEvent = new Event();
|
||||
$jumpToEvent->setCampaign($campaign);
|
||||
$jumpToEvent->setName('Jump to Condition');
|
||||
$jumpToEvent->setType('campaign.jump_to_event');
|
||||
$jumpToEvent->setEventType('action');
|
||||
$jumpToEvent->setProperties(['jumpToEvent' => $fieldValueEvent->getId()]);
|
||||
$jumpToEvent->setParent($pageHitEvent);
|
||||
$jumpToEvent->setDecisionPath('yes');
|
||||
$jumpToEvent->setTriggerMode('immediate');
|
||||
$jumpToEvent->setOrder(3);
|
||||
$this->em->persist($jumpToEvent);
|
||||
$this->em->flush();
|
||||
|
||||
$this->campaignWithJump = $campaign;
|
||||
|
||||
$campaignLead = new CampaignLead();
|
||||
$campaignLead->setCampaign($this->campaignWithJump);
|
||||
$campaignLead->setLead($this->lead);
|
||||
$campaignLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($campaignLead);
|
||||
$this->em->flush();
|
||||
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setLead($this->lead);
|
||||
$leadEventLog->setEvent($fieldValueEvent);
|
||||
$leadEventLog->setIsScheduled(false);
|
||||
$leadEventLog->setRotation(1);
|
||||
$leadEventLog->setNonActionPathTaken(false);
|
||||
$leadEventLog->setDateTriggered(new \DateTime());
|
||||
$this->em->persist($leadEventLog);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function createCampaignWithoutJump(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Campaign Without Jump');
|
||||
$campaign->setIsPublished(true);
|
||||
$campaign->setAllowRestart(true);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$fieldValueEvent = new Event();
|
||||
$fieldValueEvent->setCampaign($campaign);
|
||||
$fieldValueEvent->setName('Field Value');
|
||||
$fieldValueEvent->setType('lead.field_value');
|
||||
$fieldValueEvent->setEventType('condition');
|
||||
$fieldValueEvent->setProperties([
|
||||
'field' => 'firstname',
|
||||
'operator' => '!empty',
|
||||
'value' => null,
|
||||
'properties' => [
|
||||
'field' => 'firstname',
|
||||
'operator' => '!empty',
|
||||
'value' => null,
|
||||
],
|
||||
]);
|
||||
$fieldValueEvent->setTriggerMode('immediate');
|
||||
$fieldValueEvent->setOrder(1);
|
||||
$this->em->persist($fieldValueEvent);
|
||||
$this->em->flush();
|
||||
|
||||
$pageHitEvent = new Event();
|
||||
$pageHitEvent->setCampaign($campaign);
|
||||
$pageHitEvent->setName('Page Hit');
|
||||
$pageHitEvent->setType('page.pagehit');
|
||||
$pageHitEvent->setEventType('decision');
|
||||
$pageHitEvent->setProperties([
|
||||
'pages' => [],
|
||||
'properties' => [
|
||||
'pages' => [],
|
||||
],
|
||||
]);
|
||||
$pageHitEvent->setParent($fieldValueEvent);
|
||||
$pageHitEvent->setDecisionPath('yes');
|
||||
$pageHitEvent->setChannel('page');
|
||||
$pageHitEvent->setOrder(2);
|
||||
$this->em->persist($pageHitEvent);
|
||||
$this->em->flush();
|
||||
|
||||
$this->campaignWithoutJump = $campaign;
|
||||
|
||||
$campaignLead = new CampaignLead();
|
||||
$campaignLead->setCampaign($this->campaignWithoutJump);
|
||||
$campaignLead->setLead($this->lead);
|
||||
$campaignLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($campaignLead);
|
||||
$this->em->flush();
|
||||
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setLead($this->lead);
|
||||
$leadEventLog->setEvent($fieldValueEvent);
|
||||
$leadEventLog->setIsScheduled(false);
|
||||
$leadEventLog->setRotation(1);
|
||||
$leadEventLog->setNonActionPathTaken(false);
|
||||
$leadEventLog->setDateTriggered(new \DateTime());
|
||||
$this->em->persist($leadEventLog);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function clearEm(): void
|
||||
{
|
||||
foreach ([Campaign::class, Event::class, LeadEventLog::class] as $entity) {
|
||||
$this->em->clear($entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Campaign;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class DetailsTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testDetailsPageLoadCorrectly(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Campaign A');
|
||||
$campaign->setCanvasSettings([
|
||||
'nodes' => [
|
||||
0 => [
|
||||
'id' => '148',
|
||||
'positionX' => '760',
|
||||
'positionY' => '155',
|
||||
],
|
||||
1 => [
|
||||
'id' => 'lists',
|
||||
'positionX' => '860',
|
||||
'positionY' => '50',
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
0 => [
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => '148',
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', sprintf('/s/campaigns/view/%s', $campaign->getId()));
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful();
|
||||
Assert::assertStringContainsString($campaign->getName(), $response->getContent());
|
||||
Assert::assertStringContainsString(sprintf('data-target-url="/s/campaigns/view/%s/contact/1"', $campaign->getId()), $response->getContent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Campaign;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignMember;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class JumpToActionTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @see https://github.com/mautic/mautic/pull/11568
|
||||
*/
|
||||
public function testInfiniteLoop(): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('loop@expe.rt');
|
||||
$contact->setDateIdentified(new \DateTime());
|
||||
$contact->setLastActive(new \DateTime());
|
||||
|
||||
$tag = new Tag();
|
||||
$tag->setTag('VisitedPageA');
|
||||
|
||||
$decision = new Event();
|
||||
$decision->setOrder(1);
|
||||
$decision->setName('URL is hit');
|
||||
$decision->setType('page.pagehit');
|
||||
$decision->setEventType('decision');
|
||||
$decision->setProperties([
|
||||
'url' => 'https://mautic.org',
|
||||
]);
|
||||
|
||||
$addTag = new Event();
|
||||
$addTag->setOrder(2);
|
||||
$addTag->setParent($decision);
|
||||
$addTag->setName('Add tag');
|
||||
$addTag->setType('lead.changetags');
|
||||
$addTag->setEventType('action');
|
||||
$addTag->setTriggerInterval(1);
|
||||
$addTag->setTriggerIntervalUnit('i');
|
||||
$addTag->setTriggerMode('interval');
|
||||
$addTag->setDecisionPath('yes');
|
||||
$addTag->setProperties([
|
||||
'add_tags' => ['VisitedPageA'],
|
||||
]);
|
||||
|
||||
$jumpTo = new Event();
|
||||
$jumpTo->setOrder(2);
|
||||
$jumpTo->setParent($decision);
|
||||
$jumpTo->setName('Jump to');
|
||||
$jumpTo->setType('campaign.jump_to_event');
|
||||
$jumpTo->setEventType('action');
|
||||
$jumpTo->setTriggerInterval(1);
|
||||
$jumpTo->setTriggerIntervalUnit('i');
|
||||
$jumpTo->setTriggerMode('interval');
|
||||
$jumpTo->setDecisionPath('no');
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->addEvents([$decision, $addTag, $jumpTo]);
|
||||
$campaign->setName('Campaign A');
|
||||
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setLead($contact);
|
||||
$campaignMember->setCampaign($campaign);
|
||||
$campaignMember->setDateAdded(new \DateTime('-61 seconds'));
|
||||
|
||||
$decision->setCampaign($campaign);
|
||||
$decision->addChild($addTag);
|
||||
$decision->addChild($jumpTo);
|
||||
$addTag->setCampaign($campaign);
|
||||
$jumpTo->setCampaign($campaign);
|
||||
$campaign->addLead(0, $campaignMember);
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->persist($decision);
|
||||
$this->em->persist($addTag);
|
||||
$this->em->persist($jumpTo);
|
||||
$this->em->persist($contact);
|
||||
$this->em->persist($campaignMember);
|
||||
$this->em->persist($tag);
|
||||
$this->em->flush();
|
||||
|
||||
$jumpTo->setProperties(['jumpToEvent' => $addTag->getId()]);
|
||||
|
||||
$campaign->setCanvasSettings(
|
||||
[
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => $decision->getId(),
|
||||
'positionX' => '1080',
|
||||
'positionY' => '155',
|
||||
],
|
||||
[
|
||||
'id' => $addTag->getId(),
|
||||
'positionX' => '980',
|
||||
'positionY' => '260',
|
||||
],
|
||||
[
|
||||
'id' => $jumpTo->getId(),
|
||||
'positionX' => '1220',
|
||||
'positionY' => '260',
|
||||
],
|
||||
[
|
||||
'id' => 'lists',
|
||||
'positionX' => '860',
|
||||
'positionY' => '1',
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => $decision->getId(),
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
[
|
||||
'sourceId' => $decision->getId(),
|
||||
'targetId' => $addTag->getId(),
|
||||
'anchors' => [
|
||||
'source' => 'yes',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
[
|
||||
'sourceId' => $decision->getId(),
|
||||
'targetId' => $jumpTo->getId(),
|
||||
'anchors' => [
|
||||
'source' => 'no',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->persist($jumpTo);
|
||||
$this->em->flush();
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => $campaign->getId()]);
|
||||
|
||||
$eventLogs = $this->getEventLogsForContact($contact);
|
||||
|
||||
Assert::assertCount(3, $eventLogs, '3 event logs should be scheduled to be executed in 1 minute');
|
||||
Assert::assertSame(['URL is hit', 'Jump to', 'Add tag'], $this->getEventNames($eventLogs));
|
||||
|
||||
// Time travel 2 minutes into the future:
|
||||
foreach ($eventLogs as $eventLog) {
|
||||
$eventLog->setTriggerDate(new \DateTime('-2 minutes'));
|
||||
$eventLog->setDateTriggered(new \DateTime('-2 minutes'));
|
||||
$this->em->persist($eventLog);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($eventLog);
|
||||
$this->em->detach($jumpTo);
|
||||
$this->em->detach($eventLog);
|
||||
$this->em->detach($decision);
|
||||
$this->em->detach($addTag);
|
||||
$this->em->detach($campaignMember);
|
||||
$this->em->detach($tag);
|
||||
|
||||
// Executing the command for the second time should not schedule any new events:
|
||||
$this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => $campaign->getId()]);
|
||||
|
||||
$eventLogs = $this->getEventLogsForContact($contact);
|
||||
|
||||
Assert::assertCount(3, $eventLogs); // This was 6 before the fix.
|
||||
Assert::assertSame(['URL is hit', 'Jump to', 'Add tag'], $this->getEventNames($eventLogs));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadEventLog[]
|
||||
*/
|
||||
private function getEventLogsForContact(Lead $contact): array
|
||||
{
|
||||
$eventLogRepository = $this->em->getRepository(LeadEventLog::class);
|
||||
|
||||
return $eventLogRepository->findBy(['lead' => $contact->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LeadEventLog[] $eventLogs
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function getEventNames(array $eventLogs): array
|
||||
{
|
||||
return array_map(
|
||||
fn (LeadEventLog $eventLog) => $eventLog->getEvent()->getName(),
|
||||
$eventLogs
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class CampaignAuditLogTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
|
||||
public function testCampaignAuditLog(): void
|
||||
{
|
||||
// Create a Segment.
|
||||
$segment = $this->createSegment('seg1', []);
|
||||
|
||||
$campaign = $this->createCampaign('Audit Log Campaign');
|
||||
$campaign->addList($segment);
|
||||
$campaign->setIsPublished(true);
|
||||
|
||||
$event = new Event();
|
||||
$event->setName('Change points event');
|
||||
$event->setType('lead.changepoints');
|
||||
$event->setEventType('action');
|
||||
$event->setOrder(1);
|
||||
$event->setProperties(['points' => 21]);
|
||||
$event->setTriggerMode('date');
|
||||
$event->setTriggerDate(new \DateTime('2023-09-27 21:37'));
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$campaignId = $campaign->getId();
|
||||
$eventId = $event->getId();
|
||||
$modifiedEvents = []; // Initialize empty for consistency with API approach
|
||||
|
||||
// 2. Update the event through API to test EventController and create audit log.
|
||||
|
||||
// 2.b Get the event edit form.
|
||||
$uri = "/s/campaigns/events/edit/{$eventId}?campaignId={$campaignId}&anchor=leadsource";
|
||||
$this->client->xmlHttpRequest('GET', $uri, ['modifiedEvents' => json_encode($modifiedEvents)]);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Update the event.
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[canvasSettings][droppedX]' => '863',
|
||||
'campaignevent[canvasSettings][droppedY]' => '363',
|
||||
'campaignevent[name]' => '2 contact points after 1 day',
|
||||
'campaignevent[triggerMode]' => 'interval',
|
||||
'campaignevent[triggerDate]' => '2023-09-27 21:37',
|
||||
'campaignevent[triggerInterval]' => '1',
|
||||
'campaignevent[triggerIntervalUnit]' => 'd',
|
||||
'campaignevent[triggerHour]' => '',
|
||||
'campaignevent[triggerRestrictedStartHour]' => '',
|
||||
'campaignevent[triggerRestrictedStopHour]' => '',
|
||||
'campaignevent[anchor]' => 'no',
|
||||
'campaignevent[properties][points]' => '2',
|
||||
'campaignevent[properties][group]' => '',
|
||||
'campaignevent[type]' => 'lead.changepoints',
|
||||
'campaignevent[eventType]' => 'action',
|
||||
'campaignevent[anchorEventType]' => 'condition',
|
||||
'campaignevent[campaignId]' => $campaignId,
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$formData = $form->getPhpValues();
|
||||
$formData['modifiedEvents'] = json_encode($modifiedEvents);
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $formData);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertTrue($responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
|
||||
// 2.c Save campaign through CampaignModel to trigger audit log creation
|
||||
$campaignModel = static::getContainer()->get('mautic.campaign.model.campaign');
|
||||
$campaign = $campaignModel->getEntity($campaignId);
|
||||
$event = $this->em->find(Event::class, $eventId);
|
||||
$event->setName('2 contact points after 1 day');
|
||||
$campaign->addEvent($eventId, $event);
|
||||
$campaignModel->saveEntity($campaign);
|
||||
$this->em->clear();
|
||||
|
||||
// 3. View the campaign.
|
||||
$campaignViewUrl = '/s/campaigns/view/'.$campaignId;
|
||||
$this->client->request(Request::METHOD_GET, $campaignViewUrl);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$translator = static::getContainer()->get('translator');
|
||||
\assert($translator instanceof TranslatorInterface);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
$translator->trans('mautic.campaign.changelog.event_updated'),
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
$translator->trans('mautic.campaign.changelog.event_updated_details', ['%event_id%' => $eventId]),
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
}
|
||||
|
||||
public function testCampaignMultipleProjectAdditionsShowInAuditLog(): void
|
||||
{
|
||||
$campaignModel = CampaignAuditLogTest::getContainer()->get('mautic.campaign.model.campaign');
|
||||
|
||||
// Create projects first
|
||||
$project1 = $this->createProject('First Project');
|
||||
$project2 = $this->createProject('Second Project');
|
||||
$this->em->flush();
|
||||
|
||||
// Create a campaign without projects
|
||||
$campaign = $this->createCampaign('Campaign for Multiple Additions');
|
||||
$campaignModel->saveEntity($campaign);
|
||||
$this->em->flush();
|
||||
$campaignId = $campaign->getId();
|
||||
|
||||
// Add first project
|
||||
$campaign->addProject($project1);
|
||||
$campaignModel->saveEntity($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
// Add second project
|
||||
$campaign->addProject($project2);
|
||||
$campaignModel->saveEntity($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
// View the campaign to see audit log
|
||||
$campaignViewUrl = '/s/campaigns/view/'.$campaignId;
|
||||
$this->client->request(Request::METHOD_GET, $campaignViewUrl);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$responseContent = $this->client->getResponse()->getContent();
|
||||
|
||||
// Verify both project names appear
|
||||
$this->assertStringContainsString('First Project', $responseContent);
|
||||
$this->assertStringContainsString('Second Project', $responseContent);
|
||||
|
||||
// Should show the progression in audit log
|
||||
$this->assertStringContainsString('First Project, Second Project', $responseContent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class CampaignBuilderEditFieldValueConditionTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CampaignControllerTrait;
|
||||
use CreateTestEntitiesTrait;
|
||||
|
||||
public function testCampaignBuilderFormForFieldValueConditionForInOperator(): void
|
||||
{
|
||||
$campaign = $this->setupCampaignWithLeadList();
|
||||
$version = $campaign->getVersion();
|
||||
|
||||
$campaignCondition = $this->setupCampaignEvent($campaign);
|
||||
|
||||
$campaignAction = new Event();
|
||||
$campaignAction->setCampaign($campaign);
|
||||
$campaignAction->setParent($campaignCondition);
|
||||
$campaignAction->setName('Send Email 1');
|
||||
$campaignAction->setType('email.send');
|
||||
$campaignAction->setEventType('action');
|
||||
$campaignAction->setProperties([]);
|
||||
$this->em->persist($campaignAction);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$conditionArray = $campaignCondition->convertToArray();
|
||||
unset($conditionArray['campaign'], $conditionArray['children'], $conditionArray['log'], $conditionArray['changes']);
|
||||
|
||||
$campaignArray = $campaignAction->convertToArray();
|
||||
unset($campaignArray['campaign'], $campaignArray['children'], $campaignArray['log'], $campaignArray['changes'], $campaignArray['parent']);
|
||||
|
||||
$modifiedEvents = [
|
||||
$campaignCondition->getId() => $conditionArray,
|
||||
$campaignAction->getId() => $campaignArray,
|
||||
];
|
||||
|
||||
$payload = [
|
||||
'modifiedEvents' => json_encode($modifiedEvents),
|
||||
];
|
||||
|
||||
$this->client->request(Request::METHOD_POST, sprintf('/s/campaigns/events/edit/%s', $campaignCondition->getId()), $payload, [], $this->createAjaxHeaders());
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
// version should be incremented as campaign's "modified by user" is updated
|
||||
$this->refreshAndSubmitForm($campaign, ++$version);
|
||||
}
|
||||
|
||||
public function testSwitchScalarValueToAnArrayOne(): void
|
||||
{
|
||||
$campaign = $this->setupCampaignWithLeadList();
|
||||
|
||||
$campaignCondition = $this->setupCampaignEvent($campaign);
|
||||
// Start with a scalar value for the 'value' property
|
||||
$campaignCondition->setProperties([
|
||||
'field' => 'country',
|
||||
'operator' => '=',
|
||||
'value' => 'Afghanistan', // scalar value
|
||||
]);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Convert the event to array format and change from scalar to array value
|
||||
$conditionArray = $campaignCondition->convertToArray();
|
||||
unset($conditionArray['campaign'], $conditionArray['children'], $conditionArray['log'], $conditionArray['changes']);
|
||||
|
||||
// Change the operator to 'in' and value to array (this is the core test scenario)
|
||||
$conditionArray['properties']['operator'] = 'in';
|
||||
$conditionArray['properties']['value'] = ['Albania']; // array value
|
||||
|
||||
$modifiedEvents = [
|
||||
$campaignCondition->getId() => $conditionArray,
|
||||
];
|
||||
|
||||
$payload = [
|
||||
'modifiedEvents' => json_encode($modifiedEvents),
|
||||
];
|
||||
|
||||
// The main test: ensure the EventController can handle scalar to array conversion without HTTP 500
|
||||
$this->client->request(Request::METHOD_POST, sprintf('/s/campaigns/events/edit/%s', $campaignCondition->getId()), $payload, [], $this->createAjaxHeaders());
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
// This is the core assertion - the request should succeed (no HTTP 500)
|
||||
Assert::assertTrue($response->isOk(), 'EventController should handle scalar to array value conversion without HTTP 500');
|
||||
|
||||
// Additional verification: ensure response is valid JSON
|
||||
Assert::assertJson($response->getContent());
|
||||
}
|
||||
|
||||
private function setupCampaignWithLeadList(): Campaign
|
||||
{
|
||||
$leadList = new LeadList();
|
||||
$leadList->setName('Test list');
|
||||
$leadList->setPublicName('Test list');
|
||||
$leadList->setAlias('test-list');
|
||||
$this->em->persist($leadList);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign');
|
||||
$campaign->addList($leadList);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test Lead');
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function setupCampaignEvent(Campaign $campaign): Event
|
||||
{
|
||||
$campaignCondition = new Event();
|
||||
$campaignCondition->setCampaign($campaign);
|
||||
$campaignCondition->setName('Check for country');
|
||||
$campaignCondition->setType('lead.field_value');
|
||||
$campaignCondition->setEventType('condition');
|
||||
$campaignCondition->setProperties([
|
||||
'field' => 'country',
|
||||
'operator' => 'in',
|
||||
'value' => ['Afghanistan'],
|
||||
]);
|
||||
$this->em->persist($campaignCondition);
|
||||
|
||||
return $campaignCondition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Controller;
|
||||
|
||||
use Doctrine\ORM\Exception\NotSupported;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use GuzzleHttp\Utils;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CoreBundle\Helper\ExportHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\CoreBundle\Tests\Functional\UserEntityTrait;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Entity\UserRepository;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CampaignControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
use UserEntityTrait;
|
||||
|
||||
private Lead $contactOne;
|
||||
private Lead $contactTwo;
|
||||
private Lead $contactThree;
|
||||
private Campaign $campaign;
|
||||
|
||||
/**
|
||||
* @throws NotSupported
|
||||
* @throws ORMException
|
||||
* @throws MappingException
|
||||
*/
|
||||
public function testContactsGridForValidPermissions(): void
|
||||
{
|
||||
$nonAdminUser = $this->setupCampaignData(38, 0);
|
||||
|
||||
$this->loginOtherUser($nonAdminUser);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/campaigns/view/'.$this->campaign->getId().'/contact/1');
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$content = $this->client->getResponse()->getContent();
|
||||
$this->assertStringContainsString($this->contactOne->getName(), $content);
|
||||
$this->assertStringContainsString($this->contactTwo->getName(), $content);
|
||||
$this->assertStringContainsString($this->contactThree->getName(), $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws MappingException
|
||||
* @throws ORMException
|
||||
* @throws NotSupported
|
||||
*/
|
||||
public function testContactsGridWhenIncompleteValidPermissions(): void
|
||||
{
|
||||
$nonAdminUser = $this->setupCampaignData(2, 0);
|
||||
|
||||
$this->loginOtherUser($nonAdminUser);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/campaigns/view/'.$this->campaign->getId().'/contact/1');
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$content = $this->client->getResponse()->getContent();
|
||||
$this->assertStringContainsString('No Contacts Found', $content, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
* @throws MappingException
|
||||
* @throws OptimisticLockException
|
||||
* @throws NotSupported
|
||||
*/
|
||||
private function setupCampaignData(int $bitwise, int $export): User
|
||||
{
|
||||
/** @var UserRepository $userRepository */
|
||||
$userRepository = $this->em->getRepository(User::class);
|
||||
$adminUser = $userRepository->findOneBy(['username' => 'admin']);
|
||||
|
||||
// create users
|
||||
$nonAdminUser = $this->createUserWithPermission([
|
||||
'user-name' => 'non-admin',
|
||||
'email' => 'non-admin@mautic-test.com',
|
||||
'first-name' => 'non-admin',
|
||||
'last-name' => 'non-admin',
|
||||
'role' => [
|
||||
'name' => 'perm_non_admin',
|
||||
'permissions' => [
|
||||
'lead:leads' => $bitwise,
|
||||
'campaign:campaigns' => 2,
|
||||
'campaign:export:enable' => $export,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// create contacts
|
||||
$this->contactOne = $this->createLead('John', '', '', $adminUser);
|
||||
$this->contactTwo = $this->createLead('Alex', '', '', $adminUser);
|
||||
$this->contactThree = $this->createLead('Gemini', '', '', $nonAdminUser);
|
||||
|
||||
// Create Segment
|
||||
$segment = $this->createSegment('seg1', []);
|
||||
|
||||
// Add contacts to segment
|
||||
$this->createListLead($segment, $this->contactOne);
|
||||
$this->createListLead($segment, $this->contactTwo);
|
||||
$this->createListLead($segment, $this->contactThree);
|
||||
|
||||
$this->campaign = $this->createCampaign('Campaign');
|
||||
$this->campaign->addList($segment);
|
||||
|
||||
$this->createEvent('Add 10 points', $this->campaign,
|
||||
'lead.changepoints',
|
||||
'action',
|
||||
['points' => 10]
|
||||
);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->testSymfonyCommand('mautic:campaigns:update', ['--campaign-id' => $this->campaign->getId(), '-vv']);
|
||||
|
||||
return $nonAdminUser;
|
||||
}
|
||||
|
||||
public function testCountsProcessedCampaignsMethodCountsProcessedCampaignsCorrectly(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test Campaign');
|
||||
$this->em->persist($campaign);
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test Lead');
|
||||
$this->em->persist($lead);
|
||||
|
||||
$campaignEvent1 = new Event();
|
||||
$campaignEvent1->setCampaign($campaign);
|
||||
$campaignEvent1->setName('Send Email 1');
|
||||
$campaignEvent1->setType('email.send');
|
||||
$campaignEvent1->setEventType('action');
|
||||
$campaignEvent1->setProperties([]);
|
||||
$this->em->persist($campaignEvent1);
|
||||
|
||||
$campaignEvent2 = new Event();
|
||||
$campaignEvent2->setCampaign($campaign);
|
||||
$campaignEvent2->setName('Jump to send email 1');
|
||||
$campaignEvent2->setType('campaign.jump_to_event');
|
||||
$campaignEvent2->setEventType('action');
|
||||
$campaignEvent2->setProperties([]);
|
||||
$this->em->persist($campaignEvent2);
|
||||
|
||||
$campaignLead = new CampaignLead();
|
||||
$campaignLead->setCampaign($campaign);
|
||||
$campaignLead->setLead($lead);
|
||||
$campaignLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($campaignLead);
|
||||
|
||||
$leadEventLog1 = new LeadEventLog();
|
||||
$leadEventLog1->setLead($lead);
|
||||
$leadEventLog1->setEvent($campaignEvent1);
|
||||
$leadEventLog1->setIsScheduled(true);
|
||||
$leadEventLog1->setRotation(1);
|
||||
$this->em->persist($leadEventLog1);
|
||||
|
||||
$leadEventLog2 = new LeadEventLog();
|
||||
$leadEventLog2->setLead($lead);
|
||||
$leadEventLog2->setEvent($campaignEvent2);
|
||||
$leadEventLog1->setRotation(1);
|
||||
$this->em->persist($leadEventLog2);
|
||||
|
||||
$leadEventLog3 = new LeadEventLog();
|
||||
$leadEventLog3->setLead($lead);
|
||||
$leadEventLog3->setEvent($campaignEvent1);
|
||||
$leadEventLog1->setRotation(2);
|
||||
$this->em->persist($leadEventLog3);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$eventsStatistics = $this->getEventsStatistics($campaign);
|
||||
$expectedEventsStatistics = [
|
||||
0 => [
|
||||
'successPercent' => '100%',
|
||||
'completed' => '1',
|
||||
'pending' => '1',
|
||||
],
|
||||
1 => [
|
||||
'successPercent' => '100%',
|
||||
'completed' => '1',
|
||||
'pending' => '0',
|
||||
],
|
||||
];
|
||||
|
||||
Assert::assertSame($expectedEventsStatistics, $eventsStatistics, 'Events statistics doesn\'t match the actual events in the database.');
|
||||
}
|
||||
|
||||
private function getCrawler(Campaign $campaign): Crawler
|
||||
{
|
||||
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
$before = $now->modify('-1 month');
|
||||
$after = $now->modify('+1 month');
|
||||
$url = sprintf('s/campaigns/event/stats/%d/%s/%s', $campaign->getId(), $before->format('Y-m-d'), $after->format('Y-m-d'));
|
||||
$this->client->request('GET', $url);
|
||||
$response = $this->client->getResponse();
|
||||
$body = Utils::jsonDecode($response->getContent(), true);
|
||||
$this->client->restart();
|
||||
|
||||
return new Crawler($body['actions']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string, string>>
|
||||
*/
|
||||
private function getEventsStatistics(Campaign $campaign): array
|
||||
{
|
||||
$crawler = $this->getCrawler($campaign);
|
||||
$events = [];
|
||||
for ($eventIndex = 0;; ++$eventIndex) {
|
||||
$node = $crawler->filter('.campaign-event-list')->filter('span')->eq($eventIndex * 3);
|
||||
if (1 > $node->count()) {
|
||||
break;
|
||||
}
|
||||
$events[] = [
|
||||
'successPercent' => trim($crawler->filter('.campaign-event-list')->filter('span')->eq($eventIndex * 3)->html()),
|
||||
'completed' => trim($crawler->filter('.campaign-event-list')->filter('span')->eq($eventIndex * 3 + 1)->html()),
|
||||
'pending' => trim($crawler->filter('.campaign-event-list')->filter('span')->eq($eventIndex * 3 + 2)->html()),
|
||||
];
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
public function testExportAction(): void
|
||||
{
|
||||
$nonAdminUser = $this->setupCampaignData(2, 1024);
|
||||
|
||||
$this->loginOtherUser($nonAdminUser);
|
||||
|
||||
$this->client->request('GET', '/s/campaigns/export/'.$this->campaign->getId());
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
$this->assertSame('application/zip', $response->headers->get('Content-Type'));
|
||||
$this->assertStringContainsString('.zip', $response->headers->get('Content-Disposition'));
|
||||
}
|
||||
|
||||
public function testBatchExportAction(): void
|
||||
{
|
||||
$nonAdminUser = $this->setupCampaignData(2, 1024);
|
||||
$this->loginOtherUser($nonAdminUser);
|
||||
|
||||
$this->client->request(
|
||||
'GET',
|
||||
'/s/campaigns/batchExport',
|
||||
[
|
||||
'filetype' => 'zip',
|
||||
'ids' => json_encode([$this->campaign->getId()]),
|
||||
]
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
$this->assertEquals('application/zip', $response->headers->get('Content-Type'));
|
||||
$this->assertStringContainsString('.zip', (string) $response->headers->get('Content-Disposition'));
|
||||
}
|
||||
|
||||
public function testExportActionAccessDenied(): void
|
||||
{
|
||||
$nonAdminUser = $this->setupCampaignData(2, 0);
|
||||
|
||||
$this->loginOtherUser($nonAdminUser);
|
||||
|
||||
$this->client->request('GET', '/s/campaigns/export/'.$this->campaign->getId());
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testExportActionCampaignNotFound(): void
|
||||
{
|
||||
$nonAdminUser = $this->setupCampaignData(38, 1024);
|
||||
|
||||
$this->loginOtherUser($nonAdminUser);
|
||||
|
||||
$this->client->request('GET', '/s/campaigns/export/999999'); // Non-existent campaign ID
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_NOT_FOUND, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testExportFileNotCreated(): void
|
||||
{
|
||||
$nonAdminUser = $this->setupCampaignData(38, 1024); // Ensures export permission
|
||||
|
||||
// Mock the ExportHelper to simulate file creation failure
|
||||
$exportHelperMock = $this->createMock(ExportHelper::class);
|
||||
$exportHelperMock->method('writeToZipFile')->willReturn('');
|
||||
|
||||
// Inject the mock into the container
|
||||
static::getContainer()->set(ExportHelper::class, $exportHelperMock);
|
||||
|
||||
$this->loginOtherUser($nonAdminUser);
|
||||
|
||||
$this->client->request('GET', '/s/campaigns/export/'.$this->campaign->getId());
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$responseContent = $response->getContent();
|
||||
|
||||
// Assert the response status and content
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
$this->assertJson($responseContent);
|
||||
|
||||
$responseData = json_decode($responseContent, true);
|
||||
|
||||
$this->assertArrayHasKey('error', $responseData);
|
||||
$this->assertStringContainsString('Export file could not be created', $responseData['error']);
|
||||
|
||||
$this->assertArrayHasKey('flashes', $responseData);
|
||||
}
|
||||
|
||||
public function testBatchExportAccessDenied(): void
|
||||
{
|
||||
$nonAdminUser = $this->setupCampaignData(0, 0); // No permissions for view or export
|
||||
|
||||
$this->loginOtherUser($nonAdminUser);
|
||||
|
||||
$this->client->request('GET', '/s/campaigns/batchExport');
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testBatchExportCampaignQuery(): void
|
||||
{
|
||||
$nonAdminUser = $this->setupCampaignData(2, 1024); // Ensure view permissions
|
||||
|
||||
$this->loginOtherUser($nonAdminUser);
|
||||
|
||||
$this->client->request('GET', '/s/campaigns/batchExport', [
|
||||
'ids' => json_encode([]), // Empty IDs to trigger query
|
||||
]);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testBatchExportFileNotCreated(): void
|
||||
{
|
||||
$nonAdminUser = $this->setupCampaignData(2, 1024); // Ensure view and export permissions
|
||||
|
||||
// Mock the ExportHelper to simulate file creation failure
|
||||
$exportHelperMock = $this->createMock(ExportHelper::class);
|
||||
$exportHelperMock->method('writeToZipFile')->willReturn('/invalid/path/to/file.zip');
|
||||
|
||||
// Use the test container to replace the service with the mock
|
||||
static::getContainer()->set(ExportHelper::class, $exportHelperMock);
|
||||
|
||||
$this->loginOtherUser($nonAdminUser);
|
||||
|
||||
$this->client->request('GET', '/s/campaigns/batchExport', [
|
||||
'ids' => json_encode([$this->campaign->getId()]),
|
||||
]);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$responseContent = $response->getContent();
|
||||
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
$this->assertJson($responseContent);
|
||||
|
||||
$responseData = json_decode($responseContent, true);
|
||||
|
||||
$this->assertArrayHasKey('error', $responseData);
|
||||
$this->assertStringContainsString('Export file could not be created', $responseData['error']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
trait CampaignControllerTrait
|
||||
{
|
||||
/**
|
||||
* @param array<string,mixed> $formValues
|
||||
*/
|
||||
private function refreshAndSubmitForm(Campaign $campaign, int $expectedVersion, array $formValues = []): void
|
||||
{
|
||||
$crawler = $this->refreshPage($campaign);
|
||||
$this->submitForm($crawler, $campaign, $expectedVersion, $formValues);
|
||||
}
|
||||
|
||||
private function refreshPage(Campaign $campaign): Crawler
|
||||
{
|
||||
$crawler = $this->client->request('GET', sprintf('/s/campaigns/edit/%s', $campaign->getId()));
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
Assert::assertStringContainsString('Edit Campaign', $crawler->text());
|
||||
|
||||
return $crawler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $formValues
|
||||
*/
|
||||
private function submitForm(
|
||||
Crawler $crawler,
|
||||
Campaign $campaign,
|
||||
int $expectedVersion,
|
||||
array $formValues = [],
|
||||
): Crawler {
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form->setValues($formValues);
|
||||
$newCrawler = $this->client->submit($form);
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$this->em->clear();
|
||||
$campaign = $this->em->find(Campaign::class, $campaign->getId());
|
||||
Assert::assertSame($expectedVersion, $campaign->getVersion());
|
||||
|
||||
return $newCrawler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create canvas settings with a single connection from a source to an event.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function createCanvasSettings(int $eventId, string $sourceType = 'lists'): array
|
||||
{
|
||||
return [
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => $sourceType,
|
||||
'positionX' => 100,
|
||||
'positionY' => 100,
|
||||
],
|
||||
[
|
||||
'id' => $eventId,
|
||||
'positionX' => 300,
|
||||
'positionY' => 100,
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => $sourceType,
|
||||
'targetId' => $eventId,
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create canvas settings with connections for multiple events.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function createCanvasSettingsWithMultipleEvents(
|
||||
int $firstEventId,
|
||||
int $secondEventId,
|
||||
string $sourceType = 'lists',
|
||||
): array {
|
||||
return [
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => $sourceType,
|
||||
'positionX' => 100,
|
||||
'positionY' => 100,
|
||||
],
|
||||
[
|
||||
'id' => $firstEventId,
|
||||
'positionX' => 300,
|
||||
'positionY' => 100,
|
||||
],
|
||||
[
|
||||
'id' => $secondEventId,
|
||||
'positionX' => 500,
|
||||
'positionY' => 100,
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => $sourceType,
|
||||
'targetId' => $firstEventId,
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
[
|
||||
'sourceId' => $firstEventId,
|
||||
'targetId' => $secondEventId,
|
||||
'anchors' => [
|
||||
'source' => 'bottom',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Tests\Functional\Fixtures\FixtureHelper;
|
||||
use Mautic\CoreBundle\Event\EntityImportEvent;
|
||||
use Mautic\CoreBundle\Helper\ImportHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class CampaignImportControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->useCleanupRollback = false;
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testNewAction(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/campaign/import/new');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
$this->assertStringContainsString('campaignImport', $response->getContent());
|
||||
}
|
||||
|
||||
public function testCancelAction(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Start the session by making a request
|
||||
$this->client->request(Request::METHOD_GET, '/s/campaign/import/new');
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/campaign/import/cancel');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testProgressAction(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Start the session by making a request
|
||||
$this->client->request(Request::METHOD_GET, '/s/campaign/import/new');
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/campaign/import/progress');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
$this->assertStringContainsString('campaignImport', $response->getContent());
|
||||
}
|
||||
|
||||
public function testUndoAction(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Make a dummy request to initialize session
|
||||
$this->client->request('GET', '/');
|
||||
$session = $this->client->getRequest()->getSession();
|
||||
|
||||
// Simulate import summary with NEW entities to trigger undo
|
||||
$session->set('mautic.campaign.import.summary', [
|
||||
[
|
||||
EntityImportEvent::NEW => [
|
||||
Campaign::ENTITY_NAME => [
|
||||
'ids' => [101, 102],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$session->save();
|
||||
|
||||
$this->client->request('GET', '/s/campaign/import/undo');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
$this->assertStringContainsString('The last import has been undone successfully.', $response->getContent());
|
||||
}
|
||||
|
||||
public function testUndoActionWithoutUndoData(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Dummy request to initialize session
|
||||
$this->client->request('GET', '/');
|
||||
$session = $this->client->getRequest()->getSession();
|
||||
|
||||
// Simulate import summary with only UPDATE (no NEW data)
|
||||
$session->set('mautic.campaign.import.summary', [
|
||||
[
|
||||
EntityImportEvent::UPDATE => [
|
||||
Campaign::ENTITY_NAME => [
|
||||
'ids' => [201, 202],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$session->save();
|
||||
|
||||
$this->client->request('GET', '/s/campaign/import/undo');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
$this->assertStringContainsString('No data found for import undo.', $response->getContent());
|
||||
}
|
||||
|
||||
public function testProgressActionAnalyzeDataErrors(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$this->client->request('GET', '/');
|
||||
$session = $this->client->getRequest()->getSession();
|
||||
$session->set('mautic.campaign.import.step', 2);
|
||||
$session->set('mautic.campaign.import.file', __DIR__.'/Fixtures/empty.zip');
|
||||
$session->save();
|
||||
|
||||
$importHelper = $this->createMock(ImportHelper::class);
|
||||
$importHelper->method('readZipFile')->willReturn([]);
|
||||
static::getContainer()->set(ImportHelper::class, $importHelper);
|
||||
|
||||
$this->client->request('GET', '/s/campaign/import/progress');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testProgressActionAnalyzeDataValid(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$this->client->request('GET', '/');
|
||||
$session = $this->client->getRequest()->getSession();
|
||||
|
||||
$fixturesDir = __DIR__.'/Fixtures';
|
||||
if (!is_dir($fixturesDir)) {
|
||||
mkdir($fixturesDir, 0775, true);
|
||||
}
|
||||
|
||||
$fakePath = $fixturesDir.'/fake.zip';
|
||||
file_put_contents($fakePath, 'dummy zip content');
|
||||
|
||||
$session->set('mautic.campaign.import.step', 2);
|
||||
$session->set('mautic.campaign.import.file', $fakePath);
|
||||
$session->save();
|
||||
|
||||
$importHelper = $this->createMock(ImportHelper::class);
|
||||
$importHelper->method('readZipFile')->willReturn(FixtureHelper::getPayload());
|
||||
static::getContainer()->set(ImportHelper::class, $importHelper);
|
||||
|
||||
$this->client->request('GET', '/s/campaign/import/progress');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
@unlink($fakePath);
|
||||
}
|
||||
|
||||
public function testProgressActionImportEmptyFile(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$this->client->request('GET', '/');
|
||||
$session = $this->client->getRequest()->getSession();
|
||||
$session->set('mautic.campaign.import.step', 3);
|
||||
$session->set('mautic.campaign.import.file', __DIR__.'/Fixtures/empty.zip');
|
||||
$session->save();
|
||||
|
||||
$importHelper = $this->createMock(ImportHelper::class);
|
||||
$importHelper->method('readZipFile')->willReturn([]);
|
||||
static::getContainer()->set(ImportHelper::class, $importHelper);
|
||||
|
||||
$this->client->request('GET', '/s/campaign/import/progress');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testProgressActionImportValidData(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$this->client->request('GET', '/');
|
||||
$session = $this->client->getRequest()->getSession();
|
||||
|
||||
$fixturesDir = __DIR__.'/Fixtures';
|
||||
if (!is_dir($fixturesDir)) {
|
||||
mkdir($fixturesDir, 0775, true);
|
||||
}
|
||||
|
||||
$fakePath = $fixturesDir.'/fake.zip';
|
||||
file_put_contents($fakePath, 'dummy zip content');
|
||||
|
||||
$session->set('mautic.campaign.import.step', 3);
|
||||
$session->set('mautic.campaign.import.file', $fakePath);
|
||||
$session->save();
|
||||
|
||||
$importHelper = $this->createMock(ImportHelper::class);
|
||||
$importHelper->method('readZipFile')->willReturn(FixtureHelper::getPayload());
|
||||
static::getContainer()->set(ImportHelper::class, $importHelper);
|
||||
|
||||
$this->client->request('GET', '/s/campaign/import/progress');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
@unlink($fakePath);
|
||||
}
|
||||
|
||||
public function testUploadActionWithValidFile(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'upl');
|
||||
file_put_contents($tmpFile, 'dummy zip content');
|
||||
|
||||
$fileArray = [
|
||||
'tmp_name' => $tmpFile,
|
||||
'name' => 'test.zip',
|
||||
'type' => 'application/zip',
|
||||
'size' => filesize($tmpFile),
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
];
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/s/campaign/import/upload',
|
||||
['campaign_import' => []], // POST data
|
||||
['campaign_import' => ['campaignFile' => $fileArray]]
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
|
||||
@unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class CampaignOptimisticLockTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CampaignControllerTrait;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const OPTIMISTIC_LOCK_ERROR = 'The record you are updating has been changed by someone else in the meantime. Please refresh the browser window and re-submit your changes.';
|
||||
|
||||
public function testOptimisticLock(): void
|
||||
{
|
||||
$campaign = $this->setupCampaign();
|
||||
$version = $campaign->getVersion();
|
||||
|
||||
// version should be incremented as campaign's "modified by user" is updated
|
||||
$this->refreshAndSubmitForm($campaign, ++$version);
|
||||
|
||||
// version should not be incremented as there are no changes
|
||||
$this->refreshAndSubmitForm($campaign, $version);
|
||||
|
||||
// version should be incremented as there are changes
|
||||
$this->refreshAndSubmitForm($campaign, ++$version, [
|
||||
'campaign[allowRestart]' => '1',
|
||||
'campaign[isPublished]' => '1',
|
||||
]);
|
||||
|
||||
// version should not be incremented as there are no changes
|
||||
$this->refreshAndSubmitForm($campaign, $version);
|
||||
|
||||
// refresh the page
|
||||
$pageCrawler = $this->refreshPage($campaign);
|
||||
|
||||
// we should not get an optimistic lock error as the page was refreshed, version should be incremented
|
||||
$crawler = $this->submitForm($pageCrawler, $campaign, ++$version, [
|
||||
'campaign[allowRestart]' => '0',
|
||||
]);
|
||||
Assert::assertStringNotContainsString(self::OPTIMISTIC_LOCK_ERROR, $crawler->text());
|
||||
|
||||
// we should get an optimistic lock error as the page wasn't refreshed
|
||||
$crawler = $this->submitForm($pageCrawler, $campaign, $version, [
|
||||
'campaign[isPublished]' => '1',
|
||||
]);
|
||||
Assert::assertStringContainsString(self::OPTIMISTIC_LOCK_ERROR, $crawler->text());
|
||||
|
||||
// we should get an optimistic lock error even if there is no change
|
||||
$crawler = $this->submitForm($pageCrawler, $campaign, $version);
|
||||
Assert::assertStringContainsString(self::OPTIMISTIC_LOCK_ERROR, $crawler->text());
|
||||
}
|
||||
|
||||
private function setupCampaign(): Campaign
|
||||
{
|
||||
$leadList = new LeadList();
|
||||
$leadList->setName('Test list');
|
||||
$leadList->setPublicName('Test list');
|
||||
$leadList->setAlias('test-list');
|
||||
$this->em->persist($leadList);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign');
|
||||
$campaign->addList($leadList);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test Lead');
|
||||
$this->em->persist($lead);
|
||||
|
||||
$campaignEvent = new Event();
|
||||
$campaignEvent->setCampaign($campaign);
|
||||
$campaignEvent->setName('Send Email 1');
|
||||
$campaignEvent->setType('email.send');
|
||||
$campaignEvent->setEventType('action');
|
||||
$campaignEvent->setProperties([]);
|
||||
$this->em->persist($campaignEvent);
|
||||
$this->em->flush();
|
||||
|
||||
$canvasSettings = $this->createCanvasSettings($campaignEvent->getId());
|
||||
$campaign->setCanvasSettings($canvasSettings);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Entity;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class LeadEventLogRepositoryTest extends MauticMysqlTestCase
|
||||
{
|
||||
private LeadEventLogRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->repository = $this->em->getRepository(LeadEventLog::class);
|
||||
}
|
||||
|
||||
public function testThatRemoveEventLogsMethodRemovesLogs(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$event = $this->createEvent($campaign);
|
||||
$this->createEventLog($campaign, $event);
|
||||
$this->createEventLog($campaign, $event);
|
||||
$this->createEventLog($campaign, $event);
|
||||
$this->em->flush();
|
||||
|
||||
Assert::assertCount(3, $this->repository->findAll());
|
||||
$this->repository->removeEventLogs([(string) $event->getId()]);
|
||||
Assert::assertCount(0, $this->repository->findAll());
|
||||
}
|
||||
|
||||
public function testMarkEventLogsQueued(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$event = $this->createEvent($campaign);
|
||||
$log1 = $this->createEventLog($campaign, $event);
|
||||
$log2 = $this->createEventLog($campaign, $event);
|
||||
$log3 = $this->createEventLog($campaign, $event);
|
||||
$this->em->flush();
|
||||
|
||||
Assert::assertCount(3, $this->repository->findAll());
|
||||
Assert::assertEmpty($log1->getDateQueued());
|
||||
Assert::assertEmpty($log2->getDateQueued());
|
||||
Assert::assertEmpty($log3->getDateQueued());
|
||||
|
||||
$this->repository->markEventLogsQueued([(string) $log1->getId(), (string) $log3->getId()]);
|
||||
$this->em->refresh($log1);
|
||||
$this->em->refresh($log2);
|
||||
$this->em->refresh($log3);
|
||||
|
||||
Assert::assertNotEmpty($log1->getDateQueued());
|
||||
Assert::assertEmpty($log2->getDateQueued());
|
||||
Assert::assertNotEmpty($log3->getDateQueued());
|
||||
}
|
||||
|
||||
private function createLead(): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test');
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Campaign');
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createEvent(Campaign $campaign): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName('Event');
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType('page.devicehit');
|
||||
$event->setEventType(Event::TYPE_DECISION);
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createEventLog(Campaign $campaign, ?Event $event = null): LeadEventLog
|
||||
{
|
||||
$event = $event ?: $this->createEvent($campaign);
|
||||
$lead = $this->createLead();
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setLead($lead);
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setTriggerDate(new \DateTime());
|
||||
$leadEventLog->setIsScheduled(true);
|
||||
$this->em->persist($leadEventLog);
|
||||
|
||||
return $leadEventLog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Fixtures;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
final class FixtureHelper
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em)
|
||||
{
|
||||
}
|
||||
|
||||
public function createContact(string $email): Lead
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail($email);
|
||||
|
||||
$this->em->persist($contact);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
public function addContactToCampaign(Lead $contact, Campaign $campaign): CampaignLead
|
||||
{
|
||||
$ref = new CampaignLead();
|
||||
$ref->setCampaign($campaign);
|
||||
$ref->setLead($contact);
|
||||
$ref->setDateAdded(new \DateTime());
|
||||
|
||||
$this->em->persist($ref);
|
||||
|
||||
return $ref;
|
||||
}
|
||||
|
||||
public function createCampaign(string $name): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName($name);
|
||||
$campaign->setIsPublished(true);
|
||||
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
public function createCampaignWithScheduledEvent(Campaign $campaign, int $interval = 1, string $intervalUnit = 'd', ?\DateTimeInterface $hour = null): Event
|
||||
{
|
||||
if (!$campaign->getId()) {
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
$event = new Event();
|
||||
$event->setCampaign($campaign);
|
||||
$event->setName('Adjust contact points');
|
||||
$event->setType('lead.changepoints');
|
||||
$event->setEventType('action');
|
||||
$event->setTriggerInterval($interval);
|
||||
$event->setTriggerIntervalUnit($intervalUnit);
|
||||
$event->setTriggerMode('interval');
|
||||
if ($hour) {
|
||||
$event->setTriggerHour($hour->format('H:i'));
|
||||
}
|
||||
$event->setProperties(
|
||||
[
|
||||
'canvasSettings' => [
|
||||
'droppedX' => '1080',
|
||||
'droppedY' => '155',
|
||||
],
|
||||
'name' => '',
|
||||
'triggerMode' => 'interval',
|
||||
'triggerDate' => null,
|
||||
'triggerInterval' => $interval,
|
||||
'triggerIntervalUnit' => $intervalUnit,
|
||||
'triggerHour' => $hour,
|
||||
'triggerRestrictedStartHour' => '',
|
||||
'triggerRestrictedStopHour' => '',
|
||||
'anchor' => 'leadsource',
|
||||
'properties' => ['points' => '5'],
|
||||
'type' => 'lead.changepoints',
|
||||
'eventType' => 'action',
|
||||
'anchorEventType' => 'source',
|
||||
'campaignId' => $campaign->getId(),
|
||||
'buttons' => ['save' => ''],
|
||||
'points' => 5,
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
$campaign->addEvent(0, $event);
|
||||
$campaign->setCanvasSettings(
|
||||
[
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => $event->getId(),
|
||||
'positionX' => '1080',
|
||||
'positionY' => '155',
|
||||
],
|
||||
[
|
||||
'id' => 'lists',
|
||||
'positionX' => '1180',
|
||||
'positionY' => '50',
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => $event->getId(),
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates campaign with email sent action.
|
||||
*
|
||||
* Campaign diagram:
|
||||
* -------------------
|
||||
* - Start segment -
|
||||
* -------------------
|
||||
* |
|
||||
* -------------------
|
||||
* - Send email -
|
||||
* -------------------
|
||||
*
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
*/
|
||||
public function createCampaignWithEmailSent(int $emailId): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test send email');
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$event1 = new Event();
|
||||
$event1->setCampaign($campaign);
|
||||
$event1->setName('Send email');
|
||||
$event1->setType('email.send');
|
||||
$event1->setChannel('email');
|
||||
$event1->setChannelId($emailId);
|
||||
$event1->setEventType('action');
|
||||
$event1->setTriggerMode('immediate');
|
||||
$event1->setOrder(1);
|
||||
$event1->setProperties(
|
||||
[
|
||||
'canvasSettings' => [
|
||||
'droppedX' => '549',
|
||||
'droppedY' => '155',
|
||||
],
|
||||
'name' => '',
|
||||
'triggerMode' => 'immediate',
|
||||
'triggerDate' => null,
|
||||
'triggerInterval' => '1',
|
||||
'triggerIntervalUnit' => 'd',
|
||||
'triggerHour' => '',
|
||||
'triggerRestrictedStartHour' => '',
|
||||
'triggerRestrictedStopHour' => '',
|
||||
'anchor' => 'leadsource',
|
||||
'properties' => [
|
||||
'email' => $emailId,
|
||||
'email_type' => 'transactional',
|
||||
'priority' => '2',
|
||||
'attempts' => '3',
|
||||
],
|
||||
'type' => 'email.send',
|
||||
'eventType' => 'action',
|
||||
'anchorEventType' => 'source',
|
||||
'campaignId' => 'mautic_ce6c7dddf8444e579d741c0125f18b33a5d49b45',
|
||||
'_token' => 'HgysZwvH_n0uAp47CcAcsGddRnRk65t-3crOnuLx28Y',
|
||||
'buttons' => [
|
||||
'save' => '',
|
||||
],
|
||||
'email' => $emailId,
|
||||
'email_type' => 'transactional',
|
||||
'priority' => 2,
|
||||
'attempts' => 3.0,
|
||||
]
|
||||
);
|
||||
$this->em->persist($event1);
|
||||
$this->em->flush();
|
||||
|
||||
$campaign->setCanvasSettings(
|
||||
[
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => $event1->getId(),
|
||||
'positionX' => '549',
|
||||
'positionY' => '155',
|
||||
],
|
||||
[
|
||||
'id' => 'lists',
|
||||
'positionX' => '796',
|
||||
'positionY' => '50',
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => $event1->getId(),
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
$campaign->addEvent($event1->getId(), $event1);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
public static function getPayload(): array
|
||||
{
|
||||
$fileContents = file_get_contents(__DIR__.'/entity_data.json');
|
||||
|
||||
return json_decode($fileContents, true);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Form\Validator\Constraints;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
final class InfiniteLoopValidatorFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('delayDataProvider')]
|
||||
public function testSubmitCampaignActionVariousDelayOptions(string $triggerMode, int $triggerInterval, string $triggerIntervalUnit, int $success, string $expectedString): void
|
||||
{
|
||||
$uri = '/s/campaigns/events/new?type=campaign.addremovelead&eventType=action&campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=leadsource&anchorEventType=source';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[anchor]' => 'leadsource',
|
||||
'campaignevent[properties][addTo]' => ['this'],
|
||||
'campaignevent[type]' => 'campaign.addremovelead',
|
||||
'campaignevent[eventType]' => 'action',
|
||||
'campaignevent[anchorEventType]' => 'source',
|
||||
'campaignevent[triggerMode]' => $triggerMode,
|
||||
'campaignevent[triggerInterval]' => $triggerInterval,
|
||||
'campaignevent[triggerIntervalUnit]' => $triggerIntervalUnit,
|
||||
'campaignevent[campaignId]' => 'mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775',
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $form->getPhpValues());
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
Assert::assertSame($success, $responseData['success'], $response->getContent());
|
||||
|
||||
if ($expectedString) {
|
||||
Assert::assertStringContainsString($expectedString, $responseData['newContent']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string,array<string|int>>
|
||||
*/
|
||||
public static function delayDataProvider(): iterable
|
||||
{
|
||||
yield 'The immediate mode cannot be allowed otherwise the contacts will loop too fast for no reason' => [
|
||||
'immediate',
|
||||
1,
|
||||
'i',
|
||||
0,
|
||||
'Campaign cannot restart itself without a delay. Please add at least 30 minute delay.',
|
||||
];
|
||||
|
||||
yield 'The interval mode with less than 30 minutes cannot be allowed' => [
|
||||
'interval',
|
||||
29,
|
||||
'i',
|
||||
0,
|
||||
'Your delay is only 29 minutes. It must be at least 30 minutes.',
|
||||
];
|
||||
|
||||
yield 'The interval mode with 30 minutes or more should be allowed' => [
|
||||
'interval',
|
||||
30,
|
||||
'i',
|
||||
1,
|
||||
'',
|
||||
];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('delayDataProvider')]
|
||||
public function testValidationViaCampaignApi(string $triggerMode, int $triggerInterval, string $triggerIntervalUnit, int $success, string $expectedString): void
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName('Test');
|
||||
$segment->setPublicName('Test');
|
||||
$segment->setAlias('test');
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
$payload = [
|
||||
'name' => 'Loop test',
|
||||
'events' => [
|
||||
[
|
||||
'id' => 'new_30',
|
||||
'name' => 'Change campaigns',
|
||||
'type' => 'campaign.addremovelead',
|
||||
'eventType' => 'action',
|
||||
'properties' => [
|
||||
'canvasSettings' => [
|
||||
'droppedX' => '833',
|
||||
'droppedY' => '155',
|
||||
],
|
||||
'triggerMode' => $triggerMode,
|
||||
'triggerInterval' => $triggerInterval,
|
||||
'triggerIntervalUnit' => $triggerIntervalUnit,
|
||||
'anchor' => 'leadsource',
|
||||
'properties' => [
|
||||
'addTo' => [
|
||||
'this',
|
||||
],
|
||||
],
|
||||
'type' => 'campaign.addremovelead',
|
||||
'eventType' => 'action',
|
||||
'anchorEventType' => 'source',
|
||||
'campaignId' => 'mautic_5d0923689420c9d3981255dc56b6308b92db82c2',
|
||||
'_token' => 'pDmdgUFBm2tj-Vu8IoAfiaVNYy8sdBNjwrGtO9Igut8',
|
||||
'addTo' => ['this'],
|
||||
'removeFrom' => [],
|
||||
],
|
||||
'triggerMode' => $triggerMode,
|
||||
'triggerInterval' => $triggerInterval,
|
||||
'triggerIntervalUnit' => $triggerIntervalUnit,
|
||||
'decisionPath' => null,
|
||||
'parent' => null,
|
||||
'children' => [],
|
||||
],
|
||||
[
|
||||
'id' => 'new_31',
|
||||
'name' => 'Change points',
|
||||
'type' => 'lead.changepoints',
|
||||
'eventType' => 'action',
|
||||
'properties' => [
|
||||
'canvasSettings' => [
|
||||
'droppedX' => '933',
|
||||
'droppedY' => '255',
|
||||
],
|
||||
'triggerMode' => $triggerMode,
|
||||
'triggerInterval' => $triggerInterval,
|
||||
'triggerIntervalUnit' => $triggerIntervalUnit,
|
||||
'anchor' => 'leadsource',
|
||||
'properties' => [
|
||||
'points' => 2,
|
||||
],
|
||||
'type' => 'lead.changepoints',
|
||||
'eventType' => 'action',
|
||||
'anchorEventType' => 'source',
|
||||
'campaignId' => 'mautic_5d0923689420c9d3981255dc56b6308b92db82c2',
|
||||
'_token' => 'pDmdgUFBm2tj-Vu8IoAfiaVNYy8sdBNjwrGtO9Igut8',
|
||||
'points' => 2,
|
||||
],
|
||||
'triggerMode' => $triggerMode,
|
||||
'triggerInterval' => $triggerInterval,
|
||||
'triggerIntervalUnit' => $triggerIntervalUnit,
|
||||
'decisionPath' => null,
|
||||
'parent' => null,
|
||||
'children' => [],
|
||||
],
|
||||
],
|
||||
'lists' => [$segment->getId()],
|
||||
'canvasSettings' => [
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => 'new_30',
|
||||
'positionX' => 833,
|
||||
'positionY' => 155,
|
||||
],
|
||||
[
|
||||
'id' => 'new_31',
|
||||
'positionX' => 833,
|
||||
'positionY' => 155,
|
||||
],
|
||||
[
|
||||
'id' => 'lists',
|
||||
'positionX' => 933,
|
||||
'positionY' => 50,
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => 'new_30',
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => 'new_31',
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$expectedStatusCode = $success ? 201 : 422;
|
||||
|
||||
$this->client->request('POST', '/api/campaigns/new', $payload);
|
||||
$response = $this->client->getResponse();
|
||||
self::assertResponseStatusCodeSame($expectedStatusCode, $response->getContent());
|
||||
|
||||
if ($expectedString) {
|
||||
Assert::assertStringContainsString($expectedString, $response->getContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Functional\Validator;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Tests\Functional\Controller\CampaignControllerTrait;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\FormBundle\Entity\Form;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class OrphanEventsValidationFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CampaignControllerTrait;
|
||||
|
||||
private const ORPHAN_EVENTS_ERROR_MESSAGE =
|
||||
'One or more events are orphaned and must be linked to a node before proceeding';
|
||||
|
||||
public function testCampaignWithConnectedEventsShouldSaveSuccessfully(): void
|
||||
{
|
||||
$campaign = $this->setupCampaignWithConnectedEvent();
|
||||
$version = $campaign->getVersion();
|
||||
|
||||
// Campaign with properly connected events should save without validation errors
|
||||
$this->refreshAndSubmitForm($campaign, ++$version);
|
||||
}
|
||||
|
||||
public function testCampaignWithOrphanEventsShouldFailValidation(): void
|
||||
{
|
||||
$campaign = $this->setupCampaignWithOrphanEvent();
|
||||
$version = $campaign->getVersion();
|
||||
|
||||
// Submit the form and expect validation to prevent save (version should not increment)
|
||||
$this->submitFormExpectingValidationFailure($campaign, $version);
|
||||
}
|
||||
|
||||
public function testCampaignWithMixedConnectedAndOrphanEventsShouldFailValidation(): void
|
||||
{
|
||||
$campaign = $this->setupCampaignWithMixedEvents();
|
||||
$version = $campaign->getVersion();
|
||||
|
||||
// Submit the form and expect validation to prevent save
|
||||
$this->submitFormExpectingValidationFailure($campaign, $version);
|
||||
}
|
||||
|
||||
public function testCampaignWithChainedEventsShouldSaveSuccessfully(): void
|
||||
{
|
||||
$campaign = $this->setupCampaignWithChainedEvents();
|
||||
$version = $campaign->getVersion();
|
||||
|
||||
// Campaign with properly chained events should save without validation errors
|
||||
$this->refreshAndSubmitForm($campaign, ++$version);
|
||||
}
|
||||
|
||||
public function testCampaignWithFormAsSourceShouldSaveSuccessfully(): void
|
||||
{
|
||||
$campaign = $this->setupCampaignWithFormAsSource();
|
||||
$version = $campaign->getVersion();
|
||||
|
||||
// Campaign with form as source and properly connected events should save without validation errors
|
||||
$this->refreshAndSubmitForm($campaign, ++$version);
|
||||
}
|
||||
|
||||
public function testCampaignWithFormAsSourceAndOrphanEventsShouldFailValidation(): void
|
||||
{
|
||||
$campaign = $this->setupCampaignWithFormAsSourceAndOrphanEvents();
|
||||
$version = $campaign->getVersion();
|
||||
|
||||
// Submit the form and expect validation to prevent save due to orphan events
|
||||
$this->submitFormExpectingValidationFailure($campaign, $version);
|
||||
}
|
||||
|
||||
private function setupCampaignWithConnectedEvent(): Campaign
|
||||
{
|
||||
$campaign = $this->createBaseCampaign();
|
||||
$this->addConnectedEventToCampaign($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function setupCampaignWithOrphanEvent(): Campaign
|
||||
{
|
||||
$campaign = $this->createBaseCampaign();
|
||||
|
||||
// Create an event but don't connect it via canvas settings (orphan)
|
||||
$orphanEvent = $this->createOrphanEvent($campaign);
|
||||
|
||||
// Set up canvas settings with the orphan event in nodes but not in connections
|
||||
$canvasSettings = [
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => 'lists',
|
||||
'positionX' => 100,
|
||||
'positionY' => 100,
|
||||
],
|
||||
[
|
||||
'id' => $orphanEvent->getId(),
|
||||
'positionX' => 300,
|
||||
'positionY' => 100,
|
||||
],
|
||||
],
|
||||
'connections' => [], // No connections - makes it an orphan
|
||||
];
|
||||
$campaign->setCanvasSettings($canvasSettings);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function setupCampaignWithMixedEvents(): Campaign
|
||||
{
|
||||
$campaign = $this->createBaseCampaign();
|
||||
|
||||
// Create a connected event
|
||||
$connectedEvent = new Event();
|
||||
$connectedEvent->setCampaign($campaign);
|
||||
$connectedEvent->setName('Connected Email');
|
||||
$connectedEvent->setType('email.send');
|
||||
$connectedEvent->setEventType('action');
|
||||
$connectedEvent->setProperties([]);
|
||||
$this->em->persist($connectedEvent);
|
||||
|
||||
// Create an orphan event
|
||||
$orphanEvent = $this->createOrphanEvent($campaign);
|
||||
|
||||
// Create canvas settings where only the first event is connected (second is orphan)
|
||||
$baseSettings = $this->createCanvasSettings($connectedEvent->getId());
|
||||
|
||||
// Add the orphan event to nodes but don't connect it
|
||||
$canvasSettings = $baseSettings;
|
||||
$canvasSettings['nodes'][] = [
|
||||
'id' => $orphanEvent->getId(),
|
||||
'positionX' => 500,
|
||||
'positionY' => 100,
|
||||
];
|
||||
|
||||
$campaign->setCanvasSettings($canvasSettings);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function setupCampaignWithChainedEvents(): Campaign
|
||||
{
|
||||
$campaign = $this->createBaseCampaign();
|
||||
|
||||
// Create a condition event
|
||||
$conditionEvent = new Event();
|
||||
$conditionEvent->setCampaign($campaign);
|
||||
$conditionEvent->setName('Check Country');
|
||||
$conditionEvent->setType('lead.field_value');
|
||||
$conditionEvent->setEventType('condition');
|
||||
$conditionEvent->setProperties([
|
||||
'field' => 'country',
|
||||
'operator' => '=',
|
||||
'value' => 'United States',
|
||||
]);
|
||||
$this->em->persist($conditionEvent);
|
||||
|
||||
// Create an action event chained to the condition using setParent()
|
||||
$actionEvent = new Event();
|
||||
$actionEvent->setCampaign($campaign);
|
||||
$actionEvent->setParent($conditionEvent);
|
||||
$actionEvent->setName('Send US Email');
|
||||
$actionEvent->setType('email.send');
|
||||
$actionEvent->setEventType('action');
|
||||
$actionEvent->setProperties([]);
|
||||
$this->em->persist($actionEvent);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
// Connect both events via canvas settings
|
||||
$canvasSettings = $this->createCanvasSettingsWithMultipleEvents(
|
||||
$conditionEvent->getId(),
|
||||
$actionEvent->getId()
|
||||
);
|
||||
$campaign->setCanvasSettings($canvasSettings);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function setupCampaignWithFormAsSource(): Campaign
|
||||
{
|
||||
$campaign = $this->createBaseCampaignWithForm();
|
||||
$this->addConnectedEventToCampaign($campaign, 'forms');
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function setupCampaignWithFormAsSourceAndOrphanEvents(): Campaign
|
||||
{
|
||||
$campaign = $this->createBaseCampaignWithForm();
|
||||
$this->addConnectedEventToCampaign($campaign, 'forms');
|
||||
|
||||
// Reload campaign after addConnectedEventToCampaign() clears the entity manager
|
||||
$campaign = $this->em->find(Campaign::class, $campaign->getId());
|
||||
|
||||
// Create an orphan event
|
||||
$orphanEvent = $this->createOrphanEvent($campaign);
|
||||
|
||||
// Add orphan event to canvas settings without connections
|
||||
$canvasSettings = $campaign->getCanvasSettings();
|
||||
$canvasSettings['nodes'][] = [
|
||||
'id' => $orphanEvent->getId(),
|
||||
'positionX' => 500,
|
||||
'positionY' => 100,
|
||||
];
|
||||
$campaign->setCanvasSettings($canvasSettings);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createBaseCampaign(): Campaign
|
||||
{
|
||||
$leadList = new LeadList();
|
||||
$leadList->setName('Test list');
|
||||
$leadList->setAlias('test-list');
|
||||
$leadList->setPublicName('Test list');
|
||||
$this->em->persist($leadList);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign');
|
||||
$campaign->setIsPublished(true);
|
||||
$campaign->setPublishUp(new \DateTime());
|
||||
$campaign->addList($leadList);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createBaseCampaignWithForm(): Campaign
|
||||
{
|
||||
$form = new Form();
|
||||
$form->setName('Test form');
|
||||
$form->setAlias('test-form');
|
||||
$this->em->persist($form);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign with form');
|
||||
$campaign->setIsPublished(true);
|
||||
$campaign->setPublishUp(new \DateTime());
|
||||
$campaign->addForm($form);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function submitFormExpectingValidationFailure(Campaign $campaign, int $originalVersion): void
|
||||
{
|
||||
// Submit the form and expect validation to prevent save (version should not increment)
|
||||
$crawler = $this->refreshPage($campaign);
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$newCrawler = $this->client->submit($form);
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
// Verify the validation error message is displayed
|
||||
Assert::assertStringContainsString(
|
||||
self::ORPHAN_EVENTS_ERROR_MESSAGE,
|
||||
$newCrawler->text()
|
||||
);
|
||||
|
||||
// Verify the campaign version was not incremented (save was prevented)
|
||||
$this->em->clear();
|
||||
$campaign = $this->em->find(Campaign::class, $campaign->getId());
|
||||
Assert::assertSame($originalVersion, $campaign->getVersion());
|
||||
}
|
||||
|
||||
private function addConnectedEventToCampaign(Campaign $campaign, string $sourceType = 'lists'): void
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setCampaign($campaign);
|
||||
$event->setName('Send Welcome Email');
|
||||
$event->setType('email.send');
|
||||
$event->setEventType('action');
|
||||
$event->setProperties([]);
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
// Connect the event to source via canvas settings
|
||||
$canvasSettings = $this->createCanvasSettings($event->getId(), $sourceType);
|
||||
$campaign->setCanvasSettings($canvasSettings);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
}
|
||||
|
||||
private function createOrphanEvent(Campaign $campaign): Event
|
||||
{
|
||||
$orphanEvent = new Event();
|
||||
$orphanEvent->setCampaign($campaign);
|
||||
$orphanEvent->setName('Orphan Email');
|
||||
$orphanEvent->setType('email.send');
|
||||
$orphanEvent->setEventType('action');
|
||||
$orphanEvent->setProperties([]);
|
||||
$this->em->persist($orphanEvent);
|
||||
$this->em->flush();
|
||||
|
||||
return $orphanEvent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Helper;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Event\CampaignLeadChangeEvent;
|
||||
use Mautic\CampaignBundle\Helper\CampaignEventHelper;
|
||||
use Mautic\CampaignBundle\Tests\CampaignTestAbstract;
|
||||
|
||||
class CampaignEventHelperTest extends CampaignTestAbstract
|
||||
{
|
||||
public function testValidateLeadChangeTriggerWithEmptyCampaigns(): void
|
||||
{
|
||||
$eventDetails = new CampaignLeadChangeEvent(new Campaign(), [], 'badaction');
|
||||
$event = [
|
||||
'properties' => [
|
||||
'campaigns' => [],
|
||||
'action' => 'added',
|
||||
],
|
||||
'campaign' => [
|
||||
'id' => null,
|
||||
],
|
||||
];
|
||||
$result = CampaignEventHelper::validateLeadChangeTrigger($eventDetails, $event);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testValidateLeadChangeTriggerWithUnmatchingCampaignsAndInvalidAction(): void
|
||||
{
|
||||
$eventDetails = new CampaignLeadChangeEvent(new Campaign(), [], 'badaction');
|
||||
$event = [
|
||||
'properties' => [
|
||||
'campaigns' => [3],
|
||||
'action' => 'added',
|
||||
],
|
||||
'campaign' => [
|
||||
'id' => 4,
|
||||
],
|
||||
];
|
||||
$result = CampaignEventHelper::validateLeadChangeTrigger($eventDetails, $event);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testValidateLeadChangeTriggerWithMatchingCampaignsAndInvalidAction(): void
|
||||
{
|
||||
$eventDetails = new CampaignLeadChangeEvent(new Campaign(), [], 'removed');
|
||||
$event = [
|
||||
'properties' => [
|
||||
'campaigns' => [3],
|
||||
'action' => 'added',
|
||||
],
|
||||
'campaign' => [
|
||||
'id' => 3,
|
||||
],
|
||||
];
|
||||
$result = CampaignEventHelper::validateLeadChangeTrigger($eventDetails, $event);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testValidateLeadChangeTriggerWithMatchingCampaignsAndVariousActions(): void
|
||||
{
|
||||
$actions = [
|
||||
'added' => true,
|
||||
'removed' => true,
|
||||
'invalid' => false,
|
||||
];
|
||||
|
||||
foreach ($actions as $action => $expectedResult) {
|
||||
$campaignId = 3;
|
||||
$eventDetails = new CampaignLeadChangeEvent(new Campaign(), [], $action);
|
||||
$event = [
|
||||
'properties' => [
|
||||
'campaigns' => [$campaignId, 8],
|
||||
'action' => $action,
|
||||
],
|
||||
'campaign' => [
|
||||
'id' => $campaignId,
|
||||
],
|
||||
];
|
||||
$result = CampaignEventHelper::validateLeadChangeTrigger($eventDetails, $event);
|
||||
$this->assertSame($expectedResult, $result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Helper;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor;
|
||||
use Mautic\CampaignBundle\Helper\ChannelExtractor;
|
||||
|
||||
class ChannelExtractorTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testChannelIsSet(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$config = $this->createMock(AbstractEventAccessor::class);
|
||||
$config->expects($this->once())
|
||||
->method('getChannel')
|
||||
->willReturn('email');
|
||||
|
||||
$log = new LeadEventLog();
|
||||
ChannelExtractor::setChannel($log, $event, $config);
|
||||
|
||||
$this->assertEquals('email', $log->getChannel());
|
||||
}
|
||||
|
||||
public function testChannelIsIgnoredIfSet(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$config = $this->createMock(AbstractEventAccessor::class);
|
||||
$config->expects($this->never())
|
||||
->method('getChannel');
|
||||
|
||||
$log = new LeadEventLog();
|
||||
$log->setChannel('page');
|
||||
ChannelExtractor::setChannel($log, $event, $config);
|
||||
|
||||
$this->assertEquals('page', $log->getChannel());
|
||||
}
|
||||
|
||||
public function testChannelIdIsSet(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setProperties(['email' => 1]);
|
||||
$config = $this->createMock(AbstractEventAccessor::class);
|
||||
$config->expects($this->once())
|
||||
->method('getChannel')
|
||||
->willReturn('email');
|
||||
|
||||
$config->expects($this->once())
|
||||
->method('getChannelIdField')
|
||||
->willReturn('email');
|
||||
|
||||
$log = new LeadEventLog();
|
||||
ChannelExtractor::setChannel($log, $event, $config);
|
||||
|
||||
$this->assertEquals('email', $log->getChannel());
|
||||
$this->assertEquals(1, $log->getChannelId());
|
||||
}
|
||||
|
||||
public function testChannelIdIsIgnoredIfPropertiesAreEmpty(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setProperties(null);
|
||||
$config = $this->createMock(AbstractEventAccessor::class);
|
||||
$config->expects($this->once())
|
||||
->method('getChannel')
|
||||
->willReturn('email');
|
||||
|
||||
$config->expects($this->once())
|
||||
->method('getChannelIdField')
|
||||
->willReturn('email');
|
||||
|
||||
$log = new LeadEventLog();
|
||||
ChannelExtractor::setChannel($log, $event, $config);
|
||||
|
||||
$this->assertEquals('email', $log->getChannel());
|
||||
$this->assertEquals(null, $log->getChannelId());
|
||||
}
|
||||
|
||||
public function testChannelIdIsIgnoredIfChannelIdFieldIsNotSet(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setProperties(['email' => 1]);
|
||||
$config = $this->createMock(AbstractEventAccessor::class);
|
||||
$config->expects($this->once())
|
||||
->method('getChannel')
|
||||
->willReturn('email');
|
||||
|
||||
$config->expects($this->once())
|
||||
->method('getChannelIdField')
|
||||
->willReturn(null);
|
||||
|
||||
$log = new LeadEventLog();
|
||||
ChannelExtractor::setChannel($log, $event, $config);
|
||||
|
||||
$this->assertEquals('email', $log->getChannel());
|
||||
$this->assertEquals(null, $log->getChannelId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Helper;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\EventRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContactFinder;
|
||||
use Mautic\CampaignBundle\Executioner\Helper\DecisionHelper;
|
||||
use Mautic\CampaignBundle\Executioner\Helper\InactiveHelper;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class InactiveHelperTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var EventScheduler|MockObject
|
||||
*/
|
||||
private MockObject $scheduler;
|
||||
|
||||
/**
|
||||
* @var InactiveContactFinder|MockObject
|
||||
*/
|
||||
private MockObject $inactiveContactFinder;
|
||||
|
||||
/**
|
||||
* @var LeadEventLogRepository|MockObject
|
||||
*/
|
||||
private MockObject $eventLogRepository;
|
||||
|
||||
/**
|
||||
* @var EventRepository|MockObject
|
||||
*/
|
||||
private MockObject $eventRepository;
|
||||
|
||||
/**
|
||||
* @var LeadRepository|MockObject
|
||||
*/
|
||||
private MockObject $leadRepository;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface|MockObject
|
||||
*/
|
||||
private MockObject $logger;
|
||||
|
||||
private InactiveHelper $inactiveHelper;
|
||||
|
||||
private DecisionHelper $decisionHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->scheduler = $this->createMock(EventScheduler::class);
|
||||
$this->inactiveContactFinder = $this->createMock(InactiveContactFinder::class);
|
||||
$this->eventLogRepository = $this->createMock(LeadEventLogRepository::class);
|
||||
$this->eventRepository = $this->createMock(EventRepository::class);
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->decisionHelper = new DecisionHelper($this->leadRepository);
|
||||
$this->inactiveHelper = new InactiveHelper(
|
||||
$this->scheduler,
|
||||
$this->inactiveContactFinder,
|
||||
$this->eventLogRepository,
|
||||
$this->eventRepository,
|
||||
$this->logger,
|
||||
$this->decisionHelper
|
||||
);
|
||||
}
|
||||
|
||||
public function testRemoveContactsThatAreNotApplicable(): void
|
||||
{
|
||||
$lastActiveEventId = 6;
|
||||
|
||||
// lead not applicable because of parent negative path taken
|
||||
$leadNegative = new Lead();
|
||||
$leadNegative->setId(9);
|
||||
|
||||
// lead not applicable because of parent positive path taken
|
||||
$leadNegative2 = new Lead();
|
||||
$leadNegative2->setId(10);
|
||||
|
||||
// applicable lead
|
||||
$leadPositive = new Lead();
|
||||
$leadPositive->setId(12);
|
||||
|
||||
// lead not applicable because of no parent event log
|
||||
$leadNegative3 = new Lead();
|
||||
$leadNegative3->setId(11);
|
||||
|
||||
$this->eventLogRepository->expects($this->once())
|
||||
->method('getDatesExecuted')
|
||||
->willReturn([
|
||||
$leadNegative->getId() => \DateTime::createFromFormat('Y-m-d H:i:s', '2022-05-28 21:37:00'),
|
||||
$leadNegative2->getId() => \DateTime::createFromFormat('Y-m-d H:i:s', '2022-05-28 21:37:00'),
|
||||
$leadPositive->getId() => \DateTime::createFromFormat('Y-m-d H:i:s', '2022-05-28 21:37:00'),
|
||||
$leadNegative3->getId() => \DateTime::createFromFormat('Y-m-d H:i:s', '2022-05-28 21:37:00'),
|
||||
]);
|
||||
|
||||
/** @var LeadEventLog&MockObject */
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->expects($this->exactly(3))
|
||||
->method('getNonActionPathTaken')
|
||||
->willReturnOnConsecutiveCalls(1, 0, 1);
|
||||
|
||||
/** @var Campaign&MockObject */
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
/** @var Event&MockObject */
|
||||
$parentEvent = $this->createMock(Event::class);
|
||||
$parentEvent->expects($this->exactly(4))
|
||||
->method('getLogByContactAndRotation')
|
||||
->willReturnOnConsecutiveCalls($log, $log, $log, null);
|
||||
|
||||
$event = new Event();
|
||||
$event->setParent($parentEvent);
|
||||
$event->setDecisionPath('yes');
|
||||
$event->setCampaign($campaign);
|
||||
$event->setEventType(Event::TYPE_DECISION);
|
||||
|
||||
$parentEvent->expects($this->any())
|
||||
->method('getNegativeChildren')
|
||||
->willReturnOnConsecutiveCalls(new ArrayCollection(), new ArrayCollection([$event]));
|
||||
|
||||
$parentEvent->expects($this->any())
|
||||
->method('getPositiveChildren')
|
||||
->willReturnOnConsecutiveCalls(new ArrayCollection(), new ArrayCollection());
|
||||
|
||||
$this->leadRepository->expects($this->exactly(4))
|
||||
->method('getContactRotations')
|
||||
->willReturn([]);
|
||||
|
||||
$this->scheduler->expects($this->any())
|
||||
->method('getExecutionDateTime')
|
||||
->willReturn(\DateTime::createFromFormat('Y-m-d H:i:s', '2022-05-30 12:00:00'));
|
||||
|
||||
$now = \DateTime::createFromFormat('Y-m-d H:i:s', '2022-05-31 12:00:00');
|
||||
$contacts = new ArrayCollection([
|
||||
$leadNegative->getId() => $leadNegative,
|
||||
$leadNegative2->getId() => $leadNegative2,
|
||||
$leadPositive->getId() => $leadPositive,
|
||||
$leadNegative3->getId() => $leadNegative3,
|
||||
]);
|
||||
|
||||
$this->inactiveHelper->removeContactsThatAreNotApplicable(
|
||||
$now,
|
||||
$contacts,
|
||||
$lastActiveEventId,
|
||||
new ArrayCollection([new Event()]),
|
||||
$event
|
||||
);
|
||||
|
||||
$this->assertEquals(1, $contacts->count());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Helper;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Model\NotificationModel;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Symfony\Component\Routing\Router;
|
||||
|
||||
class NotificationHelperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|UserModel
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $userModel;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|NotificationModel
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $notificationModel;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|Router
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $router;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|Translator
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $translator;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|CoreParametersHelper
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $coreParametersHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userModel = $this->createMock(UserModel::class);
|
||||
|
||||
$this->notificationModel = $this->createMock(NotificationModel::class);
|
||||
|
||||
$this->router = $this->createMock(Router::class);
|
||||
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
|
||||
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
}
|
||||
|
||||
public function testContactOwnerIsNotified(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
$user = $this->createMock(User::class);
|
||||
$user->method('getId')
|
||||
->willReturn('1');
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->once())
|
||||
->method('getOwner')
|
||||
->willReturn($user);
|
||||
|
||||
$this->userModel->expects($this->never())
|
||||
->method('getEntity');
|
||||
|
||||
$this->userModel->expects($this->never())
|
||||
->method('getSystemAdministrator');
|
||||
|
||||
$this->notificationModel->expects($this->once())
|
||||
->method('addNotification')
|
||||
->with(
|
||||
' / ',
|
||||
'error',
|
||||
false,
|
||||
$this->anything(),
|
||||
null,
|
||||
null,
|
||||
$user
|
||||
);
|
||||
|
||||
$this->getNotificationHelper()->notifyOfFailure($lead, $event);
|
||||
}
|
||||
|
||||
public function testCampaignCreatorIsNotified(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$campaign->setCreatedBy(1);
|
||||
|
||||
$user = $this->createMock(User::class);
|
||||
$user->method('getId')
|
||||
->willReturn('1');
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->once())
|
||||
->method('getOwner')
|
||||
->willReturn(null);
|
||||
|
||||
$this->userModel->expects($this->once())
|
||||
->method('getEntity')
|
||||
->willReturn($user);
|
||||
|
||||
$this->userModel->expects($this->never())
|
||||
->method('getSystemAdministrator');
|
||||
|
||||
$this->notificationModel->expects($this->once())
|
||||
->method('addNotification')
|
||||
->with(
|
||||
' / ',
|
||||
'error',
|
||||
false,
|
||||
$this->anything(),
|
||||
null,
|
||||
null,
|
||||
$user
|
||||
);
|
||||
|
||||
$this->getNotificationHelper()->notifyOfFailure($lead, $event);
|
||||
}
|
||||
|
||||
public function testSystemAdminIsNotified(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$campaign->setCreatedBy(2);
|
||||
|
||||
$user = $this->createMock(User::class);
|
||||
$user->method('getId')
|
||||
->willReturn('1');
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->once())
|
||||
->method('getOwner')
|
||||
->willReturn(null);
|
||||
|
||||
$this->userModel->expects($this->once())
|
||||
->method('getEntity')
|
||||
->willReturn(null);
|
||||
|
||||
$this->userModel->expects($this->once())
|
||||
->method('getSystemAdministrator')
|
||||
->willReturn($user);
|
||||
|
||||
$this->notificationModel->expects($this->once())
|
||||
->method('addNotification')
|
||||
->with(
|
||||
' / ',
|
||||
'error',
|
||||
false,
|
||||
$this->anything(),
|
||||
null,
|
||||
null,
|
||||
$user
|
||||
);
|
||||
|
||||
$this->getNotificationHelper()->notifyOfFailure($lead, $event);
|
||||
}
|
||||
|
||||
public function testNotificationIgnoredIfUserNotFound(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$campaign->setCreatedBy(2);
|
||||
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->once())
|
||||
->method('getOwner')
|
||||
->willReturn(null);
|
||||
|
||||
$this->userModel->expects($this->once())
|
||||
->method('getEntity')
|
||||
->willReturn(null);
|
||||
|
||||
$this->userModel->expects($this->once())
|
||||
->method('getSystemAdministrator')
|
||||
->willReturn(null);
|
||||
|
||||
$this->notificationModel->expects($this->never())
|
||||
->method('addNotification');
|
||||
|
||||
$this->getNotificationHelper()->notifyOfFailure($lead, $event);
|
||||
}
|
||||
|
||||
public function testNotificationOfUnpublishToAuthor(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$user = $this->createMock(User::class);
|
||||
$this->prepareCommonMocks($event, $user);
|
||||
|
||||
$this->coreParametersHelper
|
||||
->method('get')
|
||||
->with('campaign_send_notification_to_author')
|
||||
->willReturn(1);
|
||||
|
||||
$this->userModel->expects($this->once())
|
||||
->method('emailUser')
|
||||
->with($user, 'test', 'test');
|
||||
|
||||
$this->userModel->expects($this->never())
|
||||
->method('sendMailToEmailAddresses');
|
||||
|
||||
$this->getNotificationHelper()->notifyOfUnpublish($event);
|
||||
}
|
||||
|
||||
public function testNotificationOfUnpublishToEmailAddress(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$user = $this->createMock(User::class);
|
||||
$this->prepareCommonMocks($event, $user);
|
||||
|
||||
$emails = 'a@test.co, b@test.co';
|
||||
$this->coreParametersHelper->expects($this->exactly(2))
|
||||
->method('get')
|
||||
->willReturnMap([
|
||||
['campaign_send_notification_to_author', null, 0],
|
||||
['campaign_notification_email_addresses', null, $emails],
|
||||
]);
|
||||
|
||||
$this->userModel->expects($this->once())
|
||||
->method('sendMailToEmailAddresses')
|
||||
->with(array_map('trim', explode(',', $emails)), 'test', 'test');
|
||||
|
||||
$this->userModel->expects($this->never())
|
||||
->method('emailUser');
|
||||
|
||||
$this->getNotificationHelper()->notifyOfUnpublish($event);
|
||||
}
|
||||
|
||||
private function prepareCommonMocks(Event $event, User $user): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$event->setCampaign($campaign);
|
||||
$campaign->setCreatedBy(2);
|
||||
|
||||
$user = $this->createMock(User::class);
|
||||
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->any())
|
||||
->method('getOwner')
|
||||
->willReturn(null);
|
||||
|
||||
$user->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$this->userModel->expects($this->once())
|
||||
->method('getEntity')
|
||||
->willReturn($user);
|
||||
|
||||
$this->translator
|
||||
->expects($this->any())
|
||||
->method('trans')
|
||||
->willReturn('test');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NotificationHelper
|
||||
*/
|
||||
private function getNotificationHelper()
|
||||
{
|
||||
return new NotificationHelper(
|
||||
$this->userModel,
|
||||
$this->notificationModel,
|
||||
$this->translator,
|
||||
$this->router,
|
||||
$this->coreParametersHelper
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Membership\Action;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignMember;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository;
|
||||
use Mautic\CampaignBundle\Membership\Action\Adder;
|
||||
use Mautic\CampaignBundle\Membership\Exception\ContactCannotBeAddedToCampaignException;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class AdderTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var LeadRepository|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $leadRepository;
|
||||
|
||||
/**
|
||||
* @var LeadEventLogRepository|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $leadEventLogRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
$this->leadEventLogRepository = $this->createMock(LeadEventLogRepository::class);
|
||||
}
|
||||
|
||||
public function testNewMemberAdded(): void
|
||||
{
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('getId')
|
||||
->willReturn(1);
|
||||
$campaign->method('allowRestart')
|
||||
->willReturn(true);
|
||||
|
||||
$contact = $this->createMock(Lead::class);
|
||||
$contact->method('getId')
|
||||
->WillReturn(2);
|
||||
|
||||
$this->leadEventLogRepository->method('hasBeenInCampaignRotation')
|
||||
->with(2, 1, 1)
|
||||
->willReturn(true);
|
||||
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('saveEntity');
|
||||
|
||||
$campaignMember = $this->getAdder()->createNewMembership($contact, $campaign, true);
|
||||
|
||||
$this->assertEquals($contact, $campaignMember->getLead());
|
||||
$this->assertEquals($campaign, $campaignMember->getCampaign());
|
||||
$this->assertEquals(true, $campaignMember->wasManuallyAdded());
|
||||
$this->assertEquals(2, $campaignMember->getRotation());
|
||||
}
|
||||
|
||||
public function testManuallyRemovedAddedBackWhenManualActionAddsTheMember(): void
|
||||
{
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setManuallyRemoved(true);
|
||||
$campaignMember->setRotation(1);
|
||||
$campaign = new Campaign();
|
||||
$campaign->setAllowRestart(true);
|
||||
$campaignMember->setCampaign($campaign);
|
||||
|
||||
$this->getAdder()->updateExistingMembership($campaignMember, true);
|
||||
|
||||
$this->assertEquals(true, $campaignMember->wasManuallyAdded());
|
||||
$this->assertEquals(2, $campaignMember->getRotation());
|
||||
}
|
||||
|
||||
public function testFilterRemovedAddedBackWhenManualActionAddsTheMember(): void
|
||||
{
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setManuallyRemoved(true);
|
||||
$campaignMember->setRotation(1);
|
||||
$campaignMember->setDateLastExited(new \DateTime());
|
||||
$campaign = new Campaign();
|
||||
$campaign->setAllowRestart(true);
|
||||
$campaignMember->setCampaign($campaign);
|
||||
|
||||
$this->getAdder()->updateExistingMembership($campaignMember, false);
|
||||
|
||||
$this->assertEquals(false, $campaignMember->wasManuallyAdded());
|
||||
$this->assertEquals(2, $campaignMember->getRotation());
|
||||
}
|
||||
|
||||
public function testManuallyRemovedIsNotAddedBackWhenFilterActionAddsTheMember(): void
|
||||
{
|
||||
$this->expectException(ContactCannotBeAddedToCampaignException::class);
|
||||
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setManuallyRemoved(true);
|
||||
$campaignMember->setRotation(1);
|
||||
$campaign = new Campaign();
|
||||
$campaign->setAllowRestart(false);
|
||||
$campaignMember->setCampaign($campaign);
|
||||
|
||||
$this->getAdder()->updateExistingMembership($campaignMember, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Adder
|
||||
*/
|
||||
private function getAdder()
|
||||
{
|
||||
return new Adder($this->leadRepository, $this->leadEventLogRepository);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Membership\Action;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignMember;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository;
|
||||
use Mautic\CampaignBundle\Membership\Action\Remover;
|
||||
use Mautic\CampaignBundle\Membership\Exception\ContactAlreadyRemovedFromCampaignException;
|
||||
use Mautic\CoreBundle\Twig\Helper\DateHelper;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class RemoverTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var LeadRepository|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $leadRepository;
|
||||
|
||||
/**
|
||||
* @var LeadEventLogRepository|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $leadEventLogRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
$this->leadEventLogRepository = $this->createMock(LeadEventLogRepository::class);
|
||||
}
|
||||
|
||||
public function testMemberHasDateExitedSetWithForcedExit(): void
|
||||
{
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setManuallyRemoved(false);
|
||||
|
||||
$this->leadEventLogRepository->expects($this->once())
|
||||
->method('unscheduleEvents');
|
||||
|
||||
$this->getRemover()->updateExistingMembership($campaignMember, true);
|
||||
|
||||
$this->assertInstanceOf(\DateTime::class, $campaignMember->getDateLastExited());
|
||||
}
|
||||
|
||||
public function testMemberHasDateExistedSetToNullWhenRemovedByFilter(): void
|
||||
{
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setManuallyRemoved(false);
|
||||
|
||||
$this->leadEventLogRepository->expects($this->once())
|
||||
->method('unscheduleEvents');
|
||||
|
||||
$this->getRemover()->updateExistingMembership($campaignMember, false);
|
||||
|
||||
$this->assertNull($campaignMember->getDateLastExited());
|
||||
}
|
||||
|
||||
public function testExceptionThrownWhenMemberIsAlreadyRemoved(): void
|
||||
{
|
||||
$this->expectException(ContactAlreadyRemovedFromCampaignException::class);
|
||||
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setManuallyRemoved(true);
|
||||
|
||||
$this->getRemover()->updateExistingMembership($campaignMember, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Remover
|
||||
*/
|
||||
private function getRemover()
|
||||
{
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$dateTimeHelper = new DateHelper(
|
||||
'Y-m-d H:i:s',
|
||||
'Y-m-d H:i',
|
||||
'Y-m-d',
|
||||
'H:i',
|
||||
$translator,
|
||||
$this->createMock(\Mautic\CoreBundle\Helper\CoreParametersHelper::class)
|
||||
);
|
||||
|
||||
return new Remover($this->leadRepository, $this->leadEventLogRepository, $translator, $dateTimeHelper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Membership;
|
||||
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Event\CampaignLeadChangeEvent;
|
||||
use Mautic\CampaignBundle\Membership\Action\Adder;
|
||||
use Mautic\CampaignBundle\Membership\EventDispatcher;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class EventDispatcherTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $eventDispatcher;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
}
|
||||
|
||||
public function testLeadChangeEventDispatched(): void
|
||||
{
|
||||
$this->eventDispatcher->expects($this->once())
|
||||
->method('dispatch')
|
||||
->with($this->isInstanceOf(CampaignLeadChangeEvent::class), CampaignEvents::CAMPAIGN_ON_LEADCHANGE);
|
||||
|
||||
$this->getDispatcher()->dispatchMembershipChange(new Lead(), new Campaign(), Adder::NAME);
|
||||
}
|
||||
|
||||
public function testBatchChangeEventDispatched(): void
|
||||
{
|
||||
$this->eventDispatcher->expects($this->once())
|
||||
->method('dispatch')
|
||||
->with($this->isInstanceOf(CampaignLeadChangeEvent::class), CampaignEvents::LEAD_CAMPAIGN_BATCH_CHANGE);
|
||||
|
||||
$this->getDispatcher()->dispatchBatchMembershipChange([new Lead()], new Campaign(), Adder::NAME);
|
||||
}
|
||||
|
||||
private function getDispatcher()
|
||||
{
|
||||
return new EventDispatcher($this->eventDispatcher);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Membership;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository as CampaignMemberRepository;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CampaignBundle\Membership\MembershipBuilder;
|
||||
use Mautic\CampaignBundle\Membership\MembershipManager;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class MembershipBuilderTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MembershipManager|MockObject
|
||||
*/
|
||||
private MockObject $manager;
|
||||
|
||||
/**
|
||||
* @var CampaignMemberRepository|MockObject
|
||||
*/
|
||||
private MockObject $campaignMemberRepository;
|
||||
|
||||
/**
|
||||
* @var LeadRepository|MockObject
|
||||
*/
|
||||
private MockObject $leadRepository;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface|MockObject
|
||||
*/
|
||||
private MockObject $translator;
|
||||
|
||||
private MembershipBuilder $membershipBuilder;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->manager = $this->createMock(MembershipManager::class);
|
||||
$this->campaignMemberRepository = $this->createMock(CampaignMemberRepository::class);
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
$this->translator = $this->createMock(TranslatorInterface::class);
|
||||
$this->membershipBuilder = new MembershipBuilder(
|
||||
$this->manager,
|
||||
$this->campaignMemberRepository,
|
||||
$this->leadRepository,
|
||||
$this->translator
|
||||
);
|
||||
}
|
||||
|
||||
public function testContactCountIsSkippedWhenOutputIsNull(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$contactLimiter = new ContactLimiter(100);
|
||||
|
||||
$this->campaignMemberRepository->expects($this->never())
|
||||
->method('getCountsForCampaignContactsBySegment');
|
||||
|
||||
$this->campaignMemberRepository->expects($this->never())
|
||||
->method('getCountsForOrphanedContactsBySegments');
|
||||
|
||||
$this->campaignMemberRepository->expects($this->once())
|
||||
->method('getCampaignContactsBySegments')
|
||||
->willReturn([]);
|
||||
|
||||
$this->campaignMemberRepository->expects($this->once())
|
||||
->method('getOrphanedContacts')
|
||||
->willReturn([]);
|
||||
|
||||
$this->membershipBuilder->build($campaign, $contactLimiter, 1000);
|
||||
}
|
||||
|
||||
public function testContactsAreNotRemovedIfRunLimitReachedWhileAdding(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$contactLimiter = new ContactLimiter(100);
|
||||
|
||||
$this->campaignMemberRepository->expects($this->once())
|
||||
->method('getCampaignContactsBySegments')
|
||||
->willReturn([20, 21, 22]);
|
||||
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('getContactCollection')
|
||||
->willReturn(new ArrayCollection([new Lead(), new Lead(), new Lead()]));
|
||||
|
||||
$this->campaignMemberRepository->expects($this->never())
|
||||
->method('getOrphanedContacts');
|
||||
|
||||
$this->membershipBuilder->build($campaign, $contactLimiter, 2);
|
||||
}
|
||||
|
||||
public function testWhileLoopBreaksWithNoMoreContacts(): void
|
||||
{
|
||||
$campaign = new class extends Campaign {
|
||||
public function getId(): int
|
||||
{
|
||||
return 111;
|
||||
}
|
||||
};
|
||||
|
||||
$contactLimiter = new ContactLimiter(1);
|
||||
$matcher = $this->exactly(4);
|
||||
|
||||
$this->campaignMemberRepository->expects($matcher)
|
||||
->method('getCampaignContactsBySegments')->willReturnCallback(function (...$parameters) use ($matcher, $contactLimiter) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(111, $parameters[0]);
|
||||
$this->assertSame($contactLimiter, $parameters[1]);
|
||||
$this->assertFalse($parameters[2]);
|
||||
|
||||
return [20];
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(111, $parameters[0]);
|
||||
$this->assertSame($contactLimiter, $parameters[1]);
|
||||
$this->assertFalse($parameters[2]);
|
||||
|
||||
return [21];
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(111, $parameters[0]);
|
||||
$this->assertSame($contactLimiter, $parameters[1]);
|
||||
$this->assertFalse($parameters[2]);
|
||||
|
||||
return [22];
|
||||
}
|
||||
if (4 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(111, $parameters[0]);
|
||||
$this->assertSame($contactLimiter, $parameters[1]);
|
||||
$this->assertFalse($parameters[2]);
|
||||
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
$this->manager->expects($this->exactly(3))
|
||||
->method('addContacts');
|
||||
|
||||
$this->campaignMemberRepository->expects($this->exactly(4))
|
||||
->method('getOrphanedContacts')
|
||||
->willReturnOnConsecutiveCalls([23], [24], [25], []);
|
||||
|
||||
$this->manager->expects($this->exactly(3))
|
||||
->method('removeContacts');
|
||||
|
||||
$this->leadRepository->expects($this->exactly(6))
|
||||
->method('getContactCollection')
|
||||
->willReturn(new ArrayCollection([new Lead()]));
|
||||
|
||||
$this->membershipBuilder->build($campaign, $contactLimiter, 100);
|
||||
}
|
||||
|
||||
public function testWhileLoopBreaksWithNoMoreContactsForRepeatableCampaign(): void
|
||||
{
|
||||
$campaign = new class extends Campaign {
|
||||
public function getId(): int
|
||||
{
|
||||
return 111;
|
||||
}
|
||||
};
|
||||
|
||||
$campaign->setAllowRestart(true);
|
||||
|
||||
$contactLimiter = new ContactLimiter(1);
|
||||
$matcher = $this->exactly(4);
|
||||
|
||||
$this->campaignMemberRepository->expects($matcher)
|
||||
->method('getCampaignContactsBySegments')->willReturnCallback(function (...$parameters) use ($matcher, $contactLimiter) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(111, $parameters[0]);
|
||||
$this->assertSame($contactLimiter, $parameters[1]);
|
||||
$this->assertTrue($parameters[2]);
|
||||
|
||||
return [20];
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(111, $parameters[0]);
|
||||
$this->assertSame($contactLimiter, $parameters[1]);
|
||||
$this->assertTrue($parameters[2]);
|
||||
|
||||
return [21];
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(111, $parameters[0]);
|
||||
$this->assertSame($contactLimiter, $parameters[1]);
|
||||
$this->assertTrue($parameters[2]);
|
||||
|
||||
return [22];
|
||||
}
|
||||
if (4 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(111, $parameters[0]);
|
||||
$this->assertSame($contactLimiter, $parameters[1]);
|
||||
$this->assertTrue($parameters[2]);
|
||||
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
$this->manager->expects($this->exactly(3))
|
||||
->method('addContacts');
|
||||
|
||||
$this->campaignMemberRepository->expects($this->exactly(4))
|
||||
->method('getOrphanedContacts')
|
||||
->willReturnOnConsecutiveCalls([23], [24], [25], []);
|
||||
|
||||
$this->manager->expects($this->exactly(3))
|
||||
->method('removeContacts');
|
||||
|
||||
$this->leadRepository->expects($this->exactly(6))
|
||||
->method('getContactCollection')
|
||||
->willReturn(new ArrayCollection([new Lead()]));
|
||||
|
||||
$this->membershipBuilder->build($campaign, $contactLimiter, 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Membership;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignMember;
|
||||
use Mautic\CampaignBundle\Entity\LeadRepository;
|
||||
use Mautic\CampaignBundle\Membership\Action\Adder;
|
||||
use Mautic\CampaignBundle\Membership\Action\Remover;
|
||||
use Mautic\CampaignBundle\Membership\EventDispatcher;
|
||||
use Mautic\CampaignBundle\Membership\MembershipManager;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class MembershipManagerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var Adder|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $adder;
|
||||
|
||||
/**
|
||||
* @var Remover|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $remover;
|
||||
|
||||
/**
|
||||
* @var EventDispatcher|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $eventDispatcher;
|
||||
|
||||
/**
|
||||
* @var LeadRepository|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $leadRepository;
|
||||
|
||||
private NullLogger $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->adder = $this->createMock(Adder::class);
|
||||
$this->remover = $this->createMock(Remover::class);
|
||||
$this->eventDispatcher = $this->createMock(EventDispatcher::class);
|
||||
$this->leadRepository = $this->createMock(LeadRepository::class);
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
public function testMembershipCreatedIfNotFound(): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$campaign = new Campaign();
|
||||
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('findOneBy')
|
||||
->willReturn(null);
|
||||
|
||||
$this->adder->expects($this->once())
|
||||
->method('createNewMembership');
|
||||
|
||||
$this->eventDispatcher->expects($this->once())
|
||||
->method('dispatchMembershipChange');
|
||||
|
||||
$this->getManager()->addContact($contact, $campaign);
|
||||
}
|
||||
|
||||
public function testMembershipUpdatedIfFound(): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$campaign = new Campaign();
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setLead($contact);
|
||||
$campaignMember->setCampaign($campaign);
|
||||
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('findOneBy')
|
||||
->willReturn($campaignMember);
|
||||
|
||||
$this->adder->expects($this->once())
|
||||
->method('updateExistingMembership');
|
||||
|
||||
$this->eventDispatcher->expects($this->once())
|
||||
->method('dispatchMembershipChange');
|
||||
|
||||
$this->getManager()->addContact($contact, $campaign);
|
||||
}
|
||||
|
||||
public function testMembershipIsUpdatedWhenRemoved(): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$campaign = new Campaign();
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setLead($contact);
|
||||
$campaignMember->setCampaign($campaign);
|
||||
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('findOneBy')
|
||||
->willReturn($campaignMember);
|
||||
|
||||
$this->remover->expects($this->once())
|
||||
->method('updateExistingMembership');
|
||||
|
||||
$this->eventDispatcher->expects($this->once())
|
||||
->method('dispatchMembershipChange');
|
||||
|
||||
$this->getManager()->removeContact($contact, $campaign);
|
||||
}
|
||||
|
||||
public function testContactsAreAddedOrUpdated(): void
|
||||
{
|
||||
$contact = $this->createMock(Lead::class);
|
||||
$contact->method('getId')
|
||||
->willReturn(1);
|
||||
$contact2 = $this->createMock(Lead::class);
|
||||
$contact2->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setLead($contact2);
|
||||
$campaignMember->setCampaign($campaign);
|
||||
|
||||
// One is found and one is not
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('getCampaignMembers')
|
||||
->willReturn([$contact2->getId() => $campaignMember]);
|
||||
|
||||
$this->adder->expects($this->once())
|
||||
->method('updateExistingMembership')
|
||||
->with($campaignMember, true);
|
||||
|
||||
$this->adder->expects($this->once())
|
||||
->method('createNewMembership')
|
||||
->with($contact, $campaign, true);
|
||||
|
||||
$this->eventDispatcher->expects($this->once())
|
||||
->method('dispatchBatchMembershipChange')
|
||||
->with([$contact->getId() => $contact, $contact2->getId() => $contact2], $campaign, Adder::NAME);
|
||||
|
||||
$this->getManager()->addContacts(new ArrayCollection([1 => $contact, 2 => $contact2]), $campaign);
|
||||
}
|
||||
|
||||
public function testContactsAreRemoved(): void
|
||||
{
|
||||
$contact = $this->createMock(Lead::class);
|
||||
$contact->method('getId')
|
||||
->willReturn(1);
|
||||
$contact2 = $this->createMock(Lead::class);
|
||||
$contact2->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaignMember = new CampaignMember();
|
||||
$campaignMember->setLead($contact2);
|
||||
$campaignMember->setCampaign($campaign);
|
||||
|
||||
// One is found and one is not
|
||||
$this->leadRepository->expects($this->once())
|
||||
->method('getCampaignMembers')
|
||||
->willReturn([$contact2->getId() => $campaignMember]);
|
||||
|
||||
$this->remover->expects($this->once())
|
||||
->method('updateExistingMembership')
|
||||
->with($campaignMember, false);
|
||||
|
||||
$this->eventDispatcher->expects($this->once())
|
||||
->method('dispatchBatchMembershipChange')
|
||||
->with([$contact2->getId() => $contact2], $campaign, Remover::NAME);
|
||||
|
||||
$this->getManager()->removeContacts(new ArrayCollection([1 => $contact, 2 => $contact2]), $campaign);
|
||||
}
|
||||
|
||||
private function getManager()
|
||||
{
|
||||
return new MembershipManager($this->adder, $this->remover, $this->eventDispatcher, $this->leadRepository, $this->logger);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Model;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
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;
|
||||
|
||||
class CampaignModelFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testGetEmailsCountryStats(): void
|
||||
{
|
||||
/** @var CampaignModel $model */
|
||||
$model = $this->getContainer()->get('mautic.campaign.model.campaign');
|
||||
|
||||
$dateFrom = new \DateTimeImmutable('2023-07-21');
|
||||
$dateTo = new \DateTimeImmutable('2023-07-24');
|
||||
$leadsPayload = [
|
||||
[
|
||||
'email' => 'example1@test.com',
|
||||
'country' => 'Spain',
|
||||
'read' => true,
|
||||
'click' => true,
|
||||
],
|
||||
[
|
||||
'email' => 'example2@test.com',
|
||||
'country' => 'Spain',
|
||||
'read' => true,
|
||||
'click' => false,
|
||||
],
|
||||
[
|
||||
'email' => 'example3@test.com',
|
||||
'country' => 'Spain',
|
||||
'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,
|
||||
],
|
||||
];
|
||||
|
||||
// Create test campaign
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
foreach ($leadsPayload as $key => $values) {
|
||||
// Create lead
|
||||
$lead = new Lead();
|
||||
$lead->setEmail($values['email']);
|
||||
$lead->setCountry($values['country']);
|
||||
$this->em->persist($lead);
|
||||
|
||||
$leadsArr[] = [
|
||||
'lead' => $lead,
|
||||
'read' => $values['read'],
|
||||
'click' => $values['click'],
|
||||
];
|
||||
|
||||
// Create campaign lead and assign to lead
|
||||
$campaignLead = new CampaignLead();
|
||||
$campaignLead->setLead($lead);
|
||||
$campaignLead->setDateAdded(new \DateTime('2023-07-22'));
|
||||
$campaignLead->setManuallyAdded(true);
|
||||
$campaignLead->setCampaign($campaign);
|
||||
$this->em->persist($campaignLead);
|
||||
$this->em->flush();
|
||||
|
||||
$campaign->addLead($key, $campaignLead);
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 4; ++$i) {
|
||||
// Create email
|
||||
$email = new Email();
|
||||
$email->setName('Test email '.$i);
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
// Create email events
|
||||
$event = new Event();
|
||||
$event->setName('Send email '.$i);
|
||||
$event->setType('email.send');
|
||||
$event->setEventType('action');
|
||||
$event->setChannel('email');
|
||||
$event->setChannelId($email->getId());
|
||||
$event->setCampaign($campaign);
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
// Add events to campaign
|
||||
$campaign->addEvent($i, $event);
|
||||
|
||||
// Create campaign email sending statistics
|
||||
foreach ($leadsArr as $value) {
|
||||
$this->emulateEmailStat($value['lead'], $email, $value['read'], $event->getId());
|
||||
|
||||
if ($value['read'] && $value['click']) {
|
||||
$hits = rand(1, 5);
|
||||
$uniqueHits = rand(1, $hits);
|
||||
$this->emulateClick($value['lead'], $email, $hits, $uniqueHits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$results = $model->getCountryStats($campaign, $dateFrom, $dateTo);
|
||||
|
||||
$this->assertCount(4, $campaign->getEmailSendEvents());
|
||||
$this->assertCount(3, $results);
|
||||
$this->assertSame([
|
||||
'contacts' => [
|
||||
[
|
||||
'country' => '',
|
||||
'contacts' => '1',
|
||||
],
|
||||
[
|
||||
'country' => 'Poland',
|
||||
'contacts' => '2',
|
||||
],
|
||||
[
|
||||
'country' => 'Spain',
|
||||
'contacts' => '3',
|
||||
],
|
||||
],
|
||||
'clicked_through_count' => [
|
||||
[
|
||||
'clicked_through_count' => '4',
|
||||
'country' => '',
|
||||
],
|
||||
[
|
||||
'clicked_through_count' => '4',
|
||||
'country' => 'Poland',
|
||||
],
|
||||
[
|
||||
'clicked_through_count' => '4',
|
||||
'country' => 'Spain',
|
||||
],
|
||||
],
|
||||
'read_count' => [
|
||||
[
|
||||
'read_count' => '4',
|
||||
'country' => '',
|
||||
],
|
||||
[
|
||||
'read_count' => '8',
|
||||
'country' => 'Poland',
|
||||
],
|
||||
[
|
||||
'read_count' => '8',
|
||||
'country' => 'Spain',
|
||||
],
|
||||
],
|
||||
], $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testGetContextEntity(): void
|
||||
{
|
||||
/** @var CampaignModel $model */
|
||||
$model = $this->getContainer()->get('mautic.campaign.model.campaign');
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test email');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$id = $campaign->getId();
|
||||
$result = $model->getEntity($id);
|
||||
|
||||
$this->assertSame($campaign, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function emulateEmailStat(Lead $lead, Email $email, bool $isRead, int $sourceId): void
|
||||
{
|
||||
$stat = new Stat();
|
||||
$stat->setEmailAddress('test@test.com');
|
||||
$stat->setLead($lead);
|
||||
$stat->setDateSent(new \DateTime('2023-07-22'));
|
||||
$stat->setEmail($email);
|
||||
$stat->setIsRead($isRead);
|
||||
$stat->setSource('campaign.event');
|
||||
$stat->setSourceId($sourceId);
|
||||
$this->em->persist($stat);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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->setUniqueHits($uniqueHits);
|
||||
$redirect->setHits($hits);
|
||||
$this->em->persist($redirect);
|
||||
|
||||
$trackable = new Trackable();
|
||||
$trackable->setChannelId($email->getId());
|
||||
$trackable->setHits($hits);
|
||||
$trackable->setChannel('email');
|
||||
$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('2023-07-22'));
|
||||
$pageHit->setCode(200);
|
||||
$pageHit->setUrl($redirect->getUrl());
|
||||
$pageHit->setTrackingId($redirect->getRedirectId());
|
||||
$pageHit->setSource('email');
|
||||
$pageHit->setSourceId($email->getId());
|
||||
$this->em->persist($pageHit);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Model;
|
||||
|
||||
use Mautic\CampaignBundle\Tests\CampaignTestAbstract;
|
||||
|
||||
class CampaignModelTest extends CampaignTestAbstract
|
||||
{
|
||||
public function testGetSourceListsWithNull(): void
|
||||
{
|
||||
$model = $this->initCampaignModel();
|
||||
$lists = $model->getSourceLists();
|
||||
$this->assertTrue(isset($lists['lists']));
|
||||
$this->assertSame([parent::$mockId => parent::$mockName], $lists['lists']);
|
||||
$this->assertTrue(isset($lists['forms']));
|
||||
$this->assertSame([parent::$mockId => parent::$mockName], $lists['forms']);
|
||||
}
|
||||
|
||||
public function testGetSourceListsWithLists(): void
|
||||
{
|
||||
$model = $this->initCampaignModel();
|
||||
$lists = $model->getSourceLists('lists');
|
||||
$this->assertSame([parent::$mockId => parent::$mockName], $lists);
|
||||
}
|
||||
|
||||
public function testGetSourceListsWithForms(): void
|
||||
{
|
||||
$model = $this->initCampaignModel();
|
||||
$lists = $model->getSourceLists('forms');
|
||||
$this->assertSame([parent::$mockId => parent::$mockName], $lists);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Model;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\CampaignRepository;
|
||||
use Mautic\CampaignBundle\EventCollector\EventCollector;
|
||||
use Mautic\CampaignBundle\Membership\MembershipBuilder;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\FormBundle\Model\FormModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class CampaignModelTransactionalTest extends TestCase
|
||||
{
|
||||
private MockObject $entityManagerMock;
|
||||
private MockObject $connectionMock;
|
||||
private MockObject $userHelperMock;
|
||||
private MockObject $campaignRepositoryMock;
|
||||
private MockObject $campaignModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->connectionMock = $this->createMock(Connection::class);
|
||||
|
||||
// Create repository mock
|
||||
$this->campaignRepositoryMock = $this->createMock(CampaignRepository::class);
|
||||
$this->campaignRepositoryMock->method('setCurrentUser')
|
||||
->willReturnSelf();
|
||||
|
||||
$this->entityManagerMock = $this->createMock(EntityManager::class);
|
||||
$this->entityManagerMock->method('getConnection')
|
||||
->willReturn($this->connectionMock);
|
||||
|
||||
$this->entityManagerMock->method('getRepository')
|
||||
->with(Campaign::class)
|
||||
->willReturn($this->campaignRepositoryMock);
|
||||
|
||||
// Mock user helper
|
||||
$this->userHelperMock = $this->createMock(UserHelper::class);
|
||||
|
||||
// Create all the required dependencies as mocks
|
||||
$leadListModel = $this->createMock(ListModel::class);
|
||||
$formModel = $this->createMock(FormModel::class);
|
||||
$eventCollector = $this->createMock(EventCollector::class);
|
||||
$membershipBuilder = $this->createMock(MembershipBuilder::class);
|
||||
$contactTracker = $this->createMock(ContactTracker::class);
|
||||
$security = $this->createMock(CorePermissions::class);
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$router = $this->createMock(UrlGeneratorInterface::class);
|
||||
$translator = $this->createMock(Translator::class);
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
|
||||
// Create the campaign model mock
|
||||
$this->campaignModel = $this->getMockBuilder(CampaignModel::class)
|
||||
->setConstructorArgs([
|
||||
$leadListModel,
|
||||
$formModel,
|
||||
$eventCollector,
|
||||
$membershipBuilder,
|
||||
$contactTracker,
|
||||
$this->entityManagerMock,
|
||||
$security,
|
||||
$dispatcher,
|
||||
$router,
|
||||
$translator,
|
||||
$this->userHelperMock,
|
||||
$logger,
|
||||
$coreParametersHelper,
|
||||
])
|
||||
->onlyMethods(['saveEntity'])
|
||||
->getMock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to set up common campaign mock expectations for unpublish tests.
|
||||
*
|
||||
* @param int $campaignId The campaign ID to use
|
||||
* @param int $version The campaign version to use
|
||||
* @param bool $isPublished The current published state
|
||||
*
|
||||
* @return MockObject&Campaign The configured campaign mock
|
||||
*/
|
||||
private function createCampaignMockForUnpublish(
|
||||
int $campaignId = 5,
|
||||
int $version = 1,
|
||||
bool $isPublished = true,
|
||||
): MockObject&Campaign {
|
||||
/** @var MockObject&Campaign $campaignMock */
|
||||
$campaignMock = $this->createMock(Campaign::class);
|
||||
|
||||
$campaignMock->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn($campaignId);
|
||||
|
||||
// Mock version data from repository
|
||||
$this->campaignRepositoryMock->expects($this->once())
|
||||
->method('getCampaignPublishAndVersionData')
|
||||
->with($campaignId)
|
||||
->willReturn([
|
||||
'is_published' => $isPublished ? 1 : 0,
|
||||
'version' => $version,
|
||||
]);
|
||||
|
||||
$campaignMock->expects($this->once())
|
||||
->method('getVersion')
|
||||
->willReturn($version);
|
||||
|
||||
// Setting published flag
|
||||
$campaignMock->expects($this->once())
|
||||
->method('setIsPublished')
|
||||
->with(false);
|
||||
|
||||
$campaignMock->expects($this->once())
|
||||
->method('markForVersionIncrement');
|
||||
|
||||
return $campaignMock;
|
||||
}
|
||||
|
||||
public function testTransactionalCampaignUnPublish(): void
|
||||
{
|
||||
$campaignMock = $this->createCampaignMockForUnpublish();
|
||||
|
||||
// Saving the entity
|
||||
$this->campaignModel->expects($this->once())
|
||||
->method('saveEntity')
|
||||
->with($campaignMock);
|
||||
|
||||
$this->campaignModel->transactionalCampaignUnPublish($campaignMock);
|
||||
}
|
||||
|
||||
public function testTransactionalCampaignUnPublishWithException(): void
|
||||
{
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage('Database error');
|
||||
|
||||
$campaignMock = $this->createCampaignMockForUnpublish();
|
||||
|
||||
// Saving the entity throws an exception
|
||||
$this->campaignModel->expects($this->once())
|
||||
->method('saveEntity')
|
||||
->with($campaignMock)
|
||||
->willThrowException(new \Exception('Database error'));
|
||||
|
||||
$this->campaignModel->transactionalCampaignUnPublish($campaignMock);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\EventRepository;
|
||||
use Mautic\CampaignBundle\Event\DeleteEvent;
|
||||
use Mautic\CampaignBundle\Model\EventModel;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class EventModelTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface|MockObject
|
||||
*/
|
||||
private $entityManagerMock;
|
||||
|
||||
/**
|
||||
* @var EventRepository|MockObject
|
||||
*/
|
||||
private $eventRepositoryMock;
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface|MockObject
|
||||
*/
|
||||
private $dispatcherMock;
|
||||
|
||||
private MockObject|EventModel $eventModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->entityManagerMock = $this->createMock(EntityManagerInterface::class);
|
||||
$this->eventRepositoryMock = $this->createMock(EventRepository::class);
|
||||
$this->dispatcherMock = $this->createMock(EventDispatcherInterface::class);
|
||||
|
||||
$this->eventModel = new EventModel(
|
||||
$this->entityManagerMock,
|
||||
$this->createMock(CorePermissions::class),
|
||||
$this->dispatcherMock,
|
||||
$this->createMock(UrlGeneratorInterface::class),
|
||||
$this->createMock(Translator::class),
|
||||
$this->createMock(UserHelper::class),
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(CoreParametersHelper::class)
|
||||
);
|
||||
}
|
||||
|
||||
public function testThatClonedEventsDoNotAttemptNullingParentInDeleteEvents(): void
|
||||
{
|
||||
$this->entityManagerMock->expects($this->never())
|
||||
->method('getRepository')
|
||||
->with(Event::class)
|
||||
->willReturn($this->eventRepositoryMock);
|
||||
|
||||
$currentEvents = [
|
||||
'new1',
|
||||
'new2',
|
||||
'new3',
|
||||
];
|
||||
|
||||
$deletedEvents = [
|
||||
'new1',
|
||||
];
|
||||
|
||||
$this->eventModel->deleteEvents($currentEvents, $deletedEvents);
|
||||
}
|
||||
|
||||
public function testThatItDeletesEventLogs(): void
|
||||
{
|
||||
$idToDelete = 'old1';
|
||||
|
||||
$currentEvents = [
|
||||
'new1',
|
||||
];
|
||||
|
||||
$deletedEvents = [
|
||||
'new1',
|
||||
$idToDelete,
|
||||
];
|
||||
|
||||
$this->entityManagerMock->method('getRepository')
|
||||
->with(Event::class)
|
||||
->willReturn($this->eventRepositoryMock);
|
||||
|
||||
$this->eventRepositoryMock->expects($this->once())
|
||||
->method('nullEventRelationships')
|
||||
->with([$idToDelete]);
|
||||
|
||||
$this->eventRepositoryMock->expects($this->once())
|
||||
->method('setEventsAsDeleted')
|
||||
->with([1 => $idToDelete]);
|
||||
|
||||
$this->dispatcherMock->expects($this->once())
|
||||
->method('dispatch')
|
||||
->with(new DeleteEvent([$idToDelete]), CampaignEvents::ON_EVENT_DELETE);
|
||||
|
||||
$this->eventModel->deleteEvents($currentEvents, $deletedEvents);
|
||||
}
|
||||
|
||||
public function testDeleteEventsByCampaignId(): void
|
||||
{
|
||||
/** @var EventModel&MockObject */
|
||||
$mockModel = $this->getMockBuilder(EventModel::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['getRepository', 'deleteEventsByEventIds'])
|
||||
->getMock();
|
||||
|
||||
$mockModel->expects($this->once())
|
||||
->method('getRepository')
|
||||
->willReturn($this->eventRepositoryMock);
|
||||
|
||||
$campaignEvents = ['1', '2', '3'];
|
||||
|
||||
$this->eventRepositoryMock->expects($this->once())
|
||||
->method('getCampaignEventIds')
|
||||
->with(1)
|
||||
->willReturn($campaignEvents);
|
||||
|
||||
$mockModel->expects($this->once())->method('deleteEventsByEventIds')
|
||||
->with($campaignEvents);
|
||||
|
||||
$mockModel->deleteEventsByCampaignId(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Service;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\CampaignRepository;
|
||||
use Mautic\CampaignBundle\Service\CampaignAuditService;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\EmailRepository;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class CampaignAuditServiceTest extends TestCase
|
||||
{
|
||||
private MockObject $flashBag;
|
||||
private MockObject $urlGenerator;
|
||||
private MockObject $campaignRepository;
|
||||
private MockObject $emailRepository;
|
||||
private CampaignAuditService $campaignAuditService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->flashBag = $this->createMock(FlashBag::class);
|
||||
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$this->campaignRepository = $this->createMock(CampaignRepository::class);
|
||||
$this->emailRepository = $this->createMock(EmailRepository::class);
|
||||
|
||||
$this->campaignAuditService = new CampaignAuditService(
|
||||
$this->flashBag,
|
||||
$this->urlGenerator,
|
||||
$this->campaignRepository,
|
||||
$this->emailRepository,
|
||||
);
|
||||
}
|
||||
|
||||
public function testAddWarningForUnpublishedEmails(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setPublishDown(new \DateTime('-1 day'));
|
||||
|
||||
$email1 = new Email();
|
||||
$email1->setIsPublished(false);
|
||||
|
||||
$email2 = new Email();
|
||||
$email2->setIsPublished(true);
|
||||
$email2->setPublishDown(new \DateTime('-1 day'));
|
||||
|
||||
$this->campaignRepository->expects($this->once())
|
||||
->method('fetchEmailIdsById')
|
||||
->with($campaign->getId())
|
||||
->willReturn([1, 2]);
|
||||
|
||||
$this->emailRepository->expects($this->once())
|
||||
->method('findBy')
|
||||
->with(['id' => [1, 2]])
|
||||
->willReturn([$email1, $email2]);
|
||||
|
||||
$this->urlGenerator->expects($this->exactly(2))
|
||||
->method('generate')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
'/s/emails/edit/1',
|
||||
'/s/emails/edit/2'
|
||||
);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->flashBag->expects($matcher)
|
||||
->method('add')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('mautic.core.notice.campaign.unpublished.email', $parameters[0]);
|
||||
$this->assertSame([
|
||||
'%name%' => null,
|
||||
'%menu_link%' => 'mautic_email_index',
|
||||
'%url%' => '/s/emails/edit/1',
|
||||
], $parameters[1]);
|
||||
$this->assertSame(FlashBag::LEVEL_WARNING, $parameters[2]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('mautic.core.notice.campaign.unpublished.email', $parameters[0]);
|
||||
$this->assertSame([
|
||||
'%name%' => null,
|
||||
'%menu_link%' => 'mautic_email_index',
|
||||
'%url%' => '/s/emails/edit/2',
|
||||
], $parameters[1]);
|
||||
$this->assertSame(FlashBag::LEVEL_WARNING, $parameters[2]);
|
||||
}
|
||||
});
|
||||
|
||||
$this->campaignAuditService->addWarningForUnpublishedEmails($campaign);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user