Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user