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,81 @@
<?php
declare(strict_types=1);
namespace Mautic\ApiBundle\Tests\Functional\Controller;
use Mautic\ApiBundle\Entity\oAuth2\Client;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ClientControllerTest extends MauticMysqlTestCase
{
private const TOTAL_COUNT = 6;
#[\PHPUnit\Framework\Attributes\RunInSeparateProcess]
public function testIndexActionForPager(): void
{
$this->createApiClients();
// Test the first page without limits
$this->requestCredentialsPage();
$this->assertPaginationDetails(1);
// Test pagination with varying limits
$this->requestCredentialsPage(5);
$this->assertPaginationDetails(2);
}
private function createApiClients(): void
{
foreach (range(1, self::TOTAL_COUNT) as $i) {
$client = new Client();
$client->setName('client'.$i);
$client->setRedirectUris(['https://example.com/'.$i]);
$this->em->persist($client);
}
$this->em->flush();
$this->em->clear();
}
/**
* Make a request to the credentials page with pagination.
*/
private function requestCredentialsPage(?int $limit = null): void
{
$url = '/s/credentials?tmpl=list&name=client';
if ($limit) {
$url .= '&limit='.$limit;
}
$this->client->request(Request::METHOD_GET, $url);
}
/**
* Assert the pagination details on the response.
*
* @param int $pageCount The expected number of pages
*/
private function assertPaginationDetails(int $pageCount): void
{
$content = $this->client->getResponse()->getContent();
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
$translator = static::getContainer()->get('translator');
// Check for total item count in pagination
$this->assertStringContainsString(
$translator->trans('mautic.core.pagination.items', ['%count%' => self::TOTAL_COUNT]),
$content
);
// Check for total page count in pagination
$this->assertStringContainsString(
$translator->trans('mautic.core.pagination.pages', ['%count%' => $pageCount]),
$content
);
}
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace Mautic\ApiBundle\Tests\Functional;
use Mautic\CoreBundle\Test\IsolatedTestTrait;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* This test must run in a separate process because it sets the global constant
* MAUTIC_INSTALLER which breaks other tests.
*/
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
#[\PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses]
final class Oauth2Test extends MauticMysqlTestCase
{
use IsolatedTestTrait;
protected function setUp(): void
{
$this->useCleanupRollback = false;
parent::setUp();
}
#[\PHPUnit\Framework\Attributes\DataProvider('provideMethods')]
public function testAuthorize(string $method): void
{
// Disable the default logging in via username and password.
$this->clientServer = [];
$this->setUpSymfony($this->configParams);
$this->client->followRedirects(false);
$this->client->request(
$method,
'/oauth/v2/authorize'
);
$this->client->followRedirects(true);
$response = $this->client->getResponse();
Assert::assertSame(Response::HTTP_FOUND, $response->getStatusCode(), $response->getContent());
Assert::assertSame('https://localhost/oauth/v2/authorize_login', $response->headers->get('Location'));
}
public static function provideMethods(): \Generator
{
yield 'GET' => [Request::METHOD_GET];
yield 'POST' => [Request::METHOD_POST];
}
public function testAuthWithInvalidCredentials(): void
{
$this->client->enableReboot();
// Disable the default logging in via username and password.
$this->clientServer = [];
$this->setUpSymfony($this->configParams);
$this->client->request(
Request::METHOD_POST,
'/oauth/v2/token',
[
'grant_type' => 'client_credentials',
'client_id' => 'unicorn',
'client_secret' => 'secretUnicorn',
]
);
$response = $this->client->getResponse();
self::assertResponseStatusCodeSame(400, $response->getContent());
Assert::assertSame(
'{"errors":[{"message":"The client credentials are invalid","code":400,"type":"invalid_client"}]}',
$response->getContent()
);
}
public function testAuthWithInvalidAccessToken(): void
{
$this->client->enableReboot();
// Disable the default logging in via username and password.
$this->clientServer = [];
$this->setUpSymfony($this->configParams);
$this->client->request(
Request::METHOD_GET,
'/api/users',
[],
[],
[
'HTTP_Authorization' => 'Bearer unicorn_token',
],
);
$response = $this->client->getResponse();
self::assertResponseStatusCodeSame(401, $response->getContent());
Assert::assertSame('{"errors":[{"message":"The access token provided is invalid.","code":401,"type":"invalid_grant"}]}', $response->getContent());
}
public function testAuthWorkflow(): void
{
$this->client->disableReboot();
// Create OAuth2 credentials.
$crawler = $this->client->request(Request::METHOD_GET, 's/credentials/new');
$saveButton = $crawler->selectButton('Save');
$form = $saveButton->form();
$form['client[name]']->setValue('Auth Test');
$form['client[redirectUris]']->setValue('https://test.org');
$crawler = $this->client->submit($form);
self::assertResponseIsSuccessful();
$clientPublicKey = $crawler->filter('input#client_publicId')->attr('value');
$clientSecretKey = $crawler->filter('input#client_secret')->attr('value');
$this->logoutUser();
// Get the access token.
$this->client->request(
Request::METHOD_POST,
'/oauth/v2/token',
[
'grant_type' => 'client_credentials',
'client_id' => $clientPublicKey,
'client_secret' => $clientSecretKey,
],
);
self::assertResponseIsSuccessful();
$payload = json_decode($this->client->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
$accessToken = $payload['access_token'];
Assert::assertNotEmpty($accessToken);
// Test that the access token works by fetching users via API.
$this->client->request(
Request::METHOD_GET,
'/api/users',
[],
[],
[
'HTTP_Authorization' => "Bearer {$accessToken}",
],
);
self::assertResponseIsSuccessful();
Assert::assertStringContainsString('"users":[', $this->client->getResponse()->getContent());
}
}

View File

@@ -0,0 +1,257 @@
<?php
declare(strict_types=1);
namespace Mautic\ApiBundle\Tests\Functional\Serializer;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PageBundle\Entity\Page;
use Mautic\ProjectBundle\Entity\Project;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Response;
/**
* Functional test to verify that PUT operations update existing entities
* instead of creating new ones. This tests the PutProcessor fix end-to-end.
*/
final class PutOperationTest extends MauticMysqlTestCase
{
/**
* Test that API Platform GET endpoints work correctly.
* This helps isolate whether the issue is with PUT specifically or API Platform in general.
*/
public function testGetOperationWorks(): void
{
// Create a project
$project = new Project();
$project->setName('Test Project');
$project->setDescription('Test Description');
$this->em->persist($project);
$this->em->flush();
$projectId = $project->getId();
// Make a GET request to retrieve the project
$this->client->request('GET', '/api/v2/projects/'.$projectId);
$this->assertResponseIsSuccessful();
$responseData = json_decode($this->client->getResponse()->getContent(), true);
Assert::assertArrayHasKey('id', $responseData);
Assert::assertSame($projectId, $responseData['id']);
Assert::assertSame('Test Project', $responseData['name']);
Assert::assertSame('Test Description', $responseData['description']);
}
/**
* Test that PUT operations work globally for different entities (Page example).
* This verifies that our global PutProcessor fix works for all API Platform entities.
*/
public function testPutOperationWorksGloballyForPageEntity(): void
{
// Create a page
$page = new Page();
$page->setTitle('Original Page Title');
$page->setAlias('original-page-alias');
$page->setMetaDescription('Original Meta Description');
$this->em->persist($page);
$this->em->flush();
$originalId = $page->getId();
Assert::assertNotNull($originalId, 'Page should have an ID after persisting');
// Update the page via PUT request
$this->client->request(
'PUT',
'/api/v2/pages/'.$originalId,
[],
[],
[
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
],
json_encode([
'title' => 'Updated Page Title',
'alias' => 'updated-page-alias',
'metaDescription' => 'Updated Meta Description',
])
);
$this->assertResponseIsSuccessful();
$response = json_decode($this->client->getResponse()->getContent(), true);
// The key assertion: ID should remain the same (global fix working)
Assert::assertSame($originalId, $response['id'], 'PUT should update the existing page, not create a new one');
Assert::assertSame('Updated Page Title', $response['title']);
Assert::assertSame('updated-page-alias', $response['alias']);
Assert::assertSame('Updated Meta Description', $response['metaDescription']);
}
/**
* Test that PUT operation updates existing entity instead of creating a new one.
* This is the main regression test for the EntityContextBuilder fix.
*/
public function testPutOperationUpdatesExistingProject(): void
{
// Create initial project
$project = new Project();
$project->setName('Original Project');
$project->setDescription('Original Description');
$this->em->persist($project);
$this->em->flush();
$originalId = $project->getId();
Assert::assertNotNull($originalId, 'Project should have an ID after persisting');
// Update the project via PUT request
$this->client->request(
'PUT',
'/api/v2/projects/'.$originalId,
[],
[],
[
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
],
json_encode([
'name' => 'Updated Project',
'description' => 'Updated Description',
])
);
$this->assertResponseIsSuccessful();
$response = json_decode($this->client->getResponse()->getContent(), true);
// The key assertion: ID should remain the same (not creating a new entity)
Assert::assertSame($originalId, $response['id'], 'PUT should update the existing entity, not create a new one');
Assert::assertSame('Updated Project', $response['name']);
Assert::assertSame('Updated Description', $response['description']);
// Verify in database that only one project exists with the updated data
$this->em->clear();
$projects = $this->em->getRepository(Project::class)->findAll();
Assert::assertCount(1, $projects, 'Should only have one project in database after PUT');
Assert::assertSame($originalId, $projects[0]->getId());
Assert::assertSame('Updated Project', $projects[0]->getName());
Assert::assertSame('Updated Description', $projects[0]->getDescription());
}
/**
* Test that PUT request for non-existent entity returns 404.
*/
public function testPutOperationReturns404ForNonExistentProject(): void
{
$nonExistentId = 99999;
$this->client->request(
'PUT',
'/api/v2/projects/'.$nonExistentId,
[],
[],
[
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
],
json_encode([
'name' => 'Test Project',
'description' => 'Test Description',
])
);
Assert::assertSame(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode());
}
/**
* Test that POST operations still work correctly (create new entities).
*/
public function testPostOperationCreatesNewProject(): void
{
$this->client->request(
'POST',
'/api/v2/projects',
[],
[],
[
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
],
json_encode([
'name' => 'New Project',
'description' => 'New Description',
])
);
$this->assertResponseIsSuccessful();
$response = json_decode($this->client->getResponse()->getContent(), true);
Assert::assertIsInt($response['id']);
Assert::assertSame('New Project', $response['name']);
Assert::assertSame('New Description', $response['description']);
// Verify project was created in database
$this->em->clear();
$project = $this->em->getRepository(Project::class)->find($response['id']);
Assert::assertNotNull($project);
Assert::assertSame('New Project', $project->getName());
}
/**
* Test that PUT operation completely replaces the resource (proper HTTP PUT semantics).
* If a field is missing from the PUT request, it should be set to null in the existing entity.
*/
public function testPutOperationReplacesEntireResource(): void
{
// Create initial project with both name and description
$project = new Project();
$project->setName('Original Project');
$project->setDescription('Original Description');
$this->em->persist($project);
$this->em->flush();
$originalId = $project->getId();
Assert::assertNotNull($originalId, 'Project should have an ID after persisting');
// Update the project via PUT request with only name (no description)
// According to HTTP PUT semantics, this should clear the description
$this->client->request(
'PUT',
'/api/v2/projects/'.$originalId,
[],
[],
[
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
],
json_encode([
'name' => 'Updated Project Name Only',
// Note: description is intentionally omitted
])
);
Assert::assertSame(200, $this->client->getResponse()->getStatusCode());
$response = json_decode($this->client->getResponse()->getContent(), true);
// Verify the response shows the field was replaced (cleared)
Assert::assertSame($originalId, $response['id'], 'Should update existing project, not create new one');
Assert::assertSame('Updated Project Name Only', $response['name']);
// The API may not include null fields in the response, so check if key exists
if (array_key_exists('description', $response)) {
Assert::assertNull($response['description'], 'Description should be null since it was not provided in PUT request');
}
// Verify in database that the description was actually cleared
$this->em->clear();
$updatedProject = $this->em->getRepository(Project::class)->find($originalId);
Assert::assertNotNull($updatedProject);
Assert::assertSame('Updated Project Name Only', $updatedProject->getName());
Assert::assertNull($updatedProject->getDescription(), 'Description should be cleared in database');
}
}