Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user