diff --git a/access-functions.php b/access-functions.php index a67a869d..d86529e7 100644 --- a/access-functions.php +++ b/access-functions.php @@ -19,8 +19,7 @@ if ( ! function_exists( 'str_starts_with' ) ) { * @return bool - True if $haystack starts with $needle, false otherwise. */ function str_starts_with( $haystack, $needle ) { - $length = strlen( $needle ); - return ( substr( $haystack, 0, $length ) === $needle ); + return 0 === strpos( $haystack, $needle ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.str_starts_with } } @@ -38,11 +37,8 @@ if ( ! function_exists( 'str_ends_with' ) ) { */ function str_ends_with( $haystack, $needle ) { $length = strlen( $needle ); - if ( 0 === $length ) { - return true; - } - - return ( substr( $haystack, -$length ) === $needle ); + return 0 === $length + || strpos( $haystack, $needle, - $length ) === $length - 1; } }//end if diff --git a/codeception.dist.yml b/codeception.dist.yml index 0935e056..283cbaf9 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -66,11 +66,11 @@ modules: uploads: '/wp-content/uploads' WPLoader: wpRootFolder: '%WP_CORE_DIR%' - dbUrl: 'mysql://%DB_USER%:%DB_PASSWORD%@%DB_HOST%:%DB_PORT%/%DB_NAME%' - dbName: '%DB_NAME%' dbHost: '%DB_HOST%' + dbName: '%DB_NAME%' dbUser: '%DB_USER%' dbPassword: '%DB_PASSWORD%' + dbUrl: 'mysql://%DB_USER%:%DB_PASSWORD%@%DB_HOST%:%DB_PORT%/%DB_NAME%' tablePrefix: '%WP_TABLE_PREFIX%' domain: '%WORDPRESS_DOMAIN%' adminEmail: '%ADMIN_EMAIL%' diff --git a/includes/admin/class-general.php b/includes/admin/class-general.php index ded29c5b..eb27e307 100644 --- a/includes/admin/class-general.php +++ b/includes/admin/class-general.php @@ -77,6 +77,24 @@ class General extends Section { 'value' => defined( 'NO_QL_SESSION_HANDLER' ) ? 'on' : woographql_setting( 'disable_ql_session_handler', 'off' ), 'disabled' => defined( 'NO_QL_SESSION_HANDLER' ), ], + [ + 'name' => 'enable_ql_session_handler_on_ajax', + 'label' => __( 'Enable QL Session Handler on WC AJAX requests.', 'wp-graphql-woocommerce' ), + 'desc' => __( 'Enabling this will enable JSON Web Tokens usage on WC AJAX requests.', 'wp-graphql-woocommerce' ) + . ( defined( 'NO_QL_SESSION_HANDLER' ) ? __( ' This setting is disabled. The "NO_QL_SESSION_HANDLER" flag has been triggered with code', 'wp-graphql-woocommerce' ) : '' ), + 'type' => 'checkbox', + 'value' => defined( 'NO_QL_SESSION_HANDLER' ) ? 'off' : woographql_setting( 'enable_ql_session_handler_on_ajax', 'off' ), + 'disabled' => defined( 'NO_QL_SESSION_HANDLER' ), + ], + [ + 'name' => 'enable_ql_session_handler_on_rest', + 'label' => __( 'Enable QL Session Handler on WP REST requests.', 'wp-graphql-woocommerce' ), + 'desc' => __( 'Enabling this will enable JSON Web Tokens usage on WP REST requests.', 'wp-graphql-woocommerce' ) + . ( defined( 'NO_QL_SESSION_HANDLER' ) ? __( ' This setting is disabled. The "NO_QL_SESSION_HANDLER" flag has been triggered with code', 'wp-graphql-woocommerce' ) : '' ), + 'type' => 'checkbox', + 'value' => defined( 'NO_QL_SESSION_HANDLER' ) ? 'off' : woographql_setting( 'enable_ql_session_handler_on_rest', 'off' ), + 'disabled' => defined( 'NO_QL_SESSION_HANDLER' ), + ], [ 'name' => 'enable_unsupported_product_type', 'label' => __( 'Enable Unsupported types', 'wp-graphql-woocommerce' ), diff --git a/includes/class-type-registry.php b/includes/class-type-registry.php index 72628eea..e7c41db8 100644 --- a/includes/class-type-registry.php +++ b/includes/class-type-registry.php @@ -199,6 +199,7 @@ class Type_Registry { Mutation\Tax_Rate_Create::register_mutation(); Mutation\Tax_Rate_Delete::register_mutation(); Mutation\Tax_Rate_Update::register_mutation(); - Mutation\Update_Session::register_mutation(); + Mutation\Session_Delete::register_mutation(); + Mutation\Session_Update::register_mutation(); } } diff --git a/includes/class-woocommerce-filters.php b/includes/class-woocommerce-filters.php index a8b85321..2a26c878 100644 --- a/includes/class-woocommerce-filters.php +++ b/includes/class-woocommerce-filters.php @@ -82,6 +82,26 @@ class WooCommerce_Filters { return woographql_setting( "{$field}_nonce_param", null ); } + /** + * Returns true if the session handler should be loaded. + * + * @return boolean + */ + public static function should_load_session_handler() { + switch ( true ) { + case \WPGraphQL\Router::is_graphql_http_request(): + //phpcs:disable + case 'on' === woographql_setting( 'enable_ql_session_handler_on_ajax', 'off' ) + && ( ! empty( $_GET['wc-ajax'] ) || defined( 'WC_DOING_AJAX' ) ): + //phpcs:enable + case 'on' === woographql_setting( 'enable_ql_session_handler_on_rest', 'off' ) + && ( defined( 'REST_REQUEST' ) && REST_REQUEST ): + return true; + default: + return false; + } + } + /** * WooCommerce Session Handler callback * @@ -89,7 +109,7 @@ class WooCommerce_Filters { * @return string */ public static function woocommerce_session_handler( $session_class ) { - if ( \WPGraphQL\Router::is_graphql_http_request() ) { + if ( self::should_load_session_handler() ) { $session_class = '\WPGraphQL\WooCommerce\Utils\QL_Session_Handler'; } elseif ( WooGraphQL::auth_router_is_enabled() ) { require_once get_includes_directory() . 'utils/class-protected-router.php'; diff --git a/includes/class-wp-graphql-woocommerce.php b/includes/class-wp-graphql-woocommerce.php index 6ae6c645..bc284a1b 100644 --- a/includes/class-wp-graphql-woocommerce.php +++ b/includes/class-wp-graphql-woocommerce.php @@ -343,6 +343,8 @@ if ( ! class_exists( '\WPGraphQL\WooCommerce\WP_GraphQL_WooCommerce' ) ) : require $include_directory_path . 'mutation/class-review-update.php'; require $include_directory_path . 'mutation/class-payment-method-delete.php'; require $include_directory_path . 'mutation/class-payment-method-set-default.php'; + require $include_directory_path . 'mutation/class-session-delete.php'; + require $include_directory_path . 'mutation/class-session-update.php'; require $include_directory_path . 'mutation/class-shipping-zone-create.php'; require $include_directory_path . 'mutation/class-shipping-zone-delete.php'; require $include_directory_path . 'mutation/class-shipping-zone-locations-clear.php'; @@ -356,7 +358,6 @@ if ( ! class_exists( '\WPGraphQL\WooCommerce\WP_GraphQL_WooCommerce' ) ) : require $include_directory_path . 'mutation/class-tax-rate-create.php'; require $include_directory_path . 'mutation/class-tax-rate-delete.php'; require $include_directory_path . 'mutation/class-tax-rate-update.php'; - require $include_directory_path . 'mutation/class-update-session.php'; // Include connection class/function files. require $include_directory_path . 'connection/wc-cpt-connection-args.php'; diff --git a/includes/mutation/class-cart-add-fee.php b/includes/mutation/class-cart-add-fee.php index 41a08b39..a986eb79 100644 --- a/includes/mutation/class-cart-add-fee.php +++ b/includes/mutation/class-cart-add-fee.php @@ -106,6 +106,8 @@ class Cart_Add_Fee { // Add cart fee. \WC()->cart->add_fee( ...$cart_fee_args ); + do_action( 'woographql_update_session', true ); + // Return payload. return [ 'id' => \sanitize_title( $input['name'] ) ]; }; diff --git a/includes/mutation/class-cart-apply-coupon.php b/includes/mutation/class-cart-apply-coupon.php index dbd12a17..b136d41d 100644 --- a/includes/mutation/class-cart-apply-coupon.php +++ b/includes/mutation/class-cart-apply-coupon.php @@ -76,6 +76,8 @@ class Cart_Apply_Coupon { $reason = ''; // If validate and successful applied to cart, return payload. if ( Cart_Mutation::validate_coupon( $input['code'], $reason ) && \WC()->cart->apply_coupon( $input['code'] ) ) { + do_action( 'woographql_update_session', true ); + return [ 'code' => $input['code'] ]; } diff --git a/includes/mutation/class-cart-empty.php b/includes/mutation/class-cart-empty.php index 5aef5464..3d634d40 100644 --- a/includes/mutation/class-cart-empty.php +++ b/includes/mutation/class-cart-empty.php @@ -94,6 +94,8 @@ class Cart_Empty { */ do_action( 'graphql_woocommerce_after_empty_cart', $cloned_cart, $input, $context, $info ); + do_action( 'woographql_update_session', true ); + return [ 'cart' => $cloned_cart ]; }; } diff --git a/includes/mutation/class-cart-fill.php b/includes/mutation/class-cart-fill.php index ae767bce..9df21193 100644 --- a/includes/mutation/class-cart-fill.php +++ b/includes/mutation/class-cart-fill.php @@ -270,6 +270,8 @@ class Cart_Fill { // Recalculate totals. \WC()->cart->calculate_totals(); + do_action( 'woographql_update_session', true ); + // Return payload. return compact( 'added', diff --git a/includes/mutation/class-cart-remove-coupons.php b/includes/mutation/class-cart-remove-coupons.php index 9ebf2813..6be66f5f 100644 --- a/includes/mutation/class-cart-remove-coupons.php +++ b/includes/mutation/class-cart-remove-coupons.php @@ -86,6 +86,8 @@ class Cart_Remove_Coupons { } } + do_action( 'woographql_update_session', true ); + // Return payload. return [ 'cart' => \WC()->cart ]; }; diff --git a/includes/mutation/class-cart-remove-items.php b/includes/mutation/class-cart-remove-items.php index 0a0b265d..1c03dd39 100644 --- a/includes/mutation/class-cart-remove-items.php +++ b/includes/mutation/class-cart-remove-items.php @@ -96,6 +96,8 @@ class Cart_Remove_Items { } } + do_action( 'woographql_update_session', true ); + // Return payload. return [ 'items' => $cart_items ]; }; diff --git a/includes/mutation/class-cart-restore-items.php b/includes/mutation/class-cart-restore-items.php index 2f6979a1..aa57476a 100644 --- a/includes/mutation/class-cart-restore-items.php +++ b/includes/mutation/class-cart-restore-items.php @@ -82,6 +82,8 @@ class Cart_Restore_Items { $cart_items = Cart_Mutation::retrieve_cart_items( $input, $context, $info, 'restore' ); + do_action( 'woographql_update_session', true ); + // Return payload. return [ 'items' => $cart_items ]; }; diff --git a/includes/mutation/class-cart-update-item-quantities.php b/includes/mutation/class-cart-update-item-quantities.php index 1f455b48..a8521998 100644 --- a/includes/mutation/class-cart-update-item-quantities.php +++ b/includes/mutation/class-cart-update-item-quantities.php @@ -165,6 +165,8 @@ class Cart_Update_Item_Quantities { $info ); + do_action( 'woographql_update_session', true ); + return [ 'removed' => $removed_items, 'updated' => array_keys( $updated ), diff --git a/includes/mutation/class-cart-update-shipping-method.php b/includes/mutation/class-cart-update-shipping-method.php index cf44f0db..4a897a8d 100644 --- a/includes/mutation/class-cart-update-shipping-method.php +++ b/includes/mutation/class-cart-update-shipping-method.php @@ -78,6 +78,8 @@ class Cart_Update_Shipping_Method { // Recalculate totals. \WC()->cart->calculate_totals(); + do_action( 'woographql_update_session', true ); + return []; }; } diff --git a/includes/mutation/class-customer-update.php b/includes/mutation/class-customer-update.php index 48466cd2..f7e49f65 100644 --- a/includes/mutation/class-customer-update.php +++ b/includes/mutation/class-customer-update.php @@ -153,6 +153,10 @@ class Customer_Update { // Save customer and get customer ID. $customer->save(); + if ( $session_only ) { + do_action( 'woographql_update_session', true ); + } + // Return payload. return ! empty( $payload ) ? $payload : [ 'id' => 'session' ]; }; diff --git a/includes/mutation/class-session-delete.php b/includes/mutation/class-session-delete.php new file mode 100644 index 00000000..cceafab2 --- /dev/null +++ b/includes/mutation/class-session-delete.php @@ -0,0 +1,95 @@ + [], + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'session' => [ + 'type' => [ 'list_of' => 'MetaData' ], + 'resolve' => static function ( $payload ) { + // Guard against missing session data. + if ( empty( $payload['session'] ) ) { + return []; + } + + // Prepare session data. + $session = []; + foreach ( $payload['session'] as $key => $value ) { + $meta = new \stdClass(); + $meta->id = null; + $meta->key = $key; + $meta->value = maybe_unserialize( $value ); + $session[] = $meta; + } + + return $session; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input ) { + Cart_Mutation::check_session_token(); + + /** + * Session handler. + * + * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session + */ + $session = \WC()->session; + + // Get session data. + $session_data = $session->get_session_data(); + do_action( 'woographql_before_forget_session', $session_data, $input, $session ); + + // Clear session data. + $session->forget_session(); + + do_action( 'woographql_after_forget_session', $session_data, $input, $session ); + + // Return payload. + return [ 'session' => $session_data ]; + }; + } +} diff --git a/includes/mutation/class-update-session.php b/includes/mutation/class-session-update.php similarity index 98% rename from includes/mutation/class-update-session.php rename to includes/mutation/class-session-update.php index b335c972..a1040be6 100644 --- a/includes/mutation/class-update-session.php +++ b/includes/mutation/class-session-update.php @@ -15,9 +15,9 @@ use WPGraphQL\WooCommerce\Data\Mutation\Cart_Mutation; use WPGraphQL\WooCommerce\Model\Customer; /** - * Class - Update_Session + * Class - Session_Update */ -class Update_Session { +class Session_Update { /** * Registers mutation * diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php index 761e214c..490a41c6 100644 --- a/includes/utils/class-ql-session-handler.php +++ b/includes/utils/class-ql-session-handler.php @@ -9,6 +9,7 @@ namespace WPGraphQL\WooCommerce\Utils; use WC_Session_Handler; +use WPGraphQL\Router; use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\JWT; use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\Key; @@ -48,12 +49,20 @@ class QL_Session_Handler extends WC_Session_Handler { */ protected $_issuing_new_token = false; // @codingStandardsIgnoreLine + /** + * True when a new session cookie has been issued. + * + * @var bool $_issuing_new_cookie + */ + protected $_issuing_new_cookie = false; // @codingStandardsIgnoreLine + /** * Constructor for the session class. */ public function __construct() { + parent::__construct(); + $this->_token = apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' ); - $this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions'; } /** @@ -101,20 +110,35 @@ class QL_Session_Handler extends WC_Session_Handler { Session_Transaction_Manager::get( $this ); /** - * Necessary since Session_Transaction_Manager applies to the reference. + * Necessary since Session_Transaction_Manager applies to the reference. * * @var self $this */ - add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_token' ], 10 ); - add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 ); - add_action( 'shutdown', [ $this, 'save_data' ] ); - add_action( 'wp_logout', [ $this, 'destroy_session' ] ); - - if ( ! is_user_logged_in() ) { - add_filter( 'nonce_user_logged_out', [ $this, 'maybe_update_nonce_user_logged_out' ], 10, 2 ); + if ( Router::is_graphql_http_request() ) { + add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_token' ], 10 ); + add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 ); + add_action( 'shutdown', [ $this, 'save_data' ] ); + } else { + add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_cookie' ], 10 ); + add_action( 'shutdown', [ $this, 'save_data' ], 20 ); + add_action( 'wp_logout', [ $this, 'destroy_session' ] ); + if ( ! is_user_logged_in() ) { + add_filter( 'nonce_user_logged_out', [ $this, 'maybe_update_nonce_user_logged_out' ], 10, 2 ); + } } } + /** + * Mark the session as dirty. + * + * To trigger a save of the session data. + * + * @return void + */ + public function mark_dirty() { + $this->_dirty = true; + } + /** * Setup token and customer ID. * @@ -123,6 +147,10 @@ class QL_Session_Handler extends WC_Session_Handler { * @return void */ public function init_session_token() { + + /** + * @var object{ iat: int, exp: int, data: object{ customer_id: string } }|false|\WP_Error $token + */ $token = $this->get_session_token(); // Process existing session if not expired or invalid. @@ -147,7 +175,9 @@ class QL_Session_Handler extends WC_Session_Handler { // @phpstan-ignore-next-line $this->save_data( $guest_session_id ); - $this->set_customer_session_token( true ); + Router::is_graphql_http_request() + ? $this->set_customer_session_token( true ) + : $this->set_customer_session_cookie( true ); } // Update session expiration on each action. @@ -155,19 +185,23 @@ class QL_Session_Handler extends WC_Session_Handler { if ( $token->exp < $this->_session_expiration ) { $this->update_session_timestamp( (string) $this->_customer_id, $this->_session_expiration ); } - } else { + } elseif ( is_wp_error( $token ) ) { + add_filter( + 'graphql_woocommerce_session_token_errors', + static function ( $errors ) use ( $token ) { + $errors = $token->get_error_message(); + return $errors; + } + ); + } - // If token invalid throw warning. - if ( is_wp_error( $token ) ) { - add_filter( - 'graphql_woocommerce_session_token_errors', - static function ( $errors ) use ( $token ) { - $errors = $token->get_error_message(); - return $errors; - } - ); - } + $start_new_session = ! $token || is_wp_error( $token ); + if ( ! $start_new_session ) { + return; + } + // Distribute new session token on GraphQL requests, otherwise distribute a new session cookie. + if ( Router::is_graphql_http_request() ) { // Start new session. $this->set_session_expiration(); @@ -175,7 +209,9 @@ class QL_Session_Handler extends WC_Session_Handler { $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id(); $this->_data = $this->get_session_data(); $this->set_customer_session_token( true ); - }//end if + } else { + $this->init_session_cookie(); + } } /** @@ -259,13 +295,31 @@ class QL_Session_Handler extends WC_Session_Handler { return apply_filters( 'graphql_woocommerce_cart_session_header', $session_header ); } + /** + * Determine if a JWT is being sent in the page response. + * + * @return bool + */ + public function sending_token() { + return $this->_has_token || $this->_issuing_new_token; + } + + /** + * Determine if a HTTP cookie is being sent in the page response. + * + * @return bool + */ + public function sending_cookie() { + return $this->_has_cookie || $this->_issuing_new_cookie; + } + /** * Creates JSON Web Token for customer session. * * @return false|string */ public function build_token() { - if ( empty( $this->_session_issued ) ) { + if ( empty( $this->_session_issued ) || ! $this->sending_token() ) { return false; } @@ -362,14 +416,28 @@ class QL_Session_Handler extends WC_Session_Handler { } } + /** + * {@inheritDoc} + * + * @return void + */ + public function set_customer_session_cookie( $set ) { + parent::set_customer_session_cookie( $set ); + + if ( $set ) { + $this->_issuing_new_cookie = true; + } + } + /** * Return true if the current user has an active session, i.e. a cookie to retrieve values. * * @return bool */ public function has_session() { + // @codingStandardsIgnoreLine. - return $this->_issuing_new_token || $this->_has_token || is_user_logged_in(); + return $this->_issuing_new_token || $this->_has_token || parent::has_session(); } /** @@ -379,34 +447,16 @@ class QL_Session_Handler extends WC_Session_Handler { */ public function set_session_expiration() { $this->_session_issued = time(); - // 14 Days. - $this->_session_expiration = apply_filters( + // 47 hours. + $this->_session_expiring = apply_filters( 'wc_session_expiring', $this->_session_issued + ( 60 * 60 * 47 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + // 48 hours. + $this->_session_expiration = apply_filters( 'wc_session_expiration', $this->_session_issued + ( 60 * 60 * 48 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + $this->_session_expiration = apply_filters_deprecated( 'graphql_woocommerce_cart_session_expire', - // Seconds * Minutes * Hours * Days. - $this->_session_issued + ( 60 * 60 * 24 * 14 ) + [ $this->_session_expiration ], + 'TBD', + 'wc_session_expiration' ); - // 13 Days. - $this->_session_expiring = $this->_session_expiration - ( 60 * 60 * 24 ); - } - - /** - * Forget all session data without destroying it. - * - * @return void - */ - public function forget_session() { - if ( isset( $this->_token_to_be_sent ) ) { - unset( $this->_token_to_be_sent ); - } - wc_empty_cart(); - $this->_data = []; - $this->_dirty = false; - - // Start new session. - $this->set_session_expiration(); - - // Get Customer ID. - $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id(); } /** @@ -444,17 +494,6 @@ class QL_Session_Handler extends WC_Session_Handler { } } - /** - * Noop for \WC_Session_Handler method. - * - * Prevents potential crticial errors when calling this method. - * - * @param bool $set Should the session cookie be set. - * - * @return void - */ - public function set_customer_session_cookie( $set ) {} - /** * Returns "client_session_id". "client_session_id_expiration" is used * to keep "client_session_id" as fresh as possible. diff --git a/includes/utils/class-session-transaction-manager.php b/includes/utils/class-session-transaction-manager.php index cc4fe9fb..be6b9d56 100644 --- a/includes/utils/class-session-transaction-manager.php +++ b/includes/utils/class-session-transaction-manager.php @@ -61,7 +61,14 @@ class Session_Transaction_Manager { add_action( 'graphql_before_resolve_field', [ $this, 'update_transaction_queue' ], 10, 4 ); add_action( 'graphql_mutation_response', [ $this, 'pop_transaction_id' ], 20, 6 ); + add_action( 'woographql_session_transaction_complete', [ $this->session_handler, 'save_if_dirty' ], 10 ); + + add_action( 'woocommerce_add_to_cart', [ $this->session_handler, 'mark_dirty' ] ); + add_action( 'woocommerce_cart_item_removed', [ $this->session_handler, 'mark_dirty' ] ); + add_action( 'woocommerce_cart_item_restored', [ $this->session_handler, 'mark_dirty' ] ); + add_action( 'woocommerce_cart_item_set_quantity', [ $this->session_handler, 'mark_dirty' ] ); + add_action( 'woocommerce_cart_emptied', [ $this->session_handler, 'mark_dirty' ] ); } /** @@ -100,6 +107,7 @@ class Session_Transaction_Manager { 'updateShippingMethod', 'updateCustomer', 'updateSession', + 'forgetSession', ] ); } diff --git a/tests/_support/Factory/CartFactory.php b/tests/_support/Factory/CartFactory.php index cde85bc7..15dc7bc3 100644 --- a/tests/_support/Factory/CartFactory.php +++ b/tests/_support/Factory/CartFactory.php @@ -15,9 +15,9 @@ class CartFactory { /** * Add products to the cart. * - * @param array ...$products Product to be added to the cart. + * @param array> ...$products Product to be added to the cart. * - * @return array + * @return array */ public function add( ...$products ) { $keys = []; diff --git a/tests/functional/CartTransactionQueueCest.php b/tests/functional/CartTransactionQueueCest.php index d6f730f7..9e3d26c6 100644 --- a/tests/functional/CartTransactionQueueCest.php +++ b/tests/functional/CartTransactionQueueCest.php @@ -96,12 +96,12 @@ class CartTransactionQueueCest { ); // Retrieve JWT Authorization Token for later use. - $auth_token = $I->lodashGet( $success, 'login.authToken' ); + $auth_token = $I->lodashGet( $success, 'data.login.authToken' ); // Retrieve session token. Add as "Session %s" in the woocommerce-session HTTP header to future requests // so WooCommerce can identify the user session associated with actions made in the GraphQL requests. // You can also retrieve the token from the "woocommerce-session" HTTP response header. - $initial_session_token = $I->lodashGet( $success, 'login.sessionToken' ); + $initial_session_token = $I->lodashGet( $success, 'data.login.sessionToken' ); $headers = [ 'Authorization' => "Bearer {$auth_token}", diff --git a/tests/wpunit/QLSessionHandlerTest.php b/tests/wpunit/QLSessionHandlerTest.php index b9afbe51..93bb0eb8 100644 --- a/tests/wpunit/QLSessionHandlerTest.php +++ b/tests/wpunit/QLSessionHandlerTest.php @@ -12,8 +12,19 @@ if ( ! defined( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY' ) ) { } class QLSessionHandlerTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase { + + public function setUp(): void { + parent::setUp(); + + // before + unset( $_SERVER['HTTP_WOOCOMMERCE_SESSION'] ); + $customer_cookie_key = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH ); + wc_setcookie( $customer_cookie_key, 0, time() - HOUR_IN_SECONDS ); + unset( $_COOKIE[ $customer_cookie_key ] ); + } public function tearDown(): void { unset( $_SERVER ); + WC()->session->destroy_session(); // after parent::tearDown(); @@ -27,7 +38,10 @@ class QLSessionHandlerTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap $this->assertInstanceOf( QL_Session_Handler::class, $session ); } - public function test_init_session_token() { + public function test_init_on_graphql_request() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + // Create session handler. $session = new QL_Session_Handler(); @@ -35,7 +49,7 @@ class QLSessionHandlerTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap $this->assertFalse( $session->has_session(), 'Shouldn\'t have a session yet' ); // Initialize session. - $session->init_session_token(); + $session->init(); // Assert session has started. $this->assertTrue( $session->has_session(), 'Should have session.' ); @@ -51,16 +65,99 @@ class QLSessionHandlerTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap usleep( 1000000 ); // Initialize session token for next request. - $session->init_session_token(); + remove_action( 'woocommerce_set_cart_cookies', [ $session, 'set_customer_session_token' ] ); + remove_action( 'woographql_update_session', [ $session, 'set_customer_session_token' ] ); + remove_action( 'shutdown', [ $session, 'save_data' ] ); + + // Create new session handler. + $session = new QL_Session_Handler(); + $session->init(); $new_token = $session->build_token(); $decoded_new_token = JWT::decode( $new_token, new Key( GRAPHQL_WOOCOMMERCE_SECRET_KEY, 'HS256' ) ); // Assert new token is different than old token. $this->assertNotEquals( $old_token, $new_token, 'New token should not match token from last request.' ); $this->assertGreaterThan( $decoded_old_token->exp, $decoded_new_token->exp ); + + // Assert customer ID match + $this->assertEquals( $decoded_old_token->data->customer_id, $decoded_new_token->data->customer_id ); + } + + public function test_init_on_non_graphql_request() { + // Create session handler. + $session = new QL_Session_Handler(); + + // Assert session hasn't started. + $this->assertFalse( $session->has_session(), 'Shouldn\'t have a session yet' ); + + // Initialize session. + $session->init(); + + // Add product to cart and start the session. + $this->factory->cart->add( + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + ); + + + // Assert session has started. + //$this->assertTrue( $session->sending_cookie(), 'Issuing new customer session cookie.' ); + + // Assert no tokens are being issued. + $this->assertFalse( $session->sending_token(), 'Should not be issuing a new customer token.' ); + $this->assertFalse( $session->build_token(), 'Should not be issuing a new customer token.' ); + } + + public function test_init_on_non_graphql_request_with_session_token() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + + // Create session handler. + $session = new QL_Session_Handler(); + + // Assert session hasn't started. + $this->assertFalse( $session->has_session(), 'Shouldn\'t have a session yet' ); + + // Initialize session. + $session->init(); + + // Assert session has started. + $this->assertTrue( $session->has_session(), 'Should have session.' ); + + // Get token for future request. + $token_to_session = $session->build_token(); + + // Remove GraphQL HTTP Request filter to simulate normal request. + remove_filter( 'graphql_is_graphql_http_request', '__return_true' ); + + // Sent token to HTTP header to simulate a new request. + $_SERVER['HTTP_WOOCOMMERCE_SESSION'] = 'Session ' . $token_to_session; + + // Create session handler. + $session = new QL_Session_Handler(); + + // Assert session hasn't started. + $this->assertFalse( $session->has_session(), 'Shouldn\'t have a session yet' ); + + // Initialize session. + $session->init(); + + // Assert session has started. + $this->assertTrue( $session->has_session(), 'Should have session.' ); + + // Assert tokens are being issued. + $this->assertNotFalse( $session->build_token() ); } public function test_get_session_token() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + // Create session handler. $session = new QL_Session_Handler(); @@ -96,6 +193,9 @@ class QLSessionHandlerTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap } public function test_build_token() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + // Create session handler. $session = new QL_Session_Handler(); @@ -116,6 +216,9 @@ class QLSessionHandlerTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap } public function test_set_customer_session_token() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + // Create session handler. $session = new QL_Session_Handler(); @@ -131,6 +234,9 @@ class QLSessionHandlerTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap } public function test_forget_session() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + // Create session handler. $session = new QL_Session_Handler(); $session->init_session_token(); diff --git a/tests/wpunit/UpdateSessionMutationTest.php b/tests/wpunit/SessionMutationsTest.php similarity index 50% rename from tests/wpunit/UpdateSessionMutationTest.php rename to tests/wpunit/SessionMutationsTest.php index bd48d401..b12d1ea7 100644 --- a/tests/wpunit/UpdateSessionMutationTest.php +++ b/tests/wpunit/SessionMutationsTest.php @@ -1,6 +1,6 @@ factory->customer->create(); @@ -62,4 +62,71 @@ class UpdateSessionMutationTest extends \Tests\WPGraphQL\WooCommerce\TestCase\Wo $this->assertQuerySuccessful( $response, $expected ); } -} + + public function testForgetSessionMutation() { + // Create registered customer. + $registered = $this->factory->customer->create(); + $this->loginAs( $registered ); + + // Add products to cart. + $this->factory->cart->add( + [ + 'product_id' => $this->factory->product->createSimple(), + 'quantity' => 2, + ], + [ + 'product_id' => $this->factory->product->createSimple(), + 'quantity' => 1, + ] + ); + + // Save session. + \WC()->session->save_data(); + + // Reinitialize session. + \WC()->session->init(); + + // Confirm cart has items. + $cart_query = ' + query { + cart { + contents { + nodes { + key + } + } + } + } + '; + + $response = $this->graphql( [ 'query' => $cart_query ] ); + $this->assertQuerySuccessful( + $response, + [ $this->expectedField( 'cart.contents.nodes', static::NOT_FALSY ) ] + ); + + // Forget session. + $query = 'mutation { + forgetSession(input: {}) { + session { + id + key + value + } + } + }'; + + $response = $this->graphql( compact( 'query' ) ); + $this->assertQuerySuccessful( $response ); + + // Reinitialize session. + \WC()->session->init(); + + // Confirm cart is empty. + $response = $this->graphql( [ 'query' => $cart_query ] ); + $this->assertQuerySuccessful( + $response, + [ $this->expectedField( 'cart.contents.nodes', static::IS_FALSY ) ] + ); + } +} \ No newline at end of file