Skip to content

Commit

Permalink
bug #530 Addding assertions for product in current channel (mamazu)
Browse files Browse the repository at this point in the history
This PR was merged into the 1.0-dev branch.

Discussion
----------

Fixes #59 

## What does it do?
Currently you can add any product to any cart. After this pull request the product needs to be available in the cart's channel. So that if a product is in channel A and the cart is picked up in channel B then the product-add endpoint will now throw an error (500).

## Why a 500 and not a 400 with validation errors?
The problem is that the Validation would need to take two properties of the request into consideration: the current cart token, the product code. Which means that it has to be a "Class Constraint" and needs to access properties of the request object. This is currently not possible as the current request objects don't have getter and the properties are private. So there is no way of validating them. Therefore this pull request only adds an assertion that it is true. (validation may follow if it will be possible to do so in the future)

PS: @JakobTolkemit you might be interested.

Commits
-------

8b85fbb Addding validation for product in current channel
ba20b63 Adding getter and using a class constraint for validation
5f1c839 Removing the 500 from add to product
  • Loading branch information
lchrusciel authored Sep 22, 2019
2 parents b6462b5 + 5f1c839 commit fc87069
Show file tree
Hide file tree
Showing 27 changed files with 554 additions and 29 deletions.
49 changes: 49 additions & 0 deletions spec/Checker/ProductInCartChannelCheckerSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace spec\Sylius\ShopApiPlugin\Checker;

use Doctrine\Common\Collections\ArrayCollection;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\ShopApiPlugin\Checker\ProductInCartChannelCheckerInterface;

final class ProductInCartChannelCheckerSpec extends ObjectBehavior
{
function it_implements_product_in_cart_channel_checker_interface(): void
{
$this->shouldImplement(ProductInCartChannelCheckerInterface::class);
}

function it_returns_true_if_the_channels_match(
ProductInterface $product,
OrderInterface $order,
ChannelInterface $channel
): void {
$product->getChannels()->willReturn(new ArrayCollection([$channel->getWrappedObject()]));

$order->getChannel()->willReturn($channel);

$this->isProductInCartChannel($product, $order)->shouldReturn(true);
}

function it_returns_false_if_the_channels_do_not_match(
ProductInterface $product,
OrderInterface $order,
ChannelInterface $orderChannel,
ChannelInterface $productChannel1,
ChannelInterface $productChannel2
): void {
$product->getChannels()->willReturn(new ArrayCollection([
$productChannel1->getWrappedObject(),
$productChannel2->getWrappedObject(),
]));

$order->getChannel()->willReturn($orderChannel);

$this->isProductInCartChannel($product, $order)->shouldReturn(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Core\Repository\ProductRepositoryInterface;
use Sylius\Component\Product\Model\ProductOptionValueInterface;
use Sylius\ShopApiPlugin\Checker\ProductInCartChannelCheckerInterface;
use Sylius\ShopApiPlugin\Command\Cart\PutOptionBasedConfigurableItemToCart;
use Sylius\ShopApiPlugin\Modifier\OrderModifierInterface;

Expand All @@ -21,15 +22,17 @@ final class PutOptionBasedConfigurableItemToCartHandlerSpec extends ObjectBehavi
function let(
OrderRepositoryInterface $orderRepository,
ProductRepositoryInterface $productRepository,
OrderModifierInterface $orderModifier
OrderModifierInterface $orderModifier,
ProductInCartChannelCheckerInterface $channelChecker
): void {
$this->beConstructedWith($orderRepository, $productRepository, $orderModifier);
$this->beConstructedWith($orderRepository, $productRepository, $orderModifier, $channelChecker);
}

function it_handles_putting_new_item_to_cart(
OrderInterface $cart,
OrderRepositoryInterface $orderRepository,
OrderModifierInterface $orderModifier,
ProductInCartChannelCheckerInterface $channelChecker,
ProductInterface $tShirt,
ProductOptionValueInterface $blueOptionValue,
ProductOptionValueInterface $redOptionValue,
Expand All @@ -47,6 +50,7 @@ function it_handles_putting_new_item_to_cart(
$blueTShirt->getOptionValues()->willReturn(new ArrayCollection([$blueOptionValue->getWrappedObject()]));
$blueOptionValue->getCode()->willReturn('BLUE_OPTION_VALUE_CODE');
$blueOptionValue->getOptionCode()->willReturn('COLOR_OPTION_CODE');
$channelChecker->isProductInCartChannel($tShirt, $cart)->willReturn(true);

$redTShirt->getOptionValues()->willReturn(new ArrayCollection([$redOptionValue->getWrappedObject()]));
$redOptionValue->getCode()->willReturn('RED_OPTION_VALUE_CODE');
Expand Down Expand Up @@ -85,6 +89,7 @@ function it_throws_an_exception_if_product_variant_cannot_be_resolved(
OrderInterface $cart,
CartItemFactoryInterface $cartItemFactory,
OrderRepositoryInterface $orderRepository,
ProductInCartChannelCheckerInterface $channelChecker,
ProductInterface $tShirt,
ProductVariantInterface $blueTShirt,
ProductVariantInterface $redTShirt,
Expand All @@ -102,6 +107,7 @@ function it_throws_an_exception_if_product_variant_cannot_be_resolved(
$blueTShirt->getOptionValues()->willReturn(new ArrayCollection([$blueOptionValue->getWrappedObject()]));
$blueOptionValue->getCode()->willReturn('BLUE_OPTION_VALUE_CODE');
$blueOptionValue->getOptionCode()->willReturn('COLOR_OPTION_CODE');
$channelChecker->isProductInCartChannel($tShirt, $cart);

$redTShirt->getOptionValues()->willReturn(new ArrayCollection([$redOptionValue->getWrappedObject()]));
$redOptionValue->getCode()->willReturn('GREEN_OPTION_VALUE_CODE');
Expand All @@ -114,4 +120,22 @@ function it_throws_an_exception_if_product_variant_cannot_be_resolved(
new PutOptionBasedConfigurableItemToCart('ORDERTOKEN', 'T_SHIRT_CODE', ['COLOR_OPTION_CODE' => 'RED_OPTION_VALUE_CODE'], 5),
]);
}

function it_throws_an_exception_if_product_is_not_in_same_channel_as_cart(
OrderInterface $cart,
OrderRepositoryInterface $orderRepository,
ProductInCartChannelCheckerInterface $channelChecker,
ProductInterface $tShirt,
ProductRepositoryInterface $productRepository
): void {
$productRepository->findOneByCode('T_SHIRT_CODE')->willReturn($tShirt);

$orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($cart);

$channelChecker->isProductInCartChannel($tShirt, $cart)->willReturn(false);

$this->shouldThrow(\InvalidArgumentException::class)->during('__invoke', [
new PutOptionBasedConfigurableItemToCart('ORDERTOKEN', 'T_SHIRT_CODE', ['COLOR_OPTION_CODE' => 'RED_OPTION_VALUE_CODE'], 5),
]);
}
}
37 changes: 34 additions & 3 deletions spec/Handler/Cart/PutSimpleItemToCartHandlerSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Sylius\Component\Core\Model\ProductVariantInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Core\Repository\ProductRepositoryInterface;
use Sylius\ShopApiPlugin\Checker\ProductInCartChannelCheckerInterface;
use Sylius\ShopApiPlugin\Command\Cart\PutSimpleItemToCart;
use Sylius\ShopApiPlugin\Modifier\OrderModifierInterface;

Expand All @@ -19,9 +20,10 @@ final class PutSimpleItemToCartHandlerSpec extends ObjectBehavior
function let(
OrderRepositoryInterface $cartRepository,
ProductRepositoryInterface $productRepository,
OrderModifierInterface $orderModifier
OrderModifierInterface $orderModifier,
ProductInCartChannelCheckerInterface $channelChecker
): void {
$this->beConstructedWith($cartRepository, $productRepository, $orderModifier);
$this->beConstructedWith($cartRepository, $productRepository, $orderModifier, $channelChecker);
}

function it_handles_putting_new_item_to_cart(
Expand All @@ -30,14 +32,17 @@ function it_handles_putting_new_item_to_cart(
ProductInterface $product,
ProductRepositoryInterface $productRepository,
ProductVariantInterface $productVariant,
OrderModifierInterface $orderModifier
OrderModifierInterface $orderModifier,
ProductInCartChannelCheckerInterface $channelChecker
): void {
$productRepository->findOneBy(['code' => 'T_SHIRT_CODE'])->willReturn($product);
$product->getVariants()->willReturn(new ArrayCollection([$productVariant->getWrappedObject()]));
$product->isSimple()->willReturn(true);

$cartRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($cart);

$channelChecker->isProductInCartChannel($product, $cart)->willReturn(true);

$orderModifier->modify($cart, $productVariant, 5)->shouldBeCalled();

$this(new PutSimpleItemToCart('ORDERTOKEN', 'T_SHIRT_CODE', 5));
Expand Down Expand Up @@ -68,17 +73,43 @@ function it_throws_an_exception_if_product_has_not_been_found(
function it_throws_an_exception_if_product_is_configurable(
OrderInterface $cart,
OrderRepositoryInterface $cartRepository,
ProductInCartChannelCheckerInterface $channelChecker,
ProductInterface $product,
ProductRepositoryInterface $productRepository,
ProductVariantInterface $productVariant
): void {
$cartRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($cart);
$productRepository->findOneBy(['code' => 'T_SHIRT_CODE'])->willReturn($product);
$product->getVariants()->willReturn(new ArrayCollection([$productVariant->getWrappedObject()]));

$channelChecker->isProductInCartChannel($product, $cart)->willReturn(true);

$product->isSimple()->willReturn(false);

$this->shouldThrow(\InvalidArgumentException::class)->during('__invoke', [
new PutSimpleItemToCart('ORDERTOKEN', 'T_SHIRT_CODE', 5),
]);
}

function it_throws_an_exception_if_product_is_not_in_same_channel_as_cart(
OrderInterface $cart,
OrderRepositoryInterface $cartRepository,
ProductInCartChannelCheckerInterface $channelChecker,
ProductInterface $product,
ProductRepositoryInterface $productRepository,
ProductVariantInterface $productVariant
): void {
$cartRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($cart);
$productRepository->findOneBy(['code' => 'T_SHIRT_CODE'])->willReturn($product);
$product->getVariants()->willReturn(new ArrayCollection([$productVariant->getWrappedObject()]));
$product->isSimple()->willReturn(false);

$channelChecker->isProductInCartChannel($product, $cart)->willReturn(false);

$this->shouldThrow(\InvalidArgumentException::class)->during(
'__invoke', [
new PutSimpleItemToCart('ORDERTOKEN', 'T_SHIRT_CODE', 5),
]
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Model\ProductVariantInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Core\Repository\ProductVariantRepositoryInterface;
use Sylius\ShopApiPlugin\Checker\ProductInCartChannelCheckerInterface;
use Sylius\ShopApiPlugin\Command\Cart\PutVariantBasedConfigurableItemToCart;
use Sylius\ShopApiPlugin\Modifier\OrderModifierInterface;

Expand All @@ -17,19 +19,25 @@ final class PutVariantBasedConfigurableItemToCartHandlerSpec extends ObjectBehav
function let(
OrderRepositoryInterface $cartRepository,
ProductVariantRepositoryInterface $productVariantRepository,
OrderModifierInterface $orderModifier
OrderModifierInterface $orderModifier,
ProductInCartChannelCheckerInterface $channelChecker
): void {
$this->beConstructedWith($cartRepository, $productVariantRepository, $orderModifier);
$this->beConstructedWith($cartRepository, $productVariantRepository, $orderModifier, $channelChecker);
}

function it_handles_putting_new_item_to_cart(
OrderInterface $cart,
OrderModifierInterface $orderModifier,
OrderRepositoryInterface $cartRepository,
ProductInCartChannelCheckerInterface $channelChecker,
ProductVariantInterface $productVariant,
ProductInterface $product,
ProductVariantRepositoryInterface $productVariantRepository
): void {
$productVariantRepository->findOneByCodeAndProductCode('RED_SMALL_T_SHIRT_CODE', 'T_SHIRT_CODE')->willReturn($productVariant);
$productVariant->getProduct()->willReturn($product);

$channelChecker->isProductInCartChannel($product, $cart)->willReturn(true);

$cartRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($cart);

Expand Down Expand Up @@ -59,4 +67,24 @@ function it_throws_an_exception_if_product_has_not_been_found(
new PutVariantBasedConfigurableItemToCart('ORDERTOKEN', 'T_SHIRT_CODE', 'RED_SMALL_T_SHIRT_CODE', 5),
]);
}

function it_throws_an_exception_if_product_has_different_channel_than_cart(
OrderInterface $cart,
OrderRepositoryInterface $cartRepository,
ProductInCartChannelCheckerInterface $channelChecker,
ProductVariantRepositoryInterface $productVariantRepository,
ProductVariantInterface $productVariant,
ProductInterface $product
): void {
$productVariantRepository->findOneByCodeAndProductCode('RED_SMALL_T_SHIRT_CODE', 'T_SHIRT_CODE')->willReturn($productVariant);
$productVariant->getProduct()->willReturn($product);

$cartRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($cart);

$channelChecker->isProductInCartChannel($product, $cart)->willReturn(false);

$this->shouldThrow(\InvalidArgumentException::class)->during('__invoke', [
new PutVariantBasedConfigurableItemToCart('ORDERTOKEN', 'T_SHIRT_CODE', 'RED_SMALL_T_SHIRT_CODE', 5),
]);
}
}
101 changes: 101 additions & 0 deletions spec/Validator/Product/ProductInCartChannelValidatorSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace spec\Sylius\ShopApiPlugin\Validator\Product;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Core\Repository\ProductRepositoryInterface;
use Sylius\ShopApiPlugin\Checker\ProductInCartChannelCheckerInterface;
use Sylius\ShopApiPlugin\Request\Cart\PutOptionBasedConfigurableItemToCartRequest;
use Sylius\ShopApiPlugin\Validator\Constraints\ProductInCartChannel;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;

final class ProductInCartChannelValidatorSpec extends ObjectBehavior
{
function let(
ExecutionContextInterface $executionContext,
ProductInCartChannelCheckerInterface $productInCartChannelChecker,
ProductRepositoryInterface $productRepository,
OrderRepositoryInterface $orderRepository
): void {
$this->beConstructedWith($productInCartChannelChecker, $productRepository, $orderRepository);

$this->initialize($executionContext);
}

function it_is_constraint_validator(): void
{
$this->shouldHaveType(ConstraintValidator::class);
}

function it_skips_validation_if_the_product_is_null(
ProductInCartChannelCheckerInterface $productInCartChannelChecker,
OrderRepositoryInterface $orderRepository,
ProductRepositoryInterface $productRepository,
PutOptionBasedConfigurableItemToCartRequest $request
): void {
$request->getProductCode()->willReturn('TEST');
$request->getToken()->willReturn('ORDER_TOKEN');

$productRepository->findOneByCode('TEST')->willReturn(null);
$orderRepository->findOneBy(['tokenValue' => 'ORDER_TOKEN'])->willReturn(null);

$productInCartChannelChecker->isProductInCartChannel(Argument::any())->shouldNotBeCalled();

$this->validate($request, new ProductInCartChannel());
}

function it_does_not_add_validation_if_product_and_cart_share_a_channel(
ProductInCartChannelCheckerInterface $productInCartChannelChecker,
ProductInterface $product,
OrderRepositoryInterface $orderRepository,
OrderInterface $order,
ProductRepositoryInterface $productRepository,
PutOptionBasedConfigurableItemToCartRequest $request,
ExecutionContextInterface $executionContext
): void {
$request->getProductCode()->willReturn('TEST');
$request->getToken()->willReturn('ORDER_TOKEN');

$productRepository->findOneByCode('TEST')->willReturn($product);
$orderRepository->findOneBy(['tokenValue' => 'ORDER_TOKEN'])->willReturn($order);

$productInCartChannelChecker->isProductInCartChannel($product, $order)->willReturn(true);

$executionContext->buildViolation(Argument::any())->shouldNotBeCalled();

$this->validate($request, new ProductInCartChannel());
}

function it_adds_a_violation_if_the_product_does_not_have_the_same_channel_as_cart(
ProductInCartChannelCheckerInterface $productInCartChannelChecker,
ProductInterface $product,
OrderRepositoryInterface $orderRepository,
OrderInterface $order,
ProductRepositoryInterface $productRepository,
PutOptionBasedConfigurableItemToCartRequest $request,
ExecutionContextInterface $executionContext,
ConstraintViolationBuilderInterface $constraintViolationBuilder
): void {
$request->getProductCode()->willReturn('TEST');
$request->getToken()->willReturn('ORDER_TOKEN');

$productRepository->findOneByCode('TEST')->willReturn($product);
$orderRepository->findOneBy(['tokenValue' => 'ORDER_TOKEN'])->willReturn($order);

$productInCartChannelChecker->isProductInCartChannel($product, $order)->willReturn(false);

$executionContext->buildViolation('sylius.shop_api.product.not_in_cart_channel')->willReturn($constraintViolationBuilder);
$constraintViolationBuilder->atPath('productCode')->willReturn($constraintViolationBuilder);
$constraintViolationBuilder->addViolation()->shouldBeCalled();

$this->validate($request, new ProductInCartChannel());
}
}
16 changes: 16 additions & 0 deletions src/Checker/ProductInCartChannelChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Sylius\ShopApiPlugin\Checker;

use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\ProductInterface;

final class ProductInCartChannelChecker implements ProductInCartChannelCheckerInterface
{
public function isProductInCartChannel(ProductInterface $product, OrderInterface $cart): bool
{
return in_array($cart->getChannel(), $product->getChannels()->toArray(), true);
}
}
Loading

0 comments on commit fc87069

Please sign in to comment.