From a9cce874f87ebe2da0705cb01907a41e87bee348 Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Tue, 19 Nov 2024 20:03:27 +0100 Subject: [PATCH] Tests BeHat and PHPUnit --- composer.json | 10 +- config/services/command_providers.yaml | 2 + config/services/providers.yaml | 149 ++++--- config/services/stripe/client.yaml | 10 +- config/services/stripe/configurator.yaml | 2 +- .../cancel_authorized_order.feature | 6 +- .../stripe_checkout/cancel_order.feature | 17 +- .../stripe_web_elements/cancel_order.feature | 4 +- phpunit.xml.dist | 2 +- .../Checkout/CancelPaymentRequestHandler.php | 14 +- .../Checkout/RefundPaymentRequestHandler.php | 52 ++- .../FailedAwarePaymentRequestHandlerTrait.php | 29 ++ .../NotifyPaymentRequestHandler.php | 14 +- .../StatusPaymentRequestHandler.php | 13 +- .../CancelPaymentRequestHandler.php | 14 +- .../RefundPaymentRequestHandler.php | 51 ++- .../CapturePaymentRequestCommandProvider.php | 5 +- .../StripeConfiguratorInterface.php | 2 + src/Stripe/HttpClient/PsrClient.php | 2 +- tests/Api/JsonApiTestCase.php | 363 ++++++++++++++++++ tests/Api/Shop/PaymentRequestsTest.php | 154 ++++++++ tests/Api/Utils/HeadersBuilder.php | 103 +++++ tests/Application/config/bundles.php | 2 + .../test/fidry_alice_data_fixtures.yaml | 2 + .../fidry_alice_data_fixtures.yaml | 2 + tests/Application/config/routes.yaml | 9 - tests/Application/config/services_test.yaml | 1 + .../config/services_test}/mockers.xml | 28 +- .../Context/Setup/ManagingOrdersContext.php | 51 ++- .../Context/Ui/Shop/StripeCheckoutContext.php | 2 +- .../Ui/Shop/StripeWebElementsContext.php | 2 +- tests/Behat/Resources/services.xml | 1 - tests/Behat/Resources/services/contexts.xml | 6 +- tests/Behat/Resources/suites/ui/admin.yaml | 107 +++--- tests/DataFixtures/ORM/channel.yaml | 72 ++++ tests/DataFixtures/ORM/customer.yaml | 68 ++++ .../ORM/order_awaiting_payment.yaml | 62 +++ tests/DataFixtures/ORM/payment.yaml | 6 + tests/DataFixtures/ORM/payment_method.yaml | 101 +++++ tests/DataFixtures/ORM/product_variant.yaml | 105 +++++ tests/DataFixtures/ORM/shipping_category.yaml | 8 + tests/DataFixtures/ORM/shipping_method.yaml | 68 ++++ tests/DataFixtures/ORM/shop_user.yaml | 29 ++ .../ORM/stripe_checkout/payment_request.yaml | 25 ++ .../stripe_web_elements/payment_request.yaml | 9 + tests/DataFixtures/ORM/tax_category.yaml | 8 + .../Mocker/Api/CheckoutSessionMocker.php | 43 ++- .../Mocker/Api/PaymentIntentMocker.php | 2 +- tests/{Behat => }/Mocker/Api/RefundMocker.php | 2 +- .../Mocker/StripeCheckoutMocker.php | 31 +- tests/Mocker/StripeClientMocker.php | 22 ++ .../Mocker/StripeWebElementsMocker.php | 6 +- .../Checkout/Create/DetailsProviderTest.php | 170 ++++++++ .../Create/DetailsProviderTest.php | 103 +++++ ...equest_payment_method_stripe_checkout.json | 19 + ...ment_method_stripe_checkout_authorize.json | 19 + .../post_payment_request_with_error.json | 24 ++ 57 files changed, 1977 insertions(+), 256 deletions(-) create mode 100644 src/CommandHandler/FailedAwarePaymentRequestHandlerTrait.php create mode 100644 tests/Api/JsonApiTestCase.php create mode 100644 tests/Api/Shop/PaymentRequestsTest.php create mode 100644 tests/Api/Utils/HeadersBuilder.php create mode 100644 tests/Application/config/packages/test/fidry_alice_data_fixtures.yaml create mode 100644 tests/Application/config/packages/test_cached/fidry_alice_data_fixtures.yaml rename tests/{Behat/Resources/services => Application/config/services_test}/mockers.xml (59%) create mode 100644 tests/DataFixtures/ORM/channel.yaml create mode 100644 tests/DataFixtures/ORM/customer.yaml create mode 100644 tests/DataFixtures/ORM/order_awaiting_payment.yaml create mode 100644 tests/DataFixtures/ORM/payment.yaml create mode 100644 tests/DataFixtures/ORM/payment_method.yaml create mode 100644 tests/DataFixtures/ORM/product_variant.yaml create mode 100644 tests/DataFixtures/ORM/shipping_category.yaml create mode 100644 tests/DataFixtures/ORM/shipping_method.yaml create mode 100644 tests/DataFixtures/ORM/shop_user.yaml create mode 100644 tests/DataFixtures/ORM/stripe_checkout/payment_request.yaml create mode 100644 tests/DataFixtures/ORM/stripe_web_elements/payment_request.yaml create mode 100644 tests/DataFixtures/ORM/tax_category.yaml rename tests/{Behat => }/Mocker/Api/CheckoutSessionMocker.php (63%) rename tests/{Behat => }/Mocker/Api/PaymentIntentMocker.php (98%) rename tests/{Behat => }/Mocker/Api/RefundMocker.php (93%) rename tests/{Behat => }/Mocker/StripeCheckoutMocker.php (83%) create mode 100644 tests/Mocker/StripeClientMocker.php rename tests/{Behat => }/Mocker/StripeWebElementsMocker.php (94%) create mode 100644 tests/Provider/Checkout/Create/DetailsProviderTest.php create mode 100644 tests/Provider/WebElements/Create/DetailsProviderTest.php create mode 100644 tests/Responses/shop/payment_request/post_payment_request_payment_method_stripe_checkout.json create mode 100644 tests/Responses/shop/payment_request/post_payment_request_payment_method_stripe_checkout_authorize.json create mode 100644 tests/Responses/shop/payment_request/post_payment_request_with_error.json diff --git a/composer.json b/composer.json index 2406b80..0f43c80 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "sylius/core-bundle": "^2.0-dev", + "sylius/core-bundle": "^2.0", "stripe/stripe-php": "^16.1" }, "require-dev": { @@ -34,7 +34,7 @@ "robertfausk/behat-panther-extension": "^1.1", "sylius-labs/coding-standard": "^4.1", "sylius-labs/suite-tags-extension": "^0.2.0", - "sylius/sylius": "^2.0-dev", + "sylius/sylius": "^2.0", "symfony/browser-kit": "^6.4|^7.1", "symfony/debug-bundle": "^6.4|^7.1", "symfony/dotenv": "^6.4|^7.1", @@ -45,7 +45,11 @@ "nyholm/psr7": "^1.8", "mockery/mockery": "^1.6", "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-symfony": "^1.4" + "phpstan/phpstan-symfony": "^1.4", + "theofidry/alice-data-fixtures": "^1.7", + "doctrine/orm": "^2.20", + "doctrine/data-fixtures": "^1.8", + "lchrusciel/api-test-case": "^5.3" }, "suggest": { "sylius/shop-bundle": "Use the Sylius default front shop", diff --git a/config/services/command_providers.yaml b/config/services/command_providers.yaml index 74f81d8..16d511f 100644 --- a/config/services/command_providers.yaml +++ b/config/services/command_providers.yaml @@ -15,6 +15,8 @@ services: tags: - name: flux_se.sylius_stripe.command_provider.checkout action: !php/const Sylius\Component\Payment\Model\PaymentRequestInterface::ACTION_CAPTURE + - name: flux_se.sylius_stripe.command_provider.checkout + action: !php/const Sylius\Component\Payment\Model\PaymentRequestInterface::ACTION_AUTHORIZE flux_se.sylius_stripe.command_provider.checkout.status: class: FluxSE\SyliusStripePlugin\CommandProvider\Checkout\StatusPaymentRequestCommandProvider diff --git a/config/services/providers.yaml b/config/services/providers.yaml index eb886c4..fd9f4d8 100644 --- a/config/services/providers.yaml +++ b/config/services/providers.yaml @@ -9,90 +9,127 @@ parameters: services: # WEB ELEMENTS flux_se.sylius_stripe.provider.web_elements.create.payment_intent_params: - class: FluxSE\SyliusStripePlugin\Provider\WebElements\Create\PaymentIntentParamsProvider + class: FluxSE\SyliusStripePlugin\Provider\CompositeParamsProvider arguments: - - '@flux_se.sylius_stripe.provider.web_elements.amount' - - '@flux_se.sylius_stripe.provider.web_elements.currency' - - '@flux_se.sylius_stripe.provider.web_elements.payment_method_types' - - '@flux_se.sylius_stripe.provider.web_elements.metadata' + - !tagged_iterator flux_se.sylius_stripe.provider.web_elements.details flux_se.sylius_stripe.provider.web_elements.amount: class: FluxSE\SyliusStripePlugin\Provider\WebElements\Create\AmountProvider + tags: + - name: flux_se.sylius_stripe.provider.web_elements.details flux_se.sylius_stripe.provider.web_elements.currency: class: FluxSE\SyliusStripePlugin\Provider\WebElements\Create\CurrencyProvider - - flux_se.sylius_stripe.provider.web_elements.customer_email: - class: FluxSE\SyliusStripePlugin\Provider\CustomerEmailProvider + tags: + - name: flux_se.sylius_stripe.provider.web_elements.details flux_se.sylius_stripe.provider.web_elements.payment_method_types: class: FluxSE\SyliusStripePlugin\Provider\PaymentMethodTypesProvider + tags: + - name: flux_se.sylius_stripe.provider.web_elements.details + + flux_se.sylius_stripe.provider.web_elements.token_hash_metadata: + class: FluxSE\SyliusStripePlugin\Provider\TokenHashMetadataProvider + tags: + - name: flux_se.sylius_stripe.provider.web_elements.details - flux_se.sylius_stripe.provider.web_elements.metadata: - class: FluxSE\SyliusStripePlugin\Provider\PaymentMetadataProvider + flux_se.sylius_stripe.provider.web_elements.capture_method.manual: + class: FluxSE\SyliusStripePlugin\Provider\PaymentIntentCaptureMethodManualProvider + tags: + - name: flux_se.sylius_stripe.provider.web_elements.details # CHECKOUT - flux_se.sylius_stripe.provider.checkout.create.checkout_session_params: - class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\CheckoutSessionParamsProvider - arguments: - - '@flux_se.sylius_stripe.provider.checkout.customer_email' - - '@flux_se.sylius_stripe.provider.checkout.mode' - - '@flux_se.sylius_stripe.provider.checkout.line_items' - - '@flux_se.sylius_stripe.provider.checkout.payment_method_types' - - '@flux_se.sylius_stripe.provider.checkout.metadata' - - '@flux_se.sylius_stripe.provider.checkout.after_url' flux_se.sylius_stripe.provider.checkout.after_url.default: class: FluxSE\SyliusStripePlugin\Provider\DefaultAfterUrlProvider arguments: - '%flux_se.sylius_stripe.checkout.after_urls%' + + flux_se.sylius_stripe.provider.checkout.create.checkout_session_params: + class: FluxSE\SyliusStripePlugin\Provider\CompositeParamsProvider + arguments: + - !tagged_iterator flux_se.sylius_stripe.provider.checkout.details + flux_se.sylius_stripe.provider.checkout.after_url: - class: FluxSE\SyliusStripePlugin\Provider\AfterUrlProvider + class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\AfterUrlProvider arguments: - '@flux_se.sylius_stripe.provider.checkout.after_url.default' + tags: + - name: flux_se.sylius_stripe.provider.checkout.details - flux_se.sylius_stripe.provider.checkout.mode: - class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\ModeProvider + flux_se.sylius_stripe.provider.checkout.payment_mode: + class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\ModePaymentProvider + tags: + - name: flux_se.sylius_stripe.provider.checkout.details + + flux_se.sylius_stripe.provider.checkout.customer_email: + class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\CustomerEmailProvider + tags: + - name: flux_se.sylius_stripe.provider.checkout.details + + flux_se.sylius_stripe.provider.checkout.payment_method_types: + class: FluxSE\SyliusStripePlugin\Provider\PaymentMethodTypesProvider + tags: + - name: flux_se.sylius_stripe.provider.checkout.details + + flux_se.sylius_stripe.provider.checkout.token_hash_metadata: + class: FluxSE\SyliusStripePlugin\Provider\TokenHashMetadataProvider + tags: + - name: flux_se.sylius_stripe.provider.checkout.details + + flux_se.sylius_stripe.provider.checkout.payment_intent_data: + class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\PaymentIntentDataProvider + arguments: + - !tagged_iterator flux_se.sylius_stripe.provider.checkout.payment_intent_data + tags: + - name: flux_se.sylius_stripe.provider.checkout.details + + flux_se.sylius_stripe.provider.checkout.payment_intent_data.capture_method.manual: + class: FluxSE\SyliusStripePlugin\Provider\PaymentIntentCaptureMethodManualProvider + tags: + - name: flux_se.sylius_stripe.provider.checkout.payment_intent_data flux_se.sylius_stripe.provider.checkout.line_items: class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\LineItemsProvider arguments: - - '@flux_se.sylius_stripe.provider.checkout.line_item' - - '@flux_se.sylius_stripe.provider.checkout.shipping_line_item' + - !tagged_iterator flux_se.sylius_stripe.provider.checkout.line_item.order_item + - !tagged_iterator flux_se.sylius_stripe.provider.checkout.line_item.shipment + tags: + - name: flux_se.sylius_stripe.provider.checkout.details - flux_se.sylius_stripe.provider.checkout.line_item: - class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\LineItemProvider + flux_se.sylius_stripe.provider.checkout.line_item.order_item: + class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\LineItem\OrderItemLineItemProvider arguments: - - '@flux_se.sylius_stripe.provider.checkout.line_item_images' - - '@flux_se.sylius_stripe.provider.checkout.line_item_name' + - !tagged_iterator flux_se.sylius_stripe.provider.checkout.line_item.order_item.inner + tags: + - name: flux_se.sylius_stripe.provider.checkout.line_item.order_item - flux_se.sylius_stripe.provider.checkout.line_item_images: - class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\LineItemImagesProvider + flux_se.sylius_stripe.provider.checkout.line_item.order_item.product_data.images: + class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\LineItem\OrderItemProductDataImagesProvider arguments: - '@liip_imagine.cache.manager' - '%flux_se.sylius_stripe.line_item_image.imagine_filter%' - '%flux_se.sylius_stripe.line_item_image.fallback_image%' - '%flux_se.sylius_stripe.line_item_image.localhost_pattern%' + tags: + - name: flux_se.sylius_stripe.provider.checkout.line_item.order_item.inner - flux_se.sylius_stripe.provider.checkout.line_item_name: - class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\LinetItemNameProvider + flux_se.sylius_stripe.provider.checkout.line_item.order_item.product_data.name: + class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\LineItem\OrderItemProductDataNameProvider + tags: + - name: flux_se.sylius_stripe.provider.checkout.line_item.order_item.inner - flux_se.sylius_stripe.provider.checkout.shipping_line_item: - class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\ShippingLineItemProvider + flux_se.sylius_stripe.provider.checkout.line_item.shipment: + class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\LineItem\ShipmentLineItemProvider arguments: - - '@flux_se.sylius_stripe.provider.checkout.shipping_line_item_name' - - flux_se.sylius_stripe.provider.checkout.shipping_line_item_name: - class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\ShippingLineItemNameProvider - - flux_se.sylius_stripe.provider.checkout.customer_email: - class: FluxSE\SyliusStripePlugin\Provider\CustomerEmailProvider - - flux_se.sylius_stripe.provider.checkout.payment_method_types: - class: FluxSE\SyliusStripePlugin\Provider\PaymentMethodTypesProvider + - !tagged_iterator flux_se.sylius_stripe.provider.checkout.line_item.shipment.inner + tags: + - name: flux_se.sylius_stripe.provider.checkout.line_item.shipment - flux_se.sylius_stripe.provider.checkout.metadata: - class: FluxSE\SyliusStripePlugin\Provider\PaymentMetadataProvider + flux_se.sylius_stripe.provider.checkout.line_item.shipment.product_data.name: + class: FluxSE\SyliusStripePlugin\Provider\Checkout\Create\LineItem\ShipmentProductDataNameProvider + tags: + - name: flux_se.sylius_stripe.provider.checkout.line_item.shipment.inner FluxSE\SyliusStripePlugin\Provider\StripeNotifyPaymentProvider: arguments: @@ -104,9 +141,21 @@ services: # REFUND flux_se.sylius_stripe.provider.refund.create: - class: FluxSE\SyliusStripePlugin\Provider\Refund\Create\RefundParamsProvider + class: FluxSE\SyliusStripePlugin\Provider\CompositeParamsProvider arguments: - - '@flux_se.sylius_stripe.provider.refund.metadata' + - !tagged_iterator flux_se.sylius_stripe.provider.refund.details - flux_se.sylius_stripe.provider.refund.metadata: - class: FluxSE\SyliusStripePlugin\Provider\Refund\Create\RefundMetadataProvider + flux_se.sylius_stripe.provider.refund.metadata.amount: + class: FluxSE\SyliusStripePlugin\Provider\Refund\Create\AmountProvider + tags: + - name: flux_se.sylius_stripe.provider.refund.details + + flux_se.sylius_stripe.provider.refund.metadata.payment_intent: + class: FluxSE\SyliusStripePlugin\Provider\Refund\Create\PaymentIntentProvider + tags: + - name: flux_se.sylius_stripe.provider.refund.details + + flux_se.sylius_stripe.provider.refund.metadata.refund_token_hash: + class: FluxSE\SyliusStripePlugin\Provider\Refund\Create\RefundTokenHashMetadataProvider + tags: + - name: flux_se.sylius_stripe.provider.refund.details diff --git a/config/services/stripe/client.yaml b/config/services/stripe/client.yaml index 52219fd..21dc2bb 100644 --- a/config/services/stripe/client.yaml +++ b/config/services/stripe/client.yaml @@ -3,10 +3,16 @@ parameters: services: - flux_se.sylius_stripe.stripe.http_client: - class: FluxSE\SyliusStripePlugin\Stripe\HttpClient\PsrClient + FluxSE\SyliusStripePlugin\Stripe\HttpClient\PsrClient: + abstract: true arguments: - '@sylius.http_client' - '@sylius.http_client' - '@sylius.http_client' - '%flux_se.sylius_stripe.stripe.client.chunk_size%' + + flux_se.sylius_stripe.stripe.http_client: + parent: FluxSE\SyliusStripePlugin\Stripe\HttpClient\PsrClient + + flux_se.sylius_stripe.stripe.streaming_http_client: + parent: FluxSE\SyliusStripePlugin\Stripe\HttpClient\PsrClient diff --git a/config/services/stripe/configurator.yaml b/config/services/stripe/configurator.yaml index 29de27d..b75ea05 100644 --- a/config/services/stripe/configurator.yaml +++ b/config/services/stripe/configurator.yaml @@ -5,6 +5,6 @@ services: arguments: - '@logger' - '@flux_se.sylius_stripe.stripe.http_client' - - '@flux_se.sylius_stripe.stripe.http_client' + - '@flux_se.sylius_stripe.stripe.streaming_http_client' FluxSE\SyliusStripePlugin\Stripe\Configurator\StripeConfiguratorInterface: alias: flux_se.sylius_stripe.stripe.configurator diff --git a/features/admin/stripe_checkout/cancel_authorized_order.feature b/features/admin/stripe_checkout/cancel_authorized_order.feature index 44b2322..235988c 100644 --- a/features/admin/stripe_checkout/cancel_authorized_order.feature +++ b/features/admin/stripe_checkout/cancel_authorized_order.feature @@ -1,5 +1,5 @@ @managing_orders -Feature: Canceling an authorized order with Stripe Checkout Session +Feature: Canceling an authorized order with Stripe Checkout In order to cancel an order already authorized As an Administrator I want to be able to cancel a Stripe authorized order @@ -12,13 +12,13 @@ Feature: Canceling an authorized order with Stripe Checkout Session And there is a customer "oliver@teamarrow.com" that placed an order "#00000001" And the customer bought a single "Green Arrow" And the customer chose "Free" shipping method to "United States" with "Stripe" payment - And this order is already authorized as "pi_123" Stripe payment intent + And this order is not yet paid as "cs_test_123" Stripe Checkout Session And I am logged in as an administrator @ui @api Scenario: Cancelling the order with an authorized payment Given I am viewing the summary of this order - And I am prepared to cancel this order + And I am prepared to expire this order checkout session When I cancel this order Then I should be notified that it has been successfully updated And it should have payment with state cancelled diff --git a/features/admin/stripe_checkout/cancel_order.feature b/features/admin/stripe_checkout/cancel_order.feature index 493fca5..ad5acb7 100644 --- a/features/admin/stripe_checkout/cancel_order.feature +++ b/features/admin/stripe_checkout/cancel_order.feature @@ -1,5 +1,5 @@ @managing_orders -Feature: Canceling an order with Stripe Checkout Session +Feature: Canceling an order with Stripe Checkout In order to cancel a not paid order As an Administrator I want to be able to cancel a Stripe not paid order @@ -9,26 +9,27 @@ Feature: Canceling an order with Stripe Checkout Session And the store has a product "Green Arrow" And the store ships everywhere for free And the store has a payment method "Stripe" with a code "stripe" and Stripe Checkout payment gateway without using authorize - And there is a customer "oliver@teamarrow.com" that placed an order "#00000001" + And there is a customer "oliver@teamarrow.com" that placed an order "#00000022" And the customer bought a single "Green Arrow" And the customer chose "Free" shipping method to "United States" with "Stripe" payment - And this order is not yet paid as "cs_123" Stripe Checkout Session + And this order is not yet paid as "cs_test_123" Stripe Checkout Session And I am logged in as an administrator @ui @api Scenario: Cancelling the order when a checkout session is still available Given I am viewing the summary of this order - And I am prepared to expire the checkout session on this order + And I am prepared to expire this order checkout session When I cancel this order Then I should be notified that it has been successfully updated And it should have payment with state cancelled - And it should have payment state cancelled + And it should have payment state "Cancelled" @ui @api Scenario: Cancelling the order after the customer canceled the payment - Given this order payment has been canceled - And I am viewing the summary of this order - And I am prepared to cancel this order + Given I am viewing the summary of this order + And I am prepared to expire this order checkout session + And this order payment has been canceled + And I am prepared to expire this order checkout session When I cancel this order Then I should be notified that it has been successfully updated And it should have payment with state cancelled diff --git a/features/admin/stripe_web_elements/cancel_order.feature b/features/admin/stripe_web_elements/cancel_order.feature index 6038948..da7f01a 100644 --- a/features/admin/stripe_web_elements/cancel_order.feature +++ b/features/admin/stripe_web_elements/cancel_order.feature @@ -18,7 +18,7 @@ Feature: Canceling an order with Stripe JS @ui @api Scenario: Cancelling the order when a payment intent is still available Given I am viewing the summary of this order - And I am prepared to cancel the payment intent on this order + And I am prepared to cancel the payment intent When I cancel this order Then I should be notified that it has been successfully updated And it should have payment with state cancelled @@ -32,4 +32,4 @@ Feature: Canceling an order with Stripe JS When I cancel this order Then I should be notified that it has been successfully updated And it should have payment with state cancelled - And it should have payment state cancelled + And it should have payment state "Cancelled" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 27d7e55..449231b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -13,7 +13,7 @@ - + diff --git a/src/CommandHandler/Checkout/CancelPaymentRequestHandler.php b/src/CommandHandler/Checkout/CancelPaymentRequestHandler.php index e708828..957e3fc 100644 --- a/src/CommandHandler/Checkout/CancelPaymentRequestHandler.php +++ b/src/CommandHandler/Checkout/CancelPaymentRequestHandler.php @@ -5,6 +5,7 @@ namespace FluxSE\SyliusStripePlugin\CommandHandler\Checkout; use FluxSE\SyliusStripePlugin\Command\Checkout\CancelPaymentRequest; +use FluxSE\SyliusStripePlugin\CommandHandler\FailedAwarePaymentRequestHandlerTrait; use FluxSE\SyliusStripePlugin\Manager\Checkout\ExpireManagerInterface; use FluxSE\SyliusStripePlugin\Manager\Checkout\RetrieveManagerInterface; use FluxSE\SyliusStripePlugin\Processor\PaymentTransitionProcessorInterface; @@ -17,13 +18,16 @@ #[AsMessageHandler] final readonly class CancelPaymentRequestHandler { + use FailedAwarePaymentRequestHandlerTrait; + public function __construct( private PaymentRequestProviderInterface $paymentRequestProvider, private RetrieveManagerInterface $retrieveCheckoutManager, private ExpireManagerInterface $expireCheckoutManager, private PaymentTransitionProcessorInterface $paymentTransitionProcessor, - private StateMachineInterface $stateMachine, + StateMachineInterface $stateMachine, ) { + $this->stateMachine = $stateMachine; } public function __invoke(CancelPaymentRequest $cancelPaymentRequest): void @@ -32,7 +36,13 @@ public function __invoke(CancelPaymentRequest $cancelPaymentRequest): void /** @var string|null $id */ $id = $paymentRequest->getPayment()->getDetails()['id'] ?? null; - Assert::notNull($id, 'An id is required to retrieve the related Stripe Checkout/Session.'); + if (null === $id) { + $this->failWithReason( + $paymentRequest, + 'An id is required to retrieve the related Stripe Checkout/Session.' + ); + return; + } $session = $this->retrieveCheckoutManager->retrieve($paymentRequest, $id); if ($session::STATUS_OPEN !== $session->status) { diff --git a/src/CommandHandler/Checkout/RefundPaymentRequestHandler.php b/src/CommandHandler/Checkout/RefundPaymentRequestHandler.php index 61b13ea..ed59cc8 100644 --- a/src/CommandHandler/Checkout/RefundPaymentRequestHandler.php +++ b/src/CommandHandler/Checkout/RefundPaymentRequestHandler.php @@ -5,6 +5,7 @@ namespace FluxSE\SyliusStripePlugin\CommandHandler\Checkout; use FluxSE\SyliusStripePlugin\Command\Checkout\RefundPaymentRequest; +use FluxSE\SyliusStripePlugin\CommandHandler\FailedAwarePaymentRequestHandlerTrait; use FluxSE\SyliusStripePlugin\Manager\Checkout\RetrieveManagerInterface; use FluxSE\SyliusStripePlugin\Manager\Refund\CreateManagerInterface; use FluxSE\SyliusStripePlugin\Processor\PaymentTransitionProcessorInterface; @@ -18,13 +19,16 @@ #[AsMessageHandler] final readonly class RefundPaymentRequestHandler { + use FailedAwarePaymentRequestHandlerTrait; + public function __construct( private PaymentRequestProviderInterface $paymentRequestProvider, private RetrieveManagerInterface $retrieveCheckoutManager, private CreateManagerInterface $createRefundManager, private PaymentTransitionProcessorInterface $paymentTransitionProcessor, - private StateMachineInterface $stateMachine, + StateMachineInterface $stateMachine, ) { + $this->stateMachine = $stateMachine; } public function __invoke(RefundPaymentRequest $refundPaymentRequest): void @@ -33,26 +37,35 @@ public function __invoke(RefundPaymentRequest $refundPaymentRequest): void /** @var string|null $id */ $id = $paymentRequest->getPayment()->getDetails()['id'] ?? null; - Assert::notNull($id, 'An id is required to retrieve the related Stripe Checkout Session.'); + if (null === $id) { + $this->failWithReason( + $paymentRequest, + 'An id is required to retrieve the related Stripe Checkout/Session.' + ); + return; + } $session = $this->retrieveCheckoutManager->retrieve($paymentRequest, $id); if ($session::PAYMENT_STATUS_PAID !== $session->payment_status) { - $reason = sprintf( - 'Checkout Session payment status is "%s" instead of "%s".', - $session->payment_status, - $session::PAYMENT_STATUS_PAID, + $this->failWithReason( + $paymentRequest, + sprintf( + 'Checkout Session payment status is "%s" instead of "%s".', + $session->payment_status, + $session::PAYMENT_STATUS_PAID, + ) ); - $this->setFailed($paymentRequest, $reason); - return; } if (0 >= $session->amount_total) { - $reason = sprintf( - 'Checkout Session amount total is not greater than 0 (amount_total: %s)', - $session->amount_total, + $this->failWithReason( + $paymentRequest, + sprintf( + 'Checkout Session amount total is not greater than 0 (amount_total: %s)', + $session->amount_total, + ) ); - $this->setFailed($paymentRequest, $reason); return; } @@ -76,19 +89,4 @@ public function __invoke(RefundPaymentRequest $refundPaymentRequest): void PaymentRequestTransitions::TRANSITION_COMPLETE, ); } - - private function setFailed( - PaymentRequestInterface $paymentRequest, - string $reason, - ): void { - $paymentRequest->setResponseData([ - 'reason' => $reason, - ]); - - $this->stateMachine->apply( - $paymentRequest, - PaymentRequestTransitions::GRAPH, - PaymentRequestTransitions::TRANSITION_FAIL, - ); - } } diff --git a/src/CommandHandler/FailedAwarePaymentRequestHandlerTrait.php b/src/CommandHandler/FailedAwarePaymentRequestHandlerTrait.php new file mode 100644 index 0000000..e45bf79 --- /dev/null +++ b/src/CommandHandler/FailedAwarePaymentRequestHandlerTrait.php @@ -0,0 +1,29 @@ +setResponseData([ + 'reason' => $reason, + ]); + + $this->stateMachine->apply( + $paymentRequest, + PaymentRequestTransitions::GRAPH, + PaymentRequestTransitions::TRANSITION_FAIL, + ); + } +} diff --git a/src/CommandHandler/NotifyPaymentRequestHandler.php b/src/CommandHandler/NotifyPaymentRequestHandler.php index c464062..7bc8761 100644 --- a/src/CommandHandler/NotifyPaymentRequestHandler.php +++ b/src/CommandHandler/NotifyPaymentRequestHandler.php @@ -12,18 +12,20 @@ use Sylius\Bundle\PaymentBundle\Provider\PaymentRequestProviderInterface; use Sylius\Component\Payment\PaymentRequestTransitions; use Symfony\Component\Messenger\Attribute\AsMessageHandler; -use Webmozart\Assert\Assert; #[AsMessageHandler] final readonly class NotifyPaymentRequestHandler { + use FailedAwarePaymentRequestHandlerTrait; + public function __construct( private PaymentRequestProviderInterface $paymentRequestProvider, private RetrieveManagerInterface $retrieveManager, private WebhookEventProcessorInterface $webhookProcessor, private PaymentTransitionProcessorInterface $paymentTransitionProcessor, - private StateMachineInterface $stateMachine, + StateMachineInterface $stateMachine, ) { + $this->stateMachine = $stateMachine; } public function __invoke(AbstractNotifyPaymentRequest $capturePaymentRequest): void @@ -35,7 +37,13 @@ public function __invoke(AbstractNotifyPaymentRequest $capturePaymentRequest): v $data = $payload['event'] ?? []; /** @var string|null $id */ $id = $data['id'] ?? null; - Assert::notNull($id, 'The payment request payload "[event][id]" is null.'); + if (null === $id) { + $this->failWithReason( + $paymentRequest, + 'The payment request payload "[event][id]" is null.' + ); + return; + } $event = $this->retrieveManager->retrieve($paymentRequest, $id); diff --git a/src/CommandHandler/StatusPaymentRequestHandler.php b/src/CommandHandler/StatusPaymentRequestHandler.php index 9a1443d..d6cec1d 100644 --- a/src/CommandHandler/StatusPaymentRequestHandler.php +++ b/src/CommandHandler/StatusPaymentRequestHandler.php @@ -18,6 +18,8 @@ #[AsMessageHandler] final readonly class StatusPaymentRequestHandler { + use FailedAwarePaymentRequestHandlerTrait; + /** * @param RetrieveManagerInterface $retrieveManager */ @@ -25,8 +27,9 @@ public function __construct( private PaymentRequestProviderInterface $paymentRequestProvider, private RetrieveManagerInterface $retrieveManager, private PaymentTransitionProcessorInterface $paymentTransitionProcessor, - private StateMachineInterface $stateMachine, + StateMachineInterface $stateMachine, ) { + $this->stateMachine = $stateMachine; } public function __invoke(AbstractStatusPaymentRequest $statusPaymentRequest): void @@ -35,7 +38,13 @@ public function __invoke(AbstractStatusPaymentRequest $statusPaymentRequest): vo /** @var string|null $id */ $id = $paymentRequest->getPayment()->getDetails()['id'] ?? null; - Assert::notNull($id, 'An id is required to retrieve the related Stripe API Resource (Session|PaymentIntent).'); + if (null === $id) { + $this->failWithReason( + $paymentRequest, + 'An id is required to retrieve the related Stripe API Resource (Session|PaymentIntent).' + ); + return; + } $stripeApiResource = $this->retrieveManager->retrieve($paymentRequest, $id); diff --git a/src/CommandHandler/WebElements/CancelPaymentRequestHandler.php b/src/CommandHandler/WebElements/CancelPaymentRequestHandler.php index bf764ee..1cf437c 100644 --- a/src/CommandHandler/WebElements/CancelPaymentRequestHandler.php +++ b/src/CommandHandler/WebElements/CancelPaymentRequestHandler.php @@ -5,6 +5,7 @@ namespace FluxSE\SyliusStripePlugin\CommandHandler\WebElements; use FluxSE\SyliusStripePlugin\Command\WebElements\CancelPaymentRequest; +use FluxSE\SyliusStripePlugin\CommandHandler\FailedAwarePaymentRequestHandlerTrait; use FluxSE\SyliusStripePlugin\Manager\WebElements\CancelManagerInterface; use FluxSE\SyliusStripePlugin\Manager\WebElements\RetrieveManagerInterface; use FluxSE\SyliusStripePlugin\Processor\PaymentTransitionProcessorInterface; @@ -17,13 +18,16 @@ #[AsMessageHandler] final readonly class CancelPaymentRequestHandler { + use FailedAwarePaymentRequestHandlerTrait; + public function __construct( private PaymentRequestProviderInterface $paymentRequestProvider, private RetrieveManagerInterface $retrieveManager, private CancelManagerInterface $cancelManager, private PaymentTransitionProcessorInterface $paymentTransitionProcessor, - private StateMachineInterface $stateMachine, + StateMachineInterface $stateMachine, ) { + $this->stateMachine = $stateMachine; } public function __invoke(CancelPaymentRequest $cancelPaymentRequest): void @@ -32,7 +36,13 @@ public function __invoke(CancelPaymentRequest $cancelPaymentRequest): void /** @var string|null $id */ $id = $paymentRequest->getPayment()->getDetails()['id'] ?? null; - Assert::notNull($id, 'An id is required to retrieve the related Stripe PaymentIntent.'); + if (null === $id) { + $this->failWithReason( + $paymentRequest, + 'An id is required to retrieve the related Stripe PaymentIntent.' + ); + return; + } $paymentIntent = $this->retrieveManager->retrieve($paymentRequest, $id); diff --git a/src/CommandHandler/WebElements/RefundPaymentRequestHandler.php b/src/CommandHandler/WebElements/RefundPaymentRequestHandler.php index 8944fe1..1b365b6 100644 --- a/src/CommandHandler/WebElements/RefundPaymentRequestHandler.php +++ b/src/CommandHandler/WebElements/RefundPaymentRequestHandler.php @@ -5,6 +5,7 @@ namespace FluxSE\SyliusStripePlugin\CommandHandler\WebElements; use FluxSE\SyliusStripePlugin\Command\WebElements\RefundPaymentRequest; +use FluxSE\SyliusStripePlugin\CommandHandler\FailedAwarePaymentRequestHandlerTrait; use FluxSE\SyliusStripePlugin\Manager\Refund\CreateManagerInterface; use FluxSE\SyliusStripePlugin\Manager\WebElements\RetrieveManagerInterface; use FluxSE\SyliusStripePlugin\Processor\PaymentTransitionProcessorInterface; @@ -18,13 +19,16 @@ #[AsMessageHandler] final readonly class RefundPaymentRequestHandler { + use FailedAwarePaymentRequestHandlerTrait; + public function __construct( private PaymentRequestProviderInterface $paymentRequestProvider, private RetrieveManagerInterface $retrievePaymentIntentManager, private CreateManagerInterface $createRefundManager, private PaymentTransitionProcessorInterface $paymentTransitionProcessor, - private StateMachineInterface $stateMachine, + StateMachineInterface $stateMachine, ) { + $this->stateMachine = $stateMachine; } public function __invoke(RefundPaymentRequest $refundPaymentRequest): void @@ -33,26 +37,36 @@ public function __invoke(RefundPaymentRequest $refundPaymentRequest): void /** @var string|null $id */ $id = $paymentRequest->getPayment()->getDetails()['id'] ?? null; - Assert::notNull($id, 'An id is required to retrieve the related Stripe PaymentIntent.'); + if (null === $id) { + $this->failWithReason( + $paymentRequest, + 'An id is required to retrieve the related Stripe PaymentIntent.' + ); + return; + } $paymentIntent = $this->retrievePaymentIntentManager->retrieve($paymentRequest, $id); if ($paymentIntent::STATUS_SUCCEEDED !== $paymentIntent->status) { - $reason = sprintf( - 'Payment Intent status is "%s" instead of "%s".', - $paymentIntent->status, - $paymentIntent::STATUS_SUCCEEDED, + $this->failWithReason( + $paymentRequest, + sprintf( + 'Payment Intent status is "%s" instead of "%s".', + $paymentIntent->status, + $paymentIntent::STATUS_SUCCEEDED, + ) ); - $this->setFailed($paymentRequest, $reason); return; } if (0 >= $paymentIntent->amount) { - $reason = sprintf( - 'Payment Intent amount is not greater than 0 (amount: %s)', - $paymentIntent->amount, + $this->failWithReason( + $paymentRequest, + sprintf( + 'Payment Intent amount is not greater than 0 (amount: %s)', + $paymentIntent->amount, + ) ); - $this->setFailed($paymentRequest, $reason); return; } @@ -76,19 +90,4 @@ public function __invoke(RefundPaymentRequest $refundPaymentRequest): void PaymentRequestTransitions::TRANSITION_COMPLETE, ); } - - private function setFailed( - PaymentRequestInterface $paymentRequest, - string $reason, - ): void { - $paymentRequest->setResponseData([ - 'reason' => $reason, - ]); - - $this->stateMachine->apply( - $paymentRequest, - PaymentRequestTransitions::GRAPH, - PaymentRequestTransitions::TRANSITION_FAIL, - ); - } } diff --git a/src/CommandProvider/Checkout/CapturePaymentRequestCommandProvider.php b/src/CommandProvider/Checkout/CapturePaymentRequestCommandProvider.php index b81c2ae..e158ce0 100644 --- a/src/CommandProvider/Checkout/CapturePaymentRequestCommandProvider.php +++ b/src/CommandProvider/Checkout/CapturePaymentRequestCommandProvider.php @@ -12,7 +12,10 @@ final class CapturePaymentRequestCommandProvider implements PaymentRequestComman { public function supports(PaymentRequestInterface $paymentRequest): bool { - return $paymentRequest->getAction() === PaymentRequestInterface::ACTION_CAPTURE; + return in_array($paymentRequest->getAction(), [ + PaymentRequestInterface::ACTION_CAPTURE, + PaymentRequestInterface::ACTION_AUTHORIZE, + ], true); } public function provide(PaymentRequestInterface $paymentRequest): object diff --git a/src/Stripe/Configurator/StripeConfiguratorInterface.php b/src/Stripe/Configurator/StripeConfiguratorInterface.php index 1220bd7..17b90ec 100644 --- a/src/Stripe/Configurator/StripeConfiguratorInterface.php +++ b/src/Stripe/Configurator/StripeConfiguratorInterface.php @@ -10,4 +10,6 @@ interface StripeConfiguratorInterface * @param array $config */ public function configure(array $config): void; + + public function unConfigure(): void; } diff --git a/src/Stripe/HttpClient/PsrClient.php b/src/Stripe/HttpClient/PsrClient.php index e9f666c..5fbb08c 100644 --- a/src/Stripe/HttpClient/PsrClient.php +++ b/src/Stripe/HttpClient/PsrClient.php @@ -15,7 +15,7 @@ use Stripe\HttpClient\StreamingClientInterface; use Stripe\Util\Util; -class PsrClient implements ClientInterface, StreamingClientInterface +final class PsrClient implements ClientInterface, StreamingClientInterface { public function __construct( private PsrClientInterface $httpClient, diff --git a/tests/Api/JsonApiTestCase.php b/tests/Api/JsonApiTestCase.php new file mode 100644 index 0000000..05f244c --- /dev/null +++ b/tests/Api/JsonApiTestCase.php @@ -0,0 +1,363 @@ + 'application/ld+json', 'HTTP_ACCEPT' => 'application/ld+json']; + + public const PATCH_CONTENT_TYPE_HEADER = ['CONTENT_TYPE' => 'application/merge-patch+json', 'HTTP_ACCEPT' => 'application/ld+json']; + + public const FILE_CONTENT_TYPE_HEADER = ['CONTENT_TYPE' => 'multipart/form-data', 'HTTP_ACCEPT' => 'application/ld+json']; + + private bool $isAdminContext = false; + + private bool $isShopUserContext = false; + + private ?string $adminUserEmail = null; + + private ?string $shopUserEmail = null; + + /** @var array */ + private array $defaultGetHeaders = []; + + /** @var array */ + private array $defaultPostHeaders = []; + + /** @var array */ + private array $defaultPutHeaders = []; + + /** @var array */ + private array $defaultPatchHeaders = []; + + /** @var array */ + private array $defaultDeleteHeaders = []; + + /** + * @param array $data + */ + public function __construct(?string $name = null, array $data = [], int|string $dataName = '') + { + parent::__construct($name, $data, $dataName); + + $this->dataFixturesPath = __DIR__ . '/../DataFixtures/ORM'; + $this->expectedResponsesPath = __DIR__ . '/../Responses'; + } + + protected function setUpAdminContext(?string $email = null): void + { + $this->isAdminContext = true; + $this->adminUserEmail = $email; + } + + protected function setUpShopUserContext(?string $email = null): void + { + $this->isShopUserContext = true; + $this->shopUserEmail = $email; + } + + protected function disableAdminContext(): void + { + $this->isAdminContext = false; + } + + protected function disableShopUserContext(): void + { + $this->isShopUserContext = false; + } + + protected function setUpDefaultGetHeaders(): void + { + $this->defaultGetHeaders = [ + 'HTTP_ACCEPT' => 'application/ld+json', + 'CONTENT_TYPE' => 'application/ld+json', + ]; + } + + protected function setUpDefaultPostHeaders(): void + { + $this->defaultPostHeaders = [ + 'HTTP_ACCEPT' => 'application/ld+json', + 'CONTENT_TYPE' => 'application/ld+json', + ]; + } + + protected function setUpDefaultPutHeaders(): void + { + $this->defaultPutHeaders = [ + 'HTTP_ACCEPT' => 'application/ld+json', + 'CONTENT_TYPE' => 'application/ld+json', + ]; + } + + protected function setUpDefaultPatchHeaders(): void + { + $this->defaultPatchHeaders = [ + 'HTTP_ACCEPT' => 'application/ld+json', + 'CONTENT_TYPE' => 'application/merge-patch+json', + ]; + } + + protected function setUpDefaultDeleteHeaders(): void + { + $this->defaultDeleteHeaders = [ + 'HTTP_ACCEPT' => 'application/ld+json', + 'CONTENT_TYPE' => 'application/ld+json', + ]; + } + + protected function get(string $id): ?object + { + if (property_exists(static::class, 'container')) { + return self::$kernel->getContainer()->get($id); + } + + return parent::get($id); + } + + protected function getUploadedFile(string $path, string $name, string $type = 'image/jpg'): UploadedFile + { + return new UploadedFile(__DIR__ . '/../Resources/' . $path, $name, $type); + } + + protected function headerBuilder(): HeadersBuilder + { + return new HeadersBuilder( + $this->get('lexik_jwt_authentication.jwt_manager'), + $this->get('sylius.repository.admin_user'), + $this->get('sylius.repository.shop_user'), + self::$kernel->getContainer()->getParameter('sylius.api.authorization_header'), + ); + } + + /** + * @param array|string> $queryParameters + * @param array $headers + */ + protected function requestGet(string $uri, array $queryParameters = [], array $headers = []): Crawler + { + if (!empty($this->defaultGetHeaders)) { + $headers = array_merge($this->defaultGetHeaders, $headers); + } + + return $this->request('GET', $uri, $queryParameters, $headers); + } + + /** + * @param array|string> $queryParameters + * @param array $headers + * @param array $body + */ + protected function requestPost( + string $uri, + ?array $body = null, + array $queryParameters = [], + array $parameters = [], + array $headers = [], + array $files = [], + ): Crawler { + if (!empty($this->defaultPostHeaders)) { + $headers = array_merge($this->defaultPostHeaders, $headers); + } + + return $this->request('POST', $uri, $queryParameters, $headers, $body, $parameters, $files); + } + + /** + * @param array|string> $queryParameters + * @param array $headers + * @param array $body + */ + protected function requestPut(string $uri, ?array $body = null, array $queryParameters = [], array $headers = []): Crawler + { + if (!empty($this->defaultPutHeaders)) { + $headers = array_merge($this->defaultPutHeaders, $headers); + } + + return $this->request('PUT', $uri, $queryParameters, $headers, $body); + } + + /** + * @param array $body + * @param array $headers + * @param array|string> $queryParameters + */ + protected function requestPatch(string $uri, ?array $body = null, array $queryParameters = [], array $headers = []): Crawler + { + if (!empty($this->defaultPatchHeaders)) { + $headers = array_merge($this->defaultPatchHeaders, $headers); + } + + return $this->request('PATCH', $uri, $queryParameters, $headers, $body); + } + + /** + * @param array|string> $queryParameters + * @param array $headers + */ + protected function requestDelete(string $uri, array $queryParameters = [], array $headers = []): Crawler + { + if (!empty($this->defaultDeleteHeaders)) { + $headers = array_merge($this->defaultDeleteHeaders, $headers); + } + + return $this->request('DELETE', $uri, $queryParameters, $headers); + } + + /** @throws \Exception */ + protected function assertResponseSuccessful(string $filename): void + { + $this->assertResponse( + $this->client->getResponse(), + $filename, + Response::HTTP_OK, + ); + } + + /** @throws \Exception */ + protected function assertResponseCreated(string $filename): void + { + $this->assertResponse( + $this->client->getResponse(), + $filename, + Response::HTTP_CREATED, + ); + } + + /** @throws \Exception */ + protected function assertResponseUnprocessableEntity(string $filename): void + { + $this->assertResponse( + $this->client->getResponse(), + $filename, + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + + /** @throws \Exception */ + protected function assertResponseNotFound(string $message = 'Not Found'): void + { + $this->assertResponseErrorMessage($message, Response::HTTP_NOT_FOUND); + } + + /** @throws \Exception */ + protected function assertResponseForbidden(): void + { + $this->assertResponseErrorMessage('Access Denied.', Response::HTTP_FORBIDDEN); + } + + /** @throws \Exception */ + protected function assertResponseErrorMessage(string $message, int $code = Response::HTTP_UNPROCESSABLE_ENTITY): void + { + $content = json_decode($this->client->getResponse()->getContent(), true); + Assert::assertIsArray($content, 'Response content supposed to be an array'); + + $expectedContent = [ + '@context' => '/api/v2/contexts/Error', + '@id' => '/api/v2/errors/' . $code, + '@type' => 'hydra:Error', + 'title' => 'An error occurred', + 'detail' => $message, + 'status' => $code, + 'type' => '/errors/' . $code, + 'description' => $message, + 'hydra:description' => $message, + 'hydra:title' => 'An error occurred', + ]; + + Assert::assertSame($expectedContent, $content); + $this->assertResponseCode($this->client->getResponse(), $code); + } + + /** + * @throws \Exception + * + * @param array $expectedViolations + */ + protected function assertResponseViolations(Response $response, array $expectedViolations): void + { + if (isset($_SERVER['OPEN_ERROR_IN_BROWSER']) && true === $_SERVER['OPEN_ERROR_IN_BROWSER']) { + $this->showErrorInBrowserIfOccurred($response); + } + + $this->assertResponseCode($response, Response::HTTP_UNPROCESSABLE_ENTITY); + $this->assertJsonHeader($response); + $this->assertJsonResponseViolations($response, $expectedViolations); + } + + /** + * @throws \Exception + * + * @param array $expectedViolations + */ + protected function assertJsonResponseViolations( + Response $response, + array $expectedViolations, + bool $assertViolationsCount = true, + ): void { + $responseContent = $response->getContent() ?: ''; + $this->assertNotEmpty($responseContent); + $violations = json_decode($responseContent, true)['violations'] ?? []; + + if ($assertViolationsCount) { + $this->assertCount(count($expectedViolations), $violations, $responseContent); + } + + $violationMap = []; + foreach ($violations as $violation) { + $violationMap[$violation['propertyPath']][] = $violation['message']; + } + + foreach ($expectedViolations as $expectedViolation) { + $propertyPath = $expectedViolation['propertyPath']; + $this->assertArrayHasKey($propertyPath, $violationMap, $responseContent); + $this->assertContains($expectedViolation['message'], $violationMap[$propertyPath], $responseContent); + } + } + + /** + * @param array|string> $queryParameters + * @param array $headers + */ + protected function request( + string $method, + string $uri, + array $queryParameters = [], + array $headers = [], + ?array $body = null, + array $parameters = [], + array $files = [], + ): Crawler { + if ($this->isAdminContext) { + $email = $this->adminUserEmail ?? 'api@example.com'; + $headers = array_merge($this->headerBuilder()->withAdminUserAuthorization($email)->build(), $headers); + } + + if ($this->isShopUserContext) { + $email = $this->shopUserEmail ?? 'shop@example.com'; + $headers = array_merge($this->headerBuilder()->withShopUserAuthorization($email)->build(), $headers); + } + + $queryStrings = empty($queryParameters) ? '' : http_build_query($queryParameters); + + $uri = $queryStrings ? $uri . '?' . $queryStrings : $uri; + + return $this->client->request( + method: $method, + uri: $uri, + parameters: $parameters, + files: $files, + server: $headers, + content: is_array($body) ? json_encode($body, \JSON_THROW_ON_ERROR) : null, + ); + } +} diff --git a/tests/Api/Shop/PaymentRequestsTest.php b/tests/Api/Shop/PaymentRequestsTest.php new file mode 100644 index 0000000..211edc2 --- /dev/null +++ b/tests/Api/Shop/PaymentRequestsTest.php @@ -0,0 +1,154 @@ +setUpShopUserContext(); + + $this->stripeCheckoutSessionMocker = static::getContainer()->get(StripeCheckoutMocker::class); + + parent::setUp(); + } + + /** + * @dataProvider createPaymentRequestProvider + * + * @param string[] $fixturesPaths + * + * @throws \JsonException + */ + public function test_it_creates_a_payment_request(string $method, array $fixturesPaths, string $responsePath): void + { + $fixtures = $this->loadFixturesFromFiles($fixturesPaths); + + $this->stripeCheckoutSessionMocker->mockCaptureOrAuthorize(function() use ($fixtures, $method) { + /** @var OrderInterface $order */ + $order = $fixtures['order']; + /** @var PaymentMethodInterface $paymentMethod */ + $paymentMethod = $fixtures[$method]; + /** @var PaymentInterface $payment */ + $payment = $fixtures['payment']; + + $this->client->request( + method: 'POST', + uri: sprintf('/api/v2/shop/orders/%s/payment-requests', $order->getTokenValue()), + server: $this->headerBuilder() + ->withJsonLdAccept() + ->withJsonLdContentType() + ->withShopUserAuthorization('oliver@doe.com') + ->build(), + content: json_encode([ + 'paymentId' => $payment->getId(), + 'paymentMethodCode' => $paymentMethod->getCode(), + 'payload' => [ + 'success_url' => 'https://myshop.tld/target-path', + 'cancel_url' => 'https://myshop.tld/after-path', + ], + ], \JSON_THROW_ON_ERROR), + ); + }); + + $this->assertResponse( + $this->client->getResponse(), + $responsePath, + Response::HTTP_CREATED, + ); + } + + /** + * @dataProvider createPaymentRequestProviderWithError + * + * @param string[] $fixturesPaths + * + * @throws \JsonException + */ + public function test_it_does_not_create_a_payment_request_without_required_data(string $method, array $fixturesPaths, string $responsePath): void + { + $fixtures = $this->loadFixturesFromFiles($fixturesPaths); + + /** @var OrderInterface $order */ + $order = $fixtures['order']; + /** @var PaymentMethodInterface $paymentMethod */ + $paymentMethod = $fixtures[$method]; + /** @var PaymentInterface $payment */ + $payment = $fixtures['payment']; + + $this->client->request( + method: 'POST', + uri: sprintf('/api/v2/shop/orders/%s/payment-requests', $order->getTokenValue()), + server: $this->headerBuilder() + ->withJsonLdAccept() + ->withJsonLdContentType() + ->withShopUserAuthorization('oliver@doe.com') + ->build(), + content: json_encode([ + 'paymentId' => $payment->getId(), + 'paymentMethodCode' => $paymentMethod->getCode(), + ], \JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + $responsePath, + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + + public static function createPaymentRequestProvider(): iterable + { + foreach (['payment_method_stripe_checkout', 'payment_method_stripe_checkout_authorize'] as $method) { + yield $method => [ + $method, + [ + 'shop_user.yaml', + 'channel.yaml', + 'customer.yaml', + 'payment.yaml', + 'payment_method.yaml', + 'product_variant.yaml', + 'shipping_category.yaml', + 'tax_category.yaml', + 'shipping_method.yaml', + 'order_awaiting_payment.yaml', + ], + 'shop/payment_request/post_payment_request_'.$method, + ]; + } + } + + public static function createPaymentRequestProviderWithError(): iterable + { + foreach (['payment_method_stripe_checkout', 'payment_method_stripe_checkout_authorize'] as $method) { + yield $method => [ + $method, + [ + 'shop_user.yaml', + 'channel.yaml', + 'customer.yaml', + 'payment.yaml', + 'payment_method.yaml', + 'product_variant.yaml', + 'shipping_category.yaml', + 'tax_category.yaml', + 'shipping_method.yaml', + 'order_awaiting_payment.yaml', + ], + 'shop/payment_request/post_payment_request_with_error', + ]; + } + } +} diff --git a/tests/Api/Utils/HeadersBuilder.php b/tests/Api/Utils/HeadersBuilder.php new file mode 100644 index 0000000..e766969 --- /dev/null +++ b/tests/Api/Utils/HeadersBuilder.php @@ -0,0 +1,103 @@ + */ + private array $headers = []; + + /** + * @param UserRepositoryInterface $adminUserRepository + * @param UserRepositoryInterface $shopUserRepository + */ + public function __construct( + private JWTTokenManagerInterface $jwtManager, + private UserRepositoryInterface $adminUserRepository, + private UserRepositoryInterface $shopUserRepository, + private string $authorizationHeader, + ) { + } + + public function withJsonContentType(): self + { + $this->headers['CONTENT_TYPE'] = 'application/json'; + + return $this; + } + + public function withJsonLdContentType(): self + { + $this->headers['CONTENT_TYPE'] = 'application/ld+json'; + + return $this; + } + + public function withMergePatchJsonContentType(): self + { + $this->headers['CONTENT_TYPE'] = 'application/merge-patch+json'; + + return $this; + } + + public function withMultipartFormDataContentType(): self + { + $this->headers['CONTENT_TYPE'] = 'multipart/form-data'; + + return $this; + } + + public function withJsonAccept(): self + { + $this->headers['HTTP_ACCEPT'] = 'application/json'; + + return $this; + } + + public function withJsonLdAccept(): self + { + $this->headers['HTTP_ACCEPT'] = 'application/ld+json'; + + return $this; + } + + public function withShopUserAuthorization(string $email): self + { + $shopUser = $this->shopUserRepository->findOneByEmail($email); + + if (!$shopUser instanceof BaseUserInterface) { + throw new \InvalidArgumentException(sprintf('Shop user with email "%s" does not exist.', $email)); + } + + $this->headers['HTTP_' . $this->authorizationHeader] = 'Bearer ' . $this->jwtManager->create($shopUser); + + return $this; + } + + public function withAdminUserAuthorization(string $email): self + { + $adminUser = $this->adminUserRepository->findOneByEmail($email); + + if (!$adminUser instanceof BaseUserInterface) { + throw new \InvalidArgumentException(sprintf('Admin user with email "%s" does not exist.', $email)); + } + + $this->headers['HTTP_' . $this->authorizationHeader] = 'Bearer ' . $this->jwtManager->create($adminUser); + + return $this; + } + + /** @return array */ + public function build(): array + { + return $this->headers; + } +} diff --git a/tests/Application/config/bundles.php b/tests/Application/config/bundles.php index fbad965..0b064bb 100644 --- a/tests/Application/config/bundles.php +++ b/tests/Application/config/bundles.php @@ -58,5 +58,7 @@ Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true], Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], Sylius\TwigExtra\Symfony\SyliusTwigExtraBundle::class => ['all' => true], + Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['test' => true, 'test_cached' => true], + Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['test' => true, 'test_cached' => true], FluxSE\SyliusStripePlugin\FluxSESyliusStripePlugin::class => ['all' => true], ]; diff --git a/tests/Application/config/packages/test/fidry_alice_data_fixtures.yaml b/tests/Application/config/packages/test/fidry_alice_data_fixtures.yaml new file mode 100644 index 0000000..ae4e694 --- /dev/null +++ b/tests/Application/config/packages/test/fidry_alice_data_fixtures.yaml @@ -0,0 +1,2 @@ +fidry_alice_data_fixtures: + default_purge_mode: no_purge diff --git a/tests/Application/config/packages/test_cached/fidry_alice_data_fixtures.yaml b/tests/Application/config/packages/test_cached/fidry_alice_data_fixtures.yaml new file mode 100644 index 0000000..f5c8649 --- /dev/null +++ b/tests/Application/config/packages/test_cached/fidry_alice_data_fixtures.yaml @@ -0,0 +1,2 @@ +imports: + - { resource: "../test/fidry_alice_data_fixtures.yaml" } diff --git a/tests/Application/config/routes.yaml b/tests/Application/config/routes.yaml index 6d967c4..e69de29 100644 --- a/tests/Application/config/routes.yaml +++ b/tests/Application/config/routes.yaml @@ -1,9 +0,0 @@ -flux_se_sylius_stripe_shop: - resource: "@FluxSESyliusStripePlugin/config/shop_routing.yaml" - prefix: /{_locale} - requirements: - _locale: ^[a-z]{2}(?:_[A-Z]{2})?$ - -flux_se_sylius_stripe_admin: - resource: "@FluxSESyliusStripePlugin/config/admin_routing.yaml" - prefix: /admin diff --git a/tests/Application/config/services_test.yaml b/tests/Application/config/services_test.yaml index af40901..180c910 100644 --- a/tests/Application/config/services_test.yaml +++ b/tests/Application/config/services_test.yaml @@ -1,4 +1,5 @@ imports: + - { resource: "services_test/*.xml" } - { resource: "../../Behat/Resources/services.xml" } - { resource: "../../../vendor/sylius/sylius/src/Sylius/Behat/Resources/config/services.xml" } diff --git a/tests/Behat/Resources/services/mockers.xml b/tests/Application/config/services_test/mockers.xml similarity index 59% rename from tests/Behat/Resources/services/mockers.xml rename to tests/Application/config/services_test/mockers.xml index 01c6233..e5e54be 100644 --- a/tests/Behat/Resources/services/mockers.xml +++ b/tests/Application/config/services_test/mockers.xml @@ -3,34 +3,36 @@ + + - FluxSE\SyliusStripePlugin\Stripe\HttpClient\PsrClient - + class="Mockery\MockInterface"> + Stripe\HttpClient\ClientInterface + - + - + - + - + - - - + + + - + - - + + diff --git a/tests/Behat/Context/Setup/ManagingOrdersContext.php b/tests/Behat/Context/Setup/ManagingOrdersContext.php index 1a174ba..ace6165 100644 --- a/tests/Behat/Context/Setup/ManagingOrdersContext.php +++ b/tests/Behat/Context/Setup/ManagingOrdersContext.php @@ -11,8 +11,9 @@ use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; +use Sylius\Component\Payment\Model\PaymentInterface as BasePaymentInterface; use Sylius\Component\Payment\PaymentTransitions; -use Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\StripeCheckoutMocker; +use Tests\FluxSE\SyliusStripePlugin\Mocker\StripeCheckoutMocker; class ManagingOrdersContext implements Context { @@ -29,7 +30,7 @@ public function __construct( public function thisOrderIsAlreadyPaid(OrderInterface $order, string $stripePaymentIntentId): void { /** @var PaymentInterface $payment */ - $payment = $order->getPayments()->first(); + $payment = $order->getLastPayment(); $details = [ 'object' => PaymentIntent::OBJECT_NAME, @@ -54,7 +55,7 @@ public function thisOrderIsAlreadyPaid(OrderInterface $order, string $stripePaym public function thisOrderIsAlreadyAuthorized(OrderInterface $order, string $stripePaymentIntentId): void { /** @var PaymentInterface $payment */ - $payment = $order->getPayments()->first(); + $payment = $order->getLastPayment(); $details = [ 'object' => PaymentIntent::OBJECT_NAME, @@ -79,7 +80,7 @@ public function thisOrderIsAlreadyAuthorized(OrderInterface $order, string $stri public function thisOrderIsNotYetPaidStripeCheckoutSession(OrderInterface $order, string $stripeCheckoutSessionId): void { /** @var PaymentInterface $payment */ - $payment = $order->getPayments()->first(); + $payment = $order->getLastPayment(); $details = [ 'object' => Session::OBJECT_NAME, @@ -98,7 +99,7 @@ public function thisOrderIsNotYetPaidStripeCheckoutSession(OrderInterface $order public function thisOrderIsNotYetPaidStripeJs(OrderInterface $order, string $stripePaymentIntentId): void { /** @var PaymentInterface $payment */ - $payment = $order->getPayments()->first(); + $payment = $order->getLastPayment(); $details = [ 'object' => PaymentIntent::OBJECT_NAME, @@ -116,7 +117,7 @@ public function thisOrderIsNotYetPaidStripeJs(OrderInterface $order, string $str public function thisOrderPaymentHasBeenCancelled(OrderInterface $order): void { /** @var PaymentInterface $payment */ - $payment = $order->getPayments()->first(); + $payment = $order->getLastPayment(); $this->stateMachine->apply( $payment, @@ -132,10 +133,7 @@ public function thisOrderPaymentHasBeenCancelled(OrderInterface $order): void */ public function iAmPreparedToCancelThisOrder(OrderInterface $order): void { - /** @var PaymentInterface $payment */ - $payment = $order->getPayments()->first(); - - $details = $payment->getDetails(); + $details = $this->getLastNewPaymentDetails($order); $status = $details['status'] ?? PaymentIntent::STATUS_REQUIRES_PAYMENT_METHOD; $captureMethod = $details['capture_method'] ?? PaymentIntent::CAPTURE_METHOD_AUTOMATIC; @@ -143,15 +141,24 @@ public function iAmPreparedToCancelThisOrder(OrderInterface $order): void } /** - * @Given I am prepared to expire the checkout session on this order + * @Given /^I am prepared to expire (this order) checkout session$/ */ - public function iAmPreparedToExpireTheCheckoutSessionOnThisOrder(): void + public function iAmPreparedToExpireThisOrderCheckoutSession(OrderInterface $order): void { - $this->stripeCheckoutSessionMocker->mockExpirePayment(); + $details = $this->getLastNewPaymentDetails($order); + + if ([] === $details) { + return; + } + + $this->stripeCheckoutSessionMocker->mockExpirePayment( + $details['status'], + $details['payment_status'], + ); } /** - * @Given I am prepared to cancel the payment intent on this order + * @Given I am prepared to cancel the payment intent */ public function iAmPreparedToExpireThePaymentIntentOnThisOrder(): void { @@ -175,12 +182,22 @@ public function iAmPreparedToRefundThisOrder(): void public function iAmPreparedToCaptureAuthorizationOfThisOrder(OrderInterface $order): void { /** @var PaymentInterface $payment */ - $payment = $order->getPayments()->first(); - - $details = $payment->getDetails(); + $details = $this->getLastNewPaymentDetails($order); $status = $details['status'] ?? PaymentIntent::STATUS_REQUIRES_CAPTURE; $captureMethod = $details['capture_method'] ?? PaymentIntent::CAPTURE_METHOD_MANUAL; $this->stripeCheckoutSessionMocker->mockCaptureAuthorization($status, $captureMethod); } + + /** + * @param OrderInterface $order + * @return array + */ + private function getLastNewPaymentDetails(OrderInterface $order): array + { + /** @var PaymentInterface $payment */ + $payment = $order->getLastPayment(BasePaymentInterface::STATE_NEW); + + return $payment->getDetails(); + } } diff --git a/tests/Behat/Context/Ui/Shop/StripeCheckoutContext.php b/tests/Behat/Context/Ui/Shop/StripeCheckoutContext.php index 901bc12..9750e8a 100644 --- a/tests/Behat/Context/Ui/Shop/StripeCheckoutContext.php +++ b/tests/Behat/Context/Ui/Shop/StripeCheckoutContext.php @@ -11,8 +11,8 @@ use Stripe\Event; use Sylius\Behat\Page\Shop\Checkout\CompletePageInterface; use Sylius\Behat\Page\Shop\Order\ShowPageInterface; -use Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\StripeCheckoutMocker; use Tests\FluxSE\SyliusStripePlugin\Behat\Page\External\StripePage; +use Tests\FluxSE\SyliusStripePlugin\Mocker\StripeCheckoutMocker; class StripeCheckoutContext extends MinkContext { diff --git a/tests/Behat/Context/Ui/Shop/StripeWebElementsContext.php b/tests/Behat/Context/Ui/Shop/StripeWebElementsContext.php index 97b228b..9b571cc 100644 --- a/tests/Behat/Context/Ui/Shop/StripeWebElementsContext.php +++ b/tests/Behat/Context/Ui/Shop/StripeWebElementsContext.php @@ -11,8 +11,8 @@ use Stripe\PaymentIntent; use Sylius\Behat\Page\Shop\Checkout\CompletePageInterface; use Sylius\Behat\Page\Shop\Order\ShowPageInterface; -use Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\StripeWebElementsMocker; use Tests\FluxSE\SyliusStripePlugin\Behat\Page\External\StripePage; +use Tests\FluxSE\SyliusStripePlugin\Mocker\StripeWebElementsMocker; class StripeWebElementsContext extends MinkContext { diff --git a/tests/Behat/Resources/services.xml b/tests/Behat/Resources/services.xml index a15e9f3..20193bc 100644 --- a/tests/Behat/Resources/services.xml +++ b/tests/Behat/Resources/services.xml @@ -2,7 +2,6 @@ - diff --git a/tests/Behat/Resources/services/contexts.xml b/tests/Behat/Resources/services/contexts.xml index 18b8e8c..4c57086 100644 --- a/tests/Behat/Resources/services/contexts.xml +++ b/tests/Behat/Resources/services/contexts.xml @@ -37,12 +37,12 @@ class="Tests\FluxSE\SyliusStripePlugin\Behat\Context\Setup\ManagingOrdersContext"> - + - + @@ -50,7 +50,7 @@ - + diff --git a/tests/Behat/Resources/suites/ui/admin.yaml b/tests/Behat/Resources/suites/ui/admin.yaml index 11cca1a..511f4a1 100644 --- a/tests/Behat/Resources/suites/ui/admin.yaml +++ b/tests/Behat/Resources/suites/ui/admin.yaml @@ -1,61 +1,62 @@ default: - suites: - ui_managing_payment_methods: - contexts: - - sylius.behat.context.hook.doctrine_orm + suites: + ui_managing_payment_methods: + contexts: + - sylius.behat.context.hook.doctrine_orm - - sylius.behat.context.transform.address - - sylius.behat.context.transform.customer - - sylius.behat.context.transform.locale - - sylius.behat.context.transform.payment - - sylius.behat.context.transform.product - - sylius.behat.context.transform.shared_storage - - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.address + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.locale + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_method - - sylius.behat.context.setup.channel - - sylius.behat.context.setup.currency - - sylius.behat.context.setup.locale - - sylius.behat.context.setup.order - - sylius.behat.context.setup.payment - - sylius.behat.context.setup.product - - sylius.behat.context.setup.admin_security - - sylius.behat.context.setup.shipping - - sylius.behat.context.setup.user - - sylius.behat.context.setup.zone + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.currency + - sylius.behat.context.setup.locale + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.user + - sylius.behat.context.setup.zone - - sylius.behat.context.ui.admin.managing_payment_methods.stripe - - sylius.behat.context.ui.admin.notification - - tests.flux_se.sylius_stripe_plugin.behat.context.ui.admin.managing_payment_methods + - sylius.behat.context.ui.admin.managing_payment_methods.stripe + - sylius.behat.context.ui.admin.notification + - tests.flux_se.sylius_stripe_plugin.behat.context.ui.admin.managing_payment_methods - - tests.flux_se.sylius_stripe_plugin.behat.context.ui.admin.managing_payment_methods - filters: - tags: "@managing_payment_methods&&@ui" - ui_managing_orders: - contexts: - - sylius.behat.context.hook.doctrine_orm + - tests.flux_se.sylius_stripe_plugin.behat.context.ui.admin.managing_payment_methods + filters: + tags: "@managing_payment_methods&&@ui" + ui_managing_orders: + contexts: + - sylius.behat.context.hook.doctrine_orm - - sylius.behat.context.transform.address - - sylius.behat.context.transform.customer - - sylius.behat.context.transform.locale - - sylius.behat.context.transform.payment - - sylius.behat.context.transform.product - - sylius.behat.context.transform.shared_storage - - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.address + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.locale + - sylius.behat.context.transform.order + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_method - - sylius.behat.context.setup.channel - - sylius.behat.context.setup.currency - - sylius.behat.context.setup.locale - - sylius.behat.context.setup.order - - sylius.behat.context.setup.payment - - sylius.behat.context.setup.product - - sylius.behat.context.setup.admin_security - - sylius.behat.context.setup.shipping - - sylius.behat.context.setup.user - - sylius.behat.context.setup.zone - - tests.flux_se.sylius_stripe_plugin.behat.context.setup.managing_orders + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.currency + - sylius.behat.context.setup.locale + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.user + - sylius.behat.context.setup.zone + - tests.flux_se.sylius_stripe_plugin.behat.context.setup.managing_orders - - sylius.behat.context.ui.admin.managing_orders - - sylius.behat.context.ui.admin.notification - - tests.flux_se.sylius_stripe_plugin.behat.context.setup.stripe - filters: - tags: "@managing_orders&&@ui" + - sylius.behat.context.ui.admin.managing_orders + - sylius.behat.context.ui.admin.notification + - tests.flux_se.sylius_stripe_plugin.behat.context.setup.stripe + filters: + tags: "@managing_orders&&@ui" diff --git a/tests/DataFixtures/ORM/channel.yaml b/tests/DataFixtures/ORM/channel.yaml new file mode 100644 index 0000000..0c59d70 --- /dev/null +++ b/tests/DataFixtures/ORM/channel.yaml @@ -0,0 +1,72 @@ +Sylius\Component\Core\Model\Channel: + channel_web: + code: 'WEB' + name: 'Web Channel' + hostname: 'localhost' + contactEmail: 'web@sylius.com' + description: 'Lorem ipsum' + baseCurrency: '@currency_usd' + defaultLocale: '@locale_en' + locales: ['@locale_en', '@locale_pl'] + color: 'black' + enabled: true + menu_taxon: '@menu_taxon' + taxCalculationStrategy: 'order_items_based' + channelPriceHistoryConfig: '@web_price_history_config' + shopBillingData: '@channel_web_billing_data' + channel_mobile: + code: 'MOBILE' + name: 'Mobile Channel' + hostname: 'localhost' + contactEmail: 'mobile@sylius.com' + description: 'Lorem ipsum' + baseCurrency: '@currency_usd' + defaultLocale: '@locale_en' + locales: ['@locale_en', '@locale_pl'] + color: 'black' + enabled: true + taxCalculationStrategy: 'order_items_based' + channelPriceHistoryConfig: '@mobile_price_history_config' + shopBillingData: '@channel_mobile_billing_data' + shippingAddressInCheckoutRequired: true + +Sylius\Component\Core\Model\ChannelPriceHistoryConfig: + web_price_history_config: + lowestPriceForDiscountedProductsVisible: false + mobile_price_history_config: + lowestPriceForDiscountedProductsCheckingPeriod: 25 + lowestPriceForDiscountedProductsVisible: true + __calls: + - addTaxonExcludedFromShowingLowestPrice: ["@menu_taxon"] + +Sylius\Component\Currency\Model\Currency: + currency_usd: + code: 'USD' + +Sylius\Component\Core\Model\ShopBillingData: + channel_web_billing_data: + company: 'Web Channel Company' + taxId: 'Web Channel Tax ID' + country_code: 'EN' + street: 'Web Channel Street' + city: 'Web Channel City' + postcode: 'Web Channel Postcode' + channel_mobile_billing_data: + company: 'Mobile Channel Company' + taxId: 'Mobile Channel Tax ID' + country_code: 'PL' + street: 'Mobile Channel Street' + city: 'Mobile Channel City' + postcode: 'Mobile Channel Postcode' + +Sylius\Component\Locale\Model\Locale: + locale_en: + code: 'en_US' + locale_pl: + code: 'pl_PL' + locale_de: + code: 'de_DE' + +Sylius\Component\Core\Model\Taxon: + menu_taxon: + code: "MENU" diff --git a/tests/DataFixtures/ORM/customer.yaml b/tests/DataFixtures/ORM/customer.yaml new file mode 100644 index 0000000..8c87224 --- /dev/null +++ b/tests/DataFixtures/ORM/customer.yaml @@ -0,0 +1,68 @@ +Sylius\Component\Core\Model\Address: + address: + firstName: "John" + lastName: "Doe" + customer: '@customer_tony' + company: "CocaCola" + street: "Green Avenue" + countryCode: "US" + city: "New York" + postcode: "00000" + phoneNumber: "123456789" + provinceCode: "999" + provinceName: "east" + +Sylius\Component\Customer\Model\CustomerGroup: + group_premium: + code: 'premium' + name: 'Premium' + +Sylius\Component\Core\Model\ShopUser: + shop_user_{oliver}: + plainPassword: "sylius" + roles: [ROLE_USER] + enabled: true + customer: "@customer_" + username: "\\@doe.com" + usernameCanonical: "\\@doe.com" + shop_user_{dave}: + plainPassword: "sylius" + roles: [ROLE_USER] + enabled: false + customer: "@customer_" + username: "\\@doe.com" + usernameCanonical: "\\@doe.com" + shop_user_{tony}: + plainPassword: "sylius" + roles: [ROLE_USER] + enabled: true + verifiedAt: "<(new \\DateTime())>" + customer: "@customer_" + username: "\\@doe.com" + usernameCanonical: "\\@doe.com" + +Sylius\Component\Core\Model\Customer: + customer_{oliver, dave}: + firstName: "" + lastName: "Doe" + email: "\\@doe.com" + emailCanonical: "\\@doe.com" + birthday: "<(new \\DateTime())>" + createdAt: "<(new \\DateTime('-1 days'))>" + group: '@group_premium' + customer_{dave}: + firstName: "" + lastName: "Doe" + email: "\\@doe.com" + emailCanonical: "\\@doe.com" + birthday: "<(new \\DateTime())>" + createdAt: "<(new \\DateTime('-2 days'))>" + group: '@group_premium' + customer_{tony}: + firstName: "" + lastName: "Doe" + email: "\\@doe.com" + emailCanonical: "\\@doe.com" + birthday: "<(new \\DateTime())>" + createdAt: "<(new \\DateTime('-3 days'))>" + defaultAddress: '@address' diff --git a/tests/DataFixtures/ORM/order_awaiting_payment.yaml b/tests/DataFixtures/ORM/order_awaiting_payment.yaml new file mode 100644 index 0000000..2b60223 --- /dev/null +++ b/tests/DataFixtures/ORM/order_awaiting_payment.yaml @@ -0,0 +1,62 @@ +Sylius\Component\Core\Model\Order: + order: + channel: "@channel_web" + currencyCode: "USD" + localeCode: "en_US" + state: "new" + paymentState: "awaiting_payment" + shippingState: "ready" + tokenValue: "token" + customer: "@customer_oliver" + billingAddress: "@address" + shippingAddress: "@address" + items: [ '@order_item' ] + shipments: [ '@shipment' ] + +Sylius\Component\Core\Model\OrderItem: + order_item: + variant: "@product_variant" + order: "@order" + +Sylius\Component\Core\Model\OrderItemUnit: + order_item_unit: + __construct: ['@order_item'] + createdAt: "<(new \\DateTime())>" + updatedAt: "<(new \\DateTime())>" + +Sylius\Component\Core\Model\Adjustment: + adjustment_1: + type: promotion + label: Promotion + amount: 1000 + neutral: false + adjustable: '@order_item' + createdAt: "<(new \\DateTime())>" + updatedAt: "<(new \\DateTime())>" + adjustment_2: + type: order_promotion + label: New Year + amount: -500 + neutral: false + adjustable: '@order_item_unit' + createdAt: "<(new \\DateTime())>" + updatedAt: "<(new \\DateTime())>" + adjustment_3: + type: shipping + label: UPS + amount: 500 + neutral: false + adjustable: '@order' + createdAt: "<(new \\DateTime())>" + updatedAt: "<(new \\DateTime())>" + +Sylius\Component\Core\Model\Shipment: + shipment: + method: '@shipping_method_ups' + order: '@order' + state: 'new' + createdAt: "<(new \\DateTime())>" + updatedAt: "<(new \\DateTime())>" + adjustments: ['@adjustment_3'] + + diff --git a/tests/DataFixtures/ORM/payment.yaml b/tests/DataFixtures/ORM/payment.yaml new file mode 100644 index 0000000..0e8807d --- /dev/null +++ b/tests/DataFixtures/ORM/payment.yaml @@ -0,0 +1,6 @@ +Sylius\Component\Core\Model\Payment: + payment: + order: "@order" + state: "new" + amount: 1500 + currencyCode: "USD" diff --git a/tests/DataFixtures/ORM/payment_method.yaml b/tests/DataFixtures/ORM/payment_method.yaml new file mode 100644 index 0000000..8a9601a --- /dev/null +++ b/tests/DataFixtures/ORM/payment_method.yaml @@ -0,0 +1,101 @@ +Sylius\Component\Payment\Model\PaymentMethodTranslation: + payment_method_stripe_checkout_translation: + name: 'Stripe Checkout' + locale: 'en_US' + description: '' + translatable: '@payment_method_stripe_checkout' + payment_method_stripe_web_elements_translation: + name: 'Stripe Web Elements' + locale: 'en_US' + description: '' + translatable: '@payment_method_stripe_web_elements' + disabled_payment_method_translation: + name: 'Disabled payment method' + locale: 'en_US' + description: '' + translatable: '@disabled_payment_method' + +Sylius\Component\Core\Model\PaymentMethod: + payment_method_stripe_checkout: + code: 'STRIPE_CHECKOUT' + enabled: true + gatewayConfig: '@gateway_stripe_checkout' + currentLocale: 'en_US' + translations: + - '@payment_method_stripe_checkout_translation' + channels: ['@channel_web'] + payment_method_stripe_checkout_authorize: + code: 'STRIPE_CHECKOUT_AUTHORIZE' + enabled: true + gatewayConfig: '@gateway_stripe_checkout_authorize' + currentLocale: 'en_US' + translations: + - '@payment_method_stripe_checkout_translation' + channels: ['@channel_web'] + payment_method_stripe_web_elements: + code: 'STRIPE_WEB_ELEMENTS' + enabled: true + gatewayConfig: '@gateway_stripe_web_elements' + translations: + - '@payment_method_stripe_web_elements_translation' + channels: ['@channel_web'] + payment_method_stripe_web_elements_authorize: + code: 'STRIPE_WEB_ELEMENTS_AUTHORIZE' + enabled: true + gatewayConfig: '@gateway_stripe_web_elements_authorize' + translations: + - '@payment_method_stripe_web_elements_translation' + channels: ['@channel_web'] + disabled_payment_method: + code: 'DISABLED_PAYMENT_METHOD' + enabled: false + gatewayConfig: '@gateway_offline' + translations: + - '@disabled_payment_method_translation' + channels: ['@channel_web'] + +Sylius\Bundle\PayumBundle\Model\GatewayConfig: + gateway_stripe_checkout: + gatewayName: 'Stripe (Checkout)' + factoryName: 'stripe_checkout' + config: + use_payum: false + use_authorize: false + publishable_key: 'pk_test_123' + secret_key: 'sk_test_123' + webhook_secret_keys: + - 'whsec_test_123' + gateway_stripe_checkout_authorize: + gatewayName: 'Stripe (Checkout)' + factoryName: 'stripe_checkout' + config: + use_payum: false + use_authorize: true + publishable_key: 'pk_test_123' + secret_key: 'sk_test_123' + webhook_secret_keys: + - 'whsec_test_123' + gateway_stripe_web_elements: + gatewayName: 'Stripe (Web Elements)' + factoryName: 'stripe_web_elements' + config: + use_payum: false + use_authorize: false + publishable_key: 'pk_test_123' + secret_key: 'sk_test_123' + webhook_secret_keys: + - 'whsec_test_123' + gateway_stripe_web_elements_authorize: + gatewayName: 'Stripe (Web Elements)' + factoryName: 'stripe_web_elements' + config: + use_payum: false + use_authorize: true + publishable_key: 'pk_test_123' + secret_key: 'sk_test_123' + webhook_secret_keys: + - 'whsec_test_123' + gateway_offline: + gatewayName: 'Offline' + factoryName: 'offline' + config: [] diff --git a/tests/DataFixtures/ORM/product_variant.yaml b/tests/DataFixtures/ORM/product_variant.yaml new file mode 100644 index 0000000..f330765 --- /dev/null +++ b/tests/DataFixtures/ORM/product_variant.yaml @@ -0,0 +1,105 @@ +Sylius\Component\Core\Model\Product: + product: + fallbackLocale: en_US + currentLocale: en + code: 'MUG_SW' + +Sylius\Component\Core\Model\ProductVariant: + product_variant: + code: 'MUG' + version: 1 + product: '@product' + fallbackLocale: en_US + currentLocale: en + position: 1 + optionValues: ['@product_option_value_color_blue'] + channelPricings: + channel_web: '@product_variant_channel_web_pricing' + enabled: true + tracked: true + onHold: 0 + onHand: 10 + weight: 100.50 + width: 100.50 + height: 100.50 + depth: 100.50 + taxCategory: '@tax_category_default' + shippingCategory: '@shipping_category_default' + shippingRequired: true + product_variant_2: + code: 'MUG_2' + product: '@product' + fallbackLocale: en_US + currentLocale: en + position: 2 + channelPricings: + channel_web: '@product_variant_2_channel_web_pricing' + +Sylius\Component\Core\Model\ProductTranslation: + product_translation_mug_en_US: + locale: 'en_US' + translatable: '@product' + slug: 'mug' + name: 'Mug' + description: 'This is a mug' + shortDescription: 'Short mug description' + metaKeywords: 'mug' + metaDescription: 'Mug description' + +Sylius\Component\Product\Model\ProductVariantTranslation: + product_variant_translation: + name: 'Mug' + locale: en_US + translatable: '@product_variant' + product_variant_2_translation: + name: 'Mug 2' + locale: en_US + translatable: '@product_variant_2' + +Sylius\Component\Core\Model\ChannelPricing: + product_variant_channel_web_pricing: + channelCode: 'WEB' + price: 2000 + product_variant_2_channel_web_pricing: + channelCode: 'WEB' + price: 3000 + +Sylius\Component\Product\Model\ProductOption: + product_option_color: + code: 'COLOR' + currentLocale: 'en_US' + +Sylius\Component\Product\Model\ProductOptionTranslation: + product_option_translation_en_EN: + locale: 'en_US' + name: 'Color' + translatable: '@product_option_color' + +Sylius\Component\Product\Model\ProductOptionValue: + product_option_value_color_blue: + code: 'COLOR_BLUE' + currentLocale: 'en_US' + fallbackLocale: 'en_US' + option: '@product_option_color' + product_option_value_color_red: + code: 'COLOR_RED' + currentLocale: 'en_US' + fallbackLocale: 'en_US' + option: '@product_option_color' + +Sylius\Component\Product\Model\ProductOptionValueTranslation: + product_option_value_translation_blue: + locale: 'en_US' + value: 'Blue' + translatable: '@product_option_value_color_blue' + product_option_value_translation_red: + locale: 'en_US' + value: 'Red' + translatable: '@product_option_value_color_red' + +Sylius\Component\Core\Model\Customer: + customer: + firstName: 'John' + lastName: 'Doe' + email: 'john.doe@example.com' + emailCanonical: 'john.doe@example.com' diff --git a/tests/DataFixtures/ORM/shipping_category.yaml b/tests/DataFixtures/ORM/shipping_category.yaml new file mode 100644 index 0000000..851dae9 --- /dev/null +++ b/tests/DataFixtures/ORM/shipping_category.yaml @@ -0,0 +1,8 @@ +Sylius\Component\Shipping\Model\ShippingCategory: + shipping_category_default: + code: 'default' + name: 'Default' + shipping_category_special: + code: 'special' + name: 'Special' + description: 'For special care' diff --git a/tests/DataFixtures/ORM/shipping_method.yaml b/tests/DataFixtures/ORM/shipping_method.yaml new file mode 100644 index 0000000..f0cd134 --- /dev/null +++ b/tests/DataFixtures/ORM/shipping_method.yaml @@ -0,0 +1,68 @@ +Sylius\Component\Shipping\Model\ShippingMethodTranslation: + shipping_method_translation_ups: + name: 'UPS' + locale: 'en_US' + description: '' + translatable: '@shipping_method_ups' + shipping_method_translation_dhl: + name: 'DHL' + locale: 'en_US' + description: '' + translatable: '@shipping_method_dhl' + shipping_method_translation_fedex: + name: 'FedEx' + locale: 'en_US' + description: '' + translatable: '@shipping_method_fedex' + +Sylius\Component\Core\Model\ShippingMethod: + shipping_method_ups: + code: 'UPS' + enabled: true + calculator: 'flat_rate' + configuration: + WEB: + amount: 500 + MOBILE: + amount: 1000 + zone: '@zone_world' + currentLocale: 'en_US' + translations: ["@shipping_method_translation_ups"] + channels: ["@channel_web"] + shipping_method_dhl: + code: 'DHL' + enabled: true + calculator: 'flat_rate' + configuration: + WEB: + amount: 1000 + MOBILE: + amount: 2000 + zone: '@zone_world' + currentLocale: 'en_US' + translations: ["@shipping_method_translation_dhl"] + channels: ["@channel_web"] + shipping_method_fedex: + code: 'FEDEX' + enabled: true + calculator: 'flat_rate' + configuration: + WEB: + amount: 1000 + MOBILE: + amount: 2000 + zone: '@zone_world' + currentLocale: 'en_US' + translations: ["@shipping_method_translation_fedex"] + channels: ["@channel_mobile"] + +Sylius\Component\Addressing\Model\ZoneMember: + zone_member_{US, FR, DE}: + code: '' + +Sylius\Component\Addressing\Model\Zone: + zone_world: + code: 'WORLD' + name: 'World' + type: 'country' + members: ['@zone_member_US', '@zone_member_FR', '@zone_member_DE'] diff --git a/tests/DataFixtures/ORM/shop_user.yaml b/tests/DataFixtures/ORM/shop_user.yaml new file mode 100644 index 0000000..72c80ca --- /dev/null +++ b/tests/DataFixtures/ORM/shop_user.yaml @@ -0,0 +1,29 @@ +Sylius\Component\Core\Model\ShopUser: + default_shop_user: + plainPassword: "sylius" + roles: [ROLE_USER] + enabled: true + customer: "@default_customer" + username: "shop@example.com" + usernameCanonical: "shop@example.com" + shop_user_{oliver, dave}: + plainPassword: "sylius" + roles: [ROLE_USER] + enabled: true + customer: "@customer_" + username: "\\@doe.com" + usernameCanonical: "\\@doe.com" + +Sylius\Component\Core\Model\Customer: + default_customer: + firstName: "John" + lastName: "Doe" + email: "shop@example.com" + emailCanonical: "shop@example.com" + birthday: "<(new \\DateTime())>" + customer_{oliver, dave}: + firstName: "" + lastName: "Doe" + email: "\\@doe.com" + emailCanonical: "\\@doe.com" + birthday: "<(new \\DateTime())>" diff --git a/tests/DataFixtures/ORM/stripe_checkout/payment_request.yaml b/tests/DataFixtures/ORM/stripe_checkout/payment_request.yaml new file mode 100644 index 0000000..5fff1f1 --- /dev/null +++ b/tests/DataFixtures/ORM/stripe_checkout/payment_request.yaml @@ -0,0 +1,25 @@ +Sylius\Component\Payment\Model\PaymentRequest: + payment_request_authorize: + __construct: [ '@payment', '@payment_method_stripe_checkout_authorize' ] + state: "new" + action: "authorize" + payment_request_authorize_via_api: + __construct: [ '@payment', '@payment_method_stripe_checkout_authorize' ] + state: "new" + action: "authorize" + payload: { + "success_url": "https://myshop.tld/target-path", + "cancel_url": "https://myshop.tld/after-path", + } + payment_request_capture: + __construct: [ '@payment', '@payment_method_stripe_checkout' ] + state: "new" + action: "capture" + payment_request_capture_via_api: + __construct: [ '@payment', '@payment_method_stripe_checkout' ] + state: "new" + action: "capture" + payload: { + "success_url": "https://myshop.tld/target-path", + "cancel_url": "https://myshop.tld/after-path", + } diff --git a/tests/DataFixtures/ORM/stripe_web_elements/payment_request.yaml b/tests/DataFixtures/ORM/stripe_web_elements/payment_request.yaml new file mode 100644 index 0000000..d53877c --- /dev/null +++ b/tests/DataFixtures/ORM/stripe_web_elements/payment_request.yaml @@ -0,0 +1,9 @@ +Sylius\Component\Payment\Model\PaymentRequest: + payment_request_authorize: + __construct: [ '@payment', '@payment_method_stripe_web_elements_authorize' ] + state: "new" + action: "authorize" + payment_request_capture: + __construct: [ '@payment', '@payment_method_stripe_web_elements' ] + state: "new" + action: "capture" diff --git a/tests/DataFixtures/ORM/tax_category.yaml b/tests/DataFixtures/ORM/tax_category.yaml new file mode 100644 index 0000000..4e94218 --- /dev/null +++ b/tests/DataFixtures/ORM/tax_category.yaml @@ -0,0 +1,8 @@ +Sylius\Component\Taxation\Model\TaxCategory: + tax_category_default: + code: 'default' + name: 'Default' + tax_category_special: + code: 'special' + name: 'Special' + description: 'For peculiar places' diff --git a/tests/Behat/Mocker/Api/CheckoutSessionMocker.php b/tests/Mocker/Api/CheckoutSessionMocker.php similarity index 63% rename from tests/Behat/Mocker/Api/CheckoutSessionMocker.php rename to tests/Mocker/Api/CheckoutSessionMocker.php index 2f909f8..1b509d7 100644 --- a/tests/Behat/Mocker/Api/CheckoutSessionMocker.php +++ b/tests/Mocker/Api/CheckoutSessionMocker.php @@ -2,11 +2,12 @@ declare(strict_types=1); -namespace Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\Api; +namespace Tests\FluxSE\SyliusStripePlugin\Mocker\Api; use Mockery\MockInterface; use Stripe\Checkout\Session; use Stripe\HttpClient\ClientInterface; +use Stripe\Stripe; final class CheckoutSessionMocker { @@ -19,14 +20,16 @@ public function mockCreateAction(): void { $this->mockClient ->expects('request') - ->withArgs(['post', Session::classUrl()]) + ->withSomeOfArgs('post', $this->getCheckoutSessionBaseUrl()) ->andReturnUsing(function ($method, $absUrl, $params) { return [ json_encode(array_merge([ 'id' => 'cs_test_1', 'object' => Session::OBJECT_NAME, 'payment_intent' => 'pi_test_1', - 'url' => 'https://checkout.stripe.com/c/pay/cs_1', + 'url' => 'https://checkout.stripe.com/c/pay/cs_test_1', + 'status' => Session::STATUS_OPEN, + 'payment_status' => Session::PAYMENT_STATUS_UNPAID, ], $params), \JSON_THROW_ON_ERROR), 200, [], @@ -38,9 +41,17 @@ public function mockRetrieveAction(string $status, string $paymentStatus): void { $this->mockClient ->expects('request') - ->withArgs(['get', \Mockery::pattern('#^' . Session::classUrl() . '/cs_test_[^/]+$#')]) + ->withArgs(function ($method, $url) { + if ('get' !== $method) { + return false; + } + if (false === preg_match('#'. $this->getCheckoutSessionBaseUrl() .'/[^/]+$#', $url)) { + return false; + } + return true; + }) ->andReturnUsing(function ($method, $absUrl) use ($status, $paymentStatus) { - $id = str_replace(Session::classUrl() . '/', '', $absUrl); + $id = str_replace($this->getCheckoutSessionBaseUrl() . '/', '', $absUrl); return [ json_encode([ @@ -60,7 +71,7 @@ public function mockAllAction(string $status): void { $this->mockClient ->expects('request') - ->withArgs(['get', Session::classUrl()]) + ->with('get', $this->getCheckoutSessionBaseUrl()) ->andReturnUsing(function () use ($status) { return [ json_encode(['data' => [ @@ -80,19 +91,33 @@ public function mockExpireAction(): void { $this->mockClient ->expects('request') - ->withArgs(['get', \Mockery::pattern('#^' . Session::classUrl() . '/cs_test_[^/]+/expire$#')]) - ->andReturnUsing(function ($method, $absUrl, $params) { - $id = str_replace([Session::classUrl() . '/', '/expire'], '', $absUrl); + ->withArgs(function ($method, $url) { + if ('post' !== $method) { + return false; + } + if (false === preg_match('#'. $this->getCheckoutSessionBaseUrl() .'/[^/]+/expire$#', $url)) { + return false; + } + return true; + }) + ->andReturnUsing(function ($method, $absUrl) { + $id = str_replace([$this->getCheckoutSessionBaseUrl() . '/', '/expire'], '', $absUrl); return [ json_encode([ 'id' => $id, 'object' => Session::OBJECT_NAME, 'status' => Session::STATUS_EXPIRED, + 'payment_status' => Session::PAYMENT_STATUS_UNPAID, ], \JSON_THROW_ON_ERROR), 200, [], ]; }); } + + private function getCheckoutSessionBaseUrl(): string + { + return Stripe::$apiBase.Session::classUrl(); + } } diff --git a/tests/Behat/Mocker/Api/PaymentIntentMocker.php b/tests/Mocker/Api/PaymentIntentMocker.php similarity index 98% rename from tests/Behat/Mocker/Api/PaymentIntentMocker.php rename to tests/Mocker/Api/PaymentIntentMocker.php index 08c08a3..69d7e12 100644 --- a/tests/Behat/Mocker/Api/PaymentIntentMocker.php +++ b/tests/Mocker/Api/PaymentIntentMocker.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\Api; +namespace Tests\FluxSE\SyliusStripePlugin\Mocker\Api; use Mockery\MockInterface; use Stripe\HttpClient\ClientInterface; diff --git a/tests/Behat/Mocker/Api/RefundMocker.php b/tests/Mocker/Api/RefundMocker.php similarity index 93% rename from tests/Behat/Mocker/Api/RefundMocker.php rename to tests/Mocker/Api/RefundMocker.php index 97c7f92..bd16edc 100644 --- a/tests/Behat/Mocker/Api/RefundMocker.php +++ b/tests/Mocker/Api/RefundMocker.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\Api; +namespace Tests\FluxSE\SyliusStripePlugin\Mocker\Api; use Mockery\MockInterface; use Stripe\HttpClient\ClientInterface; diff --git a/tests/Behat/Mocker/StripeCheckoutMocker.php b/tests/Mocker/StripeCheckoutMocker.php similarity index 83% rename from tests/Behat/Mocker/StripeCheckoutMocker.php rename to tests/Mocker/StripeCheckoutMocker.php index 1d2f920..d56469f 100644 --- a/tests/Behat/Mocker/StripeCheckoutMocker.php +++ b/tests/Mocker/StripeCheckoutMocker.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace Tests\FluxSE\SyliusStripePlugin\Behat\Mocker; +namespace Tests\FluxSE\SyliusStripePlugin\Mocker; use Mockery\MockInterface; use Stripe\Checkout\Session; use Stripe\HttpClient\ClientInterface; use Stripe\PaymentIntent; -use Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\Api\CheckoutSessionMocker; -use Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\Api\PaymentIntentMocker; -use Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\Api\RefundMocker; +use Tests\FluxSE\SyliusStripePlugin\Mocker\Api\CheckoutSessionMocker; +use Tests\FluxSE\SyliusStripePlugin\Mocker\Api\PaymentIntentMocker; +use Tests\FluxSE\SyliusStripePlugin\Mocker\Api\RefundMocker; final class StripeCheckoutMocker { @@ -24,6 +24,8 @@ public function __construct( public function mockCaptureOrAuthorize(callable $action): void { + $this->mockClient->expects([]); + $this->checkoutSessionMocker->mockCreateAction(); $this->mockSessionSync( @@ -38,9 +40,11 @@ public function mockCancelPayment(string $status, string $captureMethod): void { $this->mockClient->expects([]); - $this->paymentIntentMocker->mockUpdateAction($status, $captureMethod); - $this->paymentIntentMocker->mockCancelAction($captureMethod); - $this->paymentIntentMocker->mockRetrieveAction(PaymentIntent::STATUS_CANCELED); + $this->checkoutSessionMocker->mockRetrieveAction( + Session::STATUS_COMPLETE, + Session::PAYMENT_STATUS_PAID, + ); + $this->checkoutSessionMocker->mockExpireAction(); } public function mockRefundPayment(): void @@ -50,13 +54,20 @@ public function mockRefundPayment(): void $this->refundMocker->mockCreateAction(); } - public function mockExpirePayment(): void + public function mockExpirePayment(string $status, string $paymentStatus): void { $this->mockClient->expects([]); + $this->checkoutSessionMocker->mockRetrieveAction( + $status, + $paymentStatus, + ); + + if ($status !== Session::STATUS_OPEN) { + return; + } + $this->checkoutSessionMocker->mockExpireAction(); - $this->checkoutSessionMocker->mockRetrieveAction(Session::STATUS_EXPIRED, Session::PAYMENT_STATUS_UNPAID); - $this->paymentIntentMocker->mockRetrieveAction(PaymentIntent::STATUS_CANCELED); } public function mockCaptureAuthorization(string $status, string $captureMethod): void diff --git a/tests/Mocker/StripeClientMocker.php b/tests/Mocker/StripeClientMocker.php new file mode 100644 index 0000000..8637e8f --- /dev/null +++ b/tests/Mocker/StripeClientMocker.php @@ -0,0 +1,22 @@ + $className + */ + public function __invoke(string $className): MockInterface + { + $mock = Mockery::fetchMock('stripe_client'); + + return $mock ?? Mockery::namedMock('stripe_client', $className); + } +} diff --git a/tests/Behat/Mocker/StripeWebElementsMocker.php b/tests/Mocker/StripeWebElementsMocker.php similarity index 94% rename from tests/Behat/Mocker/StripeWebElementsMocker.php rename to tests/Mocker/StripeWebElementsMocker.php index a1d7b7b..c3db613 100644 --- a/tests/Behat/Mocker/StripeWebElementsMocker.php +++ b/tests/Mocker/StripeWebElementsMocker.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Tests\FluxSE\SyliusStripePlugin\Behat\Mocker; +namespace Tests\FluxSE\SyliusStripePlugin\Mocker; use Mockery\MockInterface; use Stripe\HttpClient\ClientInterface; use Stripe\PaymentIntent; -use Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\Api\PaymentIntentMocker; -use Tests\FluxSE\SyliusStripePlugin\Behat\Mocker\Api\RefundMocker; +use Tests\FluxSE\SyliusStripePlugin\Mocker\Api\PaymentIntentMocker; +use Tests\FluxSE\SyliusStripePlugin\Mocker\Api\RefundMocker; final class StripeWebElementsMocker { diff --git a/tests/Provider/Checkout/Create/DetailsProviderTest.php b/tests/Provider/Checkout/Create/DetailsProviderTest.php new file mode 100644 index 0000000..867817d --- /dev/null +++ b/tests/Provider/Checkout/Create/DetailsProviderTest.php @@ -0,0 +1,170 @@ +loader = static::getContainer()->get('fidry_alice_data_fixtures.loader.doctrine'); + $this->entityManager = static::getContainer()->get('doctrine.orm.entity_manager'); + + $this->compositeParamsProvider = static::getContainer()->get('flux_se.sylius_stripe.provider.checkout.create.checkout_session_params'); + + $this->requestContext = static::getContainer()->get('router.request_context'); + + $this->purgeDatabase(); + } + + /** + * @param string[] $files + * @return object[] + */ + protected function loadFixtures(array $files): array + { + foreach ($files as $i=>$file) { + $files[$i] = sprintf('%s/../DataFixtures/ORM/%s', static::$kernel->getProjectDir(), $file); + } + + return $this->loader->load($files); + } + + protected function purgeDatabase(): void + { + $purger = new ORMPurger($this->entityManager); + $purger->purge(); + + $this->entityManager->clear(); + } + + /** + * @dataProvider getPaymentRequestAndExpectedDetails + */ + public function test_it_get_checkout_session_create_details( + string $paymentRequestName, + array $expectedDetails + ): void { + $fixtures = $this->loadFixtures([ + 'channel.yaml', + 'customer.yaml', + 'payment.yaml', + 'payment_method.yaml', + 'product_variant.yaml', + 'shipping_category.yaml', + 'tax_category.yaml', + 'shipping_method.yaml', + 'order_awaiting_payment.yaml', + 'stripe_checkout/payment_request.yaml', + ]); + + /** @var PaymentRequestInterface $paymentRequest */ + $paymentRequest = $fixtures[$paymentRequestName]; + + $expectedDetails['metadata']['token_hash'] = $paymentRequest->getId(); + if (null === $paymentRequest->getPayload()) { + // Using Shop UI, the locale context is given by the current request context, here we forced it. + $locale = 'en_US'; + $this->requestContext->setParameter('_locale', $locale); + + $url = sprintf('http://localhost/%s/order/after-pay/%s', $locale, $paymentRequest->getId()); + $expectedDetails['success_url'] = $url; + $expectedDetails['cancel_url'] = $url; + } + + $details = $this->compositeParamsProvider->getParams($paymentRequest); + + + self::assertEquals($expectedDetails, $details); + + // Check if tests data are corresponding + self::assertEquals( + $paymentRequest->getPayment()->getAmount(), + $paymentRequest->getPayment()->getOrder()->getTotal() + ); + } + + public static function getPaymentRequestAndExpectedDetails(): iterable + { + $expected = [ + 'customer_email' => 'oliver@doe.com', + 'line_items' => [ + [ + 'price_data' => [ + 'unit_amount' => 1000, + 'currency' => 'USD', + 'product_data' => [ + 'name' => '1x - Mug', + 'images' => [ + 'https://placehold.co/300', + ] + ] + ], + 'quantity' => 1, + ], + [ + 'price_data' => [ + 'unit_amount' => 500, + 'currency' => 'USD', + 'product_data' => [ + 'name' => 'UPS', + ] + ], + 'quantity' => 1, + ], + ], + 'mode' => 'payment', + 'success_url' => 'https://myshop.tld/target-path', + 'cancel_url' => 'https://myshop.tld/after-path', + 'metadata' => [ + 'token_hash' => '', + ], + ]; + + yield 'capture' => [ + 'payment_request_capture', + $expected, + ]; + + yield 'capture_via_api' => [ + 'payment_request_capture_via_api', + array_merge($expected, [ + 'success_url' => 'https://myshop.tld/target-path', + 'cancel_url' => 'https://myshop.tld/after-path', + ]), + ]; + + yield 'authorize' => [ + 'payment_request_authorize', + array_merge($expected, [ + 'payment_intent_data' => [ + 'capture_method' => 'manual', + ], + ]), + ]; + + yield 'authorize_via_api' => [ + 'payment_request_authorize_via_api', + array_merge($expected, [ + 'success_url' => 'https://myshop.tld/target-path', + 'cancel_url' => 'https://myshop.tld/after-path', + 'payment_intent_data' => [ + 'capture_method' => 'manual', + ], + ]), + ]; + } +} diff --git a/tests/Provider/WebElements/Create/DetailsProviderTest.php b/tests/Provider/WebElements/Create/DetailsProviderTest.php new file mode 100644 index 0000000..4001d32 --- /dev/null +++ b/tests/Provider/WebElements/Create/DetailsProviderTest.php @@ -0,0 +1,103 @@ +loader = static::getContainer()->get('fidry_alice_data_fixtures.loader.doctrine'); + $this->entityManager = static::getContainer()->get('doctrine.orm.entity_manager'); + + $this->compositeParamsProvider = static::getContainer()->get('flux_se.sylius_stripe.provider.web_elements.create.payment_intent_params'); + + $this->purgeDatabase(); + } + + protected function purgeDatabase(): void + { + $purger = new ORMPurger($this->entityManager); + $purger->purge(); + + $this->entityManager->clear(); + } + + /** + * @param string[] $files + * @return object[] + */ + protected function loadFixtures(array $files): array + { + foreach ($files as $i=>$file) { + $files[$i] = sprintf('%s/../DataFixtures/ORM/%s', static::$kernel->getProjectDir(), $file); + } + + return $this->loader->load($files); + } + + /** + * @dataProvider getPaymentRequestAndExpectedDetails + */ + public function test_it_get_checkout_session_create_details( + string $paymentRequestName, + array $expectedDetails + ): void { + $fixtures = $this->loadFixtures([ + 'channel.yaml', + 'customer.yaml', + 'payment.yaml', + 'payment_method.yaml', + 'product_variant.yaml', + 'shipping_category.yaml', + 'tax_category.yaml', + 'shipping_method.yaml', + 'order_awaiting_payment.yaml', + 'stripe_web_elements/payment_request.yaml', + ]); + + /** @var PaymentRequestInterface $paymentRequest */ + $paymentRequest = $fixtures[$paymentRequestName]; + + $params = $this->compositeParamsProvider->getParams($paymentRequest); + + $expectedDetails['metadata']['token_hash'] = $paymentRequest->getId(); + + self::assertEquals($expectedDetails, $params); + } + + public static function getPaymentRequestAndExpectedDetails(): iterable + { + $expected = [ + 'amount' => 1500, + 'currency' => 'USD', + 'metadata' => [ + 'token_hash' => '', + ], + ]; + + yield 'capture' => [ + 'payment_request_capture', + $expected, + ]; + + yield 'authorize' => [ + 'payment_request_authorize', + array_merge($expected, [ + 'capture_method' => 'manual', + ]), + ]; + } +} diff --git a/tests/Responses/shop/payment_request/post_payment_request_payment_method_stripe_checkout.json b/tests/Responses/shop/payment_request/post_payment_request_payment_method_stripe_checkout.json new file mode 100644 index 0000000..ae28428 --- /dev/null +++ b/tests/Responses/shop/payment_request/post_payment_request_payment_method_stripe_checkout.json @@ -0,0 +1,19 @@ +{ + "@context": "\/api\/v2\/contexts\/PaymentRequest", + "@id": "\/api\/v2\/shop\/payment-requests\/@string@", + "@type": "PaymentRequest", + "hash": "@string@", + "state": "processing", + "action": "capture", + "payload": { + "success_url": "https:\/\/myshop.tld\/target-path", + "cancel_url": "https:\/\/myshop.tld\/after-path" + }, + "responseData": { + "url": "https:\/\/checkout.stripe.com\/c\/pay\/cs_test_1" + }, + "payment": "\/api\/v2\/shop\/orders\/@string@\/payments\/@integer@", + "method": "\/api\/v2\/shop\/payment-methods\/STRIPE_CHECKOUT", + "createdAt": "@datetime@", + "updatedAt": "@datetime@" +} diff --git a/tests/Responses/shop/payment_request/post_payment_request_payment_method_stripe_checkout_authorize.json b/tests/Responses/shop/payment_request/post_payment_request_payment_method_stripe_checkout_authorize.json new file mode 100644 index 0000000..47351ea --- /dev/null +++ b/tests/Responses/shop/payment_request/post_payment_request_payment_method_stripe_checkout_authorize.json @@ -0,0 +1,19 @@ +{ + "@context": "\/api\/v2\/contexts\/PaymentRequest", + "@id": "\/api\/v2\/shop\/payment-requests\/@string@", + "@type": "PaymentRequest", + "hash": "@string@", + "state": "processing", + "action": "authorize", + "payload": { + "success_url": "https:\/\/myshop.tld\/target-path", + "cancel_url": "https:\/\/myshop.tld\/after-path" + }, + "responseData": { + "url": "https:\/\/checkout.stripe.com\/c\/pay\/cs_test_1" + }, + "payment": "\/api\/v2\/shop\/orders\/@string@\/payments\/@integer@", + "method": "\/api\/v2\/shop\/payment-methods\/STRIPE_CHECKOUT_AUTHORIZE", + "createdAt": "@datetime@", + "updatedAt": "@datetime@" +} diff --git a/tests/Responses/shop/payment_request/post_payment_request_with_error.json b/tests/Responses/shop/payment_request/post_payment_request_with_error.json new file mode 100644 index 0000000..a543468 --- /dev/null +++ b/tests/Responses/shop/payment_request/post_payment_request_with_error.json @@ -0,0 +1,24 @@ +{ + "@context": "\/api\/v2\/contexts\/ConstraintViolationList", + "@id": "\/api\/v2\/validation_errors\/@string@", + "@type": "ConstraintViolationList", + "status": 422, + "violations": [ + { + "propertyPath": "payload", + "message": "The payload must contain a \u0022success_url\u0022 array property to be able to redirect the visitor after the Stripe Checkout Session portal completion.", + "code": null + }, + { + "propertyPath": "payload", + "message": "The payload must contain a \u0022cancel_url\u0022 array property to be able to redirect the visitor after the Stripe Checkout Session portal completion.", + "code": null + } + ], + "detail": "payload: The payload must contain a \u0022success_url\u0022 array property to be able to redirect the visitor after the Stripe Checkout Session portal completion.\npayload: The payload must contain a \u0022cancel_url\u0022 array property to be able to redirect the visitor after the Stripe Checkout Session portal completion.", + "description": "payload: The payload must contain a \u0022success_url\u0022 array property to be able to redirect the visitor after the Stripe Checkout Session portal completion.\npayload: The payload must contain a \u0022cancel_url\u0022 array property to be able to redirect the visitor after the Stripe Checkout Session portal completion.", + "type": "\/validation_errors\/@string@", + "title": "An error occurred", + "hydra:description": "payload: The payload must contain a \u0022success_url\u0022 array property to be able to redirect the visitor after the Stripe Checkout Session portal completion.\npayload: The payload must contain a \u0022cancel_url\u0022 array property to be able to redirect the visitor after the Stripe Checkout Session portal completion.", + "hydra:title": "An error occurred" +}