Merge branch '2.0' into api-platform-3

* 2.0: (69 commits)
  Add a conflict for "doctrine/doctrine-bundle": "2.11.0"
  Disable check for security vulnerabilites step
  [CS][DX] Refactor
  [API] Adjust contract tests after changes with translations
  [API] Disallow removing the translation in default locale
  [API] Improve contract test of creating a product with invalid translation locale
  [Maintenance] Fix other possibly invalid route attribute getting
  [Maintenance] Fix AdminFilterSubscriber failing when no _route attribute present
  [ApiBundle] Fix ProductAssociationTypesTest
  [ApiBundle] Remove unnecessary method in TranslatableLocaleKeyDenormalizer
  [ApiBundle] Translation key as locale
  [ApiBundle] Fix type-hint
  [PromotionBundle] Remove unnecessary constraint
  [AttributeBundle] Standardize locale validation translations
  Standardize locale validation translations
  [ApiBundle] Improve readability
  [ApiBundle] Add contract test to check validation of locale
  [TaxonomyBundle] Add validation on locale of translation
  [ShippingBundle] Add validation on locale of translation
  [PromotionBundle] Add validation on locale of translation
  ...
This commit is contained in:
Grzegorz Sadowski 2023-11-14 13:44:48 +01:00
commit 398a14b12f
No known key found for this signature in database
GPG key ID: EF87FF4E6E3BD364
226 changed files with 4553 additions and 699 deletions

View file

@ -103,6 +103,7 @@ jobs:
- name: "Check for security vulnerabilities"
run: symfony security:check
continue-on-error: true
- name: "Validate Twig templates"
run: bin/console lint:twig src

View file

@ -27,3 +27,7 @@ references related issues.
- https://github.com/stof/StofDoctrineExtensionsBundle/issues/455
- https://github.com/doctrine-extensions/DoctrineExtensions/issues/2600
- `doctrine/doctrine-bundle:2.11.0`:
This version uses the [readonly property](https://github.com/doctrine/DoctrineBundle/blob/2.11.0/Repository/ServiceEntityRepositoryProxy.php#L34), which is available from PHP 8.1 while the package also supports PHP 7.4 and 8.0, leading to compatibility issues.

View file

@ -3,7 +3,7 @@
## Codebase
* Doctrine MongoDB and PHPCR is not longer supported in ResourceBundle and GridBundle:
* The following classes were removed:
* `Sylius\Bundle\GridBundle\Doctrine\PHPCRODM\DataSource`
@ -26,13 +26,13 @@
* `Sylius\Bundle\ResourceBundle\EventListener\ODMTranslatableListener`
* The following services were removed:
* `sylius.event_subscriber.odm_mapped_super_class`
* `sylius.event_subscriber.odm_repository_class`
* `sylius.grid_driver.doctrine.phpcrodm`
* The following parameters were removed:
* `sylius.mongodb_odm.repository.class`
* `sylius.phpcr_odm.repository.class`
@ -40,28 +40,33 @@
* `sylius.mailer.templates`
* Added the `Sylius\Component\Order\Context\ResettableCartContextInterface` that extends `Sylius\Component\Order\Context\CartContextInterface` and `Symfony\Contracts\Service\ResetInterface`.
* Added the `Sylius\Component\Order\Context\ResettableCartContextInterface` that
extends `Sylius\Component\Order\Context\CartContextInterface` and `Symfony\Contracts\Service\ResetInterface`.
* The name of the default `LiipImagineBundle`'s resolver and loader were changed from **default** to **sylius_image** ([reference](https://github.com/Sylius/Sylius/pull/12543)).
To change the default resolver and/or loader for `LiipImagineBundle`, configure `cache` and/or `data_loader` parameters under the `liip_imagine` key.
* The name of the default `LiipImagineBundle`'s resolver and loader were changed from **default** to **sylius_image
** ([reference](https://github.com/Sylius/Sylius/pull/12543)).
To change the default resolver and/or loader for `LiipImagineBundle`, configure `cache` and/or `data_loader`
parameters under the `liip_imagine` key.
* The `sylius/calendar` package has been replaced with `symfony/clock` package. All usages of the `Sylius\Calendar\Provider\DateTimeProviderInterface` class
have been replaced with `Symfony\Component\Clock\ClockInterface` class.
* The `sylius/calendar` package has been replaced with `symfony/clock` package. All usages of
the `Sylius\Calendar\Provider\DateTimeProviderInterface` class
have been replaced with `Symfony\Component\Clock\ClockInterface` class.
* The following classes were changed:
* The following classes were changed:
* `Sylius\Bundle\CoreBundle\CatalogPromotion\Announcer\CatalogPromotionAnnouncer`
* `Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account\RequestResetPasswordEmailHandler`
* `Sylius\Bundle\CoreBundle\PriceHistory\Logger\PriceChangeLogger`
* `Sylius\Bundle\CoreBundle\PriceHistory\Remover\ChannelPricingLogEntriesRemover`
* `Sylius\Bundle\ShippingBundle\Assigner\ShippingDateAssigner`
* `Sylius\Bundle\PromotionBundle\Criteria\DateRange`
* `Sylius\Bundle\ApiBundle\Applicator\ArchivingShippingMethodApplicator`
* `Sylius\Bundle\ApiBundle\CommandHandler\Account\RequestResetPasswordTokenHandler`
* `Sylius\Bundle\ApiBundle\CommandHandler\Account\VerifyCustomerAccountHandler`
* `Sylius\Component\Taxation\Checker\TaxRateDateEligibilityChecker`
* `Sylius\Bundle\CoreBundle\CatalogPromotion\Announcer\CatalogPromotionAnnouncer`
* `Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account\RequestResetPasswordEmailHandler`
* `Sylius\Bundle\CoreBundle\PriceHistory\Logger\PriceChangeLogger`
* `Sylius\Bundle\CoreBundle\PriceHistory\Remover\ChannelPricingLogEntriesRemover`
* `Sylius\Bundle\ShippingBundle\Assigner\ShippingDateAssigner`
* `Sylius\Bundle\PromotionBundle\Criteria\DateRange`
* `Sylius\Bundle\ApiBundle\Applicator\ArchivingShippingMethodApplicator`
* `Sylius\Bundle\ApiBundle\CommandHandler\Account\RequestResetPasswordTokenHandler`
* `Sylius\Bundle\ApiBundle\CommandHandler\Account\VerifyCustomerAccountHandler`
* `Sylius\Component\Taxation\Checker\TaxRateDateEligibilityChecker`
* The parameter order of `Sylius\Bundle\CoreBundle\Form\Type\Checkout\AddressType::__construct` has been changed:
```php
public function __construct(
+ private readonly AddressComparatorInterface $addressComparator,
@ -73,7 +78,9 @@
* The `\Serializable` interface has been removed from the `Sylius\Component\User\Model\UserInterface`.
* The parameter order of the `Sylius\Component\Core\OrderProcessing\OrderPaymentProcessor::__construct` has been changed:
* The parameter order of the `Sylius\Component\Core\OrderProcessing\OrderPaymentProcessor::__construct` has been
changed:
```php
public function __construct(
private OrderPaymentProviderInterface $orderPaymentProvider,
@ -112,7 +119,67 @@
): void;
```
* The `swiftmailer/swiftmailer` dependency has been removed. Use `symfony/mailer` instead.
* The following repository classes and interfaces were added, if you have custom repositories,
you need to update them to extend the new ones:
Addressing:
* `Sylius\Bundle\AddressingBundle\Doctrine\ORM\AddressRepository`
extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository`
implements `Sylius\Component\Addressing\Repository\AddressRepositoryInterface`
* `Sylius\Bundle\AddressingBundle\Doctrine\ORM\CountryRepository`
extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository`
implements `Sylius\Component\Addressing\Repository\CountryRepositoryInterface`
* `Sylius\Bundle\AddressingBundle\Doctrine\ORM\ProvinceRepository`
extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository`
implements `Sylius\Component\Addressing\Repository\ProvinceRepositoryInterface`
* `Sylius\Bundle\AddressingBundle\Doctrine\ORM\ZoneMemberRepository`
extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository`
implements `Sylius\Component\Addressing\Repository\ZoneMemberRepositoryInterface`
Attribute:
* `Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeRepository`
extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository`
implements `Sylius\Component\Attribute\Repository\AttributeRepositoryInterface`
* `Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeTranslationRepository`
extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository`
implements `Sylius\Component\Attribute\Repository\AttributeTranslationRepositoryInterface`
* `Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeValueRepository`
extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository`
implements `Sylius\Component\Attribute\Repository\AttributeValueRepositoryInterface`
Currency:
* `Sylius\Bundle\CurrencyBundle\Doctrine\ORM\CurrencyRepository`
extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository`
implements `Sylius\Component\Currency\Repository\CurrencyRepositoryInterface`
* The following repository classes and interfaces namespaces were changed, if you have custom repositories,
you need to update them to extend the new ones:
Addressing:
* `Sylius\Bundle\CoreBundle\Doctrine\ORM\AddressRepository` extended class changed from
`Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` to
`Sylius\Bundle\AddressingBundle\Doctrine\ORM\AddressRepository`
* `Sylius\Component\Core\Repository\AddressRepositoryInterface` implemented interface changed from
`Sylius\Component\Resource\Repository\RepositoryInterface` to
`Sylius\Component\Addressing\Repository\AddressRepositoryInterface`
Attribute:
* `Sylius\Bundle\CoreBundle\Doctrine\ORM\AttributeRepository` extended class changed from
`Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` to
`Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeRepository`
* `Sylius\Component\Product\Repository\ProductAttributeValueRepositoryInterface` extended interface changed from
`Sylius\Component\Resource\Repository\RepositoryInterface` to
`Sylius\Component\Attribute\Repository\AttributeValueRepositoryInterface`
## Frontend
* `use_webpack` option was removed from the `sylius_ui` configuration, and the Webpack has become the only module bundler provided by Sylius.
* `use_webpack` option was removed from the `sylius_ui` configuration, and the Webpack has become the only module
bundler provided by Sylius.
* `use_webpack` twig global variable was removed. Webpack is always used now, and there is no need to check for it.

View file

@ -3,7 +3,6 @@
"null", "true", "false",
"static", "self", "parent",
"array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", "mixed", "never",
"Swift_Message",
"ApiPlatform\\Core\\Api\\FilterInterface",
"ApiPlatform\\Core\\Api\\IriConverterInterface",
"ApiPlatform\\Core\\Api\\OperationType",
@ -82,11 +81,12 @@
"Payum\\Core\\Security\\TokenInterface",
"Payum\\Core\\Security\\Util\\Random",
"Payum\\Core\\Storage\\AbstractStorage",
"Payum\\Paypal\\ExpressCheckout\\Nvp\\PaypalExpressCheckoutGatewayFactory",
"Payum\\Stripe\\StripeCheckoutGatewayFactory",
"PHPUnit\\Framework\\ExpectationFailedException",
"Psr\\Container\\ContainerInterface",
"Psr\\Http\\Message\\RequestFactoryInterface",
"Psr\\Http\\Message\\StreamFactoryInterface",
"Stripe\\Stripe",
"Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactoryInterface"
],
"php-core-extensions" : [

View file

@ -62,9 +62,7 @@
"liip/imagine-bundle": "^2.10",
"nyholm/psr7": "^1.6",
"pagerfanta/pagerfanta": "^3.0",
"payum/core": "^1.7.3",
"payum/offline": "^1.7.3",
"payum/paypal-express-checkout-nvp": "^1.7.3",
"payum/payum-bundle": "^2.5",
"php-http/httplug": "^2.4",
"php-http/message-factory": "^1.0",
@ -186,6 +184,7 @@
"sylius/user-bundle": "self.version"
},
"conflict": {
"doctrine/doctrine-bundle": "2.11.0",
"doctrine/orm": ">= 2.16.0",
"stof/doctrine-extensions-bundle": "1.8.0"
},
@ -211,6 +210,8 @@
"matthiasnoback/symfony-dependency-injection-test": "^4.2",
"mikey179/vfsstream": "^1.6",
"mockery/mockery": "^1.4",
"payum/paypal-express-checkout-nvp": "^1.7.3",
"payum/stripe": "^1.7.3",
"phparkitect/phparkitect": "^0.2.9",
"phpspec/phpspec": "^7.2",
"phpspec/prophecy-phpunit": "^2.0",
@ -223,7 +224,6 @@
"psalm/plugin-mockery": "1.1.0",
"psr/event-dispatcher": "^1.0",
"rector/rector": "^0.18.0",
"stripe/stripe-php": "^12.0",
"sylius-labs/coding-standard": "^4.2",
"symfony/browser-kit": "^6.3.2",
"symfony/debug-bundle": "^6.3.2",

View file

@ -44,6 +44,61 @@ final class ChannelsTest extends JsonApiTestCase
);
}
/** @test */
public function it_deletes_a_channel(): void
{
$this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml']);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'GET',
uri: '/api/v2/admin/channels/MOBILE',
server: $header,
);
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_OK);
$this->client->request(
method: 'DELETE',
uri: '/api/v2/admin/channels/MOBILE',
server: $header,
);
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_NO_CONTENT);
$this->client->request(
method: 'GET',
uri: '/api/v2/admin/channels/MOBILE',
server: $header,
);
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_NOT_FOUND);
}
/** @test */
public function it_prevents_deleting_the_only_channel(): void
{
$this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml']);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'DELETE',
uri: '/api/v2/admin/channels/MOBILE',
server: $header,
);
$this->client->request(
method: 'DELETE',
uri: '/api/v2/admin/channels/WEB',
server: $header,
);
$this->assertResponse(
$this->client->getResponse(),
'admin/channel/delete_channel_that_cannot_be_deleted',
Response::HTTP_UNPROCESSABLE_ENTITY
);
}
/** @test */
public function it_gets_channels(): void
{
@ -119,7 +174,15 @@ final class ChannelsTest extends JsonApiTestCase
uri: '/api/v2/admin/channels/' . $channel->getCode(),
server: $header,
content: json_encode([
'defaultLocale' => '/api/v2/admin/locales/en_US',
'locales' => ['/api/v2/admin/locales/en_US'],
'shippingAddressInCheckoutRequired' => true,
'taxCalculationStrategy' => 'order_items_based',
'accountVerificationRequired' => false,
'name' => 'Web Store',
'description' => 'different description',
'hostname' => 'updated-hostname.com',
'color' => 'blue',
], JSON_THROW_ON_ERROR),
);
@ -129,4 +192,48 @@ final class ChannelsTest extends JsonApiTestCase
Response::HTTP_OK,
);
}
/** @test */
public function it_updates_a_shop_billing_data(): void
{
$fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml']);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
/** @var ChannelInterface $channel */
$channel = $fixtures['channel_web'];
$this->client->request(
method: 'PUT',
uri: sprintf('/api/v2/admin/channels/%s', $channel->getCode()),
server: $header,
content: json_encode([
'shopBillingData' => [
'@id' => sprintf('/api/v2/admin/shop-billing-datas/%s', $channel->getShopBillingData()->getId()),
'company' => 'DifferentCompany',
'taxId' => '123',
'countryCode' => 'DE',
'street' => 'Different Street',
'city' => 'different City',
'postcode' => '12-124'
]
], JSON_THROW_ON_ERROR),
);
$this->assertResponseCode(
$this->client->getResponse(),
Response::HTTP_OK,
);
$this->client->request(
method: 'GET',
uri: sprintf('/api/v2/admin/channels/%s/shop-billing-data', $channel->getCode()),
server: $header,
);
$this->assertResponse(
$this->client->getResponse(),
'admin/put_shop_billing_data_response',
Response::HTTP_OK,
);
}
}

View file

@ -38,7 +38,7 @@ final class ProductAssociationTypesTest extends JsonApiTestCase
$this->assertResponse(
$this->client->getResponse(),
'admin/get_product_association_type_response',
'admin/product_association_type/get_product_association_type_response',
Response::HTTP_OK,
);
}
@ -70,7 +70,7 @@ final class ProductAssociationTypesTest extends JsonApiTestCase
$this->assertResponse(
$this->client->getResponse(),
'admin/get_product_association_type_collection_response',
'admin/product_association_type/get_product_association_type_collection_response',
Response::HTTP_OK,
);
}
@ -97,11 +97,31 @@ final class ProductAssociationTypesTest extends JsonApiTestCase
$this->assertResponse(
$this->client->getResponse(),
'admin/post_product_association_type_response',
'admin/product_association_type/post_product_association_type_response',
Response::HTTP_CREATED,
);
}
/** @test */
public function it_does_not_create_product_association_type_without_required_data(): void
{
$this->loadFixturesFromFiles(['product/product_with_many_locales.yaml', 'authentication/api_administrator.yaml']);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-association-types',
server: $header,
content: '{}',
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_association_type/post_product_association_type_without_required_data_response',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
/** @test */
public function it_updates_product_association_type(): void
{
@ -116,17 +136,22 @@ final class ProductAssociationTypesTest extends JsonApiTestCase
server: $header,
content: json_encode([
'code' => 'TEST',
'translations' => ['en_US' => [
'name' => 'test',
'description' => 'test description',
'locale' => 'de_DE'
]]
'translations' => [
'en_US' => [
'@id' => sprintf('/api/v2/admin/product-association-type-translations/%s', $associationType->getTranslation('en_US')->getId()),
'name' => 'Similar products',
],
'de_DE' => [
'name' => 'test',
'description' => 'test description'
],
]
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/put_product_association_type_response',
'admin/product_association_type/put_product_association_type_response',
Response::HTTP_OK,
);
}

View file

@ -394,13 +394,7 @@ final class ProductAttributesTest extends JsonApiTestCase
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'translations' => [
'en_US' => [
'locale' => 'en_US',
],
],
], JSON_THROW_ON_ERROR),
content: '{}',
);
$this->assertResponse(
@ -423,6 +417,12 @@ final class ProductAttributesTest extends JsonApiTestCase
content: json_encode([
'code' => 'test',
'type' => 'foobar',
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Test',
],
],
], JSON_THROW_ON_ERROR),
);

View file

@ -141,7 +141,7 @@ final class ProductVariantsTest extends JsonApiTestCase
}
/** @test */
public function it_creates_a_product_variant_enabled_by_default(): void
public function it_creates_a_product_variant_enabled_by_default_with_translation_in_default_locale(): void
{
$this->loadFixturesFromFiles([
'authentication/api_administrator.yaml',
@ -168,7 +168,7 @@ final class ProductVariantsTest extends JsonApiTestCase
$this->assertResponse(
$this->client->getResponse(),
'admin/product_variant/post_product_variant_enabled_by_default_response',
'admin/product_variant/post_product_variant_enabled_by_default_with_translation_in_default_locale_response',
Response::HTTP_CREATED,
);
}
@ -325,13 +325,11 @@ final class ProductVariantsTest extends JsonApiTestCase
'minimumPrice' => 500,
]],
'translations' => [
'pl_PL' => [
'@id' => sprintf('/api/v2/admin/product-variant-translations/%s', $productVariant->getTranslation('pl_PL')->getId()),
'locale' => 'pl_PL',
'name' => 'Czerwony kubek',
'en_US' => [
'@id' => sprintf('/api/v2/admin/product-variant-translations/%s', $productVariant->getTranslation('en_US')->getId()),
'name' => 'Red mug',
],
'de_DE' => [
'locale' => 'de_DE',
'name' => 'Rote Tasse',
],
],
@ -396,6 +394,41 @@ final class ProductVariantsTest extends JsonApiTestCase
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY);
}
/** @test */
public function it_does_not_allow_to_update_product_variant_without_translation_in_default_locale(): void
{
$fixtures = $this->loadFixturesFromFiles([
'authentication/api_administrator.yaml',
'channel.yaml',
'tax_category.yaml',
'shipping_category.yaml',
'product/product_variant.yaml',
]);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
/** @var ProductVariantInterface $productVariant */
$productVariant = $fixtures['product_variant'];
$this->client->request(
method: 'PUT',
uri: sprintf('/api/v2/admin/product-variants/%s', $productVariant->getCode()),
server: $header,
content: json_encode([
'translations' => [
'de_DE' => [
'name' => 'Tasse',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_variant/put_product_variant_without_translation_in_default_locale_response',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
/** @test */
public function it_deletes_the_product_variant(): void
{

View file

@ -99,7 +99,6 @@ final class ProductsTest extends JsonApiTestCase
]],
'translations' => [
'en_US' => [
'locale' => 'en_US',
'slug' => 'mug',
'name' => 'Mug',
'description' => 'This is a mug',
@ -108,7 +107,6 @@ final class ProductsTest extends JsonApiTestCase
'metaDescription' => 'Mug description',
],
'pl_PL' => [
'locale' => 'pl_PL',
'slug' => 'kubek',
'name' => 'Kubek',
'description' => 'To jest kubek',
@ -127,6 +125,58 @@ final class ProductsTest extends JsonApiTestCase
);
}
/** @test */
public function it_does_not_create_a_product_without_required_data(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/products',
server: $header,
content: '{}',
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product/post_product_without_required_data_response',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
/** @test */
public function it_does_not_create_a_product_with_invalid_translation_locale(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/products',
server: $header,
content: json_encode([
'code' => 'MUG',
'translations' => [
'en_US' => [
'slug' => 'mug',
'name' => 'Mug',
],
'a' => [
'slug' => 'kubek',
'name' => 'Kubek',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product/post_product_with_invalid_translation_locale',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
/** @test */
public function it_updates_the_existing_product(): void
{
@ -178,7 +228,6 @@ final class ProductsTest extends JsonApiTestCase
'translations' => [
'en_US' => [
'@id' => sprintf('/api/v2/admin/product-translations/%s', $product->getTranslation('en_US')->getId()),
'locale' => 'en_US',
'slug' => 'caps/cap',
'name' => 'Cap',
'description' => 'This is a cap',
@ -188,7 +237,6 @@ final class ProductsTest extends JsonApiTestCase
],
'pl_PL' => [
'@id' => sprintf('/api/v2/admin/product-translations/%s', $product->getTranslation('pl_PL')->getId()),
'locale' => 'pl_PL',
'slug' => 'czapki/czapka',
'name' => 'Czapka',
'description' => 'To jest czapka',

View file

@ -99,13 +99,7 @@ final class TaxonsTest extends JsonApiTestCase
method: 'POST',
uri: '/api/v2/admin/taxons',
server: $header,
content: json_encode([
'translations' => [
'en_US' => [
'locale' => 'en_US',
]
]
], JSON_THROW_ON_ERROR),
content: '{}',
);
$this->assertResponse(

View file

@ -10,7 +10,7 @@ Feature: Adding a new channel
And the store operates in "United States" and "Poland"
And I am logged in as an administrator
@ui @api
@api @ui
Scenario: Adding a new channel
When I want to create a new channel
And I specify its code as "MOBILE"
@ -23,7 +23,7 @@ Feature: Adding a new channel
Then I should be notified that it has been successfully created
And the channel "Mobile channel" should appear in the registry
@ui @api
@api @ui
Scenario: Adding a new channel with additional fields
When I want to create a new channel
And I specify its code as "MOBILE"

View file

@ -10,7 +10,7 @@ Feature: Adding a new channel with shop billing data
And the store operates in "United States"
And I am logged in as an administrator
@ui @api
@api @ui
Scenario: Adding a new channel with shop billing data
When I want to create a new channel
And I specify its code as "MOBILE"

View file

@ -9,7 +9,7 @@ Feature: Browsing channels
And the store operates on another channel named "Mobile Channel"
And I am logged in as an administrator
@ui @api
@api @ui
Scenario: Browsing defined channels
When I want to browse channels
Then I should see 2 channels in the list

View file

@ -8,7 +8,7 @@ Feature: Channel unique code validation
Given the store operates on a channel identified by "WEB" code
And I am logged in as an administrator
@ui
@api @ui
Scenario: Trying to add channel with taken code
When I want to create a new channel
And I specify its code as "WEB"

View file

@ -7,7 +7,7 @@ Feature: Channel validation
Background:
Given I am logged in as an administrator
@ui
@api @ui
Scenario: Trying to add a new channel without specifying its code
When I want to create a new channel
And I name it "Mobile channel"
@ -16,7 +16,7 @@ Feature: Channel validation
Then I should be notified that code is required
And channel with name "Mobile channel" should not be added
@ui
@api @ui
Scenario: Trying to add a new channel without specifying its name
When I want to create a new channel
And I specify its code as "MOBILE"
@ -25,7 +25,7 @@ Feature: Channel validation
Then I should be notified that name is required
And channel with code "MOBILE" should not be added
@ui
@api @ui
Scenario: Trying to add a new channel without base currency
When I want to create a new channel
And I specify its code as "MOBILE"
@ -34,7 +34,7 @@ Feature: Channel validation
Then I should be notified that base currency is required
And channel with code "MOBILE" should not be added
@ui
@api @ui
Scenario: Trying to add a new channel without default locale
When I want to create a new channel
And I specify its code as "MOBILE"
@ -43,7 +43,7 @@ Feature: Channel validation
Then I should be notified that default locale is required
And channel with code "MOBILE" should not be added
@ui
@api @ui
Scenario: Trying to remove name from existing channel
Given the store operates on a channel named "Web Channel"
When I want to modify this channel

View file

@ -9,7 +9,7 @@ Feature: Deleting a channel
And the store operates on another channel named "Mobile Store"
And I am logged in as an administrator
@ui
@api @ui
Scenario: Deleted channel should disappear from the registry
When I delete channel "Web Store"
Then I should be notified that it has been successfully deleted

View file

@ -8,20 +8,12 @@ Feature: Editing channel
Given the store operates on a channel named "Web Channel"
And I am logged in as an administrator
@todo
Scenario: Trying to change channel code
@api @ui
Scenario: Being unable to change the code of an existing channel
When I want to modify a channel "Web Channel"
And I change its code to "MOBILE"
And I save my changes
Then I should be notified that code cannot be changed
And channel "Web Channel" should still have code "MOBILE"
Then I should not be able to edit its code
@ui
Scenario: Seeing disabled code field when editing channel
When I want to modify a channel "Web Channel"
Then the code field should be disabled
@ui
@api @ui
Scenario: Renaming the channel
When I want to modify a channel "Web Channel"
And I rename it to "Website store"
@ -29,7 +21,7 @@ Feature: Editing channel
Then I should be notified that it has been successfully edited
And this channel name should be "Website store"
@ui
Scenario: Seeing disabled base currency field during channel edition
@api @ui
Scenario: Being unable to change base currency of an existing channel
When I want to modify a channel "Web Channel"
Then the base currency field should be disabled
Then I should not be able to edit its base currency

View file

@ -11,7 +11,7 @@ Feature: Editing menu taxon on channel
And channel "Web Store" has menu taxon "Clothes"
And I am logged in as an administrator
@ui @javascript
@api @ui @javascript
Scenario: Editing menu taxon on the channel
When I want to modify a channel "Web Store"
And I change its menu taxon to "Guns"

View file

@ -10,14 +10,20 @@ Feature: Editing shop billing data on channel
And channel "Web Store" billing data is "Ragnarok", "Pacific Coast Hwy", "90806" "Los Angeles", "United States" with "1100110011" tax ID
And I am logged in as an administrator
@ui
@api @ui
Scenario: Editing shop billing data on channel
When I want to modify a channel "Web Store"
And I specify company as "Götterdämmerung"
And I specify tax ID as "666777"
And I specify shop billing address as "Valhalla", "123" "Asgard", "United States"
And I specify shop billing data for this channel as "Götterdämmerung", "Valhalla", "123", "Asgard", "666777" tax ID and "United States" country
And I save my changes
Then I should be notified that it has been successfully edited
And this channel company should be "Götterdämmerung"
And this channel tax ID should be "666777"
And this channel shop billing address should be "Valhalla", "123" "Asgard", "United States"
And this channel shop billing address should be "Valhalla", "123" "Asgard" and "United States" country
@api @no-ui
Scenario: Editing shop billing data with wrong country code
When I want to modify a channel "Web Store"
And I specify new country code for this channel as "ZZ"
And I save my changes
Then I should be notified that it is not a valid country
And this channel shop billing address should still be "Pacific Coast Hwy", "90806" "Los Angeles" and "United States" country

View file

@ -9,8 +9,8 @@ Feature: Not being able to add a disabled channel when no other exist
And the store has locale "English (United States)"
And I am logged in as an administrator
@ui
Scenario: Adding a new disabled channel should result
@api @ui
Scenario: Trying to add a new disabled channel when no other exist
When I want to create a new channel
And I specify its code as "MOBILE"
And I name it "Mobile channel"

View file

@ -8,8 +8,8 @@ Feature: Not being able to delete a last available channel
Given the store operates on a channel named "Web Store"
And I am logged in as an administrator
@ui
Scenario: Prevented from deleting only channel
@api @ui
Scenario: Preventing from deleting only channel
When I delete channel "Web Store"
Then I should be notified that it cannot be deleted
And this channel should still be in the registry

View file

@ -8,7 +8,7 @@ Feature: Toggling a channel
Given the store operates on a channel named "Web Channel"
And I am logged in as an administrator
@ui
@api @ui
Scenario: Disabling the last available channel
Given the channel "Web Channel" is enabled
When I want to modify this channel

View file

@ -7,20 +7,24 @@ Feature: Selecting available currencies for a channel
Background:
Given the store has currency "Euro"
And the store has locale "English (United States)"
And the store operates in "United States"
And I am logged in as an administrator
@ui
@api @ui
Scenario: Adding a new channel with currencies
When I want to create a new channel
And I specify its code as MOBILE
And I choose "Euro" as the base currency
And I name it "Mobile store"
And I allow for paying in "Euro"
And I make it available in "English (United States)"
And I choose "English (United States)" as a default locale
And I select the "Order items based" as tax calculation strategy
And I add it
Then I should be notified that it has been successfully created
And paying in Euro should be possible for the "Mobile store" channel
@ui
@api @ui
Scenario: Adding currencies to an existing channel
Given the store operates on a channel named "Web store"
When I want to modify this channel

View file

@ -8,19 +8,21 @@ Feature: Selecting default tax zone for a channel
Given the store operates on a single channel in "United States"
And I am logged in as an administrator
@ui
@api @ui
Scenario: Adding a new channel with default tax zone
When I want to create a new channel
And I specify its code as "MOBILE"
And I name it "Mobile store"
And I select the "United States" as default tax zone
And I choose "USD" as the base currency
And I make it available in "English (United States)"
And I choose "English (United States)" as a default locale
And I select the "Order items based" as tax calculation strategy
And I add it
Then I should be notified that it has been successfully created
And the default tax zone for the "Mobile store" channel should be "United States"
@ui
@api @ui
Scenario: Selecting default tax zone for existing channel
Given the store operates on a channel named "Web store"
When I want to modify this channel
@ -29,7 +31,7 @@ Feature: Selecting default tax zone for a channel
Then I should be notified that it has been successfully edited
And the default tax zone for the "Web store" channel should be "United States"
@ui
@api @ui
Scenario: Removing existing channel default tax zone
Given the store operates on a channel named "Web store"
And its default tax zone is zone "US"

View file

@ -9,7 +9,7 @@ Feature: Selecting available locales for a channel
And the store has locale "English (United States)"
And I am logged in as an administrator
@ui
@api @ui
Scenario: Adding a new channel with locales
When I want to create a new channel
And I specify its code as "MOBILE"
@ -17,11 +17,12 @@ Feature: Selecting available locales for a channel
And I make it available in "English (United States)"
And I choose "Euro" as the base currency
And I choose "English (United States)" as a default locale
And I select the "Order items based" as tax calculation strategy
And I add it
Then I should be notified that it has been successfully created
And the channel "Mobile Channel" should be available in "English (United States)"
@ui
@api @ui
Scenario: Adding locales to an existing channel
Given the store operates on a channel named "Web Channel"
When I want to modify this channel
@ -30,23 +31,23 @@ Feature: Selecting available locales for a channel
Then I should be notified that it has been successfully edited
And the channel "Web Channel" should be available in "English (United States)"
@ui
@api @ui
Scenario: Being unable to disable locale used as the default one for a channel
Given the store operates on a channel named "Web"
And this channel allows to shop using "English (United States)" and "Polish (Poland)" locales
And this channel uses the "English (United States)" locale as default
And I am modifying a channel "Web"
When I make it available only in "Polish (Poland)"
When I want to modify this channel
And I make it available only in "Polish (Poland)"
And I try to save my changes
Then I should be notified that the default locale has to be enabled
@ui
@api @ui
Scenario: Being unable to set disabled locale as a default one for a channel
Given the store has locale "Polish (Poland)"
And the store operates on a channel named "Web"
And this channel allows to shop using the "English (United States)" locale
And this channel uses the "English (United States)" locale as default
And I am modifying a channel "Web"
When I choose "Polish (Poland)" as a default locale
When I want to modify this channel
And I choose "Polish (Poland)" as a default locale
And I try to save my changes
Then I should be notified that the default locale has to be enabled

View file

@ -20,19 +20,20 @@ Feature: Selecting tax calculation strategy for a channel
Then I should be notified that it has been successfully created
And the tax calculation strategy for the "Mobile store" channel should be "Order items based"
@ui
@api @ui
Scenario: Adding a new channel with tax calculation strategy
When I want to create a new channel
And I specify its code as "MOBILE"
And I select the "Order item units based" as tax calculation strategy
And I name it "Mobile store"
And I choose "Euro" as the base currency
And I make it available in "English (United States)"
And I choose "English (United States)" as a default locale
And I add it
Then I should be notified that it has been successfully created
And the tax calculation strategy for the "Mobile store" channel should be "Order item units based"
@ui
@api @ui
Scenario: Changing tax calculation strategy of existing channel
Given the store operates on a channel named "Web store"
When I want to modify this channel

View file

@ -9,7 +9,7 @@ Feature: Toggling a channel
And the store operates on another channel named "Mobile Channel"
And I am logged in as an administrator
@ui
@api @ui
Scenario: Disabling the channel
Given the channel "Web Channel" is enabled
When I want to modify this channel
@ -18,7 +18,7 @@ Feature: Toggling a channel
Then I should be notified that it has been successfully edited
And this channel should be disabled
@ui
@api @ui
Scenario: Enabling the channel
Given the channel "Web Channel" is disabled
When I want to modify this channel

View file

@ -8,7 +8,7 @@ Feature: Seeing correct select attribute values in different locale than default
Given the store is available in "French (France)"
And I am logged in as an administrator
@ui @javascript @api
@ui @javascript @no-api
Scenario: Seeing correct attribute values in different locale than default one
When I want to create a new select product attribute
And I specify its code as "mug_material"

View file

@ -1820,6 +1820,11 @@ parameters:
count: 1
path: src/Sylius/Bundle/ApiBundle/Serializer/ShippingMethodNormalizer.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\TranslatableDenormalizer\\:\\:hasDefaultTranslation\\(\\) has parameter \\$translations with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/Serializer/TranslatableDenormalizer.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ZoneDenormalizer\\:\\:getZoneMemberByCode\\(\\) has parameter \\$zoneMembers with generic interface Doctrine\\\\Common\\\\Collections\\\\Selectable but does not specify its types\\: TKey, T$#"
count: 1

View file

@ -24,5 +24,7 @@
<server name="IS_DOCTRINE_ORM_SUPPORTED" value="true" />
<server name="ESCAPE_JSON" value="true" />
<server name="KERNEL_CLASS" value="App\Kernel" />
</php>
</phpunit>

View file

@ -62,7 +62,9 @@ interface ApiClientInterface
public function addFile(string $key, UploadedFile $file): void;
public function addRequestData(string $key, string|int|bool|array $value): void;
public function addRequestData(string $key, null|string|int|bool|array $value): void;
public function replaceRequestData(string $key, null|string|int|bool|array $value): void;
public function setSubResourceData(string $key, array $data): void;

View file

@ -200,12 +200,18 @@ final class ApiPlatformClient implements ApiClientInterface
$this->request->updateFiles([$key => $file]);
}
/** @param string|int|bool|array $value */
public function addRequestData(string $key, $value): void
public function addRequestData(string $key, null|string|int|bool|array $value): void
{
$this->request->updateContent([$key => $value]);
}
public function replaceRequestData(string $key, null|string|int|bool|array $value): void
{
$requestContent = $this->request->getContent();
$this->request->setContent(array_replace($requestContent, [$key => $value]));
}
public function updateRequestData(array $data): void
{
$this->request->updateContent($data);

View file

@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Behat\Context\Api\Admin;
use ApiPlatform\Api\IriConverterInterface;
use Behat\Behat\Context\Context;
use Sylius\Behat\Client\ApiClientInterface;
use Sylius\Behat\Client\ResponseCheckerInterface;
use Sylius\Behat\Context\Api\Resources;
use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\ShopBillingDataInterface;
use Webmozart\Assert\Assert;
final class ManagingChannelsBillingDataContext implements Context
{
public function __construct(
private ApiClientInterface $client,
private ResponseCheckerInterface $responseChecker,
private IriConverterInterface $iriConverter,
) {
}
/**
* @Then /^(this channel) company should be "([^"]+)"$/
*/
public function thisChannelCompanyShouldBe(ChannelInterface $channel, string $company): void
{
$shopBillingData = $this->getShopBillingDataFromChannel($channel);
Assert::same($shopBillingData->getCompany(), $company);
}
/**
* @Then /^(this channel) tax ID should be "([^"]+)"$/
*/
public function thisChanneTaxIdShouldBe(ChannelInterface $channel, string $taxId): void
{
$shopBillingData = $this->getShopBillingDataFromChannel($channel);
Assert::same($shopBillingData->getTaxId(), $taxId);
}
/**
* @Then /^(this channel) shop billing address should be "([^"]+)", "([^"]+)" "([^"]+)" and ("([^"]+)" country)$/
* @Then /^(this channel) shop billing address should still be "([^"]+)", "([^"]+)" "([^"]+)" and ("([^"]+)" country)$/
*/
public function thisChannelShopBillingAddressShouldBe(
ChannelInterface $channel,
string $street,
string $postcode,
string $city,
CountryInterface $country,
): void {
$shopBillingData = $this->getShopBillingDataFromChannel($channel);
Assert::same($shopBillingData->getStreet(), $street);
Assert::same($shopBillingData->getPostcode(), $postcode);
Assert::same($shopBillingData->getCity(), $city);
Assert::same($shopBillingData->getCountryCode(), $country->getCode());
}
private function getShopBillingDataFromChannel(ChannelInterface $channel): ShopBillingDataInterface
{
$this->client->show(Resources::CHANNELS, $channel->getCode());
/** @var ShopBillingDataInterface $shopBillingData */
$shopBillingData = $this->iriConverter->getResourceFromIri($this->responseChecker->getValue($this->client->getLastResponse(), 'shopBillingData'));
return $shopBillingData;
}
}

View file

@ -20,6 +20,7 @@ use Sylius\Behat\Client\ResponseCheckerInterface;
use Sylius\Behat\Context\Api\Resources;
use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface;
use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Component\Addressing\Model\ZoneInterface;
use Sylius\Component\Core\Formatter\StringInflector;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\TaxonInterface;
@ -47,6 +48,14 @@ final class ManagingChannelsContext implements Context
$this->client->buildCreateRequest(Resources::CHANNELS);
}
/**
* @When I delete channel :channel
*/
public function iDeleteChannel(ChannelInterface $channel): void
{
$this->client->delete(Resources::CHANNELS, $channel->getCode());
}
/**
* @When I want to modify a channel :channel
*/
@ -55,23 +64,78 @@ final class ManagingChannelsContext implements Context
$this->client->buildUpdateRequest(Resources::CHANNELS, $channel->getCode());
}
/**
* @When I rename it to :name
* @When I do not name it
* @When I remove its name
*/
public function iRenameIt(string $name = ''): void
{
$this->client->addRequestData('name', $name);
}
/**
* @When /^I (enable|disable) it$/
*/
public function iDisableIt(string $toggleAction): void
{
$this->client->addRequestData('enabled', $toggleAction === 'enable');
}
/**
* @When I change its menu taxon to :taxon
*/
public function iChangeItsMenuTaxonTo(TaxonInterface $taxon): void
{
$this->client->addRequestData('menuTaxon', $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin'));
}
/**
* @When I specify its :field as :value
* @When I :field it :value
* @When I set its :field as :value
* @When I define its :field as :value
* @When I do not specify its :field
*/
public function iSpecifyItsAs(string $field, string $value): void
public function iSpecifyItsAs(string $field, string $value = ''): void
{
$this->client->addRequestData($field, $value);
}
/**
* @When I choose :currency as the base currency
* @When I do not choose base currency
*/
public function iChooseAsTheBaseCurrency(CurrencyInterface $currency): void
public function iChooseAsTheBaseCurrency(?CurrencyInterface $currency = null): void
{
$this->client->addRequestData('baseCurrency', $this->sectionAwareIriConverter->getIriFromResourceInSection($currency, 'admin'));
$this->client->addRequestData(
'baseCurrency',
null === $currency ? $currency : $this->sectionAwareIriConverter->getIriFromResourceInSection($currency, 'admin'),
);
}
/**
* @When I allow for paying in :currency
*/
public function iAllowToPayingForThisChannel(CurrencyInterface $currency): void
{
$this->client->addRequestData('currencies', [$this->iriConverter->getIriFromResource($currency)]);
}
/**
* @When I select the :zone as default tax zone
*/
public function iSelectDefaultTaxZone(ZoneInterface $zone): void
{
$this->client->addRequestData('defaultTaxZone', $this->iriConverter->getIriFromResource($zone));
}
/**
* @When I remove its default tax zone
*/
public function iRemoveItsDefaultTaxZone(): void
{
$this->client->addRequestData('defaultTaxZone', null);
}
/**
@ -79,15 +143,27 @@ final class ManagingChannelsContext implements Context
*/
public function iMakeItAvailableInLocale(LocaleInterface $locale): void
{
$this->client->addRequestData('locales', [$this->iriConverter->getIriFromResource($locale)]);
$this->client->addRequestData('locales', [$this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin')]);
}
/**
* @When I make it available only in :locale
*/
public function iMakeItAvailableOnlyInLocale(LocaleInterface $locale): void
{
$this->client->replaceRequestData('locales', [$this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin')]);
}
/**
* @When I choose :locale as a default locale
* @When I do not choose default locale
*/
public function iChooseAsADefaultLocale(LocaleInterface $locale): void
public function iChooseAsADefaultLocale(?LocaleInterface $locale = null): void
{
$this->client->addRequestData('defaultLocale', $this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin'));
$this->client->addRequestData(
'defaultLocale',
null === $locale ? $locale : $this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin'),
);
}
/**
@ -165,6 +241,44 @@ final class ManagingChannelsContext implements Context
$this->shopBillingData['taxId'] = $taxId;
}
/**
* @When /^I specify shop billing data for (this channel) as "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)" tax ID and ("([^"]+)" country)$/
*/
public function iSpecifyShopBillingDataAs(
ChannelInterface $channel,
string $company,
string $street,
string $postcode,
string $city,
string $taxId,
CountryInterface $country,
): void {
$shopBillingDataId = $this->iriConverter->getIriFromResource($channel->getShopBillingData());
$this->client->addRequestData(
'shopBillingData',
[
'@id' => $shopBillingDataId,
'company' => $company,
'street' => $street,
'postcode' => $postcode,
'city' => $city,
'countryCode' => $country->getCode(),
'taxId' => $taxId,
],
);
}
/**
* @When /^I specify new country code for (this channel) as "([^"]+)"$/
*/
public function iSpecifyNewCountryCodeForThisChannelAs(ChannelInterface $channel, string $code): void
{
$shopBillingDataId = $this->iriConverter->getIriFromResource($channel->getShopBillingData());
$this->client->addRequestData('shopBillingData', ['@id' => $shopBillingDataId, 'countryCode' => $code]);
}
/**
* @When I specify shop billing address as :street, :postcode :city, :country
*/
@ -180,6 +294,15 @@ final class ManagingChannelsContext implements Context
$this->shopBillingData['countryCode'] = $country->getCode();
}
/**
* @Then I save it
*/
public function iSaveIt(): void
{
$this->iAddIt();
$this->client->update();
}
/**
* @When I select the :taxCalculationStrategy as tax calculation strategy
*/
@ -280,6 +403,74 @@ final class ManagingChannelsContext implements Context
);
}
/**
* @Then I should not be able to edit its code
*/
public function iShouldNotBeAbleToEditItsCode(): void
{
$this->client->updateRequestData(['code' => 'NEW_CODE']);
Assert::false($this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE'));
}
/**
* @Then the base currency field should be disabled
* @Then I should not be able to edit its base currency
*/
public function theBaseCurrencyFieldShouldBeDisabled(): void
{
$this->client->updateRequestData(['baseCurrency' => 'PLN']);
Assert::false($this->responseChecker->hasValue($this->client->update(), 'baseCurrency', 'PLN'));
}
/**
* @Then /^(this channel) name should be "([^"]*)"$/
*/
public function thisChannelNameShouldBe(ChannelInterface $channel, string $name): void
{
Assert::true(
$this->responseChecker->hasValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'name', $name),
sprintf('Its Channel does not have name %s.', $name),
);
}
/**
* @Then the :channel channel should no longer exist in the registry
*/
public function theChannelShouldNoLongerExistInTheRegistry(string $name): void
{
Assert::false(
$this->responseChecker->hasItemWithValue($this->client->index(Resources::CHANNELS), 'name', $name),
sprintf('Channel with name %s exists', $name),
);
}
/**
* @Then I should be notified that it has been successfully deleted
*/
public function iShouldBeNotifiedThatChannelHasBeenDeleted(): void
{
Assert::true($this->responseChecker->isDeletionSuccessful($this->client->getLastResponse()));
}
/**
* @Then /^(this channel) menu (taxon should be "([^"]+)")$/
*/
public function thisChannelMenuTaxonShouldBe(ChannelInterface $channel, TaxonInterface $taxon): void
{
Assert::true(
$this->responseChecker->hasValue(
$this->client->show(
Resources::CHANNELS,
$channel->getCode(),
),
'menuTaxon',
$this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin'),
),
);
}
/**
* @Then I should see :count channels in the list
*/
@ -299,4 +490,225 @@ final class ManagingChannelsContext implements Context
$type === 'shipping',
));
}
/**
* @Then I should be notified that it cannot be deleted
*/
public function iShouldBeNotifiedThatItCannotBeDeleted(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'The channel cannot be deleted. At least one enabled channel is required.',
);
}
/**
* @Then I should be notified that at least one channel has to be defined
*/
public function iShouldBeNotifiedThatAtLeastOneChannelHasToBeDefined(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'Must have at least one enabled entity',
);
}
/**
* @Then channel with name :channel should still be enabled
* @Then /^(this channel) should be enabled$/
*/
public function channelWithNameShouldStillBeEnabled(ChannelInterface $channel): void
{
Assert::true(
$this->responseChecker->hasValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'enabled', true),
sprintf('Channel with name %s does not exists', $channel->getName()),
);
}
/**
* @Then this channel should still be named :channel
*/
public function thisChannelShouldStillBeNamed(ChannelInterface $channel): void
{
Assert::true(
$this->responseChecker->hasValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'name', $channel->getName()),
sprintf('Channel with name %s does not exists', $channel->getName()),
);
}
/**
* @Then paying in :currency should be possible for the :channel channel
*/
public function payingInCurrencyShouldBePossibleForTheChannel(CurrencyInterface $currency, ChannelInterface $channel): void
{
$currencies = $this->responseChecker->getValue(
$this->client->show(Resources::CHANNELS, $channel->getCode()),
'currencies',
);
Assert::true(in_array($this->sectionAwareIriConverter->getIriFromResourceInSection($currency, 'admin'), $currencies));
}
/**
* @Then channel :channel should not have default tax zone
*/
public function channelShouldNotHaveDefaultTaxZone(ChannelInterface $channel): void
{
Assert::same(
$this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'defaultTaxZone'),
null,
sprintf('Channel %s has default tax zone', $channel->getName()),
);
}
/**
* @Then the default tax zone for the :channel channel should be :zone
*/
public function theDefaultTaxZoneForTheChannelShouldBe(ChannelInterface $channel, ZoneInterface $zone): void
{
Assert::same(
$this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'defaultTaxZone'),
$this->sectionAwareIriConverter->getIriFromResourceInSection($zone, 'admin'),
sprintf('Channel %s does not have %s default tax zone', $channel->getName(), $zone),
);
}
/**
* @Then the channel :channel should be available in :locale
*/
public function theChannelShouldBeAvailableIn(ChannelInterface $channel, LocaleInterface $locale): void
{
$locales = $this->responseChecker->getValue(
$this->client->show(Resources::CHANNELS, $channel->getCode()),
'locales',
);
Assert::true(in_array($this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin'), $locales));
}
/**
* @Then I should be notified that the default locale has to be enabled
*/
public function iShouldBeNotifiedThatTheDefaultLocaleHasToBeEnabled(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'defaultLocale: Default locale has to be enabled.',
);
}
/**
* @Then /^(this channel) should still be in the registry$/
*/
public function thisChannelShouldStillBeInTheRegistry(ChannelInterface $channel): void
{
Assert::true(
$this->responseChecker->hasItemWithValue($this->client->index(Resources::CHANNELS), 'code', $channel->getCode()),
sprintf('Channel with code %s does not exists', $channel->getCode()),
);
}
/**
* @Then the tax calculation strategy for the :channel channel should be :taxCalculationStrategy
*/
public function theTaxCalculationStrategyForTheChannelShouldBe(
ChannelInterface $channel,
string $taxCalculationStrategy,
): void {
Assert::same(
$this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'taxCalculationStrategy'),
StringInflector::nameToLowercaseCode($taxCalculationStrategy),
sprintf('Channel %s does not have %s tax calculation strategy', $channel->getName(), $taxCalculationStrategy),
);
}
/**
* @Then /^(this channel) should be disabled$/
*/
public function thisChannelShouldBeDisabled(ChannelInterface $channel): void
{
Assert::same(
$this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'enabled'),
false,
sprintf('Channel %s is enabled', $channel->getName()),
);
}
/**
* @Then I should be notified that :element is required
*/
public function iShouldBeNotifiedThatIsRequired(string $element): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
sprintf('%s: Please enter channel %s.', $element, $element),
);
}
/**
* @Then I should be notified that base currency is required
*/
public function iShouldBeNotifiedThatBaseCurrencyIsRequired(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'Expected IRI or nested document for attribute "baseCurrency", "NULL" given.',
);
}
/**
* @Then I should be notified that default locale is required
*/
public function iShouldBeNotifiedThatDefaultLocaleIsRequired(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'Expected IRI or nested document for attribute "defaultLocale", "NULL" given.',
);
}
/**
* @Then channel with :element :value should not be added
*/
public function channelWithShouldNotBeAdded(string $element, string $value): void
{
Assert::false(
$this->responseChecker->hasItemWithValue($this->client->index(Resources::CHANNELS), $element, $value),
sprintf('Channel with %s: %s exists', $element, $value),
);
}
/**
* @Then I should be notified that channel with this code already exists
*/
public function iShouldBeNotifiedThatChannelWithThisCodeAlreadyExists(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'code: Channel code has to be unique.',
);
}
/**
* @Then there should still be only one channel with :element :value
*/
public function thereShouldStillBeOnlyOneChannelWithCode(string $element, string $value): void
{
Assert::same(
count($this->responseChecker->getCollectionItemsWithValue($this->client->index(Resources::CHANNELS), $element, $value)),
1,
sprintf('There is more than one channel with %s: %s', $element, $value),
);
}
/**
* @Then I should be notified that it is not a valid country
*/
public function iShouldBeNotifiedThatItIsNotAValidCountryCode(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'countryCode: This value is not a valid country.',
);
}
}

View file

@ -276,7 +276,7 @@ final class ManagingProductsContext implements Context
public function iSetItsNonTranslatableAttributeTo(ProductAttributeInterface $attribute, string $value): void
{
$this->client->addSubResourceData(
'attributes',
'attributes',
[
'attribute' => $this->iriConverter->getIriFromResource($attribute),
'value' => $this->getAttributeValueInProperType($attribute, $value),
@ -290,7 +290,7 @@ final class ManagingProductsContext implements Context
public function iSetTheInvalidIntegerValueOfTheNonTranslatableAttributeTo(ProductAttributeInterface $attribute, int $value): void
{
$this->client->addSubResourceData(
'attributes',
'attributes',
[
'attribute' => $this->iriConverter->getIriFromResource($attribute),
'value' => $value,
@ -304,7 +304,7 @@ final class ManagingProductsContext implements Context
public function iSetTheInvalidStringValueOfTheNonTranslatableAttributeTo(ProductAttributeInterface $attribute, string $value): void
{
$this->client->addSubResourceData(
'attributes',
'attributes',
[
'attribute' => $this->iriConverter->getIriFromResource($attribute),
'value' => $value,
@ -320,7 +320,7 @@ final class ManagingProductsContext implements Context
public function iSetItsAttributeTo(
ProductAttributeInterface $attribute,
?string $value = null,
string $localeCode = 'en_US'
string $localeCode = 'en_US',
): void {
$this->client->addSubResourceData(
'attributes',
@ -363,7 +363,7 @@ final class ManagingProductsContext implements Context
public function iSelectValueInForTheAttribute(
string $value,
string $localeCode,
ProductAttributeInterface $attribute
ProductAttributeInterface $attribute,
): void {
$this->client->addSubResourceData(
'attributes',
@ -380,7 +380,7 @@ final class ManagingProductsContext implements Context
*/
public function iSelectValueForTheAttribute(
string $value,
ProductAttributeInterface $attribute
ProductAttributeInterface $attribute,
): void {
$this->client->addSubResourceData(
'attributes',
@ -678,7 +678,7 @@ final class ManagingProductsContext implements Context
public function nonTranslatableAttributeOfProductShouldBe(
ProductAttributeInterface $attribute,
ProductInterface $product,
string $value
string $value,
): void {
$this->client->show(Resources::PRODUCTS, $product->getCode());
@ -702,7 +702,7 @@ final class ManagingProductsContext implements Context
ProductAttributeInterface $attribute,
ProductInterface $product,
string $value,
string $localeCode = 'en_US'
string $localeCode = 'en_US',
): void {
$this->client->show(Resources::PRODUCTS, $product->getCode());
@ -718,7 +718,7 @@ final class ManagingProductsContext implements Context
foreach ($attributes as $attributeValue) {
if ($attributeValue['attribute'] === $this->sectionAwareIriConverter->getIriFromResourceInSection($attribute, 'admin')) {
throw new \InvalidArgumentException(
sprintf('Product %s have attribute %s', $product->getName(), $attribute->getName())
sprintf('Product %s have attribute %s', $product->getName(), $attribute->getName()),
);
}
}
@ -780,7 +780,7 @@ final class ManagingProductsContext implements Context
public function iShouldBeNotifiedThatTheAttributeInShouldBeLongerThan(
string $attributeName,
string $localeCode,
int $number
int $number,
): void {
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
@ -861,7 +861,7 @@ final class ManagingProductsContext implements Context
private function getAttributeValueInProperType(
ProductAttributeInterface $productAttribute,
string $value
string $value,
): string|bool|float|int {
switch ($productAttribute->getStorageType()) {
case AttributeValueInterface::STORAGE_BOOLEAN:
@ -887,7 +887,7 @@ final class ManagingProductsContext implements Context
}
throw new \InvalidArgumentException(
sprintf('Value "%s" not found in attribute "%s"', $value, $attribute->getName())
sprintf('Value "%s" not found in attribute "%s"', $value, $attribute->getName()),
);
}
@ -908,7 +908,7 @@ final class ManagingProductsContext implements Context
}
throw new \InvalidArgumentException(
sprintf('The given product does not have attribute %s', $attribute->getName())
sprintf('The given product does not have attribute %s', $attribute->getName()),
);
}

View file

@ -24,6 +24,22 @@ final class ManagingChannelsBillingDataContext implements Context
{
}
/**
* @When I specify shop billing data for this channel as :company, :street, :postcode, :city, :taxId tax ID and :country country
*/
public function iSpecifyNewShopBillingDataForChannelAs(
string $company,
string $street,
string $postcode,
string $city,
string $taxId,
CountryInterface $country,
): void {
$this->shopBillingDataElement->specifyCompany($company);
$this->shopBillingDataElement->specifyTaxId($taxId);
$this->shopBillingDataElement->specifyBillingAddress($street, $postcode, $city, $country->getCode());
}
/**
* @When I specify company as :company
*/
@ -69,6 +85,7 @@ final class ManagingChannelsBillingDataContext implements Context
}
/**
* @Then this channel shop billing address should be :street, :postcode :city and :country country
* @Then this channel shop billing address should be :street, :postcode :city, :country
*/
public function thisChannelShopBillingAddressShouldBe(

View file

@ -292,6 +292,7 @@ final class ManagingChannelsContext implements Context
* @Given I am modifying a channel :channel
* @When I want to modify a channel :channel
* @When /^I want to modify (this channel)$/
* @When I want to modify a billing data of channel :channel
*/
public function iWantToModifyChannel(ChannelInterface $channel): void
{
@ -315,6 +316,7 @@ final class ManagingChannelsContext implements Context
/**
* @When I save my changes
* @When I try to save my changes
* @When I save it
*/
public function iSaveMyChanges(): void
{
@ -332,7 +334,7 @@ final class ManagingChannelsContext implements Context
/**
* @Then there should still be only one channel with :element :value
*/
public function thereShouldStillBeOnlyOneChannelWithCode(string $element, string $value)
public function thereShouldStillBeOnlyOneChannelWithCode(string $element, string $value): void
{
$this->iWantToBrowseChannels();
@ -375,6 +377,7 @@ final class ManagingChannelsContext implements Context
/**
* @Then the code field should be disabled
* @Then I should not be able to edit its code
*/
public function theCodeFieldShouldBeDisabled(): void
{
@ -461,7 +464,7 @@ final class ManagingChannelsContext implements Context
/**
* @Then paying in :currencyCode should be possible for the :channel channel
*/
public function payingInEuroShouldBePossibleForTheChannel(string $currencyCode, ChannelInterface $channel): void
public function payingInCurrencyShouldBePossibleForTheChannel(string $currencyCode, ChannelInterface $channel): void
{
$this->updatePage->open(['id' => $channel->getId()]);
@ -607,6 +610,7 @@ final class ManagingChannelsContext implements Context
/**
* @Then the base currency field should be disabled
* @Then I should not be able to edit its base currency
*/
public function theBaseCurrencyFieldShouldBeDisabled(): void
{

View file

@ -64,6 +64,12 @@
<!-- <argument type="service" id="sylius.behat.section_iri_converter" />-->
<!-- </service>-->
<service id="Sylius\Behat\Context\Api\Admin\ManagingChannelsBillingDataContext">
<argument type="service" id="sylius.behat.api_platform_client.admin" />
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
<argument type="service" id="api_platform.iri_converter" />
</service>
<service id="sylius.behat.context.api.admin.managing_countries" class="Sylius\Behat\Context\Api\Admin\ManagingCountriesContext">
<argument type="service" id="sylius.behat.api_platform_client.admin" />
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />

View file

@ -29,6 +29,7 @@ default:
- sylius.behat.context.api.admin.managing_channels
- sylius.behat.context.api.admin.response
- sylius.behat.context.api.admin.save
- Sylius\Behat\Context\Api\Admin\ManagingChannelsBillingDataContext
- Sylius\Behat\Context\Api\Admin\ManagingChannelPriceHistoryConfigsContext
filters:

View file

@ -14,6 +14,10 @@ declare(strict_types=1);
namespace Sylius\Bundle\AddressingBundle\DependencyInjection;
use Sylius\Bundle\AddressingBundle\Controller\ProvinceController;
use Sylius\Bundle\AddressingBundle\Doctrine\ORM\AddressRepository;
use Sylius\Bundle\AddressingBundle\Doctrine\ORM\CountryRepository;
use Sylius\Bundle\AddressingBundle\Doctrine\ORM\ProvinceRepository;
use Sylius\Bundle\AddressingBundle\Doctrine\ORM\ZoneMemberRepository;
use Sylius\Bundle\AddressingBundle\Form\Type\AddressType;
use Sylius\Bundle\AddressingBundle\Form\Type\CountryType;
use Sylius\Bundle\AddressingBundle\Form\Type\ProvinceType;
@ -77,7 +81,7 @@ final class Configuration implements ConfigurationInterface
->scalarNode('model')->defaultValue(Address::class)->cannotBeEmpty()->end()
->scalarNode('interface')->defaultValue(AddressInterface::class)->cannotBeEmpty()->end()
->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end()
->scalarNode('repository')->cannotBeEmpty()->end()
->scalarNode('repository')->defaultValue(AddressRepository::class)->cannotBeEmpty()->end()
->scalarNode('factory')->defaultValue(Factory::class)->end()
->scalarNode('form')->defaultValue(AddressType::class)->cannotBeEmpty()->end()
->end()
@ -109,7 +113,7 @@ final class Configuration implements ConfigurationInterface
->scalarNode('model')->defaultValue(Country::class)->cannotBeEmpty()->end()
->scalarNode('interface')->defaultValue(CountryInterface::class)->cannotBeEmpty()->end()
->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end()
->scalarNode('repository')->cannotBeEmpty()->end()
->scalarNode('repository')->defaultValue(CountryRepository::class)->cannotBeEmpty()->end()
->scalarNode('factory')->defaultValue(Factory::class)->end()
->scalarNode('form')->defaultValue(CountryType::class)->cannotBeEmpty()->end()
->end()
@ -126,7 +130,7 @@ final class Configuration implements ConfigurationInterface
->scalarNode('model')->defaultValue(Province::class)->cannotBeEmpty()->end()
->scalarNode('interface')->defaultValue(ProvinceInterface::class)->cannotBeEmpty()->end()
->scalarNode('controller')->defaultValue(ProvinceController::class)->cannotBeEmpty()->end()
->scalarNode('repository')->cannotBeEmpty()->end()
->scalarNode('repository')->defaultValue(ProvinceRepository::class)->cannotBeEmpty()->end()
->scalarNode('factory')->defaultValue(Factory::class)->end()
->scalarNode('form')->defaultValue(ProvinceType::class)->cannotBeEmpty()->end()
->end()
@ -160,7 +164,7 @@ final class Configuration implements ConfigurationInterface
->scalarNode('model')->defaultValue(ZoneMember::class)->cannotBeEmpty()->end()
->scalarNode('interface')->defaultValue(ZoneMemberInterface::class)->cannotBeEmpty()->end()
->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end()
->scalarNode('repository')->cannotBeEmpty()->end()
->scalarNode('repository')->defaultValue(ZoneMemberRepository::class)->cannotBeEmpty()->end()
->scalarNode('factory')->defaultValue(Factory::class)->end()
->scalarNode('form')->defaultValue(ZoneMemberType::class)->cannotBeEmpty()->end()
->end()

View file

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\AddressingBundle\Doctrine\ORM;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository;
use Sylius\Component\Addressing\Model\AddressInterface;
use Sylius\Component\Addressing\Repository\AddressRepositoryInterface;
/**
* @implements AddressRepositoryInterface<AddressInterface>
*/
class AddressRepository extends EntityRepository implements AddressRepositoryInterface
{
}

View file

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\AddressingBundle\Doctrine\ORM;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository;
use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Component\Addressing\Repository\CountryRepositoryInterface;
/**
* @implements CountryRepositoryInterface<CountryInterface>
*/
class CountryRepository extends EntityRepository implements CountryRepositoryInterface
{
}

View file

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\AddressingBundle\Doctrine\ORM;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository;
use Sylius\Component\Addressing\Model\ProvinceInterface;
use Sylius\Component\Addressing\Repository\ProvinceRepositoryInterface;
/**
* @implements ProvinceRepositoryInterface<ProvinceInterface>
*/
class ProvinceRepository extends EntityRepository implements ProvinceRepositoryInterface
{
}

View file

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\AddressingBundle\Doctrine\ORM;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository;
use Sylius\Component\Addressing\Model\ZoneMemberInterface;
use Sylius\Component\Addressing\Repository\ZoneMemberRepositoryInterface;
/**
* @implements ZoneMemberRepositoryInterface<ZoneMemberInterface>
*/
class ZoneMemberRepository extends EntityRepository implements ZoneMemberRepositoryInterface
{
}

View file

@ -72,7 +72,7 @@ final readonly class RenderResetPasswordPageAction
->add('error', 'sylius.admin.password_reset.token_expired')
;
$attributes = $request->attributes->get('_sylius');
$attributes = $request->attributes->get('_sylius', []);
$redirect = $attributes['redirect'] ?? 'sylius_admin_login';
if (is_array($redirect)) {

View file

@ -56,7 +56,7 @@ final readonly class RequestPasswordResetAction
->add('success', 'sylius.admin.request_reset_password.success')
;
$options = $request->attributes->get('_sylius');
$options = $request->attributes->get('_sylius', []);
$redirectRoute = $options['redirect'] ?? 'sylius_admin_login';
if (is_array($redirectRoute)) {

View file

@ -52,7 +52,7 @@ final class ResetPasswordAction
->add('success', 'sylius.admin.password_reset.success')
;
$attributes = $request->attributes->get('_sylius');
$attributes = $request->attributes->get('_sylius', []);
$redirect = $attributes['redirect'] ?? 'sylius_admin_login';
if (is_array($redirect)) {

View file

@ -34,9 +34,11 @@ final class RemoveAvatarAction
public function __invoke(Request $request): Response
{
$userId = $request->attributes->get('id');
$userId = $request->attributes->get('id', '');
if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($userId, (string) $request->query->get('_csrf_token')))) {
if (!$this->csrfTokenManager->isTokenValid(
new CsrfToken($userId, (string) $request->query->get('_csrf_token', '')),
)) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');
}

View file

@ -38,9 +38,11 @@ final readonly class ResendOrderConfirmationEmailAction
public function __invoke(Request $request): Response
{
$orderId = $request->attributes->get('id');
$orderId = $request->attributes->get('id', '');
if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($orderId, (string) $request->query->get('_csrf_token')))) {
if (!$this->csrfTokenManager->isTokenValid(
new CsrfToken($orderId, (string) $request->query->get('_csrf_token', '')),
)) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');
}

View file

@ -38,9 +38,11 @@ final readonly class ResendShipmentConfirmationEmailAction
public function __invoke(Request $request): Response
{
$shipmentId = $request->attributes->get('id');
$shipmentId = $request->attributes->get('id', '');
if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($shipmentId, (string) $request->query->get('_csrf_token')))) {
if (!$this->csrfTokenManager->isTokenValid(
new CsrfToken($shipmentId, (string) $request->query->get('_csrf_token', '')),
)) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');
}

View file

@ -31,8 +31,13 @@ final class RemoveCatalogPromotionAction
public function __invoke(Request $request): Response
{
$catalogPromotionCode = $request->attributes->get('code');
if (null === $catalogPromotionCode) {
throw new NotFoundHttpException('The catalog promotion has not been found');
}
try {
$this->catalogPromotionRemovalProcessor->removeCatalogPromotion($request->attributes->get('code'));
$this->catalogPromotionRemovalProcessor->removeCatalogPromotion($catalogPromotionCode);
/** @var Session $session */
$session = $request->getSession();

View file

@ -44,17 +44,12 @@ final class AdminFilterSubscriber implements EventSubscriberInterface
}
$requestAttributes = $eventRequest->attributes;
$originalRoute = $requestAttributes->get('_route');
if (!$this->isIndexResourceRoute($originalRoute)) {
return;
}
if (!$this->isAdminSection($requestAttributes->get('_sylius', []))) {
return;
}
if (null === $requestAttributes->get('_controller')) {
if (
null === $requestAttributes->get('_controller') ||
!$this->isIndexResourceRoute($requestAttributes->get('_route', '')) ||
!$this->isAdminSection($requestAttributes->get('_sylius', []))
) {
return;
}
@ -80,6 +75,6 @@ final class AdminFilterSubscriber implements EventSubscriberInterface
private function isAdminSection(array $syliusParameters): bool
{
return array_key_exists('section', $syliusParameters) && 'admin' === $syliusParameters['section'];
return isset($syliusParameters['section']) && 'admin' === $syliusParameters['section'];
}
}

View file

@ -58,7 +58,7 @@ final readonly class ResourceDeleteSubscriber implements EventSubscriberInterfac
$eventRequest = $event->getRequest();
$requestAttributes = $eventRequest->attributes;
$originalRoute = $requestAttributes->get('_route');
$originalRoute = $requestAttributes->get('_route', '');
if (!$this->isMethodDelete($eventRequest) ||
!$this->isSyliusRoute($originalRoute) ||

View file

@ -16,24 +16,16 @@ namespace Sylius\Bundle\AdminBundle\Tests\MessageHandler\Admin;
use Sylius\Bundle\CoreBundle\Message\Admin\Account\SendResetPasswordEmail;
use Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account\SendResetPasswordEmailHandler;
use Sylius\Component\Core\Model\AdminUser;
use Sylius\Component\Core\Test\SwiftmailerAssertionTrait;
use Sylius\Component\Mailer\Sender\SenderInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Contracts\Translation\TranslatorInterface;
final class SendResetPasswordEmailHandlerTest extends KernelTestCase
{
use SwiftmailerAssertionTrait;
/** @test */
public function it_sends_password_reset_token_email(): void
{
if (self::isItSwiftmailerTestEnv()) {
$this->markTestSkipped('Test is relevant only for the environment without swiftmailer');
}
$container = self::bootKernel()->getContainer();
/** @var TranslatorInterface $translator */
@ -67,57 +59,4 @@ final class SendResetPasswordEmailHandlerTest extends KernelTestCase
$translator->trans('sylius.email.admin_password_reset.to_reset_your_password', [], null, 'en_US'),
);
}
/** @test */
public function it_sends_password_reset_token_email_with_swiftmailer(): void
{
if (!self::isItSwiftmailerTestEnv()) {
$this->markTestSkipped('Test is relevant only for the environment with swiftmailer');
}
$container = self::bootKernel()->getContainer();
self::setSpoolDirectory($container->getParameter('kernel.cache_dir') . '/spool');
/** @var Filesystem $filesystem */
$filesystem = $container->get('test.filesystem.public');
$filesystem->remove(self::getSpoolDirectory());
/** @var TranslatorInterface $translator */
$translator = $container->get('translator');
/** @var SenderInterface $emailSender */
$emailSender = $container->get('sylius.email_sender');
$adminUser = new AdminUser();
$adminUser->setEmail('sylius@example.com');
$adminUser->setPasswordResetToken('my_reset_token');
$adminUserRepository = $this->createMock(UserRepositoryInterface::class);
$adminUserRepository
->method('findOneByEmail')
->with('sylius@example.com')
->willReturn($adminUser)
;
$resetPasswordEmailHandler = new SendResetPasswordEmailHandler($adminUserRepository, $emailSender);
$resetPasswordEmailHandler(new SendResetPasswordEmail(
'sylius@example.com',
'en_US',
));
self::assertSpooledMessagesCountWithRecipient(1, 'sylius@example.com');
self::assertSpooledMessageWithContentHasRecipient(
$translator->trans('sylius.email.admin_password_reset.to_reset_your_password', [], null, 'en_US'),
'sylius@example.com',
);
}
private static function isItSwiftmailerTestEnv(): bool
{
$env = self::getContainer()->getParameter('kernel.environment');
return $env === 'test_with_swiftmailer';
}
}

View file

@ -81,7 +81,7 @@ final class RequestPasswordResetActionSpec extends ObjectBehavior
$flashBag->add('success', 'sylius.admin.request_reset_password.success')->shouldBeCalled();
$attributesBag->get('_sylius')->shouldBeCalled()->willReturn([
$attributesBag->get('_sylius', [])->shouldBeCalled()->willReturn([
'redirect' => 'my_custom_route',
]);
$request->attributes = $attributesBag->getWrappedObject();
@ -120,7 +120,7 @@ final class RequestPasswordResetActionSpec extends ObjectBehavior
$parameters = [
'my_parameter' => 'my_value',
];
$attributesBag->get('_sylius')->shouldBeCalled()->willReturn([
$attributesBag->get('_sylius', [])->shouldBeCalled()->willReturn([
'redirect' => [
'route' => $route,
'params' => $parameters,
@ -158,7 +158,7 @@ final class RequestPasswordResetActionSpec extends ObjectBehavior
$flashBag->add('success', 'sylius.admin.request_reset_password.success')->shouldBeCalled();
$attributesBag->get('_sylius')->shouldBeCalled()->willReturn(null);
$attributesBag->get('_sylius', [])->shouldBeCalled()->willReturn([]);
$request->attributes = $attributesBag->getWrappedObject();
$router->generate('sylius_admin_login')->shouldBeCalled()->willReturn('/login');

View file

@ -43,7 +43,7 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior
$event->isMainRequest()->willReturn(true);
$request->getRequestFormat()->willReturn('html');
$attributes->get('_route')->willReturn('sylius_admin_product_index');
$attributes->get('_route', '')->willReturn('sylius_admin_product_index');
$attributes->get('_sylius', [])->willReturn(['section' => 'admin']);
$attributes->get('_controller')->willReturn('Sylius\Bundle\AdminBundle\Controller\ProductController::indexAction');
$request->attributes = $attributes;
@ -82,7 +82,7 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior
$event->isMainRequest()->willReturn(true);
$request->getRequestFormat()->willReturn('json');
$attributes->get('_route')->willReturn('sylius_admin_product_index');
$attributes->get('_route', '')->willReturn('sylius_admin_product_index');
$attributes->get('_sylius', [])->willReturn(['section' => 'admin']);
$attributes->get('_controller')->willReturn('Sylius\Bundle\AdminBundle\Controller\ProductController::indexAction');
$request->attributes = $attributes;
@ -109,7 +109,7 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior
$event->isMainRequest()->willReturn(true);
$request->getRequestFormat()->willReturn('json');
$attributes->get('_route')->willReturn('sylius_admin_product_index');
$attributes->get('_route', '')->willReturn('sylius_admin_product_index');
$attributes->get('_sylius', [])->willReturn(['section' => 'shop']);
$attributes->get('_controller')->willReturn('Sylius\Bundle\AdminBundle\Controller\ProductController::indexAction');
$request->attributes = $attributes;
@ -136,7 +136,7 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior
$event->isMainRequest()->willReturn(true);
$request->getRequestFormat()->willReturn('json');
$attributes->get('_route')->willReturn('sylius_admin_product_index');
$attributes->get('_route', '')->willReturn('sylius_admin_product_index');
$attributes->get('_sylius', [])->willReturn(['section' => 'shop']);
$attributes->get('_controller')->willReturn(null);
$request->attributes = $attributes;
@ -153,6 +153,33 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior
$this->onKernelRequest($event);
}
function it_does_not_add_filter_to_filter_storage_if_route_is_missing(
RequestEvent $event,
Request $request,
ParameterBag $attributes,
ParameterBag $query,
FilterStorageInterface $filterStorage,
): void {
$event->isMainRequest()->willReturn(true);
$request->getRequestFormat()->willReturn('json');
$attributes->get('_route', '')->willReturn('');
$attributes->get('_sylius', [])->willReturn(['section' => 'shop']);
$attributes->get('_controller')->willReturn('Sylius\Bundle\AdminBundle\Controller\ProductController::indexAction');
$request->attributes = $attributes;
$query->all()->willReturn(['filter' => 'foo']);
$request->query = $query;
$filterStorage->all()->willReturn([]);
$event->getRequest()->willReturn($request);
$filterStorage->set(Argument::any())->shouldNotBeCalled();
$this->onKernelRequest($event);
}
function it_does_not_add_filter_to_filter_storage_if_it_is_not_a_index_resource_route(
RequestEvent $event,
Request $request,
@ -163,7 +190,7 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior
$event->isMainRequest()->willReturn(true);
$request->getRequestFormat()->willReturn('json');
$attributes->get('_route')->willReturn('sylius_admin_product_update');
$attributes->get('_route', '')->willReturn('sylius_admin_product_update');
$attributes->get('_sylius', [])->willReturn(['section' => 'shop']);
$attributes->get('_controller')->willReturn('Sylius\Bundle\AdminBundle\Controller\ProductController::indexAction');
$request->attributes = $attributes;

View file

@ -14,7 +14,6 @@ declare(strict_types=1);
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['test_with_swiftmailer' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],

View file

@ -1,4 +0,0 @@
services:
test.filesystem.public:
alias: filesystem
public: true

View file

@ -1,6 +0,0 @@
swiftmailer:
disable_delivery: true
logging: true
spool:
type: file
path: "%kernel.cache_dir%/spool"

View file

@ -1,2 +0,0 @@
sylius_mailer:
sender_adapter: sylius.email_sender.adapter.swiftmailer

View file

@ -33,9 +33,11 @@ final readonly class DeleteOrderItemAction
{
$orderItemId = $request->attributes->get('itemId');
$tokenValue = $request->attributes->get('tokenValue');
if (null === $orderItemId || null === $tokenValue) {
throw new OrderItemNotFoundException();
}
$orderItem = $this->orderItemRepository->findOneByIdAndCartTokenValue($orderItemId, $tokenValue);
if ($orderItem === null) {
throw new OrderItemNotFoundException();
}

View file

@ -31,11 +31,14 @@ final readonly class GetPaymentConfiguration
public function __invoke(Request $request): JsonResponse
{
$paymentId = $request->attributes->get('paymentId');
$tokenValue = $request->attributes->get('tokenValue');
if (null === $paymentId || null === $tokenValue) {
throw new PaymentNotFoundException();
}
/** @var PaymentInterface|null $payment */
$payment = $this->paymentRepository->findOneByOrderToken(
$request->attributes->get('paymentId'),
$request->attributes->get('tokenValue'),
);
$payment = $this->paymentRepository->findOneByOrderToken($paymentId, $tokenValue);
if ($payment === null) {
throw new PaymentNotFoundException();

View file

@ -30,8 +30,13 @@ final readonly class RemoveCatalogPromotionAction
public function __invoke(Request $request): Response
{
$catalogPromotionCode = $request->attributes->get('code');
if (null === $catalogPromotionCode) {
return new JsonResponse(status: Response::HTTP_NOT_FOUND);
}
try {
$this->catalogPromotionRemovalProcessor->removeCatalogPromotion($request->attributes->get('code'));
$this->catalogPromotionRemovalProcessor->removeCatalogPromotion($catalogPromotionCode);
return new Response(status: Response::HTTP_ACCEPTED);
} catch (CatalogPromotionNotFoundException) {

View file

@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use Sylius\Bundle\ApiBundle\Exception\ChannelCannotBeRemoved;
use Sylius\Component\Channel\Checker\ChannelDeletionCheckerInterface;
use Sylius\Component\Core\Model\ChannelInterface;
final class ChannelDataPersister implements ContextAwareDataPersisterInterface
{
public function __construct(
private ContextAwareDataPersisterInterface $decoratedDataPersister,
private ChannelDeletionCheckerInterface $channelDeletionChecker,
) {
}
/**
* @param array<string, mixed> $context
*/
public function supports($data, array $context = []): bool
{
return $data instanceof ChannelInterface;
}
/**
* @param array<string, mixed> $context
*/
public function persist($data, array $context = [])
{
return $this->decoratedDataPersister->persist($data, $context);
}
/**
* @param array<string, mixed> $context
*/
public function remove($data, array $context = []): void
{
if (!$this->channelDeletionChecker->isDeletable($data)) {
throw new ChannelCannotBeRemoved('The channel cannot be deleted. At least one enabled channel is required.');
}
$this->decoratedDataPersister->remove($data, $context);
}
}

View file

@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use ApiPlatform\Core\DataPersister\ResumableDataPersisterInterface;
use Sylius\Bundle\ApiBundle\Exception\TranslationInDefaultLocaleCannotBeRemoved;
use Sylius\Component\Resource\Model\TranslatableInterface;
use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface;
final class TranslatableDataPersister implements ContextAwareDataPersisterInterface, ResumableDataPersisterInterface
{
public function __construct(private TranslationLocaleProviderInterface $localeProvider)
{
}
/**
* @param array<array-key, mixed> $context
*/
public function supports($data, array $context = []): bool
{
return $data instanceof TranslatableInterface;
}
/**
* @param TranslatableInterface $data
* @param array<array-key, mixed> $context
*/
public function persist($data, array $context = []): object
{
$defaultLocaleCode = $this->localeProvider->getDefaultLocaleCode();
if (!$data->getTranslations()->containsKey($defaultLocaleCode)) {
throw new TranslationInDefaultLocaleCannotBeRemoved(
sprintf('Translation in the default locale "%s" cannot be removed.', $defaultLocaleCode),
);
}
return $data;
}
/**
* @param TranslatableInterface $data
* @param array<array-key, mixed> $context
*/
public function remove($data, array $context = []): mixed
{
return $data;
}
/** @param array<array-key, mixed> $context */
public function resumable(array $context = []): bool
{
return true;
}
}

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Exception;
/** @experimental */
final class ChannelCannotBeRemoved extends \RuntimeException
{
public function __construct(
string $message = 'Cannot remove, the channel is in use.',
int $code = 0,
\Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Exception;
/** @experimental */
final class TranslationInDefaultLocaleCannotBeRemoved extends \RuntimeException
{
public function __construct(
string $message = 'Translation in the default locale cannot be removed.',
int $code = 0,
\Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
}

View file

@ -51,6 +51,7 @@ api_platform:
# Sylius exception to status code mapping
SM\SMException: 422
Sylius\Bundle\ApiBundle\Exception\CannotRemoveCurrentlyLoggedInUser: 422
Sylius\Bundle\ApiBundle\Exception\ChannelCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\InvalidProductAttributeValueTypeException: 422
Sylius\Bundle\ApiBundle\Exception\OrderItemNotFoundException: 422
Sylius\Bundle\ApiBundle\Exception\OrderNoLongerEligibleForPromotion: 422
@ -60,6 +61,7 @@ api_platform:
Sylius\Bundle\ApiBundle\Exception\ProvinceCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\ShippingMethodCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\TaxonCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\TranslationInDefaultLocaleCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\ZoneCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\CannotRemoveMenuTaxonException: 409
Sylius\Bundle\LocaleBundle\Checker\Exception\LocaleIsUsedException: 422

View file

@ -80,6 +80,11 @@
</attribute>
<attribute name="validation_groups">sylius</attribute>
</itemOperation>
<itemOperation name="admin_delete">
<attribute name="method">DELETE</attribute>
<attribute name="path">/admin/channels/{code}</attribute>
</itemOperation>
</itemOperations>
<subresourceOperations>

View file

@ -146,7 +146,6 @@
<attribute name="en_US">
<attribute name="name">string</attribute>
<attribute name="slug">string</attribute>
<attribute name="locale">string</attribute>
</attribute>
</attribute>
</attribute>

View file

@ -87,7 +87,6 @@
<attribute name="en_US">
<attribute name="name">string</attribute>
<attribute name="description">string</attribute>
<attribute name="locale">string</attribute>
</attribute>
</attribute>
</attribute>

View file

@ -95,7 +95,6 @@
<attribute name="example">
<attribute name="en_US">
<attribute name="name">string</attribute>
<attribute name="locale">string</attribute>
</attribute>
</attribute>
</attribute>

View file

@ -99,7 +99,6 @@
<attribute name="example">
<attribute name="en_US">
<attribute name="name">string</attribute>
<attribute name="locale">string</attribute>
</attribute>
</attribute>
</attribute>

View file

@ -140,7 +140,6 @@
<attribute name="en_US">
<attribute name="name">string</attribute>
<attribute name="description">string</attribute>
<attribute name="locale">string</attribute>
</attribute>
</attribute>
</attribute>

View file

@ -85,7 +85,6 @@
<attribute name="en_US">
<attribute name="name">string</attribute>
<attribute name="slug">string</attribute>
<attribute name="locale">string</attribute>
</attribute>
</attribute>
</attribute>

View file

@ -24,23 +24,28 @@
<attribute name="name">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
<group>shop:channel:read</group>
</attribute>
<attribute name="enabled">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="description">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="hostname">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="color">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="baseCurrency">
<group>admin:channel:read</group>
@ -50,53 +55,65 @@
<attribute name="defaultLocale">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
<group>shop:channel:read</group>
</attribute>
<attribute name="defaultTaxZone">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="taxCalculationStrategy">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="currencies">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
<group>shop:channel:read</group>
</attribute>
<attribute name="locales">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
<group>shop:channel:read</group>
</attribute>
<attribute name="countries">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="themeName">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="contactEmail">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="contactPhoneNumber">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="skippingShippingStepAllowed">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="skippingPaymentStepAllowed">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="accountVerificationRequired">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="shippingAddressInCheckoutRequired">
<group>admin:channel:read</group>
@ -106,14 +123,17 @@
<attribute name="shopBillingData">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="menuTaxon">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="channelPriceHistoryConfig">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
</class>
</serializer>

View file

@ -21,26 +21,32 @@
</attribute>
<attribute name="company">
<group>admin:channel:create</group>
<group>admin:channel:update</group>
<group>admin:shop_billing_data:read</group>
</attribute>
<attribute name="taxId">
<group>admin:channel:create</group>
<group>admin:channel:update</group>
<group>admin:shop_billing_data:read</group>
</attribute>
<attribute name="countryCode">
<group>admin:channel:create</group>
<group>admin:channel:update</group>
<group>admin:shop_billing_data:read</group>
</attribute>
<attribute name="street">
<group>admin:channel:create</group>
<group>admin:channel:update</group>
<group>admin:shop_billing_data:read</group>
</attribute>
<attribute name="city">
<group>admin:channel:create</group>
<group>admin:channel:update</group>
<group>admin:shop_billing_data:read</group>
</attribute>
<attribute name="postcode">
<group>admin:channel:create</group>
<group>admin:channel:update</group>
<group>admin:shop_billing_data:read</group>
</attribute>
</class>

View file

@ -31,6 +31,12 @@
<tag name="api_platform.data_persister" />
</service>
<service id="Sylius\Bundle\ApiBundle\DataPersister\ChannelDataPersister">
<argument type="service" id="api_platform.doctrine.orm.data_persister" />
<argument type="service" id="sylius.checker.channel_deletion" />
<tag name="api_platform.data_persister" />
</service>
<service id="Sylius\Bundle\ApiBundle\DataPersister\MessengerDataPersister">
<argument type="service" id="api_platform.messenger.data_persister" />
<tag name="api_platform.data_persister" />
@ -74,6 +80,11 @@
<tag name="api_platform.data_persister" />
</service>
<service id="Sylius\Bundle\ApiBundle\DataPersister\TranslatableDataPersister">
<argument type="service" id="sylius.translation_locale_provider" />
<tag name="api_platform.data_persister" priority="128" />
</service>
<service id="Sylius\Bundle\ApiBundle\DataPersister\ZoneDataPersister">
<argument type="service" id="api_platform.doctrine.orm.data_persister" />
<argument type="service" id="Sylius\Component\Addressing\Checker\ZoneDeletionCheckerInterface" />

View file

@ -105,6 +105,11 @@
<!-- <tag name="serializer.normalizer" priority="64" />-->
<!-- </service>-->
<service id="Sylius\Bundle\ApiBundle\Serializer\TranslatableDenormalizer">
<argument type="service" id="sylius.translation_locale_provider" />
<tag name="serializer.normalizer" priority="64" />
</service>
<service id="date_time_normalizer" class="Symfony\Component\Serializer\Normalizer\DateTimeNormalizer">
<argument type="collection">
<argument key="datetime_format">Y-m-d H:i:s</argument>
@ -122,12 +127,17 @@
<!-- <service id="Sylius\Bundle\ApiBundle\Serializer\ChannelDenormalizer">-->
<!-- <argument type="service" id="sylius.factory.channel_price_history_config" />-->
<!-- <argument type="service" id="sylius.factory.shop_billing_data" />-->
<!-- <tag name="serializer.normalizer" priority="64" />-->
<!-- </service>-->
<!-- <service id="Sylius\Bundle\ApiBundle\Serializer\CustomerDenormalizer">-->
<!-- <argument type="service" id="clock" />-->
<!-- <tag name="serializer.normalizer" priority="64" />-->
<!-- </service>-->
<!-- <service id="Sylius\Bundle\ApiBundle\Serializer\TranslatableLocaleKeyDenormalizer">-->
<!-- <tag name="serializer.normalizer" priority="96" />-->
<!-- </service>-->
</services>
</container>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file is part of the Sylius package.
(c) Sylius Sp. z o.o.
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
-->
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/services/constraint-mapping-1.0.xsd">
<class name="Sylius\Component\Core\Model\Channel">
<property name="shopBillingData">
<constraint name="Valid">
<option name="groups">sylius</option>
</constraint>
</property>
</class>
</constraint-mapping>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file is part of the Sylius package.
(c) Sylius Sp. z o.o.
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
-->
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/services/constraint-mapping-1.0.xsd">
<class name="Sylius\Component\Core\Model\ShopBillingData">
<property name="countryCode">
<constraint name="Country">
<option name="groups">sylius</option>
</constraint>
<constraint name="NotBlank" >
<option name="allowNull">true</option>
<option name="groups">sylius</option>
</constraint>
</property>
</class>
</constraint-mapping>

View file

@ -15,6 +15,7 @@ namespace Sylius\Bundle\ApiBundle\Serializer;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\ChannelPriceHistoryConfigInterface;
use Sylius\Component\Core\Model\ShopBillingDataInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
@ -28,8 +29,14 @@ final class ChannelDenormalizer implements ContextAwareDenormalizerInterface, De
private const ALREADY_CALLED = 'sylius_channel_denormalizer_already_called';
public function __construct(private FactoryInterface $channelPriceHistoryConfigFactory)
{
/**
* @param FactoryInterface<ChannelPriceHistoryConfigInterface> $channelPriceHistoryConfigFactory
* @param FactoryInterface<ShopBillingDataInterface> $shopBillingDataFactory
*/
public function __construct(
private FactoryInterface $channelPriceHistoryConfigFactory,
private FactoryInterface $shopBillingDataFactory,
) {
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
@ -54,6 +61,12 @@ final class ChannelDenormalizer implements ContextAwareDenormalizerInterface, De
$channel->setChannelPriceHistoryConfig($channelPriceHistoryConfig);
}
if (null === $channel->getShopBillingData()) {
/** @var ShopBillingDataInterface $shopBillingData */
$shopBillingData = $this->shopBillingDataFactory->createNew();
$channel->setShopBillingData($shopBillingData);
}
return $channel;
}
}

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Serializer;
use Sylius\Component\Resource\Model\TranslatableInterface;
use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
/** @experimental */
final class TranslatableDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
private const ALREADY_CALLED = 'sylius_translatable_denormalizer_already_called_for_%s';
public function __construct(
private TranslationLocaleProviderInterface $localeProvider,
) {
}
public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
{
$context[self::getAlreadyCalledKey($type)] = true;
$defaultLocaleCode = $this->localeProvider->getDefaultLocaleCode();
if (!$this->hasDefaultTranslation($data['translations'] ?? [], $defaultLocaleCode)) {
$data['translations'][$defaultLocaleCode] = [
'locale' => $defaultLocaleCode,
];
}
return $this->denormalizer->denormalize($data, $type, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return
Request::METHOD_POST === ($context[ContextKeys::HTTP_REQUEST_METHOD_TYPE] ?? null) &&
!isset($context[self::getAlreadyCalledKey($type)]) &&
is_a($type, TranslatableInterface::class, true)
;
}
private static function getAlreadyCalledKey(string $class): string
{
return sprintf(self::ALREADY_CALLED, $class);
}
private function hasDefaultTranslation(array $translations, string $defaultLocale): bool
{
return
isset($translations[$defaultLocale]['locale']) &&
$defaultLocale === $translations[$defaultLocale]['locale']
;
}
}

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Serializer;
use Sylius\Component\Resource\Model\TranslatableInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
/** @experimental */
final class TranslatableLocaleKeyDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
private const ALREADY_CALLED = 'sylius_translatable_locale_key_denormalizer_already_called';
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return
!isset($context[self::ALREADY_CALLED]) &&
is_array($data) &&
is_a($type, TranslatableInterface::class, true)
;
}
/**
* @param array<string, array{ translations: array<mixed> }> $data
*/
public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
{
$context[self::ALREADY_CALLED] = true;
if (array_key_exists('translations', $data)) {
foreach ($data['translations'] as $key => &$translation) {
$translation['locale'] = $key;
}
}
return $this->denormalizer->denormalize($data, $type, $format, $context);
}
}

View file

@ -1 +0,0 @@
APP_ENV=test_with_swiftmailer

View file

@ -13,7 +13,6 @@ declare(strict_types=1);
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['test_with_swiftmailer' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
@ -54,9 +53,9 @@ return [
Sylius\Bundle\PayumBundle\SyliusPayumBundle::class => ['all' => true],
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
Sylius\Bundle\ApiBundle\SyliusApiBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true, 'test_with_swiftmailer' => true],
Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true, 'test_with_swiftmailer' => true],
Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true, 'test_with_swiftmailer' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true],
Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true],
Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true],
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
SyliusLabs\DoctrineMigrationsExtraBundle\SyliusLabsDoctrineMigrationsExtraBundle::class => ['all' => true],
SyliusLabs\Polyfill\Symfony\Security\Bundle\SyliusLabsPolyfillSymfonySecurityBundle::class => ['all' => true],

View file

@ -1,4 +0,0 @@
services:
test.filesystem.public:
alias: filesystem
public: true

View file

@ -1,6 +0,0 @@
swiftmailer:
disable_delivery: true
logging: true
spool:
type: file
path: "%kernel.cache_dir%/spool"

View file

@ -1,2 +0,0 @@
sylius_mailer:
sender_adapter: sylius.email_sender.adapter.swiftmailer

View file

@ -19,25 +19,18 @@ use Sylius\Bundle\ApiBundle\Command\Account\SendAccountRegistrationEmail;
use Sylius\Bundle\ApiBundle\CommandHandler\Account\SendAccountRegistrationEmailHandler;
use Sylius\Component\Channel\Repository\ChannelRepositoryInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Test\SwiftmailerAssertionTrait;
use Sylius\Component\User\Model\UserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Contracts\Translation\TranslatorInterface;
final class SendAccountRegistrationEmailHandlerTest extends KernelTestCase
{
use ProphecyTrait;
use SwiftmailerAssertionTrait;
/** @test */
public function it_sends_account_registration_email(): void
{
if ($this->isItSwiftmailerTestEnv()) {
$this->markTestSkipped('Test is relevant only for the environment without swiftmailer');
}
$container = self::getContainer();
/** @var TranslatorInterface $translator */
@ -82,71 +75,4 @@ final class SendAccountRegistrationEmailHandlerTest extends KernelTestCase
self::assertEmailAddressContains($email, 'To', 'user@example.com');
self::assertEmailHtmlBodyContains($email, $translator->trans('sylius.email.user_registration.start_shopping', [], null, 'en_US'));
}
/** @test */
public function it_sends_account_registration_email_with_swiftmailer(): void
{
if (!$this->isItSwiftmailerTestEnv()) {
$this->markTestSkipped('Test is relevant only for the environment with swiftmailer');
}
$container = self::getContainer();
self::setSpoolDirectory($container->getParameter('kernel.cache_dir') . '/spool');
/** @var Filesystem $filesystem */
$filesystem = $container->get('test.filesystem.public');
/** @var TranslatorInterface $translator */
$translator = $container->get('translator');
$filesystem->remove(self::getSpoolDirectory());
$emailSender = $container->get('sylius.email_sender');
/** @var ChannelRepositoryInterface|ObjectProphecy $channelRepository */
$channelRepository = $this->prophesize(ChannelRepositoryInterface::class);
/** @var UserRepositoryInterface|ObjectProphecy $userRepository */
$userRepository = $this->prophesize(UserRepositoryInterface::class);
/** @var ChannelInterface|ObjectProphecy $channel */
$channel = $this->prophesize(ChannelInterface::class);
/** @var UserInterface|ObjectProphecy $user */
$user = $this->prophesize(UserInterface::class);
$channel->isAccountVerificationRequired()->willReturn(false);
$channel->getHostname()->willReturn('example.com');
$user->getUsername()->willReturn('username');
$user->getEmailVerificationToken()->willReturn('token');
$channelRepository->findOneByCode('CHANNEL_CODE')->willReturn($channel->reveal());
$userRepository->findOneByEmail('user@example.com')->willReturn($user->reveal());
$sendAccountRegistrationEmailHandler = new SendAccountRegistrationEmailHandler(
$userRepository->reveal(),
$channelRepository->reveal(),
$emailSender,
);
$sendAccountRegistrationEmailHandler(
new SendAccountRegistrationEmail(
'user@example.com',
'en_US',
'CHANNEL_CODE',
),
);
self::assertSpooledMessagesCountWithRecipient(1, 'user@example.com');
self::assertSpooledMessageWithContentHasRecipient(
$translator->trans('sylius.email.user_registration.start_shopping', [], null, 'en_US'),
'user@example.com',
);
}
private function isItSwiftmailerTestEnv(): bool
{
$env = self::getContainer()->getParameter('kernel.environment');
return $env === 'test_with_swiftmailer';
}
}

View file

@ -33,10 +33,6 @@ final class SendAccountVerificationEmailHandlerTest extends KernelTestCase
/** @test */
public function it_sends_account_verification_token_email(): void
{
if ($this->isItSwiftmailerTestEnv()) {
$this->markTestSkipped('Test is relevant only for the environment without swiftmailer');
}
$container = self::getContainer();
/** @var TranslatorInterface $translator */
@ -81,11 +77,4 @@ final class SendAccountVerificationEmailHandlerTest extends KernelTestCase
$translator->trans('sylius.email.verification_token.message', [], null, 'en_US'),
);
}
private function isItSwiftmailerTestEnv(): bool
{
$env = self::getContainer()->getParameter('kernel.environment');
return $env === 'test_with_swiftmailer';
}
}

View file

@ -31,10 +31,6 @@ final class SendContactRequestHandlerTest extends KernelTestCase
/** @test */
public function it_sends_contact_request(): void
{
if ($this->isItSwiftmailerTestEnv()) {
$this->markTestSkipped('Test is relevant only for the environment without swiftmailer');
}
$container = self::getContainer();
/** @var TranslatorInterface $translator */
@ -68,11 +64,4 @@ final class SendContactRequestHandlerTest extends KernelTestCase
self::assertEmailAddressContains($email, 'To', 'shop@example.com');
self::assertEmailHtmlBodyContains($email, $translator->trans('sylius.email.contact_request.content', [], null, 'en_US'));
}
private function isItSwiftmailerTestEnv(): bool
{
$env = self::getContainer()->getParameter('kernel.environment');
return $env === 'test_with_swiftmailer';
}
}

View file

@ -32,10 +32,6 @@ final class SendOrderConfirmationEmailHandlerTest extends KernelTestCase
/** @test */
public function it_sends_order_confirmation_email(): void
{
if ($this->isItSwiftmailerTestEnv()) {
$this->markTestSkipped('Test is relevant only for the environment without swiftmailer');
}
$container = self::bootKernel()->getContainer();
$emailSender = $container->get('sylius.email_sender');
@ -70,11 +66,4 @@ final class SendOrderConfirmationEmailHandlerTest extends KernelTestCase
$this->assertEmailAddressContains($email, 'To', 'johnny.bravo@email.com');
$this->assertEmailHtmlBodyContains($email, '#000001');
}
private function isItSwiftmailerTestEnv(): bool
{
$env = $this->getContainer()->getParameter('kernel.environment');
return $env === 'test_with_swiftmailer';
}
}

View file

@ -33,10 +33,6 @@ final class SendResetPasswordEmailHandlerTest extends KernelTestCase
/** @test */
public function it_sends_password_reset_token_email(): void
{
if ($this->isItSwiftmailerTestEnv()) {
$this->markTestSkipped('Test is relevant only for the environment without swiftmailer');
}
$container = self::getContainer();
/** @var TranslatorInterface $translator */
@ -79,11 +75,4 @@ final class SendResetPasswordEmailHandlerTest extends KernelTestCase
$translator->trans('sylius.email.password_reset.to_reset_your_password_token', [], null, 'en_US'),
);
}
private function isItSwiftmailerTestEnv(): bool
{
$env = self::getContainer()->getParameter('kernel.environment');
return $env === 'test_with_swiftmailer';
}
}

View file

@ -29,10 +29,6 @@ final class SendResetPasswordEmailHandlerTest extends KernelTestCase
/** @test */
public function it_sends_password_reset_token_email(): void
{
if ($this->isItSwiftmailerTestEnv()) {
$this->markTestSkipped('Test is relevant only for the environment without swiftmailer');
}
$container = self::getContainer();
/** @var TranslatorInterface $translator */
@ -66,11 +62,4 @@ final class SendResetPasswordEmailHandlerTest extends KernelTestCase
$translator->trans('sylius.email.admin_password_reset.to_reset_your_password_token', [], null, 'en_US'),
);
}
private function isItSwiftmailerTestEnv(): bool
{
$env = self::getContainer()->getParameter('kernel.environment');
return $env === 'test_with_swiftmailer';
}
}

View file

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Exception\ChannelCannotBeRemoved;
use Sylius\Component\Channel\Checker\ChannelDeletionCheckerInterface;
use Sylius\Component\Core\Model\ChannelInterface;
final class ChannelDataPersisterSpec extends ObjectBehavior
{
function let(
ContextAwareDataPersisterInterface $decoratedDataPersister,
ChannelDeletionCheckerInterface $channelDeletionChecker,
): void {
$this->beConstructedWith($decoratedDataPersister, $channelDeletionChecker);
}
function it_supports_only_channel_entity(ChannelInterface $channel, \stdClass $object): void
{
$this->supports($channel)->shouldReturn(true);
$this->supports($object)->shouldReturn(false);
}
function it_persists_channel(
ContextAwareDataPersisterInterface $decoratedDataPersister,
ChannelInterface $channel,
): void {
$decoratedDataPersister->persist($channel, [])->willReturn($channel);
$this->persist($channel, [])->shouldReturn($channel);
}
function it_throws_an_exception_if_channel_is_not_deletable(
ChannelDeletionCheckerInterface $channelDeletionChecker,
ChannelInterface $channel,
): void {
$channelDeletionChecker->isDeletable($channel)->willReturn(false);
$this
->shouldThrow(ChannelCannotBeRemoved::class)
->during('remove', [$channel])
;
}
function it_removes_channel(
ContextAwareDataPersisterInterface $decoratedDataPersister,
ChannelDeletionCheckerInterface $channelDeletionChecker,
ChannelInterface $channel,
): void {
$channelDeletionChecker->isDeletable($channel)->willReturn(true);
$decoratedDataPersister->remove($channel, [])->willReturn(null);
$this->remove($channel, [])->shouldReturn(null);
}
}

View file

@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use ApiPlatform\Core\DataPersister\ResumableDataPersisterInterface;
use Doctrine\Common\Collections\ArrayCollection;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Exception\TranslationInDefaultLocaleCannotBeRemoved;
use Sylius\Component\Resource\Model\TranslatableInterface;
use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface;
final class TranslatableDataPersisterSpec extends ObjectBehavior
{
function let(TranslationLocaleProviderInterface $localeProvider): void
{
$this->beConstructedWith($localeProvider);
}
function it_is_a_context_aware_data_persister(): void
{
$this->shouldImplement(ContextAwareDataPersisterInterface::class);
}
function it_is_a_resumable_data_persister(): void
{
$this->shouldImplement(ResumableDataPersisterInterface::class);
}
function it_supports_only_translatable(TranslatableInterface $translatable): void
{
$this->supports(new \stdClass())->shouldReturn(false);
$this->supports($translatable)->shouldReturn(true);
}
function it_does_nothing_if_there_is_a_translation_in_default_locale(
TranslationLocaleProviderInterface $localeProvider,
TranslatableInterface $translatable,
TranslatableInterface $translation,
): void {
$localeProvider->getDefaultLocaleCode()->willReturn('en_US');
$translatable->getTranslations()->willReturn(new ArrayCollection(['en_US' => $translation]));
$this->persist($translatable)->shouldReturn($translatable);
}
function it_throws_an_exception_if_there_is_no_translation_in_default_locale(
TranslationLocaleProviderInterface $localeProvider,
TranslatableInterface $translatable,
TranslatableInterface $translation,
): void {
$localeProvider->getDefaultLocaleCode()->willReturn('en_US');
$translatable->getTranslations()->willReturn(new ArrayCollection(['de_DE' => $translation]));
$this->shouldThrow(TranslationInDefaultLocaleCannotBeRemoved::class)->during('persist', [$translatable]);
}
function it_does_nothing_during_removing_object(TranslatableInterface $translatable): void
{
$this->remove($translatable)->shouldReturn($translatable);
}
function it_is_resumable(): void
{
$this->resumable()->shouldReturn(true);
}
}

View file

@ -17,6 +17,7 @@ use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\ChannelPriceHistoryConfigInterface;
use Sylius\Component\Core\Model\ShopBillingDataInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
@ -24,9 +25,9 @@ final class ChannelDenormalizerSpec extends ObjectBehavior
{
private const ALREADY_CALLED = 'sylius_channel_denormalizer_already_called';
function let(FactoryInterface $configFactory): void
function let(FactoryInterface $configFactory, FactoryInterface $shopBillingDataFactory): void
{
$this->beConstructedWith($configFactory);
$this->beConstructedWith($configFactory, $shopBillingDataFactory);
}
function it_does_not_support_denormalization_when_the_denormalizer_has_already_been_called(): void
@ -54,15 +55,17 @@ final class ChannelDenormalizerSpec extends ObjectBehavior
$this->shouldThrow(\InvalidArgumentException::class)->during('denormalize', [[], 'string']);
}
function it_returns_channel_as_is_when_channel_price_history_config_has_already_been_set(
function it_returns_channel_as_is_when_shop_billing_data_and_channel_price_history_config_has_already_been_set(
DenormalizerInterface $denormalizer,
FactoryInterface $configFactory,
ShopBillingDataInterface $shopBillingData,
ChannelPriceHistoryConfigInterface $config,
ChannelInterface $channel,
): void {
$this->setDenormalizer($denormalizer);
$channel->getChannelPriceHistoryConfig()->willReturn($config);
$channel->getShopBillingData()->willReturn($shopBillingData);
$channel->setChannelPriceHistoryConfig(Argument::any())->shouldNotBeCalled();
$configFactory->createNew()->shouldNotBeCalled();
@ -75,12 +78,14 @@ final class ChannelDenormalizerSpec extends ObjectBehavior
function it_adds_a_new_channel_price_history_config_when_channel_has_none(
DenormalizerInterface $denormalizer,
FactoryInterface $configFactory,
ShopBillingDataInterface $shopBillingData,
ChannelPriceHistoryConfigInterface $config,
ChannelInterface $channel,
): void {
$this->setDenormalizer($denormalizer);
$channel->getChannelPriceHistoryConfig()->willReturn(null);
$channel->getShopBillingData()->willReturn($shopBillingData);
$configFactory->createNew()->willReturn($config);
$channel->setChannelPriceHistoryConfig($config)->shouldBeCalled();
@ -89,4 +94,24 @@ final class ChannelDenormalizerSpec extends ObjectBehavior
$this->denormalize([], ChannelInterface::class)->shouldReturn($channel);
}
function it_adds_a_new_shop_billing_data_when_channel_has_none(
DenormalizerInterface $denormalizer,
FactoryInterface $shopBillingDataFactory,
ShopBillingDataInterface $shopBillingData,
ChannelPriceHistoryConfigInterface $config,
ChannelInterface $channel,
): void {
$this->setDenormalizer($denormalizer);
$channel->getChannelPriceHistoryConfig()->willReturn($config);
$channel->getShopBillingData()->willReturn(null);
$shopBillingDataFactory->createNew()->willReturn($shopBillingData);
$channel->setShopBillingData($shopBillingData)->shouldBeCalled();
$denormalizer->denormalize([], ChannelInterface::class, null, [self::ALREADY_CALLED => true])->willReturn($channel);
$this->denormalize([], ChannelInterface::class)->shouldReturn($channel);
}
}

View file

@ -88,7 +88,7 @@ final class ProductAttributeValueDenormalizerSpec extends ObjectBehavior
['attribute' => '/attributes/material', 'value' => 'ceramic'],
ProductAttributeValueInterface::class,
null,
[self::ALREADY_CALLED => true]
[self::ALREADY_CALLED => true],
)
->shouldBeCalled()
;

Some files were not shown because too many files have changed in this diff Show more