[UPMERGE] 1.13 -> 2.0 (#15679)

This PR has been generated automatically.
For more details see
[upmerge_pr.yaml](/Sylius/Sylius/blob/1.13/.github/workflows/upmerge_pr.yaml).

**Remember!** The upmerge should always be merged with using `Merge pull
request` button.

In case of conflicts, please resolve them manually with usign the
following commands:
```
git fetch upstream
gh pr checkout 
git merge upstream/2.0 --no-commit
```

If you use other name for the upstream remote, please replace `upstream`
with the name of your remote pointing to the `Sylius/Sylius` repository.

Once the conflicts are resolved, please run `git merge --continue` and
change the commit title to
```
Resolve conflicts between 1.13 and 2.0
```
This commit is contained in:
Jacob Tobiasz 2023-12-25 13:14:44 +01:00 committed by GitHub
commit dcac160013
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 126 additions and 42 deletions

View file

@ -2,8 +2,13 @@ Translations
============
In the shop part of our API translatable entities are translated at the server-side and users get an already translated field in the response.
By default it will be provided in default locale configured on the channel, different locale must be configured in the header (for example `accept-language: de_DE`)
By default it will be provided in default locale configured on the channel, different locale must be configured in the header (e.g. `Accept-Language: de-DE`)
.. warning::
Remember that the chosen locale has to be enabled in the channel!
.. note::
Though Sylius' locale code format (e.g. `de_DE`) will also be accepted in `Accept-Language` header, it's not valid value according to HTTP specification.
Also, it silently changes CORS behavior - results with CORS restrictions because the "_" character is not considered safe for this header type (https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header).

View file

@ -47,7 +47,7 @@ Third scenario::
Then the cart should be empty
.. note::
The cart mentioned in the last scenario will we available once you log in again.
The cart mentioned in the last scenario will be available once you log in again.
Learn more
----------

View file

@ -18,7 +18,7 @@ Feature: Picking up the cart with the locale other than the default
@api
Scenario: Picking up the cart without specified locale
When I pick up cart
When I pick up cart without specifying locale
And I check details of my cart
Then my cart's locale should be "French (France)"

View file

@ -226,6 +226,7 @@ final class CartContext implements Context
/**
* @When I pick up (my )cart (again)
* @When I pick up cart in the :localeCode locale
* @When I pick up cart without specifying locale
* @When the visitor picks up the cart
*/
public function iPickUpMyCart(?string $localeCode = null): void
@ -812,8 +813,9 @@ final class CartContext implements Context
$request = $this->requestFactory->custom(
sprintf('%s/shop/orders', $this->apiUrlPrefix),
HttpRequest::METHOD_POST,
$localeCode ? ['HTTP_ACCEPT_LANGUAGE' => $localeCode] : [],
['HTTP_ACCEPT_LANGUAGE' => $localeCode ?? ''],
);
$this->shopClient->executeCustomRequest($request);
$tokenValue = $this->responseChecker->getValue($this->shopClient->getLastResponse(), 'tokenValue');

View file

@ -18,8 +18,18 @@ use Sylius\Component\Locale\Context\LocaleNotFoundException;
use Sylius\Component\Locale\Provider\LocaleProviderInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Locale context implementation based on Symfony Request's language negotiation (RFC 4647 based).
*
* @see Request::getPreferredLanguage()
*/
final class RequestHeaderBasedLocaleContext implements LocaleContextInterface
{
private const NO_CODE_VALID_STUB = 'NO_CODE_VALID_STUB';
/** @var array<array-key, string> $availableLocalesCodes */
private array $availableLocalesCodes = [];
public function __construct(private RequestStack $requestStack, private LocaleProviderInterface $localeProvider)
{
}
@ -31,16 +41,25 @@ final class RequestHeaderBasedLocaleContext implements LocaleContextInterface
throw new LocaleNotFoundException('No main request available.');
}
$localeCode = $request->headers->get('Accept-Language');
if (null === $localeCode) {
throw new LocaleNotFoundException('No locale is set on the master request\'s headers.');
if ([] === $this->availableLocalesCodes) {
$this->availableLocalesCodes = array_unique(array_merge(
[$this->localeProvider->getDefaultLocaleCode()],
$this->localeProvider->getAvailableLocalesCodes()
));
}
$availableLocalesCodes = $this->localeProvider->getAvailableLocalesCodes();
if (!in_array($localeCode, $availableLocalesCodes, true)) {
throw LocaleNotFoundException::notAvailable($localeCode, $availableLocalesCodes);
// Request::getPreferredLanguage() returns first available locale code if none matches. To allow detection of
// this unwanted behavior, we will prepend special locale code to the list of available locale codes.
$prependedAvailableLocalesCodes = array_merge([self::NO_CODE_VALID_STUB], $this->availableLocalesCodes);
$bestLocaleCode = $request->getPreferredLanguage($prependedAvailableLocalesCodes);
if (self::NO_CODE_VALID_STUB === $bestLocaleCode) {
throw new LocaleNotFoundException(sprintf(
'None of the available locales is acceptable: "%s".',
implode('", "', $this->availableLocalesCodes),
));
}
return $localeCode;
return $bestLocaleCode;
}
}

View file

@ -17,7 +17,6 @@ use PhpSpec\ObjectBehavior;
use Sylius\Component\Locale\Context\LocaleContextInterface;
use Sylius\Component\Locale\Context\LocaleNotFoundException;
use Sylius\Component\Locale\Provider\LocaleProviderInterface;
use Symfony\Component\HttpFoundation\HeaderBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
@ -40,42 +39,78 @@ final class RequestHeaderBasedLocaleContextSpec extends ObjectBehavior
$this->shouldThrow(LocaleNotFoundException::class)->during('getLocaleCode');
}
function it_throws_locale_not_found_exception_if_main_request_does_not_have_accept_language_in_header(
RequestStack $requestStack,
Request $request,
): void {
$requestStack->getMainRequest()->willReturn($request);
$request->headers = new HeaderBag();
$this->shouldThrow(LocaleNotFoundException::class)->during('getLocaleCode');
}
function it_throws_locale_not_found_exception_if_main_request_locale_code_is_not_among_available_ones(
function it_throws_locale_not_found_exception_if_locale_from_main_request_preferred_language_cannot_be_resolved(
RequestStack $requestStack,
LocaleProviderInterface $localeProvider,
Request $request,
): void {
$request = new Request();
$request->headers->set('Accept-Language', 'fr_FR');
$requestStack->getMainRequest()->willReturn($request);
$request->headers = new HeaderBag(['ACCEPT_LANGUAGE' => 'en_US']);
$localeProvider->getDefaultLocaleCode()->willReturn('pl_PL');
$localeProvider->getAvailableLocalesCodes()->willReturn(['pl_PL', 'de_DE']);
$this->shouldThrow(LocaleNotFoundException::class)->during('getLocaleCode');
}
function it_returns_main_request_locale_code(
function it_resolves_locale_from_main_request_preferred_language_in_locale_syntax(
RequestStack $requestStack,
LocaleProviderInterface $localeProvider,
Request $request,
): void {
$request = new Request();
$request->headers->set('Accept-Language', 'de_DE');
$requestStack->getMainRequest()->willReturn($request);
$request->headers = new HeaderBag(['Accept-Language' => 'pl_PL']);
$localeProvider->getDefaultLocaleCode()->willReturn('pl_PL');
$localeProvider->getAvailableLocalesCodes()->willReturn(['pl_PL', 'de_DE']);
$this->getLocaleCode()->shouldReturn('pl_PL');
$this->getLocaleCode()->shouldReturn('de_DE');
}
function it_resolves_locale_from_main_request_preferred_language_in_mixed_cased_language_syntax(
RequestStack $requestStack,
LocaleProviderInterface $localeProvider,
): void {
$request = new Request();
$request->headers->set('Accept-Language', 'dE-De');
$requestStack->getMainRequest()->willReturn($request);
$localeProvider->getDefaultLocaleCode()->willReturn('pl_PL');
$localeProvider->getAvailableLocalesCodes()->willReturn(['pl_PL', 'de_DE']);
$this->getLocaleCode()->shouldReturn('de_DE');
}
function it_resolves_locale_from_main_request_preferred_language_in_upper_cased_language_syntax(
RequestStack $requestStack,
LocaleProviderInterface $localeProvider,
): void {
$request = new Request();
$request->headers->set('Accept-Language', 'DE-DE');
$requestStack->getMainRequest()->willReturn($request);
$localeProvider->getDefaultLocaleCode()->willReturn('pl_PL');
$localeProvider->getAvailableLocalesCodes()->willReturn(['pl_PL', 'de_DE']);
$this->getLocaleCode()->shouldReturn('de_DE');
}
function it_resolves_locale_from_main_request_preferred_language_in_lower_cased_language_syntax(
RequestStack $requestStack,
LocaleProviderInterface $localeProvider,
): void {
$request = new Request();
$request->headers->set('Accept-Language', 'de-de');
$requestStack->getMainRequest()->willReturn($request);
$localeProvider->getDefaultLocaleCode()->willReturn('pl_PL');
$localeProvider->getAvailableLocalesCodes()->willReturn(['pl_PL', 'de_DE']);
$this->getLocaleCode()->shouldReturn('de_DE');
}
}

View file

@ -48,9 +48,9 @@ final class SyliusPayumExtensionTest extends AbstractExtensionTestCase
'gateway_config' => [
'validation_groups' => [
'paypal_express_checkout' => ['sylius', 'paypal'],
'offline' => ['sylius']
]
]
'offline' => ['sylius'],
],
],
]);
$this->assertContainerBuilderHasParameter('sylius.payum.gateway_config.validation_groups', ['paypal_express_checkout' => ['sylius', 'paypal'], 'offline' => ['sylius']]);

View file

@ -55,8 +55,11 @@ final class ProductsTest extends JsonApiTestCase
);
}
/** @test */
public function it_returns_product_with_translations_in_locale_from_header(): void
/**
* @test
* @dataProvider getGermanLocales
*/
public function it_returns_product_with_translations_in_locale_from_header(string $germanLocale): void
{
$fixtures = $this->loadFixturesFromFile('product/product_with_many_locales.yaml');
@ -68,7 +71,7 @@ final class ProductsTest extends JsonApiTestCase
server: [
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
'HTTP_ACCEPT_LANGUAGE' => 'de_DE',
'HTTP_ACCEPT_LANGUAGE' => $germanLocale,
],
);
@ -133,15 +136,19 @@ final class ProductsTest extends JsonApiTestCase
);
}
/** @test */
public function it_returns_product_attributes_collection_with_translations_in_locale_from_header(): void
{
/**
* @test
* @dataProvider getPolishLocales
*/
public function it_returns_product_attributes_collection_with_translations_in_locale_from_header(
string $polishLocale,
): void {
$this->loadFixturesFromFiles(['channel.yaml', 'product/product_attribute.yaml']);
$this->client->request(
method: 'GET',
uri: sprintf('/api/v2/shop/products/%s/attributes', 'MUG_SW'),
server: array_merge(self::CONTENT_TYPE_HEADER, ['HTTP_ACCEPT_LANGUAGE' => 'pl_PL']),
server: array_merge(self::CONTENT_TYPE_HEADER, ['HTTP_ACCEPT_LANGUAGE' => $polishLocale]),
);
$this->assertResponse(
@ -164,4 +171,20 @@ final class ProductsTest extends JsonApiTestCase
$this->assertCount(2, json_decode($this->client->getResponse()->getContent(), true)['hydra:member']);
}
public function getGermanLocales(): iterable
{
yield ['de_DE']; // Locale code syntax
yield ['de-DE']; // RFC 4647 and RFC 3066
yield ['DE-DE']; // RFC 4647 and RFC 3066
yield ['de-de']; // RFC 4647 and RFC 3066
}
public function getPolishLocales(): iterable
{
yield ['pl_PL']; // Locale code syntax
yield ['pl-PL']; // RFC 4647 and RFC 3066
yield ['PL-PL']; // RFC 4647 and RFC 3066
yield ['pl-pl']; // RFC 4647 and RFC 3066
}
}