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,142 @@
<?php
declare(strict_types=1);
namespace MauticPlugin\GrapesJsBuilderBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\EmailBundle\Entity\Email;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Entity\Plugin;
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilder;
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilderRepository;
class AssertCustomMjmlTest extends MauticMysqlTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createIntegration();
}
/**
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\Exception\ORMException
*/
public function testAssertCustomMjml(): void
{
// Create email & add GrapesJs to it.
$email = $this->createEmail();
$this->addToGrapesJsBuilder($email);
$emailId = $email->getId();
// Get the Email via API and assert customMjml.
$this->client->request('GET', '/api/emails/'.$emailId);
$this->assertResponseStatusCodeSame(200);
$content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertNotEmpty($content['email']['grapesjsbuilder']['customMjml']);
}
/**
* @throws \Doctrine\ORM\Exception\NotSupported
*/
public function testAssertCustomHtmlAndCustomMjml(): void
{
// Create email using an API call and add GrapesJS into it.
$responseData = $this->createEmailViaApi();
$emailId = $responseData['email']['id'];
$email = $this->em->getRepository(Email::class)->find($emailId);
$this->addToGrapesJsBuilder($email);
// Get email & check for both customHtml & customMjml in the response.
$this->client->request('GET', '/api/emails/'.$emailId);
$this->assertResponseStatusCodeSame(200);
$content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertNotEmpty($content['email']['customHtml']);
$this->assertNotEmpty($content['email']['grapesjsbuilder']['customMjml']);
}
/**
* @throws \Doctrine\ORM\Exception\NotSupported
*/
private function getRepository(): GrapesJsBuilderRepository
{
/** @var GrapesJsBuilderRepository $repository */
$repository = $this->em->getRepository(GrapesJsBuilder::class);
$repository->setTranslator($this->getTranslatorMock());
return $repository;
}
/**
* @throws \Doctrine\ORM\Exception\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
private function createEmail(): Email
{
$email = new Email();
$email->setName('Test email');
$email->setSubject('Test email subject');
$email->setEmailType('template');
$email->setCustomHtml('<html></html>');
$email->setIsPublished(true);
$this->em->persist($email);
$this->em->flush();
return $email;
}
private function getTranslatorMock(): Translator
{
$translator = $this->createMock(Translator::class);
$translator->method('hasId')
->willReturn(false);
return $translator;
}
private function addToGrapesJsBuilder(Email $email): void
{
$grapesJsBuilder = new GrapesJsBuilder();
$grapesJsBuilder->setEmail($email);
$grapesJsBuilder->setCustomMjml('<mjml>></mjml>');
$this->getRepository()->saveEntity($grapesJsBuilder);
}
private function createEmailViaApi(): mixed
{
$emailData = [
'name' => 'Test email',
'subject' => 'Test email subject',
'emailType' => 'template',
'customHtml' => '<html></html>',
'isPublished' => true,
];
$this->client->request('POST', '/api/emails/new', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($emailData));
$this->assertResponseStatusCodeSame(201);
return json_decode($this->client->getResponse()->getContent(), true);
}
/**
* @throws \Doctrine\ORM\Exception\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
private function createIntegration(): void
{
$plugin = new Plugin();
$plugin->setName('GrapesJS Builder');
$plugin->setBundle('GrapesJsBuilderBundle');
$this->em->persist($plugin);
$integration = new Integration();
$integration->setPlugin($plugin);
$integration->setIsPublished(true);
$integration->setName('grapesjsbuilder');
$this->em->persist($integration);
$this->em->flush();
}
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace MauticPlugin\GrapesJsBuilderBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Response;
final class FileManagerControllerFunctionalTest extends MauticMysqlTestCase
{
private const ASSETS_ENDPOINT = '/s/grapesjsbuilder/media';
private const UPLOAD_ENDPOINT = '/s/grapesjsbuilder/upload';
private const DELETE_ENDPOINT = '/s/grapesjsbuilder/delete';
private const IMAGE_COUNT = 3;
/** @var array<string> */
private array $tempFilePaths = [];
protected function beforeTearDown(): void
{
$this->cleanupTempFiles();
}
public function testAssetsManagerWorkflow(): void
{
$initialAssetCount = $this->getAssetCount();
$uploadedFiles = $this->uploadImages();
$this->assertUploadSuccessful($uploadedFiles);
$newAssetCount = $this->getAssetCount();
$this->assertEquals($initialAssetCount + self::IMAGE_COUNT, $newAssetCount);
$this->testPagination($newAssetCount);
$this->testRecentlyAddedFilesAppearFirst($uploadedFiles);
$this->deleteUploadedFiles($uploadedFiles);
$finalAssetCount = $this->getAssetCount();
$this->assertEquals($initialAssetCount, $finalAssetCount);
}
private function getAssetCount(): int
{
$response = $this->makeRequest('GET', self::ASSETS_ENDPOINT);
$content = $this->getJsonResponse($response);
$this->assertArrayHasKey('data', $content);
$this->assertArrayHasKey('page', $content);
$this->assertArrayHasKey('limit', $content);
$this->assertArrayHasKey('totalItems', $content);
$this->assertArrayHasKey('totalPages', $content);
$this->assertArrayHasKey('hasNextPage', $content);
$this->assertArrayHasKey('hasPreviousPage', $content);
return $content['totalItems'];
}
private function testPagination(int $totalAssets): void
{
$limit = 2;
$totalPages = ceil($totalAssets / $limit);
for ($page = 1; $page <= $totalPages; ++$page) {
$response = $this->makeRequest('GET', self::ASSETS_ENDPOINT."?limit={$limit}&page={$page}");
$content = $this->getJsonResponse($response);
$this->assertArrayHasKey('data', $content);
$this->assertArrayHasKey('page', $content);
$this->assertArrayHasKey('limit', $content);
$this->assertArrayHasKey('totalItems', $content);
$this->assertArrayHasKey('totalPages', $content);
$this->assertArrayHasKey('hasNextPage', $content);
$this->assertArrayHasKey('hasPreviousPage', $content);
$this->assertEquals($page, $content['page']);
$this->assertEquals($limit, $content['limit']);
$this->assertEquals($totalAssets, $content['totalItems']);
$this->assertEquals($totalPages, $content['totalPages']);
$this->assertEquals($page < $totalPages, $content['hasNextPage']);
$this->assertEquals($page > 1, $content['hasPreviousPage']);
$expectedItemCount = ($page < $totalPages) ? $limit : (($totalAssets % $limit) ?: $limit);
$this->assertCount($expectedItemCount, $content['data']);
foreach ($content['data'] as $item) {
$this->assertArrayHasKey('src', $item);
$this->assertArrayHasKey('width', $item);
$this->assertArrayHasKey('height', $item);
$this->assertArrayHasKey('type', $item);
}
}
// Test invalid page
$response = $this->makeRequest('GET', self::ASSETS_ENDPOINT."?limit={$limit}&page=".($totalPages + 1));
$content = $this->getJsonResponse($response);
$this->assertEmpty($content['data']);
}
/**
* @param array<string> $uploadedFiles
*/
private function testRecentlyAddedFilesAppearFirst(array $uploadedFiles): void
{
$response = $this->makeRequest('GET', self::ASSETS_ENDPOINT);
$content = $this->getJsonResponse($response);
$this->assertArrayHasKey('data', $content);
$this->assertNotEmpty($content['data']);
$assetList = $content['data'];
$uploadedFileNames = array_map([$this, 'getFileNameFromUrl'], $uploadedFiles);
// Check if the first 'IMAGE_COUNT' assets in the list are the recently uploaded files
for ($i = 0; $i < self::IMAGE_COUNT; ++$i) {
$this->assertArrayHasKey($i, $assetList);
$this->assertArrayHasKey('src', $assetList[$i]);
$assetFileName = $this->getFileNameFromUrl($assetList[$i]['src']);
$this->assertContains($assetFileName, $uploadedFileNames, 'Recently uploaded file not found in the first {self::IMAGE_COUNT} assets');
}
}
/**
* @return array<string>
*/
private function uploadImages(): array
{
$imageFiles = $this->createTempImageFiles();
$response = $this->makeRequest('POST', self::UPLOAD_ENDPOINT, [], ['files' => $imageFiles]);
return $this->getJsonResponse($response)['data'];
}
/**
* @return array<UploadedFile>
*/
private function createTempImageFiles(): array
{
$imageFiles = [];
for ($i = 1; $i <= self::IMAGE_COUNT; ++$i) {
$imagePath = sys_get_temp_dir()."/test-image-{$i}.png";
$this->createImage($imagePath);
$this->tempFilePaths[] = $imagePath;
$imageFiles[] = new UploadedFile($imagePath, "test-image-{$i}.png", 'image/png', null, true);
}
return $imageFiles;
}
private function createImage(string $path): void
{
$image = imagecreatetruecolor(100, 100);
imagepng($image, $path);
imagedestroy($image);
}
/**
* @param array<string> $uploadedFiles
*/
private function assertUploadSuccessful(array $uploadedFiles): void
{
$this->assertCount(self::IMAGE_COUNT, $uploadedFiles);
}
/**
* @param array<string> $uploadedFiles
*/
private function deleteUploadedFiles(array $uploadedFiles): void
{
foreach ($uploadedFiles as $uploadedFile) {
$fileName = $this->getFileNameFromUrl($uploadedFile);
$this->makeRequest('GET', self::DELETE_ENDPOINT."?filename={$fileName}");
}
}
private function getFileNameFromUrl(string $url): string
{
$fileUrlParts = explode('/', $url);
return end($fileUrlParts);
}
/**
* @param array<string, mixed> $parameters
* @param array<string, mixed> $files
*/
private function makeRequest(string $method, string $endpoint, array $parameters = [], array $files = []): Response
{
$this->client->request($method, $endpoint, $parameters, $files);
$response = $this->client->getResponse();
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
return $response;
}
/**
* @return array<string, mixed>
*/
private function getJsonResponse(Response $response): array
{
return json_decode($response->getContent(), true);
}
private function cleanupTempFiles(): void
{
foreach ($this->tempFilePaths as $path) {
if (file_exists($path)) {
unlink($path);
}
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace MauticPlugin\GrapesJsBuilderBundle\Tests\InstallFixtures\ORM;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Entity\Plugin;
use MauticPlugin\GrapesJsBuilderBundle\InstallFixtures\ORM\GrapesJsData;
use PHPUnit\Framework\Assert;
class GrapeJsDataTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testGetGroups(): void
{
Assert::assertSame(['group_install', 'group_mautic_install_data'], GrapesJsData::getGroups());
}
public function testLoad(): void
{
$findOneByCriteria = [
'name' => 'GrapesJS Builder',
'description' => 'GrapesJS Builder with MJML support for Mautic',
'version' => '1.0.0',
'author' => 'Mautic Community',
'bundle' => 'GrapesJsBuilderBundle',
];
$plugin = $this->em->getRepository(Plugin::class)->findOneBy($findOneByCriteria);
self::assertNull($plugin);
$this->loadFixtures([GrapesJsData::class]);
$plugin = $this->em->getRepository(Plugin::class)->findOneBy($findOneByCriteria);
self::assertInstanceOf(Plugin::class, $plugin);
$integration = $this->em->getRepository(Integration::class)->findOneBy(
[
'isPublished' => true,
'name' => 'GrapesJsBuilder',
'plugin' => $plugin,
]
);
self::assertInstanceOf(Integration::class, $integration);
}
}

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace MauticPlugin\GrapesJsBuilderBundle\Tests\Unit\EventSubscriber;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\EmailRepository;
use Mautic\EmailBundle\Event\EmailEditSubmitEvent;
use Mautic\EmailBundle\Helper\EmailConfigInterface;
use Mautic\EmailBundle\Model\EmailModel;
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilder;
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilderRepository;
use MauticPlugin\GrapesJsBuilderBundle\EventSubscriber\EmailSubscriber;
use MauticPlugin\GrapesJsBuilderBundle\Integration\Config;
use MauticPlugin\GrapesJsBuilderBundle\Model\GrapesJsBuilderModel;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
final class EmailSubscriberTest extends TestCase
{
/** @var MockObject&Config */
private MockObject $config;
/** @var MockObject&GrapesJsBuilderModel */
private MockObject $grapesJsBuilderModel;
/** @var MockObject&GrapesJsBuilderRepository */
private MockObject $grapesJsBuilderRepo;
private EmailModel|MockObject $emailModel;
private EmailConfigInterface|MockObject $emailConfig;
private EmailSubscriber $subscriber;
public function setUp(): void
{
$this->config = $this->createMock(Config::class);
$this->grapesJsBuilderModel = $this->createMock(GrapesJsBuilderModel::class);
$this->emailModel = $this->createMock(EmailModel::class);
$this->emailConfig = $this->createMock(EmailConfigInterface::class);
$this->grapesJsBuilderRepo = $this->createMock(GrapesJsBuilderRepository::class);
$this->subscriber = new EmailSubscriber($this->config, $this->grapesJsBuilderModel, $this->emailModel, $this->emailConfig);
$this->emailModel->method('getRepository')
->willReturn($this->createMock(EmailRepository::class));
$this->grapesJsBuilderModel->method('getRepository')
->willReturn($this->grapesJsBuilderRepo);
}
public function testManageEmailDraftExitsWhenPluginNotPublished(): void
{
$event = $this->createMock(EmailEditSubmitEvent::class);
$event->expects($this->never())
->method('getCurrentEmail');
$this->config->expects($this->once())
->method('isPublished')
->willReturn(false);
$this->subscriber->manageEmailDraft($event);
}
public function testManageEmailDraftHandlesSaveAsDraft(): void
{
$event = $this->createMock(EmailEditSubmitEvent::class);
$event->expects($this->once())
->method('getCurrentEmail')
->willReturn($this->createMock(Email::class));
$event->expects($this->once())
->method('isSaveAsDraft')
->willReturn(true);
$this->grapesJsBuilderRepo->method('findOneBy')
->willReturn($grapesJsBuilder = $this->createMock(GrapesJsBuilder::class));
$this->config->expects($this->once())
->method('isPublished')
->willReturn(true);
$grapesJsBuilder->expects($this->once())->method('setDraftCustomMjml');
$grapesJsBuilder->expects($this->once())->method('setCustomMjml');
$this->subscriber->manageEmailDraft($event);
}
public function testManageEmailDraftHandlesApply(): void
{
$event = $this->createMock(EmailEditSubmitEvent::class);
$event->expects($this->once())
->method('getCurrentEmail')
->willReturn($this->createMock(Email::class));
$event->expects($this->once())
->method('isApplyDraft')
->willReturn(true);
$this->grapesJsBuilderRepo->method('findOneBy')
->willReturn($grapesJsBuilder = $this->createMock(GrapesJsBuilder::class));
$this->config->expects($this->once())
->method('isPublished')
->willReturn(true);
$grapesJsBuilder->expects($this->once())->method('setDraftCustomMjml');
$grapesJsBuilder->expects($this->never())->method('setCustomMjml');
$this->subscriber->manageEmailDraft($event);
}
public function testManageEmailDraftHandlesDiscardDraft(): void
{
$event = $this->createMock(EmailEditSubmitEvent::class);
$event->expects($this->once())
->method('getCurrentEmail')
->willReturn($mockEmail = $this->createMock(Email::class));
$event->expects($this->once())
->method('isDiscardDraft')
->willReturn(true);
$mockEmail->expects($this->once())
->method('hasDraft')
->willReturn(true);
$this->grapesJsBuilderRepo->method('findOneBy')
->willReturn($grapesJsBuilder = $this->createMock(GrapesJsBuilder::class));
$this->config->expects($this->once())
->method('isPublished')
->willReturn(true);
$grapesJsBuilder->expects($this->once())
->method('setDraftCustomMjml')
->with(null);
$this->subscriber->manageEmailDraft($event);
}
}

View File

@@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
namespace MauticPlugin\GrapesJsBuilderBundle\Tests\Unit\Model;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\EmailRepository;
use Mautic\EmailBundle\Model\EmailModel;
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilder;
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilderRepository;
use MauticPlugin\GrapesJsBuilderBundle\Model\GrapesJsBuilderModel;
use PHPUnit\Framework\Assert;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class GrapesJsBuilderModelTest extends \PHPUnit\Framework\TestCase
{
public function testAddOrEditEntityWithoutMatchingEntityAndNoRequestQuery(): void
{
$requestStack = new class extends RequestStack {
public function __construct()
{
}
public function getCurrentRequest(): Request
{
return new Request();
}
};
$emailRepository = new class extends EmailRepository {
public int $saveEntityCallCount = 0;
public function __construct()
{
}
public function saveEntity($entity, $flush = true): void
{
++$this->saveEntityCallCount;
}
};
$emailModel = $this->getEmailModel($emailRepository);
$grapesJsBuilderRepository = new class extends GrapesJsBuilderRepository {
public int $saveEntityCallCount = 0;
public function __construct()
{
}
public function findOneBy(array $criteria, ?array $orderBy = null)
{
return null;
}
public function saveEntity($entity, $flush = true): void
{
++$this->saveEntityCallCount;
}
};
$entityManager = new class($grapesJsBuilderRepository) extends EntityManager {
public function __construct(
private GrapesJsBuilderRepository $grapesJsBuilderRepository,
) {
}
public function getRepository($entityName)
{
Assert::assertSame(GrapesJsBuilder::class, $entityName);
return $this->grapesJsBuilderRepository; // @phpstan-ignore-line
}
};
$email = new Email();
$grapeJsBuilderModel = new GrapesJsBuilderModel(
$requestStack,
$emailModel,
$entityManager,
$this->createMock(CorePermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->createMock(Router::class),
$this->getTranslator(),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class)
);
$grapeJsBuilderModel->addOrEditEntity($email);
// Not a GrapeJs email, so we are not saving anything.
Assert::assertSame(0, $grapesJsBuilderRepository->saveEntityCallCount);
Assert::assertSame(0, $emailRepository->saveEntityCallCount);
}
public function testAddOrEditEntityWithoutMatchingEntityAndGrapeRequestQuery(): void
{
$requestStack = new class extends RequestStack {
public function __construct()
{
}
public function getCurrentRequest(): Request
{
return new Request(
[],
[
'grapesjsbuilder' => [
'customMjml' => '</mjml>',
],
'emailform' => [
'customHtml' => '</html>',
],
]
);
}
};
$emailRepository = new class extends EmailRepository {
public int $saveEntityCallCount = 0;
public function __construct()
{
}
/**
* @param Email $entity
*/
public function saveEntity($entity, $flush = true): void
{
++$this->saveEntityCallCount;
Assert::assertSame('</html>', $entity->getCustomHtml());
}
};
$emailModel = $this->getEmailModel($emailRepository);
$grapesJsBuilderRepository = new class extends GrapesJsBuilderRepository {
public int $saveEntityCallCount = 0;
public function __construct()
{
}
public function findOneBy(array $criteria, ?array $orderBy = null)
{
return null;
}
/**
* @param GrapesJsBuilder $entity
*/
public function saveEntity($entity, $flush = true): void
{
++$this->saveEntityCallCount;
Assert::assertSame('</mjml>', $entity->getCustomMjml());
}
};
$entityManager = new class($grapesJsBuilderRepository) extends EntityManager {
public function __construct(
private GrapesJsBuilderRepository $grapesJsBuilderRepository,
) {
}
public function getRepository($entityName)
{
Assert::assertSame(GrapesJsBuilder::class, $entityName);
return $this->grapesJsBuilderRepository; // @phpstan-ignore-line
}
};
$email = new Email();
$grapeJsBuilderModel = new GrapesJsBuilderModel(
$requestStack,
$emailModel,
$entityManager,
$this->createMock(CorePermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->createMock(Router::class),
$this->getTranslator(),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class)
);
$grapeJsBuilderModel->addOrEditEntity($email);
// Saving the entities now.
Assert::assertSame(1, $grapesJsBuilderRepository->saveEntityCallCount);
Assert::assertSame(1, $emailRepository->saveEntityCallCount);
}
private function getEmailModel(EmailRepository $emailRepository): EmailModel
{
return new class($emailRepository) extends EmailModel {
public function __construct(
private EmailRepository $emailRepository,
) {
}
public function getRepository(): EmailRepository
{
return $this->emailRepository;
}
};
}
private function getTranslator(): Translator
{
return new class extends Translator {
public function __construct()
{
}
};
}
}