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,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);
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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'));
}
}

View File

@@ -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);
}
}

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');
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}