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,371 @@
<?php
namespace Mautic\DynamicContentBundle\Tests\EventListener;
use Mautic\AssetBundle\Helper\TokenHelper as AssetTokenHelper;
use Mautic\CoreBundle\Event\TokenReplacementEvent;
use Mautic\CoreBundle\Model\AuditLogModel;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\DynamicContentBundle\Entity\DynamicContent;
use Mautic\DynamicContentBundle\EventListener\DynamicContentSubscriber;
use Mautic\DynamicContentBundle\Helper\DynamicContentHelper;
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
use Mautic\FormBundle\Helper\TokenHelper as FormTokenHelper;
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\CompanyModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Mautic\PageBundle\Event\PageDisplayEvent;
use Mautic\PageBundle\Helper\TokenHelper as PageTokenHelper;
use Mautic\PageBundle\Model\TrackableModel;
use MauticPlugin\MauticFocusBundle\Helper\TokenHelper as FocusTokenHelper;
use PHPUnit\Framework\MockObject\MockObject;
class DynamicContentSubscriberTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject|TrackableModel
*/
private MockObject $trackableModel;
/**
* @var MockObject|PageTokenHelper
*/
private MockObject $pageTokenHelper;
/**
* @var MockObject|AssetTokenHelper
*/
private MockObject $assetTokenHelper;
/**
* @var MockObject|FormTokenHelper
*/
private MockObject $formTokenHelper;
/**
* @var MockObject|FocusTokenHelper
*/
private MockObject $focusTokenHelper;
/**
* @var MockObject|AuditLogModel
*/
private MockObject $auditLogModel;
/**
* @var MockObject|DynamicContentHelper
*/
private MockObject $dynamicContentHelper;
/**
* @var MockObject|DynamicContentModel
*/
private MockObject $dynamicContentModel;
/**
* @var MockObject|CorePermissions
*/
private MockObject $security;
/**
* @var MockObject|ContactTracker
*/
private MockObject $contactTracker;
private \PHPUnit\Framework\MockObject\MockObject|CompanyLeadRepository $companyLeadRepositoryMock;
private DynamicContentSubscriber $subscriber;
/**
* @var CompanyModel|(CompanyModel&MockObject)|MockObject
*/
private MockObject $companyModel;
protected function setUp(): void
{
parent::setUp();
$this->trackableModel = $this->createMock(TrackableModel::class);
$this->pageTokenHelper = $this->createMock(PageTokenHelper::class);
$this->assetTokenHelper = $this->createMock(AssetTokenHelper::class);
$this->formTokenHelper = $this->createMock(FormTokenHelper::class);
$this->focusTokenHelper = $this->createMock(FocusTokenHelper::class);
$this->auditLogModel = $this->createMock(AuditLogModel::class);
$this->contactTracker = $this->createMock(ContactTracker::class);
$this->dynamicContentHelper = $this->createMock(DynamicContentHelper::class);
$this->dynamicContentModel = $this->createMock(DynamicContentModel::class);
$this->security = $this->createMock(CorePermissions::class);
$this->contactTracker = $this->createMock(ContactTracker::class);
$this->companyModel = $this->createMock(CompanyModel::class);
$this->companyLeadRepositoryMock = $this->createMock(CompanyLeadRepository::class);
$this->subscriber = new DynamicContentSubscriber(
$this->trackableModel,
$this->pageTokenHelper,
$this->assetTokenHelper,
$this->formTokenHelper,
$this->focusTokenHelper,
$this->auditLogModel,
$this->dynamicContentHelper,
$this->dynamicContentModel,
$this->security,
$this->contactTracker,
$this->companyModel
);
}
/**
* This test is ensuring this error won't happen again:.
*
* DOMDocumentFragment::appendXML(): Entity: line 1: parser error : xmlParseEntityRef: no name
*
* It happens when there is an ampersand in the DWC content.
*/
public function testDecodeTokensWithAmpersandDataAttribute(): void
{
$content = <<< HTML
<!DOCTYPE html>
<html>
<head></head>
<body>
<h2>Hello there!</h2>
<div data-slot="dwc" data-param-slot-name="test-token"></div>
</body>
</html>
HTML;
$expected = <<< HTML
<!DOCTYPE html>
<html>
<head></head>
<body>
<h2>Hello there!</h2>
<a href="https://john.doe&son">Link</a>
</body>
</html>
HTML;
$dwcContent = '<a href="https://john.doe&son">Link</a>';
$event = $this->createMock(PageDisplayEvent::class);
$contact = new Lead();
$event->expects($this->once())
->method('getContent')
->willReturn($content);
$this->security->expects($this->once())
->method('isAnonymous')
->willReturn(true);
$this->contactTracker->expects($this->once())
->method('getContact')
->willReturn($contact);
$this->dynamicContentHelper->expects($this->never())
->method('convertLeadToArray');
$this->dynamicContentHelper->expects($this->once())
->method('findDwcTokens')
->with($content, $contact)
->willReturn([]);
$this->dynamicContentHelper->expects($this->once())
->method('getDynamicContentForLead')
->with('test-token', $contact)
->willReturn($dwcContent);
$event->expects($this->once())
->method('setContent')
->with($expected);
$this->subscriber->decodeTokens($event);
}
/**
* This test is ensuring this error won't happen again:.
*
* DOMDocumentFragment::appendXML(): Entity: line 1: parser error : xmlParseEntityRef: no name
*
* It happens when there is an ampersand in the DWC content.
*/
public function testDecodeTokensWithAmpersandInlineDwc(): void
{
$content = <<< HTML
<!DOCTYPE html>
<html>
<head></head>
<body>
<h2>Hello there!</h2>
{dwc=test-token}
</body>
</html>
HTML;
$expected = <<< HTML
<!DOCTYPE html>
<html>
<head></head>
<body>
<h2>Hello there!</h2>
<a href="https://john.doe&son">Link</a>
</body>
</html>
HTML;
$dwcContent = '<a href="https://john.doe&son">Link</a>';
$event = $this->createMock(PageDisplayEvent::class);
$contact = new Lead();
$event->expects($this->once())
->method('getContent')
->willReturn($content);
$this->security->expects($this->once())
->method('isAnonymous')
->willReturn(true);
$this->contactTracker->expects($this->once())
->method('getContact')
->willReturn($contact);
$this->dynamicContentHelper->expects($this->never())
->method('convertLeadToArray');
$this->dynamicContentHelper->expects($this->once())
->method('findDwcTokens')
->with($content, $contact)
->willReturn([
'{dwc=test-token}' => [
'content' => $dwcContent,
'filters' => [
[
'field' => 'email',
'operator' => '!empty',
'filter' => '',
'type' => 'email',
],
],
],
]);
$this->dynamicContentHelper->expects($this->never())
->method('getDynamicContentForLead');
$event->expects($this->once())
->method('setContent')
->with($expected);
$this->subscriber->decodeTokens($event);
}
public function testOnTokenReplacement(): void
{
$content = <<< HTML
<!DOCTYPE html>
<html>
<head></head>
<body>
<h2>Hello there!</h2>
Company name : {contactfield=companyname}
Company Country : {contactfield=companycountry}
Company website : {contactfield=companywebsite}
</body>
</html>
HTML;
$expected = <<< HTML
<!DOCTYPE html>
<html>
<head></head>
<body>
<h2>Hello there!</h2>
Company name : Doe Corp
Company Country : India
Company website : https://www.doe.corp
</body>
</html>
HTML;
$contact = $this->createMock(Lead::class);
$event = $this->createMock(TokenReplacementEvent::class);
$event
->expects($this->once())
->method('getContent')
->willReturn($content);
$event
->expects($this->once())
->method('getLead')
->willReturn($contact);
$event
->expects($this->once())
->method('getClickthrough')
->willReturn([
'slot' => 'slotOne',
'dynamic_content_id' => 1,
'lead' => 1,
]);
$contact
->expects($this->once())
->method('getProfileFields')
->willReturn([
'id' => 1,
'firstname' => 'John',
'lastname' => 'Doe',
'company' => 'Doe Corp',
'email' => 'john@doe.com',
]);
$this->companyModel
->expects($this->once())
->method('getCompanyLeadRepository')
->willReturn($this->companyLeadRepositoryMock);
$this->companyLeadRepositoryMock->expects($this->once())
->method('getPrimaryCompanyByLeadId')
->willReturn(
[
'id' => 1,
'companyname' => 'Doe Corp',
'companycountry' => 'India',
'companywebsite' => 'https://www.doe.corp',
'is_primary' => true,
]
);
$this->pageTokenHelper
->method('findPageTokens')
->willReturn([]);
$this->assetTokenHelper
->method('findAssetTokens')
->willReturn([]);
$this->formTokenHelper
->method('findFormTokens')
->willReturn([]);
$this->focusTokenHelper
->method('findFocusTokens')
->willReturn([]);
$this->trackableModel
->method('parseContentForTrackables')
->willReturn([
$content,
[],
]);
$dwc = new DynamicContent();
$dwc->setContent($content);
$this->dynamicContentModel
->expects($this->once())
->method('getEntity')
->willReturn($dwc);
$event->expects($this->once())
->method('setContent')
->with($expected);
$this->subscriber->onTokenReplacement($event);
}
}

View File

@@ -0,0 +1,292 @@
<?php
declare(strict_types=1);
namespace Mautic\DynamicContentBundle\Tests\Unit\Helper;
use Mautic\CampaignBundle\Executioner\RealTimeExecutioner;
use Mautic\CoreBundle\Event\TokenReplacementEvent;
use Mautic\DynamicContentBundle\DynamicContentEvents;
use Mautic\DynamicContentBundle\Entity\DynamicContent;
use Mautic\DynamicContentBundle\Event\ContactFiltersEvaluateEvent;
use Mautic\DynamicContentBundle\Helper\DynamicContentHelper;
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\LeadModel;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\EventDispatcher\EventDispatcher;
class DynamicContentHelperTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject&DynamicContentModel
*/
private MockObject $mockModel;
/**
* @var MockObject&RealTimeExecutioner
*/
private MockObject $realTimeExecutioner;
/**
* @var MockObject&EventDispatcher
*/
private MockObject $mockDispatcher;
/**
* @var MockObject&LeadModel
*/
private MockObject $leadModel;
private DynamicContentHelper $helper;
protected function setUp(): void
{
$this->mockModel = $this->createMock(DynamicContentModel::class);
$this->realTimeExecutioner = $this->createMock(RealTimeExecutioner::class);
$this->mockDispatcher = $this->createMock(EventDispatcher::class);
$this->leadModel = $this->createMock(LeadModel::class);
$this->helper = new DynamicContentHelper(
$this->mockModel,
$this->realTimeExecutioner,
$this->mockDispatcher,
$this->leadModel,
);
}
public function testGetDwcBySlotNameWithPublished(): void
{
$matcher = $this->exactly(2);
$this->mockModel->expects($matcher)
->method('getEntities')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame([
'filter' => [
'where' => [
[
'col' => 'e.slotName',
'expr' => 'eq',
'val' => 'test',
],
[
'col' => 'e.isPublished',
'expr' => 'eq',
'val' => 1,
],
],
],
'ignore_paginator' => true,
], $parameters[0]);
return ['some entity'];
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame([
'filter' => [
'where' => [
[
'col' => 'e.slotName',
'expr' => 'eq',
'val' => 'secondtest',
],
],
],
'ignore_paginator' => true,
], $parameters[0]);
return [];
}
});
// Only get published
$this->assertCount(1, $this->helper->getDwcsBySlotName('test', true));
// Get all
$this->assertCount(0, $this->helper->getDwcsBySlotName('secondtest'));
}
public function testGetDynamicContentSlotForLeadWithListenerFindingMatch(): void
{
$slotName = 'test';
$contact = new Lead();
$contact->setFields(['email' => 'ma@ka.t', 'id' => 123]);
$slot = new DynamicContent();
$slot->setName($slotName);
$slot->setIsCampaignBased(false);
// Setting filter that is not known to Mautic, but is for a plugin.
$slot->setFilters([['field' => 'unicorn', 'type' => 'text', 'operator' => '=', 'filter' => 'magic']]);
$slot->setContent('<p>test</p>');
$this->mockModel->method('getEntities')
->willReturn([$slot]);
$this->mockModel->method('getTranslatedEntity')
->willReturn([$slot, $slot]);
$this->leadModel->method('getEntity')
->with(123)
->willReturn($contact);
$this->mockDispatcher->method('hasListeners')->willReturn(true);
$matcher = $this->exactly(2);
$this->mockDispatcher->expects($matcher)
->method('dispatch')->willReturnCallback(function (...$parameters) use ($matcher, $contact, $slot) {
if (1 === $matcher->numberOfInvocations()) {
$callback = function (ContactFiltersEvaluateEvent $event) use ($contact, $slot) {
$this->assertSame($contact, $event->getContact());
$this->assertSame($slot->getFilters(), $event->getFilters());
$event->setIsEvaluated(true);
$event->setIsMatched(true); // Match found in a subscriber.
};
$callback($parameters[0]);
$this->assertSame(DynamicContentEvents::ON_CONTACTS_FILTER_EVALUATE, $parameters[1]);
}
if (2 === $matcher->numberOfInvocations()) {
$callback = function (TokenReplacementEvent $event) use ($contact, $slot) {
$this->assertSame($contact, $event->getLead());
$this->assertSame($slot->getContent(), $event->getContent());
};
$callback($parameters[0]);
$this->assertSame(DynamicContentEvents::TOKEN_REPLACEMENT, $parameters[1]);
}
return $parameters[0];
});
Assert::assertSame(
'<p>test</p>',
$this->helper->getDynamicContentSlotForLead($slotName, $contact)
);
}
public function testGetDynamicContentSlotForLeadWithListenerNotFindingMatch(): void
{
$slotName = 'test';
$contact = new Lead();
$contact->setFields(['email' => 'ma@ka.t', 'id' => 123]);
$slot = new DynamicContent();
$slot->setName($slotName);
$slot->setIsCampaignBased(false);
// Setting filter that is not known to Mautic, nor any plugin.
$slot->setFilters([['field' => 'unicorn', 'type' => 'text', 'operator' => '=', 'filter' => 'magic']]);
$slot->setContent('<p>test</p>');
$this->mockModel->method('getEntities')
->willReturn([$slot]);
$this->mockModel->method('getTranslatedEntity')
->willReturn([$slot, $slot]);
$this->leadModel->method('getEntity')
->with(123)
->willReturn($contact);
$this->mockDispatcher->method('hasListeners')->willReturn(true);
$matcher = $this->once();
$this->mockDispatcher->expects($matcher)
->method('dispatch')
->willReturnCallback(
function (...$parameters) use ($matcher, $contact, $slot) {
if (1 === $matcher->numberOfInvocations()) {
$callback = function (ContactFiltersEvaluateEvent $event) use ($contact, $slot) {
$this->assertSame($contact, $event->getContact());
$this->assertSame($slot->getFilters(), $event->getFilters());
// Match not found in any subscriber.
};
$callback($parameters[0]);
$this->assertSame(DynamicContentEvents::ON_CONTACTS_FILTER_EVALUATE, $parameters[1]);
}
return $parameters[0];
}
);
Assert::assertSame(
'', // No content returned as the filter did not match anything.
$this->helper->getDynamicContentSlotForLead($slotName, $contact)
);
}
public function testGetDynamicContentSlotForLeadWithNoListenerWithMatchingFilter(): void
{
$slotName = 'test';
$contact = new Lead();
$contact->setFields(['email' => 'ma@ka.t', 'id' => 123]);
$slot = new DynamicContent();
$slot->setName($slotName);
$slot->setIsCampaignBased(false);
$slot->setFilters([['field' => 'email', 'type' => 'email', 'operator' => '=', 'filter' => 'ma@ka.t']]);
$slot->setContent('<p>test</p>');
$this->mockModel->method('getEntities')
->willReturn([$slot]);
$this->mockModel->method('getTranslatedEntity')
->willReturn([$slot, $slot]);
$this->leadModel->method('getEntity')
->with(123)
->willReturn($contact);
$this->mockDispatcher->method('hasListeners')->willReturn(false);
$matcher = $this->once();
$this->mockDispatcher->expects($matcher)
->method('dispatch')
->willReturnCallback(
function (...$parameters) use ($matcher, $contact, $slot) {
if (1 === $matcher->numberOfInvocations()) {
$callback = function (TokenReplacementEvent $event) use ($contact, $slot) {
$this->assertSame($contact, $event->getLead());
$this->assertSame($slot->getContent(), $event->getContent());
};
$callback($parameters[0]);
$this->assertSame(DynamicContentEvents::TOKEN_REPLACEMENT, $parameters[1]);
}
return $parameters[0];
}
);
Assert::assertSame(
'<p>test</p>',
$this->helper->getDynamicContentSlotForLead($slotName, $contact)
);
}
public function testGetDynamicContentSlotForLeadWithNoListenerWithNotMatchingFilter(): void
{
$slotName = 'test';
$contact = new Lead();
$contact->setFields(['email' => 'ma@ka.t', 'id' => 123]);
$slot = new DynamicContent();
$slot->setName($slotName);
$slot->setIsCampaignBased(false);
$slot->setFilters([['field' => 'email', 'type' => 'email', 'operator' => '=', 'filter' => 'uni@co.rn']]);
$slot->setContent('<p>test</p>');
$this->mockModel->method('getEntities')
->willReturn([$slot]);
$this->mockModel->method('getTranslatedEntity')
->willReturn([$slot, $slot]);
$this->leadModel->method('getEntity')
->with(123)
->willReturn($contact);
$this->mockDispatcher->method('hasListeners')->willReturn(false);
$this->mockDispatcher->expects($this->never())->method('dispatch');
Assert::assertSame(
'',
$this->helper->getDynamicContentSlotForLead($slotName, $contact)
);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Mautic\DynamicContentBundle\Tests\Unit\Validator\Constraints;
use Mautic\DynamicContentBundle\Validator\Constraints\NoNesting;
use Mautic\DynamicContentBundle\Validator\Constraints\NoNestingValidator;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Context\ExecutionContext;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class NoNestingValidatorTest extends TestCase
{
private const TRANSLATED_MESSAGE = 'DWC tokens cannot be used within another DWC.';
private NoNesting $constraint;
private NoNestingValidator $validator;
private ExecutionContextInterface $context;
protected function setUp(): void
{
$this->constraint = new NoNesting();
$this->validator = new NoNestingValidator();
$this->context = $this->createContext();
$this->context->setConstraint($this->constraint);
$this->validator->initialize($this->context);
}
public function testValidateWithInvalidConstraint(): void
{
$this->expectException(UnexpectedTypeException::class);
$this->expectExceptionMessage(sprintf('Expected argument of type "%s"', NoNesting::class));
$this->validator->validate('value', new NotBlank());
}
public function testValidateWithInvalidType(): void
{
$this->expectException(UnexpectedTypeException::class);
$this->expectExceptionMessage('Expected argument of type "string", "stdClass" given');
$this->validator->validate(new \stdClass(), $this->constraint);
}
public function testValidateWithNull(): void
{
$this->validator->validate(null, $this->constraint);
Assert::assertCount(0, $this->context->getViolations(), 'No violation should be added for a null value.');
}
public function testValidateWithValidValue(): void
{
$this->validator->validate('Some valid value', $this->constraint);
Assert::assertCount(0, $this->context->getViolations(), 'No violation should be added for a valid value.');
}
public function testValidateWithInvalidValue(): void
{
$this->validator->validate('Some invalid value {dwc=some}', $this->constraint);
Assert::assertCount(1, $this->context->getViolations(), 'There should be one violation for an invalid value.');
Assert::assertSame(self::TRANSLATED_MESSAGE, $this->context->getViolations()->get(0)->getMessage());
}
private function createContext(): ExecutionContextInterface
{
$locale = 'en_US';
$validator = $this->createMock(ValidatorInterface::class);
$translator = new Translator($locale);
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', [
'mautic.dynamicContent.no_nesting' => self::TRANSLATED_MESSAGE,
], $locale, 'validators');
return new ExecutionContext($validator, null, $translator, 'validators');
}
}