diff --git a/CHANGELOG.md b/CHANGELOG.md index 890f3416..a20d4ec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Every change that reaches `main` adds a line under **Unreleased**. When a versio ## Unreleased +### Breaking +- Writing CWA content (pages, layouts, routes, component groups and their order, component positions, every component including `/move`, and every page data class, project classes included) requires `publishable.permission` (or the class's `#[Publishable(isGranted:)]`), ANDed with any `security` the operation already has; opt an operation or resource out with `extraProperties: ['publishable_permission' => false]`. Form `/submit` stays public ([#579](https://github.com/components-web-app/api-components-bundle/pull/579)) + ### Added - `emails.logo` (a Twig path, default the bundle's logo) and `emails.logo_width` (pixels, optional) set the logo at the top of every email the bundle sends, whose alt text is now the `website_name` instead of `Logo` ([#576](https://github.com/components-web-app/api-components-bundle/pull/576)) diff --git a/CLAUDE.md b/CLAUDE.md index dc00a0e8..8c1a3f4b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -294,6 +294,7 @@ Resources are fetched and cached **individually**. Never embed related data outs ### Security and users +- **Writing CWA content needs the publishable permission, whatever the application's access rules** (#569). `ContentWriteSecurityResourceMetadataCollectionFactory`, the outermost bundle metadata decorator (decoration priority -1, so it sees every operation the other bundle factories add, `/move` and uploads included), gives every `POST`/`PUT`/`PATCH`/`DELETE` operation of `Page`, `Layout`, `Route`, `ComponentGroup`, `ComponentGroupOrder`, `ComponentPosition` and every `AbstractComponent` and `AbstractPageData` subclass, project classes included, the `security` `publishable.permission`, or the class's own `#[Publishable(isGranted:)]` when it sets one (the permission `PublishableStatusChecker` resolves for the class). It goes in `security`, not `securityPostDenormalize`: the expression needs no object, and `security` runs before deserialization, so a refused write never reaches the processors. **An operation's own `security` is ANDed with it, never replaced and never an opt-out**: `Route`'s write operations carry `read_route` and `RoutableResourceMetadataCollectionFactory` gives page and page data writes `read_routable` (and `routable_security` on `POST`), which are read checks that granted any reader the write. **The opt-out is `extraProperties: [ContentWriteSecurityResourceMetadataCollectionFactory::OPT_OUT_EXTRA_PROPERTY => false]`** on an operation, or on `#[ApiResource]` for every operation of the resource; then the operation's own `security` (or none) is all that applies. Form `/submit` (`POST` and the validating `PATCH`) opts out, and the test bundle's `DummyPublishableComponent` does, so the publishable tests can still write as a user and anonymously. Reads are untouched. `ROLE_USER` never implies editing: a test that writes content logs in as an admin. `Security\ContentWriteSecurityTest` is the authority. - **A `PUT` on a user merges, never replaces** (#426). API Platform's `standard_put` defaults to `true` on 4.x and 5.x: the body is denormalized into a new object and every property is copied onto the stored one, including those nobody can write (password hash, tokens), so they took the constructor's defaults. `UserResourceMetadataCollectionFactory` sets `standard_put: false` on every `Put` of an `AbstractUser`, over any application default. `User\UserPutTest` pins it. - **`UserNormalizer` handles only a user payload (an array)** (#572). A user referenced by IRI from another resource's write reaches the serializer as a string; running `processChanges()` on it treated the stored user as a new registration and reset `emailAddressVerified`. `User\UserReferenceTest` pins it. - **A plain password is hashed only after validation, and never verifies the email address** (#570). `UserDataProcessor::processChanges()` does not hash: the API hashes in `UserPasswordStateProcessor` (on `state_processor.locator`, before the persist) and the form listener after `processChanges()`, so the `plainPassword` `Length` (in `Default` as well as `User:password:create`) sees the value on API writes; hashed during denormalization, it was already erased. Only a password reset, which proves the inbox, sets `emailAddressVerified`. `User\UserPasswordTest` pins it. diff --git a/src/ApiPlatform/Metadata/Resource/ContentWriteSecurityResourceMetadataCollectionFactory.php b/src/ApiPlatform/Metadata/Resource/ContentWriteSecurityResourceMetadataCollectionFactory.php new file mode 100644 index 00000000..27a4e941 --- /dev/null +++ b/src/ApiPlatform/Metadata/Resource/ContentWriteSecurityResourceMetadataCollectionFactory.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Silverback\ApiComponentsBundle\ApiResource\ComponentGroupOrder; +use Silverback\ApiComponentsBundle\AttributeReader\PublishableAttributeReader; +use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent; +use Silverback\ApiComponentsBundle\Entity\Core\AbstractPageData; +use Silverback\ApiComponentsBundle\Entity\Core\ComponentGroup; +use Silverback\ApiComponentsBundle\Entity\Core\ComponentPosition; +use Silverback\ApiComponentsBundle\Entity\Core\Layout; +use Silverback\ApiComponentsBundle\Entity\Core\Page; +use Silverback\ApiComponentsBundle\Entity\Core\Route; + +/** + * @author Daniel West + */ +final readonly class ContentWriteSecurityResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface +{ + public const string OPT_OUT_EXTRA_PROPERTY = 'publishable_permission'; + + private const array CONTENT_CLASSES = [ + Page::class, + Layout::class, + Route::class, + ComponentGroup::class, + ComponentGroupOrder::class, + ComponentPosition::class, + AbstractComponent::class, + AbstractPageData::class, + ]; + + private const array WRITE_METHODS = [ + HttpOperation::METHOD_POST, + HttpOperation::METHOD_PUT, + HttpOperation::METHOD_PATCH, + HttpOperation::METHOD_DELETE, + ]; + + public function __construct( + private ResourceMetadataCollectionFactoryInterface $decorated, + private PublishableAttributeReader $publishableAttributeReader, + private string $permission, + ) { + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + + if (!$this->isContent($resourceClass)) { + return $resourceMetadataCollection; + } + + $permission = $this->permission($resourceClass); + foreach ($resourceMetadataCollection as $i => $resource) { + $operations = $resource->getOperations(); + if (null === $operations) { + continue; + } + foreach ($operations as $key => $operation) { + if (!\in_array($operation->getMethod(), self::WRITE_METHODS, true) || false === ($operation->getExtraProperties()[self::OPT_OUT_EXTRA_PROPERTY] ?? true)) { + continue; + } + $operations->add($key, $operation->withSecurity(self::security($permission, $operation->getSecurity()))); + } + $resourceMetadataCollection[$i] = $resource->withOperations($operations); + } + + return $resourceMetadataCollection; + } + + private function isContent(string $resourceClass): bool + { + foreach (self::CONTENT_CLASSES as $contentClass) { + if (is_a($resourceClass, $contentClass, true)) { + return true; + } + } + + return false; + } + + private function permission(string $resourceClass): string + { + if (!$this->publishableAttributeReader->isConfigured($resourceClass)) { + return $this->permission; + } + + return $this->publishableAttributeReader->getConfiguration($resourceClass)->isGranted ?? $this->permission; + } + + private static function security(string $permission, ?string $security): string + { + if (null === $security || '' === $security) { + return $permission; + } + + return \sprintf('(%s) and (%s)', $permission, $security); + } +} diff --git a/src/DependencyInjection/SilverbackApiComponentsExtension.php b/src/DependencyInjection/SilverbackApiComponentsExtension.php index 94d1951c..383a06cd 100644 --- a/src/DependencyInjection/SilverbackApiComponentsExtension.php +++ b/src/DependencyInjection/SilverbackApiComponentsExtension.php @@ -13,6 +13,7 @@ use Doctrine\Persistence\ManagerRegistry; use Ramsey\Uuid\Doctrine\UuidType; +use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource\ContentWriteSecurityResourceMetadataCollectionFactory; use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource\RoutableResourceMetadataCollectionFactory; use Silverback\ApiComponentsBundle\AttributeReader\UploadableAttributeReader; use Silverback\ApiComponentsBundle\Command\ImagineGenerateCommand; @@ -277,6 +278,9 @@ public function load(array $configs, ContainerBuilder $container): void $definition = $container->findDefinition(RoutableResourceMetadataCollectionFactory::class); $definition->setArgument('$securityStr', $config['routable_security']); + $definition = $container->findDefinition(ContentWriteSecurityResourceMetadataCollectionFactory::class); + $definition->setArgument('$permission', $config['publishable']['permission']); + $definition = $container->findDefinition(MercureAuthorization::class); $definition->setArgument('$cookieSameSite', $config['mercure']['cookie']['samesite']); $definition->setArgument('$hubName', $config['mercure']['hub_name']); diff --git a/src/Entity/Component/Form.php b/src/Entity/Component/Form.php index 382dfb73..e07e665c 100644 --- a/src/Entity/Component/Form.php +++ b/src/Entity/Component/Form.php @@ -21,6 +21,7 @@ use ApiPlatform\Metadata\Put; use Doctrine\ORM\Mapping as ORM; use Silverback\ApiComponentsBundle\Annotation as Silverback; +use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource\ContentWriteSecurityResourceMetadataCollectionFactory; use Silverback\ApiComponentsBundle\DataProvider\StateProvider\FormStateProvider; use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent; use Silverback\ApiComponentsBundle\Entity\Utility\TimestampedTrait; @@ -52,6 +53,7 @@ write: false, serialize: true, requirements: ['id' => '[^/]+'], + extraProperties: [ContentWriteSecurityResourceMetadataCollectionFactory::OPT_OUT_EXTRA_PROPERTY => false], ), new Post( name: '_api_/forms/{id}/submit{._format}_post', @@ -62,6 +64,7 @@ write: false, serialize: true, requirements: ['id' => '[^/]+'], + extraProperties: [ContentWriteSecurityResourceMetadataCollectionFactory::OPT_OUT_EXTRA_PROPERTY => false], ), ] )] diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index 8f7b4c62..865ad6c2 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -34,6 +34,7 @@ use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Property\ComponentPropertyMetadataFactory; use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Property\ImagineFiltersPropertyMetadataFactory; use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource\ComponentResourceMetadataFactory; +use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource\ContentWriteSecurityResourceMetadataCollectionFactory; use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource\RoutableResourceMetadataCollectionFactory; use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource\RoutingPrefixResourceMetadataCollectionFactory; use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource\UploadableResourceMetadataCollectionFactory; @@ -2051,6 +2052,18 @@ ); $services->alias(UserResourceMetadataCollectionFactory::class, 'silverback.api_components.api_platform.metadata.resource.user_factory'); + $services + ->set('silverback.api_components.api_platform.metadata.resource.content_write_security_factory') + ->class(ContentWriteSecurityResourceMetadataCollectionFactory::class) + ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, -1) + ->args( + [ + new Reference('silverback.api_components.api_platform.metadata.resource.content_write_security_factory.inner'), + new Reference(PublishableAttributeReader::class), + ] + ); + $services->alias(ContentWriteSecurityResourceMetadataCollectionFactory::class, 'silverback.api_components.api_platform.metadata.resource.content_write_security_factory'); + $services ->set(VerifyEmailAddressAction::class) ->public() diff --git a/tests/ApiPlatform/Metadata/Resource/ContentWriteSecurityResourceMetadataCollectionFactoryTest.php b/tests/ApiPlatform/Metadata/Resource/ContentWriteSecurityResourceMetadataCollectionFactoryTest.php new file mode 100644 index 00000000..e27d7327 --- /dev/null +++ b/tests/ApiPlatform/Metadata/Resource/ContentWriteSecurityResourceMetadataCollectionFactoryTest.php @@ -0,0 +1,212 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silverback\ApiComponentsBundle\Tests\ApiPlatform\Metadata\Resource; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Doctrine\Persistence\ManagerRegistry; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource\ContentWriteSecurityResourceMetadataCollectionFactory; +use Silverback\ApiComponentsBundle\ApiResource\ComponentGroupOrder; +use Silverback\ApiComponentsBundle\ApiResource\OrphanedResourceReport; +use Silverback\ApiComponentsBundle\AttributeReader\PublishableAttributeReader; +use Silverback\ApiComponentsBundle\Entity\Component\Form; +use Silverback\ApiComponentsBundle\Entity\Core\CollaborationState; +use Silverback\ApiComponentsBundle\Entity\Core\ComponentGroup; +use Silverback\ApiComponentsBundle\Entity\Core\ComponentPosition; +use Silverback\ApiComponentsBundle\Entity\Core\Layout; +use Silverback\ApiComponentsBundle\Entity\Core\Page; +use Silverback\ApiComponentsBundle\Entity\Core\Route; +use Silverback\ApiComponentsBundle\Entity\Core\SiteConfigParameter; +use Silverback\ApiComponentsBundle\Tests\Functional\TestBundle\Entity\DummyComponent; +use Silverback\ApiComponentsBundle\Tests\Functional\TestBundle\Entity\DummyPublishableComponent; +use Silverback\ApiComponentsBundle\Tests\Functional\TestBundle\Entity\DummyPublishableWithSecurityGroups; +use Silverback\ApiComponentsBundle\Tests\Functional\TestBundle\Entity\PageData; +use Silverback\ApiComponentsBundle\Tests\Functional\TestBundle\Entity\User; + +final class ContentWriteSecurityResourceMetadataCollectionFactoryTest extends TestCase +{ + private const string PERMISSION = "is_granted('ROLE_ADMIN')"; + + #[DataProvider('contentClasses')] + public function test_every_write_operation_of_a_content_class_requires_the_permission_and_reads_are_unchanged(string $resourceClass): void + { + $collection = $this->factory(self::collection($resourceClass, self::operations()))->create($resourceClass); + + self::assertSame([ + 'get' => null, + 'get_collection' => null, + 'post' => self::PERMISSION, + 'put' => self::PERMISSION, + 'patch' => self::PERMISSION, + 'delete' => self::PERMISSION, + ], self::securities($collection)); + } + + public static function contentClasses(): iterable + { + yield 'a page' => [Page::class]; + yield 'a layout' => [Layout::class]; + yield 'a route' => [Route::class]; + yield 'a component group' => [ComponentGroup::class]; + yield 'a component group order' => [ComponentGroupOrder::class]; + yield 'a component position' => [ComponentPosition::class]; + yield 'a component' => [DummyComponent::class]; + yield 'a bundle component' => [Form::class]; + yield 'page data' => [PageData::class]; + yield 'a publishable component using the configured permission' => [DummyPublishableComponent::class]; + } + + #[DataProvider('otherClasses')] + public function test_a_class_that_is_not_content_is_unchanged(string $resourceClass): void + { + $collection = $this->factory(self::collection($resourceClass, self::operations()))->create($resourceClass); + + self::assertSame([ + 'get' => null, + 'get_collection' => null, + 'post' => null, + 'put' => null, + 'patch' => null, + 'delete' => null, + ], self::securities($collection)); + } + + public static function otherClasses(): iterable + { + yield 'a user' => [User::class]; + yield 'a collaboration state' => [CollaborationState::class]; + yield 'a site config parameter' => [SiteConfigParameter::class]; + yield 'an orphaned resource report' => [OrphanedResourceReport::class]; + } + + public function test_an_operation_with_its_own_security_requires_the_permission_as_well(): void + { + $collection = $this->factory(self::collection(Route::class, [ + 'patch' => new Patch(security: "is_granted('read_route', object)"), + 'get' => new Get(security: "is_granted('read_route', object)"), + ]))->create(Route::class); + + self::assertSame([ + 'patch' => "(is_granted('ROLE_ADMIN')) and (is_granted('read_route', object))", + 'get' => "is_granted('read_route', object)", + ], self::securities($collection)); + } + + public function test_a_publishable_class_with_its_own_permission_requires_that_permission_instead(): void + { + $collection = $this->factory(self::collection(DummyPublishableWithSecurityGroups::class, [ + 'patch' => new Patch(), + 'delete' => new Delete(security: "is_granted('something', object)"), + ]))->create(DummyPublishableWithSecurityGroups::class); + + self::assertSame([ + 'patch' => "is_granted('ROLE_USER')", + 'delete' => "(is_granted('ROLE_USER')) and (is_granted('something', object))", + ], self::securities($collection)); + } + + public function test_an_empty_security_is_replaced_by_the_permission(): void + { + $collection = $this->factory(self::collection(Page::class, ['patch' => new Patch(security: '')]))->create(Page::class); + + self::assertSame(['patch' => self::PERMISSION], self::securities($collection)); + } + + public function test_an_operation_that_opts_out_is_left_alone(): void + { + $collection = $this->factory(self::collection(Form::class, [ + 'submit' => new Post(extraProperties: [ContentWriteSecurityResourceMetadataCollectionFactory::OPT_OUT_EXTRA_PROPERTY => false]), + 'secured_submit' => new Patch(security: "is_granted('ROLE_USER')", extraProperties: [ContentWriteSecurityResourceMetadataCollectionFactory::OPT_OUT_EXTRA_PROPERTY => false]), + 'opted_in' => new Patch(extraProperties: [ContentWriteSecurityResourceMetadataCollectionFactory::OPT_OUT_EXTRA_PROPERTY => true]), + 'other_extra_property' => new Delete(extraProperties: ['something' => false]), + ]))->create(Form::class); + + self::assertSame([ + 'submit' => null, + 'secured_submit' => "is_granted('ROLE_USER')", + 'opted_in' => self::PERMISSION, + 'other_extra_property' => self::PERMISSION, + ], self::securities($collection)); + } + + public function test_every_resource_of_the_class_is_changed_and_one_without_operations_is_kept(): void + { + $collection = new ResourceMetadataCollection(Page::class, [ + (new ApiResource())->withOperations(new Operations(['first' => new Patch()])), + new ApiResource(), + (new ApiResource())->withOperations(new Operations(['second' => new Delete()])), + ]); + + $created = $this->factory($collection)->create(Page::class); + + self::assertCount(3, $created); + self::assertSame(['first' => self::PERMISSION], self::securities(new ResourceMetadataCollection(Page::class, [$created[0]]))); + self::assertNull($created[1]->getOperations()); + self::assertSame(['second' => self::PERMISSION], self::securities(new ResourceMetadataCollection(Page::class, [$created[2]]))); + } + + /** + * @return array + */ + private static function operations(): array + { + return [ + 'get' => new Get(), + 'get_collection' => new GetCollection(), + 'post' => new Post(), + 'put' => new Put(), + 'patch' => new Patch(), + 'delete' => new Delete(), + ]; + } + + /** + * @param array $operations + */ + private static function collection(string $resourceClass, array $operations): ResourceMetadataCollection + { + return new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withOperations(new Operations($operations))]); + } + + private function factory(ResourceMetadataCollection $collection): ContentWriteSecurityResourceMetadataCollectionFactory + { + $decorated = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $decorated->expects(self::once())->method('create')->willReturn($collection); + + return new ContentWriteSecurityResourceMetadataCollectionFactory($decorated, new PublishableAttributeReader($this->createStub(ManagerRegistry::class)), self::PERMISSION); + } + + /** + * @return array + */ + private static function securities(ResourceMetadataCollection $collection): array + { + $securities = []; + foreach ($collection as $resource) { + foreach ($resource->getOperations() ?? [] as $key => $operation) { + $securities[$key] = $operation->getSecurity(); + } + } + + return $securities; + } +} diff --git a/tests/Functional/Collection/CollectionTest.php b/tests/Functional/Collection/CollectionTest.php index d8f0d0f9..65a61334 100644 --- a/tests/Functional/Collection/CollectionTest.php +++ b/tests/Functional/Collection/CollectionTest.php @@ -34,7 +34,7 @@ final class CollectionTest extends FunctionalTestCase public function test_a_collection_component_can_be_created(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::requestJsonLd($client, 'POST', '/component/collections', '{"resourceIri": "/page_data/page_datas", "perPage": 20, "defaultQueryParameters": {"search": "something", "orderBy": "anotherThing"}}'); @@ -46,7 +46,7 @@ public function test_a_collection_component_can_be_created(): void public function test_a_collection_component_cannot_be_created_with_an_invalid_resource_iri(?string $resourceIri): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::requestJsonLd($client, 'POST', '/component/collections', ['resourceIri' => $resourceIri]); @@ -67,7 +67,7 @@ public static function invalidResourceIris(): iterable public function test_a_collection_component_can_be_deleted(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $collection = self::createCollection(); self::requestJsonLd($client, 'DELETE', self::iriOf($collection)); @@ -78,7 +78,7 @@ public function test_a_collection_component_can_be_deleted(): void public function test_the_collection_is_purged_from_the_cache_when_a_dummy_component_is_deleted(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $components = self::createDummyComponents(2); $collection = self::createCollection(); @@ -109,7 +109,7 @@ public function test_a_site_config_write_never_looks_up_collection_components_so public function test_a_collection_is_fetched_with_the_default_pagination_enabled(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::createDummyComponents(50); $collection = self::createCollection(); @@ -125,7 +125,7 @@ public function test_a_collection_is_fetched_with_the_default_pagination_enabled public function test_a_collection_component_is_created_with_per_page_configured(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::createDummyComponents(50); self::requestJsonLd($client, 'POST', '/component/collections', '{"resourceIri": "/component/dummy_components", "perPage": 3}'); @@ -138,7 +138,7 @@ public function test_a_collection_component_is_created_with_per_page_configured( public function test_the_pagination_parameter_is_configured(int $total, string $postfix): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::createDummyResourcesWithPagination(120); self::requestJsonLd($client, 'GET', '/dummy_resource_with_paginations' . $postfix); @@ -151,7 +151,7 @@ public function test_the_pagination_parameter_is_configured(int $total, string $ public function test_component_pagination_can_be_configured(int $total, string $postfix): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::createDummyResourcesWithPagination(120); $collection = self::createCollection('/dummy_resource_with_paginations'); @@ -177,7 +177,7 @@ public static function paginationParameters(): iterable public function test_a_resource_can_be_searched_directly_without_a_collection(int $total, string $postfix): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::createDummyResourcesWithFilters(80); self::requestJsonLd($client, 'GET', '/dummy_resource_with_filters' . $postfix); @@ -203,7 +203,7 @@ public static function directFilters(): iterable public function test_a_collection_has_default_query_string_parameters_and_filters(int $total, string $postfix): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::createDummyResourcesWithFilters(80); $collection = self::createCollection('/dummy_resource_with_filters', ['perPage' => 40, 'reference' => '1']); diff --git a/tests/Functional/Component/ComponentCloneTest.php b/tests/Functional/Component/ComponentCloneTest.php index e5185431..dd48f7e1 100644 --- a/tests/Functional/Component/ComponentCloneTest.php +++ b/tests/Functional/Component/ComponentCloneTest.php @@ -29,7 +29,7 @@ final class ComponentCloneTest extends FunctionalTestCase public function test_a_component_placed_in_another_group_is_cloned_by_its_iri_without_a_copy(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $source = self::createGroupWithComponentIris(1, 'source'); $target = self::createGroupWithComponentIris(1); $component = $source['components'][0]; @@ -46,7 +46,7 @@ public function test_a_component_placed_in_another_group_is_cloned_by_its_iri_wi #[DataProvider('clonePlacements')] public function test_a_clone_is_placed_at_its_anchor(string $anchor, string $at, string $expected): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $source = self::createGroupWithComponentIris(1, 'source'); $names = self::createGroupWithPositions('A0 B1 C10'); @@ -67,7 +67,7 @@ public static function clonePlacements(): iterable public function test_a_clone_of_a_type_the_target_group_does_not_allow_is_a_422(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $source = self::createGroupWithComponentIris(1, 'source'); $target = self::createGroupWithComponentIris(0); self::setAllowedComponent($target['group'], '/component/restricted_components'); @@ -82,7 +82,7 @@ public function test_a_clone_of_a_type_the_target_group_does_not_allow_is_a_422( public function test_a_clone_of_an_explicit_allow_only_type_into_a_group_that_does_not_list_it_is_a_422(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $source = self::createGroupWithComponentIris(0, 'source'); self::setAllowedComponent($source['group'], '/component/restricted_components'); $component = self::componentOf(self::addRestrictedComponentPosition($source['group'], 0)); @@ -98,7 +98,7 @@ public function test_a_clone_of_an_explicit_allow_only_type_into_a_group_that_do public function test_a_clone_of_an_explicit_allow_only_type_into_a_group_that_lists_it_is_created(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $source = self::createGroupWithComponentIris(0, 'source'); self::setAllowedComponent($source['group'], '/component/restricted_components'); $component = self::componentOf(self::addRestrictedComponentPosition($source['group'], 0)); @@ -114,7 +114,7 @@ public function test_a_clone_of_an_explicit_allow_only_type_into_a_group_that_li public function test_a_clone_into_a_group_at_its_maximum_is_a_422(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $source = self::createGroupWithComponentIris(1, 'source'); $target = self::createGroupWithComponentIris(1); self::setMaxComponents($target['group'], 1); @@ -129,7 +129,7 @@ public function test_a_clone_into_a_group_at_its_maximum_is_a_422(): void public function test_the_same_component_can_be_placed_twice_in_one_group(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(1); $component = $group['components'][0]; @@ -144,7 +144,7 @@ public function test_the_same_component_can_be_placed_twice_in_one_group(): void public function test_a_published_component_with_a_draft_is_cloned_by_its_iri_without_a_copy(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $source = self::createGroupWithComponentIris(0, 'source'); $pair = self::addPublishedComponentWithDraftTo($source['group']); $target = self::createGroupWithComponentIris(0); @@ -194,10 +194,10 @@ public function test_a_component_placed_in_a_group_is_cloned_into_a_page_data_pr self::assertComponentIsIn([$source['group']], $component); } - private static function loggedInClient(): Client + private static function adminClient(): Client { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); return $client; } diff --git a/tests/Functional/Component/ComponentGroupMaxComponentsTest.php b/tests/Functional/Component/ComponentGroupMaxComponentsTest.php index 786f669c..f8d8bc3f 100644 --- a/tests/Functional/Component/ComponentGroupMaxComponentsTest.php +++ b/tests/Functional/Component/ComponentGroupMaxComponentsTest.php @@ -28,7 +28,7 @@ final class ComponentGroupMaxComponentsTest extends FunctionalTestCase public function test_a_component_group_is_created_with_a_maximum_number_of_components(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::requestJsonLd($client, 'POST', '/_/component_groups', '{"reference": "header_button", "location": "header", "maxComponents": 1}'); @@ -64,7 +64,7 @@ public function test_a_component_group_with_no_maximum_has_a_null_max_components public function test_the_maximum_number_of_components_must_be_a_positive_integer_or_null(string $maxComponents, int $status): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(0); self::requestJsonLd($client, 'PATCH', $group['group'], \sprintf('{"maxComponents": %s}', $maxComponents)); @@ -83,7 +83,7 @@ public static function maxComponentsValues(): iterable public function test_a_component_cannot_be_added_to_a_full_component_group(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(1); self::setMaxComponents($group['group'], 1); $component = self::createDummyComponent(); @@ -99,7 +99,7 @@ public function test_a_component_cannot_be_added_to_a_full_component_group(): vo public function test_a_component_can_be_added_to_a_component_group_below_its_maximum(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(1); self::setMaxComponents($group['group'], 2); $component = self::createDummyComponent(); @@ -112,7 +112,7 @@ public function test_a_component_can_be_added_to_a_component_group_below_its_max public function test_a_component_group_with_no_maximum_takes_any_number_of_components(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(5); $component = self::createDummyComponent(); @@ -124,7 +124,7 @@ public function test_a_component_group_with_no_maximum_takes_any_number_of_compo public function test_a_dynamic_page_data_position_cannot_be_added_to_a_full_component_group(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(1); self::setMaxComponents($group['group'], 1); @@ -141,7 +141,7 @@ public function test_a_dynamic_page_data_position_cannot_be_added_to_a_full_comp public function test_a_dynamic_page_data_position_counts_towards_the_maximum(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(0); self::addPositionTo($group['group'], null, 0, 'component'); self::setMaxComponents($group['group'], 1); @@ -155,7 +155,7 @@ public function test_a_dynamic_page_data_position_counts_towards_the_maximum(): public function test_a_published_component_with_a_draft_counts_once(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(0); self::addPublishedComponentWithDraftTo($group['group']); self::setMaxComponents($group['group'], 2); @@ -169,7 +169,7 @@ public function test_a_published_component_with_a_draft_counts_once(): void public function test_a_position_hidden_by_the_allowed_components_does_not_count_towards_the_maximum(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(1); self::addRestrictedComponentPosition($group['group'], 5); self::setAllowedComponent($group['group'], '/component/dummy_components'); @@ -184,7 +184,7 @@ public function test_a_position_hidden_by_the_allowed_components_does_not_count_ public function test_a_component_cannot_be_created_straight_into_a_full_component_group(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(1); self::setMaxComponents($group['group'], 1); @@ -199,7 +199,7 @@ public function test_a_component_cannot_be_created_straight_into_a_full_componen public function test_a_component_can_be_created_straight_into_a_component_group_below_its_maximum(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(1); self::setMaxComponents($group['group'], 2); @@ -212,7 +212,7 @@ public function test_a_component_can_be_created_straight_into_a_component_group_ public function test_a_component_in_a_full_component_group_can_still_be_edited(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(1); self::setMaxComponents($group['group'], 1); @@ -225,7 +225,7 @@ public function test_a_component_in_a_full_component_group_can_still_be_edited() public function test_a_position_cannot_be_moved_into_a_full_component_group(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(1); self::setMaxComponents($group['group'], 1); $other = self::createGroupWithComponentIris(1, 'other_collection'); @@ -239,7 +239,7 @@ public function test_a_position_cannot_be_moved_into_a_full_component_group(): v public function test_a_position_can_be_moved_into_a_component_group_below_its_maximum(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(1); self::setMaxComponents($group['group'], 2); $other = self::createGroupWithComponentIris(1, 'other_collection'); @@ -252,7 +252,7 @@ public function test_a_position_can_be_moved_into_a_component_group_below_its_ma public function test_a_position_can_be_moved_within_a_full_component_group(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(2); self::setMaxComponents($group['group'], 2); @@ -265,7 +265,7 @@ public function test_a_position_can_be_moved_within_a_full_component_group(): vo public function test_lowering_the_maximum_below_the_components_a_group_holds_keeps_them_all(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(3); self::requestJsonLd($client, 'PATCH', $group['group'], '{"maxComponents": 1}'); @@ -280,7 +280,7 @@ public function test_lowering_the_maximum_below_the_components_a_group_holds_kee public function test_a_group_holding_more_components_than_its_maximum_takes_no_more(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(3); self::setMaxComponents($group['group'], 1); @@ -293,7 +293,7 @@ public function test_a_group_holding_more_components_than_its_maximum_takes_no_m public function test_a_position_in_a_group_over_its_maximum_can_still_be_moved_within_it(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(3); self::setMaxComponents($group['group'], 1); diff --git a/tests/Functional/Component/ComponentGroupOrderTest.php b/tests/Functional/Component/ComponentGroupOrderTest.php index 38c5629e..2ce7fdb9 100644 --- a/tests/Functional/Component/ComponentGroupOrderTest.php +++ b/tests/Functional/Component/ComponentGroupOrderTest.php @@ -31,7 +31,7 @@ final class ComponentGroupOrderTest extends FunctionalTestCase public function test_an_insert_after_a_position_with_room_after_it_changes_no_other_position(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10 C20'); $component = self::createDummyComponent(); @@ -45,7 +45,7 @@ public function test_an_insert_after_a_position_with_room_after_it_changes_no_ot public function test_an_insert_after_a_position_with_no_room_after_it_moves_the_positions_after_it_along_by_one(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1 C2'); $component = self::createDummyComponent(); @@ -58,7 +58,7 @@ public function test_an_insert_after_a_position_with_no_room_after_it_moves_the_ public function test_an_insert_moves_only_the_positions_it_runs_into(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1 C2 D10'); $component = self::createDummyComponent(); @@ -71,7 +71,7 @@ public function test_an_insert_moves_only_the_positions_it_runs_into(): void public function test_an_insert_before_the_first_position_starts_the_group_at_0(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1'); $component = self::createDummyComponent(); @@ -84,7 +84,7 @@ public function test_an_insert_before_the_first_position_starts_the_group_at_0() public function test_an_insert_with_no_anchor_goes_at_the_end(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); $component = self::createDummyComponent(); @@ -97,7 +97,7 @@ public function test_an_insert_with_no_anchor_goes_at_the_end(): void public function test_an_insert_after_the_first_of_two_positions_sharing_a_value_removes_the_tie(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A5 B5'); $component = self::createDummyComponent(); @@ -110,7 +110,7 @@ public function test_an_insert_after_the_first_of_two_positions_sharing_a_value_ public function test_an_insert_before_the_second_of_two_positions_sharing_a_value_removes_the_tie(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A5 B5'); $component = self::createDummyComponent(); @@ -124,7 +124,7 @@ public function test_an_insert_before_the_second_of_two_positions_sharing_a_valu public function test_an_insert_from_a_client_that_has_not_seen_another_editors_move_still_goes_after_its_anchor(): void { $client = static::createClient(); - $user = self::loginAsUser($client); + $user = self::loginAsAdmin($client); $names = self::createGroupWithPositions('A0 B10 C20'); self::sendOrder($client, $names['group'], [$names['A'], $names['C'], $names['B']]); @@ -144,7 +144,7 @@ public function test_an_insert_from_a_client_that_has_not_seen_another_editors_m #[DataProvider('anchorsNotInTheGroup')] public function test_an_anchor_that_is_not_in_the_group_is_a_422(string $anchor, string $position): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); $other = self::createGroupWithPositions('X0', 'other_collection'); self::deletePosition($names['B']); @@ -169,7 +169,7 @@ public static function anchorsNotInTheGroup(): iterable #[DataProvider('anchorsThatAreNotPositions')] public function test_an_anchor_that_does_not_resolve_to_a_position_is_a_422_never_ignored(string $anchor, ?string $iri): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); $component = self::createDummyComponent(); @@ -191,7 +191,7 @@ public static function anchorsThatAreNotPositions(): iterable #[DataProvider('anchors')] public function test_an_anchor_sent_with_a_sort_value_is_a_422(string $anchor): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); $component = self::createDummyComponent(); @@ -211,7 +211,7 @@ public static function anchors(): iterable public function test_an_insert_with_both_anchors_is_a_422_on_before(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); $component = self::createDummyComponent(); @@ -224,7 +224,7 @@ public function test_an_insert_with_both_anchors_is_a_422_on_before(): void public function test_a_sort_value_sent_without_an_anchor_is_ignored(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); $component = self::createDummyComponent(); @@ -238,7 +238,7 @@ public function test_a_sort_value_sent_without_an_anchor_is_ignored(): void public function test_a_sort_value_in_a_patch_is_ignored(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); self::requestJsonLd($client, 'PATCH', $names['A'], '{"sortValue": 20}'); @@ -250,7 +250,7 @@ public function test_a_sort_value_in_a_patch_is_ignored(): void public function test_an_anchored_insert_into_a_full_group_is_a_422(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0'); self::setMaxComponents($names['group'], 1); $component = self::createDummyComponent(); @@ -265,7 +265,7 @@ public function test_an_anchored_insert_into_a_full_group_is_a_422(): void public function test_a_component_created_straight_into_a_group_is_placed_after_its_anchor(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1'); self::requestJsonLd($client, 'POST', '/component/dummy_components', ['componentPositions' => [['componentGroup' => $names['group'], 'after' => $names['A']]]]); @@ -277,7 +277,7 @@ public function test_a_component_created_straight_into_a_group_is_placed_after_i public function test_a_component_created_straight_into_a_group_with_an_anchor_from_another_group_is_a_422_on_the_embedded_anchor(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1'); $other = self::createGroupWithPositions('X0', 'other_collection'); @@ -290,7 +290,7 @@ public function test_a_component_created_straight_into_a_group_with_an_anchor_fr public function test_a_component_created_straight_into_a_group_with_an_anchor_and_a_sort_value_is_a_422_on_the_embedded_anchor(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1'); self::requestJsonLd($client, 'POST', '/component/dummy_components', ['componentPositions' => [['componentGroup' => $names['group'], 'after' => $names['A'], 'sortValue' => 1]]]); @@ -302,7 +302,7 @@ public function test_a_component_created_straight_into_a_group_with_an_anchor_an public function test_after_an_insert_the_group_lists_its_positions_by_sort_value_including_the_new_one(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1'); $component = self::createDummyComponent(); @@ -329,7 +329,7 @@ public function test_positions_sharing_a_sort_value_are_listed_by_id(): void public function test_a_reorder_changes_the_fewest_positions(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10 C20'); self::sendOrder($client, $names['group'], [$names['B'], $names['A'], $names['C']]); @@ -341,7 +341,7 @@ public function test_a_reorder_changes_the_fewest_positions(): void public function test_a_reorder_never_gives_a_value_below_0(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1 C2'); self::sendOrder($client, $names['group'], [$names['C'], $names['A'], $names['B']]); @@ -353,7 +353,7 @@ public function test_a_reorder_never_gives_a_value_below_0(): void public function test_a_reorder_removes_ties(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A5 B5 C5'); self::sendOrder($client, $names['group'], [$names['A'], $names['B'], $names['C']]); @@ -364,7 +364,7 @@ public function test_a_reorder_removes_ties(): void public function test_a_position_hidden_by_allowed_components_follows_its_current_predecessor(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 H10 B20'); self::holdRestrictedComponent($names['H']); self::setAllowedComponent($names['group'], '/component/dummy_components'); @@ -378,7 +378,7 @@ public function test_a_position_hidden_by_allowed_components_follows_its_current public function test_a_position_added_since_the_client_loaded_the_group_follows_its_current_predecessor(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10 N15'); self::sendOrder($client, $names['group'], [$names['B'], $names['A']]); @@ -391,7 +391,7 @@ public function test_a_position_added_since_the_client_loaded_the_group_follows_ public function test_a_position_another_request_added_between_the_clients_read_and_its_reorder_is_in_the_reorder_response(): void { $client = static::createClient(); - $user = self::loginAsUser($client); + $user = self::loginAsAdmin($client); $names = self::createGroupWithPositions('A0 B10'); self::requestJsonLd($client, 'GET', $names['group'] . '/order'); @@ -414,7 +414,7 @@ public function test_a_position_another_request_added_between_the_clients_read_a public function test_a_position_deleted_since_the_client_loaded_the_group_is_ignored(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10 C20'); self::deletePosition($names['C']); @@ -431,7 +431,7 @@ public function test_a_position_deleted_since_the_client_loaded_the_group_is_ign #[DataProvider('invalidOrders')] public function test_an_order_naming_a_position_outside_the_group_or_one_position_twice_is_a_422(array $order, int $index, string $message): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); $other = self::createGroupWithPositions('X0', 'other_collection'); $iris = array_map(static fn (string $name): string => ($names + $other)[$name] ?? $name, $order); @@ -454,7 +454,7 @@ public static function invalidOrders(): iterable public function test_every_invalid_entry_of_an_order_is_a_violation_of_its_own(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); $other = self::createGroupWithPositions('X0', 'other_collection'); @@ -470,7 +470,7 @@ public function test_every_invalid_entry_of_an_order_is_a_violation_of_its_own() public function test_the_order_carries_the_cache_tags_of_its_group_and_every_listed_position_so_a_position_write_purges_it(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1 C2'); self::requestJsonLd($client, 'GET', $names['group'] . '/order'); @@ -502,7 +502,7 @@ public function test_the_order_is_the_request_data_for_the_listeners_that_read_i public function test_the_reorder_response_is_what_a_fresh_read_of_the_order_returns(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10 C20'); self::sendOrder($client, $names['group'], [$names['C'], $names['A'], $names['B']]); @@ -600,7 +600,7 @@ public function test_the_order_of_a_group_that_does_not_exist_is_a_404(): void public function test_a_reorder_ignores_the_maximum_number_of_components(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1 C2'); self::setMaxComponents($names['group'], 1); @@ -612,7 +612,7 @@ public function test_a_reorder_ignores_the_maximum_number_of_components(): void public function test_a_position_moved_into_another_group_after_an_anchor_is_placed_after_it(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1'); $other = self::createGroupWithPositions('X0 Y1', 'other_collection'); @@ -626,7 +626,7 @@ public function test_a_position_moved_into_another_group_after_an_anchor_is_plac public function test_a_position_moved_into_another_group_with_no_anchor_goes_at_the_end(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); $other = self::createGroupWithPositions('X0 Y1', 'other_collection'); @@ -640,7 +640,7 @@ public function test_a_position_moved_into_another_group_with_no_anchor_goes_at_ public function test_a_position_moved_within_its_group_by_an_anchor_is_placed_by_the_same_rule_as_a_reorder(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1 C2'); self::requestJsonLd($client, 'PATCH', $names['C'], ['before' => $names['A']]); @@ -651,7 +651,7 @@ public function test_a_position_moved_within_its_group_by_an_anchor_is_placed_by public function test_a_position_cannot_be_placed_relative_to_itself(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1'); self::requestJsonLd($client, 'PATCH', $names['A'], ['after' => $names['A']]); @@ -674,7 +674,7 @@ public function test_an_anonymous_user_cannot_reorder_a_group(): void public function test_a_reorder_of_a_group_that_does_not_exist_is_a_404(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); self::requestJsonLd($client, 'PATCH', '/_/component_groups/not-a-group/order', '{"componentPositions": []}'); @@ -683,7 +683,7 @@ public function test_a_reorder_of_a_group_that_does_not_exist_is_a_404(): void public function test_a_reorder_is_a_merge_patch(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); self::requestJsonLd($client, 'PATCH', '/_/component_groups/collection/order', ['componentPositions' => [$names['B'], $names['A']]]); @@ -695,7 +695,7 @@ public function test_a_reorder_is_a_merge_patch(): void public function test_a_reorder_sent_as_json_ld_is_refused(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); self::requestJsonLd($client, 'PATCH', '/_/component_groups/collection/order', '{"componentPositions": []}', ['content-type' => 'application/ld+json']); @@ -708,7 +708,7 @@ public function test_the_order_is_the_groups_own_iri_with_order_appended_under_t { static::bootKernel(['routePrefix' => '/_api']); $client = static::createClient(); - $user = self::loginAsUser($client); + $user = self::loginAsAdmin($client); $names = self::createGroupWithPositions('A0 B10'); self::requestJsonLd($client, 'GET', '/_api/_/component_groups/collection'); @@ -730,7 +730,7 @@ public function test_the_order_is_the_groups_own_iri_with_order_appended_under_t public function test_the_sort_values_and_component_group_of_a_reorder_body_are_read_only_and_change_nothing(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B10'); $other = self::createGroupWithPositions('X0', 'other_collection'); @@ -747,10 +747,10 @@ public function test_the_sort_values_and_component_group_of_a_reorder_body_are_r self::assertGroupHoldsPositions($other['group'], 'X0', $other); } - private static function loggedInClient(): Client + private static function adminClient(): Client { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); return $client; } diff --git a/tests/Functional/Component/ComponentGroupsTest.php b/tests/Functional/Component/ComponentGroupsTest.php index 61516b58..f906d7be 100644 --- a/tests/Functional/Component/ComponentGroupsTest.php +++ b/tests/Functional/Component/ComponentGroupsTest.php @@ -29,7 +29,7 @@ final class ComponentGroupsTest extends FunctionalTestCase public function test_i_cannot_add_a_component_collection_without_a_reference(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::requestJsonLd($client, 'POST', '/_/component_groups', '{"reference": ""}'); @@ -40,7 +40,7 @@ public function test_i_cannot_add_a_component_collection_without_a_reference(): public function test_i_can_create_a_component_collection_resource(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::requestJsonLd($client, 'POST', '/_/component_groups', '{"reference": "collection_reference", "location": "collection_location"}'); @@ -52,7 +52,7 @@ public function test_i_can_create_a_component_collection_resource(): void public function test_when_i_delete_a_component_collection_resource_locations_are_also_deleted_but_components_are_not(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(4); self::requestJsonLd($client, 'DELETE', $group['group']); @@ -65,7 +65,7 @@ public function test_when_i_delete_a_component_collection_resource_locations_are public function test_i_can_get_a_component_group_by_reference(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(0); self::requestJsonLd($client, 'GET', '/_/component_groups/collection'); @@ -198,7 +198,7 @@ public function test_linking_an_existing_component_group_to_a_layout_via_patch_p public function test_a_page_data_property_position_is_not_populated_when_the_resolved_component_type_is_not_in_allowed_components(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $publishable = new DummyPublishableComponent(); $publishable->setPublishedAt(new \DateTime()); $position = self::createPageDataPropertyPositionInARestrictedGroup('/test-page', 'publishableComponent', $publishable); diff --git a/tests/Functional/Component/ComponentMoveTest.php b/tests/Functional/Component/ComponentMoveTest.php index 41d5b8d2..6c496c91 100644 --- a/tests/Functional/Component/ComponentMoveTest.php +++ b/tests/Functional/Component/ComponentMoveTest.php @@ -496,10 +496,9 @@ public function test_an_anonymous_caller_cannot_move_a_component(): void self::assertGroupHoldsPositions($names['group'], 'A0', $names); } - public function test_a_signed_in_user_granted_the_position_write_can_move_a_position(): void + public function test_an_admin_granted_the_position_write_can_move_a_position(): void { - $client = static::createClient(); - self::loginAsUser($client); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0'); $other = self::createGroupWithPositions('X0', 'other_collection'); @@ -509,7 +508,7 @@ public function test_a_signed_in_user_granted_the_position_write_can_move_a_posi self::assertGroupHoldsPositions($other['group'], 'X0 A1', $other + ['A' => $names['A']]); } - public function test_a_signed_in_user_not_granted_the_page_data_write_cannot_move_into_its_slot(): void + public function test_a_signed_in_user_without_the_publishable_permission_cannot_move_into_a_page_data_slot(): void { $client = static::createClient(); self::loginAsUser($client); @@ -524,7 +523,7 @@ public function test_a_signed_in_user_not_granted_the_page_data_write_cannot_mov self::assertSlotHolds($site['pageData'], 'component', null); } - public function test_a_signed_in_user_not_granted_the_page_data_write_cannot_move_out_of_its_slot(): void + public function test_a_signed_in_user_without_the_publishable_permission_cannot_move_out_of_a_page_data_slot(): void { $client = static::createClient(); self::loginAsUser($client); @@ -538,10 +537,9 @@ public function test_a_signed_in_user_not_granted_the_page_data_write_cannot_mov self::assertGroupHoldsPositions($names['group'], 'A0', $names); } - public function test_a_signed_in_user_granted_the_page_data_write_can_move_into_its_slot(): void + public function test_an_admin_granted_the_page_data_write_can_move_into_its_slot(): void { - $client = static::createClient(); - self::loginAsUser($client); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0'); $site = self::pageDataSite(); self::emptySlot($site['pageData'], 'component'); diff --git a/tests/Functional/Component/ComponentPositionCycleTest.php b/tests/Functional/Component/ComponentPositionCycleTest.php index 0e354877..0c17d1e7 100644 --- a/tests/Functional/Component/ComponentPositionCycleTest.php +++ b/tests/Functional/Component/ComponentPositionCycleTest.php @@ -27,7 +27,7 @@ final class ComponentPositionCycleTest extends FunctionalTestCase public function test_a_component_cannot_be_placed_in_one_of_its_own_component_groups(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $site = self::createNestedSite(); self::postPosition($client, ['componentGroup' => $site['nested']['group'], 'component' => $site['component']]); @@ -39,7 +39,7 @@ public function test_a_component_cannot_be_placed_in_one_of_its_own_component_gr public function test_a_component_cannot_be_placed_in_a_component_group_further_down_its_own_subtree(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $site = self::createNestedSite(); self::postPosition($client, ['componentGroup' => $site['deeper']['group'], 'component' => $site['component']]); @@ -50,7 +50,7 @@ public function test_a_component_cannot_be_placed_in_a_component_group_further_d public function test_the_same_component_can_be_placed_in_two_sibling_containers(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $site = self::createNestedSite(); $sibling = self::addGroupHoldingAComponent($site['page'], 'sibling'); $siblingInner = self::addGroupHoldingAComponent($sibling['component'], 'sibling_inner'); @@ -62,7 +62,7 @@ public function test_the_same_component_can_be_placed_in_two_sibling_containers( public function test_a_position_cannot_be_changed_to_hold_a_component_whose_subtree_holds_the_position(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $site = self::createNestedSite(); self::requestJsonLd($client, 'PATCH', $site['deeper']['position'], ['component' => $site['component']]); @@ -73,7 +73,7 @@ public function test_a_position_cannot_be_changed_to_hold_a_component_whose_subt public function test_a_position_cannot_be_moved_into_a_component_group_inside_the_component_it_holds(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $site = self::createNestedSite(); self::requestJsonLd($client, 'PATCH', $site['position'], ['componentGroup' => $site['deeper']['group']]); @@ -84,7 +84,7 @@ public function test_a_position_cannot_be_moved_into_a_component_group_inside_th public function test_a_component_group_cannot_be_given_an_owner_from_inside_its_own_subtree(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $site = self::createNestedSite(); self::requestJsonLd($client, 'PATCH', $site['group'], ['components' => [$site['nested']['component']]]); @@ -96,7 +96,7 @@ public function test_a_component_group_cannot_be_given_an_owner_from_inside_its_ public function test_a_component_group_can_be_given_an_owner_from_outside_its_subtree(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $site = self::createNestedSite(); $sibling = self::addGroupHoldingAComponent($site['page'], 'sibling'); @@ -107,7 +107,7 @@ public function test_a_component_group_can_be_given_an_owner_from_outside_its_su public function test_a_component_cannot_be_written_with_a_position_inside_its_own_subtree(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $site = self::createNestedSite(); self::requestJsonLd($client, 'PATCH', $site['component'], ['componentPositions' => [['componentGroup' => $site['deeper']['group']]]]); @@ -240,10 +240,10 @@ private static function createRoutedPageWhoseGroupHoldsAPublishedComponentWithAD return ['published' => self::iriOf($published), 'draft' => self::iriOf($draft)]; } - private static function loggedInClient(): Client + private static function adminClient(): Client { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); return $client; } diff --git a/tests/Functional/Component/ComponentPositionTest.php b/tests/Functional/Component/ComponentPositionTest.php index 29b72010..7f169d64 100644 --- a/tests/Functional/Component/ComponentPositionTest.php +++ b/tests/Functional/Component/ComponentPositionTest.php @@ -27,7 +27,7 @@ final class ComponentPositionTest extends FunctionalTestCase public function test_create_a_new_component_position_resource(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); $component = self::createDummyComponent(); @@ -55,7 +55,7 @@ public function test_sort_collection_is_not_included_in_a_component_position_res #[DataProvider('restrictedComponentPlacements')] public function test_i_can_restrict_which_components_are_permitted_to_be_inside_a_component_collection_and_a_component_that_must_be_specifically_defined_as_being_allowed_to_pass_validation(string $component, int $status, string $allowedComponent): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::setAllowedComponent($group['group'], $allowedComponent); $components = ['dummy' => self::createDummyComponent()]; @@ -78,7 +78,7 @@ public static function restrictedComponentPlacements(): iterable public function test_a_component_created_straight_into_a_component_group_must_be_one_the_group_allows(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::setAllowedComponent($group['group'], '/component/restricted_components'); @@ -91,7 +91,7 @@ public function test_a_component_created_straight_into_a_component_group_must_be public function test_component_position_sort_value_auto_increments(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(1); self::postPosition($client, ['componentGroup' => $group['group'], 'component' => $group['components'][0]]); @@ -103,7 +103,7 @@ public function test_component_position_sort_value_auto_increments(): void public function test_a_position_inserted_after_another_moves_along_the_positions_it_runs_into(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(3); [$p0, $p1, $p2] = $group['positions']; @@ -117,7 +117,7 @@ public function test_a_position_inserted_after_another_moves_along_the_positions public function test_a_position_moved_after_a_later_one_keeps_every_value_that_still_fits(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(4); [$p0, $p1, $p2, $p3] = $group['positions']; @@ -131,7 +131,7 @@ public function test_a_position_moved_after_a_later_one_keeps_every_value_that_s public function test_a_position_moved_before_an_earlier_one_keeps_every_value_that_still_fits(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(4); [$p0, $p1, $p2, $p3] = $group['positions']; @@ -144,7 +144,7 @@ public function test_a_position_moved_before_an_earlier_one_keeps_every_value_th public function test_inserting_a_component_position_with_no_anchor_does_not_shift_existing_positions(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(3); [$p0, $p1, $p2] = $group['positions']; @@ -157,7 +157,7 @@ public function test_inserting_a_component_position_with_no_anchor_does_not_shif public function test_moving_a_component_position_changes_only_the_positions_whose_values_no_longer_fit(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(5); [$p0, $p1, $p2, $p3, $p4] = $group['positions']; @@ -170,7 +170,7 @@ public function test_moving_a_component_position_changes_only_the_positions_whos public function test_moving_a_component_position_into_another_group_after_an_anchor_resolves_to_unique_sort_values(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(3); $other = self::createGroupWithComponentIris(2, 'other_collection'); [$p0, $p1, $p2] = $group['positions']; @@ -184,7 +184,7 @@ public function test_moving_a_component_position_into_another_group_after_an_anc public function test_moving_a_component_position_within_a_group_that_already_holds_a_duplicate_sort_value_resolves_to_unique_sort_values(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $names = self::createGroupWithPositions('A0 B1 C1 D3'); self::requestJsonLd($client, 'PATCH', $names['A'], ['after' => $names['D']]); @@ -195,7 +195,7 @@ public function test_moving_a_component_position_within_a_group_that_already_hol public function test_cannot_create_a_dynamic_position_with_page_data_property_but_no_page_data_class(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::postPosition($client, ['componentGroup' => $group['group'], 'pageDataProperty' => 'component']); @@ -205,7 +205,7 @@ public function test_cannot_create_a_dynamic_position_with_page_data_property_bu public function test_cannot_create_a_dynamic_position_with_page_data_class_but_no_page_data_property(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::postPosition($client, ['componentGroup' => $group['group'], 'pageDataClass' => PageDataWithComponent::class]); @@ -215,7 +215,7 @@ public function test_cannot_create_a_dynamic_position_with_page_data_class_but_n public function test_cannot_create_a_dynamic_position_with_a_page_data_class_that_is_not_a_known_page_data_resource(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::postPosition($client, ['componentGroup' => $group['group'], 'pageDataProperty' => 'component', 'pageDataClass' => 'App\NotAPageDataClass']); @@ -225,7 +225,7 @@ public function test_cannot_create_a_dynamic_position_with_a_page_data_class_tha public function test_cannot_create_a_dynamic_position_where_page_data_property_is_not_a_component_typed_property_on_the_page_data_class(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::postPosition($client, ['componentGroup' => $group['group'], 'pageDataProperty' => 'notAProperty', 'pageDataClass' => PageDataWithComponent::class]); @@ -235,7 +235,7 @@ public function test_cannot_create_a_dynamic_position_where_page_data_property_i public function test_cannot_create_a_dynamic_position_where_the_resolved_component_type_is_not_in_allowed_components(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::setAllowedComponent($group['group'], '/component/dummy_components'); @@ -246,7 +246,7 @@ public function test_cannot_create_a_dynamic_position_where_the_resolved_compone public function test_can_create_a_dynamic_position_with_a_valid_page_data_class_and_page_data_property(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::postPosition($client, ['componentGroup' => $group['group'], 'pageDataProperty' => 'component', 'pageDataClass' => PageDataWithComponent::class]); @@ -256,7 +256,7 @@ public function test_can_create_a_dynamic_position_with_a_valid_page_data_class_ public function test_can_create_a_dynamic_position_where_the_component_type_matches_allowed_components(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::setAllowedComponent($group['group'], '/component/dummy_components'); @@ -267,7 +267,7 @@ public function test_can_create_a_dynamic_position_where_the_component_type_matc public function test_cannot_create_a_dynamic_position_resolving_to_an_explicit_allow_only_component_type_in_a_group_without_allowed_components(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::postPosition($client, ['componentGroup' => $group['group'], 'pageDataProperty' => 'restrictedComponent', 'pageDataClass' => PageDataWithRestrictedComponent::class]); @@ -277,7 +277,7 @@ public function test_cannot_create_a_dynamic_position_resolving_to_an_explicit_a public function test_can_create_a_dynamic_position_resolving_to_an_explicit_allow_only_component_type_when_the_group_explicitly_allows_it(): void { - $client = self::loggedInClient(); + $client = self::adminClient(); $group = self::createGroupWithComponentIris(0); self::setAllowedComponent($group['group'], '/component/restricted_components'); @@ -298,10 +298,10 @@ public function test_an_admin_can_read_page_data_class_from_a_dynamic_position() self::assertJsonNodeSame('pageDataClass', PageDataWithComponent::class); } - private static function loggedInClient(): Client + private static function adminClient(): Client { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); return $client; } diff --git a/tests/Functional/Component/ComponentTest.php b/tests/Functional/Component/ComponentTest.php index 05d4f3db..715f3d80 100644 --- a/tests/Functional/Component/ComponentTest.php +++ b/tests/Functional/Component/ComponentTest.php @@ -45,7 +45,7 @@ public function test_i_can_get_a_components_usage(): void public function test_i_can_create_a_component(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::requestJsonLd($client, 'POST', '/component/dummy_components', '{"uiComponent": "AnotherComponent", "uiClassNames": ["my-class"]}'); @@ -59,7 +59,7 @@ public function test_i_can_create_a_component(): void public function test_i_can_create_a_component_and_the_component_position_at_the_same_time(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $positions = self::createGroupWithTheId(3, self::GROUP_ID); self::requestJsonLd($client, 'POST', '/component/dummy_components', [ @@ -80,7 +80,7 @@ public function test_i_can_create_a_component_and_the_component_position_at_the_ public function test_a_component_update_can_add_a_component_position(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $group = self::createGroupWithComponentIris(0); $component = self::createDummyComponent(); @@ -96,7 +96,7 @@ public function test_a_component_update_can_add_a_component_position(): void public function test_i_can_delete_a_component(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $component = new DummyComponent(); self::persistAndFlush($component); diff --git a/tests/Functional/HttpCache/HttpCachePurgeFailureTest.php b/tests/Functional/HttpCache/HttpCachePurgeFailureTest.php index a8b40ab9..5249070f 100644 --- a/tests/Functional/HttpCache/HttpCachePurgeFailureTest.php +++ b/tests/Functional/HttpCache/HttpCachePurgeFailureTest.php @@ -38,7 +38,7 @@ final class HttpCachePurgeFailureTest extends FunctionalTestCase public function test_creating_a_resource_while_the_http_cache_is_unreachable_returns_201_logs_the_failure_and_still_publishes_to_mercure(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); MockClientCallback::setCacheUnreachable(true); self::requestJsonLd($client, 'POST', '/_/layouts', ['reference' => 'primary', 'uiComponent' => 'PrimaryLayout']); @@ -57,7 +57,7 @@ public function test_creating_a_resource_while_the_http_cache_is_unreachable_ret public function test_the_http_cache_is_reachable_again_in_the_next_test(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::requestJsonLd($client, 'POST', '/_/layouts', ['reference' => 'secondary', 'uiComponent' => 'PrimaryLayout']); diff --git a/tests/Functional/Mercure/MercurePublishFailureTest.php b/tests/Functional/Mercure/MercurePublishFailureTest.php index 2528f064..3f365bae 100644 --- a/tests/Functional/Mercure/MercurePublishFailureTest.php +++ b/tests/Functional/Mercure/MercurePublishFailureTest.php @@ -38,7 +38,7 @@ final class MercurePublishFailureTest extends FunctionalTestCase public function test_creating_a_resource_while_the_mercure_hub_is_unreachable_returns_201_and_logs_the_failure(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); HubStub::setUnreachable(true); self::requestJsonLd($client, 'POST', '/_/layouts', ['reference' => 'primary', 'uiComponent' => 'PrimaryLayout']); @@ -60,7 +60,7 @@ public function test_creating_a_resource_while_the_mercure_hub_is_unreachable_re public function test_the_mercure_hub_is_reachable_again_in_the_next_test(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::requestJsonLd($client, 'POST', '/_/layouts', ['reference' => 'secondary', 'uiComponent' => 'PrimaryLayout']); @@ -71,7 +71,7 @@ public function test_the_mercure_hub_is_reachable_again_in_the_next_test(): void public function test_a_transport_failure_from_the_http_client_while_publishing_returns_201_and_logs_the_failure(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); HubStub::failWith(new TransportException('Connection refused')); self::requestJsonLd($client, 'POST', '/_/layouts', ['reference' => 'primary', 'uiComponent' => 'PrimaryLayout']); @@ -91,7 +91,7 @@ public function test_a_transport_failure_from_the_http_client_while_publishing_r public function test_an_invalid_mercure_jwt_is_configuration_so_the_publish_still_throws_after_the_write_is_saved(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); HubStub::failWith(new MercureInvalidArgumentException('The provided JWT is not valid.')); self::requestJsonLd($client, 'POST', '/_/layouts', ['reference' => 'primary', 'uiComponent' => 'PrimaryLayout']); diff --git a/tests/Functional/Page/LayoutTest.php b/tests/Functional/Page/LayoutTest.php index 750df7e6..cb5c0ab3 100644 --- a/tests/Functional/Page/LayoutTest.php +++ b/tests/Functional/Page/LayoutTest.php @@ -30,7 +30,7 @@ final class LayoutTest extends FunctionalTestCase public function test_i_can_create_a_layout(): void { - $client = self::userClient(); + $client = self::adminClient(); self::requestJsonLd($client, 'POST', '/_/layouts', <<<'JSON' { @@ -50,7 +50,7 @@ public function test_i_can_create_a_layout(): void #[DataProvider('invalidLayouts')] public function test_the_layout_resource_validates_properly(string $reference, ?string $uiComponent, string $propertyPath, string $message): void { - $client = self::userClient(); + $client = self::adminClient(); self::createLayout('primary'); self::requestJsonLd($client, 'POST', '/_/layouts', ['reference' => $reference, 'uiComponent' => $uiComponent]); @@ -73,7 +73,7 @@ public static function invalidLayouts(): iterable public function test_i_can_delete_a_layout(): void { - $client = self::userClient(); + $client = self::adminClient(); $layout = self::createLayout(); self::requestJsonLd($client, 'DELETE', self::iriOf($layout)); @@ -83,7 +83,7 @@ public function test_i_can_delete_a_layout(): void public function test_the_layout_resources_can_be_listed(): void { - $client = self::userClient(); + $client = self::adminClient(); self::createLayout('primary'); self::createLayout('secondary'); @@ -95,7 +95,7 @@ public function test_the_layout_resources_can_be_listed(): void public function test_the_layout_search_parameter_matches_the_reference(): void { - $client = self::userClient(); + $client = self::adminClient(); self::createLayout('primary'); self::createLayout('secondary'); @@ -127,7 +127,7 @@ public function test_the_layout_resources_can_be_ordered_descending_by_created_a public function test_the_layout_search_parameter_matches_the_ui_component(): void { - $client = self::userClient(); + $client = self::adminClient(); self::createLayout('primary', 'PrimaryLayout'); self::createLayout('secondary', 'SecondaryLayout'); @@ -140,7 +140,7 @@ public function test_the_layout_search_parameter_matches_the_ui_component(): voi public function test_layout_component_groups_are_returned_as_iri_strings_not_embedded_objects(): void { - $client = self::userClient(); + $client = self::adminClient(); $page = self::newPage(); $layout = self::newLayout(); $created = self::newComponentGroupWithComponents(1); @@ -170,7 +170,7 @@ public function test_layout_component_groups_are_returned_as_iri_strings_not_emb */ private static function assertLayoutsOrdered(string $url, array $layouts, array $expectedReferences): void { - $client = self::userClient(); + $client = self::adminClient(); foreach ($layouts as $reference => $createdAt) { if (\is_int($reference)) { self::createLayout($createdAt); @@ -201,10 +201,10 @@ private static function createLayoutCreatedAt(string $reference, string $created return $layout; } - private static function userClient(): Client + private static function adminClient(): Client { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); return $client; } diff --git a/tests/Functional/Route/RouteTest.php b/tests/Functional/Route/RouteTest.php index e275eb41..ef1f0463 100644 --- a/tests/Functional/Route/RouteTest.php +++ b/tests/Functional/Route/RouteTest.php @@ -30,7 +30,7 @@ final class RouteTest extends FunctionalTestCase public function test_i_can_create_a_route(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $page = self::createPage(); $route = self::createRouteWithPage('/other'); @@ -54,7 +54,7 @@ public function test_i_cannot_create_a_route_anonymously(): void public function test_a_route_cannot_be_created_without_a_page_page_data_or_redirect(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::createPage(); self::createRouteWithPage('/other'); @@ -79,7 +79,7 @@ public function test_a_route_path_with_a_format_extension_resolves_to_the_correc public function test_a_route_created_without_a_leading_slash_is_automatically_prefixed(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $page = self::createPage(); self::requestJsonLd($client, 'POST', '/_/routes', ['path' => 'contact', 'name' => 'contact-page', 'page' => self::iriOf($page)]); @@ -91,7 +91,7 @@ public function test_a_route_created_without_a_leading_slash_is_automatically_pr public function test_i_can_delete_a_route(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $route = self::createRouteWithPage('/deletable'); self::requestJsonLd($client, 'DELETE', self::iriOf($route)); @@ -102,7 +102,7 @@ public function test_i_can_delete_a_route(): void public function test_i_can_delete_a_route_which_has_a_redirect(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createRouteRedirectingThroughAMiddleRouteTo('/contact', '/contact-new'); $iris = array_map(self::iriOf(...), [$site['final'], $site['route'], $site['middle']]); @@ -152,7 +152,7 @@ public function test_a_route_with_its_own_page_data_and_a_redirect_still_returns public function test_i_can_automatically_generate_a_route_from_a_page_data_resource(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createPageDataWithRoute(null); self::setPageDataTitle($site['pageData'], 'Programme'); @@ -167,7 +167,7 @@ public function test_i_can_automatically_generate_a_route_from_a_page_data_resou public function test_a_route_is_not_generated_for_page_data_with_no_title(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createPageDataWithRoute(null); self::requestJsonLd($client, 'POST', '/_/routes/generate', ['pageData' => self::iriOf($site['pageData'])]); @@ -182,7 +182,7 @@ public function test_a_route_is_not_generated_for_page_data_with_no_title(): voi public function test_a_route_is_not_generated_for_a_page_with_no_title(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $page = self::createPage('untitled'); self::requestJsonLd($client, 'POST', '/_/routes/generate', ['page' => self::iriOf($page)]); @@ -197,7 +197,7 @@ public function test_a_route_is_not_generated_for_a_page_with_no_title(): void public function test_a_route_is_not_generated_for_page_data_whose_existing_route_would_become_a_redirect_when_it_has_no_title(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createPageDataWithRoute('/original'); self::requestJsonLd($client, 'POST', '/_/routes/generate', ['pageData' => self::iriOf($site['pageData'])]); @@ -211,7 +211,7 @@ public function test_a_route_is_not_generated_for_page_data_whose_existing_route public function test_i_generate_a_route_for_page_data_with_a_pre_existing_route_and_the_original_route_changes_to_a_redirect(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createPageDataWithRoute('/original'); self::setPageDataTitle($site['pageData'], 'Programme'); @@ -225,7 +225,7 @@ public function test_i_generate_a_route_for_page_data_with_a_pre_existing_route_ public function test_a_route_cannot_be_generated_for_a_nested_page_whose_parent_page_has_no_route(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createRoutelessParentPageDataWithAnUnroutedChildPage(); self::requestJsonLd($client, 'POST', '/_/routes/generate', ['page' => self::iriOf($site['childPage'])]); @@ -238,7 +238,7 @@ public function test_a_route_cannot_be_generated_for_a_nested_page_whose_parent_ public function test_a_route_generated_for_a_nested_page_whose_parent_page_has_a_route_is_prefixed_with_the_parent_path(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createPageDataNestedWithinRoute('/conference/programme', '/conference'); self::setPageDataTitle($site['pageData'], 'Schedule'); @@ -251,7 +251,7 @@ public function test_a_route_generated_for_a_nested_page_whose_parent_page_has_a public function test_a_route_is_not_generated_when_the_parent_path_and_the_title_slug_together_are_longer_than_the_path_column(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createPageDataNestedWithinRoute('/conference/programme', '/' . str_repeat('a', 242)); self::setPageDataTitle($site['pageData'], 'Speaker List'); @@ -267,7 +267,7 @@ public function test_a_route_is_not_generated_when_the_parent_path_and_the_title public function test_a_route_is_generated_when_the_parent_path_and_the_title_slug_together_fill_the_path_column(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $parentPath = '/' . str_repeat('a', 241); $site = self::createPageDataNestedWithinRoute('/conference/programme', $parentPath); self::setPageDataTitle($site['pageData'], 'Speaker List'); @@ -282,7 +282,7 @@ public function test_a_route_is_generated_when_the_parent_path_and_the_title_slu public function test_a_route_can_still_be_created_explicitly_for_a_nested_page_whose_parent_page_has_no_route(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createRoutelessParentPageDataWithAnUnroutedChildPage(); self::requestJsonLd($client, 'POST', '/_/routes', ['path' => '/2027', 'name' => 'conference-2027', 'page' => self::iriOf($site['childPage'])]); @@ -294,7 +294,7 @@ public function test_a_route_can_still_be_created_explicitly_for_a_nested_page_w public function test_i_update_a_route_path_and_a_new_redirect_is_created(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::createPageDataWithRoute('/original'); self::requestJsonLd($client, 'PATCH', '/_/routes//original', ['path' => '/new']); @@ -521,7 +521,7 @@ public function test_anonymous_users_cannot_access_route_children(): void public function test_i_generate_a_route_for_a_path_that_already_exists_and_the_new_route_is_generated_with_a_postfix(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createPageDataWithRoute('/programme'); self::setPageDataTitle($site['pageData'], 'Programme'); @@ -536,7 +536,7 @@ public function test_i_generate_a_route_for_a_path_that_already_exists_and_the_n public function test_i_can_get_all_the_redirects_for_a_route_in_a_single_request(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createRouteRedirectingThroughAMiddleRouteTo('/redirect1', '/final-page'); self::requestJsonLd($client, 'GET', self::iriOf($site['final']) . '/redirects'); @@ -552,7 +552,7 @@ public function test_i_can_get_all_the_redirects_for_a_route_in_a_single_request public function test_if_i_delete_a_route_aware_resource_the_associated_routes_should_also_be_deleted(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createPageDataWithRoute('/my-route'); $pageDataIri = self::iriOf($site['pageData']); $routeIri = self::iriOf($site['route']); @@ -708,7 +708,7 @@ public function test_a_draft_component_resolved_via_page_data_property_is_includ public function test_when_i_create_a_redirect_route_the_cache_should_be_cleared_for_the_route_being_redirected_to(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $route = self::createRouteWithPage('/my-route'); self::requestJsonLd($client, 'POST', '/_/routes', '{"name": "/redirect", "path": "/redirect", "redirect": "/_/routes//my-route"}'); @@ -720,7 +720,7 @@ public function test_when_i_create_a_redirect_route_the_cache_should_be_cleared_ public function test_when_i_delete_a_route_that_redirects_the_destination_route_should_be_cleared_from_the_cache(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $site = self::createRouteRedirectingThroughAMiddleRouteTo('/my-route', '/contact-new'); $iris = array_map(self::iriOf(...), [$site['route'], $site['middle'], $site['final']]); diff --git a/tests/Functional/Security/ContentWriteSecurityTest.php b/tests/Functional/Security/ContentWriteSecurityTest.php new file mode 100644 index 00000000..b913fdcd --- /dev/null +++ b/tests/Functional/Security/ContentWriteSecurityTest.php @@ -0,0 +1,175 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silverback\ApiComponentsBundle\Tests\Functional\Security; + +use PHPUnit\Framework\Attributes\DataProvider; +use Silverback\ApiComponentsBundle\Entity\Component\Form; +use Silverback\ApiComponentsBundle\Tests\Functional\FunctionalTestCase; +use Silverback\ApiComponentsBundle\Tests\Functional\TestBundle\Entity\DummyHtmlContent; +use Silverback\ApiComponentsBundle\Tests\Functional\TestBundle\Form\TestType; +use Silverback\ApiComponentsBundle\Tests\Functional\Trait\ApiRequestTrait; +use Silverback\ApiComponentsBundle\Tests\Functional\Trait\AuthenticationTrait; +use Silverback\ApiComponentsBundle\Tests\Functional\Trait\SiteTrait; + +final class ContentWriteSecurityTest extends FunctionalTestCase +{ + use ApiRequestTrait; + use AuthenticationTrait; + use SiteTrait; + + #[DataProvider('contentWrites')] + public function test_a_signed_in_user_without_the_publishable_permission_cannot_write_cwa_content(string $method, string $target, string $suffix = ''): void + { + $client = static::createClient(); + self::loginAsUser($client); + $url = self::targetUrl($target) . $suffix; + + self::requestJsonLd($client, $method, $url, []); + + self::assertResponseStatusCodeSame(403); + } + + public static function contentWrites(): iterable + { + foreach (['dummy component' => 'component', 'html content' => 'htmlContent', 'page' => 'page', 'route' => 'route', 'layout' => 'layout', 'component group' => 'group', 'component position' => 'position', 'page data' => 'pageData', 'form' => 'form'] as $name => $target) { + yield 'a POST of a ' . $name => ['POST', $target . 'Collection']; + yield 'a PATCH of a ' . $name => ['PATCH', $target]; + yield 'a DELETE of a ' . $name => ['DELETE', $target]; + } + yield 'a PUT of a route' => ['PUT', 'route']; + yield 'a PUT of a form' => ['PUT', 'form']; + yield 'a route generation' => ['POST', 'routeGenerate']; + yield 'a component group order PATCH' => ['PATCH', 'group', '/order']; + yield 'a component move' => ['POST', 'component', '/move']; + } + + public function test_an_anonymous_caller_cannot_write_cwa_content(): void + { + $client = static::createClient(); + $site = self::createRoutedPageWithComponent('/content'); + + self::requestJsonLd($client, 'PATCH', self::iriOf($site['component']), []); + + self::assertResponseStatusCodeSame(401); + } + + public function test_an_admin_can_create_a_component(): void + { + $client = static::createClient(); + self::loginAsAdmin($client); + + self::requestJsonLd($client, 'POST', '/component/dummy_html_contents', ['html' => '

Hello

']); + + self::assertResponseStatusCodeSame(201); + } + + public function test_an_admin_can_update_a_component(): void + { + $client = static::createClient(); + self::loginAsAdmin($client); + $component = new DummyHtmlContent(); + self::persistAndFlush($component); + + self::requestJsonLd($client, 'PATCH', self::iriOf($component), ['html' => '

Changed

']); + + self::assertResponseStatusCodeSame(200); + } + + public function test_an_admin_can_create_a_page(): void + { + $client = static::createClient(); + self::loginAsAdmin($client); + $layout = self::createLayout(); + + self::requestJsonLd($client, 'POST', '/_/pages', ['reference' => 'new-page', 'isTemplate' => false, 'uiComponent' => 'Primary', 'layout' => self::iriOf($layout)]); + + self::assertResponseStatusCodeSame(201); + } + + public function test_an_admin_can_reorder_a_component_group(): void + { + $client = static::createClient(); + self::loginAsAdmin($client); + $created = self::createComponentGroupWithComponents(2); + + self::requestJsonLd($client, 'PATCH', self::iriOf($created['group']) . '/order', ['componentPositions' => [self::iriOf($created['positions'][1]), self::iriOf($created['positions'][0])]]); + + self::assertResponseStatusCodeSame(200); + } + + #[DataProvider('formSubmits')] + public function test_a_signed_in_user_can_submit_a_form(string $method, int $status): void + { + $client = static::createClient(); + self::loginAsUser($client); + $form = new Form(); + $form->formType = TestType::class; + self::persistAndFlush($form); + + self::requestJsonLd($client, $method, self::iriOf($form) . '/submit', ['test' => ['name' => 'John Smith', 'company' => 'Silverback']]); + + self::assertResponseStatusCodeSame($status); + } + + #[DataProvider('formSubmits')] + public function test_an_anonymous_caller_can_submit_a_form(string $method, int $status): void + { + $client = static::createClient(); + $form = new Form(); + $form->formType = TestType::class; + self::persistAndFlush($form); + + self::requestJsonLd($client, $method, self::iriOf($form) . '/submit', ['test' => ['name' => 'John Smith', 'company' => 'Silverback']]); + + self::assertResponseStatusCodeSame($status); + } + + public static function formSubmits(): iterable + { + yield 'a validating PATCH' => ['PATCH', 200]; + yield 'a POST' => ['POST', 201]; + } + + private static function targetUrl(string $target): string + { + return match ($target) { + 'componentCollection' => '/component/dummy_components', + 'htmlContentCollection' => '/component/dummy_html_contents', + 'pageCollection' => '/_/pages', + 'routeCollection' => '/_/routes', + 'routeGenerate' => '/_/routes/generate', + 'layoutCollection' => '/_/layouts', + 'groupCollection' => '/_/component_groups', + 'positionCollection' => '/_/component_positions', + 'pageDataCollection' => '/page_data/page_data_with_components', + 'formCollection' => '/component/forms', + default => self::siteIri($target), + }; + } + + private static function siteIri(string $target): string + { + $site = self::createRoutedPageWithComponent('/content'); + $pageData = self::createPageDataWithRoute('/page-data'); + $htmlContent = new DummyHtmlContent(); + $form = new Form(); + $form->formType = TestType::class; + self::persistAndFlush($htmlContent, $form); + + return self::iriOf(match ($target) { + 'htmlContent' => $htmlContent, + 'pageData' => $pageData['pageData'], + 'form' => $form, + default => $site[$target], + }); + } +} diff --git a/tests/Functional/TestBundle/Entity/DummyPublishableComponent.php b/tests/Functional/TestBundle/Entity/DummyPublishableComponent.php index e686cde0..cd24fdc7 100644 --- a/tests/Functional/TestBundle/Entity/DummyPublishableComponent.php +++ b/tests/Functional/TestBundle/Entity/DummyPublishableComponent.php @@ -18,6 +18,7 @@ use ApiPlatform\Metadata\QueryParameter; use Doctrine\ORM\Mapping as ORM; use Silverback\ApiComponentsBundle\Annotation as Silverback; +use Silverback\ApiComponentsBundle\ApiPlatform\Metadata\Resource\ContentWriteSecurityResourceMetadataCollectionFactory; use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent; use Silverback\ApiComponentsBundle\Entity\Utility\PublishableTrait; @@ -27,6 +28,7 @@ #[Silverback\Publishable] #[ApiResource( mercure: true, + extraProperties: [ContentWriteSecurityResourceMetadataCollectionFactory::OPT_OUT_EXTRA_PROPERTY => false], parameters: [ 'search' => new QueryParameter(filter: new FreeTextQueryFilter(new OrFilter(new PartialSearchFilter())), properties: ['reference']), ], diff --git a/tests/Functional/Trait/ApiRequestTraitTest.php b/tests/Functional/Trait/ApiRequestTraitTest.php index 926dc64e..41320267 100644 --- a/tests/Functional/Trait/ApiRequestTraitTest.php +++ b/tests/Functional/Trait/ApiRequestTraitTest.php @@ -21,7 +21,7 @@ final class ApiRequestTraitTest extends FunctionalTestCase public function test_a_post_sends_and_accepts_json_ld(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $response = self::requestJsonLd($client, 'POST', '/component/dummy_components', ['uiComponent' => 'AnotherComponent']); @@ -34,7 +34,7 @@ public function test_a_post_sends_and_accepts_json_ld(): void public function test_a_patch_is_sent_as_a_merge_patch(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $iri = self::requestJsonLd($client, 'POST', '/component/dummy_components', ['uiComponent' => 'Before'])->toArray()['@id']; $response = self::requestJsonLd($client, 'PATCH', $iri, ['uiComponent' => 'After']); @@ -47,7 +47,7 @@ public function test_a_patch_is_sent_as_a_merge_patch(): void public function test_a_string_body_is_sent_as_it_is(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); self::requestJsonLd($client, 'POST', '/component/dummy_components', '{"uiComponent": '); diff --git a/tests/Functional/Trait/JsonTraitTest.php b/tests/Functional/Trait/JsonTraitTest.php index 6e5e38b1..94d02dc2 100644 --- a/tests/Functional/Trait/JsonTraitTest.php +++ b/tests/Functional/Trait/JsonTraitTest.php @@ -63,7 +63,7 @@ public function test_a_date_more_than_a_second_from_now_is_not_now(): void public function test_the_last_response_is_read_and_validated_against_a_schema_file(): void { $client = static::createClient(); - self::loginAsUser($client); + self::loginAsAdmin($client); $response = self::requestJsonLd($client, 'POST', '/component/dummy_components', ['uiComponent' => 'AnotherComponent', 'uiClassNames' => ['my-class']]);