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\ProjectBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\ProjectBundle\Entity\Project;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
/**
* This class should simplify writing functional tests for project search functionality on various entities.
*/
abstract class AbstractProjectSearchTestCase extends MauticMysqlTestCase
{
/**
* @param string[] $expectedEntities
* @param string[] $unexpectedEntities
*/
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
abstract public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void;
/**
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
*/
abstract public static function searchDataProvider(): \Generator;
/**
* Test and assert API as well as UI.
*
* @param string[] $expectedEntities
* @param string[] $unexpectedEntities
* @param string[] $routes
*/
protected function searchAndAssert(string $searchTerm, array $expectedEntities, array $unexpectedEntities, array $routes): void
{
foreach ($routes as $route) {
$crawler = $this->client->request(Request::METHOD_GET, $route.'?search='.urlencode($searchTerm));
$this->assertResponseIsSuccessful();
$isApiRequest = str_starts_with($route, '/api/');
$content = $isApiRequest ? $this->client->getResponse()->getContent() : $crawler->filter('body')->text();
foreach ($expectedEntities as $expectedEntity) {
Assert::assertStringContainsString($expectedEntity, $content);
}
foreach ($unexpectedEntities as $unexpectedEntity) {
Assert::assertStringNotContainsString($unexpectedEntity, $content);
}
if ($isApiRequest) {
Assert::assertJson($content, 'API response should be of type JSON.');
$this->assertProjectDataInApiResponse(json_decode($content, true));
}
}
}
protected function createProject(string $name): Project
{
$project = new Project();
$project->setName($name);
$this->em->persist($project);
return $project;
}
/**
* @param mixed[] $data
*/
private function assertProjectDataInApiResponse(array $data): void
{
$projectData = $this->getProjectData($data);
if (null === $projectData) {
return;
}
Assert::assertEqualsCanonicalizing(['id', 'name'], array_keys(reset($projectData)),
'Project data should contain only "id" and "name".');
}
/**
* @param mixed[] $data
*
* @return mixed[]|null
*/
private function getProjectData(array $data): ?array
{
foreach ($data as $key => $item) {
if (!is_array($item)) {
continue;
}
if ('projects' === $key && $item) {
return $item;
}
$projectData = $this->getProjectData($item);
if (null !== $projectData) {
return $projectData;
}
}
return null;
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Mautic\ProjectBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\ProjectBundle\Entity\Project;
use Mautic\ProjectBundle\Model\ProjectModel;
use PHPUnit\Framework\Assert;
final class AjaxControllerTest extends MauticMysqlTestCase
{
public function testCreatingProjectViaMultiselectInput(): void
{
$projectNames = [
'Yellow Project',
'Blue Project',
'Red Project',
];
/** @var ProjectModel $projectModel */
$projectModel = self::getContainer()->get(ProjectModel::class);
$projects = array_map(
static function (string $projectName) use ($projectModel) {
$project = new Project();
$project->setName($projectName);
$projectModel->saveEntity($project);
return $project;
},
$projectNames
);
$this->client->request(
'POST',
'/s/ajax?action=project:addProjects',
[
'newProjectNames' => json_encode(['Green Project']),
'existingProjectIds' => json_encode([$projects[0]->getId(), $projects[1]->getId()]),
]
);
$this->assertResponseIsSuccessful();
$payload = json_decode($this->client->getResponse()->getContent(), true);
Assert::assertArrayHasKey('projects', $payload);
// The options are orderec alphabetically by name.
Assert::assertSame(
// The Blue Project is selected as it was sent as part of the existingProjectIds.
'<option selected="selected" value="'.$projects[1]->getId().'">'.$projects[1]->getName().'</option>'.
// The Green Project is selected as it was sent as part of the newProjectNames and should have next ID as it was created as 4th.
'<option selected="selected" value="'.($projects[2]->getId() + 1).'">Green Project</option>'.
// The Red Project is NOT selected as it was not sent in the AJAX request but it is listed as unselected option.
'<option value="'.$projects[2]->getId().'">'.$projects[2]->getName().'</option>'.
// The Yellow Project is selected as it was sent as part of the existingProjectIds.
'<option selected="selected" value="'.$projects[0]->getId().'">'.$projects[0]->getName().'</option>',
$payload['projects']
);
}
}

View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace Mautic\ProjectBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Model\EmailModel;
use Mautic\ProjectBundle\Entity\Project;
use Mautic\ProjectBundle\Model\ProjectModel;
final class ProjectAddEntityTest extends MauticMysqlTestCase
{
private Project $testProject;
private Email $testEmail;
protected function setUp(): void
{
parent::setUp();
/** @var ProjectModel $projectModel */
$projectModel = self::getContainer()->get(ProjectModel::class);
// Create test project
$this->testProject = new Project();
$this->testProject->setName('Test Project for Add Entity');
$this->testProject->setDescription('Test project for functional testing');
$projectModel->saveEntity($this->testProject);
// Create test email
/** @var EmailModel $emailModel */
$emailModel = self::getContainer()->get(EmailModel::class);
$this->testEmail = new Email();
$this->testEmail->setName('Test Email for Project');
$this->testEmail->setSubject('Test Email Subject');
$this->testEmail->setEmailType('template');
$this->testEmail->setTemplate('blank');
$emailModel->saveEntity($this->testEmail);
}
public function testSelectEntityTypeActionRendersModal(): void
{
$url = '/s/projects/selectEntityType/'.$this->testProject->getId();
$this->client->request('GET', $url);
$response = $this->client->getResponse();
$content = $response->getContent();
$this->assertResponseIsSuccessful();
$this->assertStringContainsString('entityType=email', $content);
}
public function testSelectEntityTypeActionNotFound(): void
{
$this->client->followRedirects(false);
$this->client->request('GET', '/s/projects/selectEntityType/99999');
$response = $this->client->getResponse();
$this->assertSame(404, $response->getStatusCode());
}
public function testAddEntityActionGetRequest(): void
{
$url = '/s/projects/addEntity/'.$this->testProject->getId().'?entityType=email';
$this->client->request('GET', $url);
$response = $this->client->getResponse();
$content = $response->getContent();
$this->assertResponseIsSuccessful();
// Should contain the form with proper structure
$this->assertStringContainsString('name="project_add_entity"', $content);
$this->assertStringContainsString('project_add_entity[entityType]', $content);
$this->assertStringContainsString('project_add_entity[projectId]', $content);
$this->assertStringContainsString('project_add_entity[entityIds][]', $content);
}
public function testAddEntityActionPostWithValidData(): void
{
// Add email to project directly using the entity relationship
$this->testEmail->addProject($this->testProject);
$this->em->persist($this->testEmail);
$this->em->flush();
// View project page to verify email was added
$url = '/s/projects/view/'.$this->testProject->getId();
$this->client->request('GET', $url);
$response = $this->client->getResponse();
$content = $response->getContent();
$this->assertResponseIsSuccessful();
$this->assertStringContainsString($this->testProject->getName(), $content);
$this->assertStringContainsString($this->testEmail->getName(), $content);
}
public function testAddEntityActionPostWithEmptyData(): void
{
$url = '/s/projects/addEntity/'.$this->testProject->getId().'?entityType=email';
// Get the form
$crawler = $this->client->request('GET', $url);
$this->assertResponseIsSuccessful();
// Submit form with no entities selected
$form = $crawler->filter('form')->first()->form();
$this->client->submit($form);
$this->assertResponseIsSuccessful();
}
public function testAddEntityActionPostWithCancelledForm(): void
{
$url = '/s/projects/addEntity/'.$this->testProject->getId().'?entityType=email';
// Get the form
$crawler = $this->client->request('GET', $url);
$this->assertResponseIsSuccessful();
// Submit form normally (simulating any button press)
$form = $crawler->filter('form')->first()->form();
$this->client->submit($form);
$this->assertResponseIsSuccessful();
}
public function testAddEntityActionWithInvalidEntityType(): void
{
$url = '/s/projects/addEntity/'.$this->testProject->getId().'?entityType=invalid_type';
// Get request with invalid entity type should redirect with error
$this->client->followRedirects();
$this->client->request('GET', $url);
$response = $this->client->getResponse();
$this->assertResponseIsSuccessful();
// Check for error message in flashes
$content = $response->getContent();
$this->assertStringContainsString('Invalid entity type', $content);
}
public function testAddEntityActionNotFound(): void
{
$this->client->followRedirects(false);
$this->client->request('GET', '/s/projects/addEntity/99999?entityType=email');
$response = $this->client->getResponse();
$this->assertSame(404, $response->getStatusCode());
}
public function testAddEntityActionWithoutPermission(): void
{
$user = $this->createAndLoginUser();
$url = '/s/projects/addEntity/'.$this->testProject->getId().'?entityType=email';
$this->client->request('GET', $url);
$this->assertResponseStatusCodeSame(403);
}
private function createAndLoginUser(): \Mautic\UserBundle\Entity\User
{
// Create non-admin role
$role = new \Mautic\UserBundle\Entity\Role();
$role->setName('Test Role');
$role->setIsAdmin(false);
$this->em->persist($role);
// Create non-admin user
$user = new \Mautic\UserBundle\Entity\User();
$user->setFirstName('Test');
$user->setLastName('User');
$user->setUsername('testuser');
$user->setEmail('test@example.com');
$hasher = self::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
$user->setPassword($hasher->hash('password'));
$user->setRole($role);
$this->em->persist($user);
$this->em->flush();
$this->loginUser($user);
return $user;
}
}

View File

@@ -0,0 +1,233 @@
<?php
declare(strict_types=1);
namespace Mautic\ProjectBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\ProjectBundle\Entity\Project;
use Mautic\ProjectBundle\Entity\ProjectRepository;
use Mautic\ProjectBundle\Model\ProjectModel;
use Mautic\UserBundle\Entity\Role;
use Mautic\UserBundle\Entity\User;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
final class ProjectControllerTest extends MauticMysqlTestCase
{
public const USERNAME = 'johny';
private ProjectRepository $projectRepository;
protected function setUp(): void
{
parent::setUp();
$projects = [
'project1',
'project2',
'project3',
'project4',
];
/** @var ProjectModel $projectModel */
$projectModel = self::getContainer()->get(ProjectModel::class);
$this->projectRepository = $projectModel->getRepository();
foreach ($projects as $projectName) {
$project = new Project();
$project->setName($projectName);
$projectModel->saveEntity($project);
}
}
#[\PHPUnit\Framework\Attributes\DataProvider('indexUrlsProvider')]
public function testIndexActionDisplaysProjects(string $url): void
{
$this->client->request('GET', $url);
$clientResponse = $this->client->getResponse();
$clientResponseContent = $clientResponse->getContent();
$this->assertResponseIsSuccessful();
$this->assertStringContainsString('project1', $clientResponseContent, 'The return must contain project1');
$this->assertStringContainsString('project2', $clientResponseContent, 'The return must contain project2');
}
/**
* @return iterable<string, array<int, string>>
*/
public static function indexUrlsProvider(): iterable
{
yield 'non-existent page nuber' => ['/s/projects/999'];
yield 'main index page with no number (meaning page=1)' => ['/s/projects'];
}
public function testIndexActionWhenFiltered(): void
{
$this->client->request('GET', '/s/projects?search=project1');
$clientResponse = $this->client->getResponse();
$clientResponseContent = $clientResponse->getContent();
$this->assertResponseIsSuccessful();
$this->assertStringContainsString('project1', $clientResponseContent, 'The return must contain project1');
$this->assertStringNotContainsString('project2', $clientResponseContent, 'The return must not contain project2');
}
public function testProjectDeletion(): void
{
$project = $this->projectRepository->findOneBy([]);
$segment = new LeadList();
$segment->setName('Test segment');
$segment->setPublicName('Test segment');
$segment->setAlias('test-segment');
$segment->addProject($project);
$this->em->persist($segment);
$this->em->flush();
$this->em->clear();
$projectId = $project->getId();
$this->client->request('POST', '/s/projects/delete/'.$projectId);
$this->assertResponseIsSuccessful();
$this->assertSame($this->projectRepository->find($projectId), null, 'Assert that project is deleted');
$this->assertCount(0, $this->em->find(LeadList::class, $segment->getId())->getProjects());
}
public function testViewAction(): void
{
$project = $this->projectRepository->findOneBy([]);
$this->client->request('GET', '/s/projects/view/'.$project->getId());
$clientResponse = $this->client->getResponse();
$clientResponseContent = $clientResponse->getContent();
$this->assertResponseIsSuccessful();
$this->assertStringContainsString($project->getName(), $clientResponseContent, 'The return must contain project');
}
public function testViewActionNotFound(): void
{
$this->client->followRedirects(false);
$this->client->request('GET', '/s/projects/view/99999');
$clientResponse = $this->client->getResponse();
$this->assertTrue($clientResponse->isRedirection(), 'Must be redirect response.');
}
public function testEditAction(): void
{
$projectName = 'Test project';
$project = $this->projectRepository->findOneBy([]);
$crawler = $this->client->request('GET', '/s/projects/edit/'.$project->getId());
$clientResponse = $this->client->getResponse();
$clientResponseContent = $clientResponse->getContent();
$this->assertTrue($clientResponse->isOk(), 'Return code must be 200.');
$this->assertStringContainsString('Edit project: '.$project->getName(), $clientResponseContent, 'The return must contain \'Edit project\' text');
$form = $crawler->selectButton('Save & Close')->form();
$form['project_entity[name]']->setValue($projectName);
$this->client->submit($form);
$this->assertSame(1, $this->projectRepository->count(['name' => $projectName]));
}
public function testEditActionNotFound(): void
{
$this->client->followRedirects(false);
$this->client->request('GET', '/s/projects/edit/99999');
$clientResponse = $this->client->getResponse();
$this->assertTrue($clientResponse->isRedirection(), 'Must be redirect response.');
}
public function testNewAction(): void
{
$projectName = 'Test project';
$crawler = $this->client->request('GET', '/s/projects/new');
$this->assertResponseIsSuccessful();
$form = $crawler->selectButton('Save')->form();
$form['project_entity[name]']->setValue($projectName);
$this->client->submit($form);
$this->assertSame(1, $this->projectRepository->count(['name' => $projectName]));
}
public function testBatchDeleteAction(): void
{
$projects = $this->projectRepository->findAll();
$projectsId = array_map(function (Project $project) {
return $project->getId();
}, $projects);
$this->client->request('POST', '/s/projects/batchDelete?ids='.json_encode($projectsId));
$this->assertResponseIsSuccessful();
$this->assertEmpty($this->projectRepository->count([]), 'All projects must be deleted.');
}
public function testEmptyProjectShouldThrowValidationError(): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/projects/new');
$this->assertResponseIsSuccessful();
$buttonCrawler = $crawler->selectButton('Save & Close');
$form = $buttonCrawler->form();
$form->setValues(['project_entity[name]' => '']);
$this->client->submit($form);
$this->assertResponseIsSuccessful();
Assert::assertStringContainsString('A name is required.', $this->client->getResponse()->getContent());
}
public function testEditProjectWithNoPermission(): void
{
$this->createAndLoginUser();
$project = $this->projectRepository->findOneBy([]);
$this->client->request(Request::METHOD_GET, '/s/projects/edit/'.$project->getId());
$this->assertResponseStatusCodeSame(403, (string) $this->client->getResponse()->getStatusCode());
}
private function createAndLoginUser(): User
{
// Create non-admin role
$role = $this->createRole();
// Create non-admin user
$user = $this->createUser($role);
$this->em->flush();
$this->em->detach($role);
$this->loginUser($user);
// $this->client->setServerParameter('PHP_AUTH_USER', self::USERNAME);
// $this->client->setServerParameter('PHP_AUTH_PW', 'mautic');
return $user;
}
private function createRole(bool $isAdmin = false): Role
{
$role = new Role();
$role->setName('Role');
$role->setIsAdmin($isAdmin);
$this->em->persist($role);
return $role;
}
private function createUser(Role $role): User
{
$user = new User();
$user->setFirstName('Jhon');
$user->setLastName('Doe');
$user->setUsername(self::USERNAME);
$user->setEmail('john.doe@email.com');
$hasher = self::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
\assert($hasher instanceof PasswordHasherInterface);
$user->setPassword($hasher->hash('mautic'));
$user->setRole($role);
$this->em->persist($user);
return $user;
}
}