Initial commit: CloudOps infrastructure platform

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

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PointBundle\Entity\Point;
use Mautic\ProjectBundle\Entity\Project;
use PHPUnit\Framework\Assert;
final class PointControllerTest extends MauticMysqlTestCase
{
public function testPointWithProject(): void
{
$point = new Point();
$point->setName('test');
$point->setType('url.hit');
$this->em->persist($point);
$project = new Project();
$project->setName('Test Project');
$this->em->persist($project);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request('GET', '/s/points/edit/'.$point->getId());
$form = $crawler->selectButton('Save')->form();
$form['point[projects]']->setValue((string) $project->getId());
$this->client->submit($form);
$this->assertResponseIsSuccessful();
$savedAsset = $this->em->find(Point::class, $point->getId());
Assert::assertSame($project->getId(), $savedAsset->getProjects()->first()->getId());
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Functional\Controller;
use Mautic\PointBundle\Entity\Point;
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
final class PointProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
{
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
{
$projectOne = $this->createProject('Project One');
$projectTwo = $this->createProject('Project Two');
$projectThree = $this->createProject('Project Three');
$pointAlpha = $this->createPoint('Point Alpha');
$pointBeta = $this->createPoint('Point Beta');
$this->createPoint('Point Gamma');
$this->createPoint('Point Delta');
$pointAlpha->addProject($projectOne);
$pointAlpha->addProject($projectTwo);
$pointBeta->addProject($projectTwo);
$pointBeta->addProject($projectThree);
$this->em->flush();
$this->em->clear();
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/points', '/s/points']);
}
/**
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
*/
public static function searchDataProvider(): \Generator
{
yield 'search by one project' => [
'searchTerm' => 'project:"Project Two"',
'expectedEntities' => ['Point Alpha', 'Point Beta'],
'unexpectedEntities' => ['Point Gamma', 'Point Delta'],
];
yield 'search by one project AND point name' => [
'searchTerm' => 'project:"Project Two" AND Beta',
'expectedEntities' => ['Point Beta'],
'unexpectedEntities' => ['Point Alpha', 'Point Gamma', 'Point Delta'],
];
yield 'search by one project OR point name' => [
'searchTerm' => 'project:"Project Two" OR Gamma',
'expectedEntities' => ['Point Alpha', 'Point Beta', 'Point Gamma'],
'unexpectedEntities' => ['Point Delta'],
];
yield 'search by NOT one project' => [
'searchTerm' => '!project:"Project Two"',
'expectedEntities' => ['Point Gamma', 'Point Delta'],
'unexpectedEntities' => ['Point Alpha', 'Point Beta'],
];
yield 'search by two projects with AND' => [
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
'expectedEntities' => ['Point Beta'],
'unexpectedEntities' => ['Point Alpha', 'Point Gamma', 'Point Delta'],
];
yield 'search by two projects with NOT AND' => [
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
'expectedEntities' => ['Point Gamma', 'Point Delta'],
'unexpectedEntities' => ['Point Alpha', 'Point Beta'],
];
yield 'search by two projects with OR' => [
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
'expectedEntities' => ['Point Alpha', 'Point Beta'],
'unexpectedEntities' => ['Point Gamma', 'Point Delta'],
];
yield 'search by two projects with NOT OR' => [
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
'expectedEntities' => ['Point Alpha', 'Point Gamma', 'Point Delta'],
'unexpectedEntities' => ['Point Beta'],
];
}
private function createPoint(string $name): Point
{
$point = new Point();
$point->setName($name);
$point->setType('url.hit');
$this->em->persist($point);
return $point;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PointBundle\Entity\Trigger;
use Mautic\ProjectBundle\Entity\Project;
use PHPUnit\Framework\Assert;
final class TriggerControllerTest extends MauticMysqlTestCase
{
public function testPointTriggerWithProject(): void
{
$trigger = new Trigger();
$trigger->setName('test');
$this->em->persist($trigger);
$project = new Project();
$project->setName('Test Project');
$this->em->persist($project);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request('GET', '/s/points/triggers/edit/'.$trigger->getId());
$form = $crawler->selectButton('Save')->form();
$form['pointtrigger[projects]']->setValue((string) $project->getId());
$this->client->submit($form);
$this->assertResponseIsSuccessful();
$savedAsset = $this->em->find(Trigger::class, $trigger->getId());
Assert::assertSame($project->getId(), $savedAsset->getProjects()->first()->getId());
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Functional\Controller;
use Mautic\PointBundle\Entity\Trigger;
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
final class TriggerProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
{
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
{
$projectOne = $this->createProject('Project One');
$projectTwo = $this->createProject('Project Two');
$projectThree = $this->createProject('Project Three');
$triggerAlpha = $this->createTrigger('Trigger Alpha');
$triggerBeta = $this->createTrigger('Trigger Beta');
$this->createTrigger('Trigger Gamma');
$this->createTrigger('Trigger Delta');
$triggerAlpha->addProject($projectOne);
$triggerAlpha->addProject($projectTwo);
$triggerBeta->addProject($projectTwo);
$triggerBeta->addProject($projectThree);
$this->em->flush();
$this->em->clear();
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/points/triggers', '/s/points/triggers']);
}
/**
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
*/
public static function searchDataProvider(): \Generator
{
yield 'search by one project' => [
'searchTerm' => 'project:"Project Two"',
'expectedEntities' => ['Trigger Alpha', 'Trigger Beta'],
'unexpectedEntities' => ['Trigger Gamma', 'Trigger Delta'],
];
yield 'search by one project AND trigger name' => [
'searchTerm' => 'project:"Project Two" AND Beta',
'expectedEntities' => ['Trigger Beta'],
'unexpectedEntities' => ['Trigger Alpha', 'Trigger Gamma', 'Trigger Delta'],
];
yield 'search by one project OR trigger name' => [
'searchTerm' => 'project:"Project Two" OR Gamma',
'expectedEntities' => ['Trigger Alpha', 'Trigger Beta', 'Trigger Gamma'],
'unexpectedEntities' => ['Trigger Delta'],
];
yield 'search by NOT one project' => [
'searchTerm' => '!project:"Project Two"',
'expectedEntities' => ['Trigger Gamma', 'Trigger Delta'],
'unexpectedEntities' => ['Trigger Alpha', 'Trigger Beta'],
];
yield 'search by two projects with AND' => [
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
'expectedEntities' => ['Trigger Beta'],
'unexpectedEntities' => ['Trigger Alpha', 'Trigger Gamma', 'Trigger Delta'],
];
yield 'search by two projects with NOT AND' => [
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
'expectedEntities' => ['Trigger Gamma', 'Trigger Delta'],
'unexpectedEntities' => ['Trigger Alpha', 'Trigger Beta'],
];
yield 'search by two projects with OR' => [
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
'expectedEntities' => ['Trigger Alpha', 'Trigger Beta'],
'unexpectedEntities' => ['Trigger Gamma', 'Trigger Delta'],
];
yield 'search by two projects with NOT OR' => [
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
'expectedEntities' => ['Trigger Alpha', 'Trigger Gamma', 'Trigger Delta'],
'unexpectedEntities' => ['Trigger Beta'],
];
}
private function createTrigger(string $name): Trigger
{
$trigger = new Trigger();
$trigger->setName($name);
$this->em->persist($trigger);
return $trigger;
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Entity\Email;
use Mautic\PointBundle\Entity\Trigger;
use Mautic\PointBundle\Entity\TriggerEvent;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\DomCrawler\Form;
use Symfony\Component\HttpFoundation\Request;
class EmailTriggerTest extends MauticMysqlTestCase
{
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
#[\PHPUnit\Framework\Attributes\RunInSeparateProcess]
public function testButtonsAreEnabledOnEditSendEmailToUserWhenEmailIsSelected(): void
{
$email = new Email();
$email->setName('Some name');
$email->setSubject('Some subject');
$email->setTemplate('Blank');
$email->setCustomHtml('Some html');
$this->em->persist($email);
$this->em->flush();
$trigger = $this->createTrigger();
$triggerEvent = $this->createTriggerEvent($trigger);
$triggerEvent->setProperties(['useremail' => ['email' => $email->getId()]]);
$this->em->flush();
$this->em->detach($trigger);
$this->em->detach($triggerEvent);
[$crawler, $form] = $this->fetchForm($trigger, $triggerEvent);
self::assertEquals($email->getId(), $form->get('pointtriggerevent[properties][useremail][email]')->getValue(), 'Current email should be selected.');
self::assertNull($crawler->selectButton('Preview')->attr('disabled'), 'Preview button should not be disabled.');
self::assertNull($crawler->selectButton('Edit Email')->attr('disabled'), 'Edit Email button should not be disabled.');
self::assertStringContainsString('"origin":"#pointtriggerevent_properties_useremail_email"', $crawler->selectButton('Preview')->attr('onclick'), 'The origin value should be correct.');
}
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
#[\PHPUnit\Framework\Attributes\RunInSeparateProcess]
public function testButtonsAreDisabledWhenEmailIsNotSelected(): void
{
$trigger = $this->createTrigger();
$triggerEvent = $this->createTriggerEvent($trigger);
$this->em->flush();
$this->em->detach($trigger);
$this->em->detach($triggerEvent);
[$crawler, $form] = $this->fetchForm($trigger, $triggerEvent);
self::assertEmpty($form->get('pointtriggerevent[properties][useremail][email]')->getValue(), 'No email should be selected.');
self::assertNotNull($crawler->selectButton('Preview')->attr('disabled'), 'Preview button should be disabled.');
self::assertNotNull($crawler->selectButton('Edit Email')->attr('disabled'), 'Edit Email button should be disabled.');
}
/**
* @return array{Crawler,Form}
*/
private function fetchForm(Trigger $trigger, TriggerEvent $triggerEvent): array
{
$this->client->request(Request::METHOD_GET, '/s/points/triggers/edit/'.$trigger->getId());
self::assertTrue($this->client->getResponse()->isSuccessful());
$uri = sprintf('/s/points/triggers/events/edit/%s?triggerId=%s', $triggerEvent->getId(), $trigger->getId());
$this->client->xmlHttpRequest(Request::METHOD_GET, $uri);
self::assertTrue($this->client->getResponse()->isSuccessful());
$responseData = json_decode($this->client->getResponse()->getContent(), true);
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
$form = $crawler->filterXPath('//form[@name="pointtriggerevent"]')->form();
return [$crawler, $form];
}
private function createTrigger(): Trigger
{
$trigger = new Trigger();
$trigger->setName('Email Trigger');
$this->em->persist($trigger);
return $trigger;
}
private function createTriggerEvent(Trigger $trigger): TriggerEvent
{
$triggerEvent = new TriggerEvent();
$triggerEvent->setTrigger($trigger);
$triggerEvent->setName('Send email to user');
$triggerEvent->setType('email.send_to_user');
$triggerEvent->setProperties([]);
$this->em->persist($triggerEvent);
return $triggerEvent;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Mautic\PointBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PointBundle\Entity\Group;
use Mautic\PointBundle\Entity\GroupContactScore;
use Mautic\PointBundle\Entity\GroupContactScoreRepository;
class GroupScoreRepositoryFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
protected GroupContactScoreRepository $repository;
public function setUp(): void
{
parent::setUp();
$this->repository = $this->em->getRepository(GroupContactScore::class);
}
public function testCompareScore(): void
{
$contact = $this->createContact('score@example.com');
$group = $this->createGroup('A');
$this->addGroupContactScore($contact, $group, 7);
$this->em->flush();
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'eq'));
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 8, 'eq'));
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 8, 'neq'));
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'neq'));
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 6, 'gt'));
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'gt'));
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 8, 'lt'));
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'lt'));
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'gte'));
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 6, 'gte'));
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 8, 'gte'));
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'lte'));
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 8, 'lte'));
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 6, 'lte'));
}
public function testCompareScoreContactWithoutScoreInGroup(): void
{
$contactWithoutScore = $this->createContact('no-score@example.com');
$group = $this->createGroup('A');
$this->em->flush();
$this->assertFalse($this->repository->compareScore($contactWithoutScore->getId(), $group->getId(), 0, 'eq'));
$this->assertFalse($this->repository->compareScore($contactWithoutScore->getId(), $group->getId(), 1, 'eq'));
}
private function createContact(
string $email,
): Lead {
$lead = new Lead();
$lead->setEmail($email);
$this->em->persist($lead);
return $lead;
}
private function createGroup(
string $name,
): Group {
$group = new Group();
$group->setName($name);
$this->em->persist($group);
return $group;
}
private function addGroupContactScore(
Lead $lead,
Group $group,
int $score,
): void {
$groupContactScore = new GroupContactScore();
$groupContactScore->setContact($lead);
$groupContactScore->setGroup($group);
$groupContactScore->setScore($score);
$lead->addGroupScore($groupContactScore);
}
}

View File

@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\EmailBundle\Entity\StatRepository;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\PointBundle\Entity\Group;
use Mautic\PointBundle\Entity\Point;
class PointActionFunctionalTest extends MauticMysqlTestCase
{
public function testPointActionReadEmail(): void
{
$this->logoutUser();
/** @var LeadModel $leadModel */
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
$lead = $this->createLead('john@doe.email');
$email = $this->createEmail();
$trackingHash = 'tracking_hash_123';
$this->createEmailStat($lead, $email, $trackingHash);
$pointAction = $this->createReadEmailAction(5);
$this->client->request('GET', '/email/'.$trackingHash.'.gif');
$lead = $leadModel->getEntity($lead->getId());
$this->assertEquals($pointAction->getDelta(), $lead->getPoints());
}
public function testPointActionWithGroupReadEmail(): void
{
$this->logoutUser();
/** @var LeadModel $leadModel */
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
$lead = $this->createLead('john@doe.email');
$email = $this->createEmail();
$group = $this->createGroup('Group A');
$trackingHash = 'tracking_hash_123';
$this->createEmailStat($lead, $email, $trackingHash);
$pointAction = $this->createReadEmailAction(5, $group);
$this->client->request('GET', '/email/'.$trackingHash.'.gif');
$this->em->clear(Lead::class);
$lead = $leadModel->getEntity($lead->getId());
$groupScore = $lead->getGroupScores()->first();
$this->assertEquals($pointAction->getDelta(), $groupScore->getScore());
// group point action shouldn't update main contact points
$this->assertEquals(0, $lead->getPoints());
}
public function testPointActionEarlyReturnWhenNoPointsAvailable(): void
{
/** @var LeadModel $leadModel */
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
$lead = $this->createLead('jane@doe.email');
$email = $this->createEmail();
$trackingHash = 'tracking_hash_no_points_456';
$this->createEmailStat($lead, $email, $trackingHash);
// Note: No point actions created for email.open type
$initialPoints = $lead->getPoints();
$this->client->request('GET', '/email/'.$trackingHash.'.gif');
$lead = $leadModel->getEntity($lead->getId());
// Points should remain unchanged as no point actions are available
$this->assertEquals($initialPoints, $lead->getPoints());
$this->assertEquals(0, $lead->getPoints());
}
private function createReadEmailAction(int $delta, ?Group $group = null): Point
{
$pointAction = new Point();
$pointAction->setName('Read email action');
$pointAction->setDelta($delta);
$pointAction->setType('email.open');
if ($group) {
$pointAction->setGroup($group);
}
$this->em->persist($pointAction);
$this->em->flush();
return $pointAction;
}
private function createEmailStat(
Lead $lead,
Email $email,
string $trackingHash,
): Stat {
/** @var StatRepository $statRepository */
$statRepository = static::getContainer()->get('mautic.email.repository.stat');
$stat = new Stat();
$stat->setTrackingHash($trackingHash);
$stat->setEmailAddress($lead->getEmail());
$stat->setLead($lead);
$stat->setDateSent(new \DateTime());
$stat->setEmail($email);
$statRepository->saveEntity($stat);
return $stat;
}
private function createLead(
string $email,
): Lead {
$lead = new Lead();
$lead->setEmail($email);
$this->em->persist($lead);
return $lead;
}
private function createEmail(): Email
{
$email = new Email();
$email->setName('Test email');
$email->setSubject('Test email subject');
$email->setEmailType('template');
$email->setCustomHtml('<h1>Email content</h1><br>{signature}');
$email->setIsPublished(true);
$email->setFromAddress('from@api.test');
$email->setFromName('API Test');
$this->em->persist($email);
return $email;
}
private function createGroup(
string $name,
): Group {
$group = new Group();
$group->setName($name);
$this->em->persist($group);
return $group;
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\Tag;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\PointBundle\Entity\Group;
use Mautic\PointBundle\Model\PointGroupModel;
use Mautic\PointBundle\Model\TriggerModel;
class PointTriggerFunctionalTest extends MauticMysqlTestCase
{
use TriggerTrait;
public function testPointsTriggerWithTagAction(): void
{
/** @var LeadModel $model */
$model = self::getContainer()->get('mautic.lead.model.lead');
$trigger = $this->createTrigger('Trigger', 5);
$this->createAddTagEvent('tag5', $trigger);
$trigger = $this->createTrigger('Trigger', 6);
$this->createAddTagEvent('tag6', $trigger);
$lead = new Lead();
$data = ['email' => 'pointtest@example.com', 'points' => 5];
$model->setFieldValues($lead, $data, false, true, true);
$model->saveEntity($lead);
$this->em->clear(Lead::class);
$lead = $model->getEntity($lead->getId());
$this->assertFalse($lead->getTags()->isEmpty());
$this->assertTrue($this->leadHasTag($lead, 'tag5'));
$this->assertFalse($this->leadHasTag($lead, 'tag6'));
}
public function testGroupPointsTriggerWithTagAction(): void
{
/** @var LeadModel $model */
$model = self::getContainer()->get('mautic.lead.model.lead');
/** @var PointGroupModel $pointGroupModel */
$pointGroupModel = self::getContainer()->get('mautic.point.model.group');
$groupA = $this->createGroup('Group A');
$groupB = $this->createGroup('Group B');
$triggerA = $this->createTrigger('Group A Trigger (should trigger)', 5, $groupA);
$this->createAddTagEvent('tagA', $triggerA);
$triggerB = $this->createTrigger('Group B Trigger (should not trigger)', 5, $groupB);
$this->createAddTagEvent('tagB', $triggerB);
$lead = new Lead();
$data = ['email' => 'pointtest@example.com', 'points' => 0];
$model->setFieldValues($lead, $data, false, true, true);
$model->saveEntity($lead);
$this->em->clear(Lead::class);
$lead = $model->getEntity($lead->getId());
$pointGroupModel->adjustPoints($lead, $groupA, 5);
$lead = $model->getEntity($lead->getId());
$this->assertFalse($this->leadHasTag($lead, 'tagB'));
$this->assertTrue($this->leadHasTag($lead, 'tagA'));
}
public function testTriggerForExistingContacts(): void
{
/** @var LeadModel $leadModel */
$leadModel = self::getContainer()->get('mautic.lead.model.lead');
/** @var TriggerModel $triggerModel */
$triggerModel = self::getContainer()->get('mautic.point.model.trigger');
$lead = new Lead();
$data = ['email' => 'pointtest@example.com', 'points' => 5];
$leadModel->setFieldValues($lead, $data, false, true, true);
$leadModel->saveEntity($lead);
$this->em->clear(Lead::class);
$triggerA = $this->createTrigger('Group A Trigger (should trigger)', 5, null, true);
$triggerEventA = $this->createAddTagEvent('tagA', $triggerA);
$triggerA->addTriggerEvent(0, $triggerEventA);
$triggerModel->saveEntity($triggerA);
$triggerB = $this->createTrigger('Group B Trigger (should not trigger)', 6, null, true);
$triggerEventB = $this->createAddTagEvent('tagB', $triggerB);
$triggerB->addTriggerEvent(0, $triggerEventB);
$triggerModel->saveEntity($triggerB);
$lead = $leadModel->getEntity($lead->getId());
$this->assertFalse($this->leadHasTag($lead, 'tagB'));
$this->assertTrue($this->leadHasTag($lead, 'tagA'));
}
public function testTriggerWithGroupForExistingContacts(): void
{
/** @var LeadModel $leadModel */
$leadModel = self::getContainer()->get('mautic.lead.model.lead');
/** @var TriggerModel $triggerModel */
$triggerModel = self::getContainer()->get('mautic.point.model.trigger');
/** @var PointGroupModel $pointGroupModel */
$pointGroupModel = self::getContainer()->get('mautic.point.model.group');
$groupA = $this->createGroup('Group A');
$groupB = $this->createGroup('Group B');
$lead = new Lead();
$data = ['email' => 'pointtest@example.com'];
$leadModel->setFieldValues($lead, $data, false, true, true);
$leadModel->saveEntity($lead);
$pointGroupModel->adjustPoints($lead, $groupA, 5);
$triggerA = $this->createTrigger('Group A Trigger (should trigger)', 5, $groupA, true);
$triggerEventA = $this->createAddTagEvent('tagA', $triggerA);
$triggerA->addTriggerEvent(0, $triggerEventA);
$triggerModel->saveEntity($triggerA);
$triggerB = $this->createTrigger('Group B Trigger (should not trigger)', 5, $groupB, true);
$triggerEventB = $this->createAddTagEvent('tagB', $triggerB);
$triggerB->addTriggerEvent(0, $triggerEventB);
$triggerModel->saveEntity($triggerB);
$lead = $leadModel->getEntity($lead->getId());
$triggerC = $this->createTrigger('General Trigger (should not trigger)', 5, $groupB, true);
$triggerEventB = $this->createAddTagEvent('tagC', $triggerC);
$triggerC->addTriggerEvent(0, $triggerEventB);
$triggerModel->saveEntity($triggerC);
$lead = $leadModel->getEntity($lead->getId());
$this->assertFalse($this->leadHasTag($lead, 'tagC'));
$this->assertFalse($this->leadHasTag($lead, 'tagB'));
$this->assertTrue($this->leadHasTag($lead, 'tagA'));
}
private function createGroup(
string $name,
): Group {
$group = new Group();
$group->setName($name);
$this->em->persist($group);
return $group;
}
private function leadHasTag(
Lead $lead,
string $tagName,
): bool {
/** @var Tag $tag */
foreach ($lead->getTags() as $tag) {
if ($tag->getTag() === $tagName) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Functional;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PointBundle\Entity\Group;
use Mautic\PointBundle\Entity\GroupContactScore;
use Mautic\ReportBundle\Entity\Report;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
class ReportSubscriberFunctionalTest extends MauticMysqlTestCase
{
protected function setUp(): void
{
$this->useCleanupRollback = false;
parent::setUp();
}
public function testContactPointLogReportWithGroup(): void
{
$this->createTestContactWithGroupPoints();
$report = new Report();
$report->setName('Contact point log');
$report->setSource('lead.pointlog');
$report->setColumns(['lp.type', 'lp.event_name', 'l.email', 'lp.delta', 'pl.name']);
$report->setTableOrder([[
'column' => 'lp.delta',
'direction' => 'DESC',
]]);
$this->em->persist($report);
$this->em->flush();
$this->em->clear();
// -- test report table in mautic panel
$crawler = $this->client->request(Request::METHOD_GET, "/s/reports/view/{$report->getId()}");
$crawlerReportTable = $crawler->filterXPath('//table[@id="reportTable"]')->first();
// convert html table to php array
$crawlerReportTable = $this->domTableToArray($crawlerReportTable);
$this->assertSame([
// no., event_type, event_name, email, points_delta, group_name
['1', 'test type', 'Adjust points', 'test2@example.com', '15', 'Group A'],
['2', 'test type', 'Adjust points', 'test3@example.com', '10', 'Group A'],
['3', 'test type', 'Adjust points', 'test1@example.com', '5', 'Group A'],
['4', 'test type', 'Adjust points', 'test3@example.com', '2', 'Group B'],
['5', 'test type', 'Adjust points', 'test2@example.com', '1', 'Group B'],
], array_slice($crawlerReportTable, 1, 5));
// -- test API report data
$this->client->request(Request::METHOD_GET, "/api/reports/{$report->getId()}");
$clientResponse = $this->client->getResponse();
$result = json_decode($clientResponse->getContent(), true);
$this->assertSame([
[
'type' => 'test type',
'event_name' => 'Adjust points',
'email' => 'test2@example.com',
'delta' => '15',
'group_name' => 'Group A',
],
[
'type' => 'test type',
'event_name' => 'Adjust points',
'email' => 'test3@example.com',
'delta' => '10',
'group_name' => 'Group A',
],
[
'type' => 'test type',
'event_name' => 'Adjust points',
'email' => 'test1@example.com',
'delta' => '5',
'group_name' => 'Group A',
],
[
'type' => 'test type',
'event_name' => 'Adjust points',
'email' => 'test3@example.com',
'delta' => '2',
'group_name' => 'Group B',
],
[
'type' => 'test type',
'event_name' => 'Adjust points',
'email' => 'test2@example.com',
'delta' => '1',
'group_name' => 'Group B',
],
], $result['data']);
}
public function testGroupScoreReport(): void
{
$this->createTestContactWithGroupPoints();
$report = new Report();
$report->setName('Group score report');
$report->setSource('group.score');
$report->setColumns(['pl.name', 'ls.score', 'l.email']);
$report->setTableOrder([[
'column' => 'ls.score',
'direction' => 'DESC',
]]);
$this->em->persist($report);
$this->em->flush();
$this->em->clear();
// -- test report table in mautic panel
$crawler = $this->client->request(Request::METHOD_GET, "/s/reports/view/{$report->getId()}");
$crawlerReportTable = $crawler->filterXPath('//table[@id="reportTable"]')->first();
// convert html table to php array
$crawlerReportTable = $this->domTableToArray($crawlerReportTable);
$this->assertSame([
// no., group_name, group_score, email
['1', 'Group A', '15', 'test2@example.com'],
['2', 'Group A', '10', 'test3@example.com'],
['3', 'Group A', '5', 'test1@example.com'],
['4', 'Group B', '2', 'test3@example.com'],
['5', 'Group B', '1', 'test2@example.com'],
], array_slice($crawlerReportTable, 1, 5));
// -- test API report data
$this->client->request(Request::METHOD_GET, "/api/reports/{$report->getId()}");
$clientResponse = $this->client->getResponse();
$result = json_decode($clientResponse->getContent(), true);
$this->assertSame([
[
'group_name' => 'Group A',
'group_score' => '15',
'email' => 'test2@example.com',
],
[
'group_name' => 'Group A',
'group_score' => '10',
'email' => 'test3@example.com',
],
[
'group_name' => 'Group A',
'group_score' => '5',
'email' => 'test1@example.com',
],
[
'group_name' => 'Group B',
'group_score' => '2',
'email' => 'test3@example.com',
],
[
'group_name' => 'Group B',
'group_score' => '1',
'email' => 'test2@example.com',
],
], $result['data']);
}
private function createTestContactWithGroupPoints(): void
{
$contactModel = static::getContainer()->get('mautic.lead.model.lead');
$groupA = $this->createGroup('Group A');
$groupB = $this->createGroup('Group B');
$this->em->flush();
$contacts = [
$this->createContact('test1@example.com'),
$this->createContact('test2@example.com'),
$this->createContact('test3@example.com'),
];
$contactModel->saveEntities($contacts);
$this->adjustContactPoints($contacts[0], 5, $groupA);
$this->adjustContactPoints($contacts[1], 15, $groupA);
$this->adjustContactPoints($contacts[2], 10, $groupA);
$this->adjustContactPoints($contacts[2], 2, $groupB);
$this->adjustContactPoints($contacts[1], 1, $groupB);
$contactModel->saveEntities($contacts);
}
private function createContact(string $email): Lead
{
$contact = new Lead();
$contact->setEmail($email);
$this->em->persist($contact);
return $contact;
}
private function adjustContactPoints(Lead $contact, int $points, Group $group): void
{
$ipAddress = new IpAddress();
$ipAddress->setIpAddress('127.0.0.1');
$contact->addPointsChangeLogEntry(
'test type',
'Adjust points',
'test action',
$points,
$ipAddress,
$group
);
$contact->adjustPoints($points);
$groupContactScore = new GroupContactScore();
$groupContactScore->setContact($contact);
$groupContactScore->setGroup($group);
$groupContactScore->setScore($points);
$contact->addGroupScore($groupContactScore);
}
private function createGroup(
string $name,
): Group {
$group = new Group();
$group->setName($name);
$this->em->persist($group);
return $group;
}
/**
* @return array<int,array<int,mixed>>
*/
private function domTableToArray(Crawler $crawler): array
{
return $crawler->filter('tr')->each(fn ($tr) => $tr->filter('td')->each(fn ($td) => trim($td->text())));
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Mautic\PointBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\PointBundle\Entity\Group;
use Mautic\PointBundle\Entity\GroupContactScore;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\ApplicationTester;
class SegmentFilterFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testGroupPointSegmentFilter(): void
{
$application = new Application(self::$kernel);
$application->setAutoExit(false);
$applicationTester = new ApplicationTester($application);
$contactA = $this->createContact('contact-a@example.com');
$contactB = $this->createContact('contact-b@example.com');
$contactC = $this->createContact('contact-c@example.com');
$groupA = $this->createGroup('Group A');
$this->em->flush();
$this->addGroupContactScore($contactA, $groupA, 1);
$this->addGroupContactScore($contactB, $groupA, 0);
$this->em->persist($contactA);
$this->em->persist($contactB);
$this->em->flush();
$segmentA = new LeadList();
$segmentA->setName('Group A points >= 1');
$segmentA->setPublicName('Group A points >= 1');
$segmentA->setAlias('group-a-points-gte1');
$segmentA->setIsPublished(true);
$segmentA->setFilters([
[
'glue' => 'and',
'field' => 'group_points_'.$groupA->getId(),
'object' => 'groups',
'type' => 'number',
'operator' => 'gte',
'properties' => [
'filter' => '1',
],
],
]);
$this->em->persist($segmentA);
$this->em->flush();
// Force Doctrine to re-fetch the entities otherwise the campaign won't know about any events.
$this->em->clear();
// Execute segment update command.
$exitCode = $applicationTester->run(
[
'command' => 'mautic:segments:update',
'-i' => $segmentA->getId(),
]
);
$this->assertSame(0, $exitCode, $applicationTester->getDisplay());
$this->client->request('GET', '/api/contacts?search=segment:group-a-points-gte1');
$clientResponse = $this->client->getResponse();
$this->assertTrue($this->client->getResponse()->isOk());
$response = json_decode($clientResponse->getContent(), true);
$this->assertEquals(1, (int) $response['total']);
$contactIds = array_column($response['contacts'], 'id');
$this->assertContains((int) $contactA->getId(), $contactIds);
$this->assertNotContains((int) $contactB->getId(), $contactIds);
$this->assertNotContains((int) $contactC->getId(), $contactIds);
}
private function createContact(
string $email,
): Lead {
$lead = new Lead();
$lead->setEmail($email);
$this->em->persist($lead);
return $lead;
}
private function createGroup(
string $name,
): Group {
$group = new Group();
$group->setName($name);
$this->em->persist($group);
return $group;
}
private function addGroupContactScore(
Lead $lead,
Group $group,
int $score,
): void {
$groupContactScore = new GroupContactScore();
$groupContactScore->setContact($lead);
$groupContactScore->setGroup($group);
$groupContactScore->setScore($score);
$lead->addGroupScore($groupContactScore);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Mautic\PointBundle\Tests\Functional;
use Mautic\PointBundle\Entity\Group;
use Mautic\PointBundle\Entity\Trigger;
use Mautic\PointBundle\Entity\TriggerEvent;
trait TriggerTrait
{
private function createTrigger(
string $name,
int $points = 0,
?Group $group = null,
bool $triggerExistingLeads = false,
): Trigger {
$trigger = new Trigger();
$trigger->setName($name);
$trigger->setPoints($points);
if (isset($group)) {
$trigger->setGroup($group);
}
if ($triggerExistingLeads) {
$trigger->setTriggerExistingLeads($triggerExistingLeads);
}
$this->em->persist($trigger);
return $trigger;
}
private function createAddTagEvent(
string $tag,
Trigger $trigger,
): TriggerEvent {
$triggerEvent = new TriggerEvent();
$triggerEvent->setTrigger($trigger);
$triggerEvent->setName('Add '.$tag);
$triggerEvent->setType('lead.changetags');
$triggerEvent->setProperties([
'add_tags' => [$tag],
'remove_tags' => [],
]);
$this->em->persist($triggerEvent);
return $triggerEvent;
}
}