From 9b94edcbec47c31768fbc8b3563e607eb06fc06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 8 Nov 2026 13:48:58 +0100 Subject: [PATCH 1/7] test: cover the sale unit scope of the admin order-comments collection --- .../AdminOrderCommentSaleUnitScopeTest.php | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php diff --git a/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php b/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php new file mode 100644 index 0000000..803a4dd --- /dev/null +++ b/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php @@ -0,0 +1,118 @@ +factory->saleUnit(); + $second = $this->factory->saleUnit(); + $connection = $this->getPropelConnection(); + + $myOrder = $this->factory->order()->setSaleUnitId($first->getId()); + $myOrder->save($connection); + $theirOrder = $this->factory->order()->setSaleUnitId($second->getId()); + $theirOrder->save($connection); + $nationalOrder = $this->factory->order(); + + $this->mine = $this->comment($myOrder->getId()); + $this->theirs = $this->comment($theirOrder->getId()); + $this->national = $this->comment($nationalOrder->getId()); + + $this->restricted = $this->factory->restrictedAdmin([AdminResources::ORDER => [AccessManager::VIEW]]); + $this->factory->adminSaleUnit($this->restricted, $first); + } + + protected function tearDown(): void + { + $this->getService(AdminSaleUnitScope::class)->reset(); + $this->logoutAdmin(); + + parent::tearDown(); + } + + public function testARestrictedAdministratorReadsTheCommentsOfItsUnitsOnly(): void + { + $this->loginAsAdminInSession($this->restricted); + + self::assertSame([$this->mine], $this->ids('/admin/order-comments')); + } + + public function testAnItemOfAnotherUnitIsOutOfReach(): void + { + $this->loginAsAdminInSession($this->restricted); + + self::assertSame([], $this->ids('/admin/order-comments', [$this->theirs], true)); + self::assertSame([], $this->ids('/admin/order-comments', [$this->national], true)); + self::assertSame([$this->mine], $this->ids('/admin/order-comments', [$this->mine], true)); + } + + public function testAnAdministratorOfNoUnitReadsEverything(): void + { + $this->loginAsAdminInSession($this->factory->restrictedAdmin([AdminResources::ORDER => [AccessManager::VIEW]])); + + self::assertSame([$this->mine, $this->theirs, $this->national], $this->ids('/admin/order-comments')); + } + + public function testTheFrontCollectionIsLeftAlone(): void + { + $this->loginAsAdminInSession($this->restricted); + + self::assertSame([$this->mine, $this->theirs, $this->national], $this->ids('/front/order-comments')); + } + + private function comment(int $orderId): int + { + $comment = (new OrderComment())->setOrderId($orderId)->setComment('comment'); + $comment->save($this->getPropelConnection()); + + return $comment->getId(); + } + + /** + * @param list|null $among + * + * @return list + */ + private function ids(string $uriTemplate, ?array $among = null, bool $item = false): array + { + $among ??= [$this->mine, $this->theirs, $this->national]; + $query = OrderCommentQuery::create()->filterById($among)->orderById(); + $extension = new AdminSaleUnitScopeExtension($this->getService(AdminSaleUnitScope::class)); + $operation = new GetCollection(uriTemplate: $uriTemplate); + + $item + ? $extension->applyToItem($query, OrderCommentResource::class, $operation) + : $extension->applyToCollection($query, OrderCommentResource::class, $operation); + + return array_map('intval', $query->select(['Id'])->find($this->getPropelConnection())->getData()); + } +} From f8b5e1082c2afed6f47bba5a8f73fc715a440640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 8 Nov 2026 13:49:46 +0100 Subject: [PATCH 2/7] test: exercise the order-comments scope through the admin API --- .../AdminOrderCommentSaleUnitScopeTest.php | 69 +++++++------------ 1 file changed, 24 insertions(+), 45 deletions(-) diff --git a/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php b/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php index 803a4dd..cadedf8 100644 --- a/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php +++ b/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php @@ -4,30 +4,22 @@ namespace OrderComment\Tests\Integration; -use ApiPlatform\Metadata\GetCollection; -use OrderComment\Api\Extension\AdminSaleUnitScopeExtension; -use OrderComment\Api\Resource\OrderComment as OrderCommentResource; use OrderComment\Model\OrderComment; -use OrderComment\Model\OrderCommentQuery; use Thelia\Core\Security\AccessManager; use Thelia\Core\Security\Resource\AdminResources; use Thelia\Domain\SaleUnit\Access\AdminSaleUnitScope; -use Thelia\Model\Admin; -use Thelia\Test\ActionIntegrationTestCase; -use Thelia\Test\Trait\LogsInAsAdmin; +use Thelia\Test\ApiTestCase; /** * The admin order comments of an administrator restricted to some sale units are * those of the orders placed on them. */ -final class AdminOrderCommentSaleUnitScopeTest extends ActionIntegrationTestCase +final class AdminOrderCommentSaleUnitScopeTest extends ApiTestCase { - use LogsInAsAdmin; - private int $mine; private int $theirs; private int $national; - private Admin $restricted; + private string $restrictedToken; protected function setUp(): void { @@ -47,46 +39,31 @@ protected function setUp(): void $this->theirs = $this->comment($theirOrder->getId()); $this->national = $this->comment($nationalOrder->getId()); - $this->restricted = $this->factory->restrictedAdmin([AdminResources::ORDER => [AccessManager::VIEW]]); - $this->factory->adminSaleUnit($this->restricted, $first); + $restricted = $this->factory->restrictedAdmin( + [AdminResources::ORDER => [AccessManager::VIEW]], + ['password' => 'password'], + ); + $this->factory->adminSaleUnit($restricted, $first); + $this->restrictedToken = $this->authenticateAsAdmin($restricted); } protected function tearDown(): void { $this->getService(AdminSaleUnitScope::class)->reset(); - $this->logoutAdmin(); parent::tearDown(); } public function testARestrictedAdministratorReadsTheCommentsOfItsUnitsOnly(): void { - $this->loginAsAdminInSession($this->restricted); - - self::assertSame([$this->mine], $this->ids('/admin/order-comments')); - } - - public function testAnItemOfAnotherUnitIsOutOfReach(): void - { - $this->loginAsAdminInSession($this->restricted); - - self::assertSame([], $this->ids('/admin/order-comments', [$this->theirs], true)); - self::assertSame([], $this->ids('/admin/order-comments', [$this->national], true)); - self::assertSame([$this->mine], $this->ids('/admin/order-comments', [$this->mine], true)); + self::assertSame([$this->mine], $this->listedIds($this->restrictedToken, [$this->mine, $this->theirs, $this->national])); } public function testAnAdministratorOfNoUnitReadsEverything(): void { - $this->loginAsAdminInSession($this->factory->restrictedAdmin([AdminResources::ORDER => [AccessManager::VIEW]])); + $ids = $this->listedIds($this->authenticateAsAdmin(), [$this->mine, $this->theirs, $this->national]); - self::assertSame([$this->mine, $this->theirs, $this->national], $this->ids('/admin/order-comments')); - } - - public function testTheFrontCollectionIsLeftAlone(): void - { - $this->loginAsAdminInSession($this->restricted); - - self::assertSame([$this->mine, $this->theirs, $this->national], $this->ids('/front/order-comments')); + self::assertSame([$this->mine, $this->theirs, $this->national], $ids); } private function comment(int $orderId): int @@ -98,21 +75,23 @@ private function comment(int $orderId): int } /** - * @param list|null $among + * @param list $among * * @return list */ - private function ids(string $uriTemplate, ?array $among = null, bool $item = false): array + private function listedIds(string $token, array $among): array { - $among ??= [$this->mine, $this->theirs, $this->national]; - $query = OrderCommentQuery::create()->filterById($among)->orderById(); - $extension = new AdminSaleUnitScopeExtension($this->getService(AdminSaleUnitScope::class)); - $operation = new GetCollection(uriTemplate: $uriTemplate); + $response = $this->jsonRequest('GET', '/api/admin/order-comments', token: $token, format: 'json'); + + self::assertSame(200, $response->getStatusCode(), (string) $response->getContent()); - $item - ? $extension->applyToItem($query, OrderCommentResource::class, $operation) - : $extension->applyToCollection($query, OrderCommentResource::class, $operation); + $ids = array_map( + static fn (array $row): int => (int) $row['id'], + json_decode((string) $response->getContent(), true, flags: \JSON_THROW_ON_ERROR), + ); + $ids = array_values(array_intersect($ids, $among)); + sort($ids); - return array_map('intval', $query->select(['Id'])->find($this->getPropelConnection())->getData()); + return $ids; } } From 70f81ef80a6627bbff3a1188ffc4f3beb9a03f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 8 Nov 2026 13:52:47 +0100 Subject: [PATCH 3/7] test: build the fixtures through the fixture factory --- .../AdminOrderCommentSaleUnitScopeTest.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php b/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php index cadedf8..abab35e 100644 --- a/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php +++ b/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php @@ -25,25 +25,26 @@ protected function setUp(): void { parent::setUp(); - $first = $this->factory->saleUnit(); - $second = $this->factory->saleUnit(); + $factory = $this->createFixtureFactory(); + $first = $factory->saleUnit(); + $second = $factory->saleUnit(); $connection = $this->getPropelConnection(); - $myOrder = $this->factory->order()->setSaleUnitId($first->getId()); + $myOrder = $factory->order()->setSaleUnitId($first->getId()); $myOrder->save($connection); - $theirOrder = $this->factory->order()->setSaleUnitId($second->getId()); + $theirOrder = $factory->order()->setSaleUnitId($second->getId()); $theirOrder->save($connection); - $nationalOrder = $this->factory->order(); + $nationalOrder = $factory->order(); $this->mine = $this->comment($myOrder->getId()); $this->theirs = $this->comment($theirOrder->getId()); $this->national = $this->comment($nationalOrder->getId()); - $restricted = $this->factory->restrictedAdmin( + $restricted = $factory->restrictedAdmin( [AdminResources::ORDER => [AccessManager::VIEW]], ['password' => 'password'], ); - $this->factory->adminSaleUnit($restricted, $first); + $factory->adminSaleUnit($restricted, $first); $this->restrictedToken = $this->authenticateAsAdmin($restricted); } From 358addcc0cdc7040a7844b4db567663789b27258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 8 Nov 2026 13:56:17 +0100 Subject: [PATCH 4/7] fix: declare the admin permission of the order-comments resource --- OrderComment.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/OrderComment.php b/OrderComment.php index f827178..fa88b59 100644 --- a/OrderComment.php +++ b/OrderComment.php @@ -21,8 +21,11 @@ use OrderComment\Model\OrderCommentQuery; use Propel\Runtime\Connection\ConnectionInterface; +use OrderComment\Api\Resource\OrderComment as OrderCommentResource; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator; use Thelia\Core\Install\Database; +use Thelia\Core\Security\Resource\AdminResources; use Thelia\Module\BaseModule; class OrderComment extends BaseModule @@ -39,6 +42,19 @@ public function postActivation(?ConnectionInterface $con = null): void } } + /** + * The admin API refuses a resource no permission is declared for: an order comment + * answers to the order permission, as the comment is shown on the order. + */ + public static function configureContainer(ContainerConfigurator $containerConfigurator): void + { + $containerConfigurator + ->parameters() + ->set('thelia.api.admin_resources', [ + OrderCommentResource::class => AdminResources::ORDER, + ]); + } + public static function configureServices(ServicesConfigurator $servicesConfigurator): void { $servicesConfigurator->load(self::getModuleCode().'\\', __DIR__) From ce0c46a9ff31f3e6f872e95e14a7cd705aa5d0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 8 Nov 2026 13:58:45 +0100 Subject: [PATCH 5/7] fix: restrict the admin order-comments collection to the sale units of a restricted administrator --- Api/Extension/AdminSaleUnitScopeExtension.php | 65 +++++++++++++++++++ CHANGELOG.md | 12 ++++ Config/module.xml | 4 +- OrderComment.php | 2 +- composer.json | 1 + 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 Api/Extension/AdminSaleUnitScopeExtension.php create mode 100644 CHANGELOG.md diff --git a/Api/Extension/AdminSaleUnitScopeExtension.php b/Api/Extension/AdminSaleUnitScopeExtension.php new file mode 100644 index 0000000..26c50c3 --- /dev/null +++ b/Api/Extension/AdminSaleUnitScopeExtension.php @@ -0,0 +1,65 @@ +restrict($query, $resourceClass, $operation); + } + + public function applyToItem(ModelCriteria $query, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + $this->restrict($query, $resourceClass, $operation); + } + + private function restrict(ModelCriteria $query, string $resourceClass, ?Operation $operation): void + { + if (OrderComment::class !== $resourceClass) { + return; + } + + if (!$operation instanceof HttpOperation || !str_starts_with($operation->getUriTemplate() ?? '', '/admin')) { + return; + } + + $allowed = $this->scope->allowedSaleUnitIds(); + + if (null === $allowed) { + return; + } + + // The ids are integers read from the database, never from the request. + $query->addAnd(OrderCommentTableMap::COL_ORDER_ID, \sprintf( + '%s IN (SELECT `order`.`id` FROM `order` WHERE `order`.`sale_unit_id` IN (%s))', + OrderCommentTableMap::COL_ORDER_ID, + implode(',', array_map('intval', $allowed)) ?: 'NULL', + ), Criteria::CUSTOM); + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cc26ab3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## 3.1.1 + +### Fixed + +- The admin `order-comments` API collection is restricted to the comments of the orders placed on the sale units of an administrator restricted to some units (a comment on a national order is left out). An administrator attached to no unit and a superadministrator are unchanged. +- The `order-comments` admin resource now declares its permission (the order one) in `thelia.api.admin_resources`; without it the admin API refuses every non-super administrator. + +### Changed + +- Requires `thelia/core` 3.3.0 or later (sale unit scope). diff --git a/Config/module.xml b/Config/module.xml index 78d6305..e039047 100644 --- a/Config/module.xml +++ b/Config/module.xml @@ -7,12 +7,12 @@ Permet au client d'ajouter un commentaire à sa commande - 3.1.0 + 3.1.1 Vincent Lopes-Vicente vlopes@openstudio.fr classic - 3.0.0 + 3.3.0 prod diff --git a/OrderComment.php b/OrderComment.php index fa88b59..3d2cb34 100644 --- a/OrderComment.php +++ b/OrderComment.php @@ -58,7 +58,7 @@ public static function configureContainer(ContainerConfigurator $containerConfig public static function configureServices(ServicesConfigurator $servicesConfigurator): void { $servicesConfigurator->load(self::getModuleCode().'\\', __DIR__) - ->exclude([__DIR__.'/I18n/*']) + ->exclude([__DIR__.'/I18n/*', __DIR__.'/Tests/*']) ->autowire() ->autoconfigure(); } diff --git a/composer.json b/composer.json index 8a86fb2..5161cc6 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,7 @@ "license": "GPL-3.0-or-later", "type": "thelia-module", "require": { + "thelia/core": ">=3.3.0", "thelia/installer": "^1.6" }, "extra": { From adc5d0c27649be420f5c8527f6d2bdd4a8c1fec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 8 Nov 2026 14:02:08 +0100 Subject: [PATCH 6/7] test: an admin resource declared by another module survives --- .../AdminResourcesDeclarationTest.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Tests/Integration/AdminResourcesDeclarationTest.php diff --git a/Tests/Integration/AdminResourcesDeclarationTest.php b/Tests/Integration/AdminResourcesDeclarationTest.php new file mode 100644 index 0000000..978321f --- /dev/null +++ b/Tests/Integration/AdminResourcesDeclarationTest.php @@ -0,0 +1,47 @@ +setParameter('thelia.api.admin_resources', ['Other\\Resource' => AdminResources::MODULE]); + + foreach (OrderComment::getCompilers() as $pass) { + $pass->process($container); + } + + self::assertSame( + ['Other\\Resource' => AdminResources::MODULE, OrderCommentResource::class => AdminResources::ORDER], + $container->getParameter('thelia.api.admin_resources'), + ); + } + + public function testTheResourceIsDeclaredWhenNoOtherModuleDeclaresAny(): void + { + $container = new ContainerBuilder(); + + foreach (OrderComment::getCompilers() as $pass) { + $pass->process($container); + } + + self::assertSame( + [OrderCommentResource::class => AdminResources::ORDER], + $container->getParameter('thelia.api.admin_resources'), + ); + } +} From d04a847e17589b58d69cab31e2ccbeb7545d6674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 8 Nov 2026 14:04:47 +0100 Subject: [PATCH 7/7] fix: add the order-comments permission to the declared admin resources instead of replacing them --- CHANGELOG.md | 2 ++ DependencyInjection/AdminApiResourcesPass.php | 33 +++++++++++++++++++ OrderComment.php | 16 ++------- 3 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 DependencyInjection/AdminApiResourcesPass.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cc26ab3..7506d51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,6 @@ ### Changed +- The admin permission of the resource is added to `thelia.api.admin_resources` by a compiler pass, so it never erases what another module declared. + - Requires `thelia/core` 3.3.0 or later (sale unit scope). diff --git a/DependencyInjection/AdminApiResourcesPass.php b/DependencyInjection/AdminApiResourcesPass.php new file mode 100644 index 0000000..3aebb5d --- /dev/null +++ b/DependencyInjection/AdminApiResourcesPass.php @@ -0,0 +1,33 @@ +hasParameter(self::PARAMETER) ? (array) $container->getParameter(self::PARAMETER) : []; + + $container->setParameter(self::PARAMETER, [ + ...$declared, + OrderCommentResource::class => AdminResources::ORDER, + ]); + } +} diff --git a/OrderComment.php b/OrderComment.php index 3d2cb34..5dcbb0d 100644 --- a/OrderComment.php +++ b/OrderComment.php @@ -21,11 +21,9 @@ use OrderComment\Model\OrderCommentQuery; use Propel\Runtime\Connection\ConnectionInterface; -use OrderComment\Api\Resource\OrderComment as OrderCommentResource; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use OrderComment\DependencyInjection\AdminApiResourcesPass; use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator; use Thelia\Core\Install\Database; -use Thelia\Core\Security\Resource\AdminResources; use Thelia\Module\BaseModule; class OrderComment extends BaseModule @@ -42,17 +40,9 @@ public function postActivation(?ConnectionInterface $con = null): void } } - /** - * The admin API refuses a resource no permission is declared for: an order comment - * answers to the order permission, as the comment is shown on the order. - */ - public static function configureContainer(ContainerConfigurator $containerConfigurator): void + public static function getCompilers(): array { - $containerConfigurator - ->parameters() - ->set('thelia.api.admin_resources', [ - OrderCommentResource::class => AdminResources::ORDER, - ]); + return [new AdminApiResourcesPass()]; } public static function configureServices(ServicesConfigurator $servicesConfigurator): void