From 2bb655e2ee11ccbba9e4d6136bac41eb7324e984 Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Mon, 13 Nov 2023 13:15:37 +0100 Subject: [PATCH] [API][Admin] Validate product variants on product images --- .../Resources/config/services/validators.xml | 4 + .../config/validation/ProductImage.xml | 4 + .../Resources/translations/validators.en.yml | 2 + .../ProductImageVariantsBelongToOwner.php | 31 +++++++ ...uctImageVariantsBelongToOwnerValidator.php | 44 ++++++++++ ...mageVariantsBelongToOwnerValidatorSpec.php | 80 +++++++++++++++++++ .../Component/Core/Model/ProductImage.php | 2 +- tests/Api/Admin/ProductImagesTest.php | 65 ++++++++++++++- .../ORM/product/product_image.yaml | 5 ++ ...t_image_with_invalid_variant_response.json | 13 +++ ...image_with_type_and_variant_response.json} | 0 ...t_image_with_invalid_variant_response.json | 13 +++ 12 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductImageVariantsBelongToOwner.php create mode 100644 src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductImageVariantsBelongToOwnerValidator.php create mode 100644 src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductImageVariantsBelongToOwnerValidatorSpec.php create mode 100644 tests/Api/Responses/Expected/admin/product_image/post_product_image_with_invalid_variant_response.json rename tests/Api/Responses/Expected/admin/product_image/{post_product_image_with_type_response.json => post_product_image_with_type_and_variant_response.json} (100%) create mode 100644 tests/Api/Responses/Expected/admin/product_image/put_product_image_with_invalid_variant_response.json diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/validators.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/validators.xml index e119efb8b9..6954783ca9 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/validators.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/validators.xml @@ -97,5 +97,9 @@ + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductImage.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductImage.xml index c3d95e9529..18be25c30d 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductImage.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductImage.xml @@ -13,6 +13,10 @@ + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.en.yml index 542535811b..a83658a543 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.en.yml @@ -98,6 +98,8 @@ sylius: file: max_size: The image is too big - {{ size }}{{ suffix }}. Maximum allowed size is {{ limit }}{{ suffix }}. upload_ini_size: The image is too big. Maximum allowed size is {{ limit }}{{ suffix }}. + product_variant: + not_belong_to_owner: 'The product variant with code "%productVariantCode%" does not belong to the product with "%ownerCode%", which is the owner of the image.' product_taxon: unique: Product taxons cannot be duplicated. product: diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductImageVariantsBelongToOwner.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductImageVariantsBelongToOwner.php new file mode 100644 index 0000000000..c61dbe8fee --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductImageVariantsBelongToOwner.php @@ -0,0 +1,31 @@ +getOwner(); + + foreach ($value->getProductVariants() as $productVariant) { + if (!$owner->hasVariant($productVariant)) { + $this->context->addViolation($constraint->message, [ + '%productVariantCode%' => $productVariant->getCode(), + '%ownerCode%' => $owner->getCode(), + ]); + } + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductImageVariantsBelongToOwnerValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductImageVariantsBelongToOwnerValidatorSpec.php new file mode 100644 index 0000000000..6cfb433b7b --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductImageVariantsBelongToOwnerValidatorSpec.php @@ -0,0 +1,80 @@ +initialize($executionContext); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldImplement(ConstraintValidatorInterface::class); + } + + function it_throws_an_exception_if_value_is_not_product_image(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new \stdClass(), new ProductImageVariantsBelongToOwner()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_product_image_variants_belong_to_owner( + Constraint $constraint, + ProductImageInterface $image, + ): void { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [$image, $constraint]) + ; + } + + function it_adds_a_violation_if_any_variant_does_not_belong_to_a_product_which_is_an_owner( + ExecutionContextInterface $executionContext, + ProductImageInterface $image, + ProductInterface $product, + ProductVariantInterface $variant, + ): void { + $image->getOwner()->willReturn($product); + $image->getProductVariants()->willReturn(new ArrayCollection([$variant->getWrappedObject()])); + + $product->getCode()->willReturn('MUG'); + $product->hasVariant($variant)->willReturn(false); + + $variant->getCode()->willReturn('GREEN_MUG'); + + $executionContext + ->addViolation( + 'sylius.product_image.product_variant.not_belong_to_owner', + ['%productVariantCode%' => 'GREEN_MUG', '%ownerCode%' => 'MUG'], + ) + ->shouldBeCalled() + ; + + $this->validate($image, new ProductImageVariantsBelongToOwner()); + } +} diff --git a/src/Sylius/Component/Core/Model/ProductImage.php b/src/Sylius/Component/Core/Model/ProductImage.php index e3c5537d61..8436f5b4b4 100644 --- a/src/Sylius/Component/Core/Model/ProductImage.php +++ b/src/Sylius/Component/Core/Model/ProductImage.php @@ -27,7 +27,7 @@ class ProductImage extends Image implements ProductImageInterface public function __construct() { - /** @var ArrayCollection $this->productVaraints */ + /** @var ArrayCollection $this->productVariants */ $this->productVariants = new ArrayCollection(); } diff --git a/tests/Api/Admin/ProductImagesTest.php b/tests/Api/Admin/ProductImagesTest.php index 8fc0c28f46..55e2205895 100644 --- a/tests/Api/Admin/ProductImagesTest.php +++ b/tests/Api/Admin/ProductImagesTest.php @@ -146,11 +146,44 @@ final class ProductImagesTest extends JsonApiTestCase $response = $this->client->getResponse(); $this->assertResponse( $response, - 'admin/product_image/post_product_image_with_type_response', + 'admin/product_image/post_product_image_with_type_and_variant_response', Response::HTTP_CREATED, ); } + /** @test */ + public function it_creates_a_product_image_with_invalid_variant(): void + { + $fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'product/product_image.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ProductInterface $product */ + $product = $fixtures['product_mug']; + + /** @var ProductVariantInterface $productVariant */ + $productVariant = $fixtures['product_variant_cap_yellow']; + + $this->client->request( + method: 'POST', + uri: sprintf('/api/v2/admin/products/%s/images', $product->getCode()), + parameters: [ + 'type' => 'banner', + 'productVariants' => [ + sprintf('/api/v2/admin/product-variants/%s', $productVariant->getCode()), + ], + ], + files: ['file' => $this->getUploadedFile('fixtures/mugs.jpg', 'mugs.jpg')], + server: $header, + ); + + $response = $this->client->getResponse(); + $this->assertResponse( + $response, + 'admin/product_image/post_product_image_with_invalid_variant_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + /** @test */ public function it_updates_only_the_type_and_variants_of_the_existing_product_image(): void { @@ -191,6 +224,36 @@ final class ProductImagesTest extends JsonApiTestCase ); } + /** @test */ + public function it_updates_the_existing_product_image_with_invalid_variant(): void + { + $fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'product/product_image.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ProductImageInterface $productImage */ + $productImage = $fixtures['product_mug_thumbnail']; + + /** @var ProductVariantInterface $productVariant */ + $productVariant = $fixtures['product_variant_cap_yellow']; + + $this->client->request( + method: 'PUT', + uri: sprintf('/api/v2/admin/product-images/%s', $productImage->getId()), + server: $header, + content: json_encode([ + 'productVariants' => [ + sprintf('/api/v2/admin/product-variants/%s', $productVariant->getCode()), + ], + ], JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_image/put_product_image_with_invalid_variant_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + /** @test */ public function it_deletes_a_product_image(): void { diff --git a/tests/Api/DataFixtures/ORM/product/product_image.yaml b/tests/Api/DataFixtures/ORM/product/product_image.yaml index 4a69f90217..df500b7474 100644 --- a/tests/Api/DataFixtures/ORM/product/product_image.yaml +++ b/tests/Api/DataFixtures/ORM/product/product_image.yaml @@ -21,6 +21,11 @@ Sylius\Component\Core\Model\ProductVariant: product: '@product_mug' fallbackLocale: 'en_US' currentLocale: 'en' + product_variant_cap_yellow: + code: 'CAP_YELLOW' + product: '@product_cap' + fallbackLocale: 'en_US' + currentLocale: 'en' Sylius\Component\Core\Model\ProductImage: product_mug_thumbnail: diff --git a/tests/Api/Responses/Expected/admin/product_image/post_product_image_with_invalid_variant_response.json b/tests/Api/Responses/Expected/admin/product_image/post_product_image_with_invalid_variant_response.json new file mode 100644 index 0000000000..8bc683c81c --- /dev/null +++ b/tests/Api/Responses/Expected/admin/product_image/post_product_image_with_invalid_variant_response.json @@ -0,0 +1,13 @@ +{ + "@context": "\/api\/v2\/contexts\/ConstraintViolationList", + "@type": "ConstraintViolationList", + "hydra:title": "An error occurred", + "hydra:description": "The product variant with code \"CAP_YELLOW\" does not belong to the product with \"MUG\", which is the owner of the image.", + "violations": [ + { + "propertyPath": "", + "message": "The product variant with code \"CAP_YELLOW\" does not belong to the product with \"MUG\", which is the owner of the image.", + "code": null + } + ] +} diff --git a/tests/Api/Responses/Expected/admin/product_image/post_product_image_with_type_response.json b/tests/Api/Responses/Expected/admin/product_image/post_product_image_with_type_and_variant_response.json similarity index 100% rename from tests/Api/Responses/Expected/admin/product_image/post_product_image_with_type_response.json rename to tests/Api/Responses/Expected/admin/product_image/post_product_image_with_type_and_variant_response.json diff --git a/tests/Api/Responses/Expected/admin/product_image/put_product_image_with_invalid_variant_response.json b/tests/Api/Responses/Expected/admin/product_image/put_product_image_with_invalid_variant_response.json new file mode 100644 index 0000000000..8bc683c81c --- /dev/null +++ b/tests/Api/Responses/Expected/admin/product_image/put_product_image_with_invalid_variant_response.json @@ -0,0 +1,13 @@ +{ + "@context": "\/api\/v2\/contexts\/ConstraintViolationList", + "@type": "ConstraintViolationList", + "hydra:title": "An error occurred", + "hydra:description": "The product variant with code \"CAP_YELLOW\" does not belong to the product with \"MUG\", which is the owner of the image.", + "violations": [ + { + "propertyPath": "", + "message": "The product variant with code \"CAP_YELLOW\" does not belong to the product with \"MUG\", which is the owner of the image.", + "code": null + } + ] +}