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,109 @@
<?php
declare(strict_types=1);
namespace Mautic\AssetBundle\Tests\Asset;
use Doctrine\ORM\ORMException;
use Doctrine\Persistence\Mapping\MappingException;
use Mautic\AssetBundle\Entity\Asset;
use Mautic\CoreBundle\Helper\CsvHelper;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
abstract class AbstractAssetTestCase extends MauticMysqlTestCase
{
protected Asset $asset;
protected string $expectedMimeType;
protected string $expectedContentDisposition;
protected string $expectedPngContent;
protected string $csvPath;
protected function setUp(): void
{
parent::setUp();
$this->generateCsv();
$assetData = [
'title' => 'Asset controller test. Preview action',
'alias' => 'Test',
'createdAt' => new \DateTime('2021-05-05 22:30:00'),
'updatedAt' => new \DateTime('2022-05-05 22:30:00'),
'createdBy' => 'User',
'storage' => 'local',
'path' => basename($this->csvPath),
'extension' => 'png',
];
$this->asset = $this->createAsset($assetData);
$this->expectedMimeType = 'text/plain; charset=UTF-8';
$this->expectedContentDisposition = 'attachment;filename="';
$this->expectedPngContent = file_get_contents($this->csvPath);
}
protected function beforeTearDown(): void
{
if (file_exists($this->csvPath)) {
unlink($this->csvPath);
}
}
/**
* Create an asset entity in the DB.
*
* @param array<string, string|mixed> $assetData
*
* @throws ORMException
* @throws MappingException
*/
protected function createAsset(array $assetData): Asset
{
$asset = new Asset();
$asset->setTitle($assetData['title']);
$asset->setAlias($assetData['alias']);
$asset->setDateAdded($assetData['createdAt'] ?? new \DateTime());
$asset->setDateModified($assetData['updatedAt'] ?? new \DateTime());
$asset->setCreatedByUser($assetData['createdBy'] ?? 'User');
$asset->setStorageLocation($assetData['storage'] ?? 'local');
$asset->setPath($assetData['path'] ?? '');
$asset->setExtension($assetData['extension'] ?? '');
$asset->setSize($this->csvPath ? filesize($this->csvPath) : 0);
$this->em->persist($asset);
$this->em->flush();
$this->em->detach($asset);
return $asset;
}
/**
* Generate the csv asset and return the path of the asset.
*/
protected function generateCsv(): void
{
$uploadDir = static::getContainer()->get('mautic.helper.core_parameters')->get('upload_dir') ?? sys_get_temp_dir();
$tmpFile = tempnam($uploadDir, 'mautic_asset_test_');
$file = fopen($tmpFile, 'w');
$initialList = [
['email', 'firstname', 'lastname'],
['john.doe@his-site.com.email', 'John', 'Doe'],
['john.smith@his-site.com.email', 'John', 'Smith'],
['jim.doe@his-site.com.email', 'Jim', 'Doe'],
[''],
['jim.smith@his-site.com.email', 'Jim', 'Smith'],
];
foreach ($initialList as $line) {
CsvHelper::putCsv($file, $line);
}
fclose($file);
$this->csvPath = $tmpFile;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Mautic\AssetBundle\Tests\Controller\Api;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
class AssetApiControllerFunctionalTest extends MauticMysqlTestCase
{
public function testCreateNewRemoteAsset(): void
{
$payload = [
'file' => 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
'storageLocation' => 'remote',
'title' => 'title',
];
$this->client->request('POST', 'api/assets/new', $payload);
$clientResponse = $this->client->getResponse();
$this->assertResponseStatusCodeSame(201, $clientResponse->getContent());
$response = json_decode($clientResponse->getContent(), true);
$this->assertEquals($payload['title'], $response['asset']['title']);
$this->assertEquals($payload['storageLocation'], $response['asset']['storageLocation']);
$this->assertStringContainsString('application/pdf', $response['asset']['mime']);
$this->assertStringContainsString('pdf', $response['asset']['extension']);
$this->assertNotNull($response['asset']['size']);
}
public function testCreateNewRemoteAssetWithVulnerableFile(): void
{
$payload = [
'file' => 'file:///etc/passwd',
'storageLocation' => 'remote',
'title' => 'title',
];
$this->client->request('POST', 'api/assets/new', $payload);
$clientResponse = $this->client->getResponse();
$this->assertResponseStatusCodeSame(400, $clientResponse->getContent());
$this->assertEquals('{"errors":[{"code":400,"message":"remotePath: The remote should be a valid URL.","details":{"remotePath":["The remote should be a valid URL."]}}]}', $clientResponse->getContent());
}
public function testCreateNewLocalAsset(): void
{
$assetsPath = $this->client->getKernel()->getContainer()->getParameter('mautic.upload_dir');
file_put_contents($assetsPath.'/file.txt', 'test');
$payload = [
'file' => 'file.txt',
'storageLocation' => 'local',
'title' => 'title',
];
$this->client->request('POST', 'api/assets/new', $payload);
$clientResponse = $this->client->getResponse();
$this->assertResponseStatusCodeSame(201, $clientResponse->getContent());
$response = json_decode($clientResponse->getContent(), true);
$this->assertEquals($payload['title'], $response['asset']['title']);
$this->assertEquals($payload['storageLocation'], $response['asset']['storageLocation']);
$this->assertStringContainsString('text/plain', $response['asset']['mime']);
$this->assertNotNull($response['asset']['size']);
$this->assertStringContainsString('txt', $response['asset']['extension']);
unlink($assetsPath.'/file.txt');
}
}

View File

@@ -0,0 +1,388 @@
<?php
declare(strict_types=1);
namespace Mautic\AssetBundle\Tests\Controller;
use Mautic\AssetBundle\Entity\Asset;
use Mautic\AssetBundle\Tests\Asset\AbstractAssetTestCase;
use Mautic\CoreBundle\Tests\Traits\ControllerTrait;
use Mautic\PageBundle\Tests\Controller\PageControllerTest;
use Mautic\ProjectBundle\Entity\Project;
use Mautic\UserBundle\Entity\Permission;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Model\RoleModel;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class AssetControllerFunctionalTest extends AbstractAssetTestCase
{
use ControllerTrait;
private const SALES_USER = 'sales';
private const ADMIN_USER = 'admin';
/**
* Index action should return status code 200.
*/
public function testIndexAction(): void
{
$asset = new Asset();
$asset->setTitle('test');
$asset->setAlias('test');
$asset->setDateAdded(new \DateTime('2020-02-07 20:29:02'));
$asset->setDateModified(new \DateTime('2020-03-21 20:29:02'));
$asset->setCreatedByUser('Test User');
$this->em->persist($asset);
$this->em->flush();
$this->em->detach($asset);
$urlAlias = 'assets';
$routeAlias = 'asset';
$column = 'dateModified';
$column2 = 'title';
$tableAlias = 'a.';
$this->getControllerColumnTests($urlAlias, $routeAlias, $column, $tableAlias, $column2);
}
public function testAssetSizes(): void
{
$this->client->request('GET', '/s/ajax?action=email:getAttachmentsSize&assets%5B%5D='.$this->asset->getId());
$this->assertResponseIsSuccessful();
Assert::assertSame('{"size":"178 bytes"}', $this->client->getResponse()->getContent());
}
/**
* Preview action should return the file content.
*/
public function testPreviewActionStreamByDefault(): void
{
$this->client->request('GET', '/s/assets/preview/'.$this->asset->getId());
ob_start();
$response = $this->client->getResponse();
$response->sendContent();
$content = ob_get_contents();
ob_end_clean();
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
$this->assertSame($this->expectedMimeType, $response->headers->get('Content-Type'));
$this->assertNotSame($this->expectedContentDisposition.$this->asset->getOriginalFileName(), $response->headers->get('Content-Disposition'));
$this->assertEquals($this->expectedPngContent, $content);
}
/**
* Preview action should return the file content.
*/
public function testPreviewActionStreamIsZero(): void
{
$this->client->request('GET', '/s/assets/preview/'.$this->asset->getId().'?stream=0&download=1');
ob_start();
$response = $this->client->getResponse();
$response->sendContent();
$content = ob_get_contents();
ob_end_clean();
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
$this->assertSame($this->expectedContentDisposition.$this->asset->getOriginalFileName(), $response->headers->get('Content-Disposition'));
$this->assertEquals($this->expectedPngContent, $content);
}
/**
* Preview action should return the html code.
*/
public function testPreviewActionStreamDownloadAreZero(): void
{
$this->client->request('GET', '/s/assets/preview/'.$this->asset->getId().'?stream=0&download=0');
ob_start();
$response = $this->client->getResponse();
$response->sendContent();
$content = ob_get_contents();
ob_end_clean();
$this->assertSame(Response::HTTP_OK, $response->getStatusCode(), $content);
$this->assertNotEquals($this->expectedPngContent, $content);
PageControllerTest::assertTrue($response->isOk());
$assetSlug = $this->asset->getId().':'.$this->asset->getAlias();
PageControllerTest::assertStringContainsString(
'/asset/'.$assetSlug,
$content,
'The return must contain the assert slug'
);
}
/**
* @param array<string, string[]> $permission
*/
#[\PHPUnit\Framework\Attributes\DataProvider('getValuesProvider')]
public function testEditWithPermissions(string $route, array $permission, int $expectedStatusCode, string $userCreatorUN): void
{
$userCreator = $this->getUser($userCreatorUN);
$userEditor = $this->getUser(self::SALES_USER);
$this->setPermission($userEditor, ['asset:assets' => $permission]);
$asset = new Asset();
$asset->setTitle('Asset A');
$asset->setAlias('asset-a');
$asset->setStorageLocation('local');
$asset->setPath('broken-image.jpg');
$asset->setExtension('jpg');
$asset->setCreatedByUser($userCreator->getUserIdentifier());
$asset->setCreatedBy($userCreator->getId());
$this->em->persist($asset);
$this->em->flush();
$this->em->clear();
$this->logoutUser();
$this->loginUser($userEditor);
$this->client->request(Request::METHOD_GET, "/s/assets/{$route}/{$asset->getId()}");
Assert::assertSame($expectedStatusCode, $this->client->getResponse()->getStatusCode());
}
/**
* @return \Generator<string, mixed[]>
*/
public static function getValuesProvider(): \Generator
{
yield 'The sales user with edit own permission can edits its own asset' => [
'route' => 'edit',
'permission' => ['editown'],
'expectedStatusCode' => Response::HTTP_OK,
'userCreatorUN' => self::SALES_USER,
];
yield 'The sales user with edit own permission cannot edit asset created by admin' => [
'route' => 'edit',
'permission' => ['editown'],
'expectedStatusCode' => Response::HTTP_FORBIDDEN,
'userCreatorUN' => self::ADMIN_USER,
];
yield 'The sales user with edit other permission can edit asset created by admin' => [
'route' => 'edit',
'permission' => ['editown', 'editother'],
'expectedStatusCode' => Response::HTTP_OK,
'userCreatorUN' => self::ADMIN_USER,
];
yield 'The sales user with view own permission cannot edit or asset created by admin' => [
'route' => 'edit',
'permission' => ['viewown'],
'expectedStatusCode' => Response::HTTP_FORBIDDEN,
'userCreatorUN' => self::ADMIN_USER,
];
yield 'The sales user with view other permission cannot edit asset created by admin' => [
'route' => 'edit',
'permission' => ['viewown', 'viewother'],
'expectedStatusCode' => Response::HTTP_FORBIDDEN,
'userCreatorUN' => self::ADMIN_USER,
];
yield 'The sales user with view own permission cannot view asset created by admin' => [
'route' => 'view',
'permission' => ['viewown'],
'expectedStatusCode' => Response::HTTP_FORBIDDEN,
'userCreatorUN' => self::ADMIN_USER,
];
yield 'The sales user with view others permission can view asset created by admin' => [
'route' => 'view',
'permission' => ['viewown', 'viewother'],
'expectedStatusCode' => Response::HTTP_OK,
'userCreatorUN' => self::ADMIN_USER,
];
yield 'The sales user with view own permission can view its own asset' => [
'route' => 'view',
'permission' => ['viewown'],
'expectedStatusCode' => Response::HTTP_OK,
'userCreatorUN' => self::SALES_USER,
];
}
public function testAssetUploadPathTraversal(): void
{
$client = $this->client;
$container = $this->getContainer();
// Get CSRF token
$csrfToken = $container->get('security.csrf.token_manager')->getToken('mautic_ajax_post')->getValue();
// Create a temporary file
$tempFile = tempnam(sys_get_temp_dir(), 'test_');
file_put_contents($tempFile, '111');
// Prepare the file for upload
$uploadedFile = new UploadedFile(
$tempFile,
'test.txt',
'text/plain',
null,
true
);
$tmpDir = 'tmp_'.substr(md5(uniqid()), 0, 13);
$client->request(
'POST',
'/s/_uploader/asset/upload',
['tempId' => '../../'.$tmpDir],
['file' => $uploadedFile],
[
'HTTP_X-Requested-With' => 'XMLHttpRequest',
'HTTP_X-CSRF-Token' => $csrfToken,
]
);
$response = $client->getResponse();
// Assert response is successful
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
// Decode JSON response
$responseData = json_decode($response->getContent(), true);
// Assert the response contains expected keys
$this->assertArrayHasKey('tmpFileName', $responseData);
// Assert file was created in the correct directory
$expectedDir = $container->getParameter('mautic.upload_dir').join('/', ['', 'tmp', $tmpDir]);
$expectedFilePath = join('/', [$expectedDir, $responseData['tmpFileName']]);
$this->assertFileExists($expectedFilePath);
// Clean up
if (file_exists($expectedFilePath)) {
unlink($expectedFilePath);
}
if (is_dir($expectedDir)) {
rmdir($expectedDir);
}
if (file_exists($tempFile)) {
unlink($tempFile);
}
}
private function getUser(string $username): User
{
$repository = $this->em->getRepository(User::class);
return $repository->findOneBy(['username' => $username]);
}
/**
* @param array<string, array<string, array<string>>> $permissions
*/
private function setPermission(User $user, array $permissions): void
{
$role = $user->getRole();
// Delete previous permissions
$this->em->createQueryBuilder()
->delete(Permission::class, 'p')
->where('p.bundle = :bundle')
->andWhere('p.role = :role_id')
->setParameters(['bundle' => 'asset', 'role_id' => $role->getId()])
->getQuery()
->execute();
// Set new permissions
$role->setIsAdmin(false);
$roleModel = static::getContainer()->get('mautic.user.model.role');
\assert($roleModel instanceof RoleModel);
$roleModel->setRolePermissions($role, $permissions);
$this->em->persist($role);
$this->em->flush();
}
public function testPostRequestWithWrongTempNameAndOriginalFileNameFileExtension(): void
{
$response = $this->client->request(
Request::METHOD_GET,
'/s/assets/new',
);
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
$form = $response->filter('form[name="asset"]')->form();
$data = $form->getPhpValues();
$data['asset']['tempName'] = 'image2.php';
$data['asset']['originalFileName'] = 'originalImage2.php';
$data['asset']['storageLocation'] = 'local';
$data['asset']['title'] = 'title';
$data['asset']['description'] = 'description';
$this->client->submit($form, $data);
preg_match_all('/Upload failed as the file extension, php/', $this->client->getResponse()->getContent(), $matches);
$this->assertCount(2, $matches[0]);
$this->assertStringContainsString('Upload failed as the file extension, php', $this->client->getResponse()->getContent());
}
public function testPostRequestWithWrongTempNameFileExtension(): void
{
$response = $this->client->request(
Request::METHOD_GET,
'/s/assets/new',
);
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
$form = $response->filter('form[name="asset"]')->form();
$data = $form->getPhpValues();
$data['asset']['tempName'] = 'image2.php';
$data['asset']['originalFileName'] = 'originalImage2.png';
$data['asset']['storageLocation'] = 'local';
$data['asset']['title'] = 'title';
$data['asset']['description'] = 'description';
$this->client->submit($form, $data);
preg_match_all('/Upload failed as the file extension, php/', $this->client->getResponse()->getContent(), $matches);
$this->assertCount(1, $matches[0]);
$this->assertStringContainsString('Upload failed as the file extension, php', $this->client->getResponse()->getContent());
}
public function testPostResquetSuccessWithCorrectFileExtension(): void
{
$response = $this->client->request(
Request::METHOD_GET,
'/s/assets/new',
);
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
$form = $response->filter('form[name="asset"]')->form();
$data = $form->getPhpValues();
$data['asset']['tempName'] = 'image.png';
$data['asset']['originalFileName'] = 'originalImage.png';
$data['asset']['storageLocation'] = 'local';
$data['asset']['title'] = 'title';
$data['asset']['description'] = 'description';
$this->client->submit($form, $data);
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
$this->assertStringNotContainsString('Upload failed as the file extension, php', $this->client->getResponse()->getContent());
}
public function testAssetWithProject(): void
{
$asset = new Asset();
$asset->setTitle('test');
$asset->setAlias('test');
$this->em->persist($asset);
$project = new Project();
$project->setName('Test Project');
$this->em->persist($project);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request('GET', '/s/assets/edit/'.$asset->getId());
$form = $crawler->selectButton('Save')->form();
$form['asset[projects]']->setValue((string) $project->getId());
$this->client->submit($form);
$this->assertResponseIsSuccessful();
$savedAsset = $this->em->find(Asset::class, $asset->getId());
Assert::assertSame($project->getId(), $savedAsset->getProjects()->first()->getId());
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Mautic\AssetBundle\Tests\Controller;
use Mautic\AssetBundle\Entity\Asset;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use PHPUnit\Framework\Assert;
class AssetDetailFunctionalTest extends MauticMysqlTestCase
{
public function testLeadViewPreventsXSS(): void
{
$title = 'aaa" onerror=alert(1) a="';
$asset = new Asset();
$asset->setTitle($title);
$asset->setAlias('dummy-alias');
$asset->setStorageLocation('local');
$asset->setPath('broken-image.jpg');
$asset->setExtension('jpg');
$this->em->persist($asset);
$this->em->flush();
$this->em->detach($asset);
$crawler = $this->client->request('GET', sprintf('/s/assets/view/%d', $asset->getId()));
$imageTag = $crawler->filter('.img-thumbnail');
$onError = $imageTag->attr('onerror');
$altProp = $imageTag->attr('alt');
Assert::assertNull($onError);
Assert::assertSame($title, $altProp);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Mautic\AssetBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class AssetDownloadFunctionalTest extends MauticMysqlTestCase
{
public function testDownloadOfNotFoundAsset(): void
{
$this->client->request(Request::METHOD_GET, '/s/logout');
// The 500 error happened only on the second request.
// It happened only if the device was already tracked.
$this->client->request(Request::METHOD_GET, '/asset/unicorn'); // returns 404 correctly
$this->client->request(Request::METHOD_GET, '/asset/unicorn'); // returned 500 but it should return 404
Assert::assertSame(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode());
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Mautic\AssetBundle\Tests\Controller;
use Mautic\AssetBundle\Entity\Asset;
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
final class AssetProjectSearchFunctionalTest 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');
$assetAlpha = $this->createAsset('Asset Alpha');
$assetBeta = $this->createAsset('Asset Beta');
$this->createAsset('Asset Gamma');
$this->createAsset('Asset Delta');
$assetAlpha->addProject($projectOne);
$assetAlpha->addProject($projectTwo);
$assetBeta->addProject($projectTwo);
$assetBeta->addProject($projectThree);
$this->em->flush();
$this->em->clear();
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/assets', '/s/assets']);
}
/**
* @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' => ['Asset Alpha', 'Asset Beta'],
'unexpectedEntities' => ['Asset Gamma', 'Asset Delta'],
];
yield 'search by one project AND asset name' => [
'searchTerm' => 'project:"Project Two" AND Beta',
'expectedEntities' => ['Asset Beta'],
'unexpectedEntities' => ['Asset Alpha', 'Asset Gamma', 'Asset Delta'],
];
yield 'search by one project OR asset name' => [
'searchTerm' => 'project:"Project Two" OR Gamma',
'expectedEntities' => ['Asset Alpha', 'Asset Beta', 'Asset Gamma'],
'unexpectedEntities' => ['Asset Delta'],
];
yield 'search by NOT one project' => [
'searchTerm' => '!project:"Project Two"',
'expectedEntities' => ['Asset Gamma', 'Asset Delta'],
'unexpectedEntities' => ['Asset Alpha', 'Asset Beta'],
];
yield 'search by two projects with AND' => [
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
'expectedEntities' => ['Asset Beta'],
'unexpectedEntities' => ['Asset Alpha', 'Asset Gamma', 'Asset Delta'],
];
yield 'search by two projects with NOT AND' => [
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
'expectedEntities' => ['Asset Gamma', 'Asset Delta'],
'unexpectedEntities' => ['Asset Alpha', 'Asset Beta'],
];
yield 'search by two projects with OR' => [
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
'expectedEntities' => ['Asset Alpha', 'Asset Beta'],
'unexpectedEntities' => ['Asset Gamma', 'Asset Delta'],
];
yield 'search by two projects with NOT OR' => [
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
'expectedEntities' => ['Asset Alpha', 'Asset Gamma', 'Asset Delta'],
'unexpectedEntities' => ['Asset Beta'],
];
}
private function createAsset(string $name): Asset
{
$asset = new Asset();
$asset->setTitle($name);
$asset->setAlias($name);
$this->em->persist($asset);
return $asset;
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Mautic\AssetBundle\Tests\Controller;
use Mautic\AssetBundle\Entity\Download;
use Mautic\AssetBundle\Tests\Asset\AbstractAssetTestCase;
class PublicControllerFunctionalTest extends AbstractAssetTestCase
{
/**
* Download action should return the file content.
*/
public function testDownloadActionStreamByDefault(): void
{
$assetSlug = $this->asset->getId().':'.$this->asset->getAlias();
$this->client->request('GET', '/asset/'.$assetSlug);
ob_start();
$response = $this->client->getResponse();
$response->sendContent();
$content = ob_get_contents();
ob_end_clean();
$this->assertResponseIsSuccessful();
$this->assertSame($this->expectedMimeType, $response->headers->get('Content-Type'));
$this->assertNotSame($this->expectedContentDisposition.$this->asset->getOriginalFileName(), $response->headers->get('Content-Disposition'));
$this->assertEquals($this->expectedPngContent, $content);
}
/**
* Download action should return the file content.
*/
public function testDownloadActionStreamIsZero(): void
{
$assetSlug = $this->asset->getId().':'.$this->asset->getAlias();
$this->client->request('GET', '/asset/'.$assetSlug.'?stream=0');
ob_start();
$response = $this->client->getResponse();
$response->sendContent();
$content = ob_get_contents();
ob_end_clean();
$this->assertResponseIsSuccessful();
$this->assertSame($this->expectedContentDisposition.$this->asset->getOriginalFileName(), $response->headers->get('Content-Disposition'));
$this->assertEquals($this->expectedPngContent, $content);
}
/**
* Download action with UTM should return the file content.
*/
public function testDownloadActionWithUTM(): void
{
$this->logoutUser();
$assetSlug = $this->asset->getId().':'.$this->asset->getAlias().'?utm_source=test2&utm_medium=test3&utm_campaign=test6&utm_term=test4&utm_content=test5';
$this->client->request('GET', '/asset/'.$assetSlug);
ob_start();
$response = $this->client->getResponse();
$response->sendContent();
$content = ob_get_contents();
ob_end_clean();
$this->assertResponseIsSuccessful();
$this->assertSame($this->expectedMimeType, $response->headers->get('Content-Type'));
$this->assertNotSame($this->expectedContentDisposition.$this->asset->getOriginalFileName(), $response->headers->get('Content-Disposition'));
$this->assertEquals($this->expectedPngContent, $content);
$downloadRepo = $this->em->getRepository(Download::class);
$download = $downloadRepo->findOneBy(['asset' => $this->asset]);
\assert($download instanceof Download);
$this->assertSame('test2', $download->getUtmSource());
$this->assertSame('test3', $download->getUtmMedium());
$this->assertSame('test4', $download->getUtmTerm());
$this->assertSame('test5', $download->getUtmContent());
$this->assertSame('test6', $download->getUtmCampaign());
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Mautic\AssetBundle\Tests\Controller;
use Mautic\AssetBundle\Tests\Asset\AbstractAssetTestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class UploadControllerFunctionalTest extends AbstractAssetTestCase
{
public function testUploadWithWrongMimetype(): void
{
// Create a php file with the content of phpinfo
$assetsPath = $this->client->getKernel()->getContainer()->getParameter('mautic.upload_dir');
$fileName = 'image2.png';
$filePath = $assetsPath.'/'.$fileName;
if (file_exists($filePath)) {
unlink($filePath);
}
copy('index.php', $filePath);
$binaryFile = new UploadedFile($filePath, $fileName, 'application/x-httpd-php', null, true);
$tmpId = 'tempId_'.time();
// Upload the file
$this->client->request(
Request::METHOD_POST,
'/s/_uploader/asset/upload',
[
'tempId' => $tmpId,
],
[
'file' => $binaryFile,
]
);
$response = $this->client->getResponse();
$this->assertStringContainsString('Upload failed as the file mimetype', $response->getContent());
$this->assertStringContainsString('text\/x-php is not allowed', $response->getContent());
unlink($filePath);
}
public function testSuccessUploadWithPng(): void
{
// Create a temporary PNG file
// Create a php file with the content of phpinfo
$assetsPath = $this->client->getKernel()->getContainer()->getParameter('mautic.upload_dir');
$assetsPathFrom = $this->client->getKernel()->getContainer()->getParameter('mautic.application_dir').'/app/assets/images/mautic_logo_db64.png';
$fileName = 'image3.png';
$filePath = $assetsPath.'/'.$fileName;
copy($assetsPathFrom, $filePath);
// Create an UploadedFile instance with the correct MIME type
$uploadedFile = new UploadedFile($filePath, $fileName, 'image/png', null, true);
$tmpId = 'tempId_'.time();
// Perform the request with the file
$this->client->request(
'POST',
'/s/_uploader/asset/upload',
['tempId' => $tmpId],
['file' => $uploadedFile]
);
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
$this->assertStringContainsString('state":1', $this->client->getResponse()->getContent());
if (file_exists($filePath)) {
unlink($filePath);
}
$data = json_decode($this->client->getResponse()->getContent(), true);
unlink($assetsPath.'/tmp/'.$tmpId.'/'.$data['tmpFileName']);
rmdir($assetsPath.'/tmp/'.$tmpId);
}
public function testUploadWithWrongExtension(): void
{
// Create a php file with the content of phpinfo
$assetsPath = $this->client->getKernel()->getContainer()->getParameter('mautic.upload_dir');
$assetsPathFrom = $this->client->getKernel()->getContainer()->getParameter('mautic.application_dir').'/app/assets/images/mautic_logo_db64.png';
$fileName = 'image2.php';
$filePath = $assetsPath.'/'.$fileName;
if (file_exists($filePath)) {
unlink($filePath);
}
copy($assetsPathFrom, $filePath);
$binaryFile = new UploadedFile($filePath, $fileName, 'image/png', null, true);
$tmpId = 'tempId_'.time();
// Upload the file
$this->client->request(
Request::METHOD_POST,
'/s/_uploader/asset/upload',
[
'tempId' => $tmpId,
],
[
'file' => $binaryFile,
]
);
$response = $this->client->getResponse();
$this->assertStringContainsString('Upload failed as the file extension', $response->getContent());
$this->assertStringContainsString('Upload failed as the file extension, php,', $response->getContent());
unlink($filePath);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Mautic\AssetBundle\Tests\DataFixtures;
use Mautic\AssetBundle\DataFixtures\ORM\LoadAssetData;
use Mautic\AssetBundle\Entity\Asset;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
class LoadAssetDataTest extends MauticMysqlTestCase
{
public function testLoadFixtures(): void
{
$this->loadFixtures([LoadAssetData::class]);
$asset = $this->em->getRepository(Asset::class)->findOneBy(
['title' => '@TOCHANGE: Asset1 Title'],
['id' => 'DESC']
);
self::assertInstanceOf(Asset::class, $asset);
self::assertEquals('asset1', $asset->getAlias());
self::assertEquals('@TOCHANGE: Asset1 Original File Name', $asset->getOriginalFileName());
self::assertEquals('fdb8e28357b02d12d068de3e5661832e21bc08ec.doc', $asset->getPath());
self::assertEquals(1, $asset->getDownloadCount());
self::assertEquals(1, $asset->getUniqueDownloadCount());
self::assertEquals(1, $asset->getRevision());
self::assertEquals('en', $asset->getLanguage());
}
public function testLoadFixturesOrder(): void
{
$loadAssetData = new LoadAssetData();
self::assertEquals(10, $loadAssetData->getOrder());
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Mautic\AssetBundle\Tests\Entity;
use Doctrine\DBAL\Query\QueryBuilder;
use Mautic\AssetBundle\Entity\Asset;
use Mautic\AssetBundle\Entity\AssetRepository;
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Translation\TranslatorInterface;
class AssetRepositoryTest extends TestCase
{
use RepositoryConfiguratorTrait;
private function getRepository(): AssetRepository
{
$repository = $this->configureRepository(Asset::class);
$this->connection->method('createQueryBuilder')->willReturnCallback(fn () => new QueryBuilder($this->connection));
$translator = $this->createMock(TranslatorInterface::class);
$translator->method('trans')->willReturnCallback(fn ($id) => match ($id) {
'mautic.asset.asset.searchcommand.isexpired' => 'is:expired',
'mautic.asset.asset.searchcommand.ispending' => 'is:pending',
default => $id,
});
$repository->setTranslator($translator);
return $repository;
}
#[\PHPUnit\Framework\Attributes\DataProvider('dataExpirationFilters')]
public function testAddSearchCommandWhereClauseHandlesExpirationFilters(string $command, string $expected): void
{
$repository = $this->getRepository();
$qb = $this->connection->createQueryBuilder();
$filter = (object) ['command' => $command, 'string' => '', 'not' => false, 'strict' => false];
$method = new \ReflectionMethod(AssetRepository::class, 'addSearchCommandWhereClause');
$method->setAccessible(true);
[$expr, $params] = $method->invoke($repository, $qb, $filter);
self::assertSame($expected, (string) $expr);
self::assertSame(['par1' => true], $params);
}
/**
* @return iterable<array{0: string, 1: string}>
*/
public static function dataExpirationFilters(): iterable
{
yield ['is:expired', "(a.isPublished = :par1 AND a.publishDown IS NOT NULL AND a.publishDown <> '' AND a.publishDown < CURRENT_TIMESTAMP())"];
yield ['is:pending', "(a.isPublished = :par1 AND a.publishUp IS NOT NULL AND a.publishUp <> '' AND a.publishUp > CURRENT_TIMESTAMP())"];
}
public function testGetSearchCommandsContainsExpirationFilters(): void
{
$repository = $this->getRepository();
$commands = $repository->getSearchCommands();
self::assertContains('mautic.asset.asset.searchcommand.isexpired', $commands);
self::assertContains('mautic.asset.asset.searchcommand.ispending', $commands);
}
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Mautic\AssetBundle\Tests\EventListener;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\AssetBundle\Entity\DownloadRepository;
use Mautic\AssetBundle\EventListener\DetermineWinnerSubscriber;
use Mautic\CoreBundle\Event\DetermineWinnerEvent;
use Mautic\PageBundle\Entity\Page;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Contracts\Translation\TranslatorInterface;
class DetermineWinnerSubscriberTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject|EntityManagerInterface
*/
private MockObject $em;
/**
* @var MockObject|TranslatorInterface
*/
private MockObject $translator;
private DetermineWinnerSubscriber $subscriber;
protected function setUp(): void
{
parent::setUp();
$this->em = $this->createMock(EntityManagerInterface::class);
$this->translator = $this->createMock(TranslatorInterface::class);
$this->subscriber = new DetermineWinnerSubscriber($this->em, $this->translator);
}
public function testOnDetermineDownloadRateWinner(): void
{
$parentMock = $this->createMock(Page::class);
$childMock = $this->createMock(Page::class);
$children = [2 => $childMock];
$repoMock = $this->createMock(DownloadRepository::class);
$parameters = ['parent' => $parentMock, 'children' => $children];
$event = new DetermineWinnerEvent($parameters);
$startDate = new \DateTime();
$transDownloads = 'downloads';
$transHits = 'hits';
$counts = [
1 => [
'count' => 20,
'id' => 1,
'name' => 'Test 5',
'total' => 100,
],
2 => [
'count' => 25,
'id' => 2,
'name' => 'Test 6',
'total' => 150,
],
];
$this->translator->method('trans')
->willReturnOnConsecutiveCalls($transDownloads, $transHits);
$this->em->expects($this->once())
->method('getRepository')
->willReturn($repoMock);
$parentMock->expects($this->any())
->method('isPublished')
->willReturn(true);
$childMock->expects($this->any())
->method('isPublished')
->willReturn(true);
$parentMock->expects($this->any())
->method('getId')
->willReturn(1);
$childMock->expects($this->any())
->method('getId')
->willReturn(2);
$parentMock->expects($this->once())
->method('getVariantStartDate')
->willReturn($startDate);
$repoMock->expects($this->once())
->method('getDownloadCountsByPage')
->with([1, 2], $startDate)
->willReturn($counts);
$this->subscriber->onDetermineDownloadRateWinner($event);
$expectedData = [
$transDownloads => [$counts[1]['count'], $counts[2]['count']],
$transHits => [$counts[1]['total'], $counts[2]['total']],
];
$abTestResults = $event->getAbTestResults();
$this->assertEquals($abTestResults['winners'], [1]);
$this->assertEquals($abTestResults['support']['data'], $expectedData);
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Mautic\AssetBundle\Tests\EventListener;
use Mautic\AssetBundle\Entity\Asset;
use Mautic\AssetBundle\Entity\Download;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\ReportBundle\Tests\Functional\AbstractReportSubscriberTestCase;
class ReportSubscriberFunctionalTest extends AbstractReportSubscriberTestCase
{
public function testAssetDownloadReportWithDncListColumn(): void
{
$leads[] = $this->createContact('test1@example.com');
$leads[] = $this->createContact('test2@example.com');
$leads[] = $this->createContact('test3@example.com');
$this->em->flush();
$this->createDnc('email', $leads[0], DoNotContact::BOUNCED);
$this->createDnc('email', $leads[1], DoNotContact::MANUAL);
$this->createDnc('email', $leads[2], DoNotContact::UNSUBSCRIBED);
$this->createDnc('sms', $leads[2], DoNotContact::MANUAL);
$this->em->flush();
$asset = $this->createAsset();
$this->emulateAssetDownload($asset, $leads[0]);
$this->emulateAssetDownload($asset, $leads[1]);
$this->emulateAssetDownload($asset, $leads[2]);
$report = $this->createReport(
source: 'asset.downloads',
columns: ['l.id', 'a.id', 'a.title', 'dnc_preferences'],
filters: [
[
'column' => 'dnc_preferences',
'glue' => 'and',
'dynamic' => null,
'condition' => 'in',
'value' => [
'email:'.DoNotContact::UNSUBSCRIBED,
'email:'.DoNotContact::BOUNCED,
],
],
],
order: [['column' => 'l.id', 'direction' => 'ASC']]
);
$expectedReport = [
[(string) $leads[0]->getId(), (string) $asset->getId(), $asset->getTitle(), 'DNC Bounced: Email'],
[(string) $leads[2]->getId(), (string) $asset->getId(), $asset->getTitle(), 'DNC Manually Unsubscribed: Text Message, DNC Unsubscribed: Email'],
];
$this->verifyReport($report->getId(), $expectedReport);
$this->verifyApiReport($report->getId(), $expectedReport);
}
private function createAsset(): Asset
{
$asset = new Asset();
$asset->setTitle('test');
$asset->setAlias('test');
$asset->setDateAdded(new \DateTime('2020-02-07 20:29:02'));
$asset->setDateModified(new \DateTime('2020-03-21 20:29:02'));
$asset->setCreatedByUser('Test User');
$this->em->persist($asset);
$this->em->flush();
return $asset;
}
private function emulateAssetDownload(Asset $asset, Lead $contact): Download
{
$assetDownload = new Download();
$assetDownload->setAsset($asset);
$assetDownload->setLead($contact);
$assetDownload->setDateDownload(new \DateTime());
$assetDownload->setCode(200);
$assetDownload->setTrackingId(random_int(1, 99999));
$this->em->persist($assetDownload);
$this->em->flush();
return $assetDownload;
}
private function createContact(string $email): Lead
{
$contact = new Lead();
$contact->setEmail($email);
$this->em->persist($contact);
return $contact;
}
public function createDnc(string $channel, Lead $contact, int $reason): DoNotContact
{
$dnc = new DoNotContact();
$dnc->setChannel($channel);
$dnc->setLead($contact);
$dnc->setReason($reason);
$dnc->setDateAdded(new \DateTime());
$this->em->persist($dnc);
return $dnc;
}
}

View File

@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace Mautic\AssetBundle\Tests\EventListener;
use Mautic\AssetBundle\Entity\DownloadRepository;
use Mautic\AssetBundle\EventListener\ReportSubscriber;
use Mautic\ChannelBundle\Helper\ChannelListHelper;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Model\CompanyReportData;
use Mautic\LeadBundle\Report\DncReportService;
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
use Mautic\ReportBundle\Entity\Report;
use Mautic\ReportBundle\Event\ReportBuilderEvent;
use Mautic\ReportBundle\Event\ReportGeneratorEvent;
use Mautic\ReportBundle\Helper\ReportHelper;
use PHPUnit\Framework\Assert;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ReportSubscriberTest extends \PHPUnit\Framework\TestCase
{
private ChannelListHelper $channelListHelper;
/**
* @var CompanyReportData|\PHPUnit\Framework\MockObject\MockObject
*/
private \PHPUnit\Framework\MockObject\MockObject $companyReportData;
/**
* @var DownloadRepository|\PHPUnit\Framework\MockObject\MockObject
*/
private \PHPUnit\Framework\MockObject\MockObject $downloadRepository;
/**
* @var QueryBuilder|\PHPUnit\Framework\MockObject\MockObject
*/
private \PHPUnit\Framework\MockObject\MockObject $queryBuilder;
/**
* @var DncReportService|\PHPUnit\Framework\MockObject\MockObject
*/
private \PHPUnit\Framework\MockObject\MockObject $dncReportService;
private ReportHelper $reportHelper;
public function setUp(): void
{
$this->queryBuilder = $this->createMock(QueryBuilder::class);
$this->channelListHelper = new ChannelListHelper($this->createMock(EventDispatcherInterface::class), $this->createMock(Translator::class));
$this->reportHelper = new ReportHelper($this->createMock(EventDispatcherInterface::class));
$this->companyReportData = $this->createMock(CompanyReportData::class);
$this->downloadRepository = $this->createMock(DownloadRepository::class);
$this->dncReportService = $this->createMock(DncReportService::class);
}
public function testOnReportBuilderWithUnknownContext(): void
{
$companyReportData = new class extends CompanyReportData {
public function __construct()
{
}
};
$downloadRepository = new class extends DownloadRepository {
public function __construct()
{
}
};
$event = new class extends ReportBuilderEvent {
public function __construct()
{
$this->context = 'unicorn';
}
};
$reportSubscriber = new ReportSubscriber($companyReportData, $downloadRepository, $this->dncReportService);
$reportSubscriber->onReportBuilder($event);
Assert::assertSame([], $event->getTables());
}
public function testOnReportBuilderWithAssetDownloadContext(): void
{
$companyReportData = new class extends CompanyReportData {
public function __construct()
{
}
/**
* @return array<mixed>
*/
public function getCompanyData(): array
{
return [];
}
};
$downloadRepository = new class extends DownloadRepository {
public function __construct()
{
}
};
$event = new ReportBuilderEvent($this->createTranslatorMock(), $this->channelListHelper, ReportSubscriber::CONTEXT_ASSET_DOWNLOAD, [], $this->reportHelper);
$reportSubscriber = new ReportSubscriber($companyReportData, $downloadRepository, $this->dncReportService);
$reportSubscriber->onReportBuilder($event);
Assert::assertSame(
[
'alias' => 'download_count',
'label' => '[trans]mautic.asset.report.download_count[/trans]',
'type' => 'int',
],
$event->getTables()['assets']['columns']['a.download_count']
);
Assert::assertSame(
[
'alias' => 'unique_download_count',
'label' => '[trans]mautic.asset.report.unique_download_count[/trans]',
'type' => 'int',
],
$event->getTables()['assets']['columns']['a.unique_download_count']
);
Assert::assertSame(
[
'alias' => 'download_count',
'label' => '[trans]mautic.asset.report.download_count[/trans]',
'type' => 'int',
'formula' => 'COUNT(ad.id)',
],
$event->getTables()['asset.downloads']['columns']['a.download_count']
);
Assert::assertSame(
[
'alias' => 'unique_download_count',
'label' => '[trans]mautic.asset.report.unique_download_count[/trans]',
'type' => 'int',
'formula' => 'COUNT(DISTINCT ad.lead_id)',
],
$event->getTables()['asset.downloads']['columns']['a.unique_download_count']
);
}
private function createTranslatorMock(): TranslatorInterface
{
return new class implements TranslatorInterface {
/**
* @param array<int|string> $parameters
*/
public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
{
return '[trans]'.$id.'[/trans]';
}
public function getLocale(): string
{
return 'en';
}
};
}
public function testGroupByDefaultConfigured(): void
{
$report = new Report();
$report->setSource(ReportSubscriber::CONTEXT_ASSET_DOWNLOAD);
$event = new ReportGeneratorEvent($report, [], $this->queryBuilder, $this->channelListHelper);
$subscriber = new ReportSubscriber($this->companyReportData, $this->downloadRepository, $this->dncReportService);
$this->queryBuilder->method('from')->willReturn($this->queryBuilder);
$this->queryBuilder->expects($this->once())
->method('groupBy')
->with('ad.id');
$this->assertFalse($event->hasGroupBy());
$subscriber->onReportGenerate($event);
}
public function testGroupByNotDefaultConfigured(): void
{
$report = new Report();
$report->setSource(ReportSubscriber::CONTEXT_ASSET_DOWNLOAD);
$this->queryBuilder->method('from')->willReturn($this->queryBuilder);
$report->setGroupBy(['a.id' => 'desc']);
$event = new ReportGeneratorEvent($report, [], $this->queryBuilder, $this->channelListHelper);
$subscriber = new ReportSubscriber($this->companyReportData, $this->downloadRepository, $this->dncReportService);
$subscriber->onReportGenerate($event);
$this->assertTrue($event->hasGroupBy());
}
}

View File

@@ -0,0 +1,287 @@
<?php
declare(strict_types=1);
namespace Mautic\AssetBundle\Tests\Model;
use Doctrine\ORM\EntityManager;
use Mautic\AssetBundle\AssetEvents;
use Mautic\AssetBundle\Entity\Asset;
use Mautic\AssetBundle\Entity\AssetRepository;
use Mautic\AssetBundle\Entity\Download;
use Mautic\AssetBundle\Model\AssetModel;
use Mautic\CacheBundle\Cache\CacheProvider;
use Mautic\CategoryBundle\Model\CategoryModel;
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\LeadBundle\Tracker\Factory\DeviceDetectorFactory\DeviceDetectorFactory;
use Mautic\LeadBundle\Tracker\Service\DeviceCreatorService\DeviceCreatorService;
use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\ServerBag;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class AssetModelTest extends \PHPUnit\Framework\TestCase
{
private AssetModel $assetModel;
private CoreParametersHelper&MockObject $coreParametersHelper;
private ContainerInterface&MockObject $container;
private CacheProvider $cacheProvider;
private LeadModel&MockObject $leadModel;
private CategoryModel&MockObject $categoryModel;
private RequestStack&MockObject $requestStack;
private IpLookupHelper&MockObject $ipLookupHelper;
private DeviceDetectorFactory $deviceDetectorFactory;
private DeviceCreatorService $deviceCreatorService;
private DeviceTrackingServiceInterface&MockObject $deviceTrackingService;
private ContactTracker&MockObject $contactTracker;
private EntityManager&MockObject $entityManager;
private CorePermissions&MockObject $corePermissions;
private EventDispatcherInterface&MockObject $eventDispatcher;
private MockObject&UrlGeneratorInterface $urlGenerator;
private Translator&MockObject $translator;
private UserHelper&MockObject $userHelper;
private LoggerInterface&MockObject $logger;
protected function setUp(): void
{
parent::setUp();
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$this->coreParametersHelper->expects($this->once())
->method('get')
->with($this->equalTo('max_size'))
->willReturn('2MB');
$this->container = $this->createMock(ContainerInterface::class);
$this->cacheProvider = new CacheProvider($this->coreParametersHelper, $this->container);
$this->leadModel = $this->createMock(LeadModel::class);
$this->categoryModel = $this->createMock(CategoryModel::class);
$this->requestStack = $this->createMock(RequestStack::class);
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
$this->deviceDetectorFactory = new DeviceDetectorFactory($this->cacheProvider);
$this->deviceCreatorService = new DeviceCreatorService();
$this->deviceTrackingService = $this->createMock(DeviceTrackingServiceInterface::class);
$this->contactTracker = $this->createMock(ContactTracker::class);
$this->entityManager = $this->createMock(EntityManager::class);
$this->corePermissions = $this->createMock(CorePermissions::class);
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$this->translator = $this->createMock(Translator::class);
$this->userHelper = $this->createMock(UserHelper::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->assetModel = new AssetModel(
$this->leadModel,
$this->categoryModel,
$this->requestStack,
$this->ipLookupHelper,
$this->deviceCreatorService,
$this->deviceDetectorFactory,
$this->deviceTrackingService,
$this->contactTracker,
$this->entityManager,
$this->corePermissions,
$this->eventDispatcher,
$this->urlGenerator,
$this->translator,
$this->userHelper,
$this->logger,
$this->coreParametersHelper,
);
}
/**
* Test that TrackDownload works only with a request.
*/
public function testTrackDownloadRequest(): void
{
$asset = new Asset();
$this->corePermissions->expects($this->once())
->method('isAnonymous')
->willReturn(true);
$this->requestStack->expects($this->once())
->method('getCurrentRequest')
->willReturn(null);
$this->entityManager->expects($this->never())
->method('persist');
$this->entityManager->expects($this->never())
->method('flush');
$this->entityManager->expects($this->never())
->method('detach');
$this->assetModel->trackDownload($asset);
}
/**
* Test that TrackDownload works successfully.
*/
public function testTrackDownload(): void
{
$asset = new Asset();
$lead = new Lead();
$this->corePermissions->expects($this->once())
->method('isAnonymous')
->willReturn(true);
$request = $this->createMock(Request::class);
$serverBag = $this->createMock(ServerBag::class);
$serverBag->expects($this->once())
->method('get')
->with($this->equalTo('HTTP_REFERER'))
->willReturn('http://localhost');
$request->server = $serverBag;
$matcher = $this->exactly(6);
$request->expects($matcher)
->method('get')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertEquals('utm_campaign', $parameters[0]);
return 'test_utm_campaign';
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertEquals('utm_content', $parameters[0]);
return 'test_utm_content';
}
if (3 === $matcher->numberOfInvocations()) {
$this->assertEquals('utm_medium', $parameters[0]);
return 'test_utm_medium';
}
if (4 === $matcher->numberOfInvocations()) {
$this->assertEquals('utm_source', $parameters[0]);
return 'test_utm_source';
}
if (5 === $matcher->numberOfInvocations()) {
$this->assertEquals('utm_term', $parameters[0]);
return 'test_utm_term';
}
if (6 === $matcher->numberOfInvocations()) {
$this->assertEquals('ct', $parameters[0]);
return false;
}
});
$this->requestStack->expects($this->once())
->method('getCurrentRequest')
->willReturn($request);
$this->deviceTrackingService->expects($this->once())
->method('isTracked')
->willReturn(false);
$this->contactTracker->expects($this->once())
->method('getContact')
->willReturn($lead);
$this->deviceTrackingService->expects($this->once())
->method('getTrackedDevice')
->willReturn(null);
$assetRepository = $this->createMock(AssetRepository::class);
$this->entityManager->expects($this->once())
->method('getRepository')
->with($this->equalTo(Asset::class))
->willReturn($assetRepository);
$assetRepository->expects($this->once())
->method('upDownloadCount')
->with(
$this->equalTo($asset->getId()),
$this->equalTo(1),
$this->equalTo(true),
);
$ipAddress = new IpAddress('127.0.0.1');
$this->ipLookupHelper->expects($this->once())
->method('getIpAddress')
->willReturn($ipAddress);
$this->eventDispatcher->expects($this->once())
->method('hasListeners')
->with($this->equalTo(AssetEvents::ASSET_ON_LOAD))
->willReturn(false);
/** @var ?Download $download */
$download = null;
$this->entityManager->expects($this->once())
->method('persist')
->with($this->callback(function ($downloadPersist) use (&$download) {
$download = $downloadPersist;
return $download instanceof Download;
}));
$this->entityManager->expects($this->once())
->method('flush');
$this->entityManager->expects($this->once())
->method('detach')
->with($this->callback(function ($downloadDetach) use (&$download) {
$this->assertSame($downloadDetach, $download);
return true;
}));
$this->assetModel->trackDownload($asset);
$this->assertEquals('test_utm_campaign', $download->getUtmCampaign());
$this->assertEquals('test_utm_content', $download->getUtmContent());
$this->assertEquals('test_utm_medium', $download->getUtmMedium());
$this->assertEquals('test_utm_source', $download->getUtmSource());
$this->assertEquals('test_utm_term', $download->getUtmTerm());
$this->assertEquals('200', $download->getCode());
$this->assertEquals($ipAddress, $download->getIpAddress());
$this->assertEquals($lead, $download->getLead());
$this->assertEquals($asset, $download->getAsset());
$this->assertEquals('http://localhost', $download->getReferer());
}
}