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,221 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Controller\Api;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\PointsChangeLog;
use Mautic\PointBundle\Entity\Group;
use Symfony\Component\HttpFoundation\Response;
final class PointGroupsApiControllerTest extends MauticMysqlTestCase
{
public function testPointGroupCRUDActions(): void
{
/** @var Translator $translator */
$translator = static::getContainer()->get('translator');
// Create a new point group
$this->client->request('POST', '/api/points/groups/new', [
'name' => 'New Point Group',
'description' => 'Description of the new point group',
]);
$createResponse = $this->client->getResponse();
$this->assertSame(Response::HTTP_CREATED, $createResponse->getStatusCode());
$responseData = json_decode($createResponse->getContent(), true);
$this->assertArrayHasKey('pointGroup', $responseData);
$createdData = $responseData['pointGroup'];
$this->assertArrayHasKey('id', $createdData);
$this->assertEquals('New Point Group', $createdData['name']);
$this->assertEquals('Description of the new point group', $createdData['description']);
// Retrieve all point groups
$this->client->request('GET', '/api/points/groups');
$getAllResponse = $this->client->getResponse();
$this->assertSame(Response::HTTP_OK, $getAllResponse->getStatusCode());
$responseData = json_decode($getAllResponse->getContent(), true);
$this->assertArrayHasKey('pointGroups', $responseData);
$this->assertEquals(1, $responseData['total']);
$allData = $responseData['pointGroups'];
$this->assertIsArray($allData);
$this->assertArrayHasKey(0, $allData); // Ensure the response is array-indexed from 0
$this->assertCount(1, $allData);
// Update the created point group
$updatePayload = [
'name' => 'Updated Point Group Name',
'description' => 'Updated description of the point group',
];
$this->client->request('PATCH', "/api/points/groups/{$createdData['id']}/edit", $updatePayload);
$updateResponse = $this->client->getResponse();
$this->assertSame(Response::HTTP_OK, $updateResponse->getStatusCode());
$responseData = json_decode($updateResponse->getContent(), true);
$this->assertArrayHasKey('pointGroup', $responseData);
$updatedData = $responseData['pointGroup'];
$this->assertEquals('Updated Point Group Name', $updatedData['name']);
$this->assertEquals('Updated description of the point group', $updatedData['description']);
// Delete the created point group
$this->client->request('DELETE', "/api/points/groups/{$createdData['id']}/delete");
$deleteResponse = $this->client->getResponse();
$this->assertSame(Response::HTTP_OK, $deleteResponse->getStatusCode());
$responseData = json_decode($deleteResponse->getContent(), true);
$this->assertArrayHasKey('pointGroup', $responseData);
$deleteData = $responseData['pointGroup'];
$this->assertEquals('Updated Point Group Name', $deleteData['name']);
$this->assertEquals('Updated description of the point group', $deleteData['description']);
// Try to GET the group that should no longer exist
$this->client->request('GET', "/api/points/groups/{$createdData['id']}");
$getResponse = $this->client->getResponse();
$this->assertSame(Response::HTTP_NOT_FOUND, $getResponse->getStatusCode());
$responseData = json_decode($getResponse->getContent(), true);
$this->assertArrayHasKey('errors', $responseData);
$this->assertCount(1, $responseData['errors']);
$this->assertSame(Response::HTTP_NOT_FOUND, $responseData['errors'][0]['code']);
$this->assertSame($translator->trans('mautic.core.error.notfound', [], 'flashes'), $responseData['errors'][0]['message']);
}
public function testContactGroupPointsActions(): void
{
/** @var Translator $translator */
$translator = static::getContainer()->get('translator');
// Arrange
$contact = $this->createContact('test@example.com');
$pointGroupA = $this->createGroup('Group A');
$pointGroupB = $this->createGroup('Group B');
$this->em->flush();
// Act & Assert
$this->adjustPointsAndAssert($contact, $pointGroupA, 'plus', 10, 10);
$this->adjustPointsAndAssert($contact, $pointGroupA, 'minus', 2, 8);
$this->adjustPointsAndAssert($contact, $pointGroupA, 'divide', 2, 4);
$this->adjustPointsAndAssert($contact, $pointGroupA, 'times', 4, 16);
$this->adjustPointsAndAssert($contact, $pointGroupB, 'set', 21, 21);
// Test GET all contact's point groups endpoint
$this->assertContactPointGroups($contact, [
[
'score' => 16,
'group' => [
'id' => $pointGroupA->getId(),
'name' => 'Group A',
'description' => '',
],
],
[
'score' => 21,
'group' => [
'id' => $pointGroupB->getId(),
'name' => 'Group B',
'description' => '',
],
],
]);
// Test GET single contact's point group endpoint
$this->assertContactSinglePointGroup($contact, $pointGroupA, 16);
$this->assertContactSinglePointGroup($contact, $pointGroupB, 21);
$this->assertPointsChangeLogEntries($contact, [
['delta' => 10, 'groupId' => $pointGroupA->getId()],
['delta' => -2, 'groupId' => $pointGroupA->getId()],
['delta' => -4, 'groupId' => $pointGroupA->getId()],
['delta' => 12, 'groupId' => $pointGroupA->getId()],
['delta' => 21, 'groupId' => $pointGroupB->getId()],
]);
// Try to GET the group points that should not exist
$this->client->request('GET', "/api/contacts/{$contact->getId()}/points/groups/0");
$response = $this->client->getResponse();
$this->assertSame(Response::HTTP_NOT_FOUND, $response->getStatusCode());
$responseData = json_decode($response->getContent(), true);
$this->assertArrayHasKey('errors', $responseData);
$this->assertCount(1, $responseData['errors']);
$this->assertSame(Response::HTTP_NOT_FOUND, $responseData['errors'][0]['code']);
$this->assertSame($translator->trans('mautic.lead.event.api.point.group.not.found'), $responseData['errors'][0]['message']);
// Try to GET the group points for a contact that should not exist
$this->client->request('GET', '/api/contacts/0/points/groups/0');
$response = $this->client->getResponse();
$this->assertSame(Response::HTTP_NOT_FOUND, $response->getStatusCode());
$responseData = json_decode($response->getContent(), true);
$this->assertArrayHasKey('errors', $responseData);
$this->assertCount(1, $responseData['errors']);
$this->assertSame(Response::HTTP_NOT_FOUND, $responseData['errors'][0]['code']);
$this->assertSame($translator->trans('mautic.lead.event.api.lead.not.found'), $responseData['errors'][0]['message']);
}
private function adjustPointsAndAssert(Lead $contact, Group $pointGroup, string $operator, int $value, int $expectedScore): void
{
$this->client->request('POST', "/api/contacts/{$contact->getId()}/points/groups/{$pointGroup->getId()}/$operator/{$value}");
$adjustResponse = $this->client->getResponse();
$this->assertSame(Response::HTTP_OK, $adjustResponse->getStatusCode());
$responseData = json_decode($adjustResponse->getContent(), true);
$this->assertSame($expectedScore, $responseData['groupScore']['score']);
}
/**
* @param array<int, array<string, mixed>> $expectedGroups
*/
private function assertContactPointGroups(Lead $contact, array $expectedGroups): void
{
$this->client->request('GET', "/api/contacts/{$contact->getId()}/points/groups");
$response = $this->client->getResponse();
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
$responseData = json_decode($response->getContent(), true);
$this->assertSame(count($expectedGroups), $responseData['total']);
$this->assertSame($expectedGroups, $responseData['groupScores']);
}
private function assertContactSinglePointGroup(Lead $contact, Group $pointGroup, int $expectedScore): void
{
$this->client->request('GET', "/api/contacts/{$contact->getId()}/points/groups/{$pointGroup->getId()}");
$response = $this->client->getResponse();
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
$responseData = json_decode($response->getContent(), true);
$this->assertSame($expectedScore, $responseData['groupScore']['score']);
}
/**
* @param array<int, array<string, mixed>> $expectedEntries
*/
private function assertPointsChangeLogEntries(Lead $contact, array $expectedEntries): void
{
$logs = $this->em->getRepository(PointsChangeLog::class)->findBy(['lead' => $contact->getId()]);
$this->assertCount(count($expectedEntries), $logs);
foreach ($expectedEntries as $index => $expectedEntry) {
$this->assertEquals($expectedEntry['delta'], $logs[$index]->getDelta());
$this->assertEquals($expectedEntry['groupId'], $logs[$index]->getGroup()->getId());
}
}
private function createContact(string $email): Lead
{
$contact = new Lead();
$contact->setEmail($email);
$this->em->persist($contact);
return $contact;
}
private function createGroup(
string $name,
): Group {
$group = new Group();
$group->setName($name);
$this->em->persist($group);
return $group;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class PointControllerTest extends MauticMysqlTestCase
{
public function testIndexActionWithoutPage(): void
{
$this->client->request(Request::METHOD_GET, '/s/points');
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
}
public function testIndexActionWithPage(): void
{
$this->client->request(Request::METHOD_GET, '/s/points/1');
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
}
public function testNewAction(): void
{
$this->client->request(Request::METHOD_GET, '/s/points/new');
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PointBundle\Model\TriggerModel;
use Mautic\PointBundle\Tests\Functional\TriggerTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class TriggerControllerTest extends MauticMysqlTestCase
{
use TriggerTrait;
public function testIndexActionWithoutPage(): void
{
$this->client->request(Request::METHOD_GET, '/s/points/triggers');
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
}
public function testIndexActionWithPage(): void
{
$this->client->request(Request::METHOD_GET, '/s/points/triggers/1');
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
}
public function testCloneAction(): void
{
/** @var TriggerModel $triggerModel */
$triggerModel = self::getContainer()->get('mautic.point.model.trigger');
$triggerRepo = $triggerModel->getRepository();
$triggerEventRepo = $triggerModel->getEventRepository();
$trigger = $this->createTrigger('Trigger', 5);
$this->createAddTagEvent('tag1', $trigger);
$this->createAddTagEvent('tag2', $trigger);
$this->em->flush();
$this->em->clear();
$this->assertCount(1, $triggerRepo->findAll());
$this->assertCount(2, $triggerEventRepo->findAll());
$crawler = $this->client->request(Request::METHOD_GET, '/s/points/triggers/clone/'.$trigger->getId());
$this->assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
$form = $crawler->selectButton('Save')->form();
$this->client->submit($form);
$this->assertCount(2, $triggerRepo->findAll());
$this->assertCount(4, $triggerEventRepo->findAll());
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Entity;
use Doctrine\Persistence\Mapping\MappingException;
use Mautic\CoreBundle\Helper\IntHelper;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PointBundle\Entity\Point;
use Symfony\Component\DomCrawler\Form;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class PointEntityValidationTest extends MauticMysqlTestCase
{
/**
* @throws MappingException
*/
#[\PHPUnit\Framework\Attributes\DataProvider('deltaScenariosProvider')]
public function testDeltaValidationOnCreate(int $delta, string $errorMessage = ''): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/points/new');
$buttonCrawler = $crawler->selectButton('Save & Close');
$form = $buttonCrawler->form();
$form['point[name]']->setValue('Add point');
$this->testPointData($form, $delta, $errorMessage);
}
#[\PHPUnit\Framework\Attributes\DataProvider('deltaScenariosProvider')]
public function testDeltaValidationOnCreateViaAPI(int $delta, string $errorMessage = ''): void
{
$this->client->request(
Request::METHOD_POST,
'/api/points/new',
[
'name' => 'Point1',
'delta' => $delta,
'isPublished' => true,
'type' => 'form.submit',
]
);
$response = $this->client->getResponse();
if ($errorMessage) {
self::assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
self::assertStringContainsString('error', $response->getContent());
self::assertStringContainsString($errorMessage, $response->getContent());
} else {
self::assertResponseStatusCodeSame(Response::HTTP_CREATED);
self::assertStringNotContainsString('error', $response->getContent());
}
}
/**
* @throws MappingException
*/
#[\PHPUnit\Framework\Attributes\DataProvider('deltaScenariosProvider')]
public function testDeltaValidationOnUpdate(int $delta, string $errorMessage = ''): void
{
$point = new Point();
$point->setName('Edit point');
$point->setDelta(5);
$point->setType('form.submit');
$point->setIsPublished(true);
$this->em->persist($point);
$this->em->flush();
$pointId = $point->getId();
$crawler = $this->client->request(Request::METHOD_GET, '/s/points/edit/'.$pointId);
$buttonCrawler = $crawler->selectButton('Save & Close');
$form = $buttonCrawler->form();
$form['point[name]']->setValue('Edit point');
$this->testPointData($form, $delta, $errorMessage);
}
/**
* @return iterable<string, array<mixed>>
*/
public static function deltaScenariosProvider(): iterable
{
yield 'within range positive number' => [3000, ''];
yield 'within range negative number' => [-7857, ''];
yield 'within range zero' => [0, ''];
yield 'upper limit' => [IntHelper::MAX_INTEGER_VALUE, ''];
yield 'lower limit' => [IntHelper::MIN_INTEGER_VALUE, ''];
yield 'above upper limit' => [IntHelper::MAX_INTEGER_VALUE + 10, 'This value should be between -2147483648 and 2147483647.'];
yield 'below lower limit' => [IntHelper::MIN_INTEGER_VALUE - 10, 'This value should be between -2147483648 and 2147483647.'];
}
/**
* @throws MappingException
*/
private function testPointData(Form $form, int $delta, string $errorMessage): void
{
$form['point[delta]']->setValue((string) $delta);
$form['point[isPublished]']->setValue('1');
$form['point[type]']->setValue('form.submit');
$this->client->submit($form);
self::assertTrue($this->client->getResponse()->isOk());
$response = $this->client->getResponse()->getContent();
self::assertStringContainsString($errorMessage, (string) $response);
$pointDetail = $this->em->getRepository(Point::class)->findOneBy(['delta' => $delta]);
'' == $errorMessage ? self::assertNotNull($pointDetail) : self::assertNull($pointDetail);
}
}

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

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Unit\Helper;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PointBundle\Helper\EventHelper;
use PHPUnit\Framework\TestCase;
class EventHelperTest extends TestCase
{
public function testEngagePointAction(): void
{
$lead = new Lead();
// Define the action array
$action = ['id' => 1, 'type' => 'helloworld.action.custom_action', 'name' => 'My custom point action', 'properties' => [], 'points' => 50];
$points = EventHelper::engagePointAction($lead, $action);
$this->assertEquals(50, $points);
$points = EventHelper::engagePointAction($lead, $action);
$this->assertEquals(0, $points, 'Second call should return 0 points because the action is already initiated for this lead and type and session.');
}
}

View File

@@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Unit\Model;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Mautic\PageBundle\Entity\Hit;
use Mautic\PageBundle\Helper\PointActionHelper;
use Mautic\PointBundle\Entity\Point;
use Mautic\PointBundle\Entity\PointRepository;
use Mautic\PointBundle\Event\PointActionEvent;
use Mautic\PointBundle\Event\PointBuilderEvent;
use Mautic\PointBundle\Model\PointGroupModel;
use Mautic\PointBundle\Model\PointModel;
use Mautic\PointBundle\PointEvents;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\EventDispatcher\Event;
class PointModelTest extends TestCase
{
private RequestStack&MockObject $requestStack;
private IpLookupHelper&MockObject $ipLookupHelper;
private LeadModel&MockObject $leadModel;
private ContactTracker&MockObject $contactTracker;
private EntityManager&MockObject $em;
private CorePermissions&MockObject $security;
private EventDispatcherInterface&MockObject $dispatcher;
private UrlGeneratorInterface&MockObject $router;
private Translator&MockObject $translator;
private UserHelper&MockObject $userHelper;
private LoggerInterface&MockObject $mauticLogger;
private CoreParametersHelper&MockObject $coreParametersHelper;
private PointGroupModel&MockObject $pointGroupModel;
private PointModel $pointModel;
protected function setUp(): void
{
$this->requestStack = $this->createMock(RequestStack::class);
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
$this->leadModel = $this->createMock(LeadModel::class);
$this->contactTracker = $this->createMock(ContactTracker::class);
$this->em = $this->createMock(EntityManager::class);
$this->security = $this->createMock(CorePermissions::class);
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
$this->router = $this->createMock(RouterInterface::class);
$this->translator = $this->createMock(Translator::class);
$this->userHelper = $this->createMock(UserHelper::class);
$this->mauticLogger = $this->createMock(LoggerInterface::class);
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$this->pointGroupModel = $this->createMock(PointGroupModel::class);
$this->pointModel = new PointModel(
$this->requestStack,
$this->ipLookupHelper,
$this->leadModel,
$this->contactTracker,
$this->em,
$this->security,
$this->dispatcher,
$this->router,
$this->translator,
$this->userHelper,
$this->mauticLogger,
$this->coreParametersHelper,
$this->pointGroupModel,
);
}
public function testTriggerUrlHitWithCallbackObject(): void
{
$type = 'url.hit';
$pointId = 98783;
$pointName = 'Point name';
$pointProperties = ['property' => 'value'];
$pointDelta = 7;
$pointGroup = null;
$ip = $this->createMock(IpAddress::class);
$this->security->method('isAnonymous')->willReturn(true);
$this->ipLookupHelper->method('getIpAddress')->willReturn($ip);
$lead = $this->createMock(Lead::class);
$lead->expects(self::once())
->method('adjustPoints')
->with($pointDelta);
$lead->expects(self::once())
->method('addPointsChangeLogEntry')
->with(
'url',
$pointId.': '.$pointName,
'hit',
$pointDelta,
$ip,
$pointGroup
);
$eventDetails = $this->createMock(Hit::class);
$repository = $this->createMock(PointRepository::class);
$this->em->expects(self::once())
->method('getRepository')
->with(Point::class)
->willReturn($repository);
$pointActionHelper = $this->createMock(PointActionHelper::class);
$pointActionHelper->expects(self::once())
->method('validateUrlHit')
->with(
$eventDetails,
[
'id' => $pointId,
'type' => $type,
'name' => $pointName,
'properties' => $pointProperties,
'points' => $pointDelta,
]
)
->willReturn(true);
$point = $this->createMock(Point::class);
$point->method('getRepeatable')->willReturn(true);
$point->method('getType')->willReturn($type);
$point->method('getId')->willReturn($pointId);
$point->method('getName')->willReturn($pointName);
$point->method('getProperties')->willReturn($pointProperties);
$point->method('getDelta')->willReturn($pointDelta);
$point->method('getGroup')->willReturn($pointGroup);
$repository->expects(self::once())
->method('getPublishedByType')
->with($type)
->willReturn([$point]);
$repository->expects(self::once())
->method('getCompletedLeadActions')
->willReturn([]);
$repository->expects(self::never())
->method('saveEntities');
$repository->expects(self::never())
->method('detachEntities');
$this->dispatcher->expects(self::exactly(2))
->method('dispatch')
->willReturnCallback(function (Event $event, string $eventName) use ($pointActionHelper, $type, $lead, $point): Event {
if (PointEvents::POINT_ON_BUILD === $eventName) {
self::assertInstanceOf(PointBuilderEvent::class, $event);
self::assertEquals(new PointBuilderEvent($this->translator), $event);
$event->addAction(
$type,
[
'callback' => [
$pointActionHelper,
'validateUrlHit',
],
'group' => 'group',
'label' => 'label',
],
);
return $event;
}
if (PointEvents::POINT_ON_ACTION === $eventName) {
$pointActionEvent = new PointActionEvent($point, $lead);
self::assertEquals($pointActionEvent, $event);
return $pointActionEvent;
}
self::fail('Unknown event called: '.$eventName);
});
$this->leadModel->expects(self::once())
->method('saveEntity')
->with($lead);
$this->pointModel->triggerAction($type, $eventDetails, null, $lead);
}
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Tests\Unit\Model;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\EmailBundle\EmailEvents;
use Mautic\EmailBundle\Form\Type\EmailToUserType;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Mautic\PointBundle\Entity\TriggerEvent;
use Mautic\PointBundle\Entity\TriggerEventRepository;
use Mautic\PointBundle\Model\TriggerEventModel;
use Mautic\PointBundle\Model\TriggerModel;
use Mautic\PointBundle\PointEvents;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class TriggerModelTest extends \PHPUnit\Framework\TestCase
{
/**
* @var IpLookupHelper&MockObject
*/
private MockObject $ipLookupHelper;
/**
* @var LeadModel&MockObject
*/
private MockObject $leadModel;
/**
* @var TriggerEventModel&MockObject
*/
private MockObject $triggerEventModel;
/**
* @var EventDispatcherInterface&MockObject
*/
private MockObject $dispatcher;
/**
* @var TranslatorInterface&MockObject
*/
private MockObject $translator;
/**
* @var EntityManager&MockObject
*/
private MockObject $entityManager;
/**
* @var TriggerEventRepository&MockObject
*/
private MockObject $triggerEventRepository;
private TriggerModel $triggerModel;
/**
* @var ContactTracker&MockObject
*/
private MockObject $contactTracker;
public function setUp(): void
{
parent::setUp();
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
$this->leadModel = $this->createMock(LeadModel::class);
$this->triggerEventModel = $this->createMock(TriggerEventModel::class);
$this->contactTracker = $this->createMock(ContactTracker::class);
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
$this->translator = $this->createMock(Translator::class);
$this->entityManager = $this->createMock(EntityManager::class);
$this->triggerEventRepository = $this->createMock(TriggerEventRepository::class);
$this->triggerModel = new TriggerModel(
$this->ipLookupHelper,
$this->leadModel,
$this->triggerEventModel,
$this->contactTracker,
$this->entityManager,
$this->createMock(CorePermissions::class),
$this->dispatcher,
$this->createMock(UrlGeneratorInterface::class),
$this->translator,
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class)
);
// reset private property cachedEvents in TriggerModel instance
$reflectionClass = new \ReflectionClass(TriggerModel::class);
$property = $reflectionClass->getProperty('cachedEvents');
$property->setAccessible(true);
$property->setValue($this->triggerModel, []);
}
public function testTriggerEvent(): void
{
$triggerEvent = new TriggerEvent();
$contact = new Lead();
$dispatchCalls = new \ArrayObject();
$triggerEvent->setType('email.send_to_user');
$this->entityManager->expects($this->once())
->method('getRepository')
->willReturn($this->triggerEventRepository);
$this->triggerEventRepository->expects($this->once())
->method('find')
->willReturn($triggerEvent);
$this->dispatcher->expects($this->exactly(2))
->method('dispatch')
->willReturnCallback(function ($event, $eventName) use ($dispatchCalls, $contact, $triggerEvent) {
$dispatchCalls->append($eventName);
if (PointEvents::TRIGGER_ON_BUILD === $eventName) {
// Emulate a subscriber:
$event->addEvent(
'email.send_to_user',
[
'group' => 'mautic.email.point.trigger',
'label' => 'mautic.email.point.trigger.send_email_to_user',
'formType' => EmailToUserType::class,
'formTypeOptions' => ['update_select' => 'pointtriggerevent_properties_useremail_email'],
'formTheme' => 'MauticEmailBundle:FormTheme\EmailSendList',
'eventName' => EmailEvents::ON_SENT_EMAIL_TO_USER,
]
);
return $event;
} elseif (EmailEvents::ON_SENT_EMAIL_TO_USER === $eventName) {
Assert::assertSame($contact, $event->getLead());
Assert::assertSame($triggerEvent, $event->getTriggerEvent());
return $event;
} else {
$this->fail("Unexpected event name: $eventName");
}
});
$this->triggerModel->triggerEvent($triggerEvent->convertToArray(), $contact, true);
// Assert both expected events were dispatched
Assert::assertContains(PointEvents::TRIGGER_ON_BUILD, $dispatchCalls);
Assert::assertContains(EmailEvents::ON_SENT_EMAIL_TO_USER, $dispatchCalls);
Assert::assertCount(2, $dispatchCalls);
}
}