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