Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ApiBundle\Tests\ApiPlatform\EventListener;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
|
||||
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
|
||||
use Mautic\ApiBundle\ApiPlatform\EventListener\MauticDenyAccessListener;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
final class MauticDenyAccessListenerTest extends TestCase
|
||||
{
|
||||
private MockObject&CorePermissions $corePermissionsMock;
|
||||
|
||||
private ApiResource $resourceMetadata;
|
||||
|
||||
private ResourceMetadataCollectionFactoryInterface&MockObject $resourceMetadataFactoryMock;
|
||||
|
||||
private RequestEvent $requestEvent;
|
||||
|
||||
private MauticDenyAccessListener $mauticDenyAccessListener;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$attributes = [
|
||||
'_api_resource_class' => 'TestClass',
|
||||
'_api_operation_name' => 'Test',
|
||||
'item_operation_name' => 'Test',
|
||||
];
|
||||
$parameterBagMock = $this->createMock(ParameterBag::class);
|
||||
$parameterBagMock
|
||||
->expects($this->exactly(1))
|
||||
->method('all')
|
||||
->willReturn($attributes);
|
||||
$formEntityMock = $this->createMock(FormEntity::class);
|
||||
$formEntityMock
|
||||
->expects($this->atMost(1))
|
||||
->method('getCreatedBy')
|
||||
->willReturn(0);
|
||||
$parameterBagMock
|
||||
->expects($this->exactly(1))
|
||||
->method('get')
|
||||
->with('data')
|
||||
->willReturn($formEntityMock);
|
||||
$requestMock = $this->createMock(Request::class);
|
||||
$requestMock->attributes = $parameterBagMock;
|
||||
$this->corePermissionsMock = $this->createMock(CorePermissions::class);
|
||||
$this->resourceMetadataFactoryMock = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
|
||||
$this->requestEvent = new RequestEvent(
|
||||
$this->createMock(HttpKernelInterface::class),
|
||||
$requestMock,
|
||||
HttpKernelInterface::MAIN_REQUEST
|
||||
);
|
||||
$this->mauticDenyAccessListener = new MauticDenyAccessListener($this->corePermissionsMock, $this->resourceMetadataFactoryMock);
|
||||
}
|
||||
|
||||
public function testOnSecurityEntityAccessAllowed(): void
|
||||
{
|
||||
$operations = [
|
||||
new Get(
|
||||
security: '"test_item:edit"',
|
||||
name: 'Test'
|
||||
),
|
||||
];
|
||||
|
||||
$this->resourceMetadata = new ApiResource(operations: $operations);
|
||||
$resourceMetadataCollection = new ResourceMetadataCollection('TestClass', [$this->resourceMetadata]);
|
||||
$this->resourceMetadataFactoryMock
|
||||
->expects($this->exactly(1))
|
||||
->method('create')
|
||||
->with('TestClass')
|
||||
->willReturn($resourceMetadataCollection);
|
||||
$this->corePermissionsMock
|
||||
->expects($this->exactly(1))
|
||||
->method('hasEntityAccess')
|
||||
->with('test_item:editown', 'test_item:editother', 0)
|
||||
->willReturn(true);
|
||||
$this->mauticDenyAccessListener->onSecurity($this->requestEvent);
|
||||
}
|
||||
|
||||
public function testOnSecurityIsGranted(): void
|
||||
{
|
||||
$operations = [
|
||||
new Get(
|
||||
security: '"test_item:write"',
|
||||
name: 'Test'
|
||||
),
|
||||
];
|
||||
$this->resourceMetadata = new ApiResource(operations: $operations);
|
||||
$resourceMetadataCollection = new ResourceMetadataCollection('TestClass', [$this->resourceMetadata]);
|
||||
$this->resourceMetadataFactoryMock
|
||||
->expects($this->exactly(1))
|
||||
->method('create')
|
||||
->with('TestClass')
|
||||
->willReturn($resourceMetadataCollection);
|
||||
$this->corePermissionsMock
|
||||
->expects($this->exactly(1))
|
||||
->method('isGranted')
|
||||
->with('test_item:write')
|
||||
->willReturn(true);
|
||||
$this->mauticDenyAccessListener->onSecurity($this->requestEvent);
|
||||
}
|
||||
|
||||
public function testOnSecurityAccessDenied(): void
|
||||
{
|
||||
$operations = [
|
||||
new Get(
|
||||
security: '"test_item:write"',
|
||||
name: 'Test'
|
||||
),
|
||||
];
|
||||
|
||||
$this->resourceMetadata = new ApiResource(operations: $operations);
|
||||
$resourceMetadataCollection = new ResourceMetadataCollection('TestClass', [$this->resourceMetadata]);
|
||||
$this->resourceMetadataFactoryMock
|
||||
->expects($this->exactly(1))
|
||||
->method('create')
|
||||
->with('TestClass')
|
||||
->willReturn($resourceMetadataCollection);
|
||||
$this->corePermissionsMock
|
||||
->expects($this->exactly(1))
|
||||
->method('isGranted')
|
||||
->with('test_item:write')
|
||||
->willReturn(false);
|
||||
$this->expectException(AccessDeniedException::class);
|
||||
$this->mauticDenyAccessListener->onSecurity($this->requestEvent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ApiBundle\Tests\ApiPlatform\EventListener;
|
||||
|
||||
use ApiPlatform\Symfony\EventListener\EventPriorities;
|
||||
use Mautic\ApiBundle\ApiPlatform\EventListener\MauticWriteSubscriber;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\ViewEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
final class MauticWriteSubscriberTest extends TestCase
|
||||
{
|
||||
private MauticWriteSubscriber $mauticWriteSubscriber;
|
||||
|
||||
private ViewEvent $event;
|
||||
|
||||
private MockObject&FormEntity $formEntityMock;
|
||||
|
||||
private Request&MockObject $requestMock;
|
||||
|
||||
private UserHelper&MockObject $userHelperMock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userHelperMock = $this->createMock(UserHelper::class);
|
||||
$this->mauticWriteSubscriber = new MauticWriteSubscriber($this->userHelperMock);
|
||||
$this->requestMock = $this->createMock(Request::class);
|
||||
$this->formEntityMock = $this->createMock(FormEntity::class);
|
||||
$kernelMock = $this->createMock(HttpKernelInterface::class);
|
||||
$this->event = new ViewEvent(
|
||||
$kernelMock,
|
||||
$this->requestMock,
|
||||
HttpKernelInterface::MAIN_REQUEST,
|
||||
$this->formEntityMock,
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
$expected = [
|
||||
'kernel.view'=> ['addData', EventPriorities::PRE_WRITE],
|
||||
];
|
||||
$this->assertEquals($expected, MauticWriteSubscriber::getSubscribedEvents());
|
||||
}
|
||||
|
||||
public function testAddDataWithWrongMethod(): void
|
||||
{
|
||||
$this->requestMock
|
||||
->expects($this->exactly(1))
|
||||
->method('getMethod')
|
||||
->willReturn('GET');
|
||||
$this->formEntityMock
|
||||
->expects($this->never())
|
||||
->method('isNew');
|
||||
$this->mauticWriteSubscriber->addData($this->event);
|
||||
}
|
||||
|
||||
public function testAddData(): void
|
||||
{
|
||||
$this->requestMock
|
||||
->expects($this->exactly(1))
|
||||
->method('getMethod')
|
||||
->willReturn('POST');
|
||||
$this->formEntityMock
|
||||
->expects($this->exactly(1))
|
||||
->method('isNew')
|
||||
->willReturn(false);
|
||||
$userMock = $this->createMock(User::class);
|
||||
$userMock
|
||||
->expects($this->exactly(1))
|
||||
->method('getName')
|
||||
->willReturn('Pepa');
|
||||
$this->userHelperMock
|
||||
->expects($this->exactly(1))
|
||||
->method('getUser')
|
||||
->willReturn($userMock);
|
||||
$this->formEntityMock
|
||||
->expects($this->exactly(1))
|
||||
->method('setModifiedBy')
|
||||
->with($userMock);
|
||||
$this->formEntityMock
|
||||
->expects($this->exactly(1))
|
||||
->method('setModifiedByUser')
|
||||
->with('Pepa');
|
||||
$this->formEntityMock
|
||||
->expects($this->exactly(1))
|
||||
->method('setDateModified')
|
||||
->withAnyParameters();
|
||||
$this->mauticWriteSubscriber->addData($this->event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ApiBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CampaignBundle\Tests\CampaignTestAbstract;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Model\AbstractCommonModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Entity\UserRepository;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\Router;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class CommonApiControllerTest extends CampaignTestAbstract
|
||||
{
|
||||
public function testAddAliasIfNotPresentWithOneColumnWithoutAlias(): void
|
||||
{
|
||||
$result = $this->getResultFromProtectedMethod('addAliasIfNotPresent', ['dateAdded', 'f']);
|
||||
|
||||
$this->assertEquals('f.dateAdded', $result);
|
||||
}
|
||||
|
||||
public function testAddAliasIfNotPresentWithOneColumnWithDifferentAlias(): void
|
||||
{
|
||||
$result = $this->getResultFromProtectedMethod('addAliasIfNotPresent', ['s.date_submitted', 'fs']);
|
||||
|
||||
$this->assertEquals('s.date_submitted', $result);
|
||||
}
|
||||
|
||||
public function testAddAliasIfNotPresentWithOneColumnWithAlias(): void
|
||||
{
|
||||
$result = $this->getResultFromProtectedMethod('addAliasIfNotPresent', ['f.dateAdded', 'f']);
|
||||
|
||||
$this->assertEquals('f.dateAdded', $result);
|
||||
}
|
||||
|
||||
public function testAddAliasIfNotPresentWithTwoColumnsWithAlias(): void
|
||||
{
|
||||
$result = $this->getResultFromProtectedMethod('addAliasIfNotPresent', ['f.dateAdded, f.dateModified', 'f']);
|
||||
|
||||
$this->assertEquals('f.dateAdded,f.dateModified', $result);
|
||||
}
|
||||
|
||||
public function testAddAliasIfNotPresentWithTwoColumnsWithoutAlias(): void
|
||||
{
|
||||
$result = $this->getResultFromProtectedMethod('addAliasIfNotPresent', ['dateAdded, dateModified', 'f']);
|
||||
|
||||
$this->assertEquals('f.dateAdded,f.dateModified', $result);
|
||||
}
|
||||
|
||||
public function testgetWhereFromRequestWithNoWhere(): void
|
||||
{
|
||||
$result = $this->getResultFromProtectedMethod('getWhereFromRequest', [new Request()]);
|
||||
|
||||
$this->assertEquals([], $result);
|
||||
}
|
||||
|
||||
public function testgetWhereFromRequestWithSomeWhere(): void
|
||||
{
|
||||
$where = [
|
||||
[
|
||||
'col' => 'id',
|
||||
'expr' => 'eq',
|
||||
'val' => 5,
|
||||
],
|
||||
];
|
||||
|
||||
$request = new Request(['where' => $where]);
|
||||
$result = $this->getResultFromProtectedMethod('getWhereFromRequest', [$request]);
|
||||
|
||||
$this->assertEquals($where, $result);
|
||||
}
|
||||
|
||||
protected function getResultFromProtectedMethod($method, array $args)
|
||||
{
|
||||
$controller = new CommonApiController(
|
||||
$this->createMock(CorePermissions::class),
|
||||
$this->createMock(Translator::class),
|
||||
$this->createMock(EntityResultHelper::class),
|
||||
$this->createMock(Router::class),
|
||||
$this->createMock(FormFactoryInterface::class),
|
||||
$this->createMock(AppVersion::class),
|
||||
$this->createMock(RequestStack::class),
|
||||
$this->createMock(ManagerRegistry::class),
|
||||
$this->createMock(ModelFactory::class),
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(CoreParametersHelper::class)
|
||||
);
|
||||
|
||||
$controllerReflection = new \ReflectionClass(CommonApiController::class);
|
||||
$method = $controllerReflection->getMethod($method);
|
||||
$method->setAccessible(true);
|
||||
|
||||
return $method->invokeArgs($controller, $args);
|
||||
}
|
||||
|
||||
public function testGetBatchEntities(): void
|
||||
{
|
||||
$controller = new class($this->createMock(CorePermissions::class), $this->createMock(Translator::class), new EntityResultHelper(), $this->createMock(Router::class), $this->createMock(FormFactoryInterface::class), $this->createMock(AppVersion::class), $this->createMock(RequestStack::class), $this->createMock(ManagerRegistry::class), $this->createMock(ModelFactory::class), $this->createMock(EventDispatcherInterface::class), $this->createMock(CoreParametersHelper::class)) extends CommonApiController {
|
||||
/**
|
||||
* @param mixed[] $parameters
|
||||
* @param mixed[] $errors
|
||||
* @param AbstractCommonModel<User> $model
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function testGetBatchEntities(array $parameters, array $errors, AbstractCommonModel $model): array
|
||||
{
|
||||
return $this->getBatchEntities($parameters, $errors, false, 'id', $model);
|
||||
}
|
||||
};
|
||||
|
||||
$errors = [];
|
||||
$parameters = [
|
||||
[
|
||||
'id' => 3,
|
||||
'username' => 'API_0YjVvxlg',
|
||||
'firstName' => 'APIAPI_0YjVvxlg',
|
||||
'lastName' => 'TestAPI_0YjVvxlg',
|
||||
'email' => 'API_0YjVvxlg@email.com',
|
||||
'plainPassword' => [
|
||||
'password' => 'topSecret007',
|
||||
'confirm' => 'topSecret007',
|
||||
],
|
||||
'role' => 1,
|
||||
],
|
||||
1 => [
|
||||
'id' => 4,
|
||||
'username' => 'API_PlEiXJyp',
|
||||
'firstName' => 'APIAPI_PlEiXJyp',
|
||||
'lastName' => 'TestAPI_PlEiXJyp',
|
||||
'email' => 'API_PlEiXJyp@email.com',
|
||||
'plainPassword' => [
|
||||
'password' => 'topSecret007',
|
||||
'confirm' => 'topSecret007',
|
||||
],
|
||||
'role' => 1,
|
||||
],
|
||||
2 => [
|
||||
'id' => 5,
|
||||
'username' => 'API_AfhKVHTr',
|
||||
'firstName' => 'APIAPI_AfhKVHTr',
|
||||
'lastName' => 'TestAPI_AfhKVHTr',
|
||||
'email' => 'API_AfhKVHTr@email.com',
|
||||
'plainPassword' => [
|
||||
'password' => 'topSecret007',
|
||||
'confirm' => 'topSecret007',
|
||||
],
|
||||
'role' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
$users = [];
|
||||
foreach ([3, 5, 4] as $userId) {
|
||||
$user = $this->createMock(User::class);
|
||||
$user->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn($userId);
|
||||
$users[] = $user;
|
||||
}
|
||||
|
||||
$repository = $this->createMock(UserRepository::class);
|
||||
$repository->expects($this->once())
|
||||
->method('getTableAlias')
|
||||
->willReturn('user');
|
||||
$model = $this->createMock(UserModel::class);
|
||||
$model->expects($this->once())
|
||||
->method('getRepository')
|
||||
->willReturn($repository);
|
||||
$model->expects($this->once())
|
||||
->method('getEntities')
|
||||
->willReturn($users);
|
||||
$entities = $controller->testGetBatchEntities($parameters, $errors, $model);
|
||||
$this->assertSame(3, $entities[0]->getId());
|
||||
$this->assertSame(4, $entities[1]->getId());
|
||||
$this->assertSame(5, $entities[2]->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ApiBundle\Tests;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EntityResultHelperTest extends TestCase
|
||||
{
|
||||
public const NEW_TITLE = 'Callback Title';
|
||||
|
||||
public function testGetArrayEntities(): void
|
||||
{
|
||||
$resultHelper = new EntityResultHelper();
|
||||
|
||||
$lead2 = new Lead();
|
||||
$lead2->setId(2);
|
||||
|
||||
$lead5 = new Lead();
|
||||
$lead5->setId(5);
|
||||
|
||||
$results = [2 => $lead2, 5 => $lead5];
|
||||
|
||||
$arrayResult = $resultHelper->getArray($results);
|
||||
|
||||
$this->assertEquals($results, $arrayResult);
|
||||
|
||||
$arrayResult = $resultHelper->getArray($results, function ($entity): void {
|
||||
$this->modifyEntityData($entity);
|
||||
});
|
||||
|
||||
foreach ($arrayResult as $entity) {
|
||||
$this->assertEquals($entity->getTitle(), self::NEW_TITLE);
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetArrayPaginator(): void
|
||||
{
|
||||
$resultHelper = new EntityResultHelper();
|
||||
|
||||
$lead2 = new Lead();
|
||||
$lead2->setId(2);
|
||||
|
||||
$lead5 = new Lead();
|
||||
$lead5->setId(5);
|
||||
|
||||
$results = [$lead2, $lead5];
|
||||
|
||||
$iterator = new \ArrayIterator($results);
|
||||
|
||||
$paginator = $this->getMockBuilder(Paginator::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['getIterator'])
|
||||
->getMock();
|
||||
|
||||
$paginator->expects($this->any())
|
||||
->method('getIterator')
|
||||
->willReturn($iterator);
|
||||
|
||||
$arrayResult = $resultHelper->getArray($paginator);
|
||||
|
||||
$this->assertEquals($results, $arrayResult);
|
||||
|
||||
$arrayResult = $resultHelper->getArray($results, function ($entity): void {
|
||||
$this->modifyEntityData($entity);
|
||||
});
|
||||
|
||||
foreach ($arrayResult as $entity) {
|
||||
$this->assertEquals($entity->getTitle(), self::NEW_TITLE);
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetArrayAppendedData(): void
|
||||
{
|
||||
$resultHelper = new EntityResultHelper();
|
||||
|
||||
$lead2 = new Lead();
|
||||
$lead2->setId(2);
|
||||
|
||||
$lead5 = new Lead();
|
||||
$lead5->setId(5);
|
||||
|
||||
$lead7 = new Lead();
|
||||
$lead7->setId(7);
|
||||
|
||||
$data = [[$lead2, 'title' => 'Title 2'], [$lead5, 'title' => 'Title 5'], [$lead7, 'title' => 'Title 7']];
|
||||
|
||||
$expectedResult = [$lead2, $lead5, $lead7];
|
||||
|
||||
$arrayResult = $resultHelper->getArray($data);
|
||||
|
||||
$this->assertEquals($expectedResult, $arrayResult);
|
||||
|
||||
foreach ($arrayResult as $entity) {
|
||||
$this->assertEquals($entity->getTitle(), 'Title '.$entity->getId());
|
||||
}
|
||||
|
||||
$arrayResult = $resultHelper->getArray($data, function ($entity): void {
|
||||
$this->modifyEntityData($entity);
|
||||
});
|
||||
|
||||
foreach ($arrayResult as $entity) {
|
||||
$this->assertEquals($entity->getTitle(), self::NEW_TITLE);
|
||||
}
|
||||
}
|
||||
|
||||
private function modifyEntityData(Lead $entity): void
|
||||
{
|
||||
$entity->setTitle(self::NEW_TITLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ApiBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\ApiBundle\EventListener\ApiSubscriber;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Tests\CommonMocks;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\HttpFoundation\HeaderBag;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
|
||||
class ApiSubscriberTest extends CommonMocks
|
||||
{
|
||||
/**
|
||||
* @var CoreParametersHelper|MockObject
|
||||
*/
|
||||
private MockObject $coreParametersHelper;
|
||||
|
||||
/**
|
||||
* @var Translator&MockObject
|
||||
*/
|
||||
private MockObject $translator;
|
||||
|
||||
/**
|
||||
* @var Request&MockObject
|
||||
*/
|
||||
private MockObject $request;
|
||||
|
||||
/**
|
||||
* @var RequestEvent&MockObject
|
||||
*/
|
||||
private MockObject $event;
|
||||
|
||||
private ApiSubscriber $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
$this->request = $this->createMock(Request::class);
|
||||
$this->request->headers = new HeaderBag();
|
||||
$this->event = $this->createMock(RequestEvent::class);
|
||||
$this->subscriber = new ApiSubscriber(
|
||||
$this->coreParametersHelper,
|
||||
$this->translator
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnKernelRequestWhenNotMasterRequest(): void
|
||||
{
|
||||
$this->event->expects($this->once())
|
||||
->method('isMainRequest')
|
||||
->willReturn(false);
|
||||
|
||||
$this->coreParametersHelper->expects($this->never())
|
||||
->method('get');
|
||||
|
||||
$this->subscriber->onKernelRequest($this->event);
|
||||
}
|
||||
|
||||
public function testOnKernelRequestOnApiRequestWhenApiDisabled(): void
|
||||
{
|
||||
$this->event->expects($this->once())
|
||||
->method('isMainRequest')
|
||||
->willReturn(true);
|
||||
|
||||
$this->event->expects($this->once())
|
||||
->method('getRequest')
|
||||
->willReturn($this->request);
|
||||
|
||||
$this->request->expects($this->once())
|
||||
->method('getRequestUri')
|
||||
->willReturn('/api/endpoint');
|
||||
|
||||
$this->coreParametersHelper->expects($this->once())
|
||||
->method('get')
|
||||
->with('api_enabled')
|
||||
->willReturn(false);
|
||||
|
||||
$this->event->expects($this->once())
|
||||
->method('setResponse')
|
||||
->with($this->isInstanceOf(JsonResponse::class))
|
||||
->willReturnCallback(
|
||||
function (JsonResponse $response): void {
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
}
|
||||
);
|
||||
|
||||
$this->subscriber->onKernelRequest($this->event);
|
||||
}
|
||||
|
||||
public function testOnKernelRequestOnApiRequestWhenApiEnabled(): void
|
||||
{
|
||||
$this->event->expects($this->once())
|
||||
->method('isMainRequest')
|
||||
->willReturn(true);
|
||||
|
||||
$this->event->expects($this->once())
|
||||
->method('getRequest')
|
||||
->willReturn($this->request);
|
||||
|
||||
$this->request->expects($this->once())
|
||||
->method('getRequestUri')
|
||||
->willReturn('/api/endpoint');
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->coreParametersHelper->expects($matcher)
|
||||
->method('get')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('api_enabled', $parameters[0]);
|
||||
|
||||
return true;
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('api_enable_basic_auth', $parameters[0]);
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
$this->subscriber->onKernelRequest($this->event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ApiBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\ApiBundle\EventListener\ConfigSubscriber;
|
||||
use Mautic\ConfigBundle\Event\ConfigEvent;
|
||||
use Mautic\CoreBundle\Tests\CommonMocks;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
|
||||
class ConfigSubscriberTest extends CommonMocks
|
||||
{
|
||||
public function testWithUnsetApiBasicAuthSetting(): void
|
||||
{
|
||||
/**
|
||||
* We need a config array where api_enable_basic_auth is not set
|
||||
* (for example, in a hosted environment where customers are not allowed
|
||||
* to enable basic auth on the API). Saving the config shouldn't throw
|
||||
* any undefined notices/warnings in that case.
|
||||
*/
|
||||
$config = ['apiconfig' => []];
|
||||
|
||||
$subscriber = new ConfigSubscriber();
|
||||
$configEvent = new ConfigEvent($config, new ParameterBag());
|
||||
|
||||
$subscriber->onConfigSave($configEvent);
|
||||
|
||||
$this->assertEquals($config, $configEvent->getConfig());
|
||||
}
|
||||
|
||||
public function testWithIntegerApiBasicAuthSetting(): void
|
||||
{
|
||||
// Make sure the subscriber converts an integer value to boolean.
|
||||
$config = [
|
||||
'apiconfig' => [
|
||||
'api_enable_basic_auth' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
$fixedConfig = [
|
||||
'api_enable_basic_auth' => true,
|
||||
];
|
||||
|
||||
$subscriber = new ConfigSubscriber();
|
||||
$configEvent = new ConfigEvent($config, new ParameterBag());
|
||||
|
||||
$subscriber->onConfigSave($configEvent);
|
||||
|
||||
$this->assertEquals($fixedConfig, $configEvent->getConfig('apiconfig'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ApiBundle\Tests\Form\Type;
|
||||
|
||||
use Mautic\ApiBundle\Entity\oAuth2\Client;
|
||||
use Mautic\ApiBundle\Form\Type\ClientType;
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ClientTypeTest extends TestCase
|
||||
{
|
||||
private ClientType $clientType;
|
||||
|
||||
/**
|
||||
* @var MockObject&RequestStack
|
||||
*/
|
||||
private MockObject $requestStack;
|
||||
|
||||
/**
|
||||
* @var MockObject&TranslatorInterface
|
||||
*/
|
||||
private MockObject $translator;
|
||||
|
||||
/**
|
||||
* @var MockObject&ValidatorInterface
|
||||
*/
|
||||
private MockObject $validator;
|
||||
|
||||
/**
|
||||
* @var MockObject&RouterInterface
|
||||
*/
|
||||
private MockObject $router;
|
||||
|
||||
/**
|
||||
* @var MockObject&FormBuilderInterface
|
||||
*/
|
||||
private MockObject $builder;
|
||||
|
||||
/**
|
||||
* @var MockObject&Request
|
||||
*/
|
||||
private MockObject $request;
|
||||
|
||||
private Client $client;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->requestStack = $this->createMock(RequestStack::class);
|
||||
$this->translator = $this->createMock(TranslatorInterface::class);
|
||||
$this->validator = $this->createMock(ValidatorInterface::class);
|
||||
$this->router = $this->createMock(RouterInterface::class);
|
||||
$this->builder = $this->createMock(FormBuilderInterface::class);
|
||||
$this->request = $this->createMock(Request::class);
|
||||
$this->client = new Client();
|
||||
|
||||
$this->requestStack->expects($this->once())
|
||||
->method('getCurrentRequest')
|
||||
->willReturn($this->request);
|
||||
|
||||
$this->request->expects($this->once())
|
||||
->method('get')
|
||||
->with('api_mode', null);
|
||||
|
||||
$this->clientType = new ClientType(
|
||||
$this->requestStack,
|
||||
$this->translator,
|
||||
$this->validator,
|
||||
$this->router
|
||||
);
|
||||
}
|
||||
|
||||
public function testThatBuildFormCallsEventSubscribers(): void
|
||||
{
|
||||
$options = [
|
||||
'data' => $this->client,
|
||||
];
|
||||
|
||||
$this->builder->expects($this->any())
|
||||
->method('create')
|
||||
->willReturnSelf();
|
||||
|
||||
$cleanSubscriber = new CleanFormSubscriber([]);
|
||||
$formExitSubscriber = new FormExitSubscriber('api.client', $options);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->builder->expects($matcher)
|
||||
->method('addEventSubscriber')->willReturnCallback(function (...$parameters) use ($matcher, $cleanSubscriber, $formExitSubscriber) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals($cleanSubscriber, $parameters[0]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals($formExitSubscriber, $parameters[0]);
|
||||
}
|
||||
|
||||
return $this->builder;
|
||||
});
|
||||
|
||||
$this->clientType->buildForm($this->builder, $options);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ApiBundle\Tests\Helper;
|
||||
|
||||
use Mautic\ApiBundle\Helper\BatchIdToEntityHelper;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class BatchIdToEntityHelperTest extends TestCase
|
||||
{
|
||||
public function testIdsAreExtractedFromIdKeyArray(): void
|
||||
{
|
||||
$parameters = ['ids' => [1, 2, 3]];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$this->assertEquals([1, 2, 3], $helper->getIds());
|
||||
|
||||
$parameters = ['ids' => [1 => 1, 2 => 2, 3 => 3]];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$this->assertEquals([1, 2, 3], $helper->getIds());
|
||||
}
|
||||
|
||||
public function testIdsAreExtractedFromIdKeyCSVString(): void
|
||||
{
|
||||
$parameters = ['ids' => '1,2,3'];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$this->assertEquals([1, 2, 3], $helper->getIds());
|
||||
}
|
||||
|
||||
public function testIdIsExtractedFromIdKeyWithNumericValue(): void
|
||||
{
|
||||
$parameters = ['ids' => '12'];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$this->assertEquals([12], $helper->getIds());
|
||||
}
|
||||
|
||||
public function testErrorSetForIdKeyThatsNotRecognized(): void
|
||||
{
|
||||
$parameters = ['ids' => 'foo'];
|
||||
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$this->assertEquals([], $helper->getIds());
|
||||
$this->assertTrue($helper->hasErrors());
|
||||
$this->assertEquals(['mautic.api.call.id_missing'], $helper->getErrors());
|
||||
}
|
||||
|
||||
public function testIdsAreExtractedFromSimpleArray(): void
|
||||
{
|
||||
$parameters = [1, 2, 3];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$this->assertEquals([1, 2, 3], $helper->getIds());
|
||||
|
||||
$parameters = [1 => 1, 2 => 2, 3 => 3];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$this->assertEquals([1, 2, 3], $helper->getIds());
|
||||
}
|
||||
|
||||
public function testIdsAreExtractedFromAssociativeArray(): void
|
||||
{
|
||||
$parameters = [
|
||||
['id' => 1, 'foo' => 'bar'],
|
||||
['id' => 2, 'foo' => 'bar'],
|
||||
['id' => 3, 'foo' => 'bar'],
|
||||
];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$this->assertEquals([1, 2, 3], $helper->getIds());
|
||||
|
||||
$parameters = [
|
||||
1 => ['id' => 1, 'foo' => 'bar'],
|
||||
2 => ['id' => 2, 'foo' => 'bar'],
|
||||
3 => ['id' => 3, 'foo' => 'bar'],
|
||||
];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$this->assertEquals([1, 2, 3], $helper->getIds());
|
||||
}
|
||||
|
||||
public function testErrorsSetForAssociativeArrayWhenIdKeyIsNotFound(): void
|
||||
{
|
||||
$parameters = [
|
||||
['id' => 1, 'foo' => 'bar'],
|
||||
['foo' => 'bar'],
|
||||
['id' => 3, 'foo' => 'bar'],
|
||||
];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$this->assertEquals([1, 3], $helper->getIds());
|
||||
|
||||
$this->assertTrue($helper->hasErrors());
|
||||
$this->assertEquals([1 => 'mautic.api.call.id_missing'], $helper->getErrors());
|
||||
}
|
||||
|
||||
public function testOriginalKeyOrderingForIdKeyArray(): void
|
||||
{
|
||||
$entityMock1 = $this->createMock(Lead::class);
|
||||
$entityMock1->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
$entityMock2 = $this->createMock(Lead::class);
|
||||
$entityMock2->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
$entityMock4 = $this->createMock(Lead::class);
|
||||
$entityMock4->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(4);
|
||||
// Simulating ID 3 as not found
|
||||
$entities = [$entityMock4, $entityMock2, $entityMock1];
|
||||
|
||||
$parameters = ['ids' => [1, 2, 3, 4]];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$orderedEntities = $helper->orderByOriginalKey($entities);
|
||||
$this->assertEquals([0, 1, 2], array_keys($orderedEntities));
|
||||
|
||||
$parameters = ['ids' => [1 => 1, 2 => 2, 3 => 3, 4 => 4]];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$orderedEntities = $helper->orderByOriginalKey($entities);
|
||||
$this->assertEquals([1, 2, 4], array_keys($orderedEntities));
|
||||
}
|
||||
|
||||
public function testOriginalKeyOrderingForIdKeyCSVString(): void
|
||||
{
|
||||
$entityMock1 = $this->createMock(Lead::class);
|
||||
$entityMock1->expects($this->never())
|
||||
->method('getId');
|
||||
$entityMock2 = $this->createMock(Lead::class);
|
||||
$entityMock2->expects($this->never())
|
||||
->method('getId');
|
||||
$entityMock4 = $this->createMock(Lead::class);
|
||||
$entityMock4->expects($this->never())
|
||||
->method('getId');
|
||||
// Simulating ID 3 as not found
|
||||
$entities = [$entityMock4, $entityMock2, $entityMock1];
|
||||
|
||||
$parameters = ['ids' => '1,2,3,4'];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$orderedEntities = $helper->orderByOriginalKey($entities);
|
||||
$this->assertEquals([0, 1, 2], array_keys($orderedEntities));
|
||||
}
|
||||
|
||||
public function testOriginalKeyOrderingForSimpleArray(): void
|
||||
{
|
||||
$entityMock1 = $this->createMock(Lead::class);
|
||||
$entityMock1->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
$entityMock2 = $this->createMock(Lead::class);
|
||||
$entityMock2->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
$entityMock4 = $this->createMock(Lead::class);
|
||||
$entityMock4->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(4);
|
||||
// Simulating ID 3 as not found
|
||||
$entities = [$entityMock4, $entityMock2, $entityMock1];
|
||||
|
||||
$parameters = [1, 2, 3, 4];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$orderedEntities = $helper->orderByOriginalKey($entities);
|
||||
$this->assertEquals([0, 1, 2], array_keys($orderedEntities));
|
||||
|
||||
$parameters = [1 => 1, 2 => 2, 3 => 3, 4 => 4];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$orderedEntities = $helper->orderByOriginalKey($entities);
|
||||
$this->assertEquals([1, 2, 4], array_keys($orderedEntities));
|
||||
}
|
||||
|
||||
public function testOriginalKeyOrderingForAssociativeArray(): void
|
||||
{
|
||||
$entityMock1 = $this->createMock(Lead::class);
|
||||
$entityMock1->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
$entityMock2 = $this->createMock(Lead::class);
|
||||
$entityMock2->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
$entityMock4 = $this->createMock(Lead::class);
|
||||
$entityMock4->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn(4);
|
||||
// Simulating ID 3 as not found
|
||||
$entities = [$entityMock4, $entityMock2, $entityMock1];
|
||||
|
||||
$parameters = [
|
||||
['id' => 1, 'foo' => 'bar'],
|
||||
['id' => 2, 'foo' => 'bar'],
|
||||
['id' => 3, 'foo' => 'bar'],
|
||||
['id' => 4, 'foo' => 'bar'],
|
||||
];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$orderedEntities = $helper->orderByOriginalKey($entities);
|
||||
$this->assertEquals([0, 1, 2], array_keys($orderedEntities));
|
||||
|
||||
$parameters = [
|
||||
1 => ['id' => 1, 'foo' => 'bar'],
|
||||
2 => ['id' => 2, 'foo' => 'bar'],
|
||||
3 => ['id' => 3, 'foo' => 'bar'],
|
||||
4 => ['id' => 4, 'foo' => 'bar'],
|
||||
];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$orderedEntities = $helper->orderByOriginalKey($entities);
|
||||
$this->assertEquals([1, 2, 4], array_keys($orderedEntities));
|
||||
}
|
||||
|
||||
public function testOriginalKeyOrderingForFullAssociativeArray(): void
|
||||
{
|
||||
$entityMock1 = $this->createMock(Lead::class);
|
||||
$entityMock1
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
$entityMock2 = $this->createMock(Lead::class);
|
||||
$entityMock2
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
$entityMock3 = $this->createMock(Lead::class);
|
||||
$entityMock3
|
||||
->method('getId')
|
||||
->willReturn(3);
|
||||
$entityMock4 = $this->createMock(Lead::class);
|
||||
$entityMock4
|
||||
->method('getId')
|
||||
->willReturn(4);
|
||||
$entities = [$entityMock4, $entityMock2, $entityMock1, $entityMock3];
|
||||
|
||||
$parameters = [
|
||||
['id' => 1, 'foo' => 'bar'],
|
||||
['id' => 2, 'foo' => 'bar'],
|
||||
['id' => 3, 'foo' => 'bar'],
|
||||
['id' => 4, 'foo' => 'bar'],
|
||||
];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$orderedEntities = $helper->orderByOriginalKey($entities);
|
||||
$this->assertEquals([0, 1, 2, 3], array_keys($orderedEntities));
|
||||
foreach ($parameters as $key => $contact) {
|
||||
Assert::assertEquals($orderedEntities[$key]->getId(), $entities[$key]->getId());
|
||||
}
|
||||
|
||||
$parameters = [
|
||||
1 => ['id' => 1, 'foo' => 'bar'],
|
||||
2 => ['id' => 2, 'foo' => 'bar'],
|
||||
3 => ['id' => 3, 'foo' => 'bar'],
|
||||
4 => ['id' => 4, 'foo' => 'bar'],
|
||||
];
|
||||
$helper = new BatchIdToEntityHelper($parameters);
|
||||
$orderedEntities = $helper->orderByOriginalKey($entities);
|
||||
$this->assertEquals([1, 2, 3, 4], array_keys($orderedEntities));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ApiBundle\Tests\Helper;
|
||||
|
||||
use Mautic\ApiBundle\Helper\RequestHelper;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\HeaderBag;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class RequestHelperTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|Request
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $request;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->request = $this->createMock(Request::class);
|
||||
}
|
||||
|
||||
public function testIsBasicAuthWithValidBasicAuth(): void
|
||||
{
|
||||
$this->request->headers = new HeaderBag(['Authorization' => 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=']);
|
||||
|
||||
$this->assertTrue(RequestHelper::hasBasicAuth($this->request));
|
||||
}
|
||||
|
||||
public function testIsBasicAuthWithInvalidBasicAuth(): void
|
||||
{
|
||||
$this->request->headers = new HeaderBag(['Authorization' => 'Invalid Basic Auth value']);
|
||||
|
||||
$this->assertFalse(RequestHelper::hasBasicAuth($this->request));
|
||||
}
|
||||
|
||||
public function testIsBasicAuthWithMissingBasicAuth(): void
|
||||
{
|
||||
$this->request->headers = new HeaderBag([]);
|
||||
|
||||
$this->assertFalse(RequestHelper::hasBasicAuth($this->request));
|
||||
}
|
||||
|
||||
public function testIsApiRequestWithOauthUrl(): void
|
||||
{
|
||||
$this->request->expects($this->once())
|
||||
->method('getRequestUri')
|
||||
->willReturn('/oauth/v2/token');
|
||||
|
||||
$this->assertTrue(RequestHelper::isApiRequest($this->request));
|
||||
}
|
||||
|
||||
public function testIsApiRequestWithApiUrl(): void
|
||||
{
|
||||
$this->request->expects($this->once())
|
||||
->method('getRequestUri')
|
||||
->willReturn('/api/contacts');
|
||||
|
||||
$this->assertTrue(RequestHelper::isApiRequest($this->request));
|
||||
}
|
||||
|
||||
public function testIsNotApiRequest(): void
|
||||
{
|
||||
$this->request->expects($this->once())
|
||||
->method('getRequestUri')
|
||||
->willReturn('/s/dashboard');
|
||||
|
||||
$this->assertFalse(RequestHelper::isApiRequest($this->request));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user