diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-fee.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-fee.php
index d5f3945d028..701a3b8c309 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-fee.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-fee.php
@@ -35,8 +35,8 @@ if ( ! defined( 'ABSPATH' ) ) {
get_total(), array( 'currency' => $order->get_currency() ) );
- if ( $refunded = $order->get_total_refunded_for_item( $item_id, 'fee' ) ) {
- echo '-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '';
+ if ( $refunded = -1 * $order->get_total_refunded_for_item( $item_id, 'fee' ) ) {
+ echo '' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '';
}
?>
@@ -59,8 +59,8 @@ if ( ! defined( 'ABSPATH' ) ) {
$order->get_currency() ) ) : '–';
- if ( $refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'fee' ) ) {
- echo '-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '';
+ if ( $refunded = -1 * $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'fee' ) ) {
+ echo '' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '';
}
?>
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php
index ffe1fc2b5ca..a36cd82f9e2 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php
@@ -59,10 +59,10 @@ $row_class = apply_filters( 'woocommerce_admin_html_order_item_class', ! empt
× ' . esc_html( $item->get_quantity() );
- $refunded_qty = $order->get_qty_refunded_for_item( $item_id );
+ $refunded_qty = -1 * $order->get_qty_refunded_for_item( $item_id );
if ( $refunded_qty ) {
- echo '-' . esc_html( $refunded_qty * -1 ) . '';
+ echo '' . esc_html( $refunded_qty * -1 ) . '';
}
?>
@@ -108,10 +108,10 @@ $row_class = apply_filters( 'woocommerce_admin_html_order_item_class', ! empt
echo '' . sprintf( esc_html__( '%s discount', 'woocommerce' ), wc_price( wc_format_decimal( $item->get_subtotal() - $item->get_total(), '' ), array( 'currency' => $order->get_currency() ) ) ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
- $refunded = $order->get_total_refunded_for_item( $item_id );
+ $refunded = -1 * $order->get_total_refunded_for_item( $item_id );
if ( $refunded ) {
- echo '-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ echo '' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?>
@@ -156,10 +156,10 @@ $row_class = apply_filters( 'woocommerce_admin_html_order_item_class', ! empt
echo '–';
}
- $refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id );
+ $refunded = -1 * $order->get_tax_refunded_for_item( $item_id, $tax_item_id );
if ( $refunded ) {
- echo '-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ echo '' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?>
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php
index e972e204b18..ade41c05ef8 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php
@@ -64,9 +64,9 @@ if ( ! defined( 'ABSPATH' ) ) {
get_total(), array( 'currency' => $order->get_currency() ) ) );
- $refunded = $order->get_total_refunded_for_item( $item_id, 'shipping' );
+ $refunded = -1 * $order->get_total_refunded_for_item( $item_id, 'shipping' );
if ( $refunded ) {
- echo wp_kses_post( '-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '' );
+ echo wp_kses_post( '' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '' );
}
?>
@@ -89,9 +89,9 @@ if ( ! defined( 'ABSPATH' ) ) {
$order->get_currency() ) ) : '–' );
- $refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'shipping' );
+ $refunded = -1 * $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'shipping' );
if ( $refunded ) {
- echo wp_kses_post( '-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '' );
+ echo wp_kses_post( '' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '' );
}
?>
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index d6f07121b77..da64bb85fdd 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -165,6 +165,10 @@ class WC_Install {
'wc_update_560_create_refund_returns_page',
'wc_update_560_db_version',
),
+ '6.0.0' => array(
+ 'wc_update_600_migrate_rate_limit_options',
+ 'wc_update_600_db_version',
+ ),
);
/**
@@ -524,6 +528,7 @@ class WC_Install {
wp_clear_scheduled_hook( 'woocommerce_cleanup_logs' );
wp_clear_scheduled_hook( 'woocommerce_geoip_updater' );
wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' );
+ wp_clear_scheduled_hook( 'woocommerce_cleanup_rate_limits' );
$ve = get_option( 'gmt_offset' ) > 0 ? '-' : '+';
@@ -545,6 +550,7 @@ class WC_Install {
wp_schedule_event( time() + ( 6 * HOUR_IN_SECONDS ), 'twicedaily', 'woocommerce_cleanup_sessions' );
wp_schedule_event( time() + MINUTE_IN_SECONDS, 'fifteendays', 'woocommerce_geoip_updater' );
wp_schedule_event( time() + 10, apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' );
+ wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_rate_limits' );
}
/**
@@ -1039,6 +1045,13 @@ CREATE TABLE {$wpdb->prefix}wc_reserved_stock (
`timestamp` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`expires` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`order_id`, `product_id`)
+) $collate;
+CREATE TABLE {$wpdb->prefix}woocommerce_rate_limits (
+ rate_limit_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ rate_limit_key varchar(200) NOT NULL,
+ rate_limit_expiry BIGINT UNSIGNED NOT NULL,
+ PRIMARY KEY (rate_limit_id),
+ UNIQUE KEY rate_limit_key (rate_limit_key($max_index_length))
) $collate;
";
@@ -1074,6 +1087,7 @@ CREATE TABLE {$wpdb->prefix}wc_reserved_stock (
"{$wpdb->prefix}woocommerce_tax_rate_locations",
"{$wpdb->prefix}woocommerce_tax_rates",
"{$wpdb->prefix}wc_reserved_stock",
+ "{$wpdb->prefix}woocommerce_rate_limits",
);
/**
diff --git a/plugins/woocommerce/includes/class-wc-rate-limiter.php b/plugins/woocommerce/includes/class-wc-rate-limiter.php
index aba4fb01489..a47f2f21fe9 100644
--- a/plugins/woocommerce/includes/class-wc-rate-limiter.php
+++ b/plugins/woocommerce/includes/class-wc-rate-limiter.php
@@ -32,13 +32,57 @@ defined( 'ABSPATH' ) || exit;
class WC_Rate_Limiter {
/**
- * Constructs Option name from action identifier.
+ * Cache group.
+ */
+ const CACHE_GROUP = 'wc_rate_limit';
+
+ /**
+ * Hook in methods.
+ */
+ public static function init() {
+ add_action( 'woocommerce_cleanup_rate_limits', array( __CLASS__, 'cleanup' ) );
+ }
+
+ /**
+ * Constructs key name from action identifier.
+ * Left in for backwards compatibility.
*
* @param string $action_id Identifier of the action.
* @return string
*/
public static function storage_id( $action_id ) {
- return 'woocommerce_rate_limit_' . $action_id;
+ return $action_id;
+ }
+
+ /**
+ * Gets a cache prefix.
+ *
+ * @param string $action_id Identifier of the action.
+ * @return string
+ */
+ protected static function get_cache_key( $action_id ) {
+ return WC_Cache_Helper::get_cache_prefix( 'rate_limit' . $action_id );
+ }
+
+ /**
+ * Retrieve a cached rate limit.
+ *
+ * @param string $action_id Identifier of the action.
+ * @return bool|int
+ */
+ protected static function get_cached( $action_id ) {
+ return wp_cache_get( self::get_cache_key( $action_id ), self::CACHE_GROUP );
+ }
+
+ /**
+ * Cache a rate limit.
+ *
+ * @param string $action_id Identifier of the action.
+ * @param int $expiry Timestamp when the limit expires.
+ * @return bool
+ */
+ protected static function set_cache( $action_id, $expiry ) {
+ return wp_cache_set( self::get_cache_key( $action_id ), $expiry, self::CACHE_GROUP );
}
/**
@@ -48,10 +92,27 @@ class WC_Rate_Limiter {
* @return bool
*/
public static function retried_too_soon( $action_id ) {
- $next_try_allowed_at = get_option( self::storage_id( $action_id ) );
+ global $wpdb;
+
+ $next_try_allowed_at = self::get_cached( $action_id );
+
+ if ( false === $next_try_allowed_at ) {
+ $next_try_allowed_at = $wpdb->get_var(
+ $wpdb->prepare(
+ "
+ SELECT rate_limit_expiry
+ FROM {$wpdb->prefix}woocommerce_rate_limits
+ WHERE rate_limit_key = %s
+ ",
+ $action_id
+ )
+ );
+
+ self::set_cache( $action_id, $next_try_allowed_at );
+ }
// No record of action running, so action is allowed to run.
- if ( false === $next_try_allowed_at ) {
+ if ( null === $next_try_allowed_at ) {
return false;
}
@@ -72,8 +133,41 @@ class WC_Rate_Limiter {
* @return bool True if the option setting was successful, false otherwise.
*/
public static function set_rate_limit( $action_id, $delay ) {
- $option_name = self::storage_id( $action_id );
+ global $wpdb;
+
$next_try_allowed_at = time() + $delay;
- return update_option( $option_name, $next_try_allowed_at );
+
+ $result = $wpdb->replace(
+ $wpdb->prefix . 'woocommerce_rate_limits',
+ array(
+ 'rate_limit_key' => $action_id,
+ 'rate_limit_expiry' => $next_try_allowed_at,
+ ),
+ array( '%s', '%d' )
+ );
+
+ self::set_cache( $action_id, $next_try_allowed_at );
+
+ return false !== $result;
+ }
+
+ /**
+ * Cleanup expired rate limits from the database and clear caches.
+ */
+ public static function cleanup() {
+ global $wpdb;
+
+ $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM {$wpdb->prefix}woocommerce_rate_limits WHERE rate_limit_expiry < %d",
+ time()
+ )
+ );
+
+ if ( class_exists( 'WC_Cache_Helper' ) ) {
+ WC_Cache_Helper::invalidate_cache_group( self::CACHE_GROUP );
+ }
}
}
+
+WC_Rate_Limiter::init();
diff --git a/plugins/woocommerce/includes/class-wc-shipping.php b/plugins/woocommerce/includes/class-wc-shipping.php
index d9157983a44..f4f9eba55f4 100644
--- a/plugins/woocommerce/includes/class-wc-shipping.php
+++ b/plugins/woocommerce/includes/class-wc-shipping.php
@@ -358,7 +358,13 @@ class WC_Shipping {
}
}
- // Filter the calculated rates.
+ /**
+ * Filter the calculated shipping rates.
+ *
+ * @see https://gist.github.com/woogists/271654709e1d27648546e83253c1a813 for cache invalidation methods.
+ * @param array $package['rates'] Package rates.
+ * @param array $package Package of cart items.
+ */
$package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package );
// Store in session to avoid recalculation.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
index 00966cedc30..47bb55b7a9e 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
@@ -207,10 +207,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
$args['post_type'] = $this->post_type;
}
- $orderby = $request->get_param( 'orderby' );
- $order = $request->get_param( 'order' );
-
- $ordering_args = WC()->query->get_catalog_ordering_args( $orderby, $order );
+ $ordering_args = WC()->query->get_catalog_ordering_args( $args['orderby'], $args['order'] );
$args['orderby'] = $ordering_args['orderby'];
$args['order'] = $ordering_args['order'];
if ( $ordering_args['meta_key'] ) {
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index 653b5a49010..a845efd3a23 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -2297,3 +2297,41 @@ function wc_update_560_create_refund_returns_page() {
function wc_update_560_db_version() {
WC_Install::update_db_version( '5.6.0' );
}
+
+/**
+ * Migrate rate limit options to the new table.
+ *
+ * See @link https://github.com/woocommerce/woocommerce/issues/27103.
+ */
+function wc_update_600_migrate_rate_limit_options() {
+ global $wpdb;
+
+ $rate_limits = $wpdb->get_results(
+ "
+ SELECT option_name, option_value
+ FROM $wpdb->options
+ WHERE option_name LIKE 'woocommerce_rate_limit_add_payment_method_%'
+ ",
+ ARRAY_A
+ );
+ $prefix_length = strlen( 'woocommerce_rate_limit_' );
+
+ foreach ( $rate_limits as $rate_limit ) {
+ $new_delay = (int) $rate_limit['option_value'] - time();
+
+ // Migrate the limit if it hasn't expired yet.
+ if ( 0 < $new_delay ) {
+ $action_id = substr( $rate_limit['option_name'], $prefix_length );
+ WC_Rate_Limiter::set_rate_limit( $action_id, $new_delay );
+ }
+
+ delete_option( $rate_limit['option_name'] );
+ }
+}
+
+/**
+ * Update DB version to 6.0.0.
+ */
+function wc_update_600_db_version() {
+ WC_Install::update_db_version( '6.0.0' );
+}
diff --git a/plugins/woocommerce/tests/e2e/api-core-tests/CHANGELOG.md b/plugins/woocommerce/tests/e2e/api-core-tests/CHANGELOG.md
index 0db666cd1f5..f9c672179e7 100644
--- a/plugins/woocommerce/tests/e2e/api-core-tests/CHANGELOG.md
+++ b/plugins/woocommerce/tests/e2e/api-core-tests/CHANGELOG.md
@@ -5,4 +5,4 @@
## Added
- Coupons API Tests
- Refunds API Tests
-- Products API Tests
\ No newline at end of file
+- Products API Tests
diff --git a/plugins/woocommerce/tests/e2e/api-core-tests/data/index.js b/plugins/woocommerce/tests/e2e/api-core-tests/data/index.js
index b944044d8dc..2bfd95b1b2e 100644
--- a/plugins/woocommerce/tests/e2e/api-core-tests/data/index.js
+++ b/plugins/woocommerce/tests/e2e/api-core-tests/data/index.js
@@ -1,5 +1,6 @@
const { order, getOrderExample } = require('./order');
const { coupon } = require('./coupon');
+const { refund } = require('./refund');
const shared = require('./shared');
module.exports = {
@@ -7,4 +8,5 @@ module.exports = {
getOrderExample,
coupon,
shared,
+ refund,
};
diff --git a/plugins/woocommerce/tests/e2e/api-core-tests/data/refund.js b/plugins/woocommerce/tests/e2e/api-core-tests/data/refund.js
new file mode 100644
index 00000000000..13a35f0cb58
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e/api-core-tests/data/refund.js
@@ -0,0 +1,18 @@
+/**
+ * A basic refund.
+ *
+ * For more details on the order refund properties, see:
+ *
+ * https://woocommerce.github.io/woocommerce-rest-api-docs/#order-refund-properties
+ *
+ */
+const refund = {
+ api_refund: false,
+ amount: '1.00',
+ reason: 'Late delivery refund.',
+ line_items: [],
+};
+
+module.exports = {
+ refund: refund,
+};
diff --git a/plugins/woocommerce/tests/e2e/api-core-tests/endpoints/index.js b/plugins/woocommerce/tests/e2e/api-core-tests/endpoints/index.js
index 1b36341726a..43953654d8c 100644
--- a/plugins/woocommerce/tests/e2e/api-core-tests/endpoints/index.js
+++ b/plugins/woocommerce/tests/e2e/api-core-tests/endpoints/index.js
@@ -1,9 +1,11 @@
const { ordersApi } = require('./orders');
const { couponsApi } = require('./coupons');
const { productsApi } = require('./products');
+const { refundsApi } = require('./refunds');
module.exports = {
ordersApi,
couponsApi,
productsApi,
+ refundsApi,
};
diff --git a/plugins/woocommerce/tests/e2e/api-core-tests/endpoints/refunds.js b/plugins/woocommerce/tests/e2e/api-core-tests/endpoints/refunds.js
new file mode 100644
index 00000000000..8a958fee3d0
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e/api-core-tests/endpoints/refunds.js
@@ -0,0 +1,60 @@
+/**
+ * Internal dependencies
+ */
+const {
+ getRequest,
+ postRequest,
+ putRequest,
+ deleteRequest,
+} = require( '../utils/request' );
+
+/**
+ * WooCommerce Refunds endpoints.
+ *
+ * https://woocommerce.github.io/woocommerce-rest-api-docs/#refunds
+ */
+const refundsApi = {
+ name: 'Refunds',
+ create: {
+ name: 'Create a refund',
+ method: 'POST',
+ path: 'orders//refunds',
+ responseCode: 201,
+ refund: async ( orderId, refundDetails ) =>
+ postRequest( `orders/${ orderId }/refunds`, refundDetails ),
+ },
+ retrieve: {
+ name: 'Retrieve a refund',
+ method: 'GET',
+ path: 'orders//refunds/',
+ responseCode: 200,
+ refund: async ( orderId, refundId ) =>
+ getRequest( `orders/${ orderId }/refunds/${ refundId }` ),
+ },
+ listAll: {
+ name: 'List all refunds',
+ method: 'GET',
+ path: 'orders//refunds',
+ responseCode: 200,
+ refunds: async ( orderId ) =>
+ getRequest( `orders/${ orderId }/refunds` ),
+ },
+ delete: {
+ name: 'Delete a refund',
+ method: 'DELETE',
+ path: 'orders//refunds/',
+ responseCode: 200,
+ payload: {
+ force: false,
+ },
+ refund: async ( orderId, refundId, deletePermanently ) =>
+ deleteRequest(
+ `orders/${ orderId }/refunds/${ refundId }`,
+ deletePermanently
+ ),
+ },
+};
+
+module.exports = {
+ refundsApi: refundsApi,
+};
diff --git a/plugins/woocommerce/tests/e2e/api-core-tests/tests/products/products.test.js b/plugins/woocommerce/tests/e2e/api-core-tests/tests/products/products.test.js
index 1ba5d8594d9..b111d55391b 100644
--- a/plugins/woocommerce/tests/e2e/api-core-tests/tests/products/products.test.js
+++ b/plugins/woocommerce/tests/e2e/api-core-tests/tests/products/products.test.js
@@ -569,31 +569,35 @@ const { productsApi } = require('../../endpoints/products');
} );
} );
- // This case will remain skipped until orderby slug is fixed.
- // See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099.
- it.skip( 'slug', async () => {
+ it( 'slug', async () => {
+ const productNamesBySlugAsc = [
+ 'Polo', // The Polo isn't published so it has an empty slug.
+ ...productNamesAsc.filter( p => p !== 'Polo' ),
+ ];
+ const productNamesBySlugDesc = [ ...productNamesBySlugAsc ].reverse();
+
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'slug',
- per_page: productNamesAsc.length,
+ per_page: productNamesBySlugAsc.length,
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
result1.body.forEach( ( { name }, idx ) => {
- expect( name ).toBe( productNamesAsc[ idx ] );
+ expect( name ).toBe( productNamesBySlugAsc[ idx ] );
} );
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'slug',
- per_page: productNamesDesc.length,
+ per_page: productNamesBySlugDesc.length,
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
result2.body.forEach( ( { name }, idx ) => {
- expect( name ).toBe( productNamesDesc[ idx ] );
+ expect( name ).toBe( productNamesBySlugDesc[ idx ] );
} );
} );
@@ -676,7 +680,7 @@ const { productsApi } = require('../../endpoints/products');
// This case will remain skipped until orderby include is fixed.
// See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099.
- it.skip( 'include', async () => {
+ it( 'include', async () => {
const includeIds = [
sampleData.groupedProducts[ 0 ].id,
sampleData.simpleProducts[ 3 ].id,
diff --git a/plugins/woocommerce/tests/e2e/api-core-tests/tests/refunds/refunds.test.js b/plugins/woocommerce/tests/e2e/api-core-tests/tests/refunds/refunds.test.js
new file mode 100644
index 00000000000..b39bbb34ff4
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e/api-core-tests/tests/refunds/refunds.test.js
@@ -0,0 +1,122 @@
+const { refundsApi } = require( '../../endpoints/refunds' );
+const { ordersApi } = require( '../../endpoints/orders' );
+const { productsApi } = require( '../../endpoints/products' );
+const { refund } = require( '../../data' );
+
+/**
+ * Tests for the WooCommerce Refunds API.
+ *
+ * @group api
+ * @group refunds
+ *
+ */
+describe( 'Refunds API tests', () => {
+ let expectedRefund;
+ let orderId;
+ let productId;
+
+ beforeAll( async () => {
+ // Create a product and save its product ID
+ const product = {
+ name: 'Simple Product for Refunds API tests',
+ regular_price: '100',
+ };
+ const createProductResponse = await productsApi.create.product(
+ product
+ );
+ productId = createProductResponse.body.id;
+
+ // Create an order with a product line item, and save its Order ID
+ const order = {
+ status: 'pending',
+ line_items: [
+ {
+ product_id: productId,
+ },
+ ],
+ };
+ const createOrderResponse = await ordersApi.create.order( order );
+ orderId = createOrderResponse.body.id;
+
+ // Setup the expected refund object
+ expectedRefund = {
+ ...refund,
+ line_items: [
+ {
+ product_id: productId,
+ },
+ ],
+ };
+ } );
+
+ afterAll( async () => {
+ // Cleanup the created product and order
+ await productsApi.delete.product( productId, true );
+ await ordersApi.delete.order( orderId, true );
+ } );
+
+ it( 'can create a refund', async () => {
+ const { status, body } = await refundsApi.create.refund(
+ orderId,
+ expectedRefund
+ );
+ expect( status ).toEqual( refundsApi.create.responseCode );
+ expect( body.id ).toBeDefined();
+
+ // Save the refund ID
+ expectedRefund.id = body.id;
+
+ // Verify that the order was refunded.
+ const getOrderResponse = await ordersApi.retrieve.order( orderId );
+ expect( getOrderResponse.body.refunds ).toHaveLength( 1 );
+ expect( getOrderResponse.body.refunds[ 0 ].id ).toEqual(
+ expectedRefund.id
+ );
+ expect( getOrderResponse.body.refunds[ 0 ].reason ).toEqual(
+ expectedRefund.reason
+ );
+ expect( getOrderResponse.body.refunds[ 0 ].total ).toEqual(
+ `-${ expectedRefund.amount }`
+ );
+ } );
+
+ it( 'can retrieve a refund', async () => {
+ const { status, body } = await refundsApi.retrieve.refund(
+ orderId,
+ expectedRefund.id
+ );
+
+ expect( status ).toEqual( refundsApi.retrieve.responseCode );
+ expect( body.id ).toEqual( expectedRefund.id );
+ } );
+
+ it( 'can list all refunds', async () => {
+ const { status, body } = await refundsApi.listAll.refunds( orderId );
+
+ expect( status ).toEqual( refundsApi.listAll.responseCode );
+ expect( body ).toHaveLength( 1 );
+ expect( body[ 0 ].id ).toEqual( expectedRefund.id );
+ } );
+
+ it( 'can delete a refund', async () => {
+ const { status, body } = await refundsApi.delete.refund(
+ orderId,
+ expectedRefund.id,
+ true
+ );
+
+ expect( status ).toEqual( refundsApi.delete.responseCode );
+ expect( body.id ).toEqual( expectedRefund.id );
+
+ // Verify that the refund cannot be retrieved
+ const retrieveRefundResponse = await refundsApi.retrieve.refund(
+ orderId,
+ expectedRefund.id
+ );
+ expect( retrieveRefundResponse.status ).toEqual( 404 );
+
+ // Verify that the order no longer has a refund
+ const retrieveOrderResponse = await ordersApi.retrieve.order( orderId );
+ expect( retrieveOrderResponse.body.refunds ).toHaveLength( 0 );
+ } );
+} );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/util/class-wc-rate-limiter.php b/plugins/woocommerce/tests/legacy/unit-tests/util/class-wc-rate-limiter.php
index 7ec2562d042..2ac073511f0 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/util/class-wc-rate-limiter.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/util/class-wc-rate-limiter.php
@@ -37,16 +37,69 @@ class WC_Tests_Rate_Limiter extends WC_Unit_Test_Case {
$rate_limit_id_1 = $action_identifier . $user_1_id;
$rate_limit_id_2 = $action_identifier . $user_2_id;
- WC_Rate_Limiter::set_rate_limit( $rate_limit_id_1, 1 );
+ WC_Rate_Limiter::set_rate_limit( $rate_limit_id_1, 0 );
$this->assertEquals( true, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon allowed action to run too soon before the delay.' );
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user before the delay.' );
// As retired_too_soon bails if current time <= limit, the actual time needs to be at least 1 second after the limit.
- sleep( 2 );
+ sleep( 1 );
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon did not allow action to run after the designated delay.' );
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user after the designated delay.' );
}
+ /**
+ * Test setting the limit and running rate limited actions when missing cache.
+ */
+ public function test_rate_limit_limits_without_cache() {
+ $action_identifier = 'action_1';
+ $user_1_id = 10;
+ $user_2_id = 15;
+
+ $rate_limit_id_1 = $action_identifier . $user_1_id;
+ $rate_limit_id_2 = $action_identifier . $user_2_id;
+
+ WC_Rate_Limiter::set_rate_limit( $rate_limit_id_1, 0 );
+ // Clear cached value for user 1.
+ wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'rate_limit' . $rate_limit_id_1 ), WC_Rate_Limiter::CACHE_GROUP );
+
+ $this->assertEquals( true, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon allowed action to run too soon before the delay.' );
+ $this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user before the delay.' );
+
+ // Clear cached values for both users.
+ wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'rate_limit' . $rate_limit_id_1 ), WC_Rate_Limiter::CACHE_GROUP );
+ wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'rate_limit' . $rate_limit_id_2 ), WC_Rate_Limiter::CACHE_GROUP );
+
+ // As retired_too_soon bails if current time <= limit, the actual time needs to be at least 1 second after the limit.
+ sleep( 1 );
+
+ $this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon did not allow action to run after the designated delay.' );
+ $this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user after the designated delay.' );
+ }
+
+ /**
+ * Test that rate limit option migration only processes unexpired limits.
+ */
+ public function test_rate_limit_option_migration() {
+ global $wpdb;
+
+ // Add some options to be migrated.
+ add_option( 'woocommerce_rate_limit_add_payment_method_123', time() + 1000 );
+ add_option( 'woocommerce_rate_limit_add_payment_method_234', time() - 1 );
+
+ // Run the migration function.
+ include_once WC_Unit_Tests_Bootstrap::instance()->plugin_dir . '/includes/wc-update-functions.php';
+ wc_update_600_migrate_rate_limit_options();
+
+ // Ensure that only the _123 limit was migrated.
+ $migrated = $wpdb->get_col( "SELECT rate_limit_key FROM {$wpdb->prefix}woocommerce_rate_limits" );
+
+ $this->assertCount( 1, $migrated );
+ $this->assertEquals( 'add_payment_method_123', $migrated[0] );
+
+ // Verify that all rate limit options were deleted.
+ $this->assertFalse( get_option( 'woocommerce_rate_limit_add_payment_method_123' ) );
+ $this->assertFalse( get_option( 'woocommerce_rate_limit_add_payment_method_234' ) );
+ }
}
diff --git a/plugins/woocommerce/uninstall.php b/plugins/woocommerce/uninstall.php
index 8a764b801a5..613dc9fd924 100644
--- a/plugins/woocommerce/uninstall.php
+++ b/plugins/woocommerce/uninstall.php
@@ -19,6 +19,7 @@ wp_clear_scheduled_hook( 'woocommerce_cleanup_personal_data' );
wp_clear_scheduled_hook( 'woocommerce_cleanup_logs' );
wp_clear_scheduled_hook( 'woocommerce_geoip_updater' );
wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' );
+wp_clear_scheduled_hook( 'woocommerce_cleanup_rate_limits' );
/*
* Only remove ALL product and page data if WC_REMOVE_ALL_DATA constant is set to true in user's