diff --git a/.github/workflows/stalebot.yml b/.github/workflows/stalebot.yml
new file mode 100644
index 00000000000..b52a926ea49
--- /dev/null
+++ b/.github/workflows/stalebot.yml
@@ -0,0 +1,20 @@
+name: 'Close stale needs-feedback issues'
+on:
+ schedule:
+ - cron: '0 21 * * *'
+
+jobs:
+ stale:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/stale@v3
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ stale-issue-message: "As a part of this repository’s maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed."
+ close-issue-message: 'This issue was closed because it has been 14 days with no activity.'
+ days-before-issue-stale: 7
+ days-before-issue-close: 7
+ days-before-pr-close: -1
+ only-issue-labels: 'needs feedback'
+ close-issue-label: "category: can't reproduce"
+ debug-only: true
diff --git a/composer.json b/composer.json
index 4b3c4d352bd..385182966bc 100644
--- a/composer.json
+++ b/composer.json
@@ -14,7 +14,7 @@
],
"require": {
"php": ">=7.0",
- "automattic/jetpack-autoloader": "2.9.1",
+ "automattic/jetpack-autoloader": "2.10.1",
"automattic/jetpack-constants": "1.5.1",
"composer/installers": "~1.7",
"maxmind-db/reader": "1.6.0",
diff --git a/composer.lock b/composer.lock
index a5c1f0ab2ad..2eb76ff282e 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,32 +4,39 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "d0a0153dda851ca2c8e79bed1813ba5f",
+ "content-hash": "b1d6d94c8cfae572ab27c288c6865787",
"packages": [
{
"name": "automattic/jetpack-autoloader",
- "version": "v2.9.1",
+ "version": "2.10.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-autoloader.git",
- "reference": "d6ca2cc26ad6963e1be19b3338a9e98f40d9bd88"
+ "reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/d6ca2cc26ad6963e1be19b3338a9e98f40d9bd88",
- "reference": "d6ca2cc26ad6963e1be19b3338a9e98f40d9bd88",
+ "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/20393c4677765c3e737dcb5aee7a3f7b90dce4b3",
+ "reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.1 || ^2.0"
},
"require-dev": {
+ "automattic/jetpack-changelogger": "^1.1",
"yoast/phpunit-polyfills": "0.2.0"
},
"type": "composer-plugin",
"extra": {
"class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin",
- "mirror-repo": "Automattic/jetpack-autoloader"
+ "mirror-repo": "Automattic/jetpack-autoloader",
+ "changelogger": {
+ "link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}"
+ },
+ "branch-alias": {
+ "dev-master": "2.10.x-dev"
+ }
},
"autoload": {
"classmap": [
@@ -45,9 +52,9 @@
],
"description": "Creates a custom autoloader for a plugin or theme.",
"support": {
- "source": "https://github.com/Automattic/jetpack-autoloader/tree/v2.9.1"
+ "source": "https://github.com/Automattic/jetpack-autoloader/tree/2.10.1"
},
- "time": "2021-02-05T19:07:06+00:00"
+ "time": "2021-03-30T15:15:59+00:00"
},
{
"name": "automattic/jetpack-constants",
diff --git a/includes/abstracts/abstract-wc-settings-api.php b/includes/abstracts/abstract-wc-settings-api.php
index f33a62365fc..35bdbe522b7 100644
--- a/includes/abstracts/abstract-wc-settings-api.php
+++ b/includes/abstracts/abstract-wc-settings-api.php
@@ -696,6 +696,7 @@ abstract class WC_Settings_API {
);
$data = wp_parse_args( $data, $defaults );
+ $value = $this->get_option( $key );
ob_start();
?>
@@ -708,7 +709,15 @@ abstract class WC_Settings_API {
get_description_html( $data ); // WPCS: XSS ok. ?>
diff --git a/includes/class-wc-cart-session.php b/includes/class-wc-cart-session.php
index d8eb30ca8e9..50f2291a68c 100644
--- a/includes/class-wc-cart-session.php
+++ b/includes/class-wc-cart-session.php
@@ -175,6 +175,10 @@ final class WC_Cart_Session {
if ( $update_cart_session || is_null( WC()->session->get( 'cart_totals', null ) ) ) {
WC()->session->set( 'cart', $this->get_cart_for_session() );
$this->cart->calculate_totals();
+
+ if ( $merge_saved_cart ) {
+ $this->persistent_cart_update();
+ }
}
// If this is a re-order, redirect to the cart page to get rid of the `order_again` query string.
diff --git a/includes/class-wc-customer.php b/includes/class-wc-customer.php
index 3c5c53079f3..d66efff9d2c 100644
--- a/includes/class-wc-customer.php
+++ b/includes/class-wc-customer.php
@@ -449,7 +449,19 @@ class WC_Customer extends WC_Legacy_Customer {
* @return array
*/
public function get_billing( $context = 'view' ) {
- return $this->get_prop( 'billing', $context );
+ $value = null;
+ $prop = 'billing';
+
+ if ( array_key_exists( $prop, $this->data ) ) {
+ $changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array();
+ $value = array_merge( $this->data[ $prop ], $changes );
+
+ if ( 'view' === $context ) {
+ $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
+ }
+ }
+
+ return $value;
}
/**
@@ -580,7 +592,19 @@ class WC_Customer extends WC_Legacy_Customer {
* @return array
*/
public function get_shipping( $context = 'view' ) {
- return $this->get_prop( 'shipping', $context );
+ $value = null;
+ $prop = 'shipping';
+
+ if ( array_key_exists( $prop, $this->data ) ) {
+ $changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array();
+ $value = array_merge( $this->data[ $prop ], $changes );
+
+ if ( 'view' === $context ) {
+ $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
+ }
+ }
+
+ return $value;
}
/**
diff --git a/includes/class-wc-session-handler.php b/includes/class-wc-session-handler.php
index cacda9e59a2..0c6dda12484 100644
--- a/includes/class-wc-session-handler.php
+++ b/includes/class-wc-session-handler.php
@@ -75,7 +75,7 @@ class WC_Session_Handler extends WC_Session {
add_action( 'wp_logout', array( $this, 'destroy_session' ) );
if ( ! is_user_logged_in() ) {
- add_filter( 'nonce_user_logged_out', array( $this, 'nonce_user_logged_out' ) );
+ add_filter( 'nonce_user_logged_out', array( $this, 'maybe_update_nonce_user_logged_out' ), 10, 2 );
}
}
@@ -187,6 +187,25 @@ class WC_Session_Handler extends WC_Session {
return $customer_id;
}
+ /**
+ * Get session unique ID for requests if session is initialized or user ID if logged in.
+ * Introduced to help with unit tests.
+ *
+ * @since 5.3.0
+ * @return string
+ */
+ public function get_customer_unique_id() {
+ $customer_id = '';
+
+ if ( $this->has_session() && $this->_customer_id ) {
+ $customer_id = $this->_customer_id;
+ } elseif ( is_user_logged_in() ) {
+ $customer_id = (string) get_current_user_id();
+ }
+
+ return $customer_id;
+ }
+
/**
* Get the session cookie, if set. Otherwise return false.
*
@@ -288,13 +307,33 @@ class WC_Session_Handler extends WC_Session {
/**
* When a user is logged out, ensure they have a unique nonce by using the customer/session ID.
*
+ * @deprecated 5.3.0
* @param int $uid User ID.
- * @return string
+ * @return int|string
*/
public function nonce_user_logged_out( $uid ) {
+ wc_deprecated_function( 'WC_Session_Handler::nonce_user_logged_out', '5.3', 'WC_Session_Handler::maybe_update_nonce_user_logged_out' );
+
return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
}
+ /**
+ * When a user is logged out, ensure they have a unique nonce to manage cart and more using the customer/session ID.
+ * This filter runs everything `wp_verify_nonce()` and `wp_create_nonce()` gets called.
+ *
+ * @since 5.3.0
+ * @param int $uid User ID.
+ * @param string $action The nonce action.
+ * @return int|string
+ */
+ public function maybe_update_nonce_user_logged_out( $uid, $action ) {
+ if ( Automattic\WooCommerce\Utilities\StringUtil::starts_with( $action, 'woocommerce' ) ) {
+ return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
+ }
+
+ return $uid;
+ }
+
/**
* Cleanup session data from the database and clear caches.
*/
diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php
index 785df5eae48..19ba9b67ff5 100644
--- a/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php
+++ b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php
@@ -4,8 +4,6 @@
*
* Handles requests to the /taxes endpoint.
*
- * @author WooThemes
- * @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
@@ -40,67 +38,79 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
* Register the routes for taxes.
*/
public function register_routes() {
- register_rest_route( $this->namespace, '/' . $this->rest_base, array(
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_items' ),
- 'permission_callback' => array( $this, 'get_items_permissions_check' ),
- 'args' => $this->get_collection_params(),
- ),
- array(
- 'methods' => WP_REST_Server::CREATABLE,
- 'callback' => array( $this, 'create_item' ),
- 'permission_callback' => array( $this, 'create_item_permissions_check' ),
- 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
- ),
- 'schema' => array( $this, 'get_public_item_schema' ),
- ) );
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
+ 'args' => $this->get_collection_params(),
+ ),
+ array(
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => array( $this, 'create_item' ),
+ 'permission_callback' => array( $this, 'create_item_permissions_check' ),
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
- register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array(
- 'args' => array(
- 'id' => array(
- 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
- 'type' => 'integer',
- ),
- ),
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/(?P[\d]+)',
array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_item' ),
- 'permission_callback' => array( $this, 'get_item_permissions_check' ),
- 'args' => array(
- 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
- ),
- ),
- array(
- 'methods' => WP_REST_Server::EDITABLE,
- 'callback' => array( $this, 'update_item' ),
- 'permission_callback' => array( $this, 'update_item_permissions_check' ),
- 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
- ),
- array(
- 'methods' => WP_REST_Server::DELETABLE,
- 'callback' => array( $this, 'delete_item' ),
- 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
- 'args' => array(
- 'force' => array(
- 'default' => false,
- 'type' => 'boolean',
- 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
+ 'args' => array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
+ 'type' => 'integer',
),
),
- ),
- 'schema' => array( $this, 'get_public_item_schema' ),
- ) );
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_item' ),
+ 'permission_callback' => array( $this, 'get_item_permissions_check' ),
+ 'args' => array(
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $this, 'update_item' ),
+ 'permission_callback' => array( $this, 'update_item_permissions_check' ),
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::DELETABLE,
+ 'callback' => array( $this, 'delete_item' ),
+ 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+ 'args' => array(
+ 'force' => array(
+ 'default' => false,
+ 'type' => 'boolean',
+ 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
+ ),
+ ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
- register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/batch',
array(
- 'methods' => WP_REST_Server::EDITABLE,
- 'callback' => array( $this, 'batch_items' ),
- 'permission_callback' => array( $this, 'batch_items_permissions_check' ),
- 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
- ),
- 'schema' => array( $this, 'get_public_batch_schema' ),
- ) );
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $this, 'batch_items' ),
+ 'permission_callback' => array( $this, 'batch_items_permissions_check' ),
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
+ ),
+ 'schema' => array( $this, 'get_public_batch_schema' ),
+ )
+ );
}
/**
@@ -200,7 +210,7 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
public function get_items( $request ) {
global $wpdb;
- $prepared_args = array();
+ $prepared_args = array();
$prepared_args['order'] = $request['order'];
$prepared_args['number'] = $request['per_page'];
if ( ! empty( $request['offset'] ) ) {
@@ -208,9 +218,10 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
} else {
$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
}
- $orderby_possibles = array(
- 'id' => 'tax_rate_id',
- 'order' => 'tax_rate_order',
+ $orderby_possibles = array(
+ 'id' => 'tax_rate_id',
+ 'order' => 'tax_rate_order',
+ 'priority' => 'tax_rate_priority',
);
$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
$prepared_args['class'] = $request['class'];
@@ -223,30 +234,42 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
*/
$prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request );
- $query = "
+ $orderby = sanitize_key( $prepared_args['orderby'] ) . ' ' . sanitize_key( $prepared_args['order'] );
+ $query = "
SELECT *
FROM {$wpdb->prefix}woocommerce_tax_rates
- WHERE 1 = 1
+ %s
+ ORDER BY {$orderby}
+ LIMIT %%d, %%d
";
+ $wpdb_prepare_args = array(
+ $prepared_args['offset'],
+ $prepared_args['number'],
+ );
+
// Filter by tax class.
- if ( ! empty( $prepared_args['class'] ) ) {
+ if ( empty( $prepared_args['class'] ) ) {
+ $query = sprintf( $query, '' );
+ } else {
$class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : '';
- $query .= " AND tax_rate_class = '$class'";
+ array_unshift( $wpdb_prepare_args, $class );
+ $query = sprintf( $query, 'WHERE tax_rate_class = %s' );
}
- // Order tax rates.
- $order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) );
-
- // Pagination.
- $pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] );
-
// Query taxes.
- $results = $wpdb->get_results( $query . $order_by . $pagination );
+ // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
+ $results = $wpdb->get_results(
+ $wpdb->prepare(
+ $query,
+ $wpdb_prepare_args
+ )
+ );
+ // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
$taxes = array();
foreach ( $results as $tax ) {
- $data = $this->prepare_item_for_response( $tax, $request );
+ $data = $this->prepare_item_for_response( $tax, $request );
$taxes[] = $this->prepare_response_for_collection( $data );
}
@@ -254,10 +277,18 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
// Store pagination values for headers then unset for count query.
$per_page = (int) $prepared_args['number'];
- $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
+ $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
// Query only for ids.
- $wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) );
+ // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
+ $query = str_replace( 'SELECT *', 'SELECT tax_rate_id', $query );
+ $wpdb->get_results(
+ $wpdb->prepare(
+ $query,
+ $wpdb_prepare_args
+ )
+ );
+ // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
// Calculate totals.
$total_taxes = (int) $wpdb->num_rows;
@@ -287,13 +318,13 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
* Take tax data from the request and return the updated or newly created rate.
*
* @param WP_REST_Request $request Full details about the request.
- * @param stdClass|null $current Existing tax object.
+ * @param stdClass|null $current Existing tax object.
* @return object
*/
protected function create_or_update_tax( $request, $current = null ) {
- $id = absint( isset( $request['id'] ) ? $request['id'] : 0 );
- $data = array();
- $fields = array(
+ $id = absint( isset( $request['id'] ) ? $request['id'] : 0 );
+ $data = array();
+ $fields = array(
'tax_rate_country',
'tax_rate_state',
'tax_rate',
@@ -321,25 +352,25 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
// Add to data array.
switch ( $key ) {
- case 'tax_rate_priority' :
- case 'tax_rate_compound' :
- case 'tax_rate_shipping' :
- case 'tax_rate_order' :
+ case 'tax_rate_priority':
+ case 'tax_rate_compound':
+ case 'tax_rate_shipping':
+ case 'tax_rate_order':
$data[ $field ] = absint( $request[ $key ] );
break;
- case 'tax_rate_class' :
+ case 'tax_rate_class':
$data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : '';
break;
- default :
+ default:
$data[ $field ] = wc_clean( $request[ $key ] );
break;
}
}
- if ( $id ) {
- WC_Tax::_update_tax_rate( $id, $data );
- } else {
+ if ( ! $id ) {
$id = WC_Tax::_insert_tax_rate( $data );
+ } elseif ( $data ) {
+ WC_Tax::_update_tax_rate( $id, $data );
}
// Add locales.
@@ -538,7 +569,7 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
*/
protected function prepare_links( $tax ) {
$links = array(
- 'self' => array(
+ 'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ),
),
'collection' => array(
@@ -592,18 +623,18 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
'title' => 'tax',
'type' => 'object',
'properties' => array(
- 'id' => array(
+ 'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
- 'country' => array(
+ 'country' => array(
'description' => __( 'Country ISO 3166 code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
- 'state' => array(
+ 'state' => array(
'description' => __( 'State code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
@@ -613,17 +644,17 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
- 'city' => array(
+ 'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
- 'rate' => array(
+ 'rate' => array(
'description' => __( 'Tax rate.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
- 'name' => array(
+ 'name' => array(
'description' => __( 'Tax rate name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
@@ -646,12 +677,12 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
'default' => true,
'context' => array( 'view', 'edit' ),
),
- 'order' => array(
+ 'order' => array(
'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
- 'class' => array(
+ 'class' => array(
'description' => __( 'Tax class.', 'woocommerce' ),
'type' => 'string',
'default' => 'standard',
@@ -674,54 +705,55 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
$params['context'] = $this->get_context_param();
$params['context']['default'] = 'view';
- $params['page'] = array(
- 'description' => __( 'Current page of the collection.', 'woocommerce' ),
- 'type' => 'integer',
- 'default' => 1,
- 'sanitize_callback' => 'absint',
- 'validate_callback' => 'rest_validate_request_arg',
- 'minimum' => 1,
+ $params['page'] = array(
+ 'description' => __( 'Current page of the collection.', 'woocommerce' ),
+ 'type' => 'integer',
+ 'default' => 1,
+ 'sanitize_callback' => 'absint',
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'minimum' => 1,
);
$params['per_page'] = array(
- 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
- 'type' => 'integer',
- 'default' => 10,
- 'minimum' => 1,
- 'maximum' => 100,
- 'sanitize_callback' => 'absint',
- 'validate_callback' => 'rest_validate_request_arg',
+ 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
+ 'type' => 'integer',
+ 'default' => 10,
+ 'minimum' => 1,
+ 'maximum' => 100,
+ 'sanitize_callback' => 'absint',
+ 'validate_callback' => 'rest_validate_request_arg',
);
- $params['offset'] = array(
- 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
- 'type' => 'integer',
- 'sanitize_callback' => 'absint',
- 'validate_callback' => 'rest_validate_request_arg',
+ $params['offset'] = array(
+ 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
+ 'type' => 'integer',
+ 'sanitize_callback' => 'absint',
+ 'validate_callback' => 'rest_validate_request_arg',
);
- $params['order'] = array(
- 'default' => 'asc',
- 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
- 'enum' => array( 'asc', 'desc' ),
- 'sanitize_callback' => 'sanitize_key',
- 'type' => 'string',
- 'validate_callback' => 'rest_validate_request_arg',
+ $params['order'] = array(
+ 'default' => 'asc',
+ 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
+ 'enum' => array( 'asc', 'desc' ),
+ 'sanitize_callback' => 'sanitize_key',
+ 'type' => 'string',
+ 'validate_callback' => 'rest_validate_request_arg',
);
- $params['orderby'] = array(
- 'default' => 'order',
- 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
- 'enum' => array(
+ $params['orderby'] = array(
+ 'default' => 'order',
+ 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
+ 'enum' => array(
'id',
'order',
+ 'priority',
),
- 'sanitize_callback' => 'sanitize_key',
- 'type' => 'string',
- 'validate_callback' => 'rest_validate_request_arg',
+ 'sanitize_callback' => 'sanitize_key',
+ 'type' => 'string',
+ 'validate_callback' => 'rest_validate_request_arg',
);
- $params['class'] = array(
- 'description' => __( 'Sort by tax class.', 'woocommerce' ),
- 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ),
- 'sanitize_callback' => 'sanitize_title',
- 'type' => 'string',
- 'validate_callback' => 'rest_validate_request_arg',
+ $params['class'] = array(
+ 'description' => __( 'Sort by tax class.', 'woocommerce' ),
+ 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ),
+ 'sanitize_callback' => 'sanitize_title',
+ 'type' => 'string',
+ 'validate_callback' => 'rest_validate_request_arg',
);
return $params;
diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php
index 758686fbc44..6fef3703eb6 100644
--- a/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php
+++ b/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php
@@ -74,7 +74,7 @@ class WC_REST_Taxes_Controller extends WC_REST_Taxes_V2_Controller {
$schema = parent::get_item_schema();
$schema['properties']['postcodes'] = array(
- 'description' => __( 'List of postcodes / ZIPs.', 'woocommerce' ),
+ 'description' => __( 'List of postcodes / ZIPs. Introduced in WooCommerce 5.3.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
@@ -83,7 +83,7 @@ class WC_REST_Taxes_Controller extends WC_REST_Taxes_V2_Controller {
);
$schema['properties']['cities'] = array(
- 'description' => __( 'List of city names.', 'woocommerce' ),
+ 'description' => __( 'List of city names. Introduced in WooCommerce 5.3.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
@@ -91,6 +91,51 @@ class WC_REST_Taxes_Controller extends WC_REST_Taxes_V2_Controller {
'context' => array( 'view', 'edit' ),
);
+ $schema['properties']['postcode']['description'] =
+ __( "Postcode/ZIP, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'postcodes' should be used instead.", 'woocommerce' );
+
+ $schema['properties']['city']['description'] =
+ __( "City name, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'cities' should be used instead.", 'woocommerce' );
+
return $schema;
}
+
+ /**
+ * Create a single tax.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_Error|WP_REST_Response The response, or an error.
+ */
+ public function create_item( $request ) {
+ $this->adjust_cities_and_postcodes( $request );
+
+ return parent::create_item( $request );
+ }
+
+ /**
+ * Update a single tax.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_Error|WP_REST_Response The response, or an error.
+ */
+ public function update_item( $request ) {
+ $this->adjust_cities_and_postcodes( $request );
+
+ return parent::update_item( $request );
+ }
+
+ /**
+ * Convert array "cities" and "postcodes" parameters
+ * into semicolon-separated strings "city" and "postcode".
+ *
+ * @param WP_REST_Request $request The request to adjust.
+ */
+ private function adjust_cities_and_postcodes( &$request ) {
+ if ( isset( $request['cities'] ) ) {
+ $request['city'] = join( ';', $request['cities'] );
+ }
+ if ( isset( $request['postcodes'] ) ) {
+ $request['postcode'] = join( ';', $request['postcodes'] );
+ }
+ }
}
diff --git a/lib/composer.json b/lib/composer.json
index 7e9e4481f95..f3fa6563375 100644
--- a/lib/composer.json
+++ b/lib/composer.json
@@ -8,7 +8,7 @@
"psr/container": "^1.0"
},
"require-dev": {
- "league/container": "3.3.3"
+ "league/container": "3.3.5"
},
"config": {
"platform": {
diff --git a/lib/composer.lock b/lib/composer.lock
index ac31c819004..bff7bf66ac0 100644
--- a/lib/composer.lock
+++ b/lib/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "df548645b5c00d585705cd10c6ffd3f7",
+ "content-hash": "9ae561875707d59bc392f6329d4f565a",
"packages": [
{
"name": "psr/container",
@@ -59,21 +59,21 @@
"packages-dev": [
{
"name": "league/container",
- "version": "3.3.3",
+ "version": "3.3.5",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
- "reference": "7dc67bdf89efc338e674863c0ea70a63efe4de05"
+ "reference": "048ab87810f508dbedbcb7ae941b606eb8ee353b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/container/zipball/7dc67bdf89efc338e674863c0ea70a63efe4de05",
- "reference": "7dc67bdf89efc338e674863c0ea70a63efe4de05",
+ "url": "https://api.github.com/repos/thephpleague/container/zipball/048ab87810f508dbedbcb7ae941b606eb8ee353b",
+ "reference": "048ab87810f508dbedbcb7ae941b606eb8ee353b",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
- "psr/container": "^1.0"
+ "psr/container": "^1.0.0 || ^2.0.0"
},
"provide": {
"psr/container-implementation": "^1.0"
@@ -83,11 +83,14 @@
},
"require-dev": {
"phpunit/phpunit": "^6.0",
- "squizlabs/php_codesniffer": "^3.3"
+ "roave/security-advisories": "dev-master",
+ "scrutinizer/ocular": "^1.8",
+ "squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"branch-alias": {
+ "dev-master": "3.x-dev",
"dev-3.x": "3.x-dev",
"dev-2.x": "2.x-dev",
"dev-1.x": "1.x-dev"
@@ -127,7 +130,7 @@
"type": "github"
}
],
- "time": "2020-09-28T13:38:44+00:00"
+ "time": "2021-03-16T09:42:56+00:00"
}
],
"aliases": [],
diff --git a/tests/e2e/core-tests/CHANGELOG.md b/tests/e2e/core-tests/CHANGELOG.md
index e7fce31e1cf..da06a897a58 100644
--- a/tests/e2e/core-tests/CHANGELOG.md
+++ b/tests/e2e/core-tests/CHANGELOG.md
@@ -2,6 +2,10 @@
# 0.1.3
+## Added
+
+- Shopper My Account Create Account
+
## Fixed
- removed use of ES6 `import`
diff --git a/tests/e2e/core-tests/README.md b/tests/e2e/core-tests/README.md
index e9ddf7cdcb6..c1143db4bc2 100644
--- a/tests/e2e/core-tests/README.md
+++ b/tests/e2e/core-tests/README.md
@@ -78,6 +78,7 @@ The functions to access the core tests are:
- `runSingleProductPageTest` - Shopper can view single product page in many variations (simple, variable, grouped)
- `runVariableProductUpdateTest` - Shopper can view and update variations on a variable product
- `runCheckoutCreateAccountTest` - Shopper can create an account during checkout
+ - `runMyAccountCreateAccountTest` - Shopper can create an account via my account page
### REST API
diff --git a/tests/e2e/core-tests/specs/index.js b/tests/e2e/core-tests/specs/index.js
index 6689b3e4b78..bedc43e7d22 100644
--- a/tests/e2e/core-tests/specs/index.js
+++ b/tests/e2e/core-tests/specs/index.js
@@ -16,6 +16,7 @@ const runCheckoutApplyCouponsTest = require( './shopper/front-end-checkout-coupo
const runCheckoutPageTest = require( './shopper/front-end-checkout.test' );
const runMyAccountPageTest = require( './shopper/front-end-my-account.test' );
const runMyAccountPayOrderTest = require( './shopper/front-end-my-account-pay-order.test' );
+const runMyAccountCreateAccountTest = require( './shopper/front-end-my-account-create-account.test' );
const runSingleProductPageTest = require( './shopper/front-end-single-product.test' );
const runVariableProductUpdateTest = require( './shopper/front-end-variable-product-updates.test' );
const runCheckoutCreateAccountTest = require( './shopper/front-end-checkout-create-account.test' );
@@ -61,6 +62,7 @@ const runShopperTests = () => {
runCheckoutPageTest();
runMyAccountPageTest();
runMyAccountPayOrderTest();
+ runMyAccountCreateAccountTest();
runSingleProductPageTest();
runVariableProductUpdateTest();
runCheckoutCreateAccountTest();
@@ -136,4 +138,5 @@ module.exports = {
runAnalyticsPageLoadsTest,
runCheckoutCreateAccountTest,
runImportProductsTest,
+ runMyAccountCreateAccountTest,
};
diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-order-searching.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-order-searching.test.js
index ce392d06303..465d68583a7 100644
--- a/tests/e2e/core-tests/specs/merchant/wp-admin-order-searching.test.js
+++ b/tests/e2e/core-tests/specs/merchant/wp-admin-order-searching.test.js
@@ -6,7 +6,6 @@
const {
merchant,
clearAndFillInput,
- selectOptionInSelect2,
searchForOrder,
createSimpleProduct,
addProductToOrder,
@@ -20,40 +19,36 @@ const runOrderSearchingTest = () => {
await merchant.login();
await createSimpleProduct('Wanted Product');
- await Promise.all([
- // Create new order for testing
- await merchant.openNewOrder(),
- await page.waitForSelector('#order_status'),
- await page.click('#customer_user'),
- await page.click('span.select2-search > input.select2-search__field'),
- await page.type('span.select2-search > input.select2-search__field', 'Customer'),
- await page.waitFor(2000), // to avoid flakyness
- await page.keyboard.press('Enter'),
- ]);
+ // Create new order for testing
+ await merchant.openNewOrder();
+ await page.waitForSelector('#order_status');
+ await page.click('#customer_user');
+ await page.click('span.select2-search > input.select2-search__field');
+ await page.type('span.select2-search > input.select2-search__field', 'Customer');
+ await page.waitFor(2000); // to avoid flakyness
+ await page.keyboard.press('Enter');
- await Promise.all([
- // Change the shipping data
- await page.waitFor(1000), // to avoid flakiness
- await page.waitForSelector('#_shipping_first_name'),
- await clearAndFillInput('#_shipping_first_name', 'Tim'),
- await clearAndFillInput('#_shipping_last_name', 'Clark'),
- await clearAndFillInput('#_shipping_address_1', 'Oxford Ave'),
- await clearAndFillInput('#_shipping_address_2', 'Linwood Ave'),
- await clearAndFillInput('#_shipping_city', 'Buffalo'),
- await clearAndFillInput('#_shipping_postcode', '14201'),
- await page.keyboard.press('Tab'),
- await page.keyboard.press('Tab'),
- await page.keyboard.press('Enter'),
- await page.select('select[name="_shipping_state"]', 'NY'),
- ]);
+ // Change the shipping data
+ await page.waitFor(1000); // to avoid flakiness
+ await page.click('.billing-same-as-shipping');
+ await page.keyboard.press('Enter');
+ await page.waitForSelector('#_shipping_first_name');
+ await clearAndFillInput('#_shipping_first_name', 'Tim');
+ await clearAndFillInput('#_shipping_last_name', 'Clark');
+ await clearAndFillInput('#_shipping_address_1', 'Oxford Ave');
+ await clearAndFillInput('#_shipping_address_2', 'Linwood Ave');
+ await clearAndFillInput('#_shipping_city', 'Buffalo');
+ await clearAndFillInput('#_shipping_postcode', '14201');
// Get the post id
const variablePostId = await page.$('#post_ID');
orderId = (await(await variablePostId.getProperty('value')).jsonValue());
- // Save new order
+ // Save new order and add desired product to order
await clickUpdateOrder('Order updated.', true);
await addProductToOrder(orderId, 'Wanted Product');
+
+ // Open All Orders view
await merchant.openAllOrdersView();
});
@@ -126,7 +121,7 @@ const runOrderSearchingTest = () => {
})
it('can search for order by shipping state name', async () => {
- await searchForOrder('NY', orderId, 'John Doe');
+ await searchForOrder('CA', orderId, 'John Doe');
})
it('can search for order by item name', async () => {
diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-settings-shipping-zones.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-settings-shipping-zones.test.js
index 1a49f147158..e7da3ffe377 100644
--- a/tests/e2e/core-tests/specs/merchant/wp-admin-settings-shipping-zones.test.js
+++ b/tests/e2e/core-tests/specs/merchant/wp-admin-settings-shipping-zones.test.js
@@ -17,7 +17,7 @@ const {
const config = require( 'config' );
const simpleProductPrice = config.has( 'products.simple.price' ) ? config.get( 'products.simple.price' ) : '9.99';
const simpleProductName = config.get( 'products.simple.name' );
-const california = 'California, United States (US)';
+const california = 'state:US:CA';
const sanFranciscoZIP = '94107';
const shippingZoneNameUS = 'US with Flat rate';
const shippingZoneNameFL = 'CA with Free shipping';
@@ -90,10 +90,6 @@ const runAddNewShippingZoneTest = () => {
await selectOptionInSelect2('New York');
await expect(page).toClick('button[name="calc_shipping"]');
- // Set shipping postcode to 10010
- await clearAndFillInput('#calc_shipping_postcode', '10010');
- await expect(page).toClick('button[name="calc_shipping"]');
-
// Verify shipping costs
await page.waitForSelector('.order-total');
await expect(page).toMatchElement('.shipping .amount', {text: '$10.00'});
@@ -102,6 +98,7 @@ const runAddNewShippingZoneTest = () => {
it('allows customer to benefit from a Free shipping if in CA', async () => {
await page.reload();
+
// Set shipping state to California
await expect(page).toClick('a.shipping-calculator-button');
await expect(page).toClick('#select2-calc_shipping_state-container');
@@ -119,6 +116,7 @@ const runAddNewShippingZoneTest = () => {
it('allows customer to benefit from a free Local pickup if in SF', async () => {
await page.reload();
+
// Set shipping postcode to 94107
await expect(page).toClick('a.shipping-calculator-button');
await clearAndFillInput('#calc_shipping_postcode', '94107');
diff --git a/tests/e2e/core-tests/specs/shopper/front-end-my-account-create-account.test.js b/tests/e2e/core-tests/specs/shopper/front-end-my-account-create-account.test.js
new file mode 100644
index 00000000000..c48e9cd9e9f
--- /dev/null
+++ b/tests/e2e/core-tests/specs/shopper/front-end-my-account-create-account.test.js
@@ -0,0 +1,41 @@
+/* eslint-disable jest/no-export, jest/no-disabled-tests */
+/**
+ * Internal dependencies
+ */
+const {
+ shopper,
+ merchant,
+ setCheckbox,
+ settingsPageSaveChanges,
+} = require( '@woocommerce/e2e-utils' );
+
+const runMyAccountCreateAccountTest = () => {
+ describe('Shopper My Account Create Account', () => {
+ beforeAll(async () => {
+ await merchant.login();
+
+ // Set checkbox in the settings to enable registration in my account
+ await merchant.openSettings('account');
+ await setCheckbox('#woocommerce_enable_myaccount_registration');
+ await settingsPageSaveChanges();
+
+ await merchant.logout();
+ });
+
+ it('can create a new account via my account', async () => {
+ await shopper.gotoMyAccount();
+ await page.waitForSelector('.woocommerce-form-register');
+ await expect(page).toFill('input#reg_email', 'john.doe.test@example.com');
+ await expect(page).toClick('button[name="register"]');
+ await page.waitForNavigation({waitUntil: 'networkidle0'});
+ await expect(page).toMatchElement('h1', 'My account');
+
+ // Verify user has been created successfully
+ await merchant.login();
+ await merchant.openAllUsersView();
+ await expect(page).toMatchElement('td.email.column-email > a', {text: 'john.doe.test@example.com'});
+ });
+ });
+};
+
+module.exports = runMyAccountCreateAccountTest;
diff --git a/tests/e2e/specs/front-end/test-my-account-create-account.js b/tests/e2e/specs/front-end/test-my-account-create-account.js
new file mode 100644
index 00000000000..cfcbfdc780b
--- /dev/null
+++ b/tests/e2e/specs/front-end/test-my-account-create-account.js
@@ -0,0 +1,6 @@
+/*
+ * Internal dependencies
+ */
+const { runMyAccountCreateAccountTest } = require( '@woocommerce/e2e-core-tests' );
+
+runMyAccountCreateAccountTest();
diff --git a/tests/e2e/utils/src/components.js b/tests/e2e/utils/src/components.js
index cc6de0161fe..c449622937b 100644
--- a/tests/e2e/utils/src/components.js
+++ b/tests/e2e/utils/src/components.js
@@ -6,7 +6,14 @@
* Internal dependencies
*/
import { merchant } from './flows';
-import { clickTab, uiUnblocked, verifyCheckboxIsUnset, evalAndClick, selectOptionInSelect2, setCheckbox } from './page-utils';
+import {
+ clickTab,
+ uiUnblocked,
+ verifyCheckboxIsUnset,
+ selectOptionInSelect2,
+ setCheckbox,
+ unsetCheckbox
+} from './page-utils';
import factories from './factories';
const config = require( 'config' );
@@ -143,7 +150,8 @@ const completeOnboardingWizard = async () => {
await waitAndClickPrimary( false );
// Skip installing extensions
- await evalAndClick( '.components-checkbox-control__input' );
+ await unsetCheckbox( '.components-checkbox-control__input' );
+ await verifyCheckboxIsUnset( '.components-checkbox-control__input' );
await waitAndClickPrimary();
// Theme section
@@ -464,11 +472,11 @@ const createCoupon = async ( couponAmount = '5', discountType = 'Fixed cart disc
* Adds a shipping zone along with a shipping method.
*
* @param zoneName Shipping zone name.
- * @param zoneLocation Shiping zone location. Defaults to United States (US).
+ * @param zoneLocation Shiping zone location. Defaults to country:US. For states use: state:US:CA
* @param zipCode Shipping zone zip code. Defaults to empty one space.
* @param zoneMethod Shipping method type. Defaults to flat_rate (use also: free_shipping or local_pickup)
*/
-const addShippingZoneAndMethod = async ( zoneName, zoneLocation = 'United States (US)', zipCode = ' ', zoneMethod = 'flat_rate' ) => {
+const addShippingZoneAndMethod = async ( zoneName, zoneLocation = 'country:US', zipCode = ' ', zoneMethod = 'flat_rate' ) => {
await merchant.openNewShipping();
// Fill shipping zone name
@@ -476,12 +484,7 @@ const addShippingZoneAndMethod = async ( zoneName, zoneLocation = 'United States
await expect(page).toFill('input#zone_name', zoneName);
// Select shipping zone location
- // (.toSelect is not best option here because a lot of   are present in country/state names)
- await expect(page).toFill('#zone_locations', zoneLocation);
- await uiUnblocked();
- await page.keyboard.press('Tab');
- await uiUnblocked();
- await page.keyboard.press('Enter');
+ await expect(page).toSelect('select[name="zone_locations"]', zoneLocation);
// Fill shipping zone postcode if needed otherwise just put empty space
await page.waitForSelector('a.wc-shipping-zone-postcodes-toggle');
@@ -491,14 +494,12 @@ const addShippingZoneAndMethod = async ( zoneName, zoneLocation = 'United States
await expect(page).toClick('button#submit');
// Add shipping zone method
- await uiUnblocked();
+ await page.waitFor(1000);
await expect(page).toClick('button.wc-shipping-zone-add-method', {text:'Add shipping method'});
await page.waitForSelector('.wc-shipping-zone-method-selector');
await expect(page).toSelect('select[name="add_method_id"]', zoneMethod);
- await uiUnblocked();
await expect(page).toClick('button#btn-ok');
await page.waitForSelector('#zone_locations');
- await uiUnblocked();
};
/**
diff --git a/tests/legacy/framework/class-wc-rest-unit-test-case.php b/tests/legacy/framework/class-wc-rest-unit-test-case.php
index 5ad76c053e7..013ec0ace8d 100644
--- a/tests/legacy/framework/class-wc-rest-unit-test-case.php
+++ b/tests/legacy/framework/class-wc-rest-unit-test-case.php
@@ -4,11 +4,18 @@
*
* Provides REST API specific methods and setup/teardown.
*
+ * @package WooCommerce\Tests
* @since 3.0
*/
+/**
+ * Base class for REST related unit test classes.
+ */
class WC_REST_Unit_Test_Case extends WC_Unit_Test_Case {
+ /**
+ * @var WP_REST_Server
+ */
protected $server;
/**
@@ -36,4 +43,64 @@ class WC_REST_Unit_Test_Case extends WC_Unit_Test_Case {
unset( $this->server );
$wp_rest_server = null;
}
+
+ /**
+ * Perform a REST request.
+ *
+ * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'.
+ * @param string $verb HTTP verb for the request, default is GET.
+ * @param array|null $body_params Body parameters for the request, null if none are required.
+ * @param array|null $query_params Query string parameters for the request, null if none are required.
+ * @return array Result from the request.
+ */
+ public function do_rest_request( $url, $verb = 'GET', $body_params = null, $query_params = null ) {
+ if ( '/' !== $url[0] ) {
+ $url = '/wc/v3/' . $url;
+ }
+
+ $request = new WP_REST_Request( $verb, $url );
+ if ( ! is_null( $query_params ) ) {
+ $request->set_query_params( $query_params );
+ }
+ if ( ! is_null( $body_params ) ) {
+ $request->set_body_params( $body_params );
+ }
+
+ return $this->server->dispatch( $request );
+ }
+
+ /**
+ * Perform a GET REST request.
+ *
+ * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'.
+ * @param array|null $query_params Query string parameters for the request, null if none are required.
+ * @return WP_REST_Response The response for the request.
+ */
+ public function do_rest_get_request( $url, $query_params = null ) {
+ return $this->do_rest_request( $url, 'GET', null, $query_params );
+ }
+
+ /**
+ * Perform a POST REST request.
+ *
+ * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'.
+ * @param array|null $body_params Body parameters for the request, null if none are required.
+ * @param array|null $query_params Query string parameters for the request, null if none are required.
+ * @return array Result from the request.
+ */
+ public function do_rest_post_request( $url, $body_params = null, $query_params = null ) {
+ return $this->do_rest_request( $url, 'POST', $body_params, $query_params );
+ }
+
+ /**
+ * Perform a PUT REST request.
+ *
+ * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'.
+ * @param array|null $body_params Body parameters for the request, null if none are required.
+ * @param array|null $query_params Query string parameters for the request, null if none are required.
+ * @return array Result from the request.
+ */
+ public function do_rest_put_request( $url, $body_params = null, $query_params = null ) {
+ return $this->do_rest_request( $url, 'PUT', $body_params, $query_params );
+ }
}
diff --git a/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php b/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php
index 4149f90fbb6..7af97b30165 100644
--- a/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php
+++ b/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php
@@ -10,6 +10,9 @@
*/
class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
+ /**
+ * Setup.
+ */
public function setUp() {
parent::setUp();
@@ -17,6 +20,9 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->create_session();
}
+ /**
+ * @testdox Test that save data should insert new row.
+ */
public function test_save_data_should_insert_new_row() {
$current_session_data = $this->get_session_from_db( $this->session_key );
// delete session to make sure a new row is created in the DB.
@@ -35,6 +41,9 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->assertEquals( array( 'cart' => 'new cart' ), wp_cache_get( $this->cache_prefix . $this->session_key, WC_SESSION_CACHE_GROUP ) );
}
+ /**
+ * @testdox Test that save data should replace existing row.
+ */
public function test_save_data_should_replace_existing_row() {
$current_session_data = $this->get_session_from_db( $this->session_key );
@@ -49,23 +58,35 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->assertTrue( is_numeric( $updated_session_data->session_expiry ) );
}
+ /**
+ * @testdox Test that get_setting() should use cache.
+ */
public function test_get_session_should_use_cache() {
$session = $this->handler->get_session( $this->session_key );
$this->assertEquals( array( 'cart' => 'fake cart' ), $session );
}
+ /**
+ * @testdox Test that get_setting() shouldn't use cache.
+ */
public function test_get_session_should_not_use_cache() {
wp_cache_delete( $this->cache_prefix . $this->session_key, WC_SESSION_CACHE_GROUP );
$session = $this->handler->get_session( $this->session_key );
$this->assertEquals( array( 'cart' => 'fake cart' ), $session );
}
+ /**
+ * @testdox Test that get_setting() should return default value.
+ */
public function test_get_session_should_return_default_value() {
$default_session = array( 'session' => 'default' );
$session = $this->handler->get_session( 'non-existent key', $default_session );
$this->assertEquals( $default_session, $session );
}
+ /**
+ * @testdox Test delete_session().
+ */
public function test_delete_session() {
global $wpdb;
@@ -82,6 +103,9 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->assertNull( $session_id );
}
+ /**
+ * @testdox Test update_session_timestamp().
+ */
public function test_update_session_timestamp() {
global $wpdb;
@@ -98,6 +122,14 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->assertEquals( $timestamp, $session_expiry );
}
+ /**
+ * @testdox Test that nonce of user logged out is only changed by WooCommerce.
+ */
+ public function test_maybe_update_nonce_user_logged_out() {
+ $this->assertEquals( 1, $this->handler->maybe_update_nonce_user_logged_out( 1, 'wp_rest' ) );
+ $this->assertEquals( $this->handler->get_customer_unique_id(), $this->handler->maybe_update_nonce_user_logged_out( 1, 'woocommerce-something' ) );
+ }
+
/**
* Helper function to create a WC session and save it to the DB.
*/
@@ -113,7 +145,7 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
/**
* Helper function to get session data from DB.
*
- * @param string $session_key
+ * @param string $session_key Session key.
* @return stdClass
*/
protected function get_session_from_db( $session_key ) {
diff --git a/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller-tests.php b/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller-tests.php
new file mode 100644
index 00000000000..e991716cc93
--- /dev/null
+++ b/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller-tests.php
@@ -0,0 +1,288 @@
+user = $this->factory->user->create(
+ array(
+ 'role' => 'administrator',
+ )
+ );
+ }
+
+ /**
+ * Data provider for test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes.
+ *
+ * @return array
+ */
+ public function data_provider_for_test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes() {
+ return array(
+ array(
+ array(
+ 'city' => 'Osaka;Kyoto;Kobe',
+ 'postcode' => '5555;7777;8888',
+ ),
+ 'create',
+ ),
+ array(
+ array(
+ 'cities' => array(
+ 'Osaka',
+ 'Kyoto',
+ 'Kobe',
+ ),
+ 'postcodes' => array(
+ '5555',
+ '7777',
+ '8888',
+ ),
+ ),
+ 'create',
+ ),
+ array(
+ array(
+ 'city' => 'Osaka;Kyoto;Kobe',
+ 'postcode' => '5555;7777;8888',
+ ),
+ 'update',
+ ),
+ array(
+ array(
+ 'cities' => array(
+ 'Osaka',
+ 'Kyoto',
+ 'Kobe',
+ ),
+ 'postcodes' => array(
+ '5555',
+ '7777',
+ '8888',
+ ),
+ ),
+ 'update',
+ ),
+ );
+ }
+
+ /**
+ * @testdox It is possible to create or update a tax rate passing either "city"/"postcode" (strings) or "cities"/"postcodes" (arrays) fields.
+ *
+ * @dataProvider data_provider_for_test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes
+ *
+ * @param array $request_body The body for the REST request.
+ * @param string $action The action to perform, 'create' or 'update'.
+ */
+ public function test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes( $request_body, $action ) {
+ global $wpdb;
+
+ wp_set_current_user( $this->user );
+
+ if ( 'create' === $action ) {
+ $tax_rate_id = null;
+
+ $request_body = array_merge(
+ $request_body,
+ array(
+ 'country' => 'JP',
+ 'rate' => '1',
+ 'name' => 'Fake Tax',
+ )
+ );
+
+ $verb = 'POST';
+ $url = 'taxes';
+ $success_status = 201;
+ } else {
+ $tax_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'JP',
+ 'tax_rate' => '1',
+ 'tax_rate_name' => 'Fake Tax',
+ )
+ );
+
+ WC_Tax::_update_tax_rate_cities( $tax_rate_id, 'Tokyo' );
+ WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, '0000' );
+
+ $verb = 'PUT';
+ $url = 'taxes/' . $tax_rate_id;
+ $success_status = 200;
+ }
+
+ $response = $this->do_rest_request( $url, $verb, $request_body );
+ $this->assertEquals( $success_status, $response->get_status() );
+ if ( ! $tax_rate_id ) {
+ $tax_rate_id = $response->get_data()['id'];
+ }
+
+ $data = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT location_type, GROUP_CONCAT(location_code SEPARATOR ';') as items
+ FROM {$wpdb->prefix}woocommerce_tax_rate_locations
+ WHERE tax_rate_id=%d
+ GROUP BY location_type",
+ $tax_rate_id
+ ),
+ OBJECT_K
+ );
+
+ $this->assertEquals( 'OSAKA;KYOTO;KOBE', $data['city']->items );
+ $this->assertEquals( '5555;7777;8888', $data['postcode']->items );
+ }
+
+ /**
+ * @testdox The response for tax rate(s) includes the "city"/"postcode" (strings) and "cities"/"postcodes" (arrays) fields.
+ *
+ * @testWith [true]
+ * [false]
+ *
+ * @param bool $request_one True to request only one tax, false to request all the taxes.
+ */
+ public function test_get_tax_response_includes_cities_and_postcodes_as_arrays( $request_one ) {
+ wp_set_current_user( $this->user );
+
+ $tax_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'JP',
+ 'tax_rate' => '1',
+ 'tax_rate_name' => 'Fake Tax',
+ )
+ );
+
+ WC_Tax::_update_tax_rate_cities( $tax_id, 'Osaka;Kyoto;Kobe' );
+ WC_Tax::_update_tax_rate_postcodes( $tax_id, '5555;7777;8888' );
+
+ if ( $request_one ) {
+ $response = $this->do_rest_get_request( 'taxes/' . $tax_id );
+ } else {
+ $response = $this->do_rest_get_request( 'taxes' );
+ }
+
+ $this->assertEquals( 200, $response->get_status() );
+ $data = $response->get_data();
+ if ( ! $request_one ) {
+ $data = current( $data );
+ }
+
+ $this->assertEquals( 'KOBE', $data['city'] );
+ $this->assertEquals( '8888', $data['postcode'] );
+ $this->assertEquals( array( 'OSAKA', 'KYOTO', 'KOBE' ), $data['cities'] );
+ $this->assertEquals( array( '5555', '7777', '8888' ), $data['postcodes'] );
+ }
+
+ /**
+ * @testdox The response of a REST API request for taxes can be sorted by priority.
+ *
+ * @testWith ["asc"]
+ * ["desc"]
+ *
+ * @param string $order_type Sort type, 'asc' or 'desc'.
+ */
+ public function test_get_tax_response_can_be_sorted_by_priority( $order_type ) {
+ wp_set_current_user( $this->user );
+
+ $tax_id_1 = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'JP',
+ 'tax_rate' => '1',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_name' => 'Fake Tax 1',
+ )
+ );
+ $tax_id_3 = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'JP',
+ 'tax_rate' => '1',
+ 'tax_rate_priority' => 3,
+ 'tax_rate_name' => 'Fake Tax 3',
+ )
+ );
+ $tax_id_2 = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'JP',
+ 'tax_rate' => '1',
+ 'tax_rate_priority' => 2,
+ 'tax_rate_name' => 'Fake Tax 2',
+ )
+ );
+
+ $response = $this->do_rest_get_request(
+ 'taxes',
+ array(
+ 'orderby' => 'priority',
+ 'order' => $order_type,
+ )
+ );
+
+ $this->assertEquals( 200, $response->get_status() );
+ $data = array_values( $response->get_data() );
+ $ids = array_map(
+ function( $item ) {
+ return $item['id'];
+ },
+ $data
+ );
+
+ if ( 'asc' === $order_type ) {
+ $expected = array( $tax_id_1, $tax_id_2, $tax_id_3 );
+ } else {
+ $expected = array( $tax_id_3, $tax_id_2, $tax_id_1 );
+ }
+ $this->assertEquals( $expected, $ids );
+ }
+
+ /**
+ * @testdox Tax rates can be queries filtering by tax class.
+ *
+ * @testWith ["standard"]
+ * ["reduced-rate"]
+ * ["zero-rate"]
+ *
+ * @param string $class The tax class name to try getting the taxes for.
+ */
+ public function test_can_get_taxes_filtering_by_class( $class ) {
+ wp_set_current_user( $this->user );
+
+ $classes = array( 'standard', 'reduced-rate', 'zero-rate' );
+
+ $tax_ids_by_class = array();
+ foreach ( $classes as $class ) {
+ $tax_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'JP',
+ 'tax_rate' => '1',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_name' => 'Fake Tax',
+ 'tax_rate_class' => $class,
+ )
+ );
+ $tax_ids_by_class[ $class ] = $tax_id;
+ }
+
+ $response = $this->do_rest_get_request(
+ 'taxes',
+ array(
+ 'class' => $class,
+ )
+ );
+
+ $this->assertEquals( 200, $response->get_status() );
+ $data = array_values( $response->get_data() );
+ $ids = array_map(
+ function( $item ) {
+ return $item['id'];
+ },
+ $data
+ );
+
+ $this->assertEquals( array( $tax_ids_by_class[ $class ] ), $ids );
+ }
+}