From 49acd5ae67021702c0054a1f2190aebbb7cabeab Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Fri, 22 May 2026 00:19:51 +0200 Subject: [PATCH 01/11] Add name converter service to context builders for improved attribute normalization --- .../Resources/config/services/context_builders.xml | 14 ++++++++++++++ .../ContextBuilder/AbstractInputContextBuilder.php | 7 +++++++ .../ChannelCodeAwareContextBuilder.php | 4 +++- .../LocaleCodeAwareContextBuilder.php | 4 +++- .../LoggedInCustomerEmailAwareContextBuilder.php | 4 +++- .../LoggedInShopUserIdAwareContextBuilder.php | 4 +++- .../PaymentRequestActionAwareContextBuilder.php | 4 +++- .../UriVariablesAwareContextBuilder.php | 4 +++- 8 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/context_builders.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/context_builders.xml index 0c1b324019..a6b5e92a09 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/context_builders.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/context_builders.xml @@ -24,6 +24,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\ChannelCodeAware Sylius\Bundle\ApiBundle\Attribute\ChannelCodeAware::DEFAULT_ARGUMENT_NAME @@ -36,6 +37,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\LoggedInCustomerEmailAware Sylius\Bundle\ApiBundle\Attribute\LoggedInCustomerEmailAware::DEFAULT_ARGUMENT_NAME @@ -58,6 +60,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\LocaleCodeAware Sylius\Bundle\ApiBundle\Attribute\LocaleCodeAware::DEFAULT_ARGUMENT_NAME @@ -89,6 +92,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\ShopUserIdAware Sylius\Bundle\ApiBundle\Attribute\ShopUserIdAware::DEFAULT_ARGUMENT_NAME @@ -101,6 +105,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\ShipmentIdAware Sylius\Bundle\ApiBundle\Attribute\ShipmentIdAware::DEFAULT_ARGUMENT_NAME Sylius\Component\Core\Model\ShipmentInterface @@ -113,6 +118,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\PaymentIdAware Sylius\Bundle\ApiBundle\Attribute\PaymentIdAware::DEFAULT_ARGUMENT_NAME Sylius\Component\Core\Model\PaymentInterface @@ -125,6 +131,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\PaymentRequestHashAware Sylius\Bundle\ApiBundle\Attribute\PaymentRequestHashAware::DEFAULT_ARGUMENT_NAME Sylius\Component\Payment\Model\PaymentRequestInterface @@ -137,6 +144,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\OrderTokenValueAware Sylius\Bundle\ApiBundle\Attribute\OrderTokenValueAware::DEFAULT_ARGUMENT_NAME Sylius\Component\Core\Model\OrderInterface @@ -149,6 +157,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\OrderItemIdAware Sylius\Bundle\ApiBundle\Attribute\OrderItemIdAware::DEFAULT_ARGUMENT_NAME Sylius\Component\Core\Model\OrderItemInterface @@ -161,6 +170,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\PromotionCodeAware Sylius\Bundle\ApiBundle\Attribute\PromotionCodeAware::DEFAULT_ARGUMENT_NAME Sylius\Component\Core\Model\PromotionInterface @@ -173,6 +183,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\TokenAware Sylius\Bundle\ApiBundle\Attribute\TokenAware::DEFAULT_ARGUMENT_NAME Sylius\Bundle\ApiBundle\Command\Admin\Account\ResetPassword @@ -185,6 +196,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\TokenAware Sylius\Bundle\ApiBundle\Attribute\TokenAware::DEFAULT_ARGUMENT_NAME Sylius\Bundle\ApiBundle\Command\Account\ResetPassword @@ -197,6 +209,7 @@ decoration-priority="64" > + Sylius\Bundle\ApiBundle\Attribute\TokenAware Sylius\Bundle\ApiBundle\Attribute\TokenAware::DEFAULT_ARGUMENT_NAME Sylius\Bundle\ApiBundle\Command\Account\VerifyShopUser @@ -210,6 +223,7 @@ > + Sylius\Bundle\ApiBundle\Attribute\PaymentRequestActionAware Sylius\Bundle\ApiBundle\Attribute\PaymentRequestActionAware::DEFAULT_ARGUMENT_NAME diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/AbstractInputContextBuilder.php b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/AbstractInputContextBuilder.php index b58986c2dd..c77799a6c3 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/AbstractInputContextBuilder.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/AbstractInputContextBuilder.php @@ -15,12 +15,15 @@ namespace Sylius\Bundle\ApiBundle\Serializer\ContextBuilder; use ApiPlatform\State\SerializerContextBuilderInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; abstract class AbstractInputContextBuilder implements SerializerContextBuilderInterface { public function __construct( protected readonly SerializerContextBuilderInterface $decoratedContextBuilder, + protected readonly ?NameConverterInterface $nameConverter, protected readonly string $attributeClass, protected readonly string $defaultConstructorArgumentName, ) { @@ -36,6 +39,10 @@ abstract class AbstractInputContextBuilder implements SerializerContextBuilderIn } $constructorArgumentName = $this->getConstructorArgumentName($inputClass) ?? $this->defaultConstructorArgumentName; + $constructorArgumentName = $this->nameConverter + ? $this->nameConverter->normalize($constructorArgumentName, $inputClass, JsonEncoder::FORMAT, $context) + : $constructorArgumentName + ; if (isset($context[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS][$inputClass]) && is_array($context[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS][$inputClass])) { $context[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS][$inputClass] = array_merge($context[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS][$inputClass], [$constructorArgumentName => $this->resolveValue($context, $extractedAttributes)]); diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/ChannelCodeAwareContextBuilder.php b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/ChannelCodeAwareContextBuilder.php index 3bf8dfff69..b63a18969d 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/ChannelCodeAwareContextBuilder.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/ChannelCodeAwareContextBuilder.php @@ -16,16 +16,18 @@ namespace Sylius\Bundle\ApiBundle\Serializer\ContextBuilder; use ApiPlatform\State\SerializerContextBuilderInterface; use Sylius\Component\Channel\Context\ChannelContextInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; final class ChannelCodeAwareContextBuilder extends AbstractInputContextBuilder { public function __construct( SerializerContextBuilderInterface $decoratedContextBuilder, + ?NameConverterInterface $nameConverter, string $attributeClass, string $defaultConstructorArgumentName, private readonly ChannelContextInterface $channelContext, ) { - parent::__construct($decoratedContextBuilder, $attributeClass, $defaultConstructorArgumentName); + parent::__construct($decoratedContextBuilder, $nameConverter, $attributeClass, $defaultConstructorArgumentName); } protected function supports(Request $request, array $context, ?array $extractedAttributes): bool diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LocaleCodeAwareContextBuilder.php b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LocaleCodeAwareContextBuilder.php index 53c1bcf798..d15e90496e 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LocaleCodeAwareContextBuilder.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LocaleCodeAwareContextBuilder.php @@ -16,16 +16,18 @@ namespace Sylius\Bundle\ApiBundle\Serializer\ContextBuilder; use ApiPlatform\State\SerializerContextBuilderInterface; use Sylius\Component\Locale\Context\LocaleContextInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; final class LocaleCodeAwareContextBuilder extends AbstractInputContextBuilder { public function __construct( SerializerContextBuilderInterface $decoratedContextBuilder, + ?NameConverterInterface $nameConverter, string $attributeClass, string $defaultConstructorArgumentName, private readonly LocaleContextInterface $localeContext, ) { - parent::__construct($decoratedContextBuilder, $attributeClass, $defaultConstructorArgumentName); + parent::__construct($decoratedContextBuilder, $nameConverter, $attributeClass, $defaultConstructorArgumentName); } protected function supports(Request $request, array $context, ?array $extractedAttributes): bool diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LoggedInCustomerEmailAwareContextBuilder.php b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LoggedInCustomerEmailAwareContextBuilder.php index 3cf3623bd7..ab21ee1417 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LoggedInCustomerEmailAwareContextBuilder.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LoggedInCustomerEmailAwareContextBuilder.php @@ -19,17 +19,19 @@ use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\User\Model\UserInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class LoggedInCustomerEmailAwareContextBuilder extends AbstractInputContextBuilder { public function __construct( SerializerContextBuilderInterface $decoratedContextBuilder, + ?NameConverterInterface $nameConverter, string $attributeClass, string $defaultConstructorArgumentName, private readonly UserContextInterface $userContext, ) { - parent::__construct($decoratedContextBuilder, $attributeClass, $defaultConstructorArgumentName); + parent::__construct($decoratedContextBuilder, $nameConverter, $attributeClass, $defaultConstructorArgumentName); } protected function supports(Request $request, array $context, ?array $extractedAttributes): bool diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LoggedInShopUserIdAwareContextBuilder.php b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LoggedInShopUserIdAwareContextBuilder.php index aa059d131d..51c6069c5c 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LoggedInShopUserIdAwareContextBuilder.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/LoggedInShopUserIdAwareContextBuilder.php @@ -17,16 +17,18 @@ use ApiPlatform\State\SerializerContextBuilderInterface; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; final class LoggedInShopUserIdAwareContextBuilder extends AbstractInputContextBuilder { public function __construct( SerializerContextBuilderInterface $decoratedContextBuilder, + ?NameConverterInterface $nameConverter, string $attributeClass, string $defaultConstructorArgumentName, private readonly UserContextInterface $userContext, ) { - parent::__construct($decoratedContextBuilder, $attributeClass, $defaultConstructorArgumentName); + parent::__construct($decoratedContextBuilder, $nameConverter, $attributeClass, $defaultConstructorArgumentName); } protected function supports(Request $request, array $context, ?array $extractedAttributes): bool diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/PaymentRequestActionAwareContextBuilder.php b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/PaymentRequestActionAwareContextBuilder.php index 901d1e73c7..1c9cc70181 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/PaymentRequestActionAwareContextBuilder.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/PaymentRequestActionAwareContextBuilder.php @@ -18,6 +18,7 @@ use Sylius\Bundle\ApiBundle\Attribute\PaymentRequestActionAware; use Sylius\Bundle\ApiBundle\Converter\IriToIdentifierConverterInterface; use Sylius\Bundle\PaymentBundle\Provider\DefaultActionProviderInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** @experimental */ final class PaymentRequestActionAwareContextBuilder extends AbstractInputContextBuilder @@ -27,11 +28,12 @@ final class PaymentRequestActionAwareContextBuilder extends AbstractInputContext public function __construct( private readonly IriToIdentifierConverterInterface $iriToIdentifierConverter, SerializerContextBuilderInterface $decoratedContextBuilder, + ?NameConverterInterface $nameConverter, string $attributeClass, string $defaultConstructorArgumentName, private readonly DefaultActionProviderInterface $defaultActionProvider, ) { - parent::__construct($decoratedContextBuilder, $attributeClass, $defaultConstructorArgumentName); + parent::__construct($decoratedContextBuilder, $nameConverter, $attributeClass, $defaultConstructorArgumentName); } public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/UriVariablesAwareContextBuilder.php b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/UriVariablesAwareContextBuilder.php index 33e262dbfd..03aa72cf42 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/UriVariablesAwareContextBuilder.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ContextBuilder/UriVariablesAwareContextBuilder.php @@ -16,16 +16,18 @@ namespace Sylius\Bundle\ApiBundle\Serializer\ContextBuilder; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\State\SerializerContextBuilderInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; final class UriVariablesAwareContextBuilder extends AbstractInputContextBuilder { public function __construct( SerializerContextBuilderInterface $decoratedContextBuilder, + ?NameConverterInterface $nameConverter, string $attributeClass, string $defaultConstructorArgumentName, private readonly string $objectInterface, ) { - parent::__construct($decoratedContextBuilder, $attributeClass, $defaultConstructorArgumentName); + parent::__construct($decoratedContextBuilder, $nameConverter, $attributeClass, $defaultConstructorArgumentName); } protected function supports(Request $request, array $context, ?array $extractedAttributes): bool From f2a2e7dfa837dd5994e14f5613caee687c8314b2 Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Fri, 22 May 2026 10:31:41 +0200 Subject: [PATCH 02/11] Add null parameter to context builders for improved flexibility in tests --- .../ContextBuilder/ChannelCodeAwareContextBuilderTest.php | 1 + .../ContextBuilder/LocaleCodeAwareContextBuilderTest.php | 1 + .../LoggedInCustomerEmailAwareContextBuilderTest.php | 1 + .../LoggedInShopUserIdAwareContextBuilderTest.php | 1 + .../PaymentRequestActionAwareContextBuilderTest.php | 1 + .../ContextBuilder/UriVariablesAwareContextBuilderTest.php | 3 +++ 6 files changed, 8 insertions(+) diff --git a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/ChannelCodeAwareContextBuilderTest.php b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/ChannelCodeAwareContextBuilderTest.php index 69fe705d49..ba8e882a71 100644 --- a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/ChannelCodeAwareContextBuilderTest.php +++ b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/ChannelCodeAwareContextBuilderTest.php @@ -39,6 +39,7 @@ final class ChannelCodeAwareContextBuilderTest extends TestCase $this->channelContext = $this->createMock(ChannelContextInterface::class); $this->channelCodeAwareContextBuilder = new ChannelCodeAwareContextBuilder( $this->decoratedContextBuilder, + null, ChannelCodeAware::class, 'channelCode', $this->channelContext, diff --git a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LocaleCodeAwareContextBuilderTest.php b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LocaleCodeAwareContextBuilderTest.php index e680816659..c1bca6c170 100644 --- a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LocaleCodeAwareContextBuilderTest.php +++ b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LocaleCodeAwareContextBuilderTest.php @@ -38,6 +38,7 @@ final class LocaleCodeAwareContextBuilderTest extends TestCase $this->localeContext = $this->createMock(LocaleContextInterface::class); $this->localeCodeAwareContextBuilder = new LocaleCodeAwareContextBuilder( $this->decoratedContextBuilder, + null, LocaleCodeAware::class, 'localeCode', $this->localeContext, diff --git a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LoggedInCustomerEmailAwareContextBuilderTest.php b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LoggedInCustomerEmailAwareContextBuilderTest.php index 7f99534dbc..391f9d75ca 100644 --- a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LoggedInCustomerEmailAwareContextBuilderTest.php +++ b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LoggedInCustomerEmailAwareContextBuilderTest.php @@ -41,6 +41,7 @@ final class LoggedInCustomerEmailAwareContextBuilderTest extends TestCase $this->userContext = $this->createMock(UserContextInterface::class); $this->loggedInCustomerEmailAwareContextBuilder = new LoggedInCustomerEmailAwareContextBuilder( $this->decoratedContextBuilder, + null, LoggedInCustomerEmailAware::class, 'email', $this->userContext, diff --git a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LoggedInShopUserIdAwareContextBuilderTest.php b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LoggedInShopUserIdAwareContextBuilderTest.php index e32a6b3183..b7d5280ce5 100644 --- a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LoggedInShopUserIdAwareContextBuilderTest.php +++ b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/LoggedInShopUserIdAwareContextBuilderTest.php @@ -39,6 +39,7 @@ final class LoggedInShopUserIdAwareContextBuilderTest extends TestCase $this->userContext = $this->createMock(UserContextInterface::class); $this->loggedInShopUserIdAwareContextBuilder = new LoggedInShopUserIdAwareContextBuilder( $this->decoratedContextBuilder, + null, ShopUserIdAware::class, 'shopUserId', $this->userContext, diff --git a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/PaymentRequestActionAwareContextBuilderTest.php b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/PaymentRequestActionAwareContextBuilderTest.php index 27cb756a0b..5cae3a85af 100644 --- a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/PaymentRequestActionAwareContextBuilderTest.php +++ b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/PaymentRequestActionAwareContextBuilderTest.php @@ -43,6 +43,7 @@ final class PaymentRequestActionAwareContextBuilderTest extends TestCase $this->paymentRequestActionAwareContextBuilder = new PaymentRequestActionAwareContextBuilder( $this->iriToIdentifierConverterMock, $this->decoratedContextBuilderMock, + null, PaymentRequestActionAware::class, PaymentRequestActionAware::DEFAULT_ARGUMENT_NAME, $this->defaultActionProviderMock, diff --git a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/UriVariablesAwareContextBuilderTest.php b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/UriVariablesAwareContextBuilderTest.php index 2b4d3e70ed..c1330cc484 100644 --- a/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/UriVariablesAwareContextBuilderTest.php +++ b/src/Sylius/Bundle/ApiBundle/tests/Serializer/ContextBuilder/UriVariablesAwareContextBuilderTest.php @@ -42,6 +42,7 @@ final class UriVariablesAwareContextBuilderTest extends TestCase $this->decoratedContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); $this->uriVariablesAwareContextBuilder = new UriVariablesAwareContextBuilder( $this->decoratedContextBuilder, + null, ShipmentIdAware::class, 'shipmentId', ShipmentInterface::class, @@ -163,6 +164,7 @@ final class UriVariablesAwareContextBuilderTest extends TestCase $this->uriVariablesAwareContextBuilder = new UriVariablesAwareContextBuilder( $this->decoratedContextBuilder, + null, OrderTokenValueAware::class, 'orderTokenValue', OrderInterface::class, @@ -197,6 +199,7 @@ final class UriVariablesAwareContextBuilderTest extends TestCase $this->uriVariablesAwareContextBuilder = new UriVariablesAwareContextBuilder( $this->decoratedContextBuilder, + null, OrderItemIdAware::class, 'orderItemId', OrderItemInterface::class, From 55f2ea3d51dee3acf742d91317676b2a24fc0ccd Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Tue, 26 May 2026 10:06:49 +0200 Subject: [PATCH 03/11] Replace serializer normalizer service with API Platform normalizer in serializers.xml --- .../ApiBundle/Resources/config/services/serializers.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml index e20860d869..dc93f85547 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml @@ -18,7 +18,7 @@ > - + %sylius.model.address.class% %sylius.model.address.interface% @@ -44,7 +44,7 @@ sylius:shop:product:index sylius:shop:product:show - + sylius:shop:product:index:default_variant sylius:shop:product:show:default_variant @@ -80,7 +80,7 @@ - + @@ -119,7 +119,7 @@ - + From 1a954e88cd5e6c96b3619482f57784dceb57f250 Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Thu, 28 May 2026 15:18:54 +0200 Subject: [PATCH 04/11] Update CommandDenormalizer to use API Platform name converter and handle null cases --- .../ApiBundle/Resources/config/services/serializers.xml | 2 +- .../ApiBundle/Serializer/Denormalizer/CommandDenormalizer.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml index dc93f85547..dae8c32143 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml @@ -32,7 +32,7 @@ - + diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/Denormalizer/CommandDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/Denormalizer/CommandDenormalizer.php index c4ea7cbb87..98f8ca655e 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/Denormalizer/CommandDenormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/Denormalizer/CommandDenormalizer.php @@ -24,7 +24,7 @@ final class CommandDenormalizer implements DenormalizerInterface { public function __construct( private DenormalizerInterface $itemNormalizer, - private NameConverterInterface $nameConverter, + private ?NameConverterInterface $nameConverter, ) { } @@ -70,6 +70,6 @@ final class CommandDenormalizer implements DenormalizerInterface private function normalizeFieldName(string $field, string $class): string { - return $this->nameConverter->normalize($field, $class); + return $this->nameConverter ? $this->nameConverter->normalize($field, $class) : $field; } } From 0505d40d7d5ec77173f88d973dd9f2cfec4b687c Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Thu, 28 May 2026 16:26:04 +0200 Subject: [PATCH 05/11] Add name converter integration to ApiPlatformClient and ResponseChecker for attribute normalization --- src/Sylius/Behat/Client/ApiPlatformClient.php | 53 ++++++++++++++++--- src/Sylius/Behat/Client/ResponseChecker.php | 50 +++++++++++------ .../Behat/Resources/config/services/api.xml | 5 +- 3 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/Sylius/Behat/Client/ApiPlatformClient.php b/src/Sylius/Behat/Client/ApiPlatformClient.php index 3bdea38d8b..cc7f9b3e01 100644 --- a/src/Sylius/Behat/Client/ApiPlatformClient.php +++ b/src/Sylius/Behat/Client/ApiPlatformClient.php @@ -18,6 +18,7 @@ use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; final class ApiPlatformClient implements ApiClientInterface { @@ -30,6 +31,7 @@ final class ApiPlatformClient implements ApiClientInterface private readonly SharedStorageInterface $sharedStorage, private readonly RequestFactoryInterface $requestFactory, private readonly ResponseCheckerInterface $responseChecker, + private readonly ?NameConverterInterface $nameConverter, private readonly string $authorizationHeader, private readonly string $section, ) { @@ -210,7 +212,7 @@ final class ApiPlatformClient implements ApiClientInterface public function setRequestData(array $data): self { - $this->request->setContent($data); + $this->request->setContent($this->convertArrayAttributesRecursively($data)); return $this; } @@ -233,7 +235,9 @@ final class ApiPlatformClient implements ApiClientInterface /** @param array $value */ public function addRequestData(string $key, array|bool|int|string|null $value): self { - $this->request->updateContent([$key => $value]); + $this->request->updateContent([ + $this->getNormalizedKey($key) => is_array($value) ? $this->convertArrayAttributesRecursively($value) : $value, + ]); return $this; } @@ -243,35 +247,37 @@ final class ApiPlatformClient implements ApiClientInterface { $requestContent = $this->request->getContent(); - $this->request->setContent(array_replace($requestContent, [$key => $value])); + $this->request->setContent(array_replace($requestContent, [ + $this->getNormalizedKey($key) => is_array($value) ? $this->convertArrayAttributesRecursively($value) : $value, + ])); } /** @param array $data */ public function updateRequestData(array $data): void { - $this->request->updateContent($data); + $this->request->updateContent($this->convertArrayAttributesRecursively($data)); } /** @param array $data */ public function setSubResourceData(string $key, array $data): void { - $this->request->setSubResource($key, $data); + $this->request->setSubResource($this->getNormalizedKey($key), $this->convertArrayAttributesRecursively($data)); } /** @param array $data */ public function addSubResourceData(string $key, array $data): void { - $this->request->addSubResource($key, $data); + $this->request->addSubResource($this->getNormalizedKey($key), $this->convertArrayAttributesRecursively($data)); } public function removeSubResourceIri(string $subResourceKey, string $iri): void { - $this->request->removeSubResource($subResourceKey, $iri); + $this->request->removeSubResource($this->getNormalizedKey($subResourceKey), $iri); } public function removeSubResourceObject(string $subResourceKey, string $value, string $key = '@id'): void { - $this->request->removeSubResource($subResourceKey, $value, $key); + $this->request->removeSubResource($this->getNormalizedKey($subResourceKey), $value, $key); } /** @return array */ @@ -389,4 +395,35 @@ final class ApiPlatformClient implements ApiClientInterface throw new \InvalidArgumentException('URI should not start with "api".'); } } + + /** + * @param array $data + * + * @return array + */ + private function convertArrayAttributesRecursively(array $data): array + { + if (array_is_list($data)) { + return array_map( + fn (mixed $value): mixed => is_array($value) ? $this->convertArrayAttributesRecursively($value) : $value, + $data, + ); + } + + $convertedData = []; + + foreach ($data as $attribute => $value) { + $convertedAttribute = is_string($attribute) ? $this->getNormalizedKey($attribute) : $attribute; + $convertedData[$convertedAttribute] = is_array($value) + ? $this->convertArrayAttributesRecursively($value) + : $value; + } + + return $convertedData; + } + + private function getNormalizedKey(string $key): string + { + return $this->nameConverter ? $this->nameConverter->normalize($key) : $key; + } } diff --git a/src/Sylius/Behat/Client/ResponseChecker.php b/src/Sylius/Behat/Client/ResponseChecker.php index 0e70817904..a159de4fd1 100644 --- a/src/Sylius/Behat/Client/ResponseChecker.php +++ b/src/Sylius/Behat/Client/ResponseChecker.php @@ -16,6 +16,7 @@ namespace Sylius\Behat\Client; use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse; use Sylius\Behat\Service\SprintfResponseEscaper; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class ResponseChecker implements ResponseCheckerInterface @@ -23,8 +24,9 @@ final class ResponseChecker implements ResponseCheckerInterface /** @var array */ private array $errors; - public function __construct() - { + public function __construct( + private ?NameConverterInterface $nameConverter + ) { $this->errors = []; } @@ -45,21 +47,24 @@ final class ResponseChecker implements ResponseCheckerInterface public function getCollectionItemsWithValue(Response $response, string $key, string $value): array { - $items = array_filter($this->getCollection($response), fn (array $item): bool => $item[$key] === $value); + $items = array_filter( + $this->getCollection($response), + fn (array $item): bool => $item[$this->getNormalizedKey($key)] === $value, + ); return $items; } public function getValue(Response $response, string $key) { - return $this->getResponseContentValue($response, $key); + return $this->getResponseContentValue($response, $this->getNormalizedKey($key)); } public function getTranslationValue(Response $response, string $key, ?string $localeCode = 'en_US'): string { $translations = $this->getResponseContentValue($response, 'translations'); - return $translations[$localeCode][$key]; + return $translations[$localeCode][$this->getNormalizedKey($key)]; } public function getError(Response $response): ?string @@ -118,23 +123,27 @@ final class ResponseChecker implements ResponseCheckerInterface public function hasValue(Response $response, string $key, bool|int|string|null $value, bool $isCaseSensitive = true): bool { + $normalizedKey = $this->getNormalizedKey($key); + if ($isCaseSensitive) { - return $this->getResponseContentValue($response, $key) === $value; + return $this->getResponseContentValue($response, $normalizedKey) === $value; } - return strcasecmp((string) $this->getResponseContentValue($response, $key), (string) $value) === 0; + return strcasecmp((string) $this->getResponseContentValue($response, $normalizedKey), (string) $value) === 0; } public function hasValueInCollection(Response $response, string $key, bool|int|string $value): bool { - return in_array($value, $this->getResponseContentValue($response, $key), true); + return in_array($value, $this->getResponseContentValue($response, $this->getNormalizedKey($key)), true); } /** @param string|int $value */ public function hasItemWithValue(Response $response, string $key, $value): bool { + $normalizedKey = $this->getNormalizedKey($key); + foreach ($this->getCollection($response) as $resource) { - if ($resource[$key] === $value) { + if ($resource[$normalizedKey] === $value) { return true; } } @@ -192,27 +201,29 @@ final class ResponseChecker implements ResponseCheckerInterface $this->assertIsArray($resource); - return $resource[$key] === $expectedValue; + return $resource[$this->getNormalizedKey($key)] === $expectedValue; } /** @param string|array $value */ public function hasItemOnPositionWithValue(Response $response, int $position, string $key, $value): bool { - return $this->getCollection($response)[$position][$key] === $value; + return $this->getCollection($response)[$position][$this->getNormalizedKey($key)] === $value; } public function hasItemWithTranslation(Response $response, string $locale, string $key, string $translation): bool { + $normalizedKey = $this->getNormalizedKey($key); + if (!$this->hasCollection($response)) { $resource = $this->getResponseContent($response); - if (isset($resource['translations'][$locale]) && $resource['translations'][$locale][$key] === $translation) { + if (isset($resource['translations'][$locale]) && $resource['translations'][$locale][$normalizedKey] === $translation) { return true; } } foreach ($this->getCollection($response) as $resource) { - if (isset($resource['translations'][$locale]) && $resource['translations'][$locale][$key] === $translation) { + if (isset($resource['translations'][$locale]) && $resource['translations'][$locale][$normalizedKey] === $translation) { return true; } } @@ -222,8 +233,10 @@ final class ResponseChecker implements ResponseCheckerInterface public function hasItemWithTranslationInCollection(array $items, string $locale, string $key, string $translation): bool { + $normalizedKey = $this->getNormalizedKey($key); + foreach ($items as $item) { - if (isset($item['translations'][$locale]) && $item['translations'][$locale][$key] === $translation) { + if (isset($item['translations'][$locale]) && $item['translations'][$locale][$normalizedKey] === $translation) { return true; } } @@ -235,14 +248,14 @@ final class ResponseChecker implements ResponseCheckerInterface { $content = json_decode($response->getContent(), true); - return array_key_exists($key, $content); + return array_key_exists($this->getNormalizedKey($key), $content); } public function hasTranslation(Response $response, string $locale, string $key, string $translation): bool { $resource = $this->getResponseContent($response); - return isset($resource['translations'][$locale]) && $resource['translations'][$locale][$key] === $translation; + return isset($resource['translations'][$locale]) && $resource['translations'][$locale][$this->getNormalizedKey($key)] === $translation; } public function hasItemWithValues(Response $response, array $parameters): bool @@ -376,4 +389,9 @@ final class ResponseChecker implements ResponseCheckerInterface ), ); } + + private function getNormalizedKey(string $key): string + { + return $this->nameConverter ? $this->nameConverter->normalize($key) : $key; + } } diff --git a/src/Sylius/Behat/Resources/config/services/api.xml b/src/Sylius/Behat/Resources/config/services/api.xml index 96d250a285..7582f2cb04 100644 --- a/src/Sylius/Behat/Resources/config/services/api.xml +++ b/src/Sylius/Behat/Resources/config/services/api.xml @@ -18,6 +18,7 @@ + %sylius.api.authorization_header% @@ -29,7 +30,9 @@ admin - + + + From 64c1f2931400fc2b6a5e5d08cab751cfdedf4731 Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Thu, 28 May 2026 18:13:53 +0200 Subject: [PATCH 06/11] Normalize keys in itemHasValues method for accurate value comparison --- src/Sylius/Behat/Client/ResponseChecker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sylius/Behat/Client/ResponseChecker.php b/src/Sylius/Behat/Client/ResponseChecker.php index a159de4fd1..4c7613c959 100644 --- a/src/Sylius/Behat/Client/ResponseChecker.php +++ b/src/Sylius/Behat/Client/ResponseChecker.php @@ -360,7 +360,7 @@ final class ResponseChecker implements ResponseCheckerInterface private function itemHasValues(array $element, array $parameters): bool { foreach ($parameters as $key => $value) { - if ($element[$key] !== $value) { + if ($element[$this->getNormalizedKey($key)] !== $value) { return false; } } From cd07d06f991d6d6df22562c861bfcb9f4c9a5fe8 Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Thu, 28 May 2026 18:16:17 +0200 Subject: [PATCH 07/11] Integrate name converter for key normalization in ManagingOrdersContext --- .../Api/Admin/ManagingOrdersContext.php | 89 +++++++++++++------ .../config/services/contexts/api/admin.xml | 1 + 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php index d115ffe613..45137814ad 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php @@ -39,6 +39,7 @@ use Sylius\Component\Shipping\ShipmentTransitions; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Intl\Countries; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class ManagingOrdersContext implements Context @@ -50,6 +51,7 @@ final readonly class ManagingOrdersContext implements Context private SecurityServiceInterface $adminSecurityService, private SharedStorageInterface $sharedStorage, private SharedSecurityServiceInterface $sharedSecurityService, + private ?NameConverterInterface $nameConverter ) { } @@ -327,8 +329,9 @@ final readonly class ManagingOrdersContext implements Context /** @var array{productName: string}[] $items */ $items = json_decode($lastResponseContent, true)['items']; + $productNameKey = $this->getNormalizedKey('productName'); foreach ($items as $item) { - if ($item['productName'] === $itemName) { + if ($item[$productNameKey] === $itemName) { $this->sharedStorage->set('item', $item); return; @@ -480,10 +483,12 @@ final readonly class ManagingOrdersContext implements Context { $order = $this->responseChecker->getCollection($this->client->getLastResponse())[0]; + $paymentStateKey = $this->getNormalizedKey('paymentState'); + $tokenValueKey = $this->getNormalizedKey('tokenValue'); Assert::same( - $order['paymentState'], + $order[$paymentStateKey], strtolower($orderPaymentState), - sprintf('Order "%s" does not have "%s" payment state', $order['tokenValue'], $orderPaymentState), + sprintf('Order "%s" does not have "%s" payment state', $order[$tokenValueKey], $orderPaymentState), ); } @@ -502,8 +507,9 @@ final readonly class ManagingOrdersContext implements Context { $items = $this->responseChecker->getValue($this->client->getLastResponse(), 'items'); + $productNameKey = $this->getNormalizedKey('productName'); foreach ($items as $item) { - if ($item['productName'] === $productName) { + if ($item[$productNameKey] === $productName) { return; } } @@ -860,8 +866,9 @@ final readonly class ManagingOrdersContext implements Context */ public function iShouldSeeAsProvinceInTheShippingAddress(string $provinceName): void { + $provinceNameKey = $this->getNormalizedKey('provinceName'); Assert::same( - $this->responseChecker->getValue($this->client->getLastResponse(), 'shippingAddress')['provinceName'], + $this->responseChecker->getValue($this->client->getLastResponse(), 'shippingAddress')[$provinceNameKey], $provinceName, ); } @@ -871,8 +878,9 @@ final readonly class ManagingOrdersContext implements Context */ public function iShouldSeeAsProvinceInTheBillingAddress(string $provinceName): void { + $provinceNameKey = $this->getNormalizedKey('provinceName'); Assert::same( - $this->responseChecker->getValue($this->client->getLastResponse(), 'billingAddress')['provinceName'], + $this->responseChecker->getValue($this->client->getLastResponse(), 'billingAddress')[$provinceNameKey], $provinceName, ); } @@ -895,7 +903,8 @@ final readonly class ManagingOrdersContext implements Context */ public function itemUnitPriceShouldBe(array $orderItem, string $unitPrice): void { - Assert::same($this->getTotalAsInt($unitPrice), $orderItem['unitPrice']); + $unitPriceKey = $this->getNormalizedKey('unitPrice'); + Assert::same($this->getTotalAsInt($unitPrice), $orderItem[$unitPriceKey]); } /** @@ -943,14 +952,18 @@ final readonly class ManagingOrdersContext implements Context { $orderItem = $this->sharedStorage->get('item'); + $typeKey = $this->getNormalizedKey('type'); + $amountKey = $this->getNormalizedKey('amount'); + $unitPriceKey = $this->getNormalizedKey('unitPrice'); + $quantityKey = $this->getNormalizedKey('quantity'); $unitPromotionAdjustments = 0; foreach ($this->responseChecker->getCollection($this->client->getLastResponse()) as $item) { - if (in_array($item['type'], [AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT])) { - $unitPromotionAdjustments += $item['amount']; + if (in_array($item[$typeKey], [AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT])) { + $unitPromotionAdjustments += $item[$amountKey]; } } - Assert::same($this->getTotalAsInt($subtotal), $orderItem['unitPrice'] * $orderItem['quantity'] + $unitPromotionAdjustments); + Assert::same($this->getTotalAsInt($subtotal), $orderItem[$unitPriceKey] * $orderItem[$quantityKey] + $unitPromotionAdjustments); } /** @@ -1041,9 +1054,10 @@ final readonly class ManagingOrdersContext implements Context */ public function productUnitPriceShouldBe(string $productName, string $price): void { + $unitPriceKey = $this->getNormalizedKey('unitPrice'); $this->iCheckData($productName); $orderItem = $this->sharedStorage->get('item'); - Assert::same($this->getTotalAsInt($price), $orderItem['unitPrice']); + Assert::same($this->getTotalAsInt($price), $orderItem[$unitPriceKey]); } /** @@ -1052,7 +1066,8 @@ final readonly class ManagingOrdersContext implements Context public function productDiscountedUnitPriceShouldBe(string $productName, string $price): void { $orderItem = $this->sharedStorage->get('item'); - Assert::same($this->getTotalAsInt($price), $orderItem['fullDiscountedUnitPrice']); + $fullDiscountedUnitPriceKey = $this->getNormalizedKey('fullDiscountedUnitPrice'); + Assert::same($this->getTotalAsInt($price), $orderItem[$fullDiscountedUnitPriceKey]); } /** @@ -1061,6 +1076,7 @@ final readonly class ManagingOrdersContext implements Context public function productQuantityShouldBe(string $productName, int $quantity): void { $orderItem = $this->sharedStorage->get('item'); + $quantityKey = $this->getNormalizedKey('quantity'); Assert::same($quantity, $orderItem['quantity']); } @@ -1077,9 +1093,12 @@ final readonly class ManagingOrdersContext implements Context AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, ); + $orderItemUnitsKey = $this->getNormalizedKey('orderItemUnits'); + $unitsKey = $this->getNormalizedKey('units'); + $amountKey = $this->getNormalizedKey('amount'); foreach ($adjustments as $adjustment) { - if (in_array($adjustment['orderItemUnit'], $orderItem['units'])) { - Assert::same($this->getTotalAsInt($price), $adjustment['amount']); + if (in_array($adjustment[$orderItemUnitsKey], $orderItem[$unitsKey])) { + Assert::same($this->getTotalAsInt($price), $adjustment[$amountKey]); return; } @@ -1099,9 +1118,12 @@ final readonly class ManagingOrdersContext implements Context AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT, ); + $orderItemUnitsKey = $this->getNormalizedKey('orderItemUnits'); + $unitsKey = $this->getNormalizedKey('units'); + $amountKey = $this->getNormalizedKey('amount'); foreach ($adjustments as $adjustment) { - if (in_array($adjustment['orderItemUnit'], $orderItem['units'])) { - Assert::same($this->getTotalAsInt(trim($price, ' ~')), $adjustment['amount']); + if (in_array($adjustment[$orderItemUnitsKey], $orderItem[$unitsKey])) { + Assert::same($this->getTotalAsInt(trim($price, ' ~')), $adjustment[$amountKey]); return; } @@ -1116,16 +1138,22 @@ final readonly class ManagingOrdersContext implements Context $orderItem = $this->sharedStorage->get('item'); $response = $this->getAdjustmentsResponseForOrder(true); + $typeKey = $this->getNormalizedKey('type'); + $orderItemUnitsKey = $this->getNormalizedKey('orderItemUnits'); + $unitsKey = $this->getNormalizedKey('units'); + $amountKey = $this->getNormalizedKey('amount'); $unitPromotionAdjustments = 0; foreach ($this->responseChecker->getCollection($response) as $adjustment) { - if (in_array($adjustment['type'], [AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT])) { - if (in_array($adjustment['orderItemUnit'], $orderItem['units'])) { - $unitPromotionAdjustments += $adjustment['amount']; + if (in_array($adjustment[$typeKey], [AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT])) { + if (in_array($adjustment[$orderItemUnitsKey], $orderItem[$unitsKey])) { + $unitPromotionAdjustments += $adjustment[$amountKey]; } } } - Assert::same($this->getTotalAsInt($subTotal), $orderItem['unitPrice'] * $orderItem['quantity'] + $unitPromotionAdjustments); + $quantityKey = $this->getNormalizedKey('quantity'); + $unitPriceKey = $this->getNormalizedKey('unitPrice'); + Assert::same($this->getTotalAsInt($subTotal), $orderItem[$unitPriceKey] * $orderItem[$quantityKey] + $unitPromotionAdjustments); } /** @@ -1305,11 +1333,17 @@ final readonly class ManagingOrdersContext implements Context string $city, string $countryName, ): void { - Assert::same($address['firstName'] . ' ' . $address['lastName'], $customerName); - Assert::same($address['street'], $street); - Assert::same($address['postcode'], $postcode); - Assert::same($address['city'], $city); - Assert::same($address['countryCode'], $this->getCountryCodeFromName($countryName)); + $firstNameKey = $this->getNormalizedKey('firstName'); + $lastNameKey = $this->getNormalizedKey('lastName'); + $streetKey = $this->getNormalizedKey('street'); + $postcodeKey = $this->getNormalizedKey('postcode'); + $cityKey = $this->getNormalizedKey('city'); + $countryCodeKey = $this->getNormalizedKey('countryCode'); + Assert::same($address[$firstNameKey] . ' ' . $address[$lastNameKey], $customerName); + Assert::same($address[$streetKey], $street); + Assert::same($address[$postcodeKey], $postcode); + Assert::same($address[$cityKey], $city); + Assert::same($address[$countryCodeKey], $this->getCountryCodeFromName($countryName)); } private function getCountryCodeFromName(string $name): string @@ -1352,4 +1386,9 @@ final readonly class ManagingOrdersContext implements Context forgetResponse: $forgetResponse, ); } + + private function getNormalizedKey(string $key): string + { + return $this->nameConverter ? $this->nameConverter->normalize($key) : $key; + } } diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml index 27d92bb1c4..8b66cc9d94 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml @@ -208,6 +208,7 @@ + From f19adbe7233666a9cbf5715cdda4dfd5629c73c4 Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Thu, 28 May 2026 18:47:01 +0200 Subject: [PATCH 08/11] Simplify convertArrayAttributesRecursively --- src/Sylius/Behat/Client/ApiPlatformClient.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Sylius/Behat/Client/ApiPlatformClient.php b/src/Sylius/Behat/Client/ApiPlatformClient.php index cc7f9b3e01..f958eb308d 100644 --- a/src/Sylius/Behat/Client/ApiPlatformClient.php +++ b/src/Sylius/Behat/Client/ApiPlatformClient.php @@ -397,23 +397,17 @@ final class ApiPlatformClient implements ApiClientInterface } /** - * @param array $data + * @param array $data * - * @return array + * @return array */ private function convertArrayAttributesRecursively(array $data): array { - if (array_is_list($data)) { - return array_map( - fn (mixed $value): mixed => is_array($value) ? $this->convertArrayAttributesRecursively($value) : $value, - $data, - ); - } - $convertedData = []; + /** @var array|bool|int|string|null $value */ foreach ($data as $attribute => $value) { - $convertedAttribute = is_string($attribute) ? $this->getNormalizedKey($attribute) : $attribute; + $convertedAttribute = $this->getNormalizedKey($attribute); $convertedData[$convertedAttribute] = is_array($value) ? $this->convertArrayAttributesRecursively($value) : $value; From 7dbc0ab30cbf9f0acd36775e4db751856113fa08 Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Mon, 1 Jun 2026 10:39:30 +0200 Subject: [PATCH 09/11] Simplify convertArrayAttributesRecursively --- src/Sylius/Behat/Client/ApiPlatformClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sylius/Behat/Client/ApiPlatformClient.php b/src/Sylius/Behat/Client/ApiPlatformClient.php index f958eb308d..16f39c989c 100644 --- a/src/Sylius/Behat/Client/ApiPlatformClient.php +++ b/src/Sylius/Behat/Client/ApiPlatformClient.php @@ -407,7 +407,7 @@ final class ApiPlatformClient implements ApiClientInterface /** @var array|bool|int|string|null $value */ foreach ($data as $attribute => $value) { - $convertedAttribute = $this->getNormalizedKey($attribute); + $convertedAttribute = is_string($attribute) ? $this->getNormalizedKey($attribute) : $attribute; $convertedData[$convertedAttribute] = is_array($value) ? $this->convertArrayAttributesRecursively($value) : $value; From 1484d67c1fc58583554f7e22c388f7339022fd68 Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Mon, 1 Jun 2026 10:41:13 +0200 Subject: [PATCH 10/11] fix: missing and wrong keys --- .../Context/Api/Admin/ManagingOrdersContext.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php index 45137814ad..173f15bc61 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php @@ -1077,7 +1077,7 @@ final readonly class ManagingOrdersContext implements Context { $orderItem = $this->sharedStorage->get('item'); $quantityKey = $this->getNormalizedKey('quantity'); - Assert::same($quantity, $orderItem['quantity']); + Assert::same($quantity, $orderItem[$quantityKey]); } /** @@ -1093,11 +1093,11 @@ final readonly class ManagingOrdersContext implements Context AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, ); - $orderItemUnitsKey = $this->getNormalizedKey('orderItemUnits'); + $orderItemUnitKey = $this->getNormalizedKey('orderItemUnit'); $unitsKey = $this->getNormalizedKey('units'); $amountKey = $this->getNormalizedKey('amount'); foreach ($adjustments as $adjustment) { - if (in_array($adjustment[$orderItemUnitsKey], $orderItem[$unitsKey])) { + if (in_array($adjustment[$orderItemUnitKey], $orderItem[$unitsKey])) { Assert::same($this->getTotalAsInt($price), $adjustment[$amountKey]); return; @@ -1118,11 +1118,11 @@ final readonly class ManagingOrdersContext implements Context AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT, ); - $orderItemUnitsKey = $this->getNormalizedKey('orderItemUnits'); + $orderItemUnitKey = $this->getNormalizedKey('orderItemUnit'); $unitsKey = $this->getNormalizedKey('units'); $amountKey = $this->getNormalizedKey('amount'); foreach ($adjustments as $adjustment) { - if (in_array($adjustment[$orderItemUnitsKey], $orderItem[$unitsKey])) { + if (in_array($adjustment[$orderItemUnitKey], $orderItem[$unitsKey])) { Assert::same($this->getTotalAsInt(trim($price, ' ~')), $adjustment[$amountKey]); return; @@ -1139,13 +1139,13 @@ final readonly class ManagingOrdersContext implements Context $response = $this->getAdjustmentsResponseForOrder(true); $typeKey = $this->getNormalizedKey('type'); - $orderItemUnitsKey = $this->getNormalizedKey('orderItemUnits'); + $orderItemUnitKey = $this->getNormalizedKey('orderItemUnit'); $unitsKey = $this->getNormalizedKey('units'); $amountKey = $this->getNormalizedKey('amount'); $unitPromotionAdjustments = 0; foreach ($this->responseChecker->getCollection($response) as $adjustment) { if (in_array($adjustment[$typeKey], [AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT])) { - if (in_array($adjustment[$orderItemUnitsKey], $orderItem[$unitsKey])) { + if (in_array($adjustment[$orderItemUnitKey], $orderItem[$unitsKey])) { $unitPromotionAdjustments += $adjustment[$amountKey]; } } From b7158f3fb087b60c86a27a5ebf57f97a39d6d11d Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Mon, 1 Jun 2026 19:34:41 +0200 Subject: [PATCH 11/11] feat: implement NormalizedKeyAwareTrait for key normalization across contexts --- ...CatalogPromotionProductVariantsContext.php | 17 ++- .../Admin/ChannelPricingLogEntryContext.php | 9 +- .../ManagingChannelsBillingDataContext.php | 17 ++- .../Admin/ManagingExchangeRatesContext.php | 13 +- .../Api/Admin/ManagingOrdersContext.php | 22 ++-- .../Admin/ManagingProductVariantsContext.php | 26 +++- .../Api/Admin/ManagingProductsContext.php | 51 +++++--- .../Api/Admin/ManagingPromotionsContext.php | 22 +++- .../Context/Api/NormalizedKeyAwareTrait.php | 22 ++++ .../Behat/Context/Api/Shop/AddressContext.php | 43 ++++-- .../Behat/Context/Api/Shop/CartContext.php | 103 ++++++++++----- .../Checkout/CheckoutOrderDetailsContext.php | 7 +- .../Shop/Checkout/CheckoutShippingContext.php | 11 +- .../Context/Api/Shop/CheckoutContext.php | 122 +++++++++++------- .../Context/Api/Shop/ExchangeRateContext.php | 11 +- .../Behat/Context/Api/Shop/OrderContext.php | 50 ++++--- .../Behat/Context/Api/Shop/PaymentContext.php | 7 +- .../Api/Shop/PaymentRequestContext.php | 11 +- .../Behat/Context/Api/Shop/ProductContext.php | 83 +++++++----- .../Api/Shop/ProductVariantContext.php | 49 +++---- .../Context/Api/Shop/RegistrationContext.php | 14 +- .../Context/Api/Shop/ShipmentContext.php | 7 +- .../config/services/contexts/api/admin.xml | 7 + .../config/services/contexts/api/shop.xml | 13 ++ 24 files changed, 502 insertions(+), 235 deletions(-) create mode 100644 src/Sylius/Behat/Context/Api/NormalizedKeyAwareTrait.php diff --git a/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php index 94f22db40e..97fdcd15f5 100644 --- a/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php @@ -17,19 +17,24 @@ use ApiPlatform\Metadata\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Component\Core\Model\CatalogPromotionInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class BrowsingCatalogPromotionProductVariantsContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, + private ?NameConverterInterface $nameConverter, ) { } @@ -145,9 +150,13 @@ final class BrowsingCatalogPromotionProductVariantsContext implements Context ): bool { $variantData = $this->getDataOfVariantWithCode($variant->getCode()); - $promotions = $variantData['channelPricings'][$channel->getCode()]['appliedPromotions'] ?? []; + $channelPricingsKey = $this->getNormalizedKey('channelPricings'); + $appliedPromotionsKey = $this->getNormalizedKey('appliedPromotions'); + $codeKey = $this->getNormalizedKey('code'); + + $promotions = $variantData[$channelPricingsKey][$channel->getCode()][$appliedPromotionsKey] ?? []; foreach ($promotions as $promotion) { - if ($promotion['code'] === $catalogPromotion->getCode()) { + if ($promotion[$codeKey] === $catalogPromotion->getCode()) { return true; } } @@ -158,8 +167,10 @@ final class BrowsingCatalogPromotionProductVariantsContext implements Context private function getDataOfVariantWithCode(string $code): array { $variantsData = $this->responseChecker->getCollection($this->client->getLastResponse()); + $codeKey = $this->getNormalizedKey('code'); + foreach ($variantsData as $variantData) { - if ($variantData['code'] === $code) { + if ($variantData[$codeKey] === $code) { return $variantData; } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ChannelPricingLogEntryContext.php b/src/Sylius/Behat/Context/Api/Admin/ChannelPricingLogEntryContext.php index b9a9177380..958282de32 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ChannelPricingLogEntryContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ChannelPricingLogEntryContext.php @@ -16,17 +16,22 @@ namespace Sylius\Behat\Context\Api\Admin; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\ProductVariantInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class ChannelPricingLogEntryContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private SharedStorageInterface $sharedStorage, + private ?NameConverterInterface $nameConverter, ) { } @@ -69,8 +74,8 @@ final class ChannelPricingLogEntryContext implements Context $logEntry = $this->responseChecker->getCollection($this->client->getLastResponse())[$position - 1]; - Assert::same($logEntry['price'], $price); - Assert::same($logEntry['originalPrice'], $originalPrice); + Assert::same($logEntry[$this->getNormalizedKey('price')], $price); + Assert::same($logEntry[$this->getNormalizedKey('originalPrice')], $originalPrice); Assert::keyExists($logEntry, 'loggedAt'); } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsBillingDataContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsBillingDataContext.php index cf23806728..cd24e54eca 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsBillingDataContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsBillingDataContext.php @@ -16,16 +16,21 @@ namespace Sylius\Behat\Context\Api\Admin; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Component\Addressing\Model\CountryInterface; use Sylius\Component\Core\Model\ChannelInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class ManagingChannelsBillingDataContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, + private ?NameConverterInterface $nameConverter, ) { } @@ -36,7 +41,7 @@ final readonly class ManagingChannelsBillingDataContext implements Context { $shopBillingData = $this->getShopBillingDataFromChannel($channel); - Assert::same($shopBillingData['company'], $company); + Assert::same($shopBillingData[$this->getNormalizedKey('company')], $company); } /** @@ -46,7 +51,7 @@ final readonly class ManagingChannelsBillingDataContext implements Context { $shopBillingData = $this->getShopBillingDataFromChannel($channel); - Assert::same($shopBillingData['taxId'], $taxId); + Assert::same($shopBillingData[$this->getNormalizedKey('taxId')], $taxId); } /** @@ -62,10 +67,10 @@ final readonly class ManagingChannelsBillingDataContext implements Context ): void { $shopBillingData = $this->getShopBillingDataFromChannel($channel); - Assert::same($shopBillingData['street'], $street); - Assert::same($shopBillingData['postcode'], $postcode); - Assert::same($shopBillingData['city'], $city); - Assert::same($shopBillingData['countryCode'], $country->getCode()); + Assert::same($shopBillingData[$this->getNormalizedKey('street')], $street); + Assert::same($shopBillingData[$this->getNormalizedKey('postcode')], $postcode); + Assert::same($shopBillingData[$this->getNormalizedKey('city')], $city); + Assert::same($shopBillingData[$this->getNormalizedKey('countryCode')], $country->getCode()); } /** diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingExchangeRatesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingExchangeRatesContext.php index ff70e5123c..add62d4505 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingExchangeRatesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingExchangeRatesContext.php @@ -16,19 +16,24 @@ namespace Sylius\Behat\Context\Api\Admin; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Currency\Model\CurrencyInterface; use Sylius\Component\Currency\Model\ExchangeRateInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class ManagingExchangeRatesContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private SharedStorageInterface $sharedStorage, private string $apiUrlPrefix, + private ?NameConverterInterface $nameConverter, ) { } @@ -237,7 +242,7 @@ final readonly class ManagingExchangeRatesContext implements Context $exchangeRate->getTargetCurrency(), ); - Assert::same($exchangeRate['ratio'], $ratio); + Assert::same($exchangeRate[$this->getNormalizedKey('ratio')], $ratio); } /** @@ -361,8 +366,8 @@ final readonly class ManagingExchangeRatesContext implements Context /** @var array $item */ foreach ($this->responseChecker->getCollection($this->client->index(Resources::EXCHANGE_RATES)) as $item) { if ( - $item['sourceCurrency'] === sprintf('%s/admin/currencies/%s', $this->apiUrlPrefix, $sourceCurrency->getCode()) && - $item['targetCurrency'] === sprintf('%s/admin/currencies/%s', $this->apiUrlPrefix, $targetCurrency->getCode()) + $item[$this->getNormalizedKey('sourceCurrency')] === sprintf('%s/admin/currencies/%s', $this->apiUrlPrefix, $sourceCurrency->getCode()) && + $item[$this->getNormalizedKey('targetCurrency')] === sprintf('%s/admin/currencies/%s', $this->apiUrlPrefix, $targetCurrency->getCode()) ) { return $item; } @@ -382,6 +387,6 @@ final readonly class ManagingExchangeRatesContext implements Context return false; } - return $exchangeRateResponse['ratio'] === $ratio; + return $exchangeRateResponse[$this->getNormalizedKey('ratio')] === $ratio; } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php index 173f15bc61..74be0a50e4 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php @@ -327,7 +327,7 @@ final readonly class ManagingOrdersContext implements Context /** @var string $lastResponseContent */ $lastResponseContent = $this->client->getLastResponse()->getContent(); /** @var array{productName: string}[] $items */ - $items = json_decode($lastResponseContent, true)['items']; + $items = json_decode($lastResponseContent, true)[$this->getNormalizedKey('items')]; $productNameKey = $this->getNormalizedKey('productName'); foreach ($items as $item) { @@ -730,7 +730,7 @@ final readonly class ManagingOrdersContext implements Context $items = $this->responseChecker->getValue($this->client->getLastResponse(), 'hydra:member'); $firstItem = $items[0]; - Assert::same($firstItem['number'], str_replace('#', '', $number)); + Assert::same($firstItem[$this->getNormalizedKey('number')], str_replace('#', '', $number)); } /** @@ -745,7 +745,7 @@ final readonly class ManagingOrdersContext implements Context )[0]; Assert::same( - $order['total'], + $order[$this->getNormalizedKey('total')], $total, ); } @@ -773,7 +773,7 @@ final readonly class ManagingOrdersContext implements Context $firstItem = array_pop($itemsWithCurrency); Assert::notEmpty($firstItem); - Assert::same($firstItem['total'], $total); + Assert::same($firstItem[$this->getNormalizedKey('total')], $total); } /** @@ -793,7 +793,7 @@ final readonly class ManagingOrdersContext implements Context public function itShouldBeShippedViaTheShippingMethod(ShippingMethodInterface $shippingMethod): void { Assert::same( - $this->responseChecker->getValue($this->client->getLastResponse(), 'shipments')[0]['method'], + $this->responseChecker->getValue($this->client->getLastResponse(), 'shipments')[0][$this->getNormalizedKey('method')], $this->iriConverter->getIriFromResource($shippingMethod), ); } @@ -804,7 +804,7 @@ final readonly class ManagingOrdersContext implements Context public function itShouldBePaidWith(PaymentMethodInterface $paymentMethod): void { Assert::same( - $this->responseChecker->getValue($this->client->getLastResponse(), 'payments')[0]['method'], + $this->responseChecker->getValue($this->client->getLastResponse(), 'payments')[0][$this->getNormalizedKey('method')], $this->iriConverter->getIriFromResource($paymentMethod), ); } @@ -912,7 +912,7 @@ final readonly class ManagingOrdersContext implements Context */ public function itemTotalShouldBe(array $orderItem, string $total): void { - Assert::same($this->getTotalAsInt($total), $orderItem['total']); + Assert::same($this->getTotalAsInt($total), $orderItem[$this->getNormalizedKey('total')]); } /** @@ -920,7 +920,7 @@ final readonly class ManagingOrdersContext implements Context */ public function itemCodeShouldBe(array $orderItem, string $code): void { - Assert::endsWith($orderItem['variant'], $code); + Assert::endsWith($orderItem[$this->getNormalizedKey('variant')], $code); } /** @@ -928,7 +928,7 @@ final readonly class ManagingOrdersContext implements Context */ public function itemQuantityShouldBe(array $orderItem, int $quantity): void { - Assert::same($quantity, $orderItem['quantity']); + Assert::same($quantity, $orderItem[$this->getNormalizedKey('quantity')]); } /** @@ -1007,8 +1007,8 @@ final readonly class ManagingOrdersContext implements Context $totalTax = 0; foreach ($unitPromotionAdjustments as $unitPromotionAdjustment) { - if (true === $unitPromotionAdjustment['neutral']) { - $totalTax += $unitPromotionAdjustment['amount']; + if (true === $unitPromotionAdjustment[$this->getNormalizedKey('neutral')]) { + $totalTax += $unitPromotionAdjustment[$this->getNormalizedKey('amount')]; } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php index f65ec59b23..9f700cd50f 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php @@ -17,6 +17,7 @@ use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Admin\Helper\ValidationTrait; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\Converter\IriConverterInterface; use Sylius\Component\Core\Formatter\StringInflector; @@ -27,11 +28,13 @@ use Sylius\Component\Product\Model\ProductOptionInterface; use Sylius\Component\Product\Model\ProductOptionValueInterface; use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; use Sylius\Component\Shipping\Model\ShippingCategoryInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class ManagingProductVariantsContext implements Context { use ValidationTrait; + use NormalizedKeyAwareTrait; private const FIRST_COLLECTION_ITEM = 0; @@ -42,6 +45,7 @@ final class ManagingProductVariantsContext implements Context private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, + private ?NameConverterInterface $nameConverter, ) { } @@ -374,12 +378,15 @@ final class ManagingProductVariantsContext implements Context 'name' => $name, ]; - $translationInLocale = $response[self::FIRST_COLLECTION_ITEM]['translations'][$localeCode]; + $translationsKey = $this->getNormalizedKey('translations'); + $nameKey = $this->getNormalizedKey('name'); + + $translationInLocale = $response[self::FIRST_COLLECTION_ITEM][$translationsKey][$localeCode]; Assert::allInArray( $expectedTranslation, $translationInLocale, - sprintf('Expected translation %s, got %s', $expectedTranslation['name'], $translationInLocale['name']), + sprintf('Expected translation %s, got %s', $expectedTranslation['name'], $translationInLocale[$nameKey]), ); } @@ -394,7 +401,10 @@ final class ManagingProductVariantsContext implements Context ): void { $response = $this->responseChecker->getCollection($this->client->index(Resources::PRODUCT_VARIANTS)); - Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['price'], $price); + $channelPricingsKey = $this->getNormalizedKey('channelPricings'); + $priceKey = $this->getNormalizedKey('price'); + + Assert::same($response[self::FIRST_COLLECTION_ITEM][$channelPricingsKey][$channel->getCode()][$priceKey], $price); } /** @@ -408,7 +418,10 @@ final class ManagingProductVariantsContext implements Context ): void { $response = $this->responseChecker->getCollection($this->client->index(Resources::PRODUCT_VARIANTS)); - Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['originalPrice'], $originalPrice); + $channelPricingsKey = $this->getNormalizedKey('channelPricings'); + $originalPriceKey = $this->getNormalizedKey('originalPrice'); + + Assert::same($response[self::FIRST_COLLECTION_ITEM][$channelPricingsKey][$channel->getCode()][$originalPriceKey], $originalPrice); } /** @@ -428,7 +441,10 @@ final class ManagingProductVariantsContext implements Context { $response = $this->responseChecker->getCollection($this->client->index(Resources::PRODUCT_VARIANTS)); - Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['minimumPrice'], $minimumPrice); + $channelPricingsKey = $this->getNormalizedKey('channelPricings'); + $minimumPriceKey = $this->getNormalizedKey('minimumPrice'); + + Assert::same($response[self::FIRST_COLLECTION_ITEM][$channelPricingsKey][$channel->getCode()][$minimumPriceKey], $minimumPrice); } /** diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php index 1e855f9cc5..5d3118e191 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php @@ -18,6 +18,7 @@ use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\RequestBuilder; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Admin\Helper\ValidationTrait; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\Converter\IriConverterInterface; use Sylius\Behat\Service\SharedStorageInterface; @@ -35,11 +36,13 @@ use Sylius\Component\Product\Model\ProductAttributeInterface; use Sylius\Component\Product\Model\ProductOptionInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class ManagingProductsContext implements Context { use ValidationTrait; + use NormalizedKeyAwareTrait; public const SORT_TYPES = ['ascending' => 'asc', 'descending' => 'desc']; @@ -49,6 +52,7 @@ final readonly class ManagingProductsContext implements Context private IriConverterInterface $iriConverter, private SharedStorageInterface $sharedStorage, private string $apiUrlPrefix, + private ?NameConverterInterface $nameConverter, ) { } @@ -360,10 +364,12 @@ final readonly class ManagingProductsContext implements Context { $images = $this->responseChecker->getValue($this->client->showByIri($this->sharedStorage->get('productIri')), 'images'); $productCode = $this->responseChecker->getValue($this->client->getLastResponse(), 'code'); + $typeKey = $this->getNormalizedKey('type'); + $idKey = $this->getNormalizedKey('id'); foreach ($images as $key => $imageData) { - if ($imageData['type'] === $imageType) { - $imageId = $imageData['id']; + if ($imageData[$typeKey] === $imageType) { + $imageId = $imageData[$idKey]; } } @@ -541,7 +547,7 @@ final readonly class ManagingProductsContext implements Context public function iShouldSeeVariants(int $count): void { Assert::count( - $this->responseChecker->getResponseContent($this->client->getLastResponse())['variants'] ?? [], + $this->responseChecker->getResponseContent($this->client->getLastResponse())[$this->getNormalizedKey('variants')] ?? [], $count, ); } @@ -650,8 +656,11 @@ final readonly class ManagingProductsContext implements Context { $images = $this->responseChecker->getValue($this->client->showByIri($this->sharedStorage->get('productIri')), 'images'); - Assert::same($images[count($images) - 2]['type'], $imageType); - Assert::same($images[count($images) - 2]['position'], $position); + $typeKey = $this->getNormalizedKey('type'); + $positionKey = $this->getNormalizedKey('position'); + + Assert::same($images[count($images) - 2][$typeKey], $imageType); + Assert::same($images[count($images) - 2][$positionKey], $position); } /** @@ -661,8 +670,11 @@ final readonly class ManagingProductsContext implements Context { $images = $this->responseChecker->getValue($this->client->showByIri($this->sharedStorage->get('productIri')), 'images'); - Assert::same($images[count($images) - 1]['type'], $imageType); - Assert::same($images[count($images) - 1]['position'], $position); + $typeKey = $this->getNormalizedKey('type'); + $positionKey = $this->getNormalizedKey('position'); + + Assert::same($images[count($images) - 1][$typeKey], $imageType); + Assert::same($images[count($images) - 1][$positionKey], $position); } /** @@ -831,7 +843,7 @@ final readonly class ManagingProductsContext implements Context $productFromResponse = $this->responseChecker->getResponseContent($response); Assert::true( - in_array($this->iriConverter->getIriFromResourceInSection($productOption, 'admin'), $productFromResponse['options'], true), + in_array($this->iriConverter->getIriFromResourceInSection($productOption, 'admin'), $productFromResponse[$this->getNormalizedKey('options')], true), sprintf('Product with option %s does not exist', $productOption->getName()), ); } @@ -976,8 +988,9 @@ final readonly class ManagingProductsContext implements Context public function productShouldNotHaveAttribute(ProductInterface $product, ProductAttributeInterface $attribute): void { $attributes = $this->responseChecker->getValue($this->client->getLastResponse(), 'attributes'); + $attributeKey = $this->getNormalizedKey('attribute'); foreach ($attributes as $attributeValue) { - if ($attributeValue['attribute'] === $this->iriConverter->getIriFromResourceInSection($attribute, 'admin')) { + if ($attributeValue[$attributeKey] === $this->iriConverter->getIriFromResourceInSection($attribute, 'admin')) { throw new \InvalidArgumentException( sprintf('Product %s have attribute %s', $product->getName(), $attribute->getName()), ); @@ -1093,11 +1106,14 @@ final readonly class ManagingProductsContext implements Context private function getFieldValueOfProduct(array $product, string $field): ?string { if ($field === 'code') { - return $product['code']; + return $product[$this->getNormalizedKey('code')]; } if ($field === 'name') { - return $product['translations'][$this->getAdminLocaleCode()]['name'] ?? null; + $translationsKey = $this->getNormalizedKey('translations'); + $nameKey = $this->getNormalizedKey('name'); + + return $product[$translationsKey][$this->getAdminLocaleCode()][$nameKey] ?? null; } return null; @@ -1106,10 +1122,12 @@ final readonly class ManagingProductsContext implements Context private function hasProductImage(Response $response, ProductInterface $product): bool { $productFromResponse = $this->responseChecker->getResponseContent($response); + $imagesKey = $this->getNormalizedKey('images'); + $pathKey = $this->getNormalizedKey('path'); return - isset($productFromResponse['images'][0]) && - str_contains($productFromResponse['images'][0]['path'], $product->getImages()->first()->getPath()) + isset($productFromResponse[$imagesKey][0]) && + str_contains($productFromResponse[$imagesKey][0][$pathKey], $product->getImages()->first()->getPath()) ; } @@ -1171,9 +1189,12 @@ final readonly class ManagingProductsContext implements Context $attributeIri = $this->iriConverter->getIriFromResourceInSection($attribute, 'admin'); $attributes = $this->responseChecker->getValue($this->client->getLastResponse(), 'attributes'); + $attributeKey = $this->getNormalizedKey('attribute'); + $localeCodeKey = $this->getNormalizedKey('localeCode'); + $valueKey = $this->getNormalizedKey('value'); foreach ($attributes as $attributeValue) { - if ($attributeValue['attribute'] === $attributeIri && $attributeValue['localeCode'] === $localeCode) { - $this->assertAttributeValue($value, $attributeValue['value']); + if ($attributeValue[$attributeKey] === $attributeIri && $attributeValue[$localeCodeKey] === $localeCode) { + $this->assertAttributeValue($value, $attributeValue[$valueKey]); return; } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php index 12efb51785..52472af97a 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php @@ -18,6 +18,7 @@ use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Admin\Helper\ValidationTrait; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; @@ -35,16 +36,19 @@ use Sylius\Component\Core\Promotion\Checker\Rule\ItemTotalRuleChecker; use Sylius\Component\Core\Promotion\Checker\Rule\TotalOfItemsFromTaxonRuleChecker; use Sylius\Component\Customer\Model\CustomerGroupInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class ManagingPromotionsContext implements Context { use ValidationTrait; + use NormalizedKeyAwareTrait; public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, + private ?NameConverterInterface $nameConverter, ) { } @@ -509,7 +513,7 @@ final readonly class ManagingPromotionsContext implements Context )); Assert::true( - $returnedPromotion['couponBased'], + $returnedPromotion[$this->getNormalizedKey('couponBased')], sprintf('The promotion %s isn\'t coupon based', $promotion->getName()), ); } @@ -659,9 +663,12 @@ final readonly class ManagingPromotionsContext implements Context public function itShouldHaveOfItemPercentageDiscount(float $percentage, ChannelInterface $channel): void { $actions = $this->responseChecker->getValue($this->client->getLastResponse(), 'actions'); + $typeKey = $this->getNormalizedKey('type'); + $configurationKey = $this->getNormalizedKey('configuration'); + $percentageKey = $this->getNormalizedKey('percentage'); foreach ($actions as $action) { - if ($action['type'] === 'unit_percentage_discount') { - Assert::same((float) $action['configuration'][$channel->getCode()]['percentage'], $percentage); + if ($action[$typeKey] === 'unit_percentage_discount') { + Assert::same((float) $action[$configurationKey][$channel->getCode()][$percentageKey], $percentage); } } } @@ -672,7 +679,10 @@ final readonly class ManagingPromotionsContext implements Context public function itShouldHaveOfOrderPercentageDiscount(float $percentage): void { $actions = $this->responseChecker->getValue($this->client->getLastResponse(), 'actions'); - Assert::same((float) $actions[0]['configuration']['percentage'], $percentage); + Assert::same( + (float) $actions[0][$this->getNormalizedKey('configuration')][$this->getNormalizedKey('percentage')], + $percentage, + ); } /** @@ -891,9 +901,9 @@ final readonly class ManagingPromotionsContext implements Context )); Assert::same( - $returnedPromotion['used'], + $returnedPromotion[$this->getNormalizedKey('used')], $usage, - sprintf('The promotion %s has been used %s times', $promotion->getName(), $returnedPromotion['used']), + sprintf('The promotion %s has been used %s times', $promotion->getName(), $returnedPromotion[$this->getNormalizedKey('used')]), ); } diff --git a/src/Sylius/Behat/Context/Api/NormalizedKeyAwareTrait.php b/src/Sylius/Behat/Context/Api/NormalizedKeyAwareTrait.php new file mode 100644 index 0000000000..76b93077f5 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/NormalizedKeyAwareTrait.php @@ -0,0 +1,22 @@ +nameConverter?->normalize($key) ?? $key; + } +} diff --git a/src/Sylius/Behat/Context/Api/Shop/AddressContext.php b/src/Sylius/Behat/Context/Api/Shop/AddressContext.php index 5d7c7a84fa..9c1debafeb 100644 --- a/src/Sylius/Behat/Context/Api/Shop/AddressContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/AddressContext.php @@ -16,20 +16,25 @@ namespace Sylius\Behat\Context\Api\Shop; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\Converter\IriConverterInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Addressing\Model\ProvinceInterface; use Sylius\Component\Core\Model\AddressInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class AddressContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, private SharedStorageInterface $sharedStorage, + private ?NameConverterInterface $nameConverter, ) { } @@ -359,7 +364,7 @@ final readonly class AddressContext implements Context { $response = $this->responseChecker->getResponseContent($this->client->getLastResponse()); - Assert::same(count($response['violations']), $expectedCount); + Assert::same(count($response[$this->getNormalizedKey('violations')]), $expectedCount); } /** @@ -438,16 +443,25 @@ final readonly class AddressContext implements Context private function addressBookHasAddress(array $addressBook, AddressInterface $addressToCompare): bool { + $firstNameKey = $this->getNormalizedKey('firstName'); + $lastNameKey = $this->getNormalizedKey('lastName'); + $countryCodeKey = $this->getNormalizedKey('countryCode'); + $streetKey = $this->getNormalizedKey('street'); + $cityKey = $this->getNormalizedKey('city'); + $postcodeKey = $this->getNormalizedKey('postcode'); + $provinceNameKey = $this->getNormalizedKey('provinceName'); + $provinceCodeKey = $this->getNormalizedKey('provinceCode'); + foreach ($addressBook as $address) { if ( - $address['firstName'] === $addressToCompare->getFirstName() && - $address['lastName'] === $addressToCompare->getLastName() && - $address['countryCode'] === $addressToCompare->getCountryCode() && - $address['street'] === $addressToCompare->getStreet() && - $address['city'] === $addressToCompare->getCity() && - $address['postcode'] === $addressToCompare->getPostcode() && - $address['provinceName'] === $addressToCompare->getProvinceName() && - $address['provinceCode'] === $addressToCompare->getProvinceCode() + $address[$firstNameKey] === $addressToCompare->getFirstName() && + $address[$lastNameKey] === $addressToCompare->getLastName() && + $address[$countryCodeKey] === $addressToCompare->getCountryCode() && + $address[$streetKey] === $addressToCompare->getStreet() && + $address[$cityKey] === $addressToCompare->getCity() && + $address[$postcodeKey] === $addressToCompare->getPostcode() && + $address[$provinceNameKey] === $addressToCompare->getProvinceName() && + $address[$provinceCodeKey] === $addressToCompare->getProvinceCode() ) { return true; } @@ -460,12 +474,15 @@ final readonly class AddressContext implements Context { Assert::notNull($fullName); [$firstName, $lastName] = explode(' ', $fullName); + $firstNameKey = $this->getNormalizedKey('firstName'); + $lastNameKey = $this->getNormalizedKey('lastName'); + $idKey = $this->getNormalizedKey('id'); $addresses = $this->responseChecker->getCollection($this->client->getLastResponse()); /** @var AddressInterface $address */ foreach ($addresses as $address) { - if ($firstName === $address['firstName'] && $lastName === $address['lastName']) { - return (string) $address['id']; + if ($firstName === $address[$firstNameKey] && $lastName === $address[$lastNameKey]) { + return (string) $address[$idKey]; } } @@ -487,11 +504,13 @@ final readonly class AddressContext implements Context { Assert::notNull($fullName); [$firstName, $lastName] = explode(' ', $fullName); + $firstNameKey = $this->getNormalizedKey('firstName'); + $lastNameKey = $this->getNormalizedKey('lastName'); $addresses = $this->responseChecker->getCollection($this->client->index(Resources::ADDRESSES)); /** @var AddressInterface $address */ foreach ($addresses as $address) { - if ($firstName === $address['firstName'] && $lastName === $address['lastName']) { + if ($firstName === $address[$firstNameKey] && $lastName === $address[$lastNameKey]) { return $address['@id']; } } diff --git a/src/Sylius/Behat/Context/Api/Shop/CartContext.php b/src/Sylius/Behat/Context/Api/Shop/CartContext.php index b0c20df8dc..a64e6e178c 100644 --- a/src/Sylius/Behat/Context/Api/Shop/CartContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/CartContext.php @@ -20,6 +20,7 @@ use Behat\Step\When; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\RequestFactoryInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Behat\Service\SprintfResponseEscaper; @@ -31,10 +32,13 @@ use Sylius\Component\Locale\Model\LocaleInterface; use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class CartContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $shopClient, private ApiClientInterface $adminClient, @@ -45,6 +49,7 @@ final class CartContext implements Context private RequestFactoryInterface $requestFactory, private string $apiUrlPrefix, private OrderRepositoryInterface $orderRepository, + private ?NameConverterInterface $nameConverter, ) { } @@ -146,19 +151,24 @@ final class CartContext implements Context string $productOptionValue, ): void { $productData = json_decode($this->shopClient->show(Resources::PRODUCTS, $product->getCode())->getContent(), true, 512, \JSON_THROW_ON_ERROR); + $optionsKey = $this->getNormalizedKey('options'); + $nameKey = $this->getNormalizedKey('name'); + $valuesKey = $this->getNormalizedKey('values'); + $valueKey = $this->getNormalizedKey('value'); + $codeKey = $this->getNormalizedKey('code'); $variantIri = null; - foreach ($productData['options'] as $optionIri) { + foreach ($productData[$optionsKey] as $optionIri) { $optionData = json_decode($this->shopClient->showByIri($optionIri)->getContent(), true, 512, \JSON_THROW_ON_ERROR); - if ($optionData['name'] !== $productOption) { + if ($optionData[$nameKey] !== $productOption) { continue; } - foreach ($optionData['values'] as $valueIri) { + foreach ($optionData[$valuesKey] as $valueIri) { $optionValueData = json_decode($this->shopClient->showByIri($valueIri)->getContent(), true, 512, \JSON_THROW_ON_ERROR); - if ($optionValueData['value'] !== $productOptionValue) { + if ($optionValueData[$valueKey] !== $productOptionValue) { continue; } @@ -170,7 +180,7 @@ final class CartContext implements Context Assert::same($variantsData['hydra:totalItems'], 1); - $variantIri = $variantsData['@id'] . '/' . $variantsData['hydra:member'][0]['code']; + $variantIri = $variantsData['@id'] . '/' . $variantsData['hydra:member'][0][$codeKey]; } } @@ -188,7 +198,7 @@ final class CartContext implements Context 'items', ); $request->updateContent([ - 'productCode' => $productData['code'], + 'productCode' => $productData[$codeKey], 'productVariant' => $variantIri, 'quantity' => 1, ]); @@ -211,14 +221,14 @@ final class CartContext implements Context } $itemResponse = $this->getOrderItemResponseFromProductInCart($product, $tokenValue); - $this->changeQuantityOfOrderItem((string) $itemResponse['id'], $quantity, $tokenValue); + $this->changeQuantityOfOrderItem((string) $itemResponse[$this->getNormalizedKey('id')], $quantity, $tokenValue); } #[When('/^I remove (product "[^"]+") from the (cart)$/')] public function iRemoveProductFromTheCart(ProductInterface $product, string $tokenValue): void { $itemResponse = $this->getOrderItemResponseFromProductInCart($product, $tokenValue); - $this->removeOrderItemFromCart((string) $itemResponse['id'], $tokenValue); + $this->removeOrderItemFromCart((string) $itemResponse[$this->getNormalizedKey('id')], $tokenValue); } /** @@ -227,7 +237,7 @@ final class CartContext implements Context public function iRemoveVariantFromTheCart(ProductVariantInterface $variant, string $tokenValue): void { $itemResponse = $this->getOrderItemResponseFromProductVariantInCart($variant, $tokenValue); - $this->removeOrderItemFromCart((string) $itemResponse['id'], $tokenValue); + $this->removeOrderItemFromCart((string) $itemResponse[$this->getNormalizedKey('id')], $tokenValue); } /** @@ -448,10 +458,12 @@ final class CartContext implements Context public function iShouldSeeProductWithUnitPriceInMyCart(string $productName, int $unitPrice): void { $response = $this->shopClient->getLastResponse(); + $productNameKey = $this->getNormalizedKey('productName'); + $unitPriceKey = $this->getNormalizedKey('unitPrice'); foreach ($this->responseChecker->getValue($response, 'items') as $item) { - if ($item['productName'] === $productName) { - Assert::same($item['unitPrice'], $unitPrice); + if ($item[$productNameKey] === $productName) { + Assert::same($item[$unitPriceKey], $unitPrice); return; } @@ -467,10 +479,12 @@ final class CartContext implements Context public function iShouldSeeProductWithDiscountedUnitPriceInMyCart(string $productName, int $discountedUnitPrice): void { $response = $this->shopClient->getLastResponse(); + $productNameKey = $this->getNormalizedKey('productName'); + $discountedUnitPriceKey = $this->getNormalizedKey('discountedUnitPrice'); foreach ($this->responseChecker->getValue($response, 'items') as $item) { - if ($item['productName'] === $productName) { - Assert::same($item['discountedUnitPrice'], $discountedUnitPrice); + if ($item[$productNameKey] === $productName) { + Assert::same($item[$discountedUnitPriceKey], $discountedUnitPrice); return; } @@ -486,10 +500,12 @@ final class CartContext implements Context public function theProductShouldHaveTotalPriceInTheCart(string $productName, int $totalPrice): void { $response = $this->shopClient->getLastResponse(); + $productNameKey = $this->getNormalizedKey('productName'); + $totalKey = $this->getNormalizedKey('total'); foreach ($this->responseChecker->getValue($response, 'items') as $item) { - if ($item['productName'] === $productName) { - Assert::same($item['total'], $totalPrice); + if ($item[$productNameKey] === $productName) { + Assert::same($item[$totalKey], $totalPrice); return; } @@ -510,7 +526,7 @@ final class CartContext implements Context Assert::count($items, 1); if (null !== $productName) { - Assert::same($items[0]['productName'], $productName); + Assert::same($items[0][$this->getNormalizedKey('productName')], $productName); } $this->sharedStorage->set('item', $items[0]); @@ -767,19 +783,23 @@ final class CartContext implements Context public function thisItemShouldHaveOptionValue(string $expectedOptionName, string $expectedOptionValueValue): void { $item = $this->sharedStorage->get('item'); + $variantKey = $this->getNormalizedKey('variant'); + $valueKey = $this->getNormalizedKey('value'); + $optionKey = $this->getNormalizedKey('option'); + $nameKey = $this->getNormalizedKey('name'); - $optionValues = $this->responseChecker->getValue($this->shopClient->showByIri($item['variant']), 'optionValues'); + $optionValues = $this->responseChecker->getValue($this->shopClient->showByIri($item[$variantKey]), 'optionValues'); foreach ($optionValues as $optionValueIri) { $optionValue = $this->responseChecker->getResponseContent($this->shopClient->showByIri($optionValueIri)); - if ($optionValue['value'] !== $expectedOptionValueValue) { + if ($optionValue[$valueKey] !== $expectedOptionValueValue) { continue; } - $option = $this->responseChecker->getResponseContent($this->shopClient->showByIri($optionValue['option'])); + $option = $this->responseChecker->getResponseContent($this->shopClient->showByIri($optionValue[$optionKey])); - if ($option['name'] === $expectedOptionName) { + if ($option[$nameKey] === $expectedOptionName) { return; } } @@ -795,10 +815,12 @@ final class CartContext implements Context public function iShouldSeeWithOriginalPriceInMyCart(string $productName, int $originalPrice): void { $response = $this->shopClient->getLastResponse(); + $productNameKey = $this->getNormalizedKey('productName'); + $originalUnitPriceKey = $this->getNormalizedKey('originalUnitPrice'); foreach ($this->responseChecker->getValue($response, 'items') as $item) { - if ($item['productName'] === $productName) { - Assert::same($item['originalUnitPrice'], $originalPrice); + if ($item[$productNameKey] === $productName) { + Assert::same($item[$originalUnitPriceKey], $originalPrice); return; } @@ -813,11 +835,14 @@ final class CartContext implements Context public function iShouldSeeOnlyWithUnitPriceInMyCart(string $productName, int $unitPrice): void { $response = $this->shopClient->getLastResponse(); + $productNameKey = $this->getNormalizedKey('productName'); + $unitPriceKey = $this->getNormalizedKey('unitPrice'); + $originalPriceKey = $this->getNormalizedKey('originalPrice'); foreach ($this->responseChecker->getValue($response, 'items') as $item) { - if ($item['productName'] === $productName) { - Assert::same($item['unitPrice'], $unitPrice); - Assert::false(isset($item['originalPrice'])); + if ($item[$productNameKey] === $productName) { + Assert::same($item[$unitPriceKey], $unitPrice); + Assert::false(isset($item[$originalPriceKey])); return; } @@ -907,28 +932,32 @@ final class CartContext implements Context private function getProductForItem(array $item): Response { - if (!isset($item['variant'])) { + $variantKey = $this->getNormalizedKey('variant'); + + if (!isset($item[$variantKey])) { throw new \InvalidArgumentException( 'Expected array to have variant key and variant to have product, but one these keys is missing. Current array: ' . serialize($item), ); } - $response = $this->shopClient->showByIri(urldecode($item['variant'])); + $response = $this->shopClient->showByIri(urldecode($item[$variantKey])); return $this->shopClient->showByIri(urldecode($this->responseChecker->getValue($response, 'product'))); } private function getProductVariantForItem(array $item): Response { - if (!isset($item['variant'])) { + $variantKey = $this->getNormalizedKey('variant'); + + if (!isset($item[$variantKey])) { throw new \InvalidArgumentException( 'Expected array to have variant key and variant to have product, but one these keys is missing. Current array: ' . serialize($item), ); } - $request = $this->requestFactory->custom($item['variant'], HttpRequest::METHOD_GET); + $request = $this->requestFactory->custom($item[$variantKey], HttpRequest::METHOD_GET); $this->shopClient->executeCustomRequest($request); return $this->shopClient->getLastResponse(); @@ -981,9 +1010,11 @@ final class CartContext implements Context private function hasItemWithNameAndQuantity(Response $response, string $productName, int $quantity): bool { $items = $this->responseChecker->getCollection($response); + $productNameKey = $this->getNormalizedKey('productName'); + $quantityKey = $this->getNormalizedKey('quantity'); foreach ($items as $item) { - if ($item['productName'] === $productName && $item['quantity'] === $quantity) { + if ($item[$productNameKey] === $productName && $item[$quantityKey] === $quantity) { return true; } } @@ -998,7 +1029,7 @@ final class CartContext implements Context foreach ($items as $item) { $productResponse = $this->getProductForItem($item); if ($this->responseChecker->hasTranslation($productResponse, 'en_US', 'name', $productName)) { - $this->assertItemQuantity($productResponse, $item['quantity'], $quantity); + $this->assertItemQuantity($productResponse, $item[$this->getNormalizedKey('quantity')], $quantity); return; } @@ -1014,7 +1045,7 @@ final class CartContext implements Context foreach ($items as $item) { $productResponse = $this->getProductForItem($item); if ($this->responseChecker->hasValue($productResponse, 'name', $productName)) { - $this->assertItemQuantity($cartResponse, $item['quantity'], $quantity); + $this->assertItemQuantity($cartResponse, $item[$this->getNormalizedKey('quantity')], $quantity); return; } @@ -1038,10 +1069,12 @@ final class CartContext implements Context private function compareItemPrice(string $productName, int $productPrice, string $priceType = 'total'): void { $items = $this->responseChecker->getValue($this->getCartResponse(), 'items'); + $productNameKey = $this->getNormalizedKey('productName'); + $priceTypeKey = $this->getNormalizedKey($priceType); foreach ($items as $item) { - if ($item['productName'] === $productName) { - Assert::same($item[$priceType], $productPrice); + if ($item[$productNameKey] === $productName) { + Assert::same($item[$priceTypeKey], $productPrice); return; } @@ -1061,7 +1094,7 @@ final class CartContext implements Context if ($this->responseChecker->hasValue($productResponse, 'name', $product->getName())) { $variantForItem = $this->getProductVariantForItem($item); - return $this->responseChecker->getValue($variantForItem, 'price') * $item['quantity']; + return $this->responseChecker->getValue($variantForItem, 'price') * $item[$this->getNormalizedKey('quantity')]; } } diff --git a/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutOrderDetailsContext.php b/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutOrderDetailsContext.php index bd575698e8..0a07eb5e08 100644 --- a/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutOrderDetailsContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutOrderDetailsContext.php @@ -16,19 +16,24 @@ namespace Sylius\Behat\Context\Api\Shop\Checkout; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\OrderPaymentStates; use Sylius\Component\Payment\Model\PaymentInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class CheckoutOrderDetailsContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private SharedStorageInterface $sharedStorage, private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, + private ?NameConverterInterface $nameConverter, ) { } @@ -82,7 +87,7 @@ final class CheckoutOrderDetailsContext implements Context $payments = $this->responseChecker->getValue($response, 'payments'); $payment = end($payments); - $paymentId = $payment['id']; + $paymentId = $payment[$this->getNormalizedKey('id')]; $response = $this->client->requestGet(sprintf('orders/%s/payments/%s', $this->sharedStorage->get('cart_token'), $paymentId)); return $this->responseChecker->getValue($response, 'state'); diff --git a/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutShippingContext.php b/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutShippingContext.php index 45e07f597c..de8af4043a 100644 --- a/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutShippingContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutShippingContext.php @@ -19,14 +19,18 @@ use Behat\Step\Then; use Behat\Step\When; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\ShippingMethodInterface; use Sylius\Component\Core\OrderCheckoutStates; use Sylius\Component\Core\Repository\ShippingMethodRepositoryInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class CheckoutShippingContext implements Context { + use NormalizedKeyAwareTrait; + /** @param ShippingMethodRepositoryInterface $shippingMethodRepository */ public function __construct( private ApiClientInterface $client, @@ -34,6 +38,7 @@ final readonly class CheckoutShippingContext implements Context private SharedStorageInterface $sharedStorage, private IriConverterInterface $iriConverter, private ShippingMethodRepositoryInterface $shippingMethodRepository, + private ?NameConverterInterface $nameConverter, ) { } @@ -55,7 +60,7 @@ final readonly class CheckoutShippingContext implements Context uri: sprintf( 'orders/%s/shipments/%s', $this->sharedStorage->get('cart_token'), - $content['shipments'][0]['id'], + $content[$this->getNormalizedKey('shipments')][0][$this->getNormalizedKey('id')], ), body: ['shippingMethod' => '/api/v2/shop/shipping-methods/NON_EXISTING'], ); @@ -75,7 +80,7 @@ final readonly class CheckoutShippingContext implements Context uri: sprintf( 'orders/%s/shipments/%s', $this->sharedStorage->get('cart_token'), - $content['shipments'][0]['id'], + $content[$this->getNormalizedKey('shipments')][0][$this->getNormalizedKey('id')], ), body: ['shippingMethod' => $this->iriConverter->getIriFromResource($shippingMethod)], ); @@ -174,7 +179,7 @@ final readonly class CheckoutShippingContext implements Context uri: sprintf( 'orders/%s/shipments/%s', $this->sharedStorage->get('cart_token'), - $content['shipments'][0]['id'], + $content[$this->getNormalizedKey('shipments')][0][$this->getNormalizedKey('id')], ), body: ['shippingMethod' => $this->iriConverter->getIriFromResource( $shippingMethod ?? $this->shippingMethodRepository->findOneBy([]), diff --git a/src/Sylius/Behat/Context/Api/Shop/CheckoutContext.php b/src/Sylius/Behat/Context/Api/Shop/CheckoutContext.php index bb6744d312..f32aeeb630 100644 --- a/src/Sylius/Behat/Context/Api/Shop/CheckoutContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/CheckoutContext.php @@ -20,6 +20,7 @@ use Behat\Step\When; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\RequestFactoryInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Context\Api\Shop\Checkout\CheckoutShippingContext; use Sylius\Behat\Service\Converter\IriConverterInterface; @@ -43,10 +44,13 @@ use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; use Sylius\Resource\Doctrine\Persistence\RepositoryInterface; use Symfony\Component\HttpFoundation\Request as HTTPRequest; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class CheckoutContext implements Context { + use NormalizedKeyAwareTrait; + public const CHECKOUT_STATE_TYPES = [ 'address' => OrderCheckoutStates::STATE_ADDRESSED, 'shipping method' => OrderCheckoutStates::STATE_SHIPPING_SELECTED, @@ -73,6 +77,7 @@ final class CheckoutContext implements Context private readonly AddressFactoryInterface $addressFactory, private readonly string $shippingMethodClass, private readonly string $paymentMethodClass, + private readonly ?NameConverterInterface $nameConverter, ) { } @@ -103,9 +108,9 @@ final class CheckoutContext implements Context uri: sprintf( 'orders/%s/shipments/%s', $this->sharedStorage->get('cart_token'), - $content['shipments'][0]['id'], + $content[$this->getNormalizedKey('shipments')][0][$this->getNormalizedKey('id')], ), - body: ['shippingMethod' => $content['shipments'][0]['method']], + body: ['shippingMethod' => $content[$this->getNormalizedKey('shipments')][0][$this->getNormalizedKey('method')]], ); } @@ -128,7 +133,7 @@ final class CheckoutContext implements Context uri: sprintf( 'orders/%s/shipments/%s', $this->sharedStorage->get('cart_token'), - $content['shipments'][0]['id'], + $content[$this->getNormalizedKey('shipments')][0][$this->getNormalizedKey('id')], ), body: ['shippingMethod' => $this->iriConverter->getIriFromResource($shippingMethod)], ); @@ -470,7 +475,7 @@ final class CheckoutContext implements Context Resources::ORDERS, $this->sharedStorage->get('cart_token'), HTTPRequest::METHOD_PATCH, - sprintf('shipments/%s', $this->getCart()['shipments'][0]['id']), + sprintf('shipments/%s', $this->getCart()[$this->getNormalizedKey('shipments')][0][$this->getNormalizedKey('id')]), ); $request->setContent([ 'shippingMethod' => $this->iriConverter->getIriFromResource( @@ -493,7 +498,7 @@ final class CheckoutContext implements Context Resources::ORDERS, $this->sharedStorage->get('cart_token'), HTTPRequest::METHOD_PATCH, - sprintf('payments/%s', $cart['payments'][0]['id']), + sprintf('payments/%s', $cart[$this->getNormalizedKey('payments')][0][$this->getNormalizedKey('id')]), ); $request->setContent([ 'paymentMethod' => $this->iriConverter->getIriFromResource( @@ -556,7 +561,7 @@ final class CheckoutContext implements Context Resources::ORDERS, $this->sharedStorage->get('cart_token'), HTTPRequest::METHOD_PATCH, - \sprintf('payments/%s', $this->getCart()['payments'][0]['id']), + \sprintf('payments/%s', $this->getCart()[$this->getNormalizedKey('payments')][0][$this->getNormalizedKey('id')]), ); $request->setContent(['paymentMethod' => $this->iriConverter->getIriFromResource($paymentMethod)]); @@ -628,12 +633,13 @@ final class CheckoutContext implements Context Assert::notEmpty($payments, 'No payments found in response.'); $paymentMethodIri = $this->iriConverter->getIriFromResource($paymentMethod); + $methodKey = $this->getNormalizedKey('method'); foreach ($payments as $payment) { - if ($payment['method'] !== $paymentMethodIri) { + if ($payment[$methodKey] !== $paymentMethodIri) { continue; } - $customRequest = $this->requestFactory->custom($payment['method'], HTTPRequest::METHOD_GET); + $customRequest = $this->requestFactory->custom($payment[$methodKey], HTTPRequest::METHOD_GET); $paymentMethodResponse = $this->client->executeCustomRequest($customRequest); Assert::same( $this->responseChecker->getValue( @@ -704,7 +710,7 @@ final class CheckoutContext implements Context { $paymentMethods = $this->getPossiblePaymentMethods(); - Assert::notFalse(array_search($paymentMethodName, array_column($paymentMethods, 'name'), true)); + Assert::notFalse(array_search($paymentMethodName, array_column($paymentMethods, $this->getNormalizedKey('name')), true)); } /** @@ -715,7 +721,7 @@ final class CheckoutContext implements Context $paymentMethods = $this->getPossiblePaymentMethods(); foreach ($paymentMethodsNames as $paymentMethodName) { - Assert::inArray($paymentMethodName, array_column($paymentMethods, 'name')); + Assert::inArray($paymentMethodName, array_column($paymentMethods, $this->getNormalizedKey('name'))); } } @@ -727,7 +733,7 @@ final class CheckoutContext implements Context $paymentMethods = $this->getPossiblePaymentMethods(); foreach ($paymentMethodsNames as $paymentMethodName) { - Assert::false(in_array($paymentMethodName, array_column($paymentMethods, 'name'), true)); + Assert::false(in_array($paymentMethodName, array_column($paymentMethods, $this->getNormalizedKey('name')), true)); } } @@ -740,10 +746,10 @@ final class CheckoutContext implements Context Assert::notEmpty($paymentMethods); if ($choice === 'first') { - Assert::same(reset($paymentMethods)['name'], $paymentMethodName); + Assert::same(reset($paymentMethods)[$this->getNormalizedKey('name')], $paymentMethodName); } if ($choice === 'last') { - Assert::same(end($paymentMethods)['name'], $paymentMethodName); + Assert::same(end($paymentMethods)[$this->getNormalizedKey('name')], $paymentMethodName); } } @@ -752,7 +758,7 @@ final class CheckoutContext implements Context */ public function iShouldStillBeOnTheCheckoutAddressingStep(): void { - Assert::same($this->getCart()['checkoutState'], OrderCheckoutStates::STATE_CART); + Assert::same($this->getCart()[$this->getNormalizedKey('checkoutState')], OrderCheckoutStates::STATE_CART); } /** @@ -773,7 +779,7 @@ final class CheckoutContext implements Context { $response = $this->client->getLastResponse(); - Assert::true(empty($this->responseChecker->getResponseContent($response)['payments'])); + Assert::true(empty($this->responseChecker->getResponseContent($response)[$this->getNormalizedKey('payments')])); } /** @@ -801,7 +807,7 @@ final class CheckoutContext implements Context $paymentMethodName = $paymentMethod->getName(); foreach ($paymentMethods as $method) { - if ($method['name'] === $paymentMethodName) { + if ($method[$this->getNormalizedKey('name')] === $paymentMethodName) { return; } } @@ -818,7 +824,7 @@ final class CheckoutContext implements Context $shippingMethodName = $shippingMethod->getName(); foreach ($shippingMethods as $method) { - if ($method['name'] === $shippingMethodName) { + if ($method[$this->getNormalizedKey('name')] === $shippingMethodName) { return; } } @@ -838,7 +844,7 @@ final class CheckoutContext implements Context public function iShouldNotBeAbleToProceedCheckoutShippingStep(): void { Assert::same($this->getCheckoutState(), OrderCheckoutStates::STATE_ADDRESSED); - Assert::isEmpty($this->getCart()['shipments']); + Assert::isEmpty($this->getCart()[$this->getNormalizedKey('shipments')]); } /** @@ -847,7 +853,7 @@ final class CheckoutContext implements Context public function iShouldNotBeAbleToProceedCheckoutPaymentStep(): void { $this->iShouldBeOnTheCheckoutPaymentStep(); - Assert::isEmpty($this->getCart()['payments']); + Assert::isEmpty($this->getCart()[$this->getNormalizedKey('payments')]); } /** @@ -938,7 +944,7 @@ final class CheckoutContext implements Context { $shippingMethods = $this->getCartShippingMethods($this->getCart()); - Assert::true($shippingMethods[0]['code'] === $shippingMethod->getCode()); + Assert::true($shippingMethods[0][$this->getNormalizedKey('code')] === $shippingMethod->getCode()); } /** @@ -948,7 +954,7 @@ final class CheckoutContext implements Context { $shippingMethods = $this->getCartShippingMethods($this->getCart()); - Assert::true(end($shippingMethods)['code'] === $shippingMethod->getCode()); + Assert::true(end($shippingMethods)[$this->getNormalizedKey('code')] === $shippingMethod->getCode()); } /** @@ -956,7 +962,7 @@ final class CheckoutContext implements Context */ public function myOrderTotalShouldBe(int $total): void { - Assert::same($total, (int) $this->getCart()['total']); + Assert::same($total, (int) $this->getCart()[$this->getNormalizedKey('total')]); } /** @@ -1052,7 +1058,7 @@ final class CheckoutContext implements Context Assert::true($response->getStatusCode() === 422); /** @var array|null $violations */ - $violations = $this->responseChecker->getResponseContent($response)['violations']; + $violations = $this->responseChecker->getResponseContent($response)[$this->getNormalizedKey('violations')]; $detailType .= 'Address'; @@ -1061,7 +1067,7 @@ final class CheckoutContext implements Context $violations, $detailType . '.' . StringInflector::nameToCamelCase($element), ); - Assert::same($violation['message'], sprintf('Please enter %s.', $element)); + Assert::same($violation[$this->getNormalizedKey('message')], sprintf('Please enter %s.', $element)); } } @@ -1071,14 +1077,14 @@ final class CheckoutContext implements Context public function iShouldBeNotifiedThatTheInShippingDetailsIsRequired(string $element, string $type): void { /** @var array|null $violations */ - $violations = $this->responseChecker->getResponseContent($this->client->getLastResponse())['violations']; + $violations = $this->responseChecker->getResponseContent($this->client->getLastResponse())[$this->getNormalizedKey('violations')]; $type .= 'Address'; $violation = $this->getViolation( $violations, $type . '.' . StringInflector::nameToCamelCase($element), ); - Assert::same($violation['message'], sprintf('Please enter %s.', $element)); + Assert::same($violation[$this->getNormalizedKey('message')], sprintf('Please enter %s.', $element)); } /** @@ -1302,9 +1308,11 @@ final class CheckoutContext implements Context public function iShouldBeCheckingOutAs(string $email): void { $cart = $this->getCart(); + $customerKey = $this->getNormalizedKey('customer'); + $emailKey = $this->getNormalizedKey('email'); - Assert::notNull($cart['customer'], sprintf('Customer with an email "%s" was not expected to be null.', $email)); - Assert::same($cart['customer']['email'], $email); + Assert::notNull($cart[$customerKey], sprintf('Customer with an email "%s" was not expected to be null.', $email)); + Assert::same($cart[$customerKey][$emailKey], $email); } /** @@ -1399,7 +1407,11 @@ final class CheckoutContext implements Context private function getCartShippingMethods(array $cart): array { $this->client->customAction( - sprintf('/api/v2/shop/orders/%s/shipments/%s/methods', $cart['tokenValue'], $cart['shipments'][0]['id']), + sprintf( + '/api/v2/shop/orders/%s/shipments/%s/methods', + $cart[$this->getNormalizedKey('tokenValue')], + $cart[$this->getNormalizedKey('shipments')][0][$this->getNormalizedKey('id')], + ), HTTPRequest::METHOD_GET, ); @@ -1409,7 +1421,7 @@ final class CheckoutContext implements Context private function hasShippingMethod(ShippingMethodInterface $shippingMethod): bool { foreach ($this->getCartShippingMethods($this->getCart()) as $cartShippingMethod) { - if ($cartShippingMethod['code'] === $shippingMethod->getCode()) { + if ($cartShippingMethod[$this->getNormalizedKey('code')] === $shippingMethod->getCode()) { return true; } } @@ -1421,8 +1433,8 @@ final class CheckoutContext implements Context { foreach ($this->getCartShippingMethods($this->getCart()) as $cartShippingMethod) { if ( - $cartShippingMethod['price'] === $fee && - $cartShippingMethod['code'] === $shippingMethod->getCode() + $cartShippingMethod[$this->getNormalizedKey('price')] === $fee && + $cartShippingMethod[$this->getNormalizedKey('code')] === $shippingMethod->getCode() ) { return true; } @@ -1455,7 +1467,10 @@ final class CheckoutContext implements Context $items = $this->responseChecker->getValue($this->client->getLastResponse(), 'items'); foreach ($items as $item) { - if ($item['productName'] === $productName && $item['quantity'] === $quantity) { + if ( + $item[$this->getNormalizedKey('productName')] === $productName && + $item[$this->getNormalizedKey('quantity')] === $quantity + ) { return true; } } @@ -1469,7 +1484,10 @@ final class CheckoutContext implements Context $items = $this->responseChecker->getValue($this->client->getLastResponse(), 'items'); foreach ($items as $item) { - if ($item['productName'] === $productName && $item['unitPrice'] === $unitPrice) { + if ( + $item[$this->getNormalizedKey('productName')] === $productName && + $item[$this->getNormalizedKey('unitPrice')] === $unitPrice + ) { return true; } } @@ -1516,7 +1534,7 @@ final class CheckoutContext implements Context private function getViolation(array $violations, string $element): array { - return $violations[array_search($element, array_column($violations, 'propertyPath'), true)]; + return $violations[array_search($element, array_column($violations, $this->getNormalizedKey('propertyPath')), true)]; } private function hasFullNameInAddress(string $fullName, string $addressType): void @@ -1527,11 +1545,11 @@ final class CheckoutContext implements Context $addressType .= 'Address'; Assert::same( - $this->responseChecker->getResponseContent($response)[$addressType]['firstName'], + $this->responseChecker->getResponseContent($response)[$addressType][$this->getNormalizedKey('firstName')], $names[0], ); Assert::same( - $this->responseChecker->getResponseContent($response)[$addressType]['lastName'], + $this->responseChecker->getResponseContent($response)[$addressType][$this->getNormalizedKey('lastName')], $names[1], ); } @@ -1542,7 +1560,7 @@ final class CheckoutContext implements Context $addressType .= 'Address'; Assert::same( - $this->responseChecker->getResponseContent($response)[$addressType]['provinceName'], + $this->responseChecker->getResponseContent($response)[$addressType][$this->getNormalizedKey('provinceName')], $provinceName, ); } @@ -1587,6 +1605,8 @@ final class CheckoutContext implements Context private function getAddressByFieldValue(array $addressBook, string $fieldName, string $fieldValue): array { + $fieldName = $this->getNormalizedKey($fieldName); + foreach ($addressBook as $address) { if ($address[$fieldName] === $fieldValue) { return $address; @@ -1601,15 +1621,23 @@ final class CheckoutContext implements Context */ private function addressesAreEqual(array $address, AddressInterface $addressToCompare): bool { + $firstNameKey = $this->getNormalizedKey('firstName'); + $lastNameKey = $this->getNormalizedKey('lastName'); + $countryCodeKey = $this->getNormalizedKey('countryCode'); + $streetKey = $this->getNormalizedKey('street'); + $cityKey = $this->getNormalizedKey('city'); + $postcodeKey = $this->getNormalizedKey('postcode'); + $provinceNameKey = $this->getNormalizedKey('provinceName'); + if ( - $address['firstName'] === $addressToCompare->getFirstName() && - $address['lastName'] === $addressToCompare->getLastName() && - $address['countryCode'] === $addressToCompare->getCountryCode() && - $address['street'] === $addressToCompare->getStreet() && - $address['city'] === $addressToCompare->getCity() && - $address['postcode'] === $addressToCompare->getPostcode() && - ($addressToCompare->getProvinceName() !== null && isset($address['provinceName'])) ? - $address['provinceName'] === $addressToCompare->getProvinceName() : true + $address[$firstNameKey] === $addressToCompare->getFirstName() && + $address[$lastNameKey] === $addressToCompare->getLastName() && + $address[$countryCodeKey] === $addressToCompare->getCountryCode() && + $address[$streetKey] === $addressToCompare->getStreet() && + $address[$cityKey] === $addressToCompare->getCity() && + $address[$postcodeKey] === $addressToCompare->getPostcode() && + ($addressToCompare->getProvinceName() !== null && isset($address[$provinceNameKey])) ? + $address[$provinceNameKey] === $addressToCompare->getProvinceName() : true ) { return true; } @@ -1649,7 +1677,7 @@ final class CheckoutContext implements Context Resources::ORDERS, $this->sharedStorage->get('cart_token'), HTTPRequest::METHOD_PATCH, - sprintf('shipments/%s', $this->getCart()['shipments'][0]['id']), + sprintf('shipments/%s', $this->getCart()[$this->getNormalizedKey('shipments')][0][$this->getNormalizedKey('id')]), ); $request->setContent(['shippingMethod' => $this->iriConverter->getIriFromResource($shippingMethod)]); diff --git a/src/Sylius/Behat/Context/Api/Shop/ExchangeRateContext.php b/src/Sylius/Behat/Context/Api/Shop/ExchangeRateContext.php index a2c163fab9..e1d9a56444 100644 --- a/src/Sylius/Behat/Context/Api/Shop/ExchangeRateContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/ExchangeRateContext.php @@ -16,14 +16,19 @@ namespace Sylius\Behat\Context\Api\Shop; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class ExchangeRateContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, + private ?NameConverterInterface $nameConverter, ) { } @@ -50,7 +55,7 @@ final readonly class ExchangeRateContext implements Context { $exchangeRate = $this->getExchangeRateByTargetCurrency($sourceCurrency, $targetCurrency); - Assert::same($exchangeRate['ratio'], $ratio); + Assert::same($exchangeRate[$this->getNormalizedKey('ratio')], $ratio); } /** @@ -71,8 +76,8 @@ final readonly class ExchangeRateContext implements Context foreach ($exchangeRates as $exchangeRate) { if ( - str_ends_with($exchangeRate['sourceCurrency'], $sourceCurrencyCode) && - str_ends_with($exchangeRate['targetCurrency'], $targetCurrencyCode) + str_ends_with($exchangeRate[$this->getNormalizedKey('sourceCurrency')], $sourceCurrencyCode) && + str_ends_with($exchangeRate[$this->getNormalizedKey('targetCurrency')], $targetCurrencyCode) ) { return $exchangeRate; } diff --git a/src/Sylius/Behat/Context/Api/Shop/OrderContext.php b/src/Sylius/Behat/Context/Api/Shop/OrderContext.php index b7b8904ccd..f22ae90823 100644 --- a/src/Sylius/Behat/Context/Api/Shop/OrderContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/OrderContext.php @@ -18,6 +18,7 @@ use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\RequestFactoryInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\SecurityServiceInterface; use Sylius\Behat\Service\SharedStorageInterface; @@ -32,10 +33,13 @@ use Sylius\Component\Core\Model\PromotionInterface; use Sylius\Component\Core\OrderCheckoutStates; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class OrderContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $shopClient, private ApiClientInterface $adminClient, @@ -45,6 +49,7 @@ final readonly class OrderContext implements Context private SecurityServiceInterface $securityService, private RequestFactoryInterface $requestFactory, private string $apiUrlPrefix, + private ?NameConverterInterface $nameConverter, ) { } @@ -134,15 +139,21 @@ final readonly class OrderContext implements Context string $addressType, ): void { $address = $this->responseChecker->getValue($this->shopClient->getLastResponse(), ($addressType . 'Address')); + $firstNameKey = $this->getNormalizedKey('firstName'); + $lastNameKey = $this->getNormalizedKey('lastName'); + $streetKey = $this->getNormalizedKey('street'); + $postcodeKey = $this->getNormalizedKey('postcode'); + $cityKey = $this->getNormalizedKey('city'); + $countryCodeKey = $this->getNormalizedKey('countryCode'); $names = explode(' ', $customerName); - Assert::same($address['firstName'], $names[0]); - Assert::same($address['lastName'], $names[1]); - Assert::same($address['street'], $street); - Assert::same($address['postcode'], $postcode); - Assert::same($address['city'], $city); - Assert::same($address['countryCode'], $country->getCode()); + Assert::same($address[$firstNameKey], $names[0]); + Assert::same($address[$lastNameKey], $names[1]); + Assert::same($address[$streetKey], $street); + Assert::same($address[$postcodeKey], $postcode); + Assert::same($address[$cityKey], $city); + Assert::same($address[$countryCodeKey], $country->getCode()); } /** @@ -159,9 +170,10 @@ final readonly class OrderContext implements Context public function theProductShouldBeInTheItemsList(string $productName): void { $items = $this->responseChecker->getValue($this->shopClient->getLastResponse(), 'items'); + $productNameKey = $this->getNormalizedKey('productName'); foreach ($items as $item) { - if ($item['productName'] === $productName) { + if ($item[$productNameKey] === $productName) { return; } } @@ -197,7 +209,7 @@ final readonly class OrderContext implements Context { $address = $this->responseChecker->getValue($this->shopClient->getLastResponse(), ($addressType . 'Address')); - Assert::same($address['provinceName'], $provinceName); + Assert::same($address[$this->getNormalizedKey('provinceName')], $provinceName); } /** @@ -208,9 +220,10 @@ final readonly class OrderContext implements Context $items = $this->responseChecker->getValue($this->shopClient->getLastResponse(), 'items'); $subtotal = 0; + $subtotalKey = $this->getNormalizedKey('subtotal'); foreach ($items as $item) { - $subtotal = $subtotal + $item['subtotal']; + $subtotal += $item[$subtotalKey]; } Assert::same($subtotal, $expectedSubtotal); @@ -252,7 +265,7 @@ final readonly class OrderContext implements Context { $adjustment = $this->getAdjustmentWithLabel($promotion->getName()); Assert::notNull($adjustment); - Assert::same($discount, $adjustment['amount']); + Assert::same($discount, $adjustment[$this->getNormalizedKey('amount')]); } /** @@ -263,8 +276,9 @@ final readonly class OrderContext implements Context $discount = 0; $itemId = $this->geOrderItemIdForProductInCart($product, $this->sharedStorage->get('cart_token')); $adjustments = $this->getAdjustmentsForOrderItem($itemId); + $amountKey = $this->getNormalizedKey('amount'); foreach ($adjustments as $adjustment) { - $discount += $adjustment['amount']; + $discount += $adjustment[$amountKey]; } Assert::same(-$discount, $amount); @@ -296,7 +310,7 @@ final readonly class OrderContext implements Context ->getValue($this->shopClient->show(Resources::ORDERS, $this->sharedStorage->get('cart_token')), 'payments')[0] ; - Assert::same($this->iriConverter->getIriFromResource($paymentMethod), $payment['method']); + Assert::same($this->iriConverter->getIriFromResource($paymentMethod), $payment[$this->getNormalizedKey('method')]); } /** @@ -322,7 +336,7 @@ final readonly class OrderContext implements Context { $paymentMethodIri = $this ->responseChecker - ->getValue($this->shopClient->getLastResponse(), 'payments')[0]['method'] + ->getValue($this->shopClient->getLastResponse(), 'payments')[0][$this->getNormalizedKey('method')] ; Assert::same($this->iriConverter->getResourceFromIri($paymentMethodIri)->getCode(), $paymentMethod->getCode()); @@ -384,7 +398,7 @@ final readonly class OrderContext implements Context foreach ($items as $item) { $response = $this->getProductForItem($item); if ($this->responseChecker->hasValue($response, 'code', $product->getCode())) { - return (string) $item['id']; + return (string) $item[$this->getNormalizedKey('id')]; } } @@ -393,14 +407,14 @@ final readonly class OrderContext implements Context private function getProductForItem(array $item): Response { - if (!isset($item['variant'])) { + if (!isset($item[$this->getNormalizedKey('variant')])) { throw new \InvalidArgumentException( 'Expected array to have variant key, but this key is missing. Current array: ' . json_encode($item), ); } - $request = $this->requestFactory->custom($item['variant'], HttpRequest::METHOD_GET); + $request = $this->requestFactory->custom($item[$this->getNormalizedKey('variant')], HttpRequest::METHOD_GET); $this->shopClient->executeCustomRequest($request); return $this->shopClient->showByIri($this->responseChecker->getValue($this->shopClient->getLastResponse(), 'product')); @@ -409,7 +423,7 @@ final readonly class OrderContext implements Context private function getAdjustmentWithLabel(string $label): ?array { $adjustments = $this->getAdjustmentsForOrder(); - $index = array_search($label, array_column($adjustments, 'label')); + $index = array_search($label, array_column($adjustments, $this->getNormalizedKey('label'))); if ($index) { return $adjustments[$index]; } @@ -429,7 +443,7 @@ final readonly class OrderContext implements Context /** @var int $index */ foreach ($adjustments as $index => $adjustment) { - if (-$amounts[$index] !== $adjustment['amount']) { + if (-$amounts[$index] !== $adjustment[$this->getNormalizedKey('amount')]) { return false; } } diff --git a/src/Sylius/Behat/Context/Api/Shop/PaymentContext.php b/src/Sylius/Behat/Context/Api/Shop/PaymentContext.php index 546146d39c..8ed2ea04c6 100644 --- a/src/Sylius/Behat/Context/Api/Shop/PaymentContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/PaymentContext.php @@ -16,18 +16,23 @@ namespace Sylius\Behat\Context\Api\Shop; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class PaymentContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private SharedStorageInterface $sharedStorage, + private ?NameConverterInterface $nameConverter, ) { } @@ -65,7 +70,7 @@ final readonly class PaymentContext implements Context $payments = $this->responseChecker->getValue($response, 'payments'); $token = $this->responseChecker->getValue($response, 'tokenValue'); - $response = $this->client->requestGet(sprintf('orders/%s/payments/%s', $token, $payments[0]['id'])); + $response = $this->client->requestGet(sprintf('orders/%s/payments/%s', $token, $payments[0][$this->getNormalizedKey('id')])); Assert::true($this->responseChecker->hasValue($response, 'state', $state, isCaseSensitive: false)); } diff --git a/src/Sylius/Behat/Context/Api/Shop/PaymentRequestContext.php b/src/Sylius/Behat/Context/Api/Shop/PaymentRequestContext.php index ba030a29aa..6f236dedb8 100644 --- a/src/Sylius/Behat/Context/Api/Shop/PaymentRequestContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/PaymentRequestContext.php @@ -18,21 +18,26 @@ use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\Request; use Sylius\Behat\Client\RequestFactoryInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\Component\Payment\Repository\PaymentRequestRepositoryInterface; use Symfony\Component\HttpFoundation\Request as HTTPRequest; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class PaymentRequestContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private SharedStorageInterface $sharedStorage, private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private RequestFactoryInterface $requestFactory, private PaymentRequestRepositoryInterface $paymentRequestRepository, + private ?NameConverterInterface $nameConverter, ) { } @@ -85,8 +90,8 @@ final readonly class PaymentRequestContext implements Context ); $request->setContent([ - 'paymentId' => $payment['id'], - 'paymentMethodCode' => $payment['method'], + 'paymentId' => $payment[$this->getNormalizedKey('id')], + 'paymentMethodCode' => $payment[$this->getNormalizedKey('method')], 'payload' => $payload, ]); @@ -112,7 +117,7 @@ final readonly class PaymentRequestContext implements Context $orderToken = $this->sharedStorage->get('cart_token'); $order = $this->client->show(Resources::ORDERS, $orderToken); $payments = $this->responseChecker->getValue($order, 'payments'); - $paymentId = end($payments)['id']; + $paymentId = end($payments)[$this->getNormalizedKey('id')]; $paymentRequest = $this->paymentRequestRepository->findOneBy(['payment' => $paymentId, 'action' => $action]); return $paymentRequest ? $this->requestFactory->custom('/api/v2/shop/payment-requests/' . $paymentRequest->getHash(), HttpRequest::METHOD_GET, [], $this->client->getToken()) : null; diff --git a/src/Sylius/Behat/Context/Api/Shop/ProductContext.php b/src/Sylius/Behat/Context/Api/Shop/ProductContext.php index bcc81bf720..b489217b6d 100644 --- a/src/Sylius/Behat/Context/Api/Shop/ProductContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/ProductContext.php @@ -20,6 +20,7 @@ use Doctrine\Persistence\ObjectManager; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\RequestFactoryInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\Setter\ChannelContextSetterInterface; use Sylius\Behat\Service\SharedStorageInterface; @@ -32,10 +33,13 @@ use Sylius\Component\Product\Model\ProductVariantInterface; use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class ProductContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, @@ -46,6 +50,7 @@ final class ProductContext implements Context private ObjectManager $objectManager, private string $apiUrlPrefix, private ProductVariantResolverInterface $productVariantResolver, + private ?NameConverterInterface $nameConverter, ) { } @@ -289,8 +294,8 @@ final class ProductContext implements Context $checkedVariant = $this->sharedStorage->get('product_variant'); $variant = $this->fetchItemByIri($this->iriConverter->getIriFromResource($checkedVariant)); - Assert::same($variant['price'], $price); - Assert::same($variant['code'], $checkedVariant->getCode()); + Assert::same($variant[$this->getNormalizedKey('price')], $price); + Assert::same($variant[$this->getNormalizedKey('code')], $checkedVariant->getCode()); } /** @@ -303,8 +308,8 @@ final class ProductContext implements Context $checkedVariant = $this->sharedStorage->get('product_variant'); $variant = $this->responseChecker->getResponseContent($this->client->getLastResponse()); - Assert::same($variant['originalPrice'], $originalPrice); - Assert::same($variant['code'], $checkedVariant->getCode()); + Assert::same($variant[$this->getNormalizedKey('originalPrice')], $originalPrice); + Assert::same($variant[$this->getNormalizedKey('code')], $checkedVariant->getCode()); } /** @@ -314,8 +319,8 @@ final class ProductContext implements Context { $variant = $this->responseChecker->getResponseContent($this->client->getLastResponse()); - Assert::same($variant['originalPrice'], $variant['price']); - Assert::keyNotExists($variant, 'appliedPromotions'); + Assert::same($variant[$this->getNormalizedKey('originalPrice')], $variant[$this->getNormalizedKey('price')]); + Assert::keyNotExists($variant, $this->getNormalizedKey('appliedPromotions')); } /** @@ -324,8 +329,11 @@ final class ProductContext implements Context public function iShouldNotSeeAnyOriginalPrice(): void { $product = $this->responseChecker->getResponseContent($this->client->getLastResponse()); + $defaultVariantDataKey = $this->getNormalizedKey('defaultVariantData'); + $originalPriceKey = $this->getNormalizedKey('originalPrice'); + $priceKey = $this->getNormalizedKey('price'); - Assert::same($product['defaultVariantData']['originalPrice'], $product['defaultVariantData']['price']); + Assert::same($product[$defaultVariantDataKey][$originalPriceKey], $product[$defaultVariantDataKey][$priceKey]); } /** @@ -384,7 +392,7 @@ final class ProductContext implements Context { $products = $this->responseChecker->getCollection($this->client->getLastResponse()); - Assert::same($products[0]['code'], $code); + Assert::same($products[0][$this->getNormalizedKey('code')], $code); } /** @@ -394,7 +402,7 @@ final class ProductContext implements Context { $products = $this->responseChecker->getCollection($this->client->getLastResponse()); - Assert::same(end($products)['code'], $code); + Assert::same(end($products)[$this->getNormalizedKey('code')], $code); } /** @@ -404,7 +412,7 @@ final class ProductContext implements Context { $products = $this->responseChecker->getCollection($this->client->getLastResponse()); - Assert::same($products[0]['name'], $name); + Assert::same($products[0][$this->getNormalizedKey('name')], $name); } /** @@ -414,8 +422,8 @@ final class ProductContext implements Context { $product = $this->responseChecker->getCollection($this->client->resend())[0]; - Assert::same($product['name'], $name); - Assert::same($product['defaultVariantData']['price'], $price); + Assert::same($product[$this->getNormalizedKey('name')], $name); + Assert::same($product[$this->getNormalizedKey('defaultVariantData')][$this->getNormalizedKey('price')], $price); } /** @@ -425,7 +433,7 @@ final class ProductContext implements Context { $products = $this->responseChecker->getCollection($this->client->getLastResponse()); - Assert::same(end($products)['name'], $name); + Assert::same(end($products)[$this->getNormalizedKey('name')], $name); } /** @@ -436,8 +444,8 @@ final class ProductContext implements Context $products = $this->responseChecker->getCollection($this->client->resend()); $product = end($products); - Assert::same($product['name'], $name); - Assert::same($product['defaultVariantData']['price'], $price); + Assert::same($product[$this->getNormalizedKey('name')], $name); + Assert::same($product[$this->getNormalizedKey('defaultVariantData')][$this->getNormalizedKey('price')], $price); } /** @@ -477,7 +485,7 @@ final class ProductContext implements Context { $images = $this->responseChecker->getValue($this->client->getLastResponse(), 'images'); - Assert::same($images[0]['type'], $type); + Assert::same($images[0][$this->getNormalizedKey('type')], $type); } /** @@ -487,7 +495,7 @@ final class ProductContext implements Context { $images = $this->responseChecker->getValue($this->client->getLastResponse(), 'images'); - Assert::same($images[1]['type'], $type); + Assert::same($images[1][$this->getNormalizedKey('type')], $type); } /** @@ -540,7 +548,7 @@ final class ProductContext implements Context $productNamesFromResponse = new ArrayCollection(); foreach ($this->responseChecker->getCollection($this->client->getLastResponse()) as $productItem) { - $productNamesFromResponse->add($productItem['name']); + $productNamesFromResponse->add($productItem[$this->getNormalizedKey('name')]); } foreach ($productNamesFromResponse as $key => $name) { @@ -555,7 +563,7 @@ final class ProductContext implements Context { $defaultVariant = $this->responseChecker->getValue($this->client->getLastResponse(), 'defaultVariantData'); - Assert::same($defaultVariant['price'], $price); + Assert::same($defaultVariant[$this->getNormalizedKey('price')], $price); } /** @@ -691,10 +699,10 @@ final class ProductContext implements Context public function iShouldNotSeeInformationAboutItsLowestPrice(): void { $product = $this->responseChecker->getResponseContent($this->client->getLastResponse()); - $variant = $product['defaultVariantData']; + $variant = $product[$this->getNormalizedKey('defaultVariantData')]; - Assert::keyExists($variant, 'lowestPriceBeforeDiscount'); - Assert::same($variant['lowestPriceBeforeDiscount'], null); + Assert::keyExists($variant, $this->getNormalizedKey('lowestPriceBeforeDiscount')); + Assert::same($variant[$this->getNormalizedKey('lowestPriceBeforeDiscount')], null); } /** @@ -703,10 +711,10 @@ final class ProductContext implements Context public function iShouldSeeAsItsLowestPriceBeforeTheDiscount(int $lowestPriceBeforeDiscount): void { $product = $this->responseChecker->getResponseContent($this->client->getLastResponse()); - $variant = $product['defaultVariantData']; + $variant = $product[$this->getNormalizedKey('defaultVariantData')]; - Assert::keyExists($variant, 'lowestPriceBeforeDiscount'); - Assert::same($variant['lowestPriceBeforeDiscount'], $lowestPriceBeforeDiscount); + Assert::keyExists($variant, $this->getNormalizedKey('lowestPriceBeforeDiscount')); + Assert::same($variant[$this->getNormalizedKey('lowestPriceBeforeDiscount')], $lowestPriceBeforeDiscount); } /** @@ -731,12 +739,15 @@ final class ProductContext implements Context ?string $productCode = null, string $priceType = 'price', ): bool { + $codeKey = $this->getNormalizedKey('code'); + $variantsKey = $this->getNormalizedKey('variants'); + foreach ($products as $product) { - if ($productCode !== null && $product['code'] !== $productCode) { + if ($productCode !== null && $product[$codeKey] !== $productCode) { continue; } - foreach ($product['variants'] as $variantIri) { + foreach ($product[$variantsKey] as $variantIri) { $request = $this->requestFactory->custom($variantIri, HttpRequest::METHOD_GET); $response = $this->client->executeCustomRequest($request); @@ -755,7 +766,7 @@ final class ProductContext implements Context private function hasProductWithName(array $products, string $name): bool { foreach ($products as $product) { - if ($product['name'] === $name) { + if ($product[$this->getNormalizedKey('name')] === $name) { return true; } } @@ -765,8 +776,11 @@ final class ProductContext implements Context private function hasProductWithNameAndShortDescription(array $products, string $name, string $shortDescription): bool { + $nameKey = $this->getNormalizedKey('name'); + $shortDescriptionKey = $this->getNormalizedKey('shortDescription'); + foreach ($products as $product) { - if ($product['name'] === $name && $product['shortDescription'] === $shortDescription) { + if ($product[$nameKey] === $name && $product[$shortDescriptionKey] === $shortDescription) { return true; } } @@ -784,11 +798,14 @@ final class ProductContext implements Context ); foreach ($productVariants as $productVariant) { - foreach ($productVariant['optionValues'] as $optionValueIri) { + foreach ($productVariant[$this->getNormalizedKey('optionValues')] as $optionValueIri) { $optionValueData = $this->fetchItemByIri($optionValueIri); - $optionData = $this->fetchItemByIri($optionValueData['option']); + $optionData = $this->fetchItemByIri($optionValueData[$this->getNormalizedKey('option')]); - if ($optionData['name'] === $expectedOptionName && $optionValueData['value'] === $expectedOptionValueValue) { + if ( + $optionData[$this->getNormalizedKey('name')] === $expectedOptionName && + $optionValueData[$this->getNormalizedKey('value')] === $expectedOptionValueValue + ) { return true; } } @@ -812,7 +829,7 @@ final class ProductContext implements Context { $images = $this->responseChecker->getValue($this->client->getLastResponse(), 'images'); - return $images[0]['type'] === 'main' && $images[0]['path']; + return $images[0][$this->getNormalizedKey('type')] === 'main' && $images[0][$this->getNormalizedKey('path')]; } private function hasAssociationsWithProducts( diff --git a/src/Sylius/Behat/Context/Api/Shop/ProductVariantContext.php b/src/Sylius/Behat/Context/Api/Shop/ProductVariantContext.php index 66dee2edf0..21adcd236d 100644 --- a/src/Sylius/Behat/Context/Api/Shop/ProductVariantContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/ProductVariantContext.php @@ -17,21 +17,26 @@ use ApiPlatform\Metadata\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Product\Model\ProductOptionValueInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class ProductVariantContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private SharedStorageInterface $sharedStorage, private IriConverterInterface $iriConverter, + private ?NameConverterInterface $nameConverter, ) { } @@ -94,7 +99,7 @@ final class ProductVariantContext implements Context { $response = $this->responseChecker->getResponseContent($this->client->getLastResponse()); - Assert::same($response['price'], $price); + Assert::same($response[$this->getNormalizedKey('price')], $price); } /** @@ -104,7 +109,7 @@ final class ProductVariantContext implements Context { $response = $this->responseChecker->getResponseContent($this->client->getLastResponse()); - Assert::same($response['originalPrice'], $originalPrice); + Assert::same($response[$this->getNormalizedKey('originalPrice')], $originalPrice); } /** @@ -122,13 +127,13 @@ final class ProductVariantContext implements Context ): void { $content = $this->findVariant($variant); - Assert::same($content['price'], $price); - Assert::same($content['originalPrice'], $originalPrice); - foreach ($content['appliedPromotions'] as $promotionIri) { + Assert::same($content[$this->getNormalizedKey('price')], $price); + Assert::same($content[$this->getNormalizedKey('originalPrice')], $originalPrice); + foreach ($content[$this->getNormalizedKey('appliedPromotions')] as $promotionIri) { $catalogPromotionContent = $this->responseChecker->getResponseContent( $this->client->showByIri($promotionIri), ); - Assert::inArray($catalogPromotionContent['label'], $promotionsNames); + Assert::inArray($catalogPromotionContent[$this->getNormalizedKey('label')], $promotionsNames); } } @@ -143,9 +148,9 @@ final class ProductVariantContext implements Context ): void { $content = $this->findVariant($variant); - Assert::same($content['price'], $price); - Assert::same($content['originalPrice'], $originalPrice); - Assert::count($content['appliedPromotions'], $numberOfPromotions); + Assert::same($content[$this->getNormalizedKey('price')], $price); + Assert::same($content[$this->getNormalizedKey('originalPrice')], $originalPrice); + Assert::count($content[$this->getNormalizedKey('appliedPromotions')], $numberOfPromotions); } /** @@ -158,13 +163,13 @@ final class ProductVariantContext implements Context string $promotionName, ): void { $variantContent = $this->findVariant($variant); - $catalogPromotionResponse = $this->client->showByIri($variantContent['appliedPromotions'][0]); + $catalogPromotionResponse = $this->client->showByIri($variantContent[$this->getNormalizedKey('appliedPromotions')][0]); $catalogPromotionContent = $this->responseChecker->getResponseContent($catalogPromotionResponse); - Assert::count($variantContent['appliedPromotions'], 1); - Assert::same($variantContent['price'], $price); - Assert::same($variantContent['originalPrice'], $originalPrice); - Assert::same($catalogPromotionContent['label'], $promotionName); + Assert::count($variantContent[$this->getNormalizedKey('appliedPromotions')], 1); + Assert::same($variantContent[$this->getNormalizedKey('price')], $price); + Assert::same($variantContent[$this->getNormalizedKey('originalPrice')], $originalPrice); + Assert::same($catalogPromotionContent[$this->getNormalizedKey('label')], $promotionName); } /** @@ -206,7 +211,7 @@ final class ProductVariantContext implements Context $items = $this->responseChecker->getCollectionItemsWithValue($response, 'code', $variant->getCode()); $item = array_pop($items); - Assert::keyNotExists($item, 'appliedPromotions'); + Assert::keyNotExists($item, $this->getNormalizedKey('appliedPromotions')); } /** @@ -227,7 +232,7 @@ final class ProductVariantContext implements Context { $content = $this->responseChecker->getResponseContent($this->client->show(Resources::PRODUCT_VARIANTS, $variant->getCode())); - Assert::keyNotExists($content, 'appliedPromotions'); + Assert::keyNotExists($content, $this->getNormalizedKey('appliedPromotions')); } /** @@ -243,7 +248,7 @@ final class ProductVariantContext implements Context $content = $this->responseChecker->getResponseContent($this->client->show(Resources::PRODUCT_VARIANTS, $variant->getCode())); Assert::keyExists( $content, - 'appliedPromotions', + $this->getNormalizedKey('appliedPromotions'), sprintf('%s variant should be discounted', $variant->getName()), ); } @@ -262,7 +267,7 @@ final class ProductVariantContext implements Context $content = $this->responseChecker->getResponseContent($this->client->show(Resources::PRODUCT_VARIANTS, $variant->getCode())); Assert::keyNotExists( $content, - 'appliedPromotions', + $this->getNormalizedKey('appliedPromotions'), sprintf('%s variant should not be discounted', $variant->getName()), ); } @@ -293,7 +298,7 @@ final class ProductVariantContext implements Context foreach ($variants as $variant) { $content = $this->responseChecker->getResponseContent($this->client->show(Resources::PRODUCT_VARIANTS, $variant->getCode())); Assert::same( - $content['name'], + $content[$this->getNormalizedKey('name')], $variant->getName(), sprintf('%s variant should be visible', $variant->getName()), ); @@ -313,11 +318,11 @@ final class ProductVariantContext implements Context Assert::greaterThan(count($variants), $position - 1, 'There are less variants than expected'); $variant = $variants[$position - 1]; - Assert::same($variant['price'], $price); + Assert::same($variant[$this->getNormalizedKey('price')], $price); Assert::true( $this->isOptionValueInVariant( - $variant['optionValues'], + $variant[$this->getNormalizedKey('optionValues')], $expectedOptionName, $expectedOptionValueValue, ), @@ -335,7 +340,7 @@ final class ProductVariantContext implements Context foreach ($variants as $variant) { Assert::false( $this->isOptionValueInVariant( - $variant['optionValues'], + $variant[$this->getNormalizedKey('optionValues')], $expectedOptionName, $expectedOptionValueValue, ), diff --git a/src/Sylius/Behat/Context/Api/Shop/RegistrationContext.php b/src/Sylius/Behat/Context/Api/Shop/RegistrationContext.php index 451aa23d45..0123ea6ef4 100644 --- a/src/Sylius/Behat/Context/Api/Shop/RegistrationContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/RegistrationContext.php @@ -17,17 +17,20 @@ use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\RequestFactoryInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Context\Ui\Admin\Helper\SecurePasswordTrait; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\CustomerInterface; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final class RegistrationContext implements Context { use SecurePasswordTrait; + use NormalizedKeyAwareTrait; private array $content = []; @@ -38,6 +41,7 @@ final class RegistrationContext implements Context private ResponseCheckerInterface $responseChecker, private RequestFactoryInterface $requestFactory, private string $apiUrlPrefix, + private ?NameConverterInterface $nameConverter, ) { } @@ -268,7 +272,7 @@ final class RegistrationContext implements Context $response = $this->shopClient->show(Resources::CUSTOMERS, (string) $customer->getId()); - Assert::true($this->responseChecker->getResponseContent($response)['subscribedToNewsletter']); + Assert::true($this->responseChecker->getResponseContent($response)[$this->getNormalizedKey('subscribedToNewsletter')]); } /** @@ -282,11 +286,13 @@ final class RegistrationContext implements Context private function assertFieldValidationMessage(string $path, string $message): void { $decodedResponse = $this->getResponseContent(); + $violationsKey = $this->getNormalizedKey('violations'); + $codeKey = $this->getNormalizedKey('code'); - Assert::keyExists($decodedResponse, 'violations'); + Assert::keyExists($decodedResponse, $violationsKey); Assert::same( - $decodedResponse['violations'][0], - ['propertyPath' => $path, 'message' => $message, 'code' => $decodedResponse['violations'][0]['code']], + $decodedResponse[$violationsKey][0], + ['propertyPath' => $path, 'message' => $message, 'code' => $decodedResponse[$violationsKey][0][$codeKey]], ); } diff --git a/src/Sylius/Behat/Context/Api/Shop/ShipmentContext.php b/src/Sylius/Behat/Context/Api/Shop/ShipmentContext.php index 6454bace41..ffeb22d580 100644 --- a/src/Sylius/Behat/Context/Api/Shop/ShipmentContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/ShipmentContext.php @@ -16,18 +16,23 @@ namespace Sylius\Behat\Context\Api\Shop; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\NormalizedKeyAwareTrait; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\ShipmentInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Webmozart\Assert\Assert; final readonly class ShipmentContext implements Context { + use NormalizedKeyAwareTrait; + public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private SharedStorageInterface $sharedStorage, + private ?NameConverterInterface $nameConverter, ) { } @@ -58,7 +63,7 @@ final readonly class ShipmentContext implements Context $shipments = $this->responseChecker->getValue($response, 'shipments'); $token = $this->responseChecker->getValue($response, 'tokenValue'); - $response = $this->client->requestGet(sprintf('orders/%s/shipments/%s', $token, $shipments[0]['id'])); + $response = $this->client->requestGet(sprintf('orders/%s/shipments/%s', $token, $shipments[0][$this->getNormalizedKey('id')])); Assert::true($this->responseChecker->hasValue($response, 'state', $state, isCaseSensitive: false)); } diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml index 8b66cc9d94..0aba48f2c4 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml @@ -32,6 +32,7 @@ + @@ -68,6 +69,7 @@ + @@ -87,6 +89,7 @@ %sylius.security.api_route% + @@ -135,6 +138,7 @@ %sylius.security.api_route% + @@ -142,6 +146,7 @@ + @@ -238,6 +243,7 @@ + @@ -266,6 +272,7 @@ + diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/shop.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/shop.xml index 70f12849eb..524c0d05e6 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/shop.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/shop.xml @@ -23,6 +23,7 @@ + @@ -48,6 +49,7 @@ %sylius.security.api_route% + @@ -64,6 +66,7 @@ + @@ -85,6 +88,7 @@ %sylius.model.shipping_method.class% %sylius.model.payment_method.class% + @@ -116,6 +120,7 @@ %sylius.security.api_route% + @@ -129,6 +134,7 @@ + @@ -145,6 +151,7 @@ %sylius.security.api_route% + @@ -156,6 +163,7 @@ %sylius.security.api_route% + @@ -170,18 +178,21 @@ + + + @@ -202,6 +213,7 @@ + @@ -214,6 +226,7 @@ +