Commit ddbf76e7 authored by Jonathan Sacksick's avatar Jonathan Sacksick Committed by Jonathan Sacksick
Browse files

Issue #3088598 by mglaman, jsacksick: Add an availability checker which checks...

Issue #3088598 by mglaman, jsacksick: Add an availability checker which checks if the purchasable entity is accessible or not.
parent dcbb0f50
Loading
Loading
Loading
Loading
+10 −7
Original line number Diff line number Diff line
@@ -83,7 +83,7 @@ class CheckoutAccessTest extends CartKernelTestBase {
      'type' => 'default',
      'title' => $this->randomMachineName(),
      'stores' => [$this->store],
      'variations' => [$this->variation],
      'variations' => [$variation],
    ]);
    $product->save();
    $this->variation = $this->reloadEntity($variation);
@@ -95,8 +95,8 @@ class CheckoutAccessTest extends CartKernelTestBase {
   * Tests that users need the `access checkout` permission.
   */
  public function testAccessCheckoutPermission() {
    $user_with_access = $this->createUser([], ['access checkout']);
    $user_without_access = $this->createUser([], []);
    $user_with_access = $this->createUser([], ['access checkout', 'view commerce_product']);
    $user_without_access = $this->createUser([], ['view commerce_product']);

    $order = $this->createOrder($user_with_access);
    $request = $this->createRequest($order);
@@ -111,8 +111,8 @@ class CheckoutAccessTest extends CartKernelTestBase {
   * Tests that only the order's owner can view its checkout.
   */
  public function testOwnerCheckoutAccess() {
    $user1 = $this->createUser([], ['access checkout']);
    $user2 = $this->createUser([], ['access checkout']);
    $user1 = $this->createUser([], ['access checkout', 'view commerce_product']);
    $user2 = $this->createUser([], ['access checkout', 'view commerce_product']);
    /** @var \Drupal\commerce_order\Entity\Order $order */
    $order = $this->createOrder($user1);
    $request = $this->createRequest($order);
@@ -124,7 +124,7 @@ class CheckoutAccessTest extends CartKernelTestBase {
   * Tests that canceled orders cannot enter checkout.
   */
  public function testCanceledOrderCheckout() {
    $user1 = $this->createUser([], ['access checkout']);
    $user1 = $this->createUser([], ['access checkout', 'view commerce_product']);
    $order = $this->createOrder($user1);
    $order->getState()->applyTransitionById('cancel');
    $request = $this->createRequest($order);
@@ -135,7 +135,7 @@ class CheckoutAccessTest extends CartKernelTestBase {
   * Tests that an order must have items to enter checkout.
   */
  public function testOrderMustHaveItems() {
    $user1 = $this->createUser([], ['access checkout']);
    $user1 = $this->createUser([], ['access checkout', 'view commerce_product']);
    $order = $this->createOrder($user1);
    $order->setItems([]);
    $request = $this->createRequest($order);
@@ -196,6 +196,9 @@ class CheckoutAccessTest extends CartKernelTestBase {
    $order_item->save();
    $order->addItem($order_item);
    $order->save();
    $order = $this->reloadEntity($order);
    assert($order instanceof OrderInterface);
    $this->assertCount(1, $order->getItems());

    return $order;
  }
+5 −0
Original line number Diff line number Diff line
@@ -102,3 +102,8 @@ services:
  commerce_order.price_calculator:
    class: Drupal\commerce_order\PriceCalculator
    arguments: ['@commerce_order.adjustment_transformer', '@commerce_order.chain_order_type_resolver', '@commerce_price.chain_price_resolver', '@entity_type.manager', '@request_stack']

  commerce_order.entity_accessible_availability_checker:
    class: Drupal\commerce_order\EntityAccessibleAvailabilityChecker
    tags:
      - { name: commerce_order.availability_checker }
+38 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\commerce_order;

use Drupal\commerce\Context;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\Core\Entity\EntityPublishedInterface;

/**
 * Determines availability based on the purchased entity access.
 */
final class EntityAccessibleAvailabilityChecker implements AvailabilityCheckerInterface {

  /**
   * {@inheritdoc}
   */
  public function applies(OrderItemInterface $order_item) {
    return $order_item->hasPurchasedEntity() && $order_item->getPurchasedEntity();
  }

  /**
   * {@inheritdoc}
   */
  public function check(OrderItemInterface $order_item, Context $context) {
    $purchasable_entity = $order_item->getPurchasedEntity();
    // If the purchasable entity is publishable, immediately return false if
    // it is unpublished and skip entity access checks for performance.
    if ($purchasable_entity instanceof EntityPublishedInterface && $purchasable_entity->isPublished() === FALSE) {
      return AvailabilityResult::unavailable();
    }
    if (!$purchasable_entity->access('view', $context->getCustomer())) {
      return AvailabilityResult::unavailable();
    }

    return AvailabilityResult::neutral();
  }

}
+19 −8
Original line number Diff line number Diff line
@@ -9,6 +9,8 @@ use Drupal\commerce_order\Entity\OrderItem;
use Drupal\commerce_price\Price;
use Drupal\commerce_product\Entity\Product;
use Drupal\commerce_product\Entity\ProductVariation;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;

/**
@@ -17,7 +19,7 @@ use Symfony\Component\Validator\ConstraintViolationInterface;
 * @group commerce_order
 * @coversDefaultClass \Drupal\commerce_order\Plugin\Validation\Constraint\PurchasedEntityAvailableConstraintValidator
 */
final class PurchasedEntityConstraintValidatorTest extends OrderKernelTestBase {
final class PurchasedEntityConstraintValidatorTest extends OrderKernelTestBase implements ServiceModifierInterface {

  /**
   * Modules to enable.
@@ -28,6 +30,14 @@ final class PurchasedEntityConstraintValidatorTest extends OrderKernelTestBase {
    'commerce_order_test',
  ];

  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    // We're focusing on testing the test availability checker.
    $container->removeDefinition('commerce_order.entity_accessible_availability_checker');
  }

  /**
   * Tests the availability constraint.
   *
@@ -43,8 +53,9 @@ final class PurchasedEntityConstraintValidatorTest extends OrderKernelTestBase {
   * @dataProvider dataProviderCheckerData
   * @covers ::validate
   */
  public function testAvailabilityConstraint($sku, $order_state, AvailabilityResult $expected_check_result, $expected_constraint) {
  public function testAvailabilityConstraint($sku, $order_state, AvailabilityResult $expected_check_result, bool $expected_constraint) {
    $context = new Context($this->createUser(), $this->store);
    /** @var \Drupal\commerce_order\AvailabilityManagerInterface $availability_manager */
    $availability_manager = $this->container->get('commerce_order.availability_manager');

    $product_variation = $this->createTestProductVariation([
@@ -207,18 +218,18 @@ final class PurchasedEntityConstraintValidatorTest extends OrderKernelTestBase {
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  protected function createTestProductVariation(array $variation_data) {
    /** @var \Drupal\commerce_product\Entity\ProductVariation $product_variation */
    $product_variation = ProductVariation::create($variation_data + [
      'type' => 'default',
    ]);
    $product_variation->save();
    /** @var \Drupal\commerce_product\Entity\Product $product */
    $product = Product::create([
      'title' => 'test product',
      'type' => 'default',
      'stores' => [$this->store->id()],
      'variations' => [$product_variation],
    ]);
    /** @var \Drupal\commerce_product\Entity\ProductVariation $product_variation */
    $product_variation = ProductVariation::create($variation_data + [
      'type' => 'default',
    ]);
    $product_variation->save();
    $product->addVariation($product_variation);
    $product->save();
    return $this->reloadEntity($product_variation);
  }
+79 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\commerce_order\Unit;

use Drupal\commerce\Context;
use Drupal\commerce_order\AvailabilityResult;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\commerce_order\EntityAccessibleAvailabilityChecker;
use Drupal\commerce\PurchasableEntityInterface;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\commerce_store\Entity\StoreInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\user\UserInterface;
use Prophecy\Argument;

/**
 * @coversDefaultClass \Drupal\commerce_order\EntityAccessibleAvailabilityChecker
 * @group commerce
 */
class EntityAccessibleAvailabilityCheckerTest extends UnitTestCase {

  /**
   * @covers ::applies
   */
  public function testApplies() {
    $checker = new EntityAccessibleAvailabilityChecker();

    $purchasable_entity = $this->prophesize(PurchasableEntityInterface::class);
    $order_item = $this->prophesize(OrderItemInterface::class);
    $order_item->hasPurchasedEntity()->willReturn(TRUE);
    $order_item->getPurchasedEntity()->willReturn($purchasable_entity);
    $this->assertTrue($checker->applies($order_item->reveal()));

    $order_item->hasPurchasedEntity()->willReturn(FALSE);
    $this->assertFalse($checker->applies($order_item->reveal()));

    $order_item->hasPurchasedEntity()->willReturn(TRUE);
    $order_item->getPurchasedEntity()->willReturn(NULL);
    $this->assertFalse($checker->applies($order_item->reveal()));
  }

  /**
   * @covers ::check
   */
  public function testCheck() {
    $checker = new EntityAccessibleAvailabilityChecker();
    $context = new Context(
      $this->prophesize(UserInterface::class)->reveal(),
      $this->prophesize(StoreInterface::class)->reveal()
    );

    // Purchasable entity doesn't implement the EntityPublishedInterface,
    // hence the isPublished() check will be skipped.
    $purchasable_entity = $this->prophesize(PurchasableEntityInterface::class);
    $purchasable_entity->access('view', Argument::type(AccountInterface::class))->willReturn(TRUE);
    $order_item = $this->prophesize(OrderItemInterface::class);
    $order_item->getPurchasedEntity()->willReturn($purchasable_entity);
    $this->assertEquals(AvailabilityResult::neutral(), $checker->check($order_item->reveal(), $context));
    $purchasable_entity->access('view', Argument::type(AccountInterface::class))->willReturn(FALSE);
    $order_item->getPurchasedEntity()->willReturn($purchasable_entity);
    $this->assertEquals(AvailabilityResult::unavailable(), $checker->check($order_item->reveal(), $context));

    $purchasable_entity = $this->prophesize(ProductVariationInterface::class);
    $purchasable_entity->access('view', Argument::type(AccountInterface::class))->willReturn(TRUE);
    $purchasable_entity->isPublished()->willReturn(TRUE);
    $order_item = $this->prophesize(OrderItemInterface::class);
    $order_item->getPurchasedEntity()->willReturn($purchasable_entity);
    $this->assertEquals(AvailabilityResult::neutral(), $checker->check($order_item->reveal(), $context));
    $purchasable_entity->isPublished()->willReturn(FALSE);
    $order_item->getPurchasedEntity()->willReturn($purchasable_entity);
    $this->assertEquals(AvailabilityResult::unavailable(), $checker->check($order_item->reveal(), $context));

    $purchasable_entity->isPublished()->willReturn(TRUE);
    $purchasable_entity->access('view', Argument::type(AccountInterface::class))->willReturn(FALSE);
    $this->assertEquals(AvailabilityResult::unavailable(), $checker->check($order_item->reveal(), $context));
  }

}