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,258 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Model;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\CoreBundle\Helper\ClickthroughHelper;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PageBundle\Entity\Hit;
use Mautic\PageBundle\Entity\Page;
use Mautic\PageBundle\Entity\Redirect;
use Mautic\PageBundle\Tests\PageTestAbstract;
use Symfony\Component\HttpFoundation\Request;
class PageModelTest extends PageTestAbstract
{
public function testUtf8CharsInTitleWithTransletirationEnabled(): void
{
$providedTitle = '你好,世界';
$expectedTitle = 'ni hao, shi jie';
$hit = new Hit();
$page = new Page();
$request = new Request();
$contact = new Lead();
$pageModel = $this->getPageModel();
$hit->setIpAddress(new IpAddress());
$hit->setQuery(['page_title' => $providedTitle]);
$pageModel->processPageHit($hit, $page, $request, $contact, false);
$this->assertSame($expectedTitle, $hit->getUrlTitle());
$this->assertSame(['page_title' => $expectedTitle], $hit->getQuery());
}
public function testUtf8CharsInTitleWithTransletirationDisabled(): void
{
$providedTitle = '你好,世界';
$expectedTitle = '你好,世界';
$hit = new Hit();
$page = new Page();
$request = new Request();
$contact = new Lead();
$pageModel = $this->getPageModel(false);
$hit->setIpAddress(new IpAddress());
$hit->setQuery(['page_title' => $providedTitle]);
$pageModel->processPageHit($hit, $page, $request, $contact, false);
$this->assertSame($expectedTitle, $hit->getUrlTitle());
$this->assertSame(['page_title' => $expectedTitle], $hit->getQuery());
}
public function testGenerateUrlWhenCalledReturnsValidUrl(): void
{
$page = new Page();
$page->setAlias('this-is-a-test');
$pageModel = $this->getPageModel();
$this->router->expects($this->once())
->method('generate')
->willReturnCallback(
function (string $route, array $routeParams, int $referenceType) {
$this->assertSame('mautic_page_public', $route);
$this->assertSame(['slug' => 'this-is-a-test'], $routeParams);
$this->assertSame(0, $referenceType);
return '/'.$routeParams['slug'];
}
);
$url = $pageModel->generateUrl($page);
$this->assertStringContainsString('/this-is-a-test', $url);
}
public function testUrlTitleFallbacksToPageTitleWhenNotInQuery(): void
{
$providedTitle = '你好,世界';
$expectedTitle = 'ni hao, shi jie';
$hit = new Hit();
$page = new Page();
$request = new Request();
$contact = new Lead();
$ipAddress = new IpAddress();
$pageModel = $this->getPageModel();
$page->setTitle($providedTitle);
$hit->setIpAddress($ipAddress);
$hit->setQuery([]);
$pageModel->processPageHit($hit, $page, $request, $contact, false);
$this->assertSame($expectedTitle, $hit->getUrlTitle());
}
public function testCleanQueryWhenCalledReturnsSafeAndValidData(): void
{
$pageModel = $this->getPageModel();
$pageModelReflection = new \ReflectionClass($pageModel::class);
$cleanQueryMethod = $pageModelReflection->getMethod('cleanQuery');
$cleanQueryMethod->setAccessible(true);
$res = $cleanQueryMethod->invokeArgs($pageModel, [
[
'page_title' => 'Mautic & PHP',
'page_url' => 'http://mautic.com/page/test?hello=world&lorem=ipsum&q=this%20has%20spaces',
'page_language' => 'en',
],
]);
$this->assertEquals($res, [
'page_title' => 'Mautic &#38; PHP',
'page_url' => 'http://mautic.com/page/test?hello=world&lorem=ipsum&q=this%20has%20spaces',
'page_language' => 'en',
]);
}
/**
* Test getHitQuery when the hit is a Request
* (e.g. POST Ajax or Landingpage hit).
*/
public function testGetHitQueryRequest(): void
{
$pageModel = $this->getPageModel();
foreach ($this->getQueryParams() as $params) {
$request = new Request($params);
$query = $pageModel->getHitQuery($request);
$this->assertUtmQuery($query);
}
}
/**
* Test getHitQuery when the hit is a Redirect.
*/
public function testGetHitQueryRedirect(): void
{
$pageModel = $this->getPageModel();
$request = new Request();
$redirect = new Redirect();
foreach ($this->getQueryParams() as $params) {
$redirect->setUrl($params['page_url']);
$query = $pageModel->getHitQuery($request, $redirect);
$this->assertUtmQuery($query);
}
}
/**
* This test is somewhat synthetic to test the missing $query['ct'].
*/
public function testNoClickThroughInQuery(): void
{
$redirectUrl = '/somewhat';
$pageModel = $this->getPageModel();
$ipAddress = $this->createMock(IpAddress::class);
$ipAddress->method('isTrackable')->willReturn(true);
$this->security->method('isAnonymous')->willReturn(true);
$this->ipLookupHelper->method('getIpAddress')->willReturn($ipAddress);
$this->companyModel->method('fetchCompanyFields')->willReturn([]);
$redirect = $this->createMock(Redirect::class);
$redirect->method('getUrl')->willReturn($redirectUrl);
$this->contactRequestHelper->expects($this->once())
->method('getContactFromQuery')
->with(['page_url' => $redirectUrl])
->willReturn(null);
$result = $pageModel->hitPage($redirect, new Request());
self::assertFalse($result);
}
private function assertUtmQuery(array $query): void
{
$this->assertArrayHasKey('utm_source', $query, 'utm_source not found');
$this->assertArrayHasKey('utm_medium', $query, 'utm_medium not found');
$this->assertArrayHasKey('utm_campaign', $query, 'utm_campaign not found');
$this->assertArrayHasKey('utm_content', $query, 'utm_content not found');
// evaluate all utm tags that they contain the key name in the value
foreach ($query as $key => $value) {
if (str_contains($key, 'utm_')) {
$this->assertNotFalse(strpos($value, (string) $key), sprintf('%s not found in %s', $key, $value));
}
}
}
private function getQueryParams(): array
{
$utm = [
'utm_source' => 'test-utm_source',
'utm_medium' => 'test-utm_medium',
'utm_campaign'=> 'test-utm_campaign',
'utm_content' => 'test-utm_content',
];
$querystring = '';
foreach ($utm as $key => $value) {
$querystring .= sprintf('&%s=%s', $key, $value);
}
$ctParams = [
'source' => ['email', '4'],
'email' => 4,
'stat' => '5f5dedc3b0dc0366144010',
'lead' => 2,
'channel' => [
'email' => 4,
],
];
$ct = ClickthroughHelper::encodeArrayForUrl($ctParams);
$params = [[
'page_title' => 'Testpage',
'page_language' => 'en-GB',
'page_referrer' => '',
'page_url' => sprintf('https://www.domain.com/testpage/?%s', $querystring),
'counter' => 0,
'mautic_device_id'=> 'nowvkqdf6113236eokcg7qs',
'resolution' => '1792x1120',
'timezone_offset' => -120,
'platform' => 'MacIntel',
'do_not_track' => 1,
'adblock' => false,
'fingerprint' => 'fec25ab2d659c4153c7f1d5724841132',
], [
'page_title' => 'Testpage Special Chars',
'page_language' => 'en-GB',
'page_referrer' => '',
'page_url' => 'https://www.domain.com/testpage/?utm_source=t%C3%A9%C3%A0%C3%A8st-utm_source&utm_medium=t%C3%A4%C3%B6ust-utm_medium&utm_campaign=te+%20%C2%B0st-utm_campaign&utm_content=t%E4%BD%A0%E5%A5%BDt-utm_content',
'counter' => 0,
'mautic_device_id'=> 'nowvkqdf6113236eokcg7qs',
'resolution' => '1792x1120',
'timezone_offset' => -120,
'platform' => 'MacIntel',
'do_not_track' => 1,
'adblock' => false,
'fingerprint' => 'fec25ab2d659c4153c7f1d5724841132',
], [
'page_title' => 'Testpage With Encoded Params',
'page_language' => 'en-GB',
'page_referrer' => '',
'page_url' => sprintf('https://www.domain.com/testpage/?ct=%s&%s', $ct, $querystring),
'counter' => 0,
'mautic_device_id'=> 'nowvkqdf6113236eokcg7qs',
'resolution' => '1792x1120',
'timezone_offset' => -120,
'platform' => 'MacIntel',
'do_not_track' => 1,
'adblock' => false,
'fingerprint' => 'fec25ab2d659c4153c7f1d5724841132',
]];
return $params;
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Mautic\PageBundle\Tests\Model;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Shortener\Shortener;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\PageBundle\Entity\Redirect;
use Mautic\PageBundle\Event\RedirectGenerationEvent;
use Mautic\PageBundle\Model\RedirectModel;
use Mautic\PageBundle\PageEvents;
use Mautic\PageBundle\Tests\PageTestAbstract;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\EventDispatcher\EventDispatcher;
class RedirectModelTest extends PageTestAbstract
{
public function testCreateRedirectEntityWhenCalledReturnsRedirect(): void
{
$redirectModel = $this->getRedirectModel();
$entity = $redirectModel->createRedirectEntity('http://some-url.com');
$this->assertInstanceOf(Redirect::class, $entity);
}
public function testGenerateRedirectUrlWhenCalledReturnsValidUrl(): void
{
$redirect = new Redirect();
$redirect->setUrl('http://some-url.com');
$redirect->setRedirectId('redirect-id');
$redirectModel = $this->getRedirectModel();
$url = $redirectModel->generateRedirectUrl($redirect);
$this->assertStringContainsString($url, 'http://some-url.com');
}
public function testRedirectGenerationEvent(): void
{
$shortener = $this->createMock(Shortener::class);
$dispatcher = new EventDispatcher();
$url = 'https://mautic.org';
$clickthrough = ['foo' => 'bar'];
$router = $this->createMock(Router::class);
$router->expects($this->exactly(2))
->method('generate')
->willReturn($url);
$model = new RedirectModel(
$this->createMock(EntityManagerInterface::class),
$this->createMock(CorePermissions::class),
$dispatcher,
$router,
$this->createMock(Translator::class),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class),
$shortener
);
$redirect = new Redirect();
$redirect->setUrl($url);
// URL should just have foo = bar in the CT
$url = $model->generateRedirectUrl($redirect, $clickthrough);
$this->assertEquals('https://mautic.org?ct=YToxOntzOjM6ImZvbyI7czozOiJiYXIiO30%3D', $url);
// Add the listener to append something else to the CT
$dispatcher->addListener(
PageEvents::ON_REDIRECT_GENERATE,
function (RedirectGenerationEvent $event): void {
$event->setInClickthrough('bar', 'foo');
}
);
$url = $model->generateRedirectUrl($redirect, $clickthrough);
$this->assertEquals('https://mautic.org?ct=YToyOntzOjM6ImZvbyI7czozOiJiYXIiO3M6MzoiYmFyIjtzOjM6ImZvbyI7fQ%3D%3D', $url);
}
}

View File

@@ -0,0 +1,688 @@
<?php
namespace Mautic\PageBundle\Tests\Model;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Entity\LeadFieldRepository;
use Mautic\PageBundle\Entity\Redirect;
use Mautic\PageBundle\Entity\Trackable;
use Mautic\PageBundle\Model\RedirectModel;
use Mautic\PageBundle\Model\TrackableModel;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[\PHPUnit\Framework\Attributes\CoversClass(TrackableModel::class)]
class TrackableModelTest extends TestCase
{
#[\PHPUnit\Framework\Attributes\TestDox('Test that content is detected as HTML')]
public function testHtmlIsDetectedInContent(): void
{
$mockRedirectModel = $this->createMock(RedirectModel::class);
$mockLeadFieldRepository = $this->createMock(LeadFieldRepository::class);
$mockModel = $this->getMockBuilder(TrackableModel::class)
->setConstructorArgs([
$mockRedirectModel,
$mockLeadFieldRepository,
$this->createMock(EntityManagerInterface::class),
$this->createMock(CorePermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->createMock(UrlGeneratorInterface::class),
$this->createMock(Translator::class),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class),
])
->onlyMethods(['getDoNotTrackList', 'getEntitiesFromUrls', 'createTrackingTokens', 'extractTrackablesFromHtml'])
->getMock();
$mockModel->expects($this->once())
->method('getEntitiesFromUrls')
->willReturn([]);
$mockModel->expects($this->once())
->method('getDoNotTrackList')
->willReturn([]);
$mockModel->expects($this->once())
->method('extractTrackablesFromHtml')
->willReturn(
[
'',
[],
]
);
$mockModel->expects($this->once())
->method('createTrackingTokens')
->willReturn([]);
[$content, $trackables] = $mockModel->parseContentForTrackables(
$this->generateContent('https://foo-bar.com', 'html'),
[],
'email',
1
);
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that content is detected as plain text')]
public function testPlainTextIsDetectedInContent(): void
{
$mockRedirectModel = $this->createMock(RedirectModel::class);
$mockLeadFieldRepository = $this->createMock(LeadFieldRepository::class);
$mockModel = $this->getMockBuilder(TrackableModel::class)
->setConstructorArgs([
$mockRedirectModel,
$mockLeadFieldRepository,
$this->createMock(EntityManagerInterface::class),
$this->createMock(CorePermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->createMock(UrlGeneratorInterface::class),
$this->createMock(Translator::class),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class),
])
->onlyMethods(['getDoNotTrackList', 'getEntitiesFromUrls', 'createTrackingTokens', 'extractTrackablesFromText'])
->getMock();
$mockModel->expects($this->once())
->method('getDoNotTrackList')
->willReturn([]);
$mockModel->expects($this->once())
->method('getEntitiesFromUrls')
->willReturn([]);
$mockModel->expects($this->once())
->method('extractTrackablesFromText')
->willReturn(
[
'',
[],
]
);
$mockModel->expects($this->once())
->method('createTrackingTokens')
->willReturn([]);
[$content, $trackables] = $mockModel->parseContentForTrackables(
$this->generateContent('https://foo-bar.com', 'text'),
[],
'email',
1
);
}
#[\PHPUnit\Framework\Attributes\DataProvider('trackMapProvider')]
#[\PHPUnit\Framework\Attributes\TestDox('Test that a standard link with a standard query is parsed correctly')]
public function testStandardLinkWithStandardQuery(?bool $useMap): void
{
$url = 'https://foo-bar.com?foo=bar&amp;one=two&three=four&amp;five=six';
$model = $this->getModel();
if (null !== $useMap) {
$emailContent = $this->generateContent($url, 'html', false, $useMap);
} else {
$emailContent = $this->generateContent($url, 'html', false, true)
.$this->generateContent($url, 'html', false, false);
}
[$content, $trackables] = $model->parseContentForTrackables(
$emailContent,
[],
'email',
1
);
$tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match);
// Assert that a trackable token exists
Assert::assertTrue((bool) $tokenFound, $content);
// Assert the Trackable exists
Assert::assertArrayHasKey($match[0], $trackables);
// Assert that exactly one trackable found
Assert::assertCount(1, $trackables);
// Assert that the URL redirect equals $url
$redirect = $trackables[$match[0]]->getRedirect();
Assert::assertEquals(str_replace('&amp;', '&', $url), $redirect->getUrl());
}
#[\PHPUnit\Framework\Attributes\DataProvider('trackMapProvider')]
#[\PHPUnit\Framework\Attributes\TestDox('Test that a standard link without a query parses correctly')]
public function testStandardLinkWithoutQuery(?bool $useMap): void
{
$url = 'https://foo-bar.com';
$model = $this->getModel();
if (null !== $useMap) {
$emailContent = $this->generateContent($url, 'html', false, $useMap);
} else {
$emailContent = $this->generateContent($url, 'html', false, true)
.$this->generateContent($url, 'html', false, false);
}
[$content, $trackables] = $model->parseContentForTrackables(
$emailContent,
[],
'email',
1
);
$tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match);
// Assert that a trackable token exists
Assert::assertTrue((bool) $tokenFound, $content);
// Assert the Trackable exists
Assert::assertArrayHasKey($match[0], $trackables);
// Assert that exactly one trackable found
Assert::assertCount(1, $trackables);
// Assert that the URL redirect equals $url
$redirect = $trackables[$match[0]]->getRedirect();
Assert::assertEquals($url, $redirect->getUrl());
}
#[\PHPUnit\Framework\Attributes\DataProvider('trackMapProvider')]
#[\PHPUnit\Framework\Attributes\TestDox('Test that a standard link with a tokenized query parses correctly')]
public function testStandardLinkWithTokenizedQuery(?bool $useMap): void
{
$url = 'https://foo-bar.com?foo={contactfield=bar}&bar=foo';
$model = $this->getModel();
if (null !== $useMap) {
$emailContent = $this->generateContent($url, 'html', false, $useMap);
} else {
$emailContent = $this->generateContent($url, 'html', false, true)
.$this->generateContent($url, 'html', false, false);
}
[$content, $trackables] = $model->parseContentForTrackables(
$emailContent,
[
'{contactfield=bar}' => '',
],
'email',
1
);
$tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match);
// Assert that a trackable token exists
Assert::assertTrue((bool) $tokenFound, $content);
// Assert that exactly one trackable found
Assert::assertCount(1, $trackables);
// Assert the Trackable exists
Assert::assertArrayHasKey('{trackable='.$match[1].'}', $trackables);
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a token used in place of a URL is parsed properly')]
public function testTokenizedDomain(): void
{
$url = 'http://{contactfield=foo}.org';
$model = $this->getModel();
[$content, $trackables] = $model->parseContentForTrackables(
$this->generateContent($url, 'html'),
[
'{contactfield=foo}' => 'mautic',
],
'email',
1
);
$tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match);
// Assert that a trackable token exists
$this->assertTrue((bool) $tokenFound, $content);
// Assert the Trackable exists
$this->assertArrayHasKey('{trackable='.$match[1].'}', $trackables);
}
public function testTokenizedHostWithScheme(): void
{
$url = '{contactfield=foo}';
$model = $this->getModel();
[$content, $trackables] = $model->parseContentForTrackables(
$this->generateContent($url, 'html'),
[
'{contactfield=foo}' => 'https://mautic.org',
],
'email',
1
);
$tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match);
// Assert that a trackable token exists
$this->assertTrue((bool) $tokenFound, $content);
// Assert the Trackable exists
$this->assertArrayHasKey('{trackable='.$match[1].'}', $trackables);
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a token used in place of a URL is parsed')]
public function testTokenizedHostWithQuery(): void
{
$url = 'http://{contactfield=foo}.com?foo=bar';
$model = $this->getModel();
[$content, $trackables] = $model->parseContentForTrackables(
$this->generateContent($url, 'html'),
[
'{contactfield=foo}' => '',
],
'email',
1
);
$tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match);
// Assert that a trackable token exists
$this->assertTrue((bool) $tokenFound, $content);
// Assert the Trackable exists
$this->assertArrayHasKey('{trackable='.$match[1].'}', $trackables);
}
public function testTokenizedHostWithTokenizedQuery(): void
{
$url = 'http://{contactfield=foo}.com?foo={contactfield=bar}';
$model = $this->getModel();
[$content, $trackables] = $model->parseContentForTrackables(
$this->generateContent($url, 'html'),
[
'{contactfield=foo}' => '',
'{contactfield=bar}' => '',
],
'email',
1
);
$tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match);
// Assert that a trackable token exists
$this->assertTrue((bool) $tokenFound, $content);
// Assert the Trackable exists
$this->assertArrayHasKey('{trackable='.$match[1].'}', $trackables);
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that tokens that are supposed to be ignored are')]
public function testIgnoredTokensAreNotConverted(): void
{
$url = 'https://{unsubscribe_url}';
$model = $this->getModel(['{unsubscribe_url}']);
[$content, $trackables] = $model->parseContentForTrackables(
$this->generateContent($url, 'html'),
[
'{unsubscribe_url}' => 'https://domain.com/email/unsubscribe/xxxxxxx',
],
'email',
1
);
$this->assertEmpty($trackables, $content);
$this->assertFalse(strpos($content, $url), 'https:// should have been stripped from the token URL');
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that tokens that are supposed to be ignored are')]
public function testUnsupportedTokensAreNotConverted(): void
{
$url = '{random_token}';
$model = $this->getModel();
[$content, $trackables] = $model->parseContentForTrackables(
$this->generateContent($url, 'text'),
[
'{unsubscribe_url}' => 'https://domain.com/email/unsubscribe/xxxxxxx',
],
'email',
1
);
$this->assertEmpty($trackables, $content);
}
public function testTokenWithDefaultValueInPlaintextWillCountAsOne(): void
{
$url = '{contactfield=website|https://mautic.org}';
$model = $this->getModel();
$inputContent = $this->generateContent($url, 'text');
[$content, $trackables] = $model->parseContentForTrackables(
$inputContent,
[
'{contactfield=website}' => 'https://mautic.org/about-us',
],
'email',
1
);
$tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match);
// Assert that a trackable token exists
$this->assertTrue((bool) $tokenFound, $content);
// Assert the Trackable exists
$trackableKey = '{trackable='.$match[1].'}';
$this->assertArrayHasKey('{trackable='.$match[1].'}', $trackables);
$this->assertEquals(1, count($trackables));
$this->assertEquals('{contactfield=website|https://mautic.org}', $trackables[$trackableKey]->getRedirect()->getUrl());
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that a URL injected into the do not track list is not converted')]
public function testIgnoredUrlDoesNotCrash(): void
{
$url = 'https://domain.com';
$model = $this->getModel([$url]);
[$content, $trackables] = $model->parseContentForTrackables(
$this->generateContent($url, 'html'),
[],
'email',
1
);
$this->assertTrue(str_contains($content, $url), $content);
}
#[\PHPUnit\Framework\Attributes\DataProvider('trackMapProvider')]
#[\PHPUnit\Framework\Attributes\TestDox('Test that a token used in place of a URL is not parsed')]
public function testTokenAsHostIsConvertedToTrackableToken(?bool $useMap): void
{
$url = 'http://{pagelink=1}';
$model = $this->getModel();
if (null !== $useMap) {
$emailContent = $this->generateContent($url, 'html', false, $useMap);
} else {
$emailContent = $this->generateContent($url, 'html', false, true)
.$this->generateContent($url, 'html', false, false);
}
[$content, $trackables] = $model->parseContentForTrackables(
$emailContent,
[
'{pagelink=1}' => 'http://foo-bar.com',
],
'email',
1
);
$token = array_key_first($trackables);
Assert::assertNotEmpty($trackables, $content);
Assert::assertStringContainsString($token, $content);
// Assert that exactly one trackable found
Assert::assertCount(1, $trackables);
}
#[\PHPUnit\Framework\Attributes\DataProvider('trackMapProvider')]
#[\PHPUnit\Framework\Attributes\TestDox('Test that a URLs with same base or correctly replaced')]
public function testUrlsWithSameBaseAreReplacedCorrectly(?bool $useMap): void
{
$urls = [
'https://foo-bar.com',
'https://foo-bar.com?foo=bar',
'https://FOO-bar.com/bar',
];
$model = $this->getModel();
if (null !== $useMap) {
$emailContent = $this->generateContent($urls, 'html', false, $useMap);
} else {
$emailContent = $this->generateContent($urls, 'html', false, true)
.$this->generateContent($urls, 'html', false, false);
}
[$content, $trackables] = $model->parseContentForTrackables(
$emailContent,
[],
'email',
1
);
// Assert that both trackables found
Assert::assertCount(3, $trackables);
foreach ($trackables as $redirectId => $trackable) {
// If the shared base was correctly parsed, all generated tokens will be in the content
Assert::assertNotFalse(strpos($content, (string) $redirectId), $content);
}
}
#[\PHPUnit\Framework\Attributes\TestDox('Test that css images are not converted if there are no links')]
public function testCssUrlsAreNotConvertedIfThereAreNoLinks(): void
{
$model = $this->getModel();
[$content, $trackables] = $model->parseContentForTrackables(
'<style> .mf-modal { background-image: url(\'https://www.mautic.org/wp-content/uploads/2014/08/iTunesArtwork.png\'); } </style>',
[],
'email',
1
);
$this->assertEmpty($trackables);
}
#[\PHPUnit\Framework\Attributes\TestDox('Tests that URLs in the plaintext does not contaminate HTML')]
public function testPlainTextDoesNotContaminateHtml(): void
{
$model = $this->getModel();
$html = <<<TEXT
Hi {contactfield=firstname},
<br />
Come to our office in {contactfield=city}!
<br />
John Smith<br />
VP of Sales<br />
https://plaintexttest.io
TEXT;
$plainText = strip_tags($html);
$combined = [$html, $plainText];
[$content, $trackables] = $model->parseContentForTrackables(
$combined,
[],
'email',
1
);
$this->assertCount(1, $trackables);
// No links so no trackables
$this->assertEquals($html, $content[0]);
$token = array_key_first($trackables);
self::assertNotNull($token);
$this->assertEquals(str_replace('https://plaintexttest.io', $token, $plainText), $content[1]);
}
#[\PHPUnit\Framework\Attributes\TestDox('Tests that URL based contact fields are found in plain text')]
public function testPlainTextFindsUrlContactFields(): void
{
$model = $this->getModel([], ['website']);
$html = <<<TEXT
Hi {contactfield=firstname},
<br />
Come to our office in {contactfield=city}!
<br />
John Smith<br />
VP of Sales<br />
{contactfield=website}
TEXT;
$plainText = strip_tags($html);
$combined = [$html, $plainText];
[$content, $trackables] = $model->parseContentForTrackables(
$combined,
[],
'email',
1
);
$this->assertCount(1, $trackables);
// No links so no trackables
$this->assertEquals($html, $content[0]);
$token = array_key_first($trackables);
self::assertNotNull($token);
$this->assertEquals(str_replace('{contactfield=website}', $token, $plainText), $content[1]);
}
/**
* @param array $doNotTrack
* @param array $urlFieldsForPlaintext
*
* @return TrackableModel|\PHPUnit\Framework\MockObject\MockObject
*/
protected function getModel($doNotTrack = [], $urlFieldsForPlaintext = [])
{
// Add default DoNotTrack
$doNotTrack = array_merge(
$doNotTrack,
[
'{webview_url}',
'{unsubscribe_url}',
'{trackable=(.*?)}',
]
);
$mockRedirectModel = $this->createMock(RedirectModel::class);
$mockLeadFieldRepository = $this->createMock(LeadFieldRepository::class);
$mockModel = $this->getMockBuilder(TrackableModel::class)
->setConstructorArgs([
$mockRedirectModel,
$mockLeadFieldRepository,
$this->createMock(EntityManagerInterface::class),
$this->createMock(CorePermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->createMock(UrlGeneratorInterface::class),
$this->createMock(Translator::class),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class),
])
->onlyMethods(['getDoNotTrackList', 'getEntitiesFromUrls', 'getContactFieldUrlTokens'])
->getMock();
$mockModel->expects($this->once())
->method('getDoNotTrackList')
->willReturn($doNotTrack);
$mockModel->expects($this->any())
->method('getEntitiesFromUrls')
->willReturnCallback(
function ($trackableUrls, $channel, $channelId) {
$entities = [];
foreach ($trackableUrls as $url) {
$entities[$url] = $this->getTrackableEntity($url);
}
return $entities;
}
);
$mockModel->expects($this->any())
->method('getContactFieldUrlTokens')
->willReturn($urlFieldsForPlaintext);
return $mockModel;
}
/**
* @return Trackable
*/
protected function getTrackableEntity($url)
{
$redirect = new Redirect();
$redirect->setUrl($url);
$redirect->setRedirectId();
$trackable = new Trackable();
$trackable->setChannel('email')
->setChannelId(1)
->setRedirect($redirect)
->setHits(random_int(1, 10))
->setUniqueHits(random_int(1, 10));
return $trackable;
}
/**
* @param array<int, string>|string $urls
*/
protected function generateContent($urls, string $type, bool $doNotTrack = false, bool $useMap = false): string
{
$content = '';
if (!is_array($urls)) {
$urls = [$urls];
}
foreach ($urls as $url) {
if ('html' === $type) {
$dnc = ($doNotTrack) ? ' mautic:disable-tracking' : '';
if ($useMap) {
$content .= <<<CONTENT
ABC123 321ABC
ABC123 <map><area href="$url"$dnc alt="alt" /></map> 321ABC
CONTENT;
} else {
$content .= <<<CONTENT
ABC123 321ABC
ABC123 <a href="$url"$dnc>$url</a> 321ABC
CONTENT;
}
} else {
$content .= <<<CONTENT
ABC123 321ABC
ABC123 $url 321ABC
CONTENT;
}
}
return $content;
}
/**
* @return array<array<bool|null>> Use null to include both <a> and <map> tags
*/
public static function trackMapProvider(): array
{
return [
[true],
[false],
[null],
];
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Mautic\PageBundle\Tests\Model;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Mautic\PageBundle\Model\PageModel;
use Mautic\PageBundle\Model\Tracking404Model;
class Tracking404ModelTest extends \PHPUnit\Framework\TestCase
{
/**
* @var ContactTracker|\PHPUnit\Framework\MockObject\MockObject
*/
private \PHPUnit\Framework\MockObject\MockObject $mockContactTracker;
/**
* @var CoreParametersHelper|\PHPUnit\Framework\MockObject\MockObject
*/
private \PHPUnit\Framework\MockObject\MockObject $mockCoreParametersHelper;
/**
* @var PageModel|\PHPUnit\Framework\MockObject\MockObject
*/
private \PHPUnit\Framework\MockObject\MockObject $mockPageModel;
private Lead $lead;
public function setUp(): void
{
parent::setUp();
$this->mockCoreParametersHelper = $this->createMock(CoreParametersHelper::class);
$this->mockContactTracker = $this->createMock(ContactTracker::class);
$this->mockPageModel = $this->createMock(PageModel::class);
$this->lead = new Lead();
}
public function testIsTrackableIfTracking404OptionEnabled(): void
{
$this->mockCoreParametersHelper->expects($this->once())
->method('get')
->with('do_not_track_404_anonymous')
->willReturn(true);
$tracking404Model = new Tracking404Model($this->mockCoreParametersHelper, $this->mockContactTracker, $this->mockPageModel);
$this->assertFalse($tracking404Model->isTrackable());
}
public function testIsTrackableIfTracking404OptionDisable(): void
{
$this->mockCoreParametersHelper->expects($this->once())
->method('get')
->with('do_not_track_404_anonymous')
->willReturn(false);
$tracking404Model = new Tracking404Model($this->mockCoreParametersHelper, $this->mockContactTracker, $this->mockPageModel);
$this->assertTrue($tracking404Model->isTrackable());
}
public function testIsTrackableForIdentifiedContacts(): void
{
$this->mockCoreParametersHelper->expects($this->once())
->method('get')
->with('do_not_track_404_anonymous')
->willReturn(true);
$this->lead->setFirstname('identified');
$this->mockContactTracker->expects($this->any())
->method('getContactByTrackedDevice')
->willReturn($this->lead);
$tracking404Model = new Tracking404Model($this->mockCoreParametersHelper, $this->mockContactTracker, $this->mockPageModel);
$this->assertTrue($tracking404Model->isTrackable());
}
public function testIsTrackableForAnonymouse(): void
{
$this->mockCoreParametersHelper->expects($this->once())
->method('get')
->with('do_not_track_404_anonymous')
->willReturn(true);
$this->mockContactTracker->expects($this->any())
->method('getContactByTrackedDevice')
->willReturn($this->lead);
$tracking404Model = new Tracking404Model($this->mockCoreParametersHelper, $this->mockContactTracker, $this->mockPageModel);
$this->assertFalse($tracking404Model->isTrackable());
}
}